@skrr-ai/cli 0.1.94 → 0.1.95

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.
Files changed (120) hide show
  1. package/README.md +153 -165
  2. package/bin/run.js +32 -0
  3. package/bin/update-npm-runner.cjs +33 -0
  4. package/bin/update-session.cjs +194 -0
  5. package/bin/update-worker.cjs +208 -0
  6. package/dist/base-command.js +0 -15
  7. package/dist/commands/code/doctor.d.ts +1 -0
  8. package/dist/commands/code/doctor.js +6 -0
  9. package/dist/commands/code/index.js +4 -0
  10. package/dist/commands/code/install.d.ts +5 -62
  11. package/dist/commands/code/install.js +86 -279
  12. package/dist/commands/code/rollback.d.ts +8 -0
  13. package/dist/commands/code/rollback.js +20 -0
  14. package/dist/commands/code/update.d.ts +8 -37
  15. package/dist/commands/code/update.js +134 -213
  16. package/dist/commands/daemon/install.js +4 -0
  17. package/dist/commands/generate-recovery-code.d.ts +13 -1
  18. package/dist/commands/generate-recovery-code.js +146 -13
  19. package/dist/commands/instructions/guide.d.ts +20 -0
  20. package/dist/commands/instructions/guide.js +84 -0
  21. package/dist/commands/instructions/install.js +57 -33
  22. package/dist/commands/instructions/setup.d.ts +14 -0
  23. package/dist/commands/instructions/setup.js +72 -0
  24. package/dist/commands/integrations/execute.js +1 -1
  25. package/dist/commands/login.d.ts +5 -6
  26. package/dist/commands/login.js +27 -11
  27. package/dist/commands/logout.js +7 -7
  28. package/dist/commands/security/overview.d.ts +11 -0
  29. package/dist/commands/security/overview.js +158 -0
  30. package/dist/commands/spaces/claim.d.ts +12 -0
  31. package/dist/commands/spaces/claim.js +42 -0
  32. package/dist/commands/spaces/ownership.d.ts +12 -0
  33. package/dist/commands/spaces/ownership.js +46 -0
  34. package/dist/commands/tasks/cancel-cascade.js +1 -1
  35. package/dist/commands/tasks/create-tree.d.ts +5 -1
  36. package/dist/commands/tasks/create-tree.js +35 -7
  37. package/dist/commands/tasks/interrupt.js +2 -2
  38. package/dist/commands/tasks/pause-schedule.js +1 -1
  39. package/dist/commands/token/list.d.ts +3 -5
  40. package/dist/commands/token/list.js +99 -57
  41. package/dist/commands/update.d.ts +14 -0
  42. package/dist/commands/update.js +61 -0
  43. package/dist/generated/managed-instruction-guides.json +9 -0
  44. package/dist/hooks/init.d.ts +5 -0
  45. package/dist/hooks/init.js +27 -0
  46. package/dist/lib/commitments.d.ts +0 -8
  47. package/dist/lib/commitments.js +0 -52
  48. package/dist/lib/daemon-installer.d.ts +2 -0
  49. package/dist/lib/daemon-installer.js +100 -24
  50. package/dist/lib/first-party-harness-doctor.js +31 -1
  51. package/dist/lib/first-party-harness-remote-install.d.ts +71 -0
  52. package/dist/lib/first-party-harness-remote-install.js +310 -0
  53. package/dist/lib/first-party-harness-remote-update.d.ts +46 -0
  54. package/dist/lib/first-party-harness-remote-update.js +242 -0
  55. package/dist/lib/first-party-harness-startup-update.d.ts +26 -0
  56. package/dist/lib/first-party-harness-startup-update.js +166 -0
  57. package/dist/lib/first-party-harness-update.d.ts +46 -0
  58. package/dist/lib/first-party-harness-update.js +289 -0
  59. package/dist/lib/first-party-harness.js +7 -4
  60. package/dist/lib/managed-instruction-local.d.ts +12 -0
  61. package/dist/lib/managed-instruction-local.js +58 -0
  62. package/dist/lib/managed-instruction-setup.d.ts +35 -0
  63. package/dist/lib/managed-instruction-setup.js +306 -0
  64. package/dist/lib/memory-sync.d.ts +1 -0
  65. package/dist/lib/memory-sync.js +279 -68
  66. package/dist/lib/platform-update-local.d.ts +2 -0
  67. package/dist/lib/platform-update-local.js +113 -0
  68. package/dist/lib/platform-update.d.ts +39 -0
  69. package/dist/lib/platform-update.js +84 -0
  70. package/dist/lib/profile-machine-identity-guard.d.ts +2 -0
  71. package/dist/lib/profile-machine-identity-guard.js +13 -0
  72. package/dist/lib/prompt.d.ts +1 -0
  73. package/dist/lib/prompt.js +4 -3
  74. package/dist/lib/task-batch.d.ts +9 -0
  75. package/dist/lib/task-batch.js +94 -17
  76. package/dist/lib/task-instruction-offer.d.ts +1 -4
  77. package/dist/lib/task-instruction-offer.js +3 -129
  78. package/dist/lib/update-check.d.ts +61 -53
  79. package/dist/lib/update-check.js +453 -314
  80. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarnessChannels.d.ts +3 -4
  81. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarnessChannels.js +3 -4
  82. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarnessReleaseKeys.d.ts +82 -0
  83. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarnessReleaseKeys.js +105 -0
  84. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarnessUpdater.d.ts +110 -0
  85. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarnessUpdater.js +945 -0
  86. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/harnessPermissionControls.d.ts +32 -0
  87. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/harnessPermissionControls.js +62 -0
  88. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +2 -0
  89. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +4 -1
  90. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineUuid.js +35 -5
  91. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/releasePolicy.d.ts +63 -0
  92. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/releasePolicy.js +169 -0
  93. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/releaseVersions.d.ts +2 -0
  94. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/releaseVersions.js +44 -0
  95. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarnessChannels.d.ts +3 -4
  96. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarnessChannels.js +3 -4
  97. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarnessReleaseKeys.d.ts +82 -0
  98. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarnessReleaseKeys.js +102 -0
  99. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarnessUpdater.d.ts +110 -0
  100. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarnessUpdater.js +923 -0
  101. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/harnessPermissionControls.d.ts +32 -0
  102. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/harnessPermissionControls.js +59 -0
  103. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +2 -0
  104. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +2 -0
  105. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineUuid.js +35 -5
  106. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/releasePolicy.d.ts +63 -0
  107. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/releasePolicy.js +163 -0
  108. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/releaseVersions.d.ts +2 -0
  109. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/releaseVersions.js +41 -0
  110. package/dist/node_modules/@skrr-ai/auth-core/package.json +31 -1
  111. package/dist/node_modules/@skrr-ai/data-provider/index.js +6584 -6567
  112. package/docs/v1/README.md +28 -0
  113. package/docs/v1/api.md +55 -0
  114. package/docs/v1/changelog.md +16 -0
  115. package/docs/v1/check-examples.mjs +178 -0
  116. package/docs/v1/examples/api-readonly.mjs +144 -0
  117. package/docs/v1/operations.md +87 -0
  118. package/docs/v1/quickstart.md +71 -0
  119. package/oclif.manifest.json +46007 -45462
  120. package/package.json +13 -11
