@ai-ecoverse/slicc-man 1.2.2 → 1.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -47,21 +47,21 @@ or without installing, `npx -p @ai-ecoverse/slicc-man scman tree`. The package's
47
47
 
48
48
  slicc-man's `install.sh`, `install.ps1`, `scripts/assets.mjs` and `host/upgrade.mjs` are slicc-open's ([the installer contract](https://github.com/ai-ecoverse/slicc-open#the-installer) that slicc-open, slicc-capture, slicc-audio and slicc-man share), with the names `slicc-man`, `scman` and `SCMAN`.
49
49
 
50
- **Release assets.** semantic-release's `@semantic-release/npm` packs the package into `release/`. `@semantic-release/exec` then runs `node scripts/assets.mjs release <version>`, which renames the tarball to `slicc-man-<version>.tgz`, refuses (exit 1, writing nothing) when the tarball's `package.json` has no `bin` or lacks a file a `bin` entry names, copies `install.sh` and `install.ps1` next to it, and writes `SHA256SUMS`: one `<sha256> <name>` line each for the tarball, `install.sh` and `install.ps1`, in that order. `@semantic-release/github` attaches the four files to the release.
50
+ **Release assets.** semantic-release's `@semantic-release/npm` packs the package into `release/`. `@semantic-release/exec` then runs `node scripts/assets.mjs release <version>`, which refuses (exit 1, leaving `release/` as it was, npm's tarball name included) when the tarball's `package.json` has no `bin` or lacks a file a `bin` entry names, and otherwise renames the tarball to `slicc-man-<version>.tgz`, copies `install.sh` and `install.ps1` next to it, and writes `SHA256SUMS`: one `<sha256> <name>` line each for the tarball, `install.sh` and `install.ps1`, in that order. `@semantic-release/github` attaches the four files to the release.
51
51
 
52
- **Options and exit codes.** `--version VERSION` (or `--version=VERSION`; a leading `v` is dropped) installs that release instead of the latest, `--dir DIR` (or `--dir=DIR`) puts the launcher in `DIR`, and `--help` (or `-h`) prints the usage to stdout and exits 0; `install.ps1` spells them `-Version`, `-Dir` and `-Help`. No environment variable stands in for them. `install.sh` handles its options, `--help` and the usage errors included, before it works out any directory, so they work without `HOME`; `HOME` is needed only for the defaults, and without it, and without both `--dir` and `XDG_DATA_HOME`, it exits 1 with `install.sh: HOME is not set; give --dir and set XDG_DATA_HOME`. `install.sh` exits 0 when it installed, 1 when something failed (a download, a checksum, no Node and no way to get one), with `install.sh: <what>` on stderr, and 2 for an unknown option or a missing value, with the usage on stderr. `install.ps1` exits 0 or 1 the same way when it is run as a file; run through `irm … | iex` or a script block, it prints the error and returns instead of calling `exit`, so it never closes the user's PowerShell.
52
+ **Options and exit codes.** `--version VERSION` (or `--version=VERSION`; a leading `v` is dropped) installs that release instead of the latest, `--dir DIR` (or `--dir=DIR`) puts the launcher in `DIR`, and `--help` (or `-h`) prints the usage to stdout and exits 0; `install.ps1` spells them `-Version`, `-Dir` and `-Help`. No environment variable stands in for them. `install.sh` handles its options, `--help` and the usage errors included, before it works out any directory, so they work without `HOME`; `HOME` is needed only for the defaults, and without it, and without both `--dir` and `XDG_DATA_HOME`, it exits 1 with `install.sh: HOME is not set; give --dir and set XDG_DATA_HOME`. `install.sh` exits 0 when it installed, 1 when something failed (a download, a checksum, an archive that doesn't unpack, no Node and no way to get one), with `install.sh: <what>` on stderr, and 2 for an unknown option or a missing value, with the usage on stderr. `install.ps1` exits 0 or 1 the same way when it is run as a file; run through `irm … | iex` or a script block, it prints the error and returns instead of calling `exit`, so it never closes the user's PowerShell. It sets `$PSNativeCommandUseErrorActionPreference = $false` in its own scope, so a caller's `$true` (pwsh 7.4 and later) can't turn the exit code of `node`, `tar` or `npm` into an error that stops the install. It runs `node --version`, `tar` and `npm` with `$ErrorActionPreference = 'Continue'` in a child scope, because Windows PowerShell 5.1 turns each line a native command writes to a redirected stderr into an error, and under `'Stop'` the first one would end the install before its `install.ps1:` message.
53
53
 
54
- **Node.** The installer uses the first `node` on `PATH` when it is 24 or later, otherwise one it downloaded before, into `<data>/node`. Failing both, it downloads the newest LTS release of 24 or later (or the newest release, when no LTS qualifies) from nodejs.org's `index.json`: `node-<version>-<os>-<arch>.tar.gz`, or `node-<version>-win-<arch>.zip` on Windows, checked against that release's `SHASUMS256.txt`. On musl Linux, where nodejs.org has no build, it says to install Node with the package manager and exits 1.
54
+ **Node.** The installer uses the first `node` on `PATH` when it is 24 or later, otherwise one it downloaded before, into `<data>/node`. Failing both, it downloads the newest LTS release of 24 or later (or the newest release, when no LTS qualifies) from nodejs.org's `index.json`: `node-<version>-<os>-<arch>.tar.gz`, or `node-<version>-win-<arch>.zip` on Windows, checked against that release's `SHASUMS256.txt`. An archive that doesn't unpack exits 1 with `install.sh: could not unpack node-<version>-<os>-<arch>.tar.gz` (or `install.ps1: could not unpack node-<version>-win-<arch>.zip`), and leaves neither its temporary directory nor a partial `<data>/node` behind. On musl Linux, where nodejs.org has no build, it says to install Node with the package manager and exits 1.
55
55
 
56
56
  **Downloads.** `SLICC_INSTALL_BASE_URL` replaces the directory the assets are downloaded from (by default `https://github.com/ai-ecoverse/slicc-man/releases/latest/download`, or `…/releases/download/v<version>` with `--version`), and `SLICC_NODE_DIST_BASE` `https://nodejs.org/dist`, for mirrors and tests. `install.sh` fetches with `curl`, else `wget`, and checks checksums with `sha256sum`, else `shasum -a 256`; `install.ps1` uses `Invoke-WebRequest` and `Get-FileHash`. Without `--version`, the tarball is the `slicc-man-<version>.tgz` named in `SHA256SUMS`. A tarball whose checksum doesn't match is refused before anything is unpacked.
57
57
 
58
- **Files.** The tarball is unpacked (with `tar`, `%SystemRoot%\System32\tar.exe` on Windows) into a temporary directory next to `<data>/<version>`, where `<data>` is `${XDG_DATA_HOME:-~/.local/share}/slicc-man`, or `%LOCALAPPDATA%\slicc-man` on Windows. When its `package.json` has a non-empty `dependencies` or `optionalDependencies`, the installer removes `devDependencies` from that copy of `package.json`, then runs `npm install --omit=dev --ignore-scripts --no-audit --no-fund` there, with the npm that comes with the Node it picked, run by that Node (`<node dir>/../lib/node_modules/npm`, or `node_modules\npm` next to `node.exe`; `install.sh` falls back to `npm` on `PATH`), and npm's output on stderr. Without such dependencies, as slicc-man has none, the step is skipped and needs neither npm nor the network. A failed `npm install`, or a missing npm, exits 1 and removes the temporary directory, so a failed install never leaves a broken version behind. Only then does the directory replace `<data>/<version>`. Other versions stay where they are. Each launcher names its version's entry script, `<data>/<version>/host/scman.mjs`, and the Node it was installed with, by absolute path:
58
+ **Files.** The tarball is unpacked (with `tar`, `%SystemRoot%\System32\tar.exe` on Windows) into a temporary directory next to `<data>/<version>`, where `<data>` is `${XDG_DATA_HOME:-~/.local/share}/slicc-man`, or `%LOCALAPPDATA%\slicc-man` on Windows. A tarball that doesn't unpack exits 1 with `install.sh: could not unpack slicc-man-<version>.tgz` (or `install.ps1: could not unpack slicc-man-<version>.tgz`) and removes the partial directory. When its `package.json` has a non-empty `dependencies` or `optionalDependencies`, the installer removes `devDependencies` from that copy of `package.json`, then runs `npm install --omit=dev --ignore-scripts --no-audit --no-fund` there, with the npm that comes with the Node it picked, run by that Node (`<node dir>/../lib/node_modules/npm`, or `node_modules\npm` next to `node.exe`; `install.sh` falls back to `npm` on `PATH`), and npm's output on stderr. Without such dependencies, as slicc-man has none, the step is skipped and needs neither npm nor the network. A failed `npm install`, or a missing npm, exits 1 and removes the temporary directory, so a failed install never leaves a broken version behind. Only then does the directory replace `<data>/<version>`. Other versions stay where they are. Each launcher names its version's entry script, `<data>/<version>/host/scman.mjs`, and the Node it was installed with, by absolute path:
59
59
 
60
60
  | platform | launcher in `DIR` | what it does |
61
61
  | --- | --- | --- |
62
62
  | macOS, Linux | `scman`, a POSIX sh script | sets and exports `SCMAN_INSTALL_DIR='<DIR>'`, exports `NODE_USE_ENV_PROXY=1` unless it is already set, then `exec '<node>' '<entry>' "$@"`; every path is single-quoted, so no path is ever evaluated |
63
- | Windows | `scman.ps1` (UTF-8 with a BOM) | saves the caller's `$env:SCMAN_INSTALL_DIR` and sets it, sets `$env:NODE_USE_ENV_PROXY = '1'` when it is unset, runs `& '<node.exe>' '<entry>' @scmanArgs` in `try`, restores the old `SCMAN_INSTALL_DIR` in `finally` (or removes it when it was unset) and removes a `NODE_USE_ENV_PROXY` it set, then exits with node's exit code; a `.ps1` runs inside the caller's session, and this keeps the variables from leaking into it. `@scmanArgs` is `@args` with each array argument (`a,b`) flattened into separate arguments, as PowerShell passes them to a program, and each argument as text. When PowerShell passes arguments the legacy way, which drops empty arguments and doesn't escape `"` (Windows PowerShell 5.1 whatever `$PSNativeCommandArgumentPassing` says, `$PSNativeCommandArgumentPassing = 'Legacy'`, or pwsh before 7.3 without the variable; pwsh 7.3 and later without it pass the standard way), each `"` becomes `\"` (doubling the backslashes before it), an empty argument becomes `""`, and on Windows PowerShell an argument with whitespace gets its trailing backslashes doubled, so `node.exe` gets the arguments it was given |
64
- | Windows | `scman.cmd` (UTF-8, code page 65001 while it runs) | sets `SCMAN_INSTALL_DIR` and, when it is undefined, `NODE_USE_ENV_PROXY=1` (both local through `setlocal`), then runs `"<node.exe>" "<entry>" %* & call :done %%ERRORLEVEL%% & call exit /b %%ERRORLEVEL%%` as one line, so a `)` in an argument can't end anything early and a launcher `--upgrade` rewrites mid-run is never read again; `:done` restores the code page and returns node's status, and `call exit /b` exits with it explicitly, because a bare `exit /b` leaves the exit code of `cmd /c` (which PowerShell and every other program uses to run a `.cmd`) at 0. Arguments reach it through cmd's own parsing: `%`, `^`, `&` and `)` are safe inside `"…"`, and a `\"` inside one counts as a quote to cmd, so the quotes in an argument have to pair up |
63
+ | Windows | `scman.ps1` (UTF-8 with a BOM) | saves the caller's `$env:SCMAN_INSTALL_DIR` and sets it, sets `$env:NODE_USE_ENV_PROXY = '1'` when it is unset, runs `& '<node.exe>' '<entry>' @scmanArgs` in `try`, restores the old `SCMAN_INSTALL_DIR` in `finally` (or removes it when it was unset) and removes a `NODE_USE_ENV_PROXY` it set, then exits with node's exit code. A launcher whose `node.exe` can't start (deleted, or not a program) exits 1: it sets `$global:LASTEXITCODE = $null` before the call and keeps 1 when node left it unset, and a `catch` writes PowerShell's error to stderr with `Write-Error` and keeps 1, so neither the exit code of the caller's previous program nor a caller's `trap { continue }` makes it exit 0. A `.ps1` runs inside the caller's session, and this keeps the variables from leaking into it. `@scmanArgs` is `@args` with each array argument (`a,b`) flattened into separate arguments, as PowerShell passes them to a program, and each argument as text. When PowerShell passes arguments the legacy way, which drops empty arguments and doesn't escape `"` (Windows PowerShell 5.1 whatever `$PSNativeCommandArgumentPassing` says, `$PSNativeCommandArgumentPassing = 'Legacy'`, or pwsh before 7.3 without the variable; pwsh 7.3 and later without it pass the standard way), each `"` becomes `\"` (doubling the backslashes before it), an empty argument becomes `""`, and on Windows PowerShell an argument with whitespace gets its trailing backslashes doubled, so `node.exe` gets the arguments it was given. Since PowerShell reads an unquoted `a,b` as an array, `scman https://x/?a=1,2` reaches `scman` as two arguments, so quote any argument with a comma in it (`scman 'https://x/?a=1,2'`). It also sets `$PSNativeCommandUseErrorActionPreference = $false` and `$ErrorActionPreference = 'Continue'` in its own scope, so neither a caller's `$true` (pwsh 7.4 and later) nor, on Windows PowerShell 5.1, a caller's `'Stop'` with stderr redirected can turn node's exit code or its stderr into an error |
64
+ | Windows | `scman.cmd` (UTF-8, code page 65001 while it runs) | sets `SCMAN_INSTALL_DIR` and, when it is undefined, `NODE_USE_ENV_PROXY=1` (both local through `setlocal`), then runs `"<node.exe>" "<entry>" %* & call :done %%ERRORLEVEL%% & call exit /b %%ERRORLEVEL%%` as one line, so a `)` in an argument can't end anything early and a launcher `--upgrade` rewrites mid-run is never read again; `:done` restores the code page and returns node's status, and `call exit /b` exits with it explicitly, because a bare `exit /b` leaves the exit code of `cmd /c` (which PowerShell and every other program uses to run a `.cmd`) at 0. A `node.exe` that can't start leaves cmd's own non-zero code (9009 when it is missing). Arguments reach it through cmd's own parsing: `%`, `^`, `&` and `)` are safe inside `"…"`, and a `\"` inside one counts as a quote to cmd, so the quotes in an argument have to pair up |
65
65
 
66
66
  Both Windows launchers only start `node.exe` with the entry script and the arguments. `NODE_USE_ENV_PROXY=1` makes Node's `fetch` (pages, the sitemap and `--upgrade`) honour `HTTPS_PROXY`, `HTTP_PROXY` and `NO_PROXY` the way curl does. On Windows the installer adds `DIR` to the user's `PATH` when it isn't there already, and says so, along with the PowerShell command that removes it again (`To undo: [Environment]::SetEnvironmentVariable('Path', …, 'User')`). When `Get-ExecutionPolicy` is `Restricted`, `AllSigned` or `Undefined`, PowerShell won't run `scman.ps1`, so `install.ps1` prints to stderr that it needs `Set-ExecutionPolicy -Scope CurrentUser RemoteSigned`, or `scman.cmd` instead; it never changes the policy itself. Elsewhere it prints the `export PATH=…` line to add when `DIR` isn't on `PATH`. After writing the launchers, the installer runs `scman --version` and requires the version it installed, alone (`1.2.0`) or as `scman 1.2.0` from older releases, then prints `scman <version> installed to <launcher>` to stdout.
67
67
 
@@ -172,7 +172,7 @@ Every host command takes `--upgrade`. It runs the latest release's installer (`i
172
172
 
173
173
  The commands are `scman` on a host and `man`, `apropos`, `whatis` and `scman` in SLICC.
174
174
 
175
- The launcher the installer writes sets `SCMAN_INSTALL_DIR` to its own directory; that is how `scman` knows the installer put it there (it removes the variable before it starts a pager or the installer), and without it `scman` counts as installed with npm. The installer is downloaded from `https://github.com/ai-ecoverse/slicc-man/releases/latest/download/` into a temporary directory and run as `sh install.sh --dir DIR`, or `powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -File install.ps1 -Dir DIR` (else `pwsh.exe`) without `PSModulePath` in its environment, with its stderr passed through. (Run from PowerShell 7, `scman` inherits pwsh's `PSModulePath`, and Windows PowerShell started with it loses `Get-FileHash`.) `--upgrade` goes on once the installer has exited, waiting at most 500 ms more for its stdout, so a process the installer left running can't hold it. The line the installer prints, `scman <version> installed to <launcher>`, names the new version. A download that fails is the installer failing: `<cmd>: could not download <url>: <reason>`, exit 1, and so is an installer that doesn't print that line (`<cmd>: install.sh did not say which version it installed`) or a missing `sh` or PowerShell (`<cmd>: no shell found to run install.ps1 (tried powershell.exe, pwsh.exe)`). The download has 60 seconds; after that `--upgrade` prints `<cmd>: download timed out` to stderr and exits 1. `SLICC_INSTALL_BASE_URL`, when set, replaces that release URL for `--upgrade` and for the installers' own downloads.
175
+ The launcher the installer writes sets `SCMAN_INSTALL_DIR` to its own directory; that is how `scman` knows the installer put it there (it removes the variable before it starts a pager or the installer), and without it `scman` counts as installed with npm. The installer is downloaded from `https://github.com/ai-ecoverse/slicc-man/releases/latest/download/` into a temporary directory and run as `sh install.sh --dir DIR`, or `powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -File install.ps1 -Dir DIR` (else `pwsh.exe`) without `PSModulePath` in its environment, with its stderr passed through. (Run from PowerShell 7, `scman` inherits pwsh's `PSModulePath`, and Windows PowerShell started with it loses `Get-FileHash`.) `--upgrade` goes on once the installer has exited, waiting at most 500 ms more for its stdout, so a process the installer left running can't hold it. The line the installer prints, `scman <version> installed to <launcher>`, names the new version. A download that fails is the installer failing: `<cmd>: could not download <url>: <reason>` (an HTTP error status prints `<cmd>: could not download <url>: HTTP <status>`, and its body is cancelled, not left open), exit 1, and so is an installer that doesn't print that line (`<cmd>: install.sh did not say which version it installed`) or a missing `sh` or PowerShell (`<cmd>: no shell found to run install.ps1 (tried powershell.exe, pwsh.exe)`). The download has 60 seconds; after that `--upgrade` prints `<cmd>: download timed out` to stderr and exits 1. `SLICC_INSTALL_BASE_URL`, when set, replaces that release URL for `--upgrade` and for the installers' own downloads.
176
176
 
177
177
  ## Platforms
178
178
 
@@ -195,8 +195,8 @@ npm run test:unit
195
195
  ```
196
196
 
197
197
  - `npm run lint`: Biome, the no-comment check and the other [slicc-shared-web](https://github.com/ai-ecoverse/slicc-shared-web) checks, then `shellcheck -s sh` and `dash -n` on `install.sh`.
198
- - `npm test`: the integration tests in `test/integration/`. `slicc.test.mjs` installs the files `npm pack` ships into a headless slicc-kernel in Node (`createNodeKernel`, with wasm-bash and wasm-coreutils), and runs `man`, `apropos`, `whatis` and `scman` there against a local HTTP server on an ephemeral port, reached through the kernel's network transport as `http://host.slicc.internal:<port>` (`SLICC_MAN_URL`), with fake `less` and `hello` commands on `PATH` and a terminal from `openTerminal` for the pager. `host.test.mjs` runs `scman` (in-process, so a terminal can be faked, and as `host/scman.mjs`) against a local HTTP server on an ephemeral port standing in for www.sliccy.com (`SLICC_MAN_URL`), serving copies of the site's `tree.md` and `man.md` (`pages.json`, with their expected renderings), with a temporary cache and fake tools, `less`, `cmd` and `powershell.exe` among them, on a temporary `PATH`, and `win32` forced for the Windows lookup, pager, cache and `--upgrade` checks, so they run on any machine; only CI touches the real site. A fake is a Node script; on Windows it is a copy of a small `.exe` that `csc.exe` builds from C# (`fake-windows.mjs`), which runs the fake's script with Node, so there the Windows branches run for real, the pager through the real `cmd.exe`. `install.test.mjs` runs `scripts/assets.mjs` and `install.sh` on a tarball packed from the tree, with the Node on `PATH` and with a Node download from a fake nodejs.org, without `HOME`, with dependencies for npm to install, and `--upgrade` against a release served from a local server; `install.ps1` is checked statically, and run when `pwsh` is on `PATH`, where the launchers' environment and the argument passing of `scman.ps1` (hard arguments and `a,'b c' d`, under Standard, Legacy and no `$PSNativeCommandArgumentPassing`) are checked through a copy whose entry script prints its argv. `repo.test.mjs` checks the workflow, the release config, the package's shape, what `npm pack` ships and the repo rules.
199
- - `npm run test:windows`: on Windows, the host tests and `windows.test.mjs`, which installs a tarball packed from the tree with `install.ps1` under both Windows PowerShell and `pwsh`, from a local HTTP server into a temporary `-Dir`; runs `scman.cmd` and `scman.ps1` with `--version`, `--help`, a usage error and a page from a local site, cached in `%LOCALAPPDATA%\slicc-man\cache`; passes arguments full of `)`, `&`, `%`, `^`, spaces and quotes through copies of both launchers whose entry script prints its argv; checks that neither leaks `SCMAN_INSTALL_DIR` or `NODE_USE_ENV_PROXY`, that the `.cmd` keeps the exit code and restores the code page, and that the printed PATH undo works; upgrades in place with `--upgrade`, from a `cmd` that has pwsh's `PSModulePath`; and downloads Node as a `.zip` from a fake nodejs.org. Elsewhere `windows.test.mjs` is skipped.
198
+ - `npm test`: the integration tests in `test/integration/`. `slicc.test.mjs` installs the files `npm pack` ships into a headless slicc-kernel in Node (`createNodeKernel`, with wasm-bash and wasm-coreutils), and runs `man`, `apropos`, `whatis` and `scman` there against a local HTTP server on an ephemeral port, reached through the kernel's network transport as `http://host.slicc.internal:<port>` (`SLICC_MAN_URL`), with fake `less` and `hello` commands on `PATH` and a terminal from `openTerminal` for the pager. `host.test.mjs` runs `scman` (in-process, so a terminal can be faked, and as `host/scman.mjs`) against a local HTTP server on an ephemeral port standing in for www.sliccy.com (`SLICC_MAN_URL`), serving copies of the site's `tree.md` and `man.md` (`pages.json`, with their expected renderings), with a temporary cache and fake tools, `less`, `cmd` and `powershell.exe` among them, on a temporary `PATH`, and `win32` forced for the Windows lookup, pager, cache and `--upgrade` checks, so they run on any machine; only CI touches the real site. A fake is a Node script; on Windows it is a copy of a small `.exe` that `csc.exe` builds from C# (`fake-windows.mjs`), which runs the fake's script with Node, so there the Windows branches run for real, the pager through the real `cmd.exe`. `install.test.mjs` runs `scripts/assets.mjs` and `install.sh` on a tarball packed from the tree, with the Node on `PATH` and with a Node download from a fake nodejs.org, without `HOME`, with dependencies for npm to install, a truncated tarball and Node archive whose checksums match, and `--upgrade` against a release served from a local server; `install.ps1` is checked statically, and run when `pwsh` is on `PATH`, where a strict caller (`$PSNativeCommandUseErrorActionPreference = $true`, `$ErrorActionPreference = 'Stop'`), the launchers' environment and the argument passing of `scman.ps1` (hard arguments and `a,'b c' d`, under Standard, Legacy and no `$PSNativeCommandArgumentPassing`) are checked through a copy whose entry script prints its argv, and a `scman.ps1` pointed at a `node` that is missing or isn't a program must exit 1. `repo.test.mjs` checks the workflow, the release config, the package's shape, what `npm pack` ships and the repo rules.
199
+ - `npm run test:windows`: on Windows, the host tests and `windows.test.mjs`, which installs a tarball packed from the tree with `install.ps1` under both Windows PowerShell and `pwsh`, from a local HTTP server into a temporary `-Dir`; runs `scman.cmd` and `scman.ps1` with `--version`, `--help`, a usage error and a page from a local site, cached in `%LOCALAPPDATA%\slicc-man\cache`; passes arguments full of `)`, `&`, `%`, `^`, spaces and quotes through copies of both launchers whose entry script prints its argv; checks that neither leaks `SCMAN_INSTALL_DIR` or `NODE_USE_ENV_PROXY`, that both exit non-zero (the `.ps1` exits 1, after a program that exited 0, for a default, a strict, a `trap { continue }` and a redirected-stderr caller) once the copied `node.exe` they were installed with is replaced by a file that isn't a program and then deleted, that the `.cmd` keeps the exit code and restores the code page, and that the printed PATH undo works; upgrades in place with `--upgrade`, from a `cmd` that has pwsh's `PSModulePath`; and downloads Node as a `.zip` from a fake nodejs.org, after refusing a truncated one; the installs and `scman.ps1` run under a strict caller too, and a truncated tarball exits 1 there. Elsewhere `windows.test.mjs` is skipped.
200
200
  - `npm run test:unit`: the unit tests in the gitignored `test/unit/`, with Node's coverage in `coverage/unit/lcov.info`. The pre-commit hook wants 100% coverage of the changed lines.
201
201
 
202
202
  CI is `.github/workflows/ci.yml`, the only workflow, which runs [slicc-shared-web's pipeline](https://github.com/ai-ecoverse/slicc-shared-web#pipeline) at the devDependency's version, with Node 26, which the headless kernel needs: `npm run lint`, then `npm test` on Linux, and on a pull request and in the merge queue `npm run test:windows` on `windows-latest` (`windows-test` in `ci.yml`), then on a pull request the Claude and Codex reviews. The merge queue requires `ci / integration` and `ci / review`. On a push to `main`, semantic-release releases and publishes to npm through trusted publishing, with provenance from `ci.yml`, and attaches `slicc-man-<version>.tgz`, `SHA256SUMS`, `install.sh` and `install.ps1` to the GitHub release (see [the installer](#the-installer)).
package/dist/slicc.mjs CHANGED
@@ -54,6 +54,6 @@ ${and} -l, --long don't cut lines to the terminal width
54
54
  ${parsed.usage?usage(kind,name):""}`),USAGE;if(parsed.help)return await io.out(usage(kind,name)),OK;if(parsed.version)return await io.out(`${io.version}
55
55
  `),OK;if(parsed.flags.upgrade)return io.upgrade();let url=base(env),ctx={io,env,name,flags:parsed.flags,values:parsed.values,width:width(env,io),styled:!!io.tty&&!env.NO_COLOR,site:site({url,store:io.store(folder(url)),fetch:io.fetch,now:io.now,name,timeout:io.timeout})};return kind!=="man"?keywords(kind,ctx,parsed):parsed.flags.apropos?keywords("apropos",ctx,parsed):parsed.flags.whatis?keywords("whatis",ctx,parsed):parsed.operands.length===0?(await io.err(`What manual page do you want?
56
56
  For example, try 'man man'.
57
- `),USAGE):parsed.flags.local?local(ctx,parsed.operands):pages(ctx,parsed)}var reasons={ENOENT:"No such file or directory",EACCES:"Permission denied",EISDIR:"Is a directory",ENOTDIR:"Not a directory"};var package_default={version:"1.2.2"};var kinds={apropos:"apropos",whatis:"whatis"},decoder=new TextDecoder,counter=0;function why(error){return reasons[error?.code]??error?.message??String(error)}function store(fs,dir){let file=id=>`${dir}/${id}.json`;return{async read(id){try{return decoder.decode(await fs.readFile(file(id)))}catch{return}},async write(id,text){await fs.mkdir(dir,{recursive:!0});let temp=`${dir}/.${id}.${Date.now()}.${counter++}.tmp`;try{await fs.writeFile(temp,text),await fs.rename(temp,file(id))}catch(error){throw await fs.unlink(temp).catch(()=>{}),error}},async remove(id){await fs.unlink(file(id)).catch(()=>{})}}}async function which(ctx,file){for(let dir of String(ctx.env.PATH??"").split(":")){let path=ctx.resolve(`${dir||"."}/${file}`);try{let stat=await ctx.fs.stat(path);if(stat.isFile&&(stat.mode??493)&73)return!0}catch{}}return!1}async function input(ctx){let chunks=[];for(;;){let chunk=await ctx.read(0);if(chunk.length===0)break;chunks.push(chunk)}let all=new Uint8Array(chunks.reduce((sum,chunk)=>sum+chunk.length,0)),at=0;for(let chunk of chunks)all.set(chunk,at),at+=chunk.length;return decoder.decode(all)}async function read(ctx,file){try{return file==="-"?await input(ctx):decoder.decode(await ctx.fs.readFile(ctx.resolve(file)))}catch(error){throw new Error(why(error))}}async function page(ctx,name,command,text){let err=line2=>ctx.write(2,line2);await ctx.signals.on("SIGINT",()=>{});try{let child;try{child=await ctx.spawn({argv:["sh","-c",command],stdin:"pipe"})}catch(error){return await err(`${name}: ${command}: ${why(error)}
57
+ `),USAGE):parsed.flags.local?local(ctx,parsed.operands):pages(ctx,parsed)}var reasons={ENOENT:"No such file or directory",EACCES:"Permission denied",EISDIR:"Is a directory",ENOTDIR:"Not a directory"};var package_default={version:"1.2.4"};var kinds={apropos:"apropos",whatis:"whatis"},decoder=new TextDecoder,counter=0;function why(error){return reasons[error?.code]??error?.message??String(error)}function store(fs,dir){let file=id=>`${dir}/${id}.json`;return{async read(id){try{return decoder.decode(await fs.readFile(file(id)))}catch{return}},async write(id,text){await fs.mkdir(dir,{recursive:!0});let temp=`${dir}/.${id}.${Date.now()}.${counter++}.tmp`;try{await fs.writeFile(temp,text),await fs.rename(temp,file(id))}catch(error){throw await fs.unlink(temp).catch(()=>{}),error}},async remove(id){await fs.unlink(file(id)).catch(()=>{})}}}async function which(ctx,file){for(let dir of String(ctx.env.PATH??"").split(":")){let path=ctx.resolve(`${dir||"."}/${file}`);try{let stat=await ctx.fs.stat(path);if(stat.isFile&&(stat.mode??493)&73)return!0}catch{}}return!1}async function input(ctx){let chunks=[];for(;;){let chunk=await ctx.read(0);if(chunk.length===0)break;chunks.push(chunk)}let all=new Uint8Array(chunks.reduce((sum,chunk)=>sum+chunk.length,0)),at=0;for(let chunk of chunks)all.set(chunk,at),at+=chunk.length;return decoder.decode(all)}async function read(ctx,file){try{return file==="-"?await input(ctx):decoder.decode(await ctx.fs.readFile(ctx.resolve(file)))}catch(error){throw new Error(why(error))}}async function page(ctx,name,command,text){let err=line2=>ctx.write(2,line2);await ctx.signals.on("SIGINT",()=>{});try{let child;try{child=await ctx.spawn({argv:["sh","-c",command],stdin:"pipe"})}catch(error){return await err(`${name}: ${command}: ${why(error)}
58
58
  `),2}let writer=child.stdin.getWriter();await writer.write(new TextEncoder().encode(text)).catch(()=>{}),await writer.close().catch(()=>{});let{status}=await child.wait();return status===126||status===127?2:0}finally{await ctx.signals.reset("SIGINT")}}var escapes=/(?:\u001b[\]P^_X]|[\u0090\u0098\u009d-\u009f])[\s\S]*?(?:\u0007|\u001b\\|\u009c|$)|(?:\u001b\[|\u009b)[0-?]*[ -/]*[@-~]|\u001b[ -/]*[0-~]?/g;function help(ctx,topic){let result=ctx.sync.run({argv:[topic,"--help"],stdin:"ignore",stderr:"stdout"}),text=decoder.decode(result.stdout??new Uint8Array);return clean(text.replace(escapes,""))}async function columns(ctx,tty){if(tty)try{return(await ctx.tty.size(1)).columns}catch{return}}async function main(ctx){let name=String(ctx.argv[0]??"man").split("/").pop(),kind=kinds[name]??"man",tty=await ctx.isatty(1),root=`${String(ctx.env.HOME||"/root").replace(/\/+$/,"")}/.cache/slicc-man`,io={tty,columns:await columns(ctx,tty),fetch:(url,init)=>ctx.fetch(url,init),now:Date.now,version:package_default.version,out:text=>ctx.write(1,text),err:text=>ctx.write(2,text),store:dir=>store(ctx.fs,`${root}/${dir}`),which:file=>which(ctx,file),page:(command,text)=>page(ctx,name,command,text),read:file=>read(ctx,file),help:topic=>help(ctx,topic),upgrade:async()=>(await ctx.write(1,`pnpm add -g @ai-ecoverse/slicc-man@latest
59
59
  `),0)};return run2({kind,name,argv:ctx.argv.slice(1),env:ctx.env,io})}export{main as default,help,page,store,which};
package/host/upgrade.mjs CHANGED
@@ -68,7 +68,7 @@ export async function upgrade({ name, env, platform, fds, fetch }) {
68
68
  try {
69
69
  const response = await fetch(url, { redirect: 'follow', signal: AbortSignal.timeout(60000) });
70
70
  if (!response.ok) {
71
- await response.body?.cancel();
71
+ await response.body?.cancel().catch(() => {});
72
72
  return fail(`could not download ${url}: HTTP ${response.status}`);
73
73
  }
74
74
  text = await response.text();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-ecoverse/slicc-man",
3
- "version": "1.2.2",
3
+ "version": "1.2.4",
4
4
  "description": "man, apropos and whatis for SLICC, and scman for SLICC, macOS, Linux and Windows, reading pages from www.sliccy.com/man",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -74,7 +74,7 @@
74
74
  "release": "semantic-release"
75
75
  },
76
76
  "devDependencies": {
77
- "@ai-ecoverse/slicc-kernel": "1.154.2",
77
+ "@ai-ecoverse/slicc-kernel": "1.159.0",
78
78
  "@ai-ecoverse/slicc-shared-web": "2.12.0",
79
79
  "@ai-ecoverse/wasm-bash": "5.3.0-23",
80
80
  "@ai-ecoverse/wasm-coreutils": "9.12.0-15",