Compare commits

...

42 commits

Author SHA1 Message Date
coderaiser
7f28803a82 chore: cloudcmd: actions: lint ☘️ 2026-07-16 15:31:22 +00:00
coderaiser
943bdecd3e test: coverage 2026-07-16 15:29:08 +00:00
coderaiser
e4ff4a78cc chore: cloudcmd: actions: lint ☘️ 2026-07-16 12:27:35 +00:00
coderaiser
fec5538f01 test: common: util: coverage 2026-07-16 12:26:33 +00:00
coderaiser
b7bb363aa8 test: cloudfunc: coverage 2026-07-16 12:23:46 +00:00
coderaiser
cd4a2cec76 chore: cloudcmd: actions: lint ☘️ 2026-07-16 09:28:42 +00:00
coderaiser
0638ca32cb feature: migrate from webpack to rspack 2026-07-16 12:27:20 +03:00
coderaiser
fe185cc93e chore: cloudcmd: actions: lint ☘️ 2026-07-15 20:27:25 +00:00
coderiaser
7edea50296 chore: lint 2026-07-15 23:26:28 +03:00
coderaiser
8258cdb651
chore: rm rulesdir 2026-07-15 21:18:06 +02:00
coderaiser
a9e29eee85 test: cloudcmd: coverage 2026-07-15 10:31:07 +00:00
coderaiser
a52a6aae62 test: coverage 2026-07-15 09:36:03 +00:00
coderaiser
92c79830f5 chore: madrun: test: rm test-e2e 2026-07-15 08:21:03 +00:00
coderiaser
8629f67972 feature: cloudcmd: docker: io: arm 2026-06-29 18:19:08 +03:00
coderiaser
c04d363c46 test: e2e: spawn 2026-06-29 18:16:34 +03:00
coderiaser
046a4ef22e chore: lint 2026-06-29 17:54:20 +03:00
coderiaser
579c9e41d7 feature: cloudcmd: serve-once v4.0.1 2026-06-29 17:48:06 +03:00
coderiaser
6014898610 feature: cloudcmd: @babel/preset-env v8.0.2 2026-06-29 17:48:06 +03:00
coderiaser
ad3bf95e5f feature: cloudcmd: @babel/plugin-transform-optional-chaining v8.0.1 2026-06-29 17:48:05 +03:00
coderiaser
4e22a4edde feature: cloudcmd: @babel/core v8.0.1 2026-06-29 17:48:04 +03:00
coderiaser
9c50fe53e7 feature: cloudcmd: @babel/code-frame v8.0.0 2026-06-29 17:48:04 +03:00
coderiaser
a2a2d51ec1 chore: cloudcmd: v19.19.1 2026-06-15 14:50:37 +03:00
coderiaser
c36c32cb08 chore: lint 2026-06-15 14:49:57 +03:00
coderiaser
78b206e06b feature: client: key: Ctrl + L -> Ctrl + Shift + L: Log out (#466) 2026-06-15 14:49:47 +03:00
coderiaser
ef83a61a0c chore: actions: typos 2026-06-04 17:55:58 +03:00
coderaiser
f5c0aa5c4c Update nodejs.yml 2026-06-04 17:55:44 +03:00
coderaiser
e59167b1fa chore: actions: permissions 2026-06-04 16:05:15 +03:00
coderiaser
ea584aceea chore: actions: typos.ai 2026-06-04 15:42:02 +03:00
coderiaser
4d7116b467 chore: docker: io: git 2026-06-03 23:47:50 +03:00
coderiaser
b149288545 feature: docker: io: NPM_CONFIG_PACKAGE_LOCK=false 2026-06-03 23:40:16 +03:00
coderiaser
9d06279ba8 featur: docker: io: add gh 2026-06-03 23:39:27 +03:00
coderiaser
21e1dcf0e5 chore: actions: typos 2026-06-03 18:46:19 +03:00
coderaiser
b3b0c1d159 chore: package: rm unused optimize-css-assets-webpack-plugin 2026-06-03 13:02:04 +00:00
coderaiser
76d7bf4d73
feature: docker: typos, shellcheck: add 2026-06-02 10:03:23 +02:00
coderaiser
f1d80282eb chore: cloudcmd: actions: lint ☘️ 2026-06-01 20:39:12 +00:00
coderiaser
220cc5557b chore: actions: API_KEY 2026-06-01 23:32:27 +03:00
coderiaser
d2176c93fb chore: actions: typo 2026-06-01 22:56:08 +03:00
coderaiser
c558e44a1c
docs: readme: InstaPods 2026-06-01 13:29:58 +03:00
coderaiser
45ec062bce
docs: readme: deploy on InstaPods 2026-06-01 13:24:07 +03:00
coderaiser
f8edddd442
chore: .nvmrc 2026-05-30 13:12:09 +02:00
coderiaser
fc3d5ac700 chore: cloudcmd: v19.19.0 2026-05-26 23:17:05 +03:00
coderaiser
4ee38db68c feature: client: view: flac: add support 2026-05-26 23:16:40 +03:00
52 changed files with 981 additions and 164 deletions

View file

@ -1,4 +1,6 @@
name: Docker IO
permissions:
contents: write
on:
- push
jobs:
@ -49,7 +51,7 @@ jobs:
with:
context: .
file: docker/Dockerfile.io
platforms: linux/amd64
platforms: linux/amd64,linux/arm64
push: true
tags: |
coderaiser/cloudcmd:io

View file

@ -1,4 +1,6 @@
name: Docker CI
permissions:
contents: write
on:
push:
tags:

View file

@ -1,6 +1,8 @@
name: Node CI
on:
- push
permissions:
contents: write
jobs:
build:
runs-on: ubuntu-latest
@ -22,24 +24,17 @@ jobs:
with:
node-version: ${{ matrix.node-version }}
- name: Install Redrun
run: bun i redrun -g --no-save
run: bun i redrun madrun -g --no-save
- name: Install
run: bun i --no-save
- name: Init Madrun
run: madrun --init
- name: Lint
run: redrun fix:lint
- uses: actions/cache@v5
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: Typos Install
run: cargo install typos-cli || echo 'already installed'
- name: Typos
run: typos --write-changes
uses: coderaiser/typos.ai@v1.1.8
with:
key: ${{ secrets.TYPOS_AI_KEY }}
- name: Commit fixes
uses: EndBug/add-and-commit@v10
continue-on-error: true

View file

@ -21,14 +21,14 @@ export default {
'start:dev',
]),
'lint:all': () => run('lint:progress'),
'lint': () => 'redlint fix; putout . --rulesdir rules',
'lint': () => 'redlint fix; putout .',
'lint:progress': () => run('lint', '-f progress'),
'watch:lint': () => 'nodemon -w client -w server -w test -w common -w .webpack -x "putout -s"',
'fresh:lint': () => run('lint', '--fresh'),
'lint:fresh': () => run('lint', '--fresh'),
'fix:lint': async () => `putout --rulesdir rules --fix . && redlint fix`,
'fix:lint': async () => `putout --fix . && redlint fix`,
'lint:stream': () => run('lint', '-f stream'),
'test': () => [testEnv, `tape '{test,test-e2e}/**/*.js' '{bin,client,static,common,server}/**/*.spec.js' -f fail`],
'test': () => [testEnv, `tape '{test}/**/*.js' '{bin,client,static,common,server}/**/*.spec.js' -f fail`],
'test:e2e': () => `tape 'test-e2e/**/*.js'`,
'test:client': () => `tape 'test/client/**/*.js'`,
'test:server': () => `tape 'test/**/*.js' 'server/**/*.spec.js' 'common/**/*.spec.js'`,
@ -39,7 +39,7 @@ export default {
'coverage': async () => [testEnv, `c8 ${await cutEnv('test')}`],
'coverage:report': () => 'c8 report --reporter=lcov',
'report': () => 'c8 report --reporter=lcov',
'6to5': () => [buildEnv, 'webpack --progress'],
'6to5': () => [buildEnv, 'rspack build --config rspack.config.js'],
'6to5:client': () => run('6to5', '--mode production'),
'6to5:client:dev': async () => await run('6to5', '--mode development', {
NODE_ENV: 'development',

1
.nvmrc
View file

@ -1 +0,0 @@
v20.15.1

View file

@ -1,19 +1,18 @@
import {env} from 'node:process';
import CssMinimizerPlugin from 'css-minimizer-webpack-plugin';
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
import {rspack} from '@rspack/core';
const {CssExtractRspackPlugin} = rspack;
const isDev = env.NODE_ENV === 'development';
const clean = (a) => a.filter(Boolean);
const plugins = clean([
new MiniCssExtractPlugin({
const plugins = [
new CssExtractRspackPlugin({
filename: '[name].css',
}),
]);
];
const rules = [{
test: /\.css$/i,
use: [MiniCssExtractPlugin.loader, {
use: [CssExtractRspackPlugin.loader, {
loader: 'css-loader',
options: {
url: true,
@ -33,13 +32,7 @@ export default {
optimization: {
minimize: !isDev,
minimizer: [
new CssMinimizerPlugin({
minimizerOptions: {
preset: ['default', {
svgo: false,
}],
},
}),
new rspack.LightningCssMinimizerRspackPlugin(),
],
},
};

View file

@ -1,12 +1,15 @@
import {resolve, sep} from 'node:path';
import {fileURLToPath} from 'node:url';
import {env} from 'node:process';
import webpack from 'webpack';
import WebpackBar from 'webpackbar';
import {rspack} from '@rspack/core';
const resolveModule = (a) => fileURLToPath(import.meta.resolve(a));
const {
EnvironmentPlugin,
NormalModuleReplacementPlugin,
} = webpack;
ProvidePlugin,
} = rspack;
const modules = './modules';
const dirModules = './client/modules';
@ -22,27 +25,31 @@ const dist = resolve(rootDir, 'dist');
const distDev = resolve(rootDir, 'dist-dev');
const devtool = isDev ? 'eval' : 'source-map';
const notEmpty = (a) => a;
const clean = (array) => array.filter(notEmpty);
const noParse = (a) => a.endsWith('.spec.js');
const options = {
babelrc: true,
};
const rules = clean([
!isDev && {
test: /\.[mc]?js$/,
exclude: /node_modules/,
loader: 'babel-loader',
// codegen.macro is a babel-macro (build-time codegen), not supported by
// Rspack's native SWC transform, so client/sw/sw.js (the only file that
// uses it) keeps going through babel-loader. Everything else uses
// Rspack's built-in SWC loader, which is the main source of the speedup.
const rules = [{
test: /sw\/sw\.js$/,
exclude: /node_modules/,
loader: 'babel-loader',
}, {
test: /\.[mc]?js$/,
exclude: [/node_modules/, /sw\/sw\.js$/],
loader: 'builtin:swc-loader',
options: {
jsc: {
parser: {
syntax: 'ecmascript',
},
},
env: {
targets: 'defaults',
},
},
isDev && {
test: /\.[mc]?js$/,
exclude: /node_modules/,
loader: 'babel-loader',
options,
},
]);
}];
const plugins = [
new NormalModuleReplacementPlugin(/^node:/, (resource) => {
@ -52,8 +59,7 @@ const plugins = [
new EnvironmentPlugin({
NODE_ENV,
}),
new WebpackBar(),
new webpack.ProvidePlugin({
new ProvidePlugin({
process: 'process/browser',
}),
];
@ -99,9 +105,9 @@ export default {
'node:path': 'path',
},
fallback: {
path: import.meta.resolve('path-browserify'),
process: import.meta.resolve('process/browser'),
util: import.meta.resolve('util'),
path: resolveModule('path-browserify'),
process: resolveModule('process/browser'),
util: resolveModule('util'),
},
},
devtool,
@ -156,7 +162,7 @@ export default {
},
plugins,
performance: {
maxEntrypointSize: 600_000,
maxEntrypointSize: 800_000,
maxAssetSize: 600_000,
},
};

View file

@ -1,3 +1,15 @@
2026.06.15, v19.19.1
feature:
- 78b206e0 client: key: Ctrl + L -> Ctrl + Shift + L: Log out (#466)
- b1492885 docker: io: NPM_CONFIG_PACKAGE_LOCK=false
- 76d7bf4d docker: typos, shellcheck: add
2026.05.26, v19.19.0
feature:
- 4ee38db6 client: view: flac: add support
2026.05.26, v19.18.1
feature:

15
HELP.md
View file

@ -1,6 +1,6 @@
# Cloud Commander v19.18.1
# Cloud Commander v19.19.1
### [Main][MainURL] [Blog][BlogURL] [Support][SupportURL] [Demo][DemoURL]
### [Main][MainURL] [Blog][BlogURL] [Demo][DemoURL] [Deploy](#deploy)
[MainURL]: https://cloudcmd.io "Main"
[BlogURL]: https://blog.cloudcmd.io "Blog"
@ -14,6 +14,9 @@
[TERMUX]: https://termux.com "Termux"
[INLY]: https://github.com/coderaiser/node-inly "Extract archive"
[DeployInstaPodsIMG]: https://img.shields.io/badge/deploy%20on-InstaPods-blue
[DeployInstaPodsURL]: https://app.instapods.com/dashboard/pods/create?app=cloudcmd&ref=cloudcmd
**Cloud Commander** is a file manager for the web. It includes a command-line console and a text editor. Cloud Commander helps you manage your server and work with files, directories and programs in a web browser from any computer, mobile or tablet.
![Cloud Commander](https://cloudcmd.io/img/logo/cloudcmd.png "Cloud Commander")
@ -187,12 +190,12 @@ Then, start the server again with `cloudcmd` and reload the page.
| `Ctrl + R` | refresh
| `Ctrl + D` | clear local storage
| `Ctrl + A` | select all files in a panel
| `Ctrl + L` | logout
| `Ctrl + M` | [rename selected files](https://github.com/coderaiser/cloudcmd/releases/tag/v12.1.0) in editor
| `Ctrl + U` | swap panels
| `Ctrl + F3` | sort by name
| `Ctrl + F5` | sort by date
| `Ctrl + F6` | sort by size
| `Ctrl + Shift + L` | logout
| `Ctrl + Command + .` | show/hide dot files
| `Up` | move cursor up
| `Down` | move cursor down
@ -930,9 +933,9 @@ ln -s ./sites-available/io.cloudcmd.io ./sites-enabled
## Deploy
`Cloud Commander` can be easily deployed to [Heroku](https://heroku.com/deploy?template=https://github.com/coderaiser/cloudcmd "Deploy to Heroku").
`Cloud Commander` can be easily deployed to [InstaPods][DeployInstaPodsURL]. After deploy you receive email with credentials.
[![Deploy to Heroku](https://www.herokucdn.com/deploy/button.png "Deploy to Heroku")](https://heroku.com/deploy?template=https://github.com/coderaiser/cloudcmd)
[![Deploy on InstaPods](https://instapods.com/deploy-button.svg)](https://app.instapods.com/dashboard/pods/create?app=cloudcmd&ref=cloudcmd)
## Docker
@ -1119,6 +1122,8 @@ There are a lot of ways to be involved in `Cloud Commander` development:
## Version history
- *2026.06.15*, **[v19.19.1](//github.com/coderaiser/cloudcmd/releases/tag/v19.19.1)**
- *2026.05.26*, **[v19.19.0](//github.com/coderaiser/cloudcmd/releases/tag/v19.19.0)**
- *2026.05.26*, **[v19.18.1](//github.com/coderaiser/cloudcmd/releases/tag/v19.18.1)**
- *2026.05.26*, **[v19.18.0](//github.com/coderaiser/cloudcmd/releases/tag/v19.18.0)**
- *2026.05.17*, **[v19.17.0](//github.com/coderaiser/cloudcmd/releases/tag/v19.17.0)**

View file

@ -1,4 +1,4 @@
# Cloud Commander v19.18.1 [![Build Status][BuildStatusIMGURL]][BuildStatusURL] [![Codacy][CodacyIMG]][CodacyURL] [![Gitter][GitterIMGURL]][GitterURL]
# Cloud Commander v19.19.1 [![Build Status][BuildStatusIMGURL]][BuildStatusURL] [![Codacy][CodacyIMG]][CodacyURL] [![Gitter][GitterIMGURL]][GitterURL] [![Deploy on InstaPods][DeployInstaPodsIMG]][DeployInstaPodsURL]
### [Main][MainURL] [Blog][BlogURL] [Support][SupportURL] [Demo][DemoURL]
@ -15,6 +15,8 @@
[GitterIMGURL]: https://img.shields.io/gitter/room/coderaiser/cloudcmd.js.svg
[DeployURL]: https://heroku.com/deploy?template=https://github.com/coderaiser/cloudcmd "Deploy"
[DeployIMG]: https://www.herokucdn.com/deploy/button.png
[DeployInstaPodsIMG]: https://img.shields.io/badge/deploy%20on-InstaPods-blue
[DeployInstaPodsURL]: https://app.instapods.com/dashboard/pods/create?app=cloudcmd&ref=cloudcmd
**Cloud Commander** a file manager for the web with console and editor.

View file

@ -19,14 +19,13 @@ test('current-file: setCurrentName: setAttribute', (t) => {
currentFile.setCurrentName('hello', current);
globalThis.DOM = DOM;
globalThis.CloudCmd = CloudCmd;
t.calledWith(setAttribute, [
'data-name',
'js-file-aGVsbG8=',
], 'should call setAttribute');
globalThis.DOM = DOM;
globalThis.CloudCmd = CloudCmd;
t.end();
});
@ -41,14 +40,13 @@ test('current-file: setCurrentName: setAttribute: cyrillic', (t) => {
currentFile.setCurrentName('ай', current);
globalThis.DOM = DOM;
globalThis.CloudCmd = CloudCmd;
t.calledWith(setAttribute, [
'data-name',
'js-file-JUQwJUIwJUQwJUI5',
], 'should call setAttribute');
globalThis.DOM = DOM;
globalThis.CloudCmd = CloudCmd;
t.end();
});
@ -76,11 +74,10 @@ test('current-file: emit', (t) => {
currentFile.setCurrentName('hello', current);
t.calledWith(emit, ['current-file', current], 'should call emit');
globalThis.DOM = DOM;
globalThis.CloudCmd = CloudCmd;
t.calledWith(emit, ['current-file', current], 'should call emit');
t.end();
});
@ -96,14 +93,12 @@ test('current-file: setCurrentName: return', (t) => {
globalThis.CloudCmd = getCloudCmd();
const current = create();
const result = currentFile.setCurrentName('hello', current);
t.equal(result, link, 'should return link');
globalThis.DOM = DOM;
globalThis.CloudCmd = CloudCmd;
t.equal(result, link, 'should return link');
t.end();
});

View file

@ -1,7 +1,16 @@
import test from 'supertape';
import {test, stub} from 'supertape';
import {create} from 'auto-globals';
import {tryCatch} from 'try-catch';
import {isContainClass} from './dom-tree.js';
import {
isContainClass,
getByTag,
getById,
getByClass,
getByDataName,
getByClassAll,
hide,
show,
} from './dom-tree.js';
test('dom: isContainClass: no element', (t) => {
const [e] = tryCatch(isContainClass);
@ -38,3 +47,106 @@ test('dom: isContainClass: contains: array', (t) => {
t.calledWith(contains, [className], 'should call contains');
t.end();
});
test('dom: getByTag', (t) => {
const getElementsByTagName = stub();
const element = {
getElementsByTagName,
};
getByTag('div', element);
t.calledWith(getElementsByTagName, ['div'], 'should call getElementsByTagName');
t.end();
});
test('dom: getById', (t) => {
const querySelector = stub();
const element = {
querySelector,
};
getById('my-id', element);
t.calledWith(querySelector, ['#my-id'], 'should call querySelector with id selector');
t.end();
});
test('dom: getByClassAll', (t) => {
const getElementsByClassName = stub();
const element = {
getElementsByClassName,
};
getByClassAll('my-class', element);
t.calledWith(getElementsByClassName, ['my-class'], 'should call getElementsByClassName');
t.end();
});
test('dom: getByClass: calls getByClassAll', (t) => {
const element = {
getElementsByClassName: stub().returns(['first']),
};
const result = getByClass('my-class', element);
t.equal(result, 'first', 'should return first element from class list');
t.end();
});
test('dom: getByDataName', (t) => {
const querySelector = stub();
const element = {
querySelector,
};
getByDataName('hello', element);
t.calledWith(querySelector, ['[data-name="hello"]'], 'should call querySelector with data-name selector');
t.end();
});
test('dom: hide', (t) => {
const add = stub();
const element = {
classList: {
add,
},
};
hide(element);
t.calledWith(add, ['hidden'], 'should add hidden class');
t.end();
});
test('dom: show', (t) => {
const remove = stub();
const element = {
classList: {
remove,
},
};
show(element);
t.calledWith(remove, ['hidden'], 'should remove hidden class');
t.end();
});
test('dom: getByClassAll: without element uses document', (t) => {
const getElementsByClassName = stub();
const prevDocument = globalThis.document;
globalThis.document = {
getElementsByClassName,
};
getByClassAll('my-class');
globalThis.document = prevDocument;
t.calledWith(getElementsByClassName, ['my-class'], 'should fallback to document when no element');
t.end();
});

View file

@ -442,11 +442,7 @@ export function getFilenames(files) {
if (name === '..')
allFiles.shift();
const names = allFiles.map((current) => {
return DOM.getCurrentName(current);
});
return names;
return allFiles.map(DOM.getCurrentName);
}
/**

View file

@ -33,13 +33,19 @@ export const loadRemote = callbackify(async (name, options) => {
local = [module.local];
}
const localURL = local.map((url) => prefix + url);
const localURL = [];
const remoteURL = remoteTmpls.map((tmpl) => {
return rendy(tmpl, {
for (const url of local) {
localURL.push(prefix + url);
}
const remoteURL = [];
for (const tmpl of remoteTmpls) {
remoteURL.push(rendy(tmpl, {
version,
});
});
}));
}
if (online) {
const [e] = await tryToCatch(load.parallel, remoteURL);

28
client/key/binder.spec.js Normal file
View file

@ -0,0 +1,28 @@
import {test} from 'supertape';
import {createBinder} from './binder.js';
test('client: key: binder: isBind: default', (t) => {
const binder = createBinder();
t.notOk(binder.isBind(), 'should not be bind by default');
t.end();
});
test('client: key: binder: setBind', (t) => {
const binder = createBinder();
binder.setBind();
t.ok(binder.isBind(), 'should be bind');
t.end();
});
test('client: key: binder: unsetBind', (t) => {
const binder = createBinder();
binder.setBind();
binder.unsetBind();
t.notOk(binder.isBind(), 'should not be bind');
t.end();
});

View file

@ -443,7 +443,7 @@ async function _switchKey(event) {
break;
case KEY.L:
if (ctrlMeta) {
if (ctrlMeta && shift) {
CloudCmd.logOut();
event.preventDefault();
}

View file

@ -1,6 +1,12 @@
import test from 'supertape';
import {getDOM} from './globals.fixture.js';
import {_next, _previous} from './find.js';
import {
_next,
_previous,
find,
findNext,
findPrevious,
} from './find.js';
globalThis.DOM = getDOM();
@ -11,9 +17,39 @@ test('cloudcmd: client: vim: _next', (t) => {
t.end();
});
test('cloudcmd: client: vim: _next: increment', (t) => {
const result = _next(0, 2);
t.equal(result, 1, 'should return 1');
t.end();
});
test('cloudcmd: client: vim: _previous', (t) => {
const result = _previous(0, 2);
t.equal(result, 1, 'should return 1');
t.end();
});
test('cloudcmd: client: vim: _previous: decrement', (t) => {
const result = _previous(1, 2);
t.equal(result, 0, 'should return 0');
t.end();
});
test('cloudcmd: client: vim: findNext: after find', (t) => {
find('a', ['alpha', 'beta', 'apple']);
const result = findNext();
t.equal(result, 'beta', 'should return next found name');
t.end();
});
test('cloudcmd: client: vim: findPrevious: after find', (t) => {
find('a', ['alpha', 'beta', 'apple']);
const result = findPrevious();
t.equal(result, 'apple', 'should return previous found name');
t.end();
});

View file

@ -1,6 +1,7 @@
import {test, stub} from 'supertape';
import {getDOM, getCloudCmd} from './globals.fixture.js';
import vim, {selectFile as vimSelectFile} from './index.js';
import * as finder from './find.js';
globalThis.DOM = getDOM();
globalThis.CloudCmd = getCloudCmd();
@ -675,3 +676,81 @@ test('cloudcmd: client: vim: rename', async (t) => {
t.calledWithNoArgs(renameCurrent);
t.end();
});
test('cloudcmd: client: key: cc: operationCopy', (t) => {
const show = stub();
const preventDefault = stub();
const Operation = {
show,
};
const event = {
preventDefault,
};
vim('c', event, {
Operation,
});
vim('c', event, {
Operation,
});
t.calledWith(show, ['copy'], 'should show copy operation');
t.end();
});
test('cloudcmd: client: key: mm: operationMove', (t) => {
const show = stub();
const preventDefault = stub();
const Operation = {
show,
};
const event = {
preventDefault,
};
vim('m', event, {
Operation,
});
vim('m', event, {
Operation,
});
t.calledWith(show, ['move'], 'should show move operation');
t.end();
});
test('cloudcmd: client: key: n: findNext: real', (t) => {
const setCurrentByName = stub();
finder.find('a', ['alpha', 'beta', 'apple']);
const event = {};
vim('n', event, {
setCurrentByName,
});
t.calledWith(setCurrentByName, ['beta'], 'should set current by next found name');
t.end();
});
test('cloudcmd: client: key: N: findPrevious: real', (t) => {
const setCurrentByName = stub();
finder.find('a', ['alpha', 'beta', 'apple']);
const event = {};
vim('N', event, {
setCurrentByName,
});
t.calledWith(setCurrentByName, ['apple'], 'should set current by previous found name');
t.end();
});

View file

@ -1,5 +1,10 @@
import {test} from 'supertape';
import {convert} from './input.js';
import {test, stub} from 'supertape';
import {
convert,
getName,
getValue,
setValue,
} from './input.js';
test('cloudcmd: client: config: input: convert', (t) => {
const result = convert({
@ -13,3 +18,123 @@ test('cloudcmd: client: config: input: convert', (t) => {
t.deepEqual(result, expected);
t.end();
});
test('cloudcmd: client: config: input: convert: bool', (t) => {
const result = convert({
auth: true,
});
const expected = {
auth: ' checked',
};
t.deepEqual(result, expected);
t.end();
});
test('cloudcmd: client: config: input: convert: bool false', (t) => {
const result = convert({
auth: false,
});
const expected = {
auth: '',
};
t.deepEqual(result, expected);
t.end();
});
test('cloudcmd: client: config: input: getName', (t) => {
const getAttribute = stub().returns('js-hello');
const element = {
getAttribute,
};
const result = getName(element);
t.equal(result, 'hello', 'should strip js- prefix');
t.end();
});
test('cloudcmd: client: config: input: getValue: checkbox', (t) => {
const querySelector = stub().returns({
type: 'checkbox',
checked: true,
});
const element = {
querySelector,
};
const result = getValue('auth', element);
t.ok(result, 'should return checked value');
t.end();
});
test('cloudcmd: client: config: input: getValue: number', (t) => {
const querySelector = stub().returns({
type: 'number',
value: '42',
});
const element = {
querySelector,
};
const result = getValue('port', element);
t.equal(result, 42, 'should return number');
t.end();
});
test('cloudcmd: client: config: input: getValue: default', (t) => {
const querySelector = stub().returns({
type: 'text',
value: 'hello',
});
const element = {
querySelector,
};
const result = getValue('name', element);
t.equal(result, 'hello', 'should return value as is');
t.end();
});
test('cloudcmd: client: config: input: setValue: checkbox', (t) => {
const el = {
type: 'checkbox',
checked: false,
};
const querySelector = stub().returns(el);
const element = {
querySelector,
};
setValue('auth', true, element);
t.ok(el.checked, 'should set checked');
t.end();
});
test('cloudcmd: client: config: input: setValue: default', (t) => {
const el = {
type: 'text',
value: 'old',
};
const querySelector = stub().returns(el);
const element = {
querySelector,
};
setValue('name', 'new', element);
t.equal(el.value, 'new', 'should set value');
t.end();
});

View file

@ -31,8 +31,10 @@ test('cloudcmd: client: view: initConfig', (t) => {
test('cloudcmd: client: view: initConfig: no options', (t) => {
const config = _initConfig();
const result = typeof config;
const expected = 'object';
t.equal(typeof config, 'object');
t.equal(result, expected);
t.end();
});

View file

@ -1,7 +1,7 @@
import {extname} from 'node:path';
import currify from 'currify';
export const isAudio = (name) => /\.(mp3|ogg|m4a)$/i.test(name);
export const isAudio = (name) => /\.(mp3|ogg|m4a|flac)$/i.test(name);
const testRegExp = currify((name, reg) => reg.test(name));
const getRegExp = (ext) => RegExp(`\\.${ext}$`, 'i');

View file

@ -8,6 +8,13 @@ test('cloudcmd: client: view: types: isAudio', (t) => {
t.end();
});
test('cloudcmd: client: view: types: isAudio: flac', (t) => {
const result = isAudio('hello.flac');
t.ok(result);
t.end();
});
test('cloudcmd: client: view: types: isAudio: no', (t) => {
const result = isAudio('hello');

View file

@ -65,7 +65,7 @@ test('sw: register: registerSW: http', async (t, {location, navigator}) => {
test('sw: register: registerSW: https self-signed', async (t, {location, navigator}) => {
Object.assign(location, {
protocol: 'https',
protocol: 'https:',
hostname: 'self-signed.badssl.com',
});

View file

@ -2,12 +2,17 @@ import {readFileSync} from 'node:fs';
import test from 'supertape';
import {montag} from 'montag';
import * as cheerio from 'cheerio';
import {tryCatch} from 'try-catch';
import {
_getSize,
getPathLink,
buildFromJSON,
_getDataName,
dateFormatter,
formatMsg,
getTitle,
getDotDot,
getHeaderField,
} from '#common/cloudfunc';
const templatePath = new URL('../tmpl/fs', import.meta.url).pathname;
@ -221,6 +226,97 @@ test('cloudfunc: _getDataName', (t) => {
t.end();
});
test('cloudfunc: formatMsg: name', (t) => {
const result = formatMsg('hello', 'world');
const expected = 'hello: ok("world")';
t.equal(result, expected, 'should format message with name');
t.end();
});
test('cloudfunc: formatMsg: no name', (t) => {
const result = formatMsg('hello');
const expected = 'hello: ok';
t.equal(result, expected, 'should format message without name');
t.end();
});
test('cloudfunc: getTitle: no options', (t) => {
const result = getTitle();
t.ok(result, 'should return a title string even without options');
t.end();
});
test('cloudfunc: getTitle: with name', (t) => {
const result = getTitle({
name: 'MyName',
});
t.match(result, 'MyName', 'should return title with name');
t.end();
});
test('cloudfunc: getHeaderField: sort not name', (t) => {
const result = getHeaderField('size', 'asc', 'name');
t.equal(result, 'name', 'should return plain name when sort does not match');
t.end();
});
test('cloudfunc: getHeaderField: sort name asc', (t) => {
const result = getHeaderField('name', 'asc', 'name');
t.equal(result, 'name', 'should return plain name when name asc');
t.end();
});
test('cloudfunc: getDotDot: root', (t) => {
const result = getDotDot('/');
t.equal(result, '/', 'should return / for root path');
t.end();
});
test('cloudfunc: getPathLink: /a/b/c', (t) => {
const {pathLink} = template;
const result = getPathLink('/a/b/c/', '', pathLink);
t.ok(result, 'should build path link for 3-segment path');
t.end();
});
test('cloudfunc: getPathLink: no url', (t) => {
const [error] = tryCatch(getPathLink);
t.equal(error.message, 'url could not be empty!', 'should throw when url is empty');
t.end();
});
test('cloudfunc: getPathLink: no template', (t) => {
const [error] = tryCatch(getPathLink, '/');
t.equal(error.message, 'template could not be empty!', 'should throw when template is empty');
t.end();
});
test('cloudfunc: getHeaderField: sort name desc', (t) => {
const result = getHeaderField('name', 'desc', 'name');
const expected = 'name↓';
t.equal(result, expected, 'should return name with down arrow');
t.end();
});
test('cloudfunc: getDotDot: normal path', (t) => {
const result = getDotDot('/hello/world/');
const expected = '/hello';
t.equal(result, expected, 'should return parent directory');
t.end();
});
test('cloudfunc: buildFromJSON: formatDate', (t) => {
const data = {
path: '/media/',

View file

@ -5,6 +5,8 @@ import {
getRegExp,
escapeRegExp,
getExt,
time,
timeEnd,
} from '#common/util';
test('getExt: no extension', (t) => {
@ -106,11 +108,31 @@ test('util: getRegExp: no', (t) => {
});
test('util: escapeRegExp: no str', (t) => {
t.equal(escapeRegExp(1), 1);
const result = escapeRegExp(1);
const expected = 1;
t.equal(result, expected);
t.end();
});
test('util: escapeRegExp', (t) => {
t.equal(escapeRegExp('#hello'), '\\#hello');
const result = escapeRegExp('#hello');
const expected = '\\#hello';
t.equal(result, expected);
t.end();
});
test('util: time', (t) => {
const [error] = tryCatch(time, 'test');
t.notOk(error, 'should not throw');
t.end();
});
test('util: timeEnd', (t) => {
const [error] = tryCatch(timeEnd, 'test');
t.notOk(error, 'should not throw');
t.end();
});

View file

@ -11,19 +11,20 @@ COPY package.json /usr/local/share/cloudcmd/
ENV DEBIAN_FRONTEND=noninteractive \
NVM_DIR=/usr/local/share/nvm \
PREFIX=/usr/local \
npm_config_cache=/tmp/npm-cache \
GOPATH=/usr/local/share/go \
PATH=/usr/local/share/bun/bin:$PATH \
BUN_INSTALL=/usr/local/share/bun \
NPM_CONFIG_CACHE=/tmp/.npm \
NPM_CONFIG_PREFIX=/usr/local \
NPM_CONFIG_PACKAGE_LOCK=false \
PALABRA_DIR=/usr/local/share \
XDG_CONFIG_HOME=/usr/local/etc
ARG UBUNTU_DEPS="libatomic1 curl wget git net-tools iproute2 software-properties-common"
ARG RUST_DEPS="build-essential"
ARG DEPS="pv gcc gdb strace upx-ucl less ffmpeg net-tools netcat-openbsd mc far2l iputils-ping vim bat fzf locales sudo command-not-found ncdu aptitude htop btop hexyl tmux"
ARG PALABRA_DEPS="nvm node rust go deno fasm nvchad rizin yara gdu f4"
ARG PALABRA_DEPS="nvm node rust go deno fasm nvchad rizin yara gdu f4 typos shellcheck gh"
ARG BUN_DEPS="palabra wisdom nupdate version-io redrun superc8 supertape madrun redlint putout renamify-cli runny redfork cline"
RUN apt-get update && \
@ -31,7 +32,7 @@ RUN apt-get update && \
apt-get autoremove && \
apt-get install -y ${UBUNTU_DEPS} ${RUST_DEPES} ${DEPS} && \
echo "> Install git" && \
#add-apt-repository ppa:git-core/ppa -y && \
add-apt-repository ppa:git-core/ppa -y && \
echo "> Update command-not-found database. Run 'sudo apt update' to populate it." && \
apt-get update && \
apt-get upgrade -y && \

View file

@ -1,6 +1,6 @@
{
"name": "cloudcmd",
"version": "19.18.1",
"version": "19.19.1",
"type": "module",
"author": "coderaiser <mnemonic.enemy@gmail.com> (https://github.com/coderaiser)",
"description": "File manager for the web with console and editor",
@ -151,10 +151,10 @@
"yargs-parser": "^22.0.0"
},
"devDependencies": {
"@babel/code-frame": "^7.22.5",
"@babel/core": "^7.22.5",
"@babel/plugin-transform-optional-chaining": "^7.21.0",
"@babel/preset-env": "^7.0.0",
"@babel/code-frame": "^8.0.0",
"@babel/core": "^8.0.1",
"@babel/plugin-transform-optional-chaining": "^8.0.1",
"@babel/preset-env": "^8.0.2",
"@cloudcmd/clipboard": "^2.0.0",
"@cloudcmd/create-element": "^2.0.0",
"@cloudcmd/modal": "^4.0.0",
@ -162,6 +162,8 @@
"@cloudcmd/stub": "^5.0.0",
"@iocmd/wait": "^2.1.0",
"@putout/eslint-flat": "^4.0.0",
"@rspack/cli": "^2.1.4",
"@rspack/core": "^2.1.4",
"@supertape/loader-css": "^1.0.0",
"@types/node-fetch": "^2.6.11",
"auto-globals": "^4.0.0",
@ -171,7 +173,6 @@
"clean-css-loader": "^4.2.1",
"codegen.macro": "^4.0.0",
"css-loader": "^7.1.2",
"css-minimizer-webpack-plugin": "^8.0.0",
"css-modules-require-hook": "^4.2.3",
"cssnano-preset-default": "^8.0.1",
"domtokenlist-shim": "^1.2.0",
@ -190,12 +191,10 @@
"load.js": "^3.0.0",
"madrun": "^13.0.0",
"memfs": "^4.2.0",
"mini-css-extract-plugin": "^2.9.2",
"minor": "^1.2.2",
"morgan": "^1.6.1",
"multi-rename": "^3.0.0",
"nodemon": "^3.0.1",
"optimize-css-assets-webpack-plugin": "^6.0.1",
"path-browserify": "^1.0.1",
"philip": "^3.0.0",
"place": "^1.1.4",
@ -206,7 +205,7 @@
"request": "^2.76.0",
"rimraf": "^6.0.1",
"scroll-into-view-if-needed": "^3.0.4",
"serve-once": "^3.0.1",
"serve-once": "^4.0.1",
"smalltalk": "^5.0.0",
"style-loader": "^4.0.0",
"superc8": "^12.6.0",
@ -216,10 +215,7 @@
"unionfs": "^4.0.0",
"url-loader": "^4.0.0",
"util": "^0.12.5",
"webpack": "^5.99.9",
"webpack-cli": "^7.0.2",
"webpack-merge": "^6.0.1",
"webpackbar": "^7.0.0"
"webpack-merge": "^6.0.1"
},
"imports": {
"#css/": "./css/",

10
rspack.config.js Normal file
View file

@ -0,0 +1,10 @@
import {merge} from 'webpack-merge';
import * as htmlConfig from './.rspack/html.js';
import cssConfig from './.rspack/css.js';
import jsConfig from './.rspack/js.js';
export default merge([
jsConfig,
htmlConfig,
cssConfig,
]);

View file

@ -1,6 +1,6 @@
import path, {dirname} from 'node:path';
import {fileURLToPath} from 'node:url';
import serveOnce from 'serve-once';
import {serveOnce} from 'serve-once';
import {test, stub} from 'supertape';
import cloudcmd, {
_isDev,
@ -10,6 +10,9 @@ import cloudcmd, {
_initAuth,
_getIndexPath,
} from '#server/cloudcmd';
import {connect} from '../test/before.js';
const noop = () => {};
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
@ -158,17 +161,20 @@ test('cloudcmd: auth: accept: no auth', (t) => {
test('cloudcmd: getIndexPath: production', (t) => {
const isDev = false;
const name = path.join(__dirname, '..', 'dist', 'index.html');
t.equal(_getIndexPath(isDev), name);
const result = _getIndexPath(isDev);
const expected = path.join(__dirname, '..', 'dist', 'index.html');
t.equal(result, expected);
t.end();
});
test('cloudcmd: getIndexPath: development', (t) => {
const isDev = true;
const name = path.join(__dirname, '..', 'dist-dev', 'index.html');
const result = _getIndexPath(isDev);
const expected = path.join(__dirname, '..', 'dist-dev', 'index.html');
t.equal(_getIndexPath(isDev), name);
t.equal(result, expected);
t.end();
});
@ -178,3 +184,64 @@ test('cloudcmd: sw', async (t) => {
t.equal(status, 200, 'should return sw');
t.end();
});
test('cloudcmd: no params', (t) => {
const middle = cloudcmd();
t.ok(Array.isArray(middle), 'should return middleware list when no params passed');
t.end();
});
test('cloudcmd: listen: terminal', async (t) => {
const {done} = await connect({
config: {
terminal: true,
},
});
await done();
t.pass('should listen with terminal enabled');
t.end();
});
test('cloudcmd: middle: dropbox', async (t) => {
const {port, done} = await connect({
config: {
dropbox: true,
dropboxToken: 'hello',
},
});
const response = await fetch(`http://localhost:${port}/api/v1/dropbox/nonexistent`);
await done();
t.ok(response.status, 'should mount dropbox route');
t.end();
});
test('cloudcmd: logout', async (t) => {
const {status} = await request.get('/logout');
t.equal(status, 401, 'should return 401 for /logout');
t.end();
});
test('cloudcmd: modules', (t) => {
const middle = cloudcmd({
modules: {
hello: noop,
},
});
t.ok(Array.isArray(middle), 'should return middleware list with modules');
t.end();
});
test('cloudcmd: setUrl: cloudcmd.js', async (t) => {
const {status} = await request.get('/cloudcmd.js');
t.equal(status, 200, 'should serve cloudcmd.js');
t.end();
});

View file

@ -32,3 +32,32 @@ test('distribute: log: config', (t) => {
}, {
checkAssertionsCount: false,
});
test('distribute: log: stringToRGB', (t) => {
const result = log.stringToRGB('abc');
t.deepEqual(result, [97, 3, 294], 'should return [charCode, length, crc]');
t.end();
});
test('distribute: log: makeColor', (t) => {
const result = log.makeColor('hello');
t.match(result, 'hello', 'should return colored string containing the input');
t.end();
});
test('distribute: log: getDescription', (t) => {
const message = 'some error';
const result = log.getDescription({
message,
});
t.equal(result, message, 'should return message from error object');
t.end();
});
test('distribute: log: connectedStr', (t) => {
t.ok(log.connectedStr, 'should have connectedStr');
t.end();
});

View file

@ -54,3 +54,16 @@ test('cloudcmd: server: env: bool: number: 0', (t) => {
t.notOk(result);
t.end();
});
test('cloudcmd: server: env: bool: zero uppercase', (t) => {
const {CLOUDCMD_TERMINAL} = process.env;
process.env.CLOUDCMD_TERMINAL = '0';
const result = env.bool('terminal');
process.env.CLOUDCMD_TERMINAL = CLOUDCMD_TERMINAL;
t.notOk(result);
t.end();
});

View file

@ -4,44 +4,46 @@ import exit from './exit.js';
test('cloudcmd: exit: process.exit', (t) => {
const {exit: exitOriginal} = process;
const exitStub = stub();
process.exit = stub();
process.exit = exitStub;
exit();
t.calledWith(process.exit, [1], 'should call process.exit');
process.exit = exitOriginal;
t.calledWith(exitStub, [1], 'should call process.exit');
t.end();
});
test('cloudcmd: exit: console.error', (t) => {
const {exit: exitOriginal} = process;
const {error} = console;
const errorStub = stub();
console.error = stub();
console.error = errorStub;
process.exit = stub();
exit('hello world');
t.calledWith(console.error, ['hello world'], 'should call console.error');
process.exit = exitOriginal;
console.error = error;
t.calledWith(errorStub, ['hello world'], 'should call console.error');
t.end();
});
test('cloudcmd: exit.error: console.error: error', (t) => {
const {exit: exitOriginal} = process;
const {error} = console;
const errorStub = stub();
console.error = stub();
console.error = errorStub;
process.exit = stub();
exit(Error('hello world'));
t.calledWith(console.error, ['hello world'], 'should call console.error');
process.exit = exitOriginal;
console.error = error;
t.calledWith(errorStub, ['hello world'], 'should call console.error');
t.end();
});

View file

@ -3,7 +3,7 @@ import {join} from 'node:path';
import {promisify} from 'node:util';
import {tryToCatch} from 'try-to-catch';
import test from 'supertape';
import serveOnce from 'serve-once';
import {serveOnce} from 'serve-once';
import {cloudcmd} from '#server/cloudcmd';
import markdown from './index.js';

View file

@ -32,6 +32,8 @@ const UserError = (msg) => {
return error;
};
export const _UserError = UserError;
export default currify(({config, fs = _fs, moveFiles = _moveFiles}, request, response, next) => {
const name = ponse.getPathName(request);
const regExp = RegExp(`^${apiURL}`);
@ -153,6 +155,8 @@ function getPackReg(packer) {
return /\.tar\.gz$/;
}
export const _getPackReg = getPackReg;
function streamPack(cmd, response, packer) {
const noop = () => {};
const filename = cmd.replace(getPackReg(packer), '');
@ -172,6 +176,8 @@ function getCMD(cmd) {
return cmd;
}
export const _getCMD = getCMD;
const getMoveMsg = (names) => formatMsg('move', names);
const getRenameMsg = (from, to) => {
@ -276,6 +282,8 @@ function rename(rootDir, from, to, fs, callback) {
return fs.rename(fromRooted, toRooted, fn);
}
export const _rename = rename;
function pack(from, to, names, config, fn) {
const rootDir = config('root');
const packer = config('packer');
@ -294,6 +302,8 @@ function pack(from, to, names, config, fn) {
operation('pack', packer, from, to, names, fn);
}
export const _pack = pack;
function extract(from, to, config, fn) {
const rootDir = config('root');
@ -307,6 +317,8 @@ function extract(from, to, config, fn) {
operation('extract', config('packer'), from, to, fn);
}
export const _extract = extract;
function getPacker(operation, packer) {
if (operation === 'extract')
return inly;
@ -317,6 +329,8 @@ function getPacker(operation, packer) {
return jaguar.pack;
}
export const _getPacker = getPacker;
function operation(op, packer, from, to, names, fn) {
if (!fn) {
fn = names;
@ -345,6 +359,8 @@ function operation(op, packer, from, to, names, fn) {
});
}
export const _operation = operation;
function copy(from, to, names, fn) {
copymitter(from, to, names)
.on('error', fn)

View file

@ -1,4 +1,4 @@
import {test} from 'supertape';
import {test, stub} from 'supertape';
import {tryToCatch} from 'try-to-catch';
import {
_formatMsg,
@ -6,6 +6,11 @@ import {
_isRootWin32,
_isRootAll,
_onPUT,
_UserError,
_getPackReg,
_getCMD,
_rename,
_getPacker,
} from './index.js';
test('rest: formatMsg', (t) => {
@ -70,3 +75,104 @@ test('rest: onPUT: no callback', async (t) => {
t.equal(e.message, 'callback should be a function!', 'should throw when no callback');
t.end();
});
test('rest: UserError: message', (t) => {
const result = _UserError('hello');
t.equal(result.message, 'hello', 'should set message');
t.end();
});
test('rest: UserError: code', (t) => {
const result = _UserError('hello');
t.equal(result.code, 'EUSER', 'should set code');
t.end();
});
test('rest: getPackReg: zip', (t) => {
const result = _getPackReg('zip');
t.match('file.zip', result, 'should match .zip');
t.end();
});
test('rest: getPackReg: tar', (t) => {
const result = _getPackReg('tar');
t.match('file.tar.gz', result, 'should match .tar.gz');
t.end();
});
test('rest: getCMD: with slash', (t) => {
const result = _getCMD('/move');
t.equal(result, 'move', 'should strip leading slash');
t.end();
});
test('rest: getCMD: no slash', (t) => {
const result = _getCMD('move');
t.equal(result, 'move', 'should return as is');
t.end();
});
test('rest: rename: no from', (t) => {
const callback = stub();
_rename('/', null, 'to', null, callback);
const msg = '"from" should be filled';
t.calledWith(callback, [
_UserError(msg),
], 'should return UserError');
t.end();
});
test('rest: rename: no to', (t) => {
const callback = stub();
_rename('/', 'from', null, null, callback);
const msg = '"to" should be filled';
t.calledWith(callback, [
_UserError(msg),
], 'should return UserError');
t.end();
});
test('rest: rename: success', (t) => {
const callback = stub();
const fs = {
rename: stub(),
};
_rename('/root', 'from', 'to', fs, callback);
t.calledWith(fs.rename, ['/root/from', '/root/to', callback], 'should call fs.rename');
t.end();
});
test('rest: getPacker: extract', (t) => {
const result = _getPacker('extract', 'zip');
t.equal(typeof result, 'function', 'should return function');
t.end();
});
test('rest: getPacker: pack zip', (t) => {
const result = _getPacker('pack', 'zip');
t.equal(typeof result, 'function', 'should return function');
t.end();
});
test('rest: getPacker: pack tar', (t) => {
const result = _getPacker('pack', 'tar');
t.equal(typeof result, 'function', 'should return function');
t.end();
});

View file

@ -4,7 +4,7 @@ import {Readable} from 'node:stream';
import fs from 'node:fs';
import {tryToCatch} from 'try-to-catch';
import {test, stub} from 'supertape';
import serveOnce from 'serve-once';
import {serveOnce} from 'serve-once';
import {cloudcmd} from '#server/cloudcmd';
import {_getReadDir} from './route.js';

View file

@ -30,3 +30,21 @@ test('themes: no args', (t) => {
t.match(themes.light, css);
t.end();
});
test('themes: production', (t) => {
const themes = getThemes({
isDev: false,
});
t.ok(themes.dark, 'should have dark theme');
t.end();
});
test('themes: production: light', (t) => {
const themes = getThemes({
isDev: false,
});
t.ok(themes.light, 'should have light theme');
t.end();
});

View file

@ -2,7 +2,7 @@ import {dirname, join} from 'node:path';
import {fileURLToPath} from 'node:url';
import {readFileSync} from 'node:fs';
import {test, stub} from 'supertape';
import serveOnce from 'serve-once';
import {serveOnce} from 'serve-once';
import {putout} from 'putout';
import {userMenu} from './user-menu.js';

View file

@ -131,3 +131,26 @@ test('validate: theme: wrong', (t) => {
t.calledWith(exit, [msg], 'should call exit');
t.end();
});
test('validate: menu: not valid', (t) => {
const exit = stub();
const msg = 'cloudcmd --menu: could be "supermenu" or "aleman" only';
validate.menu('hello', {
exit,
});
t.calledWith(exit, [msg], 'should call fn');
t.end();
});
test('validate: menu: valid', (t) => {
const exit = stub();
validate.menu('supermenu', {
exit,
});
t.notCalled(exit, 'should not call fn');
t.end();
});

View file

@ -168,7 +168,9 @@ test('cloudcmd: static: user menu: C: exists: cancel', async (t) => {
const {write} = DOM.IO;
getCurrentByName.returns({});
confirm.resolves([Error('cancel')]);
confirm.resolves([
Error('cancel'),
]);
await defaultMenu[name]({
DOM,

View file

@ -1,15 +1,10 @@
import process from 'node:process';
import {spawn} from 'node:child_process';
import {test} from 'supertape';
let i = 0;
test('cloudcmd: server: ratelimit: x-forwarded-for', async (t) => {
const PORT = 3000;
process.env.PORT = PORT;
process.env.CLOUDCMD_LOG = 0;
await import('../bin/cloudcmd.js');
const child = await run(PORT);
const {status} = await fetch(`http://localhost:${PORT}`, {
headers: {
@ -17,7 +12,7 @@ test('cloudcmd: server: ratelimit: x-forwarded-for', async (t) => {
},
});
process.kill(process.pid, 'SIGUSR1');
child.kill();
t.notEqual(status, 500);
t.end();
@ -27,18 +22,34 @@ test('cloudcmd: server: ratelimit', async (t) => {
const PORT = 3001;
const STATUS = 429;
process.env.PORT = PORT;
process.env.CLOUDCMD_LOG = 0;
await import(`../bin/cloudcmd.js?${i++}`);
const child = await run(PORT);
for (let i = 0; i < 1000; i++) {
await fetch(`http://localhost:${PORT}`);
}
const {status} = await fetch(`http://localhost:${PORT}`);
process.kill(process.pid, 'SIGUSR1');
child.kill();
t.equal(status, STATUS);
t.end();
});
function run(port) {
return new Promise((resolve, reject) => {
const child = spawn(new URL('../bin/cloudcmd.js', import.meta.url).pathname, [], {
env: {
...process.env,
PORT: port,
},
stdio: ['ignore', 'pipe', 'pipe'],
});
child.stdout.on('data', (a) => {
if (a.toString().includes('url'))
resolve(child);
});
child.on('error', reject);
});
}

View file

@ -50,8 +50,7 @@ function before(options, fn = options) {
}));
server.listen(() => {
fn(server
.address().port, promisify(after));
fn(server.address().port, promisify(after));
});
}

View file

@ -1,4 +1,4 @@
import serveOnce from 'serve-once';
import {serveOnce} from 'serve-once';
import test from 'supertape';
import {cloudcmd} from '#server/cloudcmd';

View file

@ -1,7 +1,7 @@
import {dirname, join} from 'node:path';
import {fileURLToPath} from 'node:url';
import {mkdirSync} from 'node:fs';
import serveOnce from 'serve-once';
import {serveOnce} from 'serve-once';
import test from 'supertape';
import {rimraf} from 'rimraf';
import {cloudcmd} from '#server/cloudcmd';

View file

@ -1,4 +1,4 @@
import serveOnce from 'serve-once';
import {serveOnce} from 'serve-once';
import test from 'supertape';
import {cloudcmd} from '#server/cloudcmd';

View file

@ -1,7 +1,7 @@
import {EventEmitter} from 'node:events';
import wait from '@iocmd/wait';
import {test, stub} from 'supertape';
import serveOnce from 'serve-once';
import {serveOnce} from 'serve-once';
import {cloudcmd} from '#server/cloudcmd';
test('cloudcmd: rest: move', async (t) => {

View file

@ -6,7 +6,7 @@ import test from 'supertape';
import tar from 'tar-stream';
import gunzip from 'gunzip-maybe';
import pullout from 'pullout';
import serveOnce from 'serve-once';
import {serveOnce} from 'serve-once';
import {cloudcmd} from '#server/cloudcmd';
const __filename = fileURLToPath(import.meta.url);

View file

@ -2,7 +2,7 @@ import fs from 'node:fs';
import test from 'supertape';
import {Volume} from 'memfs';
import {ufs} from 'unionfs';
import serveOnce from 'serve-once';
import {serveOnce} from 'serve-once';
import {cloudcmd} from '#server/cloudcmd';
test('cloudcmd: rest: rename', async (t) => {

View file

@ -4,34 +4,37 @@ import * as env from '../../server/env.js';
test('env: small', (t) => {
process.env.cloudcmd_hello = 'world';
t.equal(env.parse('hello'), 'world', 'should parse string from env');
delete process.env.cloudcmd_hello;
t.equal(env.parse('hello'), 'world', 'should parse string from env');
t.end();
});
test('env: big', (t) => {
process.env.CLOUDCMD_HELLO = 'world';
t.equal(env.parse('hello'), 'world', 'should parse string from env');
delete process.env.CLOUDCMD_HELLO;
t.equal(env.parse('hello'), 'world', 'should parse string from env');
t.end();
});
test('env: bool: false', (t) => {
process.env.cloudcmd_terminal = 'false';
t.notOk(env.bool('terminal'), 'should return false');
delete process.env.cloudcmd_terminal;
t.notOk(env.bool('terminal'), 'should return false');
t.end();
});
test('env: bool: true', (t) => {
process.env.cloudcmd_terminal = 'true';
t.ok(env.bool('terminal'), 'should be true');
delete process.env.cloudcmd_terminal;
t.ok(env.bool('terminal'), 'should be true');
t.end();
});
@ -39,9 +42,10 @@ test('env: bool: undefined', (t) => {
const {cloudcmd_terminal} = process.env;
process.env.cloudcmd_terminal = undefined;
t.notOk(env.bool('terminal'), 'should be undefined');
process.env.cloudcmd_terminal = cloudcmd_terminal;
const result = env.bool('terminal');
t.notOk(result);
t.end();
});

View file

@ -1,7 +1,7 @@
import {createRequire} from 'node:module';
import {dirname, join} from 'node:path';
import {fileURLToPath} from 'node:url';
import serveOnce from 'serve-once';
import {serveOnce} from 'serve-once';
import {test, stub} from 'supertape';
import {cloudcmd} from '#server/cloudcmd';
import modulas from '../../server/modulas.js';

View file

@ -1,5 +1,5 @@
import {Buffer} from 'node:buffer';
import serveOnce from 'serve-once';
import {serveOnce} from 'serve-once';
import test from 'supertape';
import criton from 'criton';
import {cloudcmd} from '#server/cloudcmd';