@@ -0,0 +1,194 @@
1
+ 'use strict';
2
+
3
+ // This bootstrap uses only Node built-ins: npm may replace the package while
4
+ // the updater runs, so admission and the worker must not depend on its modules.
5
+ const fs = require('node:fs');
6
+ const os = require('node:os');
7
+ const path = require('node:path');
8
+ const crypto = require('node:crypto');
9
+ const { execFileSync } = require('node:child_process');
10
+ // Protocol 1: bakery admission lock, immutable PID/birth/token leases, and
11
+ // process-group mutation ownership. Its wire/locking contract is immutable.
12
+ // Future protocols must coexist with these sessions or require an external
13
+ // package-manager update; selecting a separate lock must never bypass them.
14
+ const COORDINATION_VERSION = 1;
15
+
16
+ function runtimeDir(packageRoot) {
17
+ const key = crypto
18
+ .createHash('sha256')
19
+ .update(path.resolve(packageRoot))
20
+ .digest('hex')
21
+ .slice(0, 24);
22
+ // Installation leases deliberately ignore SKRR_CONFIG_DIR: two profiles or
23
+ // config roots still share the same npm installation.
24
+ return path.join(os.homedir(), '.skrr', 'cli-update-runtime', key);
25
+ }
26
+
27
+ function readJson(file) {
28
+ try {
29
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
30
+ } catch {
31
+ return null;
32
+ }
33
+ }
34
+
35
+ function writeJson(file, value) {
36
+ fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
37
+ const temporary = `${file}.${process.pid}.${crypto.randomBytes(6).toString('hex')}.tmp`;
38
+ try {
39
+ fs.writeFileSync(temporary, JSON.stringify(value), { mode: 0o600, flag: 'wx' });
40
+ fs.renameSync(temporary, file);
41
+ } finally {
42
+ fs.rmSync(temporary, { force: true });
43
+ }
44
+ }
45
+
46
+ function processIdentity(pid) {
47
+ if (process.platform === 'win32') return null;
48
+ try {
49
+ return (
50
+ execFileSync('ps', ['-p', String(pid), '-o', 'lstart='], {
51
+ timeout: 500,
52
+ encoding: 'utf8',
53
+ stdio: ['ignore', 'pipe', 'ignore'],
54
+ }).trim() || null
55
+ );
56
+ } catch {
57
+ return null;
58
+ }
59
+ }
60
+
61
+ function isAlive(owner) {
62
+ if (!owner || !Number.isSafeInteger(owner.pid) || owner.pid <= 0) return false;
63
+ try {
64
+ process.kill(owner.group && process.platform !== 'win32' ? -owner.pid : owner.pid, 0);
65
+ } catch (error) {
66
+ return error.code !== 'ESRCH';
67
+ }
68
+ // A recycled PID is not the process that created this lease. If the OS
69
+ // cannot disclose its identity, remain conservative rather than expiring a
70
+ // live session on a wall-clock timeout.
71
+ const identity = owner.identity && processIdentity(owner.pid);
72
+ return !identity || identity === owner.identity;
73
+ }
74
+
75
+ function wait(milliseconds) {
76
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, milliseconds);
77
+ }
78
+
79
+ function acquireLock(directory, timeoutMs = 1000) {
80
+ // Lamport's bakery algorithm, with one immutable participant filename per
81
+ // acquisition. Reaping a crashed participant never removes a replacement
82
+ // owner's lock (the stale-directory removal race of mkdir mutexes).
83
+ const deadline = Date.now() + timeoutMs;
84
+ fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
85
+ const owner = {
86
+ pid: process.pid,
87
+ identity: processIdentity(process.pid),
88
+ token: crypto.randomUUID(),
89
+ };
90
+ const participant = path.join(directory, `${owner.pid}-${owner.token}.json`);
91
+ const participants = () => {
92
+ const active = [];
93
+ for (const file of fs.readdirSync(directory)) {
94
+ if (!file.endsWith('.json')) continue;
95
+ const target = path.join(directory, file);
96
+ const record = readJson(target);
97
+ if (record && isAlive(record)) active.push(record);
98
+ else if (record) fs.rmSync(target, { force: true });
99
+ }
100
+ return active;
101
+ };
102
+ try {
103
+ writeJson(participant, { ...owner, choosing: true, ticket: 0 });
104
+ const ticket = Math.max(0, ...participants().map((record) => record.ticket || 0)) + 1;
105
+ writeJson(participant, { ...owner, choosing: false, ticket });
106
+ while (true) {
107
+ const blocked = participants().some(
108
+ (other) =>
109
+ other.token !== owner.token &&
110
+ (other.choosing ||
111
+ (other.ticket > 0 &&
112
+ (other.ticket < ticket || (other.ticket === ticket && other.token < owner.token)))),
113
+ );
114
+ if (!blocked)
115
+ return () => {
116
+ fs.rmSync(participant, { force: true });
117
+ };
118
+ if (Date.now() >= deadline)
119
+ throw Object.assign(new Error('CLI update lock is busy'), { code: 'CLI_UPDATE_BUSY' });
120
+ wait(25);
121
+ }
122
+ } catch (error) {
123
+ fs.rmSync(participant, { force: true });
124
+ throw error;
125
+ }
126
+ }
127
+
128
+ function liveLeases(directory) {
129
+ const leaseDir = path.join(directory, 'sessions');
130
+ fs.mkdirSync(leaseDir, { recursive: true, mode: 0o700 });
131
+ const live = [];
132
+ for (const file of fs.readdirSync(leaseDir)) {
133
+ const target = path.join(leaseDir, file);
134
+ const lease = readJson(target);
135
+ if (lease && isAlive(lease)) live.push(lease);
136
+ else fs.rmSync(target, { force: true });
137
+ }
138
+ return live;
139
+ }
140
+
141
+ function registerSession(packageRoot, timeoutMs = 60000) {
142
+ const directory = runtimeDir(packageRoot);
143
+ const deadline = Date.now() + timeoutMs;
144
+ while (true) {
145
+ const release = acquireLock(path.join(directory, 'admission.lock'), 1000);
146
+ try {
147
+ const mutationFile = path.join(directory, 'mutation.json');
148
+ const mutation = readJson(mutationFile);
149
+ if (!mutation || !isAlive(mutation)) {
150
+ if (mutation) fs.rmSync(mutationFile, { force: true });
151
+ const token = crypto.randomUUID();
152
+ const leaseFile = path.join(directory, 'sessions', `${process.pid}-${token}.json`);
153
+ writeJson(leaseFile, {
154
+ pid: process.pid,
155
+ identity: processIdentity(process.pid),
156
+ token,
157
+ coordinationVersion: COORDINATION_VERSION,
158
+ });
159
+ const cleanup = () => {
160
+ try {
161
+ fs.rmSync(leaseFile, { force: true });
162
+ } catch {
163
+ /* read-only home */
164
+ }
165
+ };
166
+ process.once('exit', cleanup);
167
+ return cleanup;
168
+ }
169
+ } finally {
170
+ release();
171
+ }
172
+ if (Date.now() >= deadline) {
173
+ throw Object.assign(
174
+ new Error(
175
+ 'skrr is being updated by npm. Retry shortly; inspect `skrr update --check` after the update finishes.',
176
+ ),
177
+ { code: 'CLI_UPDATE_BUSY' },
178
+ );
179
+ }
180
+ wait(100);
181
+ }
182
+ }
183
+
184
+ module.exports = {
185
+ COORDINATION_VERSION,
186
+ runtimeDir,
187
+ readJson,
188
+ writeJson,
189
+ processIdentity,
190
+ isAlive,
191
+ acquireLock,
192
+ liveLeases,
193
+ registerSession,
194
+ };
@@ -0,0 +1,208 @@
1
+ 'use strict';
2
+
3
+ // Copied next to update-session.cjs outside the npm package before launch.
4
+ const fs = require('node:fs');
5
+ const path = require('node:path');
6
+ const crypto = require('node:crypto');
7
+ const { execFile, spawn } = require('node:child_process');
8
+ const { promisify } = require('node:util');
9
+ const runtime = require('./update-session.cjs');
10
+ const execFileAsync = promisify(execFile);
11
+ const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
12
+ const MAX_WAIT_MS = 5 * 60 * 1000;
13
+ const INSTALL_TIMEOUT_MS = 10 * 60 * 1000;
14
+ const MAX_LOG_BYTES = 256 * 1024;
15
+
16
+ async function run(directory, options = {}) {
17
+ let releaseWorker;
18
+ try {
19
+ releaseWorker = runtime.acquireLock(path.join(directory, 'worker.lock'), 1000);
20
+ } catch (error) {
21
+ if (error.code === 'CLI_UPDATE_BUSY') return;
22
+ throw error;
23
+ }
24
+ const stateFile = path.join(directory, 'result.json');
25
+ let mutation = false;
26
+ let mutationChild = null;
27
+ let attempt = runtime.readJson(stateFile);
28
+ const persist = (changes) => {
29
+ attempt = { ...attempt, ...changes, updatedAt: Date.now() };
30
+ runtime.writeJson(stateFile, attempt);
31
+ };
32
+ try {
33
+ if (!attempt || !['queued', 'running'].includes(attempt.status)) return;
34
+ if (
35
+ attempt.coordinationVersion != null &&
36
+ attempt.coordinationVersion !== runtime.COORDINATION_VERSION
37
+ )
38
+ throw new Error(
39
+ 'Incompatible CLI update coordination protocol; use the owning package manager',
40
+ );
41
+ if (
42
+ !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(attempt.version) ||
43
+ !path.isAbsolute(attempt.packageRoot) ||
44
+ !path.isAbsolute(attempt.npmPath)
45
+ ) {
46
+ throw new Error('Invalid persisted CLI update request');
47
+ }
48
+ // Serialize admission and the empty-session check. Newly starting CLIs
49
+ // then wait before loading oclif/package modules until npm has finished.
50
+ const deadline = Date.now() + (options.maxWaitMs ?? MAX_WAIT_MS);
51
+ while (true) {
52
+ const releaseAdmission = runtime.acquireLock(path.join(directory, 'admission.lock'), 1000);
53
+ try {
54
+ const previousMutation = runtime.readJson(path.join(directory, 'mutation.json'));
55
+ if (
56
+ (!previousMutation || !runtime.isAlive(previousMutation)) &&
57
+ runtime.liveLeases(directory).length === 0
58
+ ) {
59
+ runtime.writeJson(path.join(directory, 'mutation.json'), {
60
+ pid: process.pid,
61
+ identity: runtime.processIdentity(process.pid),
62
+ });
63
+ mutation = true;
64
+ break;
65
+ }
66
+ } finally {
67
+ releaseAdmission();
68
+ }
69
+ if (Date.now() >= deadline) {
70
+ persist({
71
+ status: 'queued',
72
+ message: 'Waiting for active CLI sessions; the next invocation resumes this request.',
73
+ });
74
+ return;
75
+ }
76
+ await delay(250);
77
+ }
78
+ if (
79
+ runtime.readJson(path.join(attempt.packageRoot, 'package.json'))?.version === attempt.version
80
+ ) {
81
+ persist({
82
+ status: 'succeeded',
83
+ finishedAt: Date.now(),
84
+ installedVersion: attempt.version,
85
+ message: 'The requested CLI version is already installed.',
86
+ });
87
+ return;
88
+ }
89
+ const rootResult = await execFileAsync(attempt.npmPath, ['root', '-g'], {
90
+ timeout: 1500,
91
+ maxBuffer: 64 * 1024,
92
+ shell: process.platform === 'win32',
93
+ });
94
+ const npmRoot = fs.realpathSync(rootResult.stdout.trim());
95
+ const expectedPackage = path.join(npmRoot, '@skrr-ai', 'cli');
96
+ if (fs.realpathSync(expectedPackage) !== fs.realpathSync(attempt.packageRoot)) {
97
+ throw new Error(
98
+ 'npm global prefix changed; run the update with the package manager that owns this CLI',
99
+ );
100
+ }
101
+ persist({
102
+ status: 'running',
103
+ startedAt: Date.now(),
104
+ workerPid: process.pid,
105
+ message: 'npm is installing the requested CLI version.',
106
+ });
107
+ const logFile = path.join(directory, 'update.log');
108
+ fs.writeFileSync(logFile, '', { mode: 0o600 });
109
+ let logBytes = 0;
110
+ const log = (chunk) => {
111
+ const bytes = Buffer.from(chunk);
112
+ if (logBytes >= MAX_LOG_BYTES) return;
113
+ const bounded = bytes.subarray(0, MAX_LOG_BYTES - logBytes);
114
+ fs.appendFileSync(logFile, bounded);
115
+ logBytes += bounded.length;
116
+ };
117
+ await new Promise((resolve, reject) => {
118
+ const requestFile = path.join(directory, `runner-${crypto.randomUUID()}.json`);
119
+ const token = crypto.randomUUID();
120
+ runtime.writeJson(requestFile, { token, npmPath: attempt.npmPath, version: attempt.version });
121
+ const child = spawn(
122
+ process.execPath,
123
+ [path.join(__dirname, 'update-npm-runner.cjs'), requestFile],
124
+ {
125
+ detached: process.platform !== 'win32',
126
+ stdio: ['ignore', 'pipe', 'pipe'],
127
+ },
128
+ );
129
+ if (child.pid) {
130
+ mutationChild = { pid: child.pid, group: process.platform !== 'win32' };
131
+ runtime.writeJson(path.join(directory, 'mutation.json'), mutationChild);
132
+ runtime.writeJson(`${requestFile}.go`, { pid: child.pid, token });
133
+ }
134
+ child.stdout.on('data', log);
135
+ child.stderr.on('data', log);
136
+ let timedOut = false;
137
+ let forceTimer;
138
+ const signal = (name) => {
139
+ try {
140
+ if (process.platform !== 'win32' && child.pid) process.kill(-child.pid, name);
141
+ else child.kill(name);
142
+ } catch {
143
+ /* already exited */
144
+ }
145
+ };
146
+ const timer = setTimeout(() => {
147
+ timedOut = true;
148
+ signal('SIGTERM');
149
+ forceTimer = setTimeout(() => {
150
+ signal('SIGKILL');
151
+ }, 5000);
152
+ }, options.installTimeoutMs ?? INSTALL_TIMEOUT_MS);
153
+ child.once('error', (error) => {
154
+ clearTimeout(timer);
155
+ clearTimeout(forceTimer);
156
+ reject(error);
157
+ });
158
+ child.once('exit', async (code, exitSignal) => {
159
+ clearTimeout(timer);
160
+ if (timedOut) {
161
+ // npm can exit on TERM while a descendant keeps writing. Complete
162
+ // KILL escalation for the whole group before releasing admission.
163
+ signal('SIGKILL');
164
+ const stopDeadline = Date.now() + 5000;
165
+ while (mutationChild && runtime.isAlive(mutationChild) && Date.now() < stopDeadline)
166
+ await delay(50);
167
+ }
168
+ clearTimeout(forceTimer);
169
+ fs.rmSync(requestFile, { force: true });
170
+ fs.rmSync(`${requestFile}.go`, { force: true });
171
+ if (timedOut) reject(new Error('npm update exceeded the 10-minute time limit'));
172
+ else if (code === 0) resolve();
173
+ else reject(new Error(`npm update failed (${exitSignal || code}); see ${logFile}`));
174
+ });
175
+ });
176
+ const installedVersion = runtime.readJson(
177
+ path.join(attempt.packageRoot, 'package.json'),
178
+ )?.version;
179
+ if (installedVersion !== attempt.version)
180
+ throw new Error(
181
+ `npm completed but installed ${installedVersion || 'an unknown version'} instead of ${attempt.version}`,
182
+ );
183
+ persist({
184
+ status: 'succeeded',
185
+ finishedAt: Date.now(),
186
+ installedVersion,
187
+ message: 'The new CLI version is ready for the next invocation.',
188
+ });
189
+ } catch (error) {
190
+ persist({ status: 'failed', finishedAt: Date.now(), message: String(error.message || error) });
191
+ } finally {
192
+ // A crashed worker/terminated npm parent can leave a mutating process
193
+ // group alive. Keep the gate until the group is confirmed gone.
194
+ if (mutation && (!mutationChild || !runtime.isAlive(mutationChild)))
195
+ fs.rmSync(path.join(directory, 'mutation.json'), { force: true });
196
+ releaseWorker();
197
+ }
198
+ }
199
+
200
+ if (require.main === module) {
201
+ const directory = process.argv[2];
202
+ if (!directory || !path.isAbsolute(directory)) process.exitCode = 1;
203
+ else
204
+ run(directory).catch(() => {
205
+ process.exitCode = 1;
206
+ });
207
+ }
208
+ module.exports = { run };
@@ -52,7 +52,6 @@ const daemonBroker_1 = require("./lib/daemonBroker");
52
52
  const daemonBrokerRefusal_1 = require("./lib/daemonBrokerRefusal");
