@ai-ecoverse/slicc-capture 1.0.0 → 1.2.0

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
@@ -13,13 +13,13 @@ screencapture -T 3 -R 0,0,800,600 -t jpg crop.jpg
13
13
 
14
14
  ## Install
15
15
 
16
- In SLICC, the package declares `screencapture` and `sccapture` in `slicc.commands`, so installing it puts both in `/usr/bin`:
16
+ In SLICC, the package declares `screencapture` and `sccapture` in `slicc.commands` (POSIX `sh` scripts in `bin/`), so installing it puts both in `/usr/bin`. They need slicc-kernel 1.153.0 or later, for [`/dev/embedder`](#the-request) (`engines.slicc-kernel` is `>=1.153.0`; an older kernel refuses them with exit 126 and `<cmd>: @ai-ecoverse/slicc-capture@<version> needs slicc-kernel >=1.153.0; this kernel is <version>`):
17
17
 
18
18
  ```sh
19
19
  pnpm add -g @ai-ecoverse/slicc-capture
20
20
  ```
21
21
 
22
- On macOS and Linux:
22
+ On a host, the installer puts `sccapture` in `~/.local/bin`, with Node 24 or later, which it downloads (`node-<version>-<os>-<arch>.tar.gz`, or the `.zip` on Windows, checked against nodejs.org's `SHASUMS256.txt`) when none is on `PATH`:
23
23
 
24
24
  ```sh
25
25
  curl -fsSL https://get.sliccy.ai/slicc-capture | sh
@@ -31,24 +31,30 @@ On Windows, in PowerShell:
31
31
  irm https://get.sliccy.ai/slicc-capture.ps1 | iex
32
32
  ```
33
33
 
34
- Or, on any machine with Node 24 or later:
34
+ The installers are `install.sh` and `install.ps1`, attached to every [GitHub release](https://github.com/ai-ecoverse/slicc-capture/releases) with the package tarball (`slicc-capture-<version>.tgz`) and `SHA256SUMS`, which lists the tarball, `install.sh` and `install.ps1`, in the style of [mcpcat's](https://github.com/ai-ecoverse/mcpcat) `install.sh`. `@semantic-release/npm` packs the tarball into `release/` (`tarballDir`), and `@semantic-release/exec` runs `scripts/assets.mjs`, which renames it and writes `SHA256SUMS`. They check the tarball against `SHA256SUMS`, unpack it into a versioned directory (`${XDG_DATA_HOME:-~/.local/share}/slicc-capture/<version>`, or `%LOCALAPPDATA%\slicc-capture\<version>` on Windows), and write a `sccapture` launcher that runs it with that Node. Without get.sliccy.ai, the same installer comes straight from the release:
35
35
 
36
36
  ```sh
37
- npm install -g @ai-ecoverse/slicc-capture
37
+ curl -fsSL https://github.com/ai-ecoverse/slicc-capture/releases/latest/download/install.sh | sh
38
38
  ```
39
39
 
40
- or without installing, `npx -p @ai-ecoverse/slicc-capture sccapture shot.png`.
40
+ | `install.sh` | `install.ps1` | |
41
+ | --- | --- | --- |
42
+ | `--version VERSION` | `-Version VERSION` | the release to install (default: the latest) |
43
+ | `--dir DIR` | `-Dir DIR` | where the launcher goes (default: `~/.local/bin`; on Windows `%LOCALAPPDATA%\slicc-capture\bin`, added to the user's `PATH`) |
44
+ | `--help` | `-Help` | print the usage and exit 0 |
41
45
 
42
- ### The installers
46
+ Options reach `install.sh` after `sh -s --`, for example `curl -fsSL https://get.sliccy.ai/slicc-capture | sh -s -- --version 1.2.0 --dir ~/bin`, and `install.ps1` through a script block: `& ([scriptblock]::Create((irm https://get.sliccy.ai/slicc-capture.ps1))) -Version 1.2.0`. An unknown option, or `--dir` or `--version` without a value, prints `install.sh: <problem>` and the usage to stderr and exits 2; there are no environment overrides for the options. A failure is `install.sh: <problem>` (`install.ps1: <problem>`) on stderr and exit 1. `SLICC_INSTALL_BASE_URL` replaces the release download directory (by default `https://github.com/ai-ecoverse/slicc-capture/releases/latest/download`, or `…/download/v<version>` with `--version`), and `SLICC_NODE_DIST_BASE` replaces `https://nodejs.org/dist`. The installed command must answer `--version` with the version it was installed as (or the older `sccapture <version>`). On Windows the launcher is `sccapture.ps1`, next to a `sccapture.cmd` for `cmd.exe`; both only start `node.exe` with the entry script and the arguments, and `sccapture` itself never runs `cmd.exe`.
43
47
 
44
- Each release attaches `install.sh` and `install.ps1` next to the package tarball, `slicc-capture-<version>.tgz`, and a `SHA256SUMS` that lists all three. The PR that adds the installers also adds them, the tarball and `SHA256SUMS` to `@semantic-release/github`'s `assets` in `.releaserc.json`, with a test in `test/integration/repo.test.mjs`, so no release ships without them. get.sliccy.ai serves the latest release's installers. Both work like mcpcat's `install.sh`:
48
+ The get.sliccy.ai entries for `slicc-capture` and `slicc-capture.ps1` are added once the first release is out.
45
49
 
46
- - **Options:** `--version <version>` installs that release instead of the latest; `--dir <dir>` puts the launcher there instead of the default; `--help` prints the usage and exits 0. An unknown option prints `slicc-capture: unknown option: <arg>` to stderr and exits 1. The environment variables `VERSION` and `INSTALL_DIR` do the same as the options. Through a pipe, `sh -s -- --dir ~/bin` passes options to `install.sh`, and `& ([scriptblock]::Create((irm https://get.sliccy.ai/slicc-capture.ps1))) --dir C:\tools` to `install.ps1`.
47
- - **Node 24 or later:** the `node` on `PATH` if it is new enough, else the one the installer downloaded before; else it downloads the current Node LTS from nodejs.org, checks it against that release's `SHASUMS256.txt`, and keeps it in the data directory. On musl Linux, where nodejs.org has no build, it says to install Node with the package manager and exits 1.
48
- - **Where it goes:** the tarball is checked against `SHA256SUMS`, unpacked into `<data>/<version>`, and `<data>/current` points at it. `<data>` is `${XDG_DATA_HOME:-~/.local/share}/slicc-capture` on macOS and Linux, `%LOCALAPPDATA%\slicc-capture` on Windows. The launcher, `sccapture` (`sccapture.cmd` on Windows), goes into `~/.local/bin` (`%LOCALAPPDATA%\slicc-capture\bin` on Windows) and runs that Node on `<data>/<version>/host/sccapture.mjs`, so the launcher keeps working when the `node` on `PATH` changes.
49
- - **Output:** `sccapture <version> installed to <launcher>`, and, when the directory isn't on `PATH`, the line that adds it. Any failure (no `curl` or `wget`, a checksum mismatch, an unsupported OS or architecture) is one line on stderr and exit 1; on an unsupported OS it names `npm install -g @ai-ecoverse/slicc-capture` instead.
50
+ With npm, on a machine with Node 24 or later:
50
51
 
51
- The get.sliccy.ai entries for `slicc-capture` and `slicc-capture.ps1` are added once the first release is out.
52
+ ```sh
53
+ npm install -g @ai-ecoverse/slicc-capture
54
+ sccapture shot.png
55
+ ```
56
+
57
+ or without installing, `npx -p @ai-ecoverse/slicc-capture sccapture shot.png`.
52
58
 
53
59
  ## Commands
54
60
 
@@ -66,33 +72,55 @@ Short options may be grouped (`-xc`), an option's value may be attached or separ
66
72
 
67
73
  ### In SLICC
68
74
 
69
- `screencapture file` asks seven's page to capture. `getDisplayMedia` needs a user gesture, so the page first shows a prompt, `screencapture wants to capture your screen`, with **Choose…** and **Cancel**; **Choose…** opens Chrome's picker. After the user picks, the command waits for `-T` seconds, if given, takes one frame, crops it to `-R`, if given, encodes it and stops the capture.
75
+ `screencapture file` asks seven's page to capture, as a `capture.screen` request on `/dev/embedder` ([below](#the-request)). `getDisplayMedia` needs a user gesture, so the page first shows a prompt, `screencapture wants to capture your screen`, with **Choose…** and **Cancel**; **Choose…** opens Chrome's picker. After the user picks, the page waits for `-T` seconds, if given, takes one frame, crops it to `-R`, if given, encodes it and stops the capture. The command writes the bytes it gets back unchanged; an existing file is replaced only once the capture has arrived.
70
76
 
71
77
  - **The file:** written as PNG, JPEG or WebP: `-t` decides, else the file's extension (`.png`, `.jpg` or `.jpeg`, `.webp`), else PNG. JPEG and WebP use quality 0.92. On success it prints `captured <n> KB to <file>`.
72
- - **`-c`:** copies the capture to the clipboard as `image/png`, through [slicc-clipboard](https://github.com/ai-ecoverse/slicc-clipboard), and prints `captured <n> KB to clipboard`. A file named with `-c` is ignored, as with macOS's `-c`, and so is `-t`. slicc-clipboard's device is text only today; `-c` needs its image path, which is being added there.
73
- - **`-v`:** shows the capture inline in the agent's tool result. With a file, it writes the file and prints `<path> (<n> KB)` followed by the image; without one, it prints only the image; with `-c`, it copies and shows. slicc 6 printed the image as `<img:data:<type>;base64,<data>>`; seven's marker for an inline image is being agreed with slicc-agent, and `-v` uses whatever that is. macOS's `-v` (record a video) is out of scope everywhere.
78
+ - **`-c`:** captures a PNG and copies it to the clipboard as `image/png`, with a `clipboard.write` request on `/dev/embedder`, which seven's embedder (slicc-bios) answers; [slicc-clipboard](https://github.com/ai-ecoverse/slicc-clipboard)'s `/dev/clipboard` stays text only. It prints `captured <n> KB to clipboard`. A file named with `-c` is ignored, as with macOS's `-c`, and so is `-t`. The `clipboard.write` request is sent before the prompt, so a page that doesn't answer it fails at once, without a capture.
79
+ - **`-v`:** shows the capture inline in the agent's tool result, by printing one line `<img:<absolute path>>` on stdout, the marker agreed in [ai-ecoverse/slicc-agent#713](https://github.com/ai-ecoverse/slicc-agent/issues/713): the agent reads that file, detects PNG, JPEG, GIF or WebP by its magic bytes, fits the image to 1600 px wide and 1 MB of base64, and replaces the line with `<path> (<mime>, <bytes> bytes)` and the image in the tool result (at most 4 per command). With a file, `-v` writes it and prints only the marker for it; without one, it writes the capture to a new `${TMPDIR:-/tmp}/screencapture-XXXXXX.<type>`, which it leaves there for the agent, and prints the marker for that; with `-c`, it copies, prints `captured <n> KB to clipboard`, and then the marker of such a file. slicc 6's `<img:data:<type>;base64,<data>>` is gone: base64 on stdout is scrubbed and cut by the output tail. The agent's side of #713 isn't shipped yet; until it is, the marker shows as the line itself. macOS's `-v` (record a video) is out of scope everywhere.
74
80
  - **No file:** without a file, `-c` or `-v`: `screencapture: output file required (or use -c for clipboard)` and the usage to stderr, exit 2.
75
81
 
76
82
  Errors, on stderr, from slicc 6 where they still apply:
77
83
 
78
84
  | error | exit |
79
85
  | --- | --- |
80
- | `screencapture: user cancelled or permission denied` (**Cancel**, the picker closed, or the capture refused) | 1 |
81
- | `screencapture: timed out waiting for the picker` (no answer within 5 minutes) | 1 |
82
- | `screencapture: browser APIs are unavailable in this environment` (an embedder that doesn't answer capture requests) | 1 |
83
- | `screencapture: screen capture is not supported in this browser` (no `getDisplayMedia`) | 1 |
84
- | `screencapture: -R: rectangle outside the capture` | 1 |
85
- | `screencapture: failed to write file: <message>` | 1 |
86
- | `screencapture: failed to copy to clipboard: <message>` | 1 |
86
+ | `screencapture: user cancelled, permission denied, or the surface is protected` (`EACCES`: **Cancel**, the picker closed, the capture refused, or the embedder refused the surface the user picked) | 1 |
87
+ | `screencapture: timed out waiting for the picker` (no answer within 5 minutes; the page's message) | 1 |
88
+ | `screencapture: browser APIs are unavailable in this environment` (`ENOTSUP` for the header: no page answers `capture.screen`) | 1 |
89
+ | `screencapture: screen capture is not supported in this browser` (`ENOTSUP` from the page, without a message of its own: no `getDisplayMedia`) | 1 |
90
+ | `screencapture: -R: rectangle outside the capture` (the page's message) | 1 |
91
+ | `screencapture: <message>` (any other error of the page, `EIO` or `ENOENT`: the first line of its message, else the errno's text, such as `I/O error`) | 1 |
92
+ | `screencapture: the page sent no image` (an empty reply) | 1 |
93
+ | `screencapture: interrupted` (`SIGHUP`, `SIGINT` or `SIGTERM` during the request: the command stops the reader, so the page's `signal` aborts with `interrupted`, removes its temporary files and writes no file) | `128 + n` |
94
+ | `screencapture: failed to write file: <file>: <message>` | 1 |
95
+ | `screencapture: failed to copy to clipboard: this page does not copy images (no clipboard.write)` (`ENOTSUP`: no page answers `clipboard.write`) | 1 |
96
+ | `screencapture: failed to copy to clipboard: <message>` (the page's message, else the errno's text) | 1 |
97
+ | `screencapture: /dev/embedder not available (needs slicc-kernel >= 1.153.0)` (a kernel that runs the commands without the device) | 1 |
87
98
  | `screencapture: unknown option: <arg>`, then the usage | 2 |
88
99
  | `screencapture: <opt>: not supported in SLICC` | 2 |
89
100
  | `screencapture: -t <format>: not supported in SLICC` | 2 |
90
101
  | `screencapture: -T: invalid delay: <value>`, `-R: invalid rectangle: <value>`, `-D: invalid display: <value>`, `-J: invalid style: <value>` | 2 |
91
102
  | `screencapture: more than one file: not supported in SLICC` | 2 |
92
103
 
93
- As `sccapture`, the messages start with `sccapture:`.
104
+ As `sccapture`, the messages start with `sccapture:`. A line of progress from the page (`progress(line)`) reaches stderr as `<cmd>: <line>` while the command waits.
105
+
106
+ ### The request
107
+
108
+ The commands run in a kernel worker, which has no `getDisplayMedia`, so they ask seven's page over [`/dev/embedder`](https://github.com/ai-ecoverse/slicc-kernel#devembedder) (slicc-kernel 1.153.0, [#1527](https://github.com/ai-ecoverse/slicc-kernel/issues/1527)): one open file description per request, a JSON header line `{ kind, …options }`, then the body, and the reply read to end-of-file. The page side, the prompt, `getDisplayMedia`, the frame, the crop and the encoding, is a page module that slicc-bios registers for `capture.screen` and wraps with its own prompt, user activation, `selfBrowserSurface: 'exclude'` and a check of the picked surface against protected origins; it answers this contract:
94
109
 
95
- **The bridge from the kernel to the page is not settled yet.** The commands run in a kernel worker, which has no `getDisplayMedia`, so a request has to reach seven's page and the frame has to come back. slicc-clipboard does this with a kernel character device that the kernel answers from its embedder's hook; slicc-open, slicc-capture and slicc-audio are agreeing one pattern for all three in [ai-ecoverse/slicc-kernel#1527](https://github.com/ai-ecoverse/slicc-kernel/issues/1527) (pending), with the kernel and seven's embedder (slicc-bios). Until then this section describes only what the user sees. The page side (the prompt, `getDisplayMedia`, the frame, the crop and the encoding) ships in this package for the embedder; its entry point, the transport and the `engines.slicc-kernel` it needs are added here once the pattern is agreed.
110
+ - **`capture.screen`:** the header is plain JSON, with no body, and the reply is the encoded image's bytes, nothing else. The command opens the device read-write (`exec 3<>/dev/embedder`), writes the header from a child `sh` that stays until the reply is read (so the page's progress reaches stderr), and reads the reply with a `cat <&3` that it starts in the background, so that a signal to the command can stop it.
111
+
112
+ | header field | value | from |
113
+ | --- | --- | --- |
114
+ | `kind` | `"capture.screen"` | |
115
+ | `type` | `"image/png"`, `"image/jpeg"` or `"image/webp"` | `-t`, else the file's extension, else PNG; always PNG with `-c` |
116
+ | `quality` | `0.92`, only for JPEG and WebP | |
117
+ | `cursor` | `"always"` or `"never"`, for `getDisplayMedia`'s `cursor` | `-C` |
118
+ | `surface` | `"monitor"` or `"window"`, absent without one, for `displaySurface` (a hint: the user still picks) | `-m` and `-D` give `"monitor"`; `-w`, `-W` and `-J window` give `"window"`; the last one wins |
119
+ | `delay` | seconds as a JSON number (`3`, `0.5`), absent without `-T`; the page waits that long after the picker | `-T` |
120
+ | `rect` | `{ "x", "y", "width", "height" }`, whole numbers in the frame's pixels, absent without `-R`; outside the frame, the page rejects with `-R: rectangle outside the capture` | `-R` |
121
+
122
+ For example, `screencapture -C -T 2 -R 0,0,800,600 shot.jpg` sends `{"kind":"capture.screen","type":"image/jpeg","quality":0.92,"cursor":"always","delay":2,"rect":{"x":0,"y":0,"width":800,"height":600}}`. The page rejects with a `NotAllowedError` or `AbortError` for **Cancel**, a closed picker, a refused permission or a protected surface (`EACCES`); a `NotSupportedError` without `getDisplayMedia` (`ENOTSUP`); and any other error, such as a `TimeoutError` `timed out waiting for the picker` after 5 minutes, with the message the command prints (`EIO`). It aborts the capture when its `signal` does.
123
+ - **`clipboard.write`:** `-c` sends `{"kind":"clipboard.write","type":"image/png"}` and the PNG as the body, and reads the reply, which is empty, to end-of-file. slicc-bios answers it; with no page that does, the header write fails with `ENOTSUP`. The device is opened read-write and the reply read only after the whole body is written, so a page that answers without reading the body ([slicc-kernel#1536](https://github.com/ai-ecoverse/slicc-kernel/issues/1536)) is the only way a body over 1 MiB could block; the embedder's handler reads it.
96
124
 
97
125
  ### Options
98
126
 
@@ -103,8 +131,8 @@ Every option of macOS's `screencapture`, and what it does in SLICC and with `scc
103
131
  | `-a` | don't capture attached windows | no-op | passed | no-op | no-op |
104
132
  | `-b` | capture the Touch Bar | not supported | passed | not supported | not supported |
105
133
  | `-B <bundleid>` | open in that app | not supported | passed | not supported | not supported |
106
- | `-c` | to the clipboard | through slicc-clipboard, as `image/png` | passed | see [Linux](#linux) | `Clipboard.SetImage` |
107
- | `-C` | include the cursor | asks for `cursor: 'always'` (else `'never'`), which the browser may not honour | passed | see [Linux](#linux) | draws the cursor into the image |
134
+ | `-c` | to the clipboard | a `clipboard.write` request, as `image/png` | passed | see [Linux](#linux) | `Clipboard.SetImage` |
135
+ | `-C` | include the cursor | `"cursor": "always"` (else `"never"`), which the browser may not honour | passed | see [Linux](#linux) | draws the cursor into the image |
108
136
  | `-d` | show errors graphically | no-op | passed | no-op | no-op |
109
137
  | `-D <display>` | that display (1 is main) | prefers `displaySurface: 'monitor'`; the user picks the screen | passed | not supported | screen *n*: 1 is the primary, then the others in `AllScreens` order; no such screen is `sccapture: -D <n>: no such display`, exit 1 |
110
138
  | `-g` | record default audio input | not supported | not supported | not supported | not supported |
@@ -126,7 +154,7 @@ Every option of macOS's `screencapture`, and what it does in SLICC and with `scc
126
154
  | `-T <seconds>` | wait before capturing | waits after the picker | passed | `sccapture` waits, then runs the tool | `sccapture` waits, then captures |
127
155
  | `-u` | show the UI afterwards, file ignored | not supported | passed | not supported | not supported |
128
156
  | `-U` | the interactive toolbar | not supported | passed | not supported | not supported |
129
- | `-v` | record a video | inline in the tool result (slicc 6's `-v`) | not supported | not supported | not supported |
157
+ | `-v` | record a video | inline in the tool result, as an `<img:<path>>` line ([slicc-agent#713](https://github.com/ai-ecoverse/slicc-agent/issues/713)) | not supported | not supported | not supported |
130
158
  | `-V <seconds>` | record a video for that long | not supported | not supported | not supported | not supported |
131
159
  | `-w` | window selection only | prefers `displaySurface: 'window'` | passed | see [Linux](#linux) | not supported |
132
160
  | `-W` | start in window selection | prefers `displaySurface: 'window'` | passed | as `-w` | not supported |
@@ -149,7 +177,6 @@ Only `sccapture` is installed on a host (the package's `bin`, a small Node scrip
149
177
  - **The file and its format:** without `-t`, the file's extension picks the format (on macOS, `sccapture` adds `-t` for `.jpg`, `.jpeg`, `.pdf`, `.tiff`, `.gif` and `.bmp`), else PNG. `-c` copies PNG; a file, `-t` and `-P` are then ignored, except on macOS, where `screencapture` decides. Success prints nothing, as `screencapture` does.
150
178
  - **A file is required**, as in SLICC (`sccapture: output file required (or use -c for clipboard)`, exit 2), except on macOS with `-p`, `-u`, `-P`, `-M` or `-B`, which `screencapture` handles without one.
151
179
  - **Errors:** with no tool on `PATH`, `sccapture: no screenshot tool found (tried grim, gnome-screenshot)` (the names tried on that platform) to stderr, exit 1; with `-c` and no clipboard tool, `sccapture: no clipboard tool found (tried wl-copy, xclip)`, exit 1. A tool that fails, a cancelled selection included, has its stderr passed through, exit 1. An unknown option prints `sccapture: unknown option: <arg>` and the usage to stderr, exit 2; an option the table marks as not supported on that platform prints `sccapture: <opt>: not supported on <platform>`, exit 2. The values of `-T`, `-R`, `-D` and `-J` are checked as in SLICC, and `-l` takes a window id in decimal or `0x` hex, else `sccapture: -l: invalid window: <value>`, exit 2. `-h` and `--help` print the usage to stdout and exit 0.
152
- - **`--upgrade`:** the command counts as installed by the installer when it runs from `<data>/<version>/host/sccapture.mjs` with a `<data>/current` next to it, and as installed with npm otherwise. `--dir` is the directory on `PATH` whose `sccapture` (`sccapture.cmd` on Windows) names `<data>`, else the installer's default. It downloads the installer with `curl`, else `wget` (`curl.exe` on Windows), and without one prints `sccapture: no downloader found (tried curl, wget)`, exit 1; a failed download has its stderr passed through, exit 1. The installer runs with `sh` (`powershell.exe`, else `pwsh.exe`, with `-ExecutionPolicy Bypass -File` on Windows), without `VERSION` and `INSTALL_DIR` in its environment, so it installs the latest release where the command is; the new version is read from `<data>/current/package.json`.
153
180
  - **Other Unix systems** are treated as Linux.
154
181
 
155
182
  ### Linux
@@ -182,7 +209,9 @@ Every host command takes `--upgrade`. It runs the latest release's installer (`i
182
209
 
183
210
  For slicc-capture, the commands are `sccapture` on a host and `screencapture` and `sccapture` in SLICC.
184
211
 
185
- `--version` prints the version (`0.1.0`) to stdout and exits 0, on a host and in SLICC; the installers call the launcher with it. It takes no other arguments either.
212
+ The launcher the installer writes sets `SCCAPTURE_INSTALL_DIR` to its own directory; that is how `sccapture` knows the installer put it there (it removes the variable before it starts anything else), and without it `sccapture` counts as installed with npm. The installer is downloaded with Node's `fetch` from `https://github.com/ai-ecoverse/slicc-capture/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`); its stderr is passed through, and the line it prints, `sccapture <version> installed to <launcher>`, names the new version. Without `sh` (or PowerShell) it prints `sccapture: no shell found to run install.sh (tried sh)`, and a failed download `sccapture: could not download <url>: <reason>`, and exits 1. `SLICC_INSTALL_BASE_URL`, when set, replaces that release URL for `--upgrade` and for the installers' own downloads.
213
+
214
+ `--version` prints the version (`0.1.0`) to stdout and exits 0, on a host and in SLICC; the installers run the installed command with it and check it reports the version they installed. It takes no other arguments either.
186
215
 
187
216
  ## Development
188
217
 
@@ -194,9 +223,9 @@ npm run test:unit
194
223
  ```
195
224
 
196
225
  - `npm install` also installs the git hooks into this worktree's own `node_modules/` through slicc-shared-web's `slicc-install-hooks` and the committed `.githooks/`.
197
- - `npm run lint`: Biome, the no-comment check and the other [slicc-shared-web](https://github.com/ai-ecoverse/slicc-shared-web) checks.
198
- - `npm test`: the integration tests in `test/integration/`. `test/integration/repo.test.mjs` checks the workflow, the release config, the pinned devDependencies and what `npm pack` ships. The host tests run `host/` against fake `screencapture`, `grim`, `slurp`, `gnome-screenshot`, `import`, `scrot`, `wl-copy`, `xclip`, `xdg-open`, `powershell.exe`, `pwsh.exe`, `curl`, `wget` and a fake installer on a temporary `PATH`, with the platform forced, so each branch is checked on any machine; only CI touches a real screen.
199
- - `npm run test:unit`: the unit tests in the gitignored `test/unit/`, with coverage in `coverage/unit/lcov.info`. The pre-commit hook wants 100% coverage of the changed lines, and runs `diff-cover` through `uvx`, so install [uv](https://docs.astral.sh/uv/) once.
226
+ - `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` and `bin/*`.
227
+ - `npm test`: the integration tests in `test/integration/`. `commands.test.mjs` runs `screencapture` and `sccapture` on `@ai-ecoverse/slicc-kernel/node` (the version `engines.slicc-kernel` names, pinned) with wasm-bash and wasm-coreutils, and a fake `capture.screen` and `clipboard.write` handler (`createNodeKernel({ requests })`): every option's header, the bytes written unchanged, the errnos and their messages, `-c`, `-v`, and slicc-kernel 1.152.2, which has no `/dev/embedder`. `test/integration/repo.test.mjs` checks the workflow, the release config, the pinned devDependencies and what `npm pack` ships. The host tests run `host/` against fake `screencapture`, `grim`, `slurp`, `gnome-screenshot`, `import`, `scrot`, `wl-copy`, `xclip`, `xdg-open`, `powershell.exe` and `pwsh.exe` on a temporary `PATH`, and `--upgrade` against installers served from a local HTTP server, with the platform forced, so each branch is checked on any machine; only CI touches a real screen. `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 directory; `install.ps1` is checked statically, and run when `pwsh` is on `PATH`.
228
+ - `npm run test:unit`: the unit tests in the gitignored `test/unit/`, with coverage in `coverage/unit/lcov.info`. `npm run test:sh` runs the `test/unit/*.sh.mjs` ones, which trace `bin/` with `set -x` in the node kernel, and writes `coverage/sh/lcov.info`. The pre-commit hook wants 100% coverage of the changed lines of both, and runs `diff-cover` through `uvx`, so install [uv](https://docs.astral.sh/uv/) once.
200
229
 
201
230
  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: `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 the installers.
202
231
 
package/bin/sccapture ADDED
@@ -0,0 +1,2 @@
1
+ #!/bin/sh
2
+ . "${0%/*}/screencapture"
@@ -0,0 +1,310 @@
1
+ #!/bin/sh
2
+ me=${0##*/}
3
+ dev=/dev/embedder
4
+ usage() {
5
+ printf 'usage: %s [-acdimorvwxCW] [-t format] [-T seconds] [-R x,y,w,h] [-D display] [-J style] [file]\n' "$me"
6
+ }
7
+ say() {
8
+ printf '%s: %s\n' "$me" "$1" >&2
9
+ }
10
+ bad() {
11
+ say "$1"
12
+ usage >&2
13
+ exit 2
14
+ }
15
+ refuse() {
16
+ say "$1"
17
+ exit 2
18
+ }
19
+ die() {
20
+ say "$1"
21
+ exit 1
22
+ }
23
+ first() {
24
+ [ -n "$problem" ] || problem=$1
25
+ }
26
+ num() {
27
+ n=$1
28
+ while :; do
29
+ case $n in
30
+ 0[0-9]*) n=${n#0} ;;
31
+ *) break ;;
32
+ esac
33
+ done
34
+ printf '%s' "$n"
35
+ }
36
+ digits() {
37
+ case $1 in
38
+ '' | *[!0-9]*) return 1 ;;
39
+ esac
40
+ }
41
+ delay() {
42
+ case $1 in
43
+ '' | .* | *. | *.*.* | *[!0-9.]*) return 1 ;;
44
+ esac
45
+ }
46
+ rect() {
47
+ case $1 in
48
+ *,*,*,*,*) return 1 ;;
49
+ *,*,*,*) ;;
50
+ *) return 1 ;;
51
+ esac
52
+ rx=${1%%,*}
53
+ rest=${1#*,}
54
+ ry=${rest%%,*}
55
+ rest=${rest#*,}
56
+ rw=${rest%%,*}
57
+ rh=${rest#*,}
58
+ digits "$rx" && digits "$ry" && digits "$rw" && digits "$rh" || return 1
59
+ [ "$(num "$rw")" != 0 ] && [ "$(num "$rh")" != 0 ]
60
+ }
61
+ format() {
62
+ case $1 in
63
+ [pP][nN][gG]) printf png ;;
64
+ [jJ][pP][gG] | [jJ][pP][eE][gG]) printf jpeg ;;
65
+ [wW][eE][bB][pP]) printf webp ;;
66
+ esac
67
+ }
68
+ value() {
69
+ case $1 in
70
+ T)
71
+ delay "$2" || first "-T: invalid delay: $2"
72
+ wait=$2
73
+ ;;
74
+ R)
75
+ rect "$2" || first "-R: invalid rectangle: $2"
76
+ area=$2
77
+ ;;
78
+ D)
79
+ case $2 in
80
+ [1-9]*) digits "$2" || first "-D: invalid display: $2" ;;
81
+ *) first "-D: invalid display: $2" ;;
82
+ esac
83
+ surface=monitor
84
+ ;;
85
+ J)
86
+ case $2 in
87
+ window) surface=window ;;
88
+ selection | video) style=${style:-"-J $2: not supported in SLICC"} ;;
89
+ *) first "-J: invalid style: $2" ;;
90
+ esac
91
+ ;;
92
+ t) type=$2 ;;
93
+ *) off=${off:-"-$1"} ;;
94
+ esac
95
+ }
96
+ flag() {
97
+ case $1 in
98
+ c) clip=1 ;;
99
+ C) cursor=always ;;
100
+ v) view=1 ;;
101
+ m) surface=monitor ;;
102
+ w | W) surface=window ;;
103
+ a | d | i | o | r | x) ;;
104
+ *) off=${off:-"-$1"} ;;
105
+ esac
106
+ }
107
+ version() {
108
+ while IFS= read -r line; do
109
+ case $line in
110
+ *'"version": "'*)
111
+ line=${line#*'"version": "'}
112
+ printf '%s\n' "${line%%\"*}"
113
+ return
114
+ ;;
115
+ esac
116
+ done <"${0%/*}/../package.json"
117
+ }
118
+ relay() {
119
+ while IFS= read -r line; do
120
+ case $line in
121
+ *": $dev: "*) printf '%s\n' "${line#*": $dev: "}" >"$work/message" ;;
122
+ *": printf: write error: "*) printf '%s\n' "${line##*: }" >"$work/header" ;;
123
+ "cat: -: "* | "cat: write error: "*) printf '%s\n' "${line##*: }" >"$work/errno" ;;
124
+ *) printf '%s: %s\n' "$me" "${line#*: }" >&2 ;;
125
+ esac
126
+ done
127
+ }
128
+ ask() {
129
+ rm -f "$work/message" "$work/header" "$work/errno"
130
+ { { "$@"; } 2>&1 >&5 | relay; } 5>&1 &
131
+ wait "$!"
132
+ header=
133
+ errno=
134
+ message=
135
+ [ ! -f "$work/header" ] || header=$(cat "$work/header")
136
+ [ ! -f "$work/errno" ] || errno=$(cat "$work/errno")
137
+ [ ! -f "$work/message" ] || message=$(cat "$work/message")
138
+ errno=${errno:-$header}
139
+ [ -z "$errno" ]
140
+ }
141
+ because() {
142
+ printf '%s' "${message:-$errno}"
143
+ }
144
+ copyfail() {
145
+ case $header in
146
+ "Not supported") die "failed to copy to clipboard: this page does not copy images (no clipboard.write)" ;;
147
+ esac
148
+ die "failed to copy to clipboard: $(because)"
149
+ }
150
+ stop() {
151
+ [ ! -s "$work/pid" ] || kill -TERM "$(cat "$work/pid")" 2>/dev/null
152
+ wait
153
+ say interrupted
154
+ exit $((128 + $1))
155
+ }
156
+ absolute() {
157
+ case $1 in
158
+ */*) dir=${1%/*} base=${1##*/} ;;
159
+ *) dir=. base=$1 ;;
160
+ esac
161
+ dir=$(cd -- "${dir:-/}" && pwd)
162
+ printf '%s/%s' "${dir%/}" "$base"
163
+ }
164
+ cleanup() {
165
+ rm -rf "$work"
166
+ }
167
+ argc=$#
168
+ clip=
169
+ view=
170
+ cursor=never
171
+ surface=
172
+ wait=
173
+ area=
174
+ type=
175
+ style=
176
+ off=
177
+ problem=
178
+ wrong=
179
+ special=
180
+ help=
181
+ ended=
182
+ files=0
183
+ file=
184
+ while [ $# -gt 0 ]; do
185
+ arg=$1
186
+ shift
187
+ if [ -n "$ended" ]; then
188
+ files=$((files + 1))
189
+ file=${file:-$arg}
190
+ continue
191
+ fi
192
+ case $arg in
193
+ --) ended=1 ;;
194
+ --help) help=1 ;;
195
+ --version | --upgrade) special=${special:-$arg} ;;
196
+ --clipboard) clip=1 ;;
197
+ --view) view=1 ;;
198
+ --*) wrong=${wrong:-"unknown option: $arg"} ;;
199
+ -?*)
200
+ rest=${arg#-}
201
+ while [ -n "$rest" ]; do
202
+ letter=${rest%"${rest#?}"}
203
+ rest=${rest#?}
204
+ case $letter in
205
+ h) help=1 ;;
206
+ [BDGJlRtTV])
207
+ if [ -n "$rest" ]; then
208
+ value "$letter" "$rest"
209
+ elif [ $# -gt 0 ]; then
210
+ value "$letter" "$1"
211
+ shift
212
+ else
213
+ wrong=${wrong:-"-$letter: option requires a value"}
214
+ fi
215
+ rest=
216
+ ;;
217
+ [abcCdgikmMoprPsSuUvwWx]) flag "$letter" ;;
218
+ *)
219
+ wrong=${wrong:-"unknown option: -$letter"}
220
+ rest=
221
+ ;;
222
+ esac
223
+ done
224
+ ;;
225
+ *)
226
+ files=$((files + 1))
227
+ file=${file:-$arg}
228
+ ;;
229
+ esac
230
+ done
231
+ if [ -n "$help" ]; then
232
+ usage
233
+ exit 0
234
+ fi
235
+ [ -z "$wrong" ] || bad "$wrong"
236
+ if [ -n "$special" ]; then
237
+ [ "$argc" -eq 1 ] || bad "$special: takes no other arguments"
238
+ if [ "$special" = --upgrade ]; then
239
+ printf 'pnpm add -g @ai-ecoverse/slicc-capture@latest\n'
240
+ else
241
+ version
242
+ fi
243
+ exit 0
244
+ fi
245
+ [ -z "$off" ] || refuse "$off: not supported in SLICC"
246
+ [ -z "$problem" ] || refuse "$problem"
247
+ [ -z "$style" ] || refuse "$style"
248
+ mime=png
249
+ if [ -n "$clip" ]; then
250
+ file=
251
+ files=0
252
+ elif [ -n "$type" ]; then
253
+ mime=$(format "$type")
254
+ [ -n "$mime" ] || refuse "-t $type: not supported in SLICC"
255
+ else
256
+ case $file in
257
+ *.[jJ][pP][gG] | *.[jJ][pP][eE][gG]) mime=jpeg ;;
258
+ *.[wW][eE][bB][pP]) mime=webp ;;
259
+ esac
260
+ fi
261
+ [ "$files" -le 1 ] || refuse "more than one file: not supported in SLICC"
262
+ [ -n "$file" ] || [ -n "$clip" ] || [ -n "$view" ] || bad "output file required (or use -c for clipboard)"
263
+ [ -c "$dev" ] || die "$dev not available (needs slicc-kernel >= 1.153.0)"
264
+ request="{\"kind\":\"capture.screen\",\"type\":\"image/$mime\""
265
+ [ "$mime" = png ] || request="$request,\"quality\":0.92"
266
+ request="$request,\"cursor\":\"$cursor\""
267
+ [ -z "$surface" ] || request="$request,\"surface\":\"$surface\""
268
+ [ -z "$wait" ] || request="$request,\"delay\":$(num "${wait%%.*}")${wait#"${wait%%.*}"}"
269
+ [ -z "$area" ] || request="$request,\"rect\":{\"x\":$(num "$rx"),\"y\":$(num "$ry"),\"width\":$(num "$rw"),\"height\":$(num "$rh")}"
270
+ request="$request}"
271
+ work=$(mktemp -d)
272
+ trap cleanup EXIT
273
+ trap 'stop 1' HUP
274
+ trap 'stop 2' INT
275
+ trap 'stop 15' TERM
276
+ if [ -n "$clip" ]; then
277
+ exec 4<>"$dev"
278
+ ask sh -c "printf '%s\\n' \"\$1\" >&4" sh '{"kind":"clipboard.write","type":"image/png"}' || copyfail
279
+ fi
280
+ exec 3<>"$dev"
281
+ if ! ask sh -c "printf '%s\\n' \"\$1\" >&3 || exit 1; cat <&3 & echo \$! >\"\$2\"; wait \$!" sh "$request" "$work/pid" >"$work/image"; then
282
+ case $header in
283
+ "Not supported") die "browser APIs are unavailable in this environment" ;;
284
+ esac
285
+ case $errno in
286
+ "Permission denied") die "user cancelled, permission denied, or the surface is protected" ;;
287
+ "Not supported") die "${message:-screen capture is not supported in this browser}" ;;
288
+ esac
289
+ die "$(because)"
290
+ fi
291
+ exec 3<&-
292
+ size=$(wc -c <"$work/image")
293
+ [ "$size" -gt 0 ] || die "the page sent no image"
294
+ kb=$(((size + 512) / 1024))
295
+ if [ -n "$clip" ]; then
296
+ ask cat "$work/image" >&4 || copyfail
297
+ ask sh -c 'cat <&4' >/dev/null || copyfail
298
+ exec 4<&-
299
+ printf 'captured %s KB to clipboard\n' "$kb"
300
+ fi
301
+ if [ -z "$file" ]; then
302
+ [ -n "$view" ] || exit 0
303
+ file=$(mktemp "${TMPDIR:-/tmp}/screencapture-XXXXXX.$mime")
304
+ fi
305
+ err=$(tee -- "$file" <"$work/image" 2>&1 >/dev/null) || die "failed to write file: ${err#tee: }"
306
+ if [ -n "$view" ]; then
307
+ printf '<img:%s>\n' "$(absolute "$file")"
308
+ elif [ -z "$clip" ]; then
309
+ printf 'captured %s KB to %s\n' "$kb" "$file"
310
+ fi
package/host/tools.mjs CHANGED
@@ -33,7 +33,7 @@ export function spawned(path, args, { env, stdin = 'ignore', capture = false })
33
33
  let child;
