@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,27 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const config_1 = require("../lib/config");
4
+ const update_check_1 = require("../lib/update-check");
5
+ /** All commands pass through this hook, including the local code/daemon
6
+ * commands that intentionally do not use authenticated BaseCommand. */
7
+ const hook = async function (options) {
8
+ const argv = [
9
+ ...(options.id ?? '').split(':').filter(Boolean),
10
+ ...(options.argv ?? process.argv.slice(2)),
11
+ ];
12
+ if (!(0, update_check_1.shouldCheckCliUpdateAtStartup)(argv, !!process.stderr.isTTY && !!process.stdin.isTTY))
13
+ return;
14
+ try {
15
+ const baseFlag = argv.findIndex((arg) => arg === '--base-url');
16
+ const suppliedBase = argv.find((arg) => arg.startsWith('--base-url='))?.slice('--base-url='.length) ??
17
+ (baseFlag >= 0 ? argv[baseFlag + 1] : undefined);
18
+ await (0, update_check_1.maybeWarnOutdatedCli)({
19
+ currentVersion: this.config.version,
20
+ baseURL: suppliedBase || (0, config_1.loadConfig)().baseURL,
21
+ });
22
+ }
23
+ catch {
24
+ /* update discovery must not prevent command startup */
25
+ }
26
+ };
27
+ exports.default = hook;
@@ -632,14 +632,6 @@ export type EvidenceHealthView = {
632
632
  * is noise in a summary someone reads to find what is wrong.
633
633
  */
634
634
  export declare function describeEvidenceHealth(health?: EvidenceHealthView | null, criteriaCount?: number): string | undefined;
635
- /**
636
- * `stalled` is a verdict, not a reason. Printed bare it reads like `unknown`
637
- * with a harsher word, and the operator's next move is to go and read run
638
- * history to find what the reconciler had already decided. Returns undefined
639
- * for every other health value, including `unknown` — "nobody has judged" has
640
- * no diagnosis to name.
641
- */
642
- export declare function describeStalledHealth(commitment: CommitmentView): string | undefined;
643
635
  /** Render an interval back as a compact human string. */
644
636
  export declare function describeIntervalMs(ms: number | undefined): string;
645
637
  /**
@@ -11,7 +11,6 @@ exports.describeCommitmentConcurrency = describeCommitmentConcurrency;
11
11
  exports.parseIntervalToMs = parseIntervalToMs;
12
12
  exports.buildWatchedSources = buildWatchedSources;
13
13
  exports.describeEvidenceHealth = describeEvidenceHealth;
14
- exports.describeStalledHealth = describeStalledHealth;
15
14
  exports.describeIntervalMs = describeIntervalMs;
16
15
  exports.buildSuccessCriteria = buildSuccessCriteria;
17
16
  exports.cadenceLabel = cadenceLabel;
@@ -603,57 +602,6 @@ function describeEvidenceHealth(health, criteriaCount) {
603
602
  }
604
603
  return `${state}${detail}`;
605
604
  }
606
- /**
607
- * Why a commitment is `stalled`, in the reconciler's own words.
608
- *
609
- * These clauses are the CLI voicing of the `details` table in
610
- * `CommitmentLivenessReconciler.alertCopy` — an operator who gets the alert
611
- * ("<name> is stalled — its expected checks are no longer arriving") and then
612
- * runs `commitments show` must read the same sentence, or the two surfaces look
613
- * like two different problems.
614
- *
615
- * `contract_version_mismatch` has no entry there because it is not a diagnosis;
616
- * the reconciler synthesises it when the running contract is fenced behind the
617
- * authored one. It still reaches `state.liveness.diagnosisKind`, so it needs a
618
- * clause here or the one case with an obvious fix prints bare.
619
- */
620
- const STALLED_DIAGNOSES = {
621
- scheduler_stalled: 'its expected checks are no longer arriving',
622
- work_failing: 'every recent run reached the agent and failed while doing the work',
623
- runtime_failing: 'recent runs never started — the runtime refused them',
624
- result_rejected: 'every recent run reached the agent, but its result was rejected',
625
- observation_unreadable: 'the latest check could not read every configured source',
626
- execution_budget_exhausted: 'recent planned work was skipped — the execution budget is exhausted',
627
- no_advancement: 'no server-recorded advancement exists inside the declared window',
628
- owner_action_blocked: 'a completed work run is blocked pending owner action',
629
- contract_version_mismatch: 'the running contract is older than the authored one; its triggers have not resynced',
630
- };
631
- /**
632
- * `stalled` is a verdict, not a reason. Printed bare it reads like `unknown`
633
- * with a harsher word, and the operator's next move is to go and read run
634
- * history to find what the reconciler had already decided. Returns undefined
635
- * for every other health value, including `unknown` — "nobody has judged" has
636
- * no diagnosis to name.
637
- */
638
- function describeStalledHealth(commitment) {
639
- if (!['blocked', 'failed'].includes(commitment.assessment?.operation || ''))
640
- return undefined;
641
- const liveness = (commitment.state?.liveness || {});
642
- const kind = typeof liveness.diagnosisKind === 'string' ? liveness.diagnosisKind : undefined;
643
- const clause = (kind && STALLED_DIAGNOSES[kind]) ||
644
- 'this loop has stopped producing; the reconciler did not record which check failed';
645
- // What refused and what to change, when the reconciler recorded them
646
- // (OSK-12137). A machine failure has a specific fix; printing only the
647
- // verdict would send the owner to read run history for it.
648
- const code = typeof liveness.diagnosisCode === 'string' && liveness.diagnosisCode
649
- ? ` (${liveness.diagnosisCode})`
650
- : '';
651
- const since = typeof liveness.unhealthySinceAt === 'string' ? ` (since ${liveness.unhealthySinceAt})` : '';
652
- const fix = typeof liveness.diagnosisFix === 'string' && liveness.diagnosisFix
653
- ? ` — ${liveness.diagnosisFix}`
654
- : '';
655
- return `${clause}${code}${since}${fix}`;
656
- }
657
605
  /** Render an interval back as a compact human string. */
658
606
  function describeIntervalMs(ms) {
659
607
  if (!ms || !Number.isFinite(ms))
@@ -20,6 +20,8 @@ export interface DaemonInstallOptions {
20
20
  feedBase?: string;
21
21
  /** Injected for tests; defaults to global fetch. */
22
22
  fetchImpl?: typeof fetch;
23
+ /** Cancel an in-progress install, including consumption of a response body. */
24
+ signal?: AbortSignal;
23
25
  /**
24
26
  * Trust anchors. Defaults to the pinned production set; tests substitute a
25
27
  * generated keypair, exactly as the first-party harness installer does. Passing an EMPTY
@@ -105,19 +105,106 @@ function daemonPlatformKey(platform = process.platform, arch = process.arch) {
105
105
  function fail(code, error) {
106
106
  return { ok: false, code, error };
107
107
  }
108
- async function getWithTimeout(url, timeoutMs, fetchImpl) {
108
+ class DownloadSizeLimitError extends Error {
109
+ constructor(maxBytes) {
110
+ super(`download exceeds the size ceiling (${maxBytes} bytes)`);
111
+ this.name = 'DownloadSizeLimitError';
112
+ }
113
+ }
114
+ /**
115
+ * Own the request until EOF, rather than releasing its deadline at headers.
116
+ * Count delivered bytes before retaining them; Content-Length is only an early
117
+ * refusal, since it may be absent, incorrect, or describe compressed bytes.
118
+ */
119
+ async function getBytesWithTimeout(url, timeoutMs, maxBytes, fetchImpl, signal) {
120
+ signal?.throwIfAborted();
109
121
  const controller = new AbortController();
110
- const timer = setTimeout(() => controller.abort(), timeoutMs);
122
+ let response;
123
+ let reader;
124
+ const cancelBody = (reason) => {
125
+ // Cancellation is best effort: a broken producer must not extend the
126
+ // deadline by returning a cancel promise that never settles.
127
+ try {
128
+ const cancelled = reader ? reader.cancel(reason) : response?.body?.cancel(reason);
129
+ void cancelled?.catch(() => undefined);
130
+ }
131
+ catch { /* response may already be closed */ }
132
+ };
133
+ let rejectAbort;
134
+ const aborted = new Promise((_, reject) => { rejectAbort = reject; });
135
+ const onAbort = () => {
136
+ rejectAbort(controller.signal.reason);
137
+ cancelBody(controller.signal.reason);
138
+ };
139
+ controller.signal.addEventListener('abort', onAbort, { once: true });
140
+ const onCallerAbort = () => controller.abort(signal?.reason);
141
+ signal?.addEventListener('abort', onCallerAbort, { once: true });
142
+ const timer = setTimeout(() => controller.abort(new Error(`download timed out after ${timeoutMs} ms`)), timeoutMs);
111
143
  try {
112
- return await fetchImpl(url, {
144
+ const requested = fetchImpl(url, {
113
145
  signal: controller.signal,
114
146
  headers: { 'User-Agent': 'skrr-cli (daemon install)' },
147
+ }).then((res) => {
148
+ response = res;
149
+ // Injected transports can ignore AbortSignal. Dispose a response that
150
+ // arrives after our deadline rather than leaving its connection open.
151
+ if (controller.signal.aborted)
152
+ cancelBody(controller.signal.reason);
153
+ return res;
115
154
  });
155
+ const downloaded = (async () => {
156
+ response = await requested;
157
+ controller.signal.throwIfAborted();
158
+ if (!response.ok)
159
+ throw new Error(`returned HTTP ${response.status}`);
160
+ const contentLength = response.headers.get('content-length');
161
+ if (contentLength && /^\d+$/.test(contentLength.trim()) && Number(contentLength) > maxBytes) {
162
+ throw new DownloadSizeLimitError(maxBytes);
163
+ }
164
+ if (!response.body)
165
+ return Buffer.alloc(0);
166
+ reader = response.body.getReader();
167
+ // Grow one buffer rather than retaining a separate allocation per chunk.
168
+ // A producer emitting tiny chunks must not defeat the byte ceiling with
169
+ // unbounded chunk metadata.
170
+ let bytes = Buffer.alloc(0);
171
+ let receivedBytes = 0;
172
+ for (;;) {
173
+ const { done, value } = await reader.read();
174
+ controller.signal.throwIfAborted();
175
+ if (done)
176
+ break;
177
+ if (value.byteLength > maxBytes - receivedBytes)
178
+ throw new DownloadSizeLimitError(maxBytes);
179
+ const neededBytes = receivedBytes + value.byteLength;
180
+ if (neededBytes > bytes.byteLength) {
181
+ const nextBytes = Buffer.allocUnsafe(Math.min(maxBytes, Math.max(64 * 1_024, bytes.byteLength * 2, neededBytes)));
182
+ bytes.copy(nextBytes, 0, 0, receivedBytes);
183
+ bytes = nextBytes;
184
+ }
185
+ bytes.set(value, receivedBytes);
186
+ receivedBytes += value.byteLength;
187
+ }
188
+ return bytes.subarray(0, receivedBytes);
189
+ })();
190
+ // Race the entire transfer once: racing each read retains an abort-promise
191
+ // reaction per chunk until the download ends.
192
+ return await Promise.race([downloaded, aborted]);
193
+ }
194
+ catch (err) {
195
+ controller.abort(err);
196
+ throw err;
116
197
  }
117
198
  finally {
118
199
  clearTimeout(timer);
200
+ signal?.removeEventListener('abort', onCallerAbort);
201
+ controller.signal.removeEventListener('abort', onAbort);
202
+ reader?.releaseLock();
119
203
  }
120
204
  }
205
+ function errorMessage(error) {
206
+ return error instanceof Error ? error.message : String(error);
207
+ }
121
208
  /** Fetch `<feedBase>/latest/daemon-version.json` and verify its signature. */
122
209
  async function fetchVerifiedDaemonManifest(opts = {}) {
123
210
  const env = opts.env ?? process.env;
@@ -125,19 +212,16 @@ async function fetchVerifiedDaemonManifest(opts = {}) {
125
212
  const fetchImpl = opts.fetchImpl ?? fetch;
126
213
  const keys = opts.keys ?? auth_core_1.OVERSKY_RELEASE_PUBLIC_KEYS;
127
214
  const url = `${feedBase}/latest/daemon-version.json`;
128
- let res;
215
+ let text;
129
216
  try {
130
- res = await getWithTimeout(url, MANIFEST_TIMEOUT_MS, fetchImpl);
217
+ const bytes = await getBytesWithTimeout(url, MANIFEST_TIMEOUT_MS, MAX_MANIFEST_BYTES, fetchImpl, opts.signal);
218
+ text = bytes.toString('utf8');
131
219
  }
132
220
  catch (err) {
133
- return fail('manifest_unreachable', `${url}: ${err.message}`);
134
- }
135
- if (!res.ok) {
136
- return fail('manifest_unreachable', `${url} returned HTTP ${res.status}`);
137
- }
138
- const text = await res.text();
139
- if (text.length > MAX_MANIFEST_BYTES) {
140
- return fail('manifest_malformed', 'release manifest is implausibly large');
221
+ if (err instanceof DownloadSizeLimitError) {
222
+ return fail('manifest_malformed', 'release manifest is implausibly large');
223
+ }
224
+ return fail('manifest_unreachable', `${url}: ${errorMessage(err)}`);
141
225
  }
142
226
  let manifest;
143
227
  try {
@@ -172,7 +256,7 @@ async function installDaemon(opts = {}) {
172
256
  if (!platformKey) {
173
257
  return fail('unsupported_platform', `no daemon build for ${process.platform}-${process.arch}`);
174
258
  }
175
- const fetched = await fetchVerifiedDaemonManifest({ env, feedBase, fetchImpl, keys });
259
+ const fetched = await fetchVerifiedDaemonManifest({ env, feedBase, fetchImpl, keys, signal: opts.signal });
176
260
  if (!('manifest' in fetched))
177
261
  return fetched;
178
262
  const { manifest } = fetched;
@@ -193,18 +277,10 @@ async function installDaemon(opts = {}) {
193
277
  }
194
278
  let bytes;
195
279
  try {
196
- const res = await getWithTimeout(artifact.url, ARTIFACT_TIMEOUT_MS, fetchImpl);
197
- if (!res.ok) {
198
- return fail('download_failed', `${artifact.url} returned HTTP ${res.status}`);
199
- }
200
- const buf = Buffer.from(await res.arrayBuffer());
201
- if (buf.byteLength > MAX_ARTIFACT_BYTES) {
202
- return fail('download_failed', 'downloaded artifact exceeds the size ceiling');
203
- }
204
- bytes = buf;
280
+ bytes = await getBytesWithTimeout(artifact.url, ARTIFACT_TIMEOUT_MS, MAX_ARTIFACT_BYTES, fetchImpl, opts.signal);
205
281
  }
206
282
  catch (err) {
207
- return fail('download_failed', `${artifact.url}: ${err.message}`);
283
+ return fail('download_failed', `${artifact.url}: ${errorMessage(err)}`);
208
284
  }
209
285
  const actual = (0, node_crypto_1.createHash)('sha256').update(bytes).digest('hex');
210
286
  if (actual !== artifact.sha256.toLowerCase()) {
@@ -41,6 +41,7 @@ const node_path_1 = __importDefault(require("node:path"));
41
41
  const first_party_harness_1 = require("./first-party-harness");
42
42
  const auth_core_1 = require("@skrr-ai/auth-core");
43
43
  const harness_tiers_1 = require("./harness-tiers");
44
+ const first_party_harness_update_1 = require("./first-party-harness-update");
44
45
  const first_party_harness_managed_1 = require("./first-party-harness-managed");
45
46
  const first_party_harness_project_trust_1 = require("./first-party-harness-project-trust");
46
47
  /**
@@ -103,7 +104,7 @@ function releaseChannelCheck(env) {
103
104
  name: 'release-channel',
104
105
  status: 'ok',
105
106
  detail: `${channel} — ${canonical}`,
106
- remedy: 'Fetch that URL to see whether the channel has a published release, and which version.' +
107
+ remedy: 'Run `skrr code update --check` to verify the published release, or fetch that URL to inspect it.' +
107
108
  (alsoTried.length ? ` If it has none, an installer also tries ${alsoTried.join(', ')}.` : ''),
108
109
  };
109
110
  }
@@ -245,6 +246,34 @@ function engineCheck(env) {
245
246
  remedy: `Install the engine (\`${auth_core_1.FIRST_PARTY_HARNESS.command} install\`), or set ${pathVar} to an existing build.`,
246
247
  };
247
248
  }
249
+ function engineUpdateCheck(env) {
250
+ const update = (0, first_party_harness_update_1.getLocalCodeUpdateStatus)(env);
251
+ const last = update.lastAttempt;
252
+ const completed = typeof last?.completedAt === 'number' ? new Date(last.completedAt) : null;
253
+ const completedAt = completed && Number.isFinite(completed.getTime()) ? completed.toISOString() : null;
254
+ const details = [
255
+ `installed ${update.installedVersion ?? (update.activePath ? 'version not recorded' : 'none')}`,
256
+ `owner ${update.owner}`,
257
+ update.installedVerified ? 'signed release verified' : null,
258
+ update.offeredVersion ? `latest ${update.offeredVersion}` : 'channel not checked recently',
259
+ last
260
+ ? `last ${last.operation}: ${last.status}${completedAt ? ` at ${completedAt}` : ''}`
261
+ : 'no recorded update attempt',
262
+ ]
263
+ .filter(Boolean)
264
+ .join('; ');
265
+ return {
266
+ name: 'engine-update',
267
+ status: last?.status === 'failed' || update.state === 'check-failed' ? 'warn' : 'ok',
268
+ detail: details,
269
+ remedy: last?.status === 'failed'
270
+ ? `${last.error ?? last.code ?? 'Update failed.'} ${update.owner === 'managed' ? 'Retry with `skrr code update`.' : (update.message ?? update.command)}`
271
+ : (update.message ??
272
+ (update.state === 'update-available'
273
+ ? 'Run `skrr code update` to install the signed release.'
274
+ : 'Run `skrr code update --check` to check the signed channel; `skrr code rollback` restores the previous verified release.')),
275
+ };
276
+ }
248
277
  function stateDirCheck(cwd, env) {
249
278
  const project = projectStateDir(cwd);
250
279
  const user = (0, first_party_harness_1.engineHome)(env);
@@ -863,6 +892,7 @@ function agentAttributionCheck() {
863
892
  function runDoctor(cwd = process.cwd(), env = process.env) {
864
893
  const checks = [
865
894
  engineCheck(env),
895
+ engineUpdateCheck(env),
866
896
  releaseChannelCheck(env),
867
897
  stateDirCheck(cwd, env),
868
898
  compatCheck(cwd, env),
@@ -0,0 +1,71 @@
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 {
26
+ static description: string;
27
+ static examples: string[];
28
+ static flags: {
29
+ daemon: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
30
+ wait: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
31
+ json: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
32
+ };
33
+ 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
+ }