53
53
  const refresh_1 = require("./lib/refresh");
54
54
  const auth_storage_1 = require("./lib/auth-storage");
55
- const update_check_1 = require("./lib/update-check");
56
55
  const web_url_1 = require("./lib/web-url");
57
56
  const edge_error_page_1 = require("./lib/edge-error-page");
58
57
  const step_up_1 = require("./lib/step-up");
@@ -541,20 +540,6 @@ class BaseCommand extends core_1.Command {
541
540
  }
542
541
  (0, node_adapter_1.setAdapterCredentialOverride)(this.resolvedCredential);
543
542
  (0, data_provider_1.setHttpAdapter)((0, node_adapter_1.createNodeAdapter)());
544
- // Non-blocking "a newer skrr is available" advisory. Decides from a ~4h
545
- // on-disk cache (zero added latency) and refreshes in the background.
546
- // Writes at most one line to stderr, never stdout, so --json consumers
547
- // are unaffected; self-suppresses in bundled/non-TTY/opted-out contexts.
548
- // Wrapped so an advisory can never break the command it precedes.
549
- try {
550
- (0, update_check_1.maybeWarnOutdatedCli)({
551
- currentVersion: this.config.version,
552
- baseURL: this.cliConfig.baseURL,
553
- });
554
- }
555
- catch {
556
- /* advisory is strictly best-effort */
557
- }
558
543
  }