34
34
  try {
35
35
  child = spawn(path, args, {
36
- env,
36
+ env: { ...env, SCCAPTURE_INSTALL_DIR: undefined },
37
37
  stdio: [stdin, capture ? 'pipe' : 'ignore', 'pipe'],
38
38
  shell: false,
39
39
  windowsHide: true,
package/host/upgrade.mjs CHANGED
@@ -1,107 +1,101 @@
1
- import { existsSync, mkdtempSync, readFileSync, realpathSync, rmSync } from 'node:fs';
2
- import { homedir, tmpdir } from 'node:os';
3
- import { basename, dirname, join } from 'node:path';
4
- import { dirs, lookup, say, step } from './tools.mjs';
1
+ import { spawn } from 'node:child_process';
2
+ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { dirname, join } from 'node:path';
5
+ import { lookup, say } from './tools.mjs';
5
6
 
6
- const base = 'https://github.com/ai-ecoverse/slicc-capture/releases/latest/download';
7
+ const latest = 'https://github.com/ai-ecoverse/slicc-capture/releases/latest/download';
7
8
 
8
- function version(dir) {
9
- return JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8')).version;
9
+ function fail(fds, text) {
10
+ say(fds[2], `sccapture: ${text}\n`);
11
+ return 1;
10
12
  }
11
13
 
12
- function home(platform, env) {
13
- if (platform === 'win32') return join(env.LOCALAPPDATA ?? homedir(), 'slicc-capture');
14
- return join(env.XDG_DATA_HOME || join(env.HOME ?? homedir(), '.local', 'share'), 'slicc-capture');
14
+ function version(entry) {
15
+ return JSON.parse(readFileSync(join(dirname(dirname(entry)), 'package.json'), 'utf8')).version;
15
16
  }
16
17
 
17
- function real(path) {
18
+ async function download(url) {
18
19
  try {
19
- return realpathSync(path);
20
- } catch {
21
- return path;
20
+ const response = await fetch(url, { redirect: 'follow' });
21
+ if (!response.ok) return { error: `HTTP ${response.status}` };
22
+ return { text: await response.text() };
23
+ } catch (error) {
24
+ return { error: error.cause?.message ?? error.message };
22
25
  }
23
26
  }
24
27
 
25
- function launcher(platform, env, data) {
26
- const fallback =
27
- platform === 'win32'
28
- ? join(home(platform, env), 'bin')
29
- : join(env.HOME ?? homedir(), '.local', 'bin');
30
- const name = platform === 'win32' ? 'sccapture.cmd' : 'sccapture';
31
- const marks = [data];
32
- if (real(home(platform, env)) === data) marks.push(home(platform, env));
33
- for (const dir of [...dirs(platform, env), fallback]) {
34
- let text;
35
- try {
36
- text = readFileSync(join(dir, name), 'utf8');
37
- } catch {
38
- continue;
39
- }
40
- if (marks.some((mark) => text.includes(mark))) return dir;
28
+ function first(files, platform, env) {
29
+ for (const file of files) {
30
+ const path = lookup(file, platform, env);
31
+ if (path) return { file, path };
41
32
  }
42
- return fallback;
33
+ return undefined;
43
34
  }
44
35
 
45
- export function installed(entry) {
46
- const data = dirname(dirname(dirname(real(entry))));
47
- if (basename(data) !== 'slicc-capture' || !existsSync(join(data, 'current'))) return undefined;
48
- return data;
36
+ function installer(platform, env, path, dir) {
37
+ if (platform === 'win32') {
38
+ const tool = first(['powershell.exe', 'pwsh.exe'], platform, env);
39
+ const args = ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File', path];
40
+ return tool && { tool, args: [...args, '-Dir', dir] };
41
+ }
42
+ const tool = first(['sh'], platform, env);
43
+ return tool && { tool, args: [path, '--dir', dir] };
44
+ }
45
+
46
+ function wait(path, args, { env, stderr }) {
47
+ return new Promise((resolve) => {
48
+ let child;
49
+ try {
50
+ child = spawn(path, args, {
51
+ env,
52
+ stdio: ['ignore', 'pipe', stderr],
53
+ shell: false,
54
+ windowsHide: true,
55
+ });
56
+ } catch (error) {
57
+ resolve({ error });
58
+ return;
59
+ }
60
+ const out = [];
61
+ child.stdout.on('data', (chunk) => out.push(chunk));
62
+ child.on('error', (error) => resolve({ error }));
63
+ child.on('close', (code) => resolve({ code: code ?? 1, stdout: Buffer.concat(out) }));
64
+ });
49
65
  }
50
66
 
51
67
  export async function upgrade({ entry, env, platform, fds }) {
52
- const root = dirname(dirname(entry));
53
- const old = version(root);
54
- const data = installed(entry);
55
- if (!data) {
68
+ const dir = env.SCCAPTURE_INSTALL_DIR;
69
+ if (!dir) {
56
70
  say(fds[1], 'npm install -g @ai-ecoverse/slicc-capture@latest\n');
57
71
  return 0;
58
72
  }
59
- const windows = platform === 'win32';
60
- const getters = windows ? ['curl.exe'] : ['curl', 'wget'];
61
- const getter = getters
62
- .map((name) => ({ name, path: lookup(name, platform, env) }))
63
- .find((g) => g.path);
64
- if (!getter) {
65
- say(fds[2], `sccapture: no downloader found (tried ${getters.join(', ')})\n`);
66
- return 1;
67
- }
68
- const shells = windows ? ['powershell.exe', 'pwsh.exe'] : ['sh'];
69
- const shell =
70
- shells.map((name) => lookup(name, platform, env)).find(Boolean) ??
71
- (windows ? undefined : '/bin/sh');
72
- if (!shell) {
73
- say(fds[2], `sccapture: no PowerShell found (tried ${shells.join(', ')})\n`);
74
- return 1;
75
- }
76
- const child = {};
77
- for (const [key, value] of Object.entries(env)) {
78
- if (!['VERSION', 'INSTALL_DIR'].includes(key.toUpperCase())) child[key] = value;
79
- }
73
+ const name = platform === 'win32' ? 'install.ps1' : 'install.sh';
74
+ const url = `${(env.SLICC_INSTALL_BASE_URL || latest).replace(/\/+$/, '')}/${name}`;
75
+ const fetched = await download(url);
76
+ if (fetched.error) return fail(fds, `could not download ${url}: ${fetched.error}`);
80
77
  const scratch = mkdtempSync(join(tmpdir(), 'sccapture-upgrade-'));
81
78
  try {
82
- const name = windows ? 'install.ps1' : 'install.sh';
83
- const script = join(scratch, name);
84
- const url = `${base}/${name}`;
85
- const fetch = getter.name === 'wget' ? ['-q', '-O', script, url] : ['-fsSL', '-o', script, url];
86
- if (!(await step(getter.name, getter.path, fetch, { env: child }, fds))) return 1;
87
- const dir = launcher(platform, env, data);
88
- const args = windows
89
- ? [
90
- '-NoProfile',
91
- '-NonInteractive',
92
- '-ExecutionPolicy',
93
- 'Bypass',
94
- '-File',
95
- script,
96
- '--dir',
97
- dir,
98
- ]
99
- : [script, '--dir', dir];
100
- if (!(await step(name, shell, args, { env: child }, fds))) return 1;
101
- const now = version(join(data, 'current'));
79
+ const path = join(scratch, name);
80
+ writeFileSync(path, fetched.text);
81
+ const run = installer(platform, env, path, dir);
82
+ if (!run) {
83
+ const tried = platform === 'win32' ? 'powershell.exe, pwsh.exe' : 'sh';
84
+ return fail(fds, `no shell found to run ${name} (tried ${tried})`);
85
+ }
86
+ const child = { ...env };
87
+ delete child.SCCAPTURE_INSTALL_DIR;
88
+ const result = await wait(run.tool.path, run.args, { env: child, stderr: fds[2] });
89
+ if (result.error) return fail(fds, `${run.tool.file}: ${result.error.message}`);
90
+ if (result.code !== 0) return 1;
91
+ const installed = /^sccapture (\S+) installed to /m.exec(result.stdout.toString())?.[1];
92
+ if (!installed) return fail(fds, `${name} did not say which version it installed`);
93
+ const old = version(entry);
102
94
  say(
103
95
  fds[1],
104
- now === old ? `sccapture: already up to date (${old})\n` : `sccapture: ${old} → ${now}\n`
96
+ installed === old
97
+ ? `sccapture: already up to date (${old})\n`
98
+ : `sccapture: ${old} → ${installed}\n`
105
99
  );
106
100
  return 0;
107
101
  } finally {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-ecoverse/slicc-capture",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "screencapture for SLICC, and sccapture for SLICC and any host",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -9,7 +9,8 @@
9
9
  "url": "git+https://github.com/ai-ecoverse/slicc-capture.git"
10
10
  },
11
11
  "engines": {
12
- "node": ">=24"
12
+ "node": ">=24",
13
+ "slicc-kernel": ">=1.153.0"
13
14
  },
14
15
  "bin": {
15
16
  "sccapture": "host/sccapture.mjs"
@@ -25,29 +26,44 @@
25
26
  "provenance": true
26
27
  },
27
28
  "slicc": {
29
+ "commands": {
30
+ "screencapture": {
31
+ "script": "bin/screencapture"
32
+ },
33
+ "sccapture": {
34
+ "script": "bin/sccapture"
35
+ }
36
+ },
28
37
  "unitSources": [
29
38
  "host"
30
39
  ]
31
40
  },
32
41
  "files": [
42
+ "bin",
33
43
  "host"
34
44
  ],
35
45
  "scripts": {
36
46
  "prepare": "slicc-install-hooks",
37
- "lint": "slicc-lint",
47
+ "lint": "slicc-lint && shellcheck -s sh -e SC1091 install.sh bin/* && for f in install.sh bin/*; do dash -n \"$f\" || exit 1; done",
38
48
  "format": "biome check --write .",
39
49
  "test": "node --test --test-timeout=120000 test/integration/*.test.mjs",
40
50
  "test:unit": "mcr -c node_modules/@ai-ecoverse/slicc-shared-web/mcr.config.mjs node --test 'test/unit/**/*.test.mjs'",
51
+ "test:sh": "node scripts/cover.mjs",
41
52
  "build": "npm pack --dry-run",
42
53
  "trust": "npx fledgling sync -y --skip-publish",
43
54
  "trust:dry": "npx fledgling sync --dry-run -y --skip-publish",
44
55
  "release": "semantic-release"
45
56
  },
46
57
  "devDependencies": {
58
+ "@ai-ecoverse/slicc-kernel": "1.153.0",
47
59
  "@ai-ecoverse/slicc-shared-web": "2.11.3",
60
+ "@ai-ecoverse/wasm-bash": "5.3.0-23",
61
+ "@ai-ecoverse/wasm-coreutils": "9.12.0-15",
48
62
  "@biomejs/biome": "2.5.15",
63
+ "@semantic-release/exec": "7.1.0",
49
64
  "lefthook": "2.2.2",
50
65
  "monocart-coverage-reports": "2.13.2",
51
- "semantic-release": "25.0.9"
66
+ "semantic-release": "25.0.9",
67
+ "slicc-kernel-without-embedder": "npm:@ai-ecoverse/slicc-kernel@1.152.2"
52
68
  }
53
69
  }