@zergai/cyberdeck 0.2.0-beta.1 → 0.2.1-dev.35760740177

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
@@ -282,3 +282,20 @@ response is provisioning, not execution success or evidence readiness. Keep its
282
282
  exact run ID/URL; inspect `run-show` for execution, artifact status, expiry, and
283
283
  authorized download URLs. Evaluator access remains enforced by the server.
284
284
  Control commands request a state change; inspect the run to confirm it happened.
285
+
286
+ ## Updating the npm CLI
287
+
288
+ Run `zcd update` or `zcd --update` to install a newer npm
289
+ release. `zcd update --check` prints JSON without installing. The
290
+ installed channel is preserved (`dev`, `next`, or `latest`); select a channel
291
+ explicitly with `update --channel dev|next|latest`. Updates never downgrade.
292
+
293
+ Startup checks only notify on stderr when it is a terminal, never prompt or
294
+ install, and are cached for one hour. CI, redirected stderr, and help/version
295
+ requests skip checks. Set `ZERGAI_NO_UPDATE_CHECK=1` to disable startup checks.
296
+ Self-installation requires the matching macOS/Linux global npm prefix; local,
297
+ linked, npx, or other package-manager installs must use their original manager.
298
+ Existing releases need one ordinary npm upgrade before these commands exist.
299
+
300
+ See [shared updater policy](https://github.com/Epoch-ML/zerg/blob/development/docs/npm-cli-updates.md)
301
+ for installation safety, channel switching, and packaging details.
@@ -0,0 +1,21 @@
1
+ 'use strict';
2
+ const { execFile, spawn } = require('node:child_process');
3
+ const { promisify } = require('node:util');
4
+
5
+ // Kept separate so OS process contracts can be exercised independently from the
6
+ // version/cache policy. This is copied alongside the updater in each artifact.
7
+ module.exports = async function runNpm(args, options) {
8
+ if (args[0] === 'prefix') return promisify(execFile)('npm', args, { ...options, encoding: 'utf8', timeout: 5000, maxBuffer: 16384 });
9
+ await new Promise((done, fail) => {
10
+ const child = spawn('npm', args, { ...options, stdio: ['ignore', 'inherit', 'inherit'], shell: false });
11
+ // npm shares the foreground group; a terminal has already signalled it.
12
+ const relay = ![process.stdin, process.stdout, process.stderr].some(stream => stream.isTTY);
13
+ const interrupt = () => { if (relay) child.kill('SIGINT'); };
14
+ const terminate = () => { if (relay) child.kill('SIGTERM'); };
15
+ process.on('SIGINT', interrupt); process.on('SIGTERM', terminate);
16
+ const cleanup = () => { process.off('SIGINT', interrupt); process.off('SIGTERM', terminate); };
17
+ child.on('error', error => { cleanup(); fail(error); });
18
+ child.on('exit', (code, signal) => { cleanup(); if (code === 0) done(); else fail(new Error(`npm exited ${signal || code}`)); });
19
+ });
20
+ return { stdout: '' };
21
+ };
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ const { spawn } = require('node:child_process');
4
+ const { join } = require('node:path');
5
+ const { handleUpdate } = require('./update.cjs');
6
+
7
+ async function main() {
8
+ const root = join(__dirname, '..');
9
+ const args = process.argv.slice(2);
10
+ const result = await handleUpdate({ root, args });
11
+ if (result !== null) { process.exitCode = result; return; }
12
+ if (args.length === 1 && ['--help', '-h', 'help'].includes(args[0])) {
13
+ process.stdout.write('Updates: update | --update [--check] [--channel dev|next|latest]\n');
14
+ }
15
+ const metadata = require('../package.json');
16
+ // ZTC's old TUI updaters must not install silently behind this launch policy.
17
+ const env = { ...process.env };
18
+ if (['@zergai/ztc', 'zerg-ztc'].includes(metadata.name)) {
19
+ env.ZTC_NO_AUTO_UPDATE = '1'; env.ZTC_DISABLE_UPDATE = '1';
20
+ }
21
+ const child = spawn(process.execPath, [join(root, metadata.zergCli.entry), ...args], { stdio: 'inherit', env });
22
+ // Attached terminals signal the foreground group, including the child. Relay
23
+ // only for non-TTY supervisors; keep the group intact for terminal resize.
24
+ const relay = ![process.stdin, process.stdout, process.stderr].some(stream => stream.isTTY);
25
+ const interrupt = () => { if (relay) child.kill('SIGINT'); };
26
+ const terminate = () => { if (relay) child.kill('SIGTERM'); };
27
+ process.on('SIGINT', interrupt); process.on('SIGTERM', terminate);
28
+ const cleanup = () => { process.off('SIGINT', interrupt); process.off('SIGTERM', terminate); };
29
+ child.on('error', error => { cleanup(); console.error(`Unable to launch ${metadata.name}: ${error.message}`); process.exitCode = 1; });
30
+ child.on('exit', (code, signal) => {
31
+ cleanup();
32
+ if (signal) process.kill(process.pid, signal);
33
+ else process.exitCode = code ?? 1;
34
+ });
35
+ }
36
+ main().catch(error => { console.error(error.message); process.exitCode = 1; });
@@ -0,0 +1,161 @@
1
+ 'use strict';
2
+
3
+ // Copied into each npm CLI at pack time. No repo paths or runtime dependencies.
4
+ const runNpm = require('./npm-process.cjs');
5
+ const { readFile, realpath, lstat, mkdir, mkdtemp, writeFile, rename, rm } = require('node:fs/promises');
6
+ const { homedir } = require('node:os');
7
+ const { join, resolve, isAbsolute } = require('node:path');
8
+ const registry = 'https://registry.npmjs.org';
9
+ const cacheLifetime = 60 * 60 * 1000;
10
+
11
+ function parseVersion(version) {
12
+ const match = typeof version === 'string' && version.length <= 256 && /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/.exec(version);
13
+ if (!match || match.slice(1, 4).some(n => !Number.isSafeInteger(Number(n))) || (match[4] || '').split('.').some(n => /^0\d+$/.test(n))) {
14
+ throw new Error('Invalid semantic version');
15
+ }
16
+ return { core: match.slice(1, 4).map(Number), pre: match[4] ? match[4].split('.') : [] };
17
+ }
18
+
19
+ function compareVersions(left, right) {
20
+ const a = parseVersion(left), b = parseVersion(right);
21
+ for (let i = 0; i < 3; i++) if (a.core[i] !== b.core[i]) return a.core[i] > b.core[i] ? 1 : -1;
22
+ if (!a.pre.length && b.pre.length) return 1;
23
+ if (a.pre.length && !b.pre.length) return -1;
24
+ for (let i = 0; i < Math.max(a.pre.length, b.pre.length); i++) {
25
+ if (a.pre[i] === b.pre[i]) continue;
26
+ if (a.pre[i] === undefined) return -1;
27
+ if (b.pre[i] === undefined) return 1;
28
+ const an = /^\d+$/.test(a.pre[i]), bn = /^\d+$/.test(b.pre[i]);
29
+ if (an !== bn) return an ? -1 : 1;
30
+ return (an ? BigInt(a.pre[i]) > BigInt(b.pre[i]) : a.pre[i] > b.pre[i]) ? 1 : -1;
31
+ }
32
+ return 0;
33
+ }
34
+
35
+ function releaseChannel(version) {
36
+ const { pre } = parseVersion(version);
37
+ return pre[0] === 'dev' ? 'dev' : pre.length ? 'next' : 'latest';
38
+ }
39
+
40
+ function identity(metadata) {
41
+ if (!/^@zergai\/[a-z0-9][a-z0-9-]*$/.test(metadata.name) && !['zerg-ztc', 'zerg-cyberdeck'].includes(metadata.name)) {
42
+ throw new Error('Unsupported CLI package identity');
43
+ }
44
+ parseVersion(metadata.version);
45
+ }
46
+
47
+ async function checkUpdate(metadata, { fetch = globalThis.fetch, channel = releaseChannel(metadata.version), timeoutMs = 5000 } = {}) {
48
+ identity(metadata);
49
+ if (!['dev', 'next', 'latest'].includes(channel)) throw new Error('Invalid update channel');
50
+ const response = await fetch(`${registry}/${encodeURIComponent(metadata.name)}/${channel}`, {
51
+ headers: { Accept: 'application/json' }, redirect: 'error', signal: AbortSignal.timeout(timeoutMs),
52
+ });
53
+ if (!response.ok) throw new Error(`npm registry check failed (${response.status})`);
54
+ const reader = response.body.getReader();
55
+ const chunks = []; let size = 0;
56
+ try {
57
+ while (true) {
58
+ const { done, value } = await reader.read();
59
+ if (done) break;
60
+ size += value.byteLength;
61
+ if (size > 1024 * 1024) throw new Error('npm registry response too large');
62
+ chunks.push(Buffer.from(value));
63
+ }
64
+ } finally { await reader.cancel(); }
65
+ const data = JSON.parse(Buffer.concat(chunks).toString('utf8'));
66
+ if (data.name !== metadata.name) throw new Error('npm registry package identity mismatch');
67
+ if (releaseChannel(data.version) !== channel) throw new Error('npm registry version does not match the requested channel');
68
+ return { name: metadata.name, current: metadata.version, latest: data.version, channel, hasUpdate: compareVersions(data.version, metadata.version) > 0 };
69
+ }
70
+
71
+ function updateOptions(args) {
72
+ const options = { check: false };
73
+ for (let i = 1; i < args.length; i++) {
74
+ const arg = args[i];
75
+ if (arg === '--check' && !options.check) options.check = true;
76
+ else if (arg === '--channel' && !options.channel && ['dev', 'next', 'latest'].includes(args[i + 1])) options.channel = args[++i];
77
+ else if (['--help', '-h'].includes(arg) && args.length === 2) options.help = true;
78
+ else throw new Error('Usage: update [--check] [--channel dev|next|latest]');
79
+ }
80
+ return options;
81
+ }
82
+
83
+ async function install(root, metadata, info, npm, env) {
84
+ const { stdout } = await npm(['prefix', '--global'], { env });
85
+ const prefix = stdout.trim();
86
+ if (!isAbsolute(prefix)) throw new Error('Cannot identify the global npm installation');
87
+ const expected = join(prefix, 'lib/node_modules', metadata.name);
88
+ // Compare real paths AND reject npm link. Never turn npx/local/checkouts into a
89
+ // global install or update a different Node-version manager's global prefix.
90
+ const matches = await (async () => !(await lstat(expected)).isSymbolicLink() && await realpath(root) === await realpath(expected))().catch(() => false);
91
+ if (!matches) {
92
+ throw new Error('Not this global npm installation. Update using the package manager/prefix that installed this CLI.');
93
+ }
94
+ await npm(['install', '--global', '--prefix', prefix, `--registry=${registry}`, `--@zergai:registry=${registry}`, '--no-audit', '--no-fund', `${metadata.name}@${info.latest}`], { cwd: prefix, env });
95
+ const installed = JSON.parse(await readFile(join(root, 'package.json'), 'utf8'));
96
+ if (installed.name !== metadata.name || installed.version !== info.latest) throw new Error('npm did not install the requested package version');
97
+ }
98
+
99
+ async function startupCheck(metadata, env, fetch) {
100
+ const channel = releaseChannel(metadata.version);
101
+ const directory = join(env.XDG_CACHE_HOME || join(homedir(), '.cache'), 'zergai-cli-updates');
102
+ const path = join(directory, `${encodeURIComponent(metadata.name)}-${channel}.json`);
103
+ try {
104
+ const cached = JSON.parse(await readFile(path, 'utf8'));
105
+ const age = Date.now() - cached.checkedAt;
106
+ if (Number.isFinite(age) && age >= 0 && age < cacheLifetime && cached.current === metadata.version) {
107
+ if (cached.latest === null) return null;
108
+ if (releaseChannel(cached.latest) === channel) return { latest: cached.latest, hasUpdate: compareVersions(cached.latest, metadata.version) > 0 };
109
+ }
110
+ } catch { /* Missing, stale, or malformed cache: do a bounded public check. */ }
111
+ let info = null;
112
+ try { info = await checkUpdate(metadata, { fetch, timeoutMs: 800 }); }
113
+ catch { /* A registry outage must not prevent use of a CLI. */ }
114
+ let staging;
115
+ try {
116
+ await mkdir(directory, { recursive: true, mode: 0o700 });
117
+ staging = await mkdtemp(join(directory, '.check-'));
118
+ const source = join(staging, 'record');
119
+ await writeFile(source, JSON.stringify({ current: metadata.version, latest: info?.latest ?? null, checkedAt: Date.now() }), { flag: 'wx', mode: 0o600 });
120
+ await rename(source, path);
121
+ } catch { /* Read-only home/cache is supported. */ }
122
+ finally { if (staging) await rm(staging, { recursive: true, force: true }).catch(() => {}); }
123
+ return info;
124
+ }
125
+
126
+ async function handleUpdate({ root, args, env = process.env, stdout = process.stdout, stderr = process.stderr, fetch = globalThis.fetch, npm = runNpm }) {
127
+ const explicit = ['update', '--update'].includes(args[0]);
128
+ if (!explicit && (env.CI || env.ZERGAI_NO_UPDATE_CHECK === '1' || !stderr.isTTY || args.some(arg => ['help', '--help', '-h', 'version', '--version', '-V'].includes(arg)))) return null;
129
+ let options;
130
+ try { options = explicit ? updateOptions(args) : {}; }
131
+ catch (error) { stderr.write(`${error.message}\n`); return 2; }
132
+ if (options.help) {
133
+ stdout.write('Check npm and update this globally installed CLI\nUsage: update | --update [--check] [--channel dev|next|latest]\n--check prints JSON without installing. Channels are preserved by default.\nStartup only notifies; set ZERGAI_NO_UPDATE_CHECK=1 to disable checks.\n');
134
+ return 0;
135
+ }
136
+ try {
137
+ const metadata = JSON.parse(await readFile(join(root, 'package.json'), 'utf8'));
138
+ identity(metadata);
139
+ if (!explicit) {
140
+ const info = await startupCheck(metadata, env, fetch);
141
+ if (info?.hasUpdate) stderr.write(`Update available for ${metadata.name}: ${metadata.version} → ${info.latest}. Run ${Object.keys(metadata.bin || {})[0] || metadata.name} update.\n`);
142
+ return null;
143
+ }
144
+ // Explicit checks/install always consult the registry; cache is advisory only.
145
+ const info = await checkUpdate(metadata, { fetch, ...options });
146
+ if (options.check) stdout.write(`${JSON.stringify(info)}\n`);
147
+ else if (!info.hasUpdate) stderr.write(`No newer ${info.channel} release of ${metadata.name} (installed ${metadata.version}).\n`);
148
+ else {
149
+ stderr.write(`Updating ${metadata.name}: ${metadata.version} → ${info.latest} (${info.channel})…\n`);
150
+ await install(resolve(root), metadata, info, npm, env);
151
+ stderr.write(`Updated ${metadata.name} to ${info.latest}. The next launch uses the new version.\n`);
152
+ }
153
+ return 0;
154
+ } catch (error) {
155
+ if (!explicit) return null;
156
+ stderr.write(`Update failed: ${error.message}\n`);
157
+ return 1;
158
+ }
159
+ }
160
+
161
+ module.exports = { compareVersions, releaseChannel, checkUpdate, handleUpdate };
package/package.json CHANGED
@@ -1,12 +1,16 @@
1
1
  {
2
2
  "name": "@zergai/cyberdeck",
3
- "version": "0.2.0-beta.1",
3
+ "version": "0.2.1-dev.35760740177",
4
4
  "description": "Command-line client for Zerg CyberDeck clone environments and server-side SDK conformance runs.",
5
5
  "type": "module",
6
+ "zergCli": {
7
+ "entry": "dist/index.js"
8
+ },
6
9
  "bin": {
7
- "zcd": "dist/index.js"
10
+ "zcd": "npm-cli/run.cjs"
8
11
  },
9
12
  "files": [
13
+ "npm-cli/*.cjs",
10
14
  "dist/**/*.js",
11
15
  "README.md",
12
16
  "LICENSE.md",
@@ -36,12 +40,12 @@
36
40
  ],
37
41
  "publishConfig": {
38
42
  "access": "public",
39
- "tag": "next"
43
+ "tag": "dev"
40
44
  },
41
45
  "scripts": {
42
46
  "build": "tsc --build --force",
43
47
  "clean": "tsc --build --clean",
44
- "prepack": "npm run clean && npm run build",
48
+ "prepack": "npm run clean && npm run build && node ../../../scripts/npm/prepare-cli.mjs",
45
49
  "test:public-tarball": "node scripts/smoke-packed-public-api.mjs"
46
50
  },
47
51
  "dependencies": {
@@ -49,9 +53,9 @@
49
53
  "socket.io-client": "^4.8.1"
50
54
  },
51
55
  "zergRelease": {
52
- "branch": "main",
53
- "channel": "next",
54
- "sourceCommit": "3b288334492cd150a61ed06610cd2b5ad546c81b",
56
+ "branch": "development",
57
+ "channel": "dev",
58
+ "sourceCommit": "4b068dffbc8a4dd0debe7e14c61674e5d4670ff4",
55
59
  "sourceVersion": "0.1.0-beta.1"
56
60
  }
57
61
  }