@ai-ecoverse/slicc-capture 1.0.0 → 1.1.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
@@ -19,7 +19,7 @@ In SLICC, the package declares `screencapture` and `sccapture` in `slicc.command
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
 
@@ -149,7 +155,6 @@ Only `sccapture` is installed on a host (the package's `bin`, a small Node scrip
149
155
  - **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
156
  - **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
157
  - **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
158
  - **Other Unix systems** are treated as Linux.
154
159
 
155
160
  ### Linux
@@ -182,7 +187,9 @@ Every host command takes `--upgrade`. It runs the latest release's installer (`i
182
187
 
183
188
  For slicc-capture, the commands are `sccapture` on a host and `screencapture` and `sccapture` in SLICC.
184
189
 
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.
190
+ 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.
191
+
192
+ `--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
193
 
187
194
  ## Development
188
195
 
@@ -194,8 +201,8 @@ npm run test:unit
194
201
  ```
195
202
 
196
203
  - `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.
204
+ - `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`.
205
+ - `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` 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`.
199
206
  - `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.
200
207
 
201
208
  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.
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.1.0",
4
4
  "description": "screencapture for SLICC, and sccapture for SLICC and any host",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -34,7 +34,7 @@
34
34
  ],
35
35
  "scripts": {
36
36
  "prepare": "slicc-install-hooks",
37
- "lint": "slicc-lint",
37
+ "lint": "slicc-lint && shellcheck -s sh install.sh && dash -n install.sh",
38
38
  "format": "biome check --write .",
39
39
  "test": "node --test --test-timeout=120000 test/integration/*.test.mjs",
40
40
  "test:unit": "mcr -c node_modules/@ai-ecoverse/slicc-shared-web/mcr.config.mjs node --test 'test/unit/**/*.test.mjs'",
@@ -46,6 +46,7 @@
46
46
  "devDependencies": {
47
47
  "@ai-ecoverse/slicc-shared-web": "2.11.3",
48
48
  "@biomejs/biome": "2.5.15",
49
+ "@semantic-release/exec": "7.1.0",
49
50
  "lefthook": "2.2.2",
50
51
  "monocart-coverage-reports": "2.13.2",
51
52
  "semantic-release": "25.0.9"