559
544
  /**
560
545
  * Read only the two global base flags. A narrow parser — oclif's
@@ -16,6 +16,7 @@ export default class CodeDoctor extends Command {
16
16
  static examples: string[];
17
17
  static flags: {
18
18
  json: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
19
+ check: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
19
20
  };
20
21
  run(): Promise<void>;
21
22
  }
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const core_1 = require("@oclif/core");
4
4
  const auth_core_1 = require("@skrr-ai/auth-core");
5
5
  const first_party_harness_doctor_1 = require("../../lib/first-party-harness-doctor");
6
+ const first_party_harness_update_1 = require("../../lib/first-party-harness-update");
6
7
  /**
7
8
  * `skrr code doctor` — diagnose the local first-party harness installation.
8
9
  *
@@ -23,9 +24,14 @@ class CodeDoctor extends core_1.Command {
23
24
  ];
24
25
  static flags = {
25
26
  json: core_1.Flags.boolean({ description: 'Emit the report as JSON' }),
27
+ check: core_1.Flags.boolean({
28
+ description: 'Also check the signed release channel (the default diagnosis is offline)',
29
+ }),
26
30
  };
27
31
  async run() {
28
32
  const { flags } = await this.parse(CodeDoctor);
33
+ if (flags.check)
34
+ await (0, first_party_harness_update_1.checkLocalCodeUpdate)({ force: true });
29
35
  const report = (0, first_party_harness_doctor_1.runDoctor)();
30
36
  if (flags.json) {
31
37
  this.logJson(report);
@@ -5,6 +5,7 @@ const core_1 = require("@oclif/core");
5
5
  const auth_core_1 = require("@skrr-ai/auth-core");
6
6
  const first_party_harness_1 = require("../../lib/first-party-harness");
7
7
  const first_party_harness_broker_1 = require("../../lib/first-party-harness-broker");
8
+ const first_party_harness_startup_update_1 = require("../../lib/first-party-harness-startup-update");
8
9
  const first_party_harness_managed_1 = require("../../lib/first-party-harness-managed");
9
10
  const first_party_harness_project_trust_1 = require("../../lib/first-party-harness-project-trust");
10
11
  /**
@@ -93,6 +94,9 @@ async function forwardToEngine(cmd, subcommand, passthrough, options = {}) {
93
94
  // output on stdout, and one friendly sentence in the middle of it is a parse
94
95
  // error at the other end.
95
96
  const say = (line) => cmd.logToStderr(line);
97
+ if (subcommand === null && options.projectPolicy === 'tui') {
98
+ await (0, first_party_harness_startup_update_1.maybeOfferCodeUpdate)({ argv: engineArgv, say });
99
+ }
96
100
  if (agentRef && !options.managed) {
97
101
  say(`${first_party_harness_managed_1.SKRR_AGENT_FLAG} has no effect here — this command does not host a managed ` +
98
102
  'session. It was removed from the arguments passed to the engine.');
@@ -1,71 +1,14 @@
1
- import { BaseCommand } from '../../base-command';
2
- /**
3
- * `skrr code install` — install the engine from the terminal.
4
- *
5
- * ── Why this command exists ────────────────────────────────────────────────
6
- *
7
- * `skrr code`'s not-installed message offered exactly two routes and neither
8
- * worked without a browser: "In the web app: the runtime menu … → Install", or
9
- * point the engine-path override at a build you already have — which is the thing
10
- * you are trying to obtain, and the route that gives up provenance. So the
11
- * ordinary case for a coding harness (a dev box over SSH, a CI runner, a cloud
12
- * VM) had no way in (dogfood OSK-276).
13
- *
14
- * Nothing underneath was missing. The daemon has performed the verified install
15
- * since 2026-08-08, and `POST /api/daemons/:id/cli-installers/:provider/install`
16
- * has fronted it since June. The install was API-exposed and CLI-unreachable —
17
- * the shape the root `CLAUDE.md` names under CLI ↔ API parity: "an endpoint the
18
- * API exposes must be reachable from `skrr`."
19
- *
20
- * So this adds a SURFACE and no mechanism. Same route the web app posts, same
21
- * daemon job, same signature verification. A second install path would be a
22
- * second thing to keep in step with the first, and the first is the one that
23
- * verifies the signed manifest.
24
- */
25
- export default class CodeInstall extends BaseCommand {
1
+ import { Command } from '@oclif/core';
2
+ /** Install through the shared signed pipeline without a cloud login or local daemon. */
3
+ export default class CodeInstall extends Command {
26
4
  static description: string;
27
5
  static examples: string[];
28
6
  static flags: {
29
7
  daemon: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
30
8
  wait: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
31
9
  json: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
10
+ 'target-version': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
11
+ 'allow-downgrade': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
32
12
  };
33
13
  run(): Promise<void>;
34
- /** Which daemon the install targets, by the rule every daemon-picking command shares. */
35
- private resolveTarget;
36
- /**
37
- * The daemon id to install through, or the refusal.
38
- *
39
- * Refuses rather than guessing when several are active. Picking one would mean
40
- * installing a hundred megabytes onto a machine the user was not thinking
41
- * about. The candidates are CONNECTED daemons by the id the installer routes
42
- * take (`listConnectedDaemons`), so the error names values that work — the
43
- * Daemon resource list `skrr list-daemons` prints is a different id space
44
- * (OSK-9993).
45
- *
46
- * A Dedicated Runtime's own machine is refused here, before any request. The
47
- * daemon and the API refuse it too; saying so locally is what keeps the answer
48
- * the same whether or not this machine's daemon is reachable, and whether or
49
- * not it has published its id. Another of the owner's machines, named with
50
- * `--daemon`, is not this machine and goes to the API like anywhere else.
51
- */
52
- private installTarget;
53
- /**
54
- * What the daemon says about this provider, when it started no job.
55
- *
56
- * Read from the SAME list route the web app uses, so the CLI reports the
57
- * daemon's own words rather than inventing an explanation for silence. Best
58
- * effort: a failure here means falling back to the honest "does not say why",
59
- * never to a guess.
60
- */
61
- private installerNote;
62
- /**
63
- * Poll the job until it settles, printing each new log line.
64
- *
65
- * The daemon already writes a progress log — "Fetching the signed release
66
- * manifest", "Verified and installed …" — and it is the only account
67
- * of a hundred-megabyte download the user is waiting on. Printing only the
68
- * lines not yet seen keeps a poll loop from repeating the whole log.
69
- */
70
- private followJob;
71
14
  }