@ai-ecoverse/slicc-man 1.2.1 → 1.2.2

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
@@ -14,13 +14,13 @@ pnpm add -g @ai-ecoverse/slicc-man
14
14
 
15
15
  They are one self-contained ES module run by the kernel's JS ABI (`"abi": "js"`), so they need no Node in SLICC, and `engines.slicc-kernel` asks for slicc-kernel 1.91.0 or later, the oldest release the integration tests pass on (an older kernel's `fetch` can't reach `host.slicc.internal`, which they use).
16
16
 
17
- On macOS and Linux (get.sliccy.ai, once live):
17
+ On macOS and Linux:
18
18
 
19
19
  ```sh
20
20
  curl -fsSL https://get.sliccy.ai/man | sh
21
21
  ```
22
22
 
23
- On Windows, in PowerShell (get.sliccy.ai, once live):
23
+ On Windows, in PowerShell:
24
24
 
25
25
  ```powershell
26
26
  irm https://get.sliccy.ai/man.ps1 | iex
@@ -32,7 +32,7 @@ The same installers are attached to every GitHub release, as `install.sh` and `i
32
32
  curl -fsSL https://github.com/ai-ecoverse/slicc-man/releases/latest/download/install.sh | sh
33
33
  ```
34
34
 
35
- They install the release's `scman` with a launcher in `~/.local/bin` (on Windows `%LOCALAPPDATA%\slicc-man\bin`, added to the user's `PATH`), and download Node 24 into the install when no Node 24 or later is on `PATH` (the `.tar.gz`, or the `.zip` on Windows), checked against nodejs.org's `SHASUMS256.txt`. Each release also carries the package tarball, `slicc-man-<version>.tgz`, and `SHA256SUMS`, which covers the tarball, `install.sh` and `install.ps1`: the installers check the tarball against it, unpack it into `${XDG_DATA_HOME:-~/.local/share}/slicc-man/<version>` (on Windows `%LOCALAPPDATA%\slicc-man\<version>`), write the launcher (`scman`, or `scman.ps1` and `scman.cmd` on Windows, which only start `node.exe` with the entry script and the arguments, the node call and its `exit` on one line of the `.cmd`), run it with `--version` (the version alone, or `scman <version>` from older releases), and print `scman <version> installed to <launcher>`. Options pass through (`curl -fsSL https://get.sliccy.ai/man | sh -s -- --help`): `--version <version>` installs that release instead of the latest, `--dir <dir>` puts the launcher in `<dir>`, and `--help` prints the usage; `install.sh` exits 2 for an unknown option or one without its value. `install.ps1` takes the same options, spelled `-Version`, `-Dir` and `-Help`. `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` nodejs.org, for mirrors and tests.
35
+ They install the release's `scman` with a launcher in `~/.local/bin` (on Windows `%LOCALAPPDATA%\slicc-man\bin`, added to the user's `PATH`), with Node 24 or later, which they download when none is on `PATH`. Options reach `install.sh` after `sh -s --` (`curl -fsSL https://get.sliccy.ai/man | sh -s -- --version 1.2.0 --dir ~/bin`), and `install.ps1` through a script block (`& ([scriptblock]::Create((irm https://get.sliccy.ai/man.ps1))) -Version 1.2.0`). [The installer](#the-installer) has the details.
36
36
 
37
37
  On a machine with Node 24 or later, npm works too:
38
38
 
@@ -43,6 +43,28 @@ scman tree
43
43
 
44
44
  or without installing, `npx -p @ai-ecoverse/slicc-man scman tree`. The package's `bin` has only `scman`.
45
45
 
46
+ ### The installer
47
+
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
+
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.
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.
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.
55
+
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
+
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:
59
+
60
+ | platform | launcher in `DIR` | what it does |
61
+ | --- | --- | --- |
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 |
65
+
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
+
46
68
  ## man
47
69
 
48
70
  ```
@@ -125,7 +147,7 @@ A page is turned from Markdown into a man page:
125
147
 
126
148
  The width is `MANWIDTH` when it is a positive number, else the terminal's width when stdout is a terminal, else 80.
127
149
 
128
- When stdout is a terminal, bold and underline are ANSI escapes (`ESC[1m`, `ESC[4m`), unless `NO_COLOR` is set and not empty, and the page goes through a pager: `-P`, else `MANPAGER`, else `PAGER`, else `less -R` when `less` is on `PATH`, else straight to stdout. The pager is a command line run by the shell (`sh -c`, `cmd /c` on Windows), with the page on its stdin; an empty one means straight to stdout. A pager that exits non-zero doesn't change the exit code; one that can't be run is exit 2. When stdout isn't a terminal, the page is plain text, with no escapes and no pager. `apropos` and `whatis` never use a pager.
150
+ When stdout is a terminal, bold and underline are ANSI escapes (`ESC[1m`, `ESC[4m`), unless `NO_COLOR` is set and not empty, and the page goes through a pager: `-P`, else `MANPAGER`, else `PAGER`, else `less -R` when `less` is on `PATH` (on Windows, `less` with an extension from `PATHEXT`, by default `.COM`, `.EXE`, `.BAT` or `.CMD`), else straight to stdout. The pager is a command line run by the shell (`/bin/sh -c`, or `%COMSPEC% /d /s /c "<pager> & if errorlevel 9009 if not errorlevel 9010 exit 9009"` on Windows, because the exit code of `cmd /c` alone doesn't say that it couldn't find the command), with the page on its stdin; an empty one means straight to stdout. A pager that exits non-zero doesn't change the exit code; one that can't be run (the shell's 126 or 127, or cmd's 9009 on Windows) is exit 2. When stdout isn't a terminal, the page is plain text, with no escapes and no pager. `apropos` and `whatis` never use a pager.
129
151
 
130
152
  ## Offline cache
131
153
 
@@ -150,7 +172,7 @@ Every host command takes `--upgrade`. It runs the latest release's installer (`i
150
172
 
151
173
  The commands are `scman` on a host and `man`, `apropos`, `whatis` and `scman` in SLICC.
152
174
 
153
- 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/` and run as `sh install.sh --dir DIR`, or `powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -File install.ps1 -Dir DIR` (else `pwsh.exe`); the line it 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. 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>`, 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.
154
176
 
155
177
  ## Platforms
156
178
 
@@ -159,7 +181,7 @@ The launcher the installer writes sets `SCMAN_INSTALL_DIR` to its own directory;
159
181
  | SLICC | `man`, `apropos`, `whatis`, `scman` | `~/.cache/slicc-man`, in OPFS | `less -R` (seven's `less`) | the kernel's network transport |
160
182
  | macOS | `scman` | `$XDG_CACHE_HOME/slicc-man`, else `~/.cache/slicc-man` | `less -R` | direct HTTPS |
161
183
  | Linux | `scman` | `$XDG_CACHE_HOME/slicc-man`, else `~/.cache/slicc-man` | `less -R` when installed, else stdout | direct HTTPS |
162
- | Windows | `scman` | `%LOCALAPPDATA%\slicc-man\cache` | `less -R` when on `PATH` (Git for Windows has one), else stdout | direct HTTPS |
184
+ | Windows | `scman` (`scman.ps1`, `scman.cmd`) | `%LOCALAPPDATA%\slicc-man\cache` | `less -R` through `cmd /c` when on `PATH` (Git for Windows has one), else stdout | direct HTTPS |
163
185
 
164
186
  `scman -k` and `scman -f` are its `apropos` and `whatis`. Other Unix systems are treated as Linux.
165
187
 
@@ -173,10 +195,11 @@ npm run test:unit
173
195
  ```
174
196
 
175
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`.
176
- - `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` among them, on a temporary `PATH`; only CI touches the real site. `install.test.mjs` runs `install.sh` on a tarball packed from the tree, with the Node on `PATH` and with a Node download from a fake nodejs.org, and `--upgrade` against a release served from a local server; `install.ps1` is checked statically, and run when `pwsh` is on `PATH`. `repo.test.mjs` checks the workflow, the release config, the package's shape, what `npm pack` ships and the repo rules.
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.
177
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.
178
201
 
179
- 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, on Linux only, with Node 26, which the headless kernel needs: `npm run lint`, then `npm test`, 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 (`scripts/assets.mjs` renames the tarball and writes `SHA256SUMS`).
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)).
180
203
 
181
204
  The code is in `lib/` (options, the Markdown renderer, the cache and index, apropos and whatis, shared with SLICC), `host/` (`scman.mjs`, the cache directory, the pager, `--upgrade`) and `slicc/` (`main.mjs`, the JS ABI entry: the cache in `~/.cache/slicc-man` through the kernel's file system, `fetch`, the pager, the fallback and `--upgrade`). `scripts/bundle.mjs` bundles `slicc/main.mjs` and `lib/` with esbuild into `dist/slicc.mjs`, the module `slicc.commands` names, with only the version of `package.json` in it. `dist/` is a build output, not in git: `npm pack` and `npm publish` build it (`prepack`), so a release always ships a bundle of its own sources, and `slicc.test.mjs` and `repo.test.mjs` check what `npm pack` ships.
182
205
 
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.1"};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.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)}
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/pager.mjs CHANGED
@@ -6,7 +6,12 @@ export function shell(command, platform, env) {
6
6
  if (platform === 'win32') {
7
7
  return {
8
8
  file: variable(env, 'COMSPEC') || 'cmd.exe',
9
- args: ['/d', '/s', '/c', `"${command}"`],
9
+ args: [
10
+ '/d',
11
+ '/s',
12
+ '/c',
13
+ `"${command} & if errorlevel 9009 if not errorlevel 9010 exit 9009"`,
14
+ ],
10
15
  options: { windowsVerbatimArguments: true },
11
16
  };
12
17
  }
@@ -32,7 +37,8 @@ export function page(command, text, { name, platform, env, fds }) {
32
37
  child.stdin.end(text);
33
38
  child.on('close', (code) => {
34
39
  process.off('SIGINT', ignore);
35
- done(failed || code === 126 || code === 127 ? 2 : 0);
40
+ const missing = platform === 'win32' ? [9009] : [126, 127];
41
+ done(failed || missing.includes(code) ? 2 : 0);
36
42
  });
37
43
  });
38
44
  }
package/host/path.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { accessSync, constants, statSync } from 'node:fs';
2
- import { join } from 'node:path';
2
+ import { extname, join } from 'node:path';
3
3
 
4
4
  export function variable(env, name) {
5
5
  const found = Object.keys(env).find((key) => key.toUpperCase() === name);
@@ -11,14 +11,12 @@ export function lookup(file, platform, env) {
11
11
  const dirs = String((windows ? variable(env, 'PATH') : env.PATH) ?? '')
12
12
  .split(windows ? ';' : ':')
13
13
  .filter(Boolean);
14
- const exts = windows
15
- ? [
16
- '',
17
- ...String(variable(env, 'PATHEXT') || '.COM;.EXE;.BAT;.CMD')
14
+ const exts =
15
+ windows && !extname(file)
16
+ ? String(variable(env, 'PATHEXT') || '.COM;.EXE;.BAT;.CMD')
18
17
  .split(';')
19
- .filter(Boolean),
20
- ]
21
- : [''];
18
+ .filter(Boolean)
19
+ : [''];
22
20
  for (const dir of dirs) {
23
21
  for (const ext of exts) {
24
22
  const path = join(dir, file + ext);
package/host/upgrade.mjs CHANGED
@@ -27,11 +27,28 @@ function installer(platform, env, path, dir) {
27
27
 
28
28
  function wait(file, args, options) {
29
29
  return new Promise((done) => {
30
+ let child;
31
+ try {
32
+ child = spawn(file, args, options);
33
+ } catch (error) {
34
+ done({ error });
35
+ return;
36
+ }
30
37
  const out = [];
31
- const child = spawn(file, args, options);
38
+ let code;
39
+ let timer;
40
+ const finish = () => {
41
+ clearTimeout(timer);
42
+ child.stdout.destroy();
43
+ done({ code, stdout: Buffer.concat(out).toString() });
44
+ };
32
45
  child.stdout.on('data', (chunk) => out.push(chunk));
33
46
  child.on('error', (error) => done({ error }));
34
- child.on('close', (code) => done({ code, stdout: Buffer.concat(out).toString() }));
47
+ child.on('close', finish);
48
+ child.on('exit', (status) => {
49
+ code = status ?? 1;
50
+ timer = setTimeout(finish, 500);
51
+ });
35
52
  });
36
53
  }
37
54
 
@@ -70,6 +87,11 @@ export async function upgrade({ name, env, platform, fds, fetch }) {
70
87
  }
71
88
  const child = { ...env };
72
89
  delete child.SCMAN_INSTALL_DIR;
90
+ if (platform === 'win32') {
91
+ for (const key of Object.keys(child)) {
92
+ if (key.toUpperCase() === 'PSMODULEPATH') delete child[key];
93
+ }
94
+ }
73
95
  const result = await wait(run.tool, run.args, {
74
96
  env: child,
75
97
  stdio: ['ignore', 'pipe', fds[2]],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-ecoverse/slicc-man",
3
- "version": "1.2.1",
3
+ "version": "1.2.2",
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",
@@ -66,6 +66,7 @@
66
66
  "lint": "slicc-lint && shellcheck -s sh install.sh && dash -n install.sh",
67
67
  "format": "biome check --write .",
68
68
  "test": "node --test --test-timeout=120000 test/integration/*.test.mjs",
69
+ "test:windows": "node --test --test-concurrency=1 --test-timeout=600000 test/integration/host.test.mjs test/integration/windows.test.mjs",
69
70
  "test:unit": "node scripts/cover.mjs",
70
71
  "build": "npm pack --dry-run",
71
72
  "trust": "npx fledgling sync -y --skip-publish",
@@ -73,7 +74,7 @@
73
74
  "release": "semantic-release"
74
75
  },
75
76
  "devDependencies": {
76
- "@ai-ecoverse/slicc-kernel": "1.153.0",
77
+ "@ai-ecoverse/slicc-kernel": "1.154.2",
77
78
  "@ai-ecoverse/slicc-shared-web": "2.12.0",
78
79
  "@ai-ecoverse/wasm-bash": "5.3.0-23",
79
80
  "@ai-ecoverse/wasm-coreutils": "9.12.0-15",