diff --git a/.cloudcmd.menu.js b/.cloudcmd.menu.js index 0e0126b6..08637d80 100644 --- a/.cloudcmd.menu.js +++ b/.cloudcmd.menu.js @@ -1,31 +1,43 @@ export default { - 'F2 - Rename File': renameCurrent, - 'L - Lint': run('npm run lint'), - 'F - Fix Lint': run('npm run fix:lint'), - 'T - Test': run('npm run test'), - 'C - Coverage': run('npm run coverage'), - 'D - Build Dev': run('npm run build:client:dev'), - 'P - Build Prod': run('npm run build:client'), + 'F2 - Rename file': async ({DOM}) => { + await DOM.renameCurrent(); + }, + 'L - Lint': async ({CloudCmd}) => { + const {TerminalRun} = CloudCmd; + await run(TerminalRun, 'npm run lint'); + }, + 'F - Fix Lint': async ({CloudCmd}) => { + const {TerminalRun} = CloudCmd; + await run(TerminalRun, 'npm run fix:lint'); + }, + 'T - Test': async ({CloudCmd}) => { + const {TerminalRun} = CloudCmd; + + await run(TerminalRun, 'npm run test'); + }, + 'C - Coverage': async ({CloudCmd}) => { + const {TerminalRun} = CloudCmd; + + await run(TerminalRun, 'npm run coverage'); + }, + 'D - Build Dev': async ({CloudCmd}) => { + const {TerminalRun} = CloudCmd; + + await run(TerminalRun, 'npm run build:client:dev'); + CloudCmd.refresh(); + }, + 'P - Build Prod': async ({CloudCmd}) => { + const {TerminalRun} = CloudCmd; + + await run(TerminalRun, 'npm run build:client'); + CloudCmd.refresh(); + }, }; -async function renameCurrent(DOM) { - await DOM.renameCurrent(); -} - -function run(command) { - return async ({CloudCmd, DOM}) => { - const {TerminalRun, config} = CloudCmd; - - const {CurrentInfo} = DOM; - const {dirPath} = CurrentInfo; - - const cwd = config('root') + dirPath; - - return await TerminalRun.show({ - cwd, - command, - closeMessage: 'Press any key to close Terminal', - autoClose: false, - }); - }; +async function run(TerminalRun, command) { + await TerminalRun.show({ + command, + closeMessage: 'Press any key to close Terminal', + autoClose: false, + }); } diff --git a/.github/workflows/docker-io.yml b/.github/workflows/docker-io.yml deleted file mode 100644 index 8cbeb042..00000000 --- a/.github/workflows/docker-io.yml +++ /dev/null @@ -1,60 +0,0 @@ -name: Docker IO -permissions: - contents: write -on: - - push -jobs: - buildx: - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - steps: - - name: Checkout - uses: actions/checkout@v5 - - uses: oven-sh/setup-bun@v2 - with: - bun-version: latest - - name: Use Node.js 24.x - uses: actions/setup-node@v6 - with: - node-version: 24.x - - name: Install Redrun - run: bun i redrun -g --no-save - - name: NPM Install - run: bun i --no-save - - name: Lint - run: redrun lint - - name: Build - id: build - run: | - redrun build - VERSION=$(grep '"version":' package.json -m1 | cut -d\" -f4) - echo "version=$VERSION" >> $GITHUB_OUTPUT - - name: Set up QEMU - uses: docker/setup-qemu-action@v4 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v4 - - name: Login to DockerHub - uses: docker/login-action@v4 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_TOKEN }} - - name: Login to GitHub Container Registry - uses: docker/login-action@v4 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - name: Build and push io-image - uses: docker/build-push-action@v7 - with: - context: . - file: docker/Dockerfile.io - platforms: linux/amd64,linux/arm64 - push: true - tags: | - coderaiser/cloudcmd:io - coderaiser/cloudcmd:${{ steps.build.outputs.version }}-io - ghcr.io/${{ github.repository }}-io - ghcr.io/${{ github.repository }}:${{ steps.build.outputs.version }}-io diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 5c81d368..f687d5ec 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -1,6 +1,4 @@ name: Docker CI -permissions: - contents: write on: push: tags: @@ -86,3 +84,15 @@ jobs: coderaiser/cloudcmd:${{ steps.build.outputs.version }}-slim ghcr.io/${{ github.repository }}-slim ghcr.io/${{ github.repository }}:${{ steps.build.outputs.version }}-slim + - name: Build and push io-image + uses: docker/build-push-action@v7 + with: + context: . + file: docker/Dockerfile.io + platforms: linux/amd64,linux/arm64 + push: true + tags: | + coderaiser/cloudcmd:io + coderaiser/cloudcmd:${{ steps.build.outputs.version }}-io + ghcr.io/${{ github.repository }}-io + ghcr.io/${{ github.repository }}:${{ steps.build.outputs.version }}-io diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index 429e5568..0080488a 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -1,8 +1,6 @@ name: Node CI on: - push -permissions: - contents: write jobs: build: runs-on: ubuntu-latest @@ -13,7 +11,7 @@ jobs: node-version: - 22.x - 24.x - - 26.x + - 25.x steps: - uses: actions/checkout@v5 - uses: oven-sh/setup-bun@v2 @@ -24,19 +22,26 @@ jobs: with: node-version: ${{ matrix.node-version }} - name: Install Redrun - run: bun i redrun madrun -g --no-save + run: bun i redrun -g --no-save - name: Install run: bun i --no-save - - name: Init Madrun - run: madrun --init - name: Lint run: redrun fix:lint - - name: Typos - uses: coderaiser/typos.ai@v1.1.8 + - uses: actions/cache@v5 with: - key: ${{ secrets.TYPOS_AI_KEY }} + 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 - name: Commit fixes - uses: EndBug/add-and-commit@v10 + uses: EndBug/add-and-commit@v9 continue-on-error: true with: message: "chore: ${{ env.NAME }}: actions: lint ☘️" diff --git a/.madrun.js b/.madrun.js index efa8ce98..9bd30e3d 100644 --- a/.madrun.js +++ b/.madrun.js @@ -21,15 +21,14 @@ export default { 'start:dev', ]), 'lint:all': () => run('lint:progress'), - 'lint': () => 'redlint fix; putout .', + 'lint': () => 'redlint fix; putout . --rulesdir rules', '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 --fix . && redlint fix`, + 'fix:lint': async () => `putout --rulesdir rules --fix . && redlint fix`, 'lint:stream': () => run('lint', '-f stream'), - 'test': () => [testEnv, `tape '{test}/**/*.js' '{bin,client,static,common,server}/**/*.spec.js' -f fail`], - 'test:e2e': () => `tape 'test-e2e/**/*.js'`, + 'test': () => [testEnv, `tape 'test/**/*.js' '{bin,client,static,common,server}/**/*.spec.js' -f fail`], 'test:client': () => `tape 'test/client/**/*.js'`, 'test:server': () => `tape 'test/**/*.js' 'server/**/*.spec.js' 'common/**/*.spec.js'`, 'wisdom': async () => await run(['lint:all', 'build', 'test'], null, { @@ -39,7 +38,7 @@ export default { 'coverage': async () => [testEnv, `c8 ${await cutEnv('test')}`], 'coverage:report': () => 'c8 report --reporter=lcov', 'report': () => 'c8 report --reporter=lcov', - '6to5': () => [buildEnv, 'rspack build --config rspack.config.js'], + '6to5': () => [buildEnv, 'webpack --progress'], '6to5:client': () => run('6to5', '--mode production'), '6to5:client:dev': async () => await run('6to5', '--mode development', { NODE_ENV: 'development', diff --git a/.npmignore b/.npmignore index b83b5923..62747573 100644 --- a/.npmignore +++ b/.npmignore @@ -4,23 +4,26 @@ *.ai *.cdr *.eps +*.config.* *.log -*.lock +*.loc .* manifest.yml docker-compose.yml +yarn.lock now.json app.json bower.json +manifest.yml deno.json bin/release.* img/logo/cloudcmd-hq.png webpack.config.js docker -test* +test fixture fixture-* coverage diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 00000000..1efe0ac6 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +v20.15.1 diff --git a/.nycrc.json b/.nycrc.json index 1fe174f3..36d6b4cb 100644 --- a/.nycrc.json +++ b/.nycrc.json @@ -1,5 +1,5 @@ { - "checkCoverage": false, + "check-coverage": false, "all": false, "exclude": [ "**/*.spec.js", diff --git a/.rspack/css.js b/.webpack/css.js similarity index 53% rename from .rspack/css.js rename to .webpack/css.js index 10d28250..311290cc 100644 --- a/.rspack/css.js +++ b/.webpack/css.js @@ -1,18 +1,19 @@ import {env} from 'node:process'; -import {rspack} from '@rspack/core'; +import CssMinimizerPlugin from 'css-minimizer-webpack-plugin'; +import MiniCssExtractPlugin from 'mini-css-extract-plugin'; -const {CssExtractRspackPlugin} = rspack; const isDev = env.NODE_ENV === 'development'; +const clean = (a) => a.filter(Boolean); -const plugins = [ - new CssExtractRspackPlugin({ +const plugins = clean([ + new MiniCssExtractPlugin({ filename: '[name].css', }), -]; +]); const rules = [{ test: /\.css$/i, - use: [CssExtractRspackPlugin.loader, { + use: [MiniCssExtractPlugin.loader, { loader: 'css-loader', options: { url: true, @@ -32,7 +33,13 @@ export default { optimization: { minimize: !isDev, minimizer: [ - new rspack.LightningCssMinimizerRspackPlugin(), + new CssMinimizerPlugin({ + minimizerOptions: { + preset: ['default', { + svgo: false, + }], + }, + }), ], }, }; diff --git a/.rspack/html.js b/.webpack/html.js similarity index 100% rename from .rspack/html.js rename to .webpack/html.js diff --git a/.rspack/js.js b/.webpack/js.js similarity index 81% rename from .rspack/js.js rename to .webpack/js.js index 251d85f6..fdc88556 100644 --- a/.rspack/js.js +++ b/.webpack/js.js @@ -1,15 +1,12 @@ import {resolve, sep} from 'node:path'; -import {fileURLToPath} from 'node:url'; import {env} from 'node:process'; -import {rspack} from '@rspack/core'; - -const resolveModule = (a) => fileURLToPath(import.meta.resolve(a)); +import webpack from 'webpack'; +import WebpackBar from 'webpackbar'; const { EnvironmentPlugin, NormalModuleReplacementPlugin, - ProvidePlugin, -} = rspack; +} = webpack; const modules = './modules'; const dirModules = './client/modules'; @@ -25,31 +22,27 @@ const dist = resolve(rootDir, 'dist'); const distDev = resolve(rootDir, 'dist-dev'); const devtool = isDev ? 'eval' : 'source-map'; -const noParse = (a) => a.endsWith('.spec.js'); +const notEmpty = (a) => a; +const clean = (array) => array.filter(notEmpty); -// 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', - }, +const noParse = (a) => a.endsWith('.spec.js'); +const options = { + babelrc: true, +}; + +const rules = clean([ + !isDev && { + test: /\.[mc]?js$/, + exclude: /node_modules/, + loader: 'babel-loader', }, -}]; + isDev && { + test: /\.[mc]?js$/, + exclude: /node_modules/, + loader: 'babel-loader', + options, + }, +]); const plugins = [ new NormalModuleReplacementPlugin(/^node:/, (resource) => { @@ -59,7 +52,8 @@ const plugins = [ new EnvironmentPlugin({ NODE_ENV, }), - new ProvidePlugin({ + new WebpackBar(), + new webpack.ProvidePlugin({ process: 'process/browser', }), ]; @@ -105,9 +99,9 @@ export default { 'node:path': 'path', }, fallback: { - path: resolveModule('path-browserify'), - process: resolveModule('process/browser'), - util: resolveModule('util'), + path: import.meta.resolve('path-browserify'), + process: import.meta.resolve('process/browser'), + util: import.meta.resolve('util'), }, }, devtool, @@ -162,7 +156,7 @@ export default { }, plugins, performance: { - maxEntrypointSize: 800_000, + maxEntrypointSize: 600_000, maxAssetSize: 600_000, }, }; diff --git a/ChangeLog b/ChangeLog index c5e0fbca..9e8b8bbe 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,275 +1,3 @@ -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: -- 40ecef5e cloudcmd: ratelimit: X-Forwarded-For (#437) - -2026.05.26, v19.18.0 - -fix: -- 161bede8 client: menu: @putout/bundle@5.5. -- 447d990f cloudcmd: server: rest: sendError - -feature: -- 5d9628ce cloudcmd: rate limit: add support (#437) -- 2be395e6 cloudcmd: get rid of manifest: 401 basic auth - -2026.05.17, v19.17.0 - -fix: -- d96f6c46 css: query: hide time on mobile -- e7c55e05 docker: io: XDG_CONFIG_HOME - -feature: -- ae1ca0f8 cloudcmd: cssnano-preset-default v8.0.1 -- 94d5096a style: owner, mode: improve -- 807f8346 cloudcmd: superc8 v12.6.0 -- 67a95722 docker: io: cline: add -- 68eacb91 qword: add -- 4acd294b docker: io: tmux -- 05c80043 cloudcmd: @supertape/loader-css v1.0.0 -- 800a6545 cloudcmd: eslint-plugin-n v18.0.1 -- a985bb36 cloudcmd: supertape v13.0.0 -- 4315ec61 docker: io XDG_CONFIG_HOME: /etc -> /usr/local/etc - -2026.05.03, v19.16.0 - -feature: -- d82d0335 client: vim: rr for rename file -- acfa27cf docker: io: nvchad: add -- 14f009f8 docker: io: bash-completion: add -- c6c60146 docker: io: f4 -- 9a6b8935 docker: io: GOPATH -- a9bc22ce cloudcmd: operation: rm useless checks -- 36bacfe9 cloudcmd: client: key: vim: cc, mm -- 591da25c cloudcmd: ponse v8.0.0 -- 1e1b073c docker: io: /usr/src/cloudcmd -> /usr/local/share/cloudcmd - -2026.04.28, v19.15.0 - -feature: -- 9d97343b cloudcmd: operation: rm useless checks -- 46a88cfa cloudcmd: client: key: vim: cc, mm - -2026.04.28, v19.14.0 - -feature: -- 36a8b641 cloudcmd: ponse v8.0.0 -- 1c263c18 docker: io: /usr/src/cloudcmd -> /usr/local/share/cloudcmd -- ea7b828c docker: io: ubuntu: resolute -- cd6c11ac docker: io: far2l -- c4beeec6 docker: io: gdu: add - -2026.04.21, v19.13.1 - -feature: -- d5cd11e8 cloudcmd: montag v2.0.1 -- dc94d2db docker: io: add latest git version -- 1637beee docker: io: /usr/local/src -> /usr/local/share -- b1bc4e73 Docker: io: pv -- 7c0dca60 docker: io: git: master - -2026.04.15, v19.13.0 - -fix: -- 48693d9e docker: io: XDG_CACHE_HOME - -feature: -- 4b2395c5 cloudcmd: Ctrl + L: logout -- 977a8aaa docker: io: strace: add -- 7d0098fd docker: io: XDG_CACHE_HOME=/tmp/cache -- a89e901b actions: docker: io: add - -2026.04.12, v19.12.5 - -feature: -- 1cfc1a6f docker: io: only amd64 - -2026.04.12, v19.12.4 - -feature: -- 3c2b5658 docker: io: haskell -- d37f8cd3 docker: io: palabra: node - -2026.04.12, v19.12.3 - -feature: -- cb6cabd4 docker: io: PALABRA_DIR - -2026.04.11, v19.12.2 - -feature: -- 2ce11fa2 docker: io: get rid of haskell: to slow install -- aa741232 docker: io: rizin, yara - -2026.04.09, v19.12.1 - -feature: -- 5bd03215 docker: io: add PREFIX - -2026.04.09, v19.12.0 - -feature: -- 13b15b7b docker: io: palabra - -2026.04.07, v19.11.14 - -feature: -- a31beab0 docker: io: rustup env - -2026.04.07, v19.11.13 - -feature: -- 6a08479a docker: io: BUN_INSTALL - -2026.04.06, v19.11.12 - -feature: -- 46c65554 docker: io: npm_config_cache - -2026.04.06, v19.11.11 - -feature: -- ae2ce388 docker: io: $DENO_DIR - -2026.04.06, v19.11.10 - -feature: -- 12ea14ac docker: io: nvm: node - -2026.04.06, v19.11.9 - -feature: -- ea96d13f docker: io: net-tools - -2026.04.06, v19.11.8 - -feature: -- 5c08565f docker: io: debian -> ubuntu - -2026.04.05, v19.11.7 - -feature: -- 36cdef37 docker: io: bookworm - -2026.04.05, v19.11.6 - -feature: -- 845f9bd1 cloudcmd: gritty v10.2.0 - -2026.04.05, v19.11.5 - -feature: -- f9c513cc docker: io: hexyl: add - -2026.04.04, v19.11.4 - -feature: -- e7347d25 docker: io: neovim: apt -> github - -2026.04.04, v19.11.3 - -feature: -- 9970ff76 docker: io: btop -- 4395a471 docker: io: $PATH: add $HOME/.local/bin - -2026.04.04, v19.11.2 - -feature: -- c40ae8e4 docker: io: htop -- f463c5c5 docker: io: aptitude: add -- d1032f09 docker: io: remove unused - -2026.04.04, v19.11.1 - -feature: -- 7787bfc2 cloudcmd: user-menu: runFromCDN - -2026.04.04, v19.11.0 - -feature: -- 32f89d38 cloudcmd: user-menu: root - -2026.04.04, v19.10.2 - -feature: -- df4fb517 cloudcmd: aleman v2.0.1 - -2026.04.03, v19.10.1 - -feature: -- ceb7ef4f docker: io: keep /var/lib/apt/lists - -2026.04.02, v19.10.0 - -fix: -- d12e7bd0 distribute: fix event listener leak on socket disconnect (#462) - -2026.04.02, v19.9.24 - -feature: -- c4d26c6a docker: io: apt-get upgrade -- 9ddb8c29 docker: io: get rid of nix -- e4d7d441 docker: io: nix - -2026.04.01, v19.9.23 - -feature: -- 5a3413ce docker: io: nix - -2026.03.31, v19.9.22 - -feature: -- b4345ed4 docker: io: DENO_DIR - -2026.03.31, v19.9.21 - -fix: -- 5c6a9a95 css: columns: name: 40% -> 35% - -2026.03.30, v19.9.20 - -feature: -- cd0b5554 iocmd: io: nvm - -2026.03.30, v19.9.19 - -feature: -- 6a52b11e docker: io: go, rust - -2026.03.30, v19.9.18 - -feature: -- 738059f2 docker: io: fzf -- 3fc8932f docker: io: less, el_GR - -2026.03.30, v19.9.17 - -feature: -- cf424d6c docker: io: command-not-found update - -2026.03.29, v19.9.16 - -feature: -- 19347a2b docker: io: add command-not-found - -2026.03.29, v19.9.15 - -feature: -- ee170552 docker: io: ubuntu -- e04c4594 docker: io: net-tools: add - 2026.03.29, v19.9.14 fix: diff --git a/HELP.md b/HELP.md index 2967f0d0..cb2d9dc4 100644 --- a/HELP.md +++ b/HELP.md @@ -1,6 +1,6 @@ -# Cloud Commander v19.19.1 +# Cloud Commander v19.9.14 -### [Main][MainURL] [Blog][BlogURL] [Demo][DemoURL] [Deploy](#deploy) +### [Main][MainURL] [Blog][BlogURL] [Support][SupportURL] [Demo][DemoURL] [MainURL]: https://cloudcmd.io "Main" [BlogURL]: https://blog.cloudcmd.io "Blog" @@ -9,14 +9,10 @@ [DWORD]: https://github.com/cloudcmd/dword "Editor based on CodeMirror" [EDWARD]: https://github.com/cloudcmd/edward "Editor based on Ace" [DEEPWORD]: https://github.com/cloudcmd/deepword "Editor based on Monaco" -[QWORD]: https://github.com/cloudcmd/qword "Editor based on CodeMirror 6" [EDWARD_KEYS]: https://github.com/cloudcmd/edward/#hot-keys "Edward Hot keys" [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") @@ -30,7 +26,7 @@ - Server works on **Windows**, **Linux**, **Mac OS** and **Android** (with help of [Termux][TERMUX]). - Can be used local or remotely. - Adapts to screen size. -- **3 built-in editors** with support of **syntax highlighting**: [Dword][DWORD], [Edward][EDWARD] [Deepword][DEEPWORD] and [Qword][QWORD]. +- **3 built-in editors** with support of **syntax highlighting**: [Dword][DWORD], [Edward][EDWARD] and [Deepword][DEEPWORD]. - [Console](https://github.com/cloudcmd/console "Console") with support of the default OS command line. - Written in **JavaScript/Node.js**. - Built-in archives pack: **zip** and **tar.gz**. @@ -195,7 +191,6 @@ Then, start the server again with `cloudcmd` and reload the page. | `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 @@ -218,27 +213,24 @@ Then, start the server again with `cloudcmd` and reload the page. When the `--vim` option is provided, or the configuration parameter `vim` is set, the following hotkeys become available: -| Key |Operation -|:------------|:-------------------------------------------- -| `j` | navigate to next file -| `k` | navigate to previous file -| `dd` | remove current file -| `G` or `$` | navigate to bottom file -| `gg` or `^` | navigate to top file -| `v` | visual mode -| `y` | copy (selected in visual mode files) -| `p` | paste files -| `Esc` | unselect all -| `/` | find file in current directory -| `n` | navigate to next found file -| `N` | navigate to previous found file -| `md` | make directory -| `mf` | make file -| `tt` | show terminal -| `e` | edit file -| `cc` | copy -| `mm` | move -| `rr` | rename file +|Key |Operation +|:----------------------|:-------------------------------------------- +| `j` | navigate to next file +| `k` | navigate to previous file +| `dd` | remove current file +| `G` or `$` | navigate to bottom file +| `gg` or `^` | navigate to top file +| `v` | visual mode +| `y` | copy (selected in visual mode files) +| `p` | paste files +| `Esc` | unselect all +| `/` | find file in current directory +| `n` | navigate to next found file +| `N` | navigate to previous found file +| `md` | make directory +| `mf` | make file +| `tt` | show terminal +| `e` | edit file Commands can be joined, for example: @@ -933,9 +925,9 @@ ln -s ./sites-available/io.cloudcmd.io ./sites-enabled ## Deploy -`Cloud Commander` can be easily deployed to [InstaPods][DeployInstaPodsURL]. After deploy you receive email with credentials. +`Cloud Commander` can be easily deployed to [Heroku](https://heroku.com/deploy?template=https://github.com/coderaiser/cloudcmd "Deploy to Heroku"). -[![Deploy on InstaPods](https://instapods.com/deploy-button.svg)](https://app.instapods.com/dashboard/pods/create?app=cloudcmd&ref=cloudcmd) +[![Deploy to Heroku](https://www.herokucdn.com/deploy/button.png "Deploy to Heroku")](https://heroku.com/deploy?template=https://github.com/coderaiser/cloudcmd) ## Docker @@ -1122,50 +1114,6 @@ 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)** -- *2026.05.03*, **[v19.16.0](//github.com/coderaiser/cloudcmd/releases/tag/v19.16.0)** -- *2026.04.28*, **[v19.15.0](//github.com/coderaiser/cloudcmd/releases/tag/v19.15.0)** -- *2026.04.28*, **[v19.14.0](//github.com/coderaiser/cloudcmd/releases/tag/v19.14.0)** -- *2026.04.21*, **[v19.13.1](//github.com/coderaiser/cloudcmd/releases/tag/v19.13.1)** -- *2026.04.15*, **[v19.13.0](//github.com/coderaiser/cloudcmd/releases/tag/v19.13.0)** -- *2026.04.12*, **[v19.12.5](//github.com/coderaiser/cloudcmd/releases/tag/v19.12.5)** -- *2026.04.12*, **[v19.12.4](//github.com/coderaiser/cloudcmd/releases/tag/v19.12.4)** -- *2026.04.12*, **[v19.12.3](//github.com/coderaiser/cloudcmd/releases/tag/v19.12.3)** -- *2026.04.11*, **[v19.12.2](//github.com/coderaiser/cloudcmd/releases/tag/v19.12.2)** -- *2026.04.09*, **[v19.12.1](//github.com/coderaiser/cloudcmd/releases/tag/v19.12.1)** -- *2026.04.09*, **[v19.12.0](//github.com/coderaiser/cloudcmd/releases/tag/v19.12.0)** -- *2026.04.07*, **[v19.11.14](//github.com/coderaiser/cloudcmd/releases/tag/v19.11.14)** -- *2026.04.07*, **[v19.11.13](//github.com/coderaiser/cloudcmd/releases/tag/v19.11.13)** -- *2026.04.06*, **[v19.11.12](//github.com/coderaiser/cloudcmd/releases/tag/v19.11.12)** -- *2026.04.06*, **[v19.11.11](//github.com/coderaiser/cloudcmd/releases/tag/v19.11.11)** -- *2026.04.06*, **[v19.11.10](//github.com/coderaiser/cloudcmd/releases/tag/v19.11.10)** -- *2026.04.06*, **[v19.11.9](//github.com/coderaiser/cloudcmd/releases/tag/v19.11.9)** -- *2026.04.06*, **[v19.11.8](//github.com/coderaiser/cloudcmd/releases/tag/v19.11.8)** -- *2026.04.05*, **[v19.11.7](//github.com/coderaiser/cloudcmd/releases/tag/v19.11.7)** -- *2026.04.05*, **[v19.11.6](//github.com/coderaiser/cloudcmd/releases/tag/v19.11.6)** -- *2026.04.05*, **[v19.11.5](//github.com/coderaiser/cloudcmd/releases/tag/v19.11.5)** -- *2026.04.04*, **[v19.11.4](//github.com/coderaiser/cloudcmd/releases/tag/v19.11.4)** -- *2026.04.04*, **[v19.11.3](//github.com/coderaiser/cloudcmd/releases/tag/v19.11.3)** -- *2026.04.04*, **[v19.11.2](//github.com/coderaiser/cloudcmd/releases/tag/v19.11.2)** -- *2026.04.04*, **[v19.11.1](//github.com/coderaiser/cloudcmd/releases/tag/v19.11.1)** -- *2026.04.04*, **[v19.11.0](//github.com/coderaiser/cloudcmd/releases/tag/v19.11.0)** -- *2026.04.04*, **[v19.10.2](//github.com/coderaiser/cloudcmd/releases/tag/v19.10.2)** -- *2026.04.03*, **[v19.10.1](//github.com/coderaiser/cloudcmd/releases/tag/v19.10.1)** -- *2026.04.02*, **[v19.10.0](//github.com/coderaiser/cloudcmd/releases/tag/v19.10.0)** -- *2026.04.02*, **[v19.9.24](//github.com/coderaiser/cloudcmd/releases/tag/v19.9.24)** -- *2026.04.01*, **[v19.9.23](//github.com/coderaiser/cloudcmd/releases/tag/v19.9.23)** -- *2026.03.31*, **[v19.9.22](//github.com/coderaiser/cloudcmd/releases/tag/v19.9.22)** -- *2026.03.31*, **[v19.9.21](//github.com/coderaiser/cloudcmd/releases/tag/v19.9.21)** -- *2026.03.30*, **[v19.9.20](//github.com/coderaiser/cloudcmd/releases/tag/v19.9.20)** -- *2026.03.30*, **[v19.9.19](//github.com/coderaiser/cloudcmd/releases/tag/v19.9.19)** -- *2026.03.30*, **[v19.9.18](//github.com/coderaiser/cloudcmd/releases/tag/v19.9.18)** -- *2026.03.30*, **[v19.9.17](//github.com/coderaiser/cloudcmd/releases/tag/v19.9.17)** -- *2026.03.29*, **[v19.9.16](//github.com/coderaiser/cloudcmd/releases/tag/v19.9.16)** -- *2026.03.29*, **[v19.9.15](//github.com/coderaiser/cloudcmd/releases/tag/v19.9.15)** - *2026.03.29*, **[v19.9.14](//github.com/coderaiser/cloudcmd/releases/tag/v19.9.14)** - *2026.03.29*, **[v19.9.13](//github.com/coderaiser/cloudcmd/releases/tag/v19.9.13)** - *2026.03.29*, **[v19.9.12](//github.com/coderaiser/cloudcmd/releases/tag/v19.9.12)** diff --git a/README.md b/README.md index dfe8d8c6..a7dc6a2a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Cloud Commander v19.19.1 [![Build Status][BuildStatusIMGURL]][BuildStatusURL] [![Codacy][CodacyIMG]][CodacyURL] [![Gitter][GitterIMGURL]][GitterURL] [![Deploy on InstaPods][DeployInstaPodsIMG]][DeployInstaPodsURL] +# Cloud Commander v19.9.14 [![Build Status][BuildStatusIMGURL]][BuildStatusURL] [![Codacy][CodacyIMG]][CodacyURL] [![Gitter][GitterIMGURL]][GitterURL] ### [Main][MainURL] [Blog][BlogURL] [Support][SupportURL] [Demo][DemoURL] @@ -15,8 +15,6 @@ [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. diff --git a/bin/cloudcmd.js b/bin/cloudcmd.js index 6ca6716c..bdf88379 100755 --- a/bin/cloudcmd.js +++ b/bin/cloudcmd.js @@ -115,7 +115,7 @@ const yargsOptions = { 'columns': env.parse('columns') || config('columns') || '', 'theme': env.parse('theme') || config('theme') || '', 'vim': choose(env.bool('vim'), config('vim')), - 'log': choose(env.bool('log'), config('log')), + 'log': config('log'), 'import-url': env.parse('import_url') || config('importUrl'), 'import-listen': choose(env.bool('import_listen'), config('importListen')), @@ -264,8 +264,7 @@ function validateRoot(root, config) { if (root === '/') return; - if (config('log')) - console.log(`root: ${root}`); + console.log(`root: ${root}`); } async function getPassword(password) { diff --git a/client/cloudcmd.js b/client/cloudcmd.js index f2416fd4..688d97b7 100644 --- a/client/cloudcmd.js +++ b/client/cloudcmd.js @@ -36,7 +36,7 @@ async function init(config) { if (globalThis.CloudCmd.config('menu') === 'aleman') setTimeout(() => { import('https://esm.sh/@putout/processor-html'); - import('https://esm.sh/@putout/bundle@5.5'); + import('https://esm.sh/@putout/bundle'); }, 100); } diff --git a/client/dom/current-file.spec.js b/client/dom/current-file.spec.js index 85b18a29..6bf0509b 100644 --- a/client/dom/current-file.spec.js +++ b/client/dom/current-file.spec.js @@ -19,13 +19,14 @@ 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(); }); @@ -40,13 +41,14 @@ 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(); }); @@ -74,10 +76,11 @@ 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(); }); @@ -93,12 +96,14 @@ 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(); }); diff --git a/client/dom/dom-tree.spec.js b/client/dom/dom-tree.spec.js index 53b376c7..13f56f6d 100644 --- a/client/dom/dom-tree.spec.js +++ b/client/dom/dom-tree.spec.js @@ -1,16 +1,7 @@ -import {test, stub} from 'supertape'; +import test from 'supertape'; import {create} from 'auto-globals'; import {tryCatch} from 'try-catch'; -import { - isContainClass, - getByTag, - getById, - getByClass, - getByDataName, - getByClassAll, - hide, - show, -} from './dom-tree.js'; +import {isContainClass} from './dom-tree.js'; test('dom: isContainClass: no element', (t) => { const [e] = tryCatch(isContainClass); @@ -47,106 +38,3 @@ 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(); -}); diff --git a/client/dom/index.js b/client/dom/index.js index a82b6f72..f6ce6bc7 100644 --- a/client/dom/index.js +++ b/client/dom/index.js @@ -442,7 +442,11 @@ export function getFilenames(files) { if (name === '..') allFiles.shift(); - return allFiles.map(DOM.getCurrentName); + const names = allFiles.map((current) => { + return DOM.getCurrentName(current); + }); + + return names; } /** diff --git a/client/dom/load-remote.js b/client/dom/load-remote.js index 5be8e327..43af0cf5 100644 --- a/client/dom/load-remote.js +++ b/client/dom/load-remote.js @@ -33,19 +33,13 @@ export const loadRemote = callbackify(async (name, options) => { local = [module.local]; } - const localURL = []; + const localURL = local.map((url) => prefix + url); - for (const url of local) { - localURL.push(prefix + url); - } - - const remoteURL = []; - - for (const tmpl of remoteTmpls) { - remoteURL.push(rendy(tmpl, { + const remoteURL = remoteTmpls.map((tmpl) => { + return rendy(tmpl, { version, - })); - } + }); + }); if (online) { const [e] = await tryToCatch(load.parallel, remoteURL); diff --git a/client/key/binder.spec.js b/client/key/binder.spec.js deleted file mode 100644 index f9d54fdd..00000000 --- a/client/key/binder.spec.js +++ /dev/null @@ -1,28 +0,0 @@ -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(); -}); diff --git a/client/key/index.js b/client/key/index.js index 283a7f11..31f659bc 100644 --- a/client/key/index.js +++ b/client/key/index.js @@ -442,14 +442,6 @@ async function _switchKey(event) { break; - case KEY.L: - if (ctrlMeta && shift) { - CloudCmd.logOut(); - event.preventDefault(); - } - - break; - case KEY.M: if (ctrlMeta) { if (config('vim')) diff --git a/client/key/key.js b/client/key/key.js index 6772e34c..66120e74 100644 --- a/client/key/key.js +++ b/client/key/key.js @@ -22,7 +22,6 @@ export const D = 68; export const G = 71; export const J = 74; export const K = 75; -export const L = 76; export const M = 77; export const O = 79; export const P = 80; diff --git a/client/key/vim/find.spec.js b/client/key/vim/find.spec.js index 191a3652..59c9a249 100644 --- a/client/key/vim/find.spec.js +++ b/client/key/vim/find.spec.js @@ -1,12 +1,6 @@ import test from 'supertape'; import {getDOM} from './globals.fixture.js'; -import { - _next, - _previous, - find, - findNext, - findPrevious, -} from './find.js'; +import {_next, _previous} from './find.js'; globalThis.DOM = getDOM(); @@ -17,39 +11,9 @@ 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(); -}); diff --git a/client/key/vim/index.js b/client/key/vim/index.js index 981f6f19..5f67f4e4 100644 --- a/client/key/vim/index.js +++ b/client/key/vim/index.js @@ -1,3 +1,5 @@ +/* global CloudCmd */ +/* global DOM */ import vim from './vim.js'; import * as finder from './find.js'; import { @@ -33,13 +35,12 @@ const getOperations = (event, deps) => { prompt = globalThis.DOM.Dialog.prompt, preventDefault = event?.preventDefault?.bind(event), stopImmediatePropagation = event?.preventDefault?.bind(event), - promptNewFile = globalThis.DOM.promptNewFile, + promptNewFile = DOM.promptNewFile, toggleSelectedFile, Buffer = {}, createFindNext = _createFindNext, createFindPrevious = _createFindPrevious, createMakeFile = _createMakeFile, - renameCurrent, } = deps; return { @@ -55,26 +56,15 @@ const getOperations = (event, deps) => { setCurrentByName, }), escape: unselectFiles, - rename: () => { - event.preventDefault(); - renameCurrent(); - }, + remove: () => { Operation.show('delete'); }, - operationCopy: () => { - event.preventDefault(); - Operation.show('copy'); - }, - operationMove: () => { - event.preventDefault(); - Operation.show('move'); - }, makeDirectory: () => { event.stopImmediatePropagation(); event.preventDefault(); - globalThis.DOM.promptNewDir(); + DOM.promptNewDir(); }, terminal: () => { diff --git a/client/key/vim/index.spec.js b/client/key/vim/index.spec.js index 231c6577..b31e3d60 100644 --- a/client/key/vim/index.spec.js +++ b/client/key/vim/index.spec.js @@ -1,7 +1,6 @@ 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(); @@ -658,99 +657,3 @@ test('cloudcmd: client: vim: edit', async (t) => { t.calledWithNoArgs(CloudCmd.EditFileVim.show); t.end(); }); - -test('cloudcmd: client: vim: rename', async (t) => { - const DOM = getDOM(); - const renameCurrent = stub(); - - assign(DOM, { - renameCurrent, - }); - - const event = { - preventDefault: stub(), - }; - - await vim('rr', event, DOM); - - 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(); -}); diff --git a/client/key/vim/vim.js b/client/key/vim/vim.js index 95a27caf..0d0404c3 100644 --- a/client/key/vim/vim.js +++ b/client/key/vim/vim.js @@ -40,9 +40,6 @@ export default (key, operations = {}) => { makeDirectory = noop, terminal = noop, edit = noop, - operationCopy = noop, - operationMove = noop, - rename = noop, } = operations; if (key === 'Enter') @@ -114,26 +111,11 @@ export default (key, operations = {}) => { return end(); } - if (value === 'cc') { - operationCopy(); - return end(); - } - - if (value === 'mm') { - operationMove(); - return end(); - } - if (value === 'mf') { makeFile(); return end(); } - if (value === 'rr') { - rename(); - return end(); - } - if (key === 'd' && (visual() || prevStore === 'd')) { stopVisual(); remove(); diff --git a/client/key/vim/vim.spec.js b/client/key/vim/vim.spec.js index 02fd52d4..88cdd89e 100644 --- a/client/key/vim/vim.spec.js +++ b/client/key/vim/vim.spec.js @@ -43,28 +43,6 @@ test('vim: ^', (t) => { t.end(); }); -test('vim: cc', (t) => { - const operationCopy = stub(); - - vim('cc', { - operationCopy, - }); - - t.calledWithNoArgs(operationCopy); - t.end(); -}); - -test('vim: mm', (t) => { - const operationMove = stub(); - - vim('mm', { - operationMove, - }); - - t.calledWithNoArgs(operationMove); - t.end(); -}); - test('vim: w', (t) => { const moveNext = stub(); diff --git a/client/modules/config/input.spec.js b/client/modules/config/input.spec.js index 9d0866cd..537c1632 100644 --- a/client/modules/config/input.spec.js +++ b/client/modules/config/input.spec.js @@ -1,10 +1,5 @@ -import {test, stub} from 'supertape'; -import { - convert, - getName, - getValue, - setValue, -} from './input.js'; +import {test} from 'supertape'; +import {convert} from './input.js'; test('cloudcmd: client: config: input: convert', (t) => { const result = convert({ @@ -18,123 +13,3 @@ 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(); -}); diff --git a/client/modules/edit.js b/client/modules/edit.js index 593a11cb..1b726284 100644 --- a/client/modules/edit.js +++ b/client/modules/edit.js @@ -1,5 +1,5 @@ /* global CloudCmd */ -import {montag} from 'montag'; +import montag from 'montag'; import {promisify} from 'es6-promisify'; import {tryToCatch} from 'try-to-catch'; import createElement from '@cloudcmd/create-element'; diff --git a/client/modules/menu/index.js b/client/modules/menu/index.js index 7235591e..249e755c 100644 --- a/client/modules/menu/index.js +++ b/client/modules/menu/index.js @@ -1,3 +1,4 @@ +/* global CloudCmd, DOM */ import exec from 'execon'; import wrap from 'wraptile'; import createElement from '@cloudcmd/create-element'; @@ -5,7 +6,6 @@ import {getIdBySrc} from '#dom/load'; import * as RESTful from '#dom/rest'; import {FS} from '#common/cloudfunc'; -const {CloudCmd, DOM} = globalThis; const {config, Key} = CloudCmd; const { diff --git a/client/modules/operation/set-listeners.js b/client/modules/operation/set-listeners.js index e54e7d14..431c7791 100644 --- a/client/modules/operation/set-listeners.js +++ b/client/modules/operation/set-listeners.js @@ -15,12 +15,15 @@ export const setListeners = (options) => (emitter) => { } = options; let done; + let lastError; const onAbort = wraptile(({emitter, operation}) => { emitter.abort(); const msg = `${operation} aborted`; + lastError = true; + Dialog.alert(msg, { cancel: false, }); @@ -37,10 +40,13 @@ export const setListeners = (options) => (emitter) => { operation, })); + let noProgress = true; + const listeners = { progress: (value) => { done = value === 100; progress.setProgress(value); + noProgress = false; }, end: () => { @@ -48,10 +54,13 @@ export const setListeners = (options) => (emitter) => { forEachKey(removeListener, listeners); progress.remove(); - callback(); + if (lastError || done || noProgress) + callback(); }, error: async (error) => { + lastError = error; + if (noContinue) { listeners.end(error); Dialog.alert(error); diff --git a/client/modules/terminal-run.js b/client/modules/terminal-run.js index 029415ea..c7dbf9c2 100644 --- a/client/modules/terminal-run.js +++ b/client/modules/terminal-run.js @@ -51,7 +51,7 @@ export async function init() { } export async function show(options = {}) { - return await runTerminal(options); + await runTerminal(options); } const runTerminal = promisify((options, fn) => { diff --git a/client/modules/view/index.spec.js b/client/modules/view/index.spec.js index abfbe3f4..8d184548 100644 --- a/client/modules/view/index.spec.js +++ b/client/modules/view/index.spec.js @@ -31,10 +31,8 @@ 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(result, expected); + t.equal(typeof config, 'object'); t.end(); }); diff --git a/client/modules/view/types.js b/client/modules/view/types.js index 320a4bc8..2e001fe1 100644 --- a/client/modules/view/types.js +++ b/client/modules/view/types.js @@ -1,7 +1,7 @@ import {extname} from 'node:path'; import currify from 'currify'; -export const isAudio = (name) => /\.(mp3|ogg|m4a|flac)$/i.test(name); +export const isAudio = (name) => /\.(mp3|ogg|m4a)$/i.test(name); const testRegExp = currify((name, reg) => reg.test(name)); const getRegExp = (ext) => RegExp(`\\.${ext}$`, 'i'); diff --git a/client/modules/view/types.spec.js b/client/modules/view/types.spec.js index f0542c18..7ed159f4 100644 --- a/client/modules/view/types.spec.js +++ b/client/modules/view/types.spec.js @@ -8,13 +8,6 @@ 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'); diff --git a/client/sw/register.spec.js b/client/sw/register.spec.js index 52699f0b..47420ba7 100644 --- a/client/sw/register.spec.js +++ b/client/sw/register.spec.js @@ -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', }); diff --git a/common/cloudfunc.spec.js b/common/cloudfunc.spec.js index b679feeb..51408a87 100644 --- a/common/cloudfunc.spec.js +++ b/common/cloudfunc.spec.js @@ -1,18 +1,13 @@ import {readFileSync} from 'node:fs'; import test from 'supertape'; -import {montag} from 'montag'; +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; @@ -226,97 +221,6 @@ 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/', diff --git a/common/util.spec.js b/common/util.spec.js index 5a19a8be..6a32848c 100644 --- a/common/util.spec.js +++ b/common/util.spec.js @@ -5,8 +5,6 @@ import { getRegExp, escapeRegExp, getExt, - time, - timeEnd, } from '#common/util'; test('getExt: no extension', (t) => { @@ -108,31 +106,11 @@ test('util: getRegExp: no', (t) => { }); test('util: escapeRegExp: no str', (t) => { - const result = escapeRegExp(1); - const expected = 1; - - t.equal(result, expected); + t.equal(escapeRegExp(1), 1); t.end(); }); test('util: escapeRegExp', (t) => { - 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.equal(escapeRegExp('#hello'), '\\#hello'); t.end(); }); diff --git a/css/columns/name-size-date-time.css b/css/columns/name-size-date-time.css index f46e63f8..9d6e41ac 100644 --- a/css/columns/name-size-date-time.css +++ b/css/columns/name-size-date-time.css @@ -1,5 +1,5 @@ .name { - width: 35%; + width: 40%; } .size { diff --git a/css/query.css b/css/query.css index 509f1b45..b5843b85 100644 --- a/css/query.css +++ b/css/query.css @@ -161,9 +161,8 @@ .size, .date, .owner, - .time, .mode { - display: none !important; + display: none; } .name { diff --git a/css/style.css b/css/style.css index 10820444..333075ed 100644 --- a/css/style.css +++ b/css/style.css @@ -235,8 +235,7 @@ a:active { .owner { display: inline-block; - width: 12%; - + width: 13%; /* when inline-block * vertical align should be * set top to prevent additional @@ -248,7 +247,7 @@ a:active { .mode { float: right; - width: 22%; + width: 18%; color: var(--column-color); } diff --git a/docker/Dockerfile b/docker/Dockerfile index 7c5d7b64..cf1505a7 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,10 +1,8 @@ FROM node - LABEL maintainer="Coderaiser" LABEL org.opencontainers.image.source="https://github.com/coderaiser/cloudcmd" RUN mkdir -p /usr/src/cloudcmd - WORKDIR /usr/src/cloudcmd COPY package.json /usr/src/cloudcmd/ diff --git a/docker/Dockerfile.alpine b/docker/Dockerfile.alpine index 5e38ab8a..75549fae 100644 --- a/docker/Dockerfile.alpine +++ b/docker/Dockerfile.alpine @@ -1,10 +1,8 @@ FROM node:alpine - LABEL maintainer="Coderaiser" LABEL org.opencontainers.image.source="https://github.com/coderaiser/cloudcmd" RUN mkdir -p /usr/src/cloudcmd - WORKDIR /usr/src/cloudcmd COPY package.json /usr/src/cloudcmd/ @@ -23,7 +21,6 @@ RUN apk update && \ COPY . /usr/src/cloudcmd WORKDIR / - ENV cloudcmd_terminal=true ENV cloudcmd_terminal_path=gritty ENV cloudcmd_open=false @@ -32,3 +29,4 @@ ENV cloudcmd_vim=true EXPOSE 8000 ENTRYPOINT ["/usr/src/cloudcmd/bin/cloudcmd.js"] + diff --git a/docker/Dockerfile.io b/docker/Dockerfile.io index d5a24459..69fc1b7b 100644 --- a/docker/Dockerfile.io +++ b/docker/Dockerfile.io @@ -1,79 +1,38 @@ -FROM ubuntu:resolute - +FROM node LABEL maintainer="Coderaiser" LABEL org.opencontainers.image.source="https://github.com/coderaiser/cloudcmd" -RUN mkdir -p /usr/local/share/cloudcmd +RUN mkdir -p /usr/src/cloudcmd +WORKDIR /usr/src/cloudcmd -WORKDIR /usr/local/share/cloudcmd +COPY package.json /usr/src/cloudcmd/ -COPY package.json /usr/local/share/cloudcmd/ +ENV DEBIAN_FRONTEND=noninteractive -ENV DEBIAN_FRONTEND=noninteractive \ - NVM_DIR=/usr/local/share/nvm \ - 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 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 && \ - apt-get upgrade -y && \ - apt-get autoremove && \ - apt-get install -y ${UBUNTU_DEPS} ${RUST_DEPES} ${DEPS} && \ - echo "> Install git" && \ - 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 && \ - apt-get autoremove && \ +RUN apt-get update && apt-get upgrade && apt-get autoremove && \ + apt-get install -y ffmpeg netcat-openbsd mc iputils-ping vim neovim sudo locales && \ apt-get clean && \ - echo "> create user" && \ - useradd -m -s /bin/bash -u 1337 instalador && \ - chown -R instalador /usr/local && \ - chown -R instalador /tmp - -USER instalador - -RUN echo "> install bun" && \ - curl https://bun.sh/install | bash && \ - echo "> install npm globals" && \ - bun i ${BUN_DEPS} -g && \ - echo "> install rust go deno bun fasm nvim" && \ - bun ${BUN_INSTALL}/bin/palabra i ${PALABRA_DEPS} && \ - echo "> install node" && \ - . $NVM_DIR/nvm.sh - -USER root - -RUN echo "> remove user" && \ - userdel -r instalador && \ - echo "> install gritty" && \ + npm i wisdom nupdate version-io redrun superc8 \ + supertape madrun redlint putout renamify-cli runny redfork -g && \ + echo "> install bun" && \ + curl -fsSL https://bun.sh/install | bash && \ + mv ~/.bun /usr/local/src/bun && \ + chmod a+rwx /usr/local/src/bun && \ + ln -s /usr/local/src/bun/bin/bun /usr/local/bin/bun && \ + echo "> install deno" && \ + curl -fsSL https://deno.land/install.sh | sh && \ + mv ~/.deno /usr/local/src/deno && \ + chmod a+rwx /usr/local/src/deno && \ + ln -s /usr/local/src/deno/bin/deno /usr/local/bin/deno && \ bun r gritty --omit dev && \ bun i gritty --omit dev && \ bun pm cache rm && \ - echo "> setup cloudcmd" && \ - ln -s /usr/local/share/cloudcmd/bin/cloudcmd.js /usr/local/bin/cloudcmd && \ echo "> setup git" && \ git config --global core.whitespace -trailing-space && \ git config --global pull.rebase true && \ - git config --global init.defaultBranch master && \ echo "> configure bash" && \ echo "alias ls='ls --color=auto'" >> /etc/bash.bashrc && \ echo "alias buni='bun i --no-save'" >> /etc/bash.bashrc && \ - echo "alias bat='batcat'" >> /etc/bash.bashrc && \ - echo ". /usr/local/share/nvm/nvm.sh" >> /etc/bash.bashrc && \ - echo ". /usr/share/bash-completion/completions/git" >> /etc/bash.bashrc && \ echo 'PS1="\[\033[01;32m\]\u@\h\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$ "' >> /etc/bash.bashrc && \ echo "> setup inputrc" && \ echo "set editing-mode vi" >> /etc/inputrc && \ @@ -85,29 +44,23 @@ RUN echo "> remove user" && \ echo "uk_UA.UTF-8 UTF-8" >> /etc/locale.gen && \ echo "es_ES.UTF-8 UTF-8" >> /etc/locale.gen && \ echo "ja_JP.UTF-8 UTF-8" >> /etc/locale.gen && \ - echo "el_GR.UTF-8 UTF-8" >> /etc/locale.gen && \ locale-gen -COPY . /usr/local/share/cloudcmd +COPY . /usr/src/cloudcmd WORKDIR / -ENV cloudcmd_terminal=true \ - cloudcmd_terminal_path=gritty \ - cloudcmd_vim=true \ - cloudcmd_open=false \ - PATH=node_modules/.bin:$PATH \ - PATH=~/.local/bin:$PATH \ - BUN_INSTALL_CACHE_DIR=/tmp/bun-cache \ - DENO_DIR=/tmp/deno-cache \ - LANG=en_US.UTF-8 \ - LANGUAGE=en_US:en \ - LC_ALL=en_US.UTF-8 \ - TERM=xterm-256color \ - XDG_CACHE_HOME=/tmp \ - XDG_DATA_HOME=/usr/local/share \ - XDG_CONFIG_HOME=~/.config +ENV cloudcmd_terminal=true +ENV cloudcmd_terminal_path=gritty +ENV cloudcmd_open=false +ENV PATH=node_modules/.bin:$PATH + +ENV LANG=en_US.UTF-8 +ENV LANGUAGE=en_US:en +ENV LC_ALL=en_US.UTF-8 + +ENV TERM=xterm-256color EXPOSE 8000 -ENTRYPOINT ["/usr/local/share/cloudcmd/bin/cloudcmd.js"] +ENTRYPOINT ["/usr/src/cloudcmd/bin/cloudcmd.js"] diff --git a/docker/Dockerfile.slim b/docker/Dockerfile.slim index c6f273b8..6974b41c 100644 --- a/docker/Dockerfile.slim +++ b/docker/Dockerfile.slim @@ -1,7 +1,6 @@ FROM node:slim AS build RUN mkdir -p /usr/src/cloudcmd/ - WORKDIR /usr/src/cloudcmd COPY package.json /usr/src/cloudcmd/ diff --git a/eslint.config.js b/eslint.config.js index 22add654..b38d3e2e 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,5 +1,6 @@ import {safeAlign} from 'eslint-plugin-putout'; import {defineConfig} from 'eslint/config'; +import n from 'eslint-plugin-n'; import globals from 'globals'; import {matchToFlat} from '@putout/eslint-flat'; @@ -11,15 +12,23 @@ export const match = { 'client/dom/index.*': { 'no-multi-spaces': 'off', }, - 'client/**': { + '{client,static}/**/*.{js,mjs}': { + 'n/no-extraneous-require': 'off', 'n/no-unsupported-features/node-builtins': 'off', }, + 'bin/cloudcmd.js': { + 'no-console': 'off', + }, }; export default defineConfig([ safeAlign, { ignores: ['**/fixture'], rules: { 'key-spacing': 'off', + 'n/prefer-node-protocol': 'error', + }, + plugins: { + n, }, }, { files: ['{client,common,static}/**/*.js'], diff --git a/html/index.html b/html/index.html index d366ed1c..0cfc73c5 100644 --- a/html/index.html +++ b/html/index.html @@ -9,6 +9,7 @@ + @@ -44,7 +45,7 @@