@skrr-ai/cli 0.1.28 → 0.1.29

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 (66) hide show
  1. package/dist/base-command.d.ts +15 -0
  2. package/dist/base-command.js +49 -0
  3. package/dist/commands/agents/chat.d.ts +2 -0
  4. package/dist/commands/agents/chat.js +40 -2
  5. package/dist/commands/balance/show.d.ts +2 -3
  6. package/dist/commands/balance/show.js +2 -3
  7. package/dist/commands/balance/usage.d.ts +3 -11
  8. package/dist/commands/balance/usage.js +19 -72
  9. package/dist/commands/code/index.d.ts +4 -0
  10. package/dist/commands/code/index.js +9 -0
  11. package/dist/commands/code/install.d.ts +10 -2
  12. package/dist/commands/code/install.js +38 -28
  13. package/dist/commands/harnesses/leases/show.js +7 -4
  14. package/dist/commands/inbox/index.d.ts +15 -0
  15. package/dist/commands/inbox/index.js +52 -20
  16. package/dist/commands/instructions/install.d.ts +12 -0
  17. package/dist/commands/instructions/install.js +59 -14
  18. package/dist/commands/login.js +6 -0
  19. package/dist/commands/machines/dedicated/attach.js +1 -1
  20. package/dist/commands/machines/dedicated/cp.d.ts +1 -0
  21. package/dist/commands/machines/dedicated/cp.js +63 -9
  22. package/dist/commands/machines/dedicated/create.d.ts +1 -1
  23. package/dist/commands/machines/dedicated/create.js +7 -2
  24. package/dist/commands/machines/dedicated/exec.d.ts +23 -1
  25. package/dist/commands/machines/dedicated/exec.js +67 -7
  26. package/dist/commands/machines/dedicated/index.js +2 -0
  27. package/dist/commands/machines/dedicated/restore.d.ts +6 -0
  28. package/dist/commands/machines/dedicated/restore.js +7 -1
  29. package/dist/commands/machines/dedicated/sign-in.d.ts +4 -3
  30. package/dist/commands/machines/dedicated/sign-in.js +4 -3
  31. package/dist/commands/machines/dedicated/terminal.js +1 -1
  32. package/dist/commands/machines/dedicated/update-image.d.ts +15 -0
  33. package/dist/commands/machines/dedicated/update-image.js +38 -0
  34. package/dist/lib/balance.d.ts +2 -2
  35. package/dist/lib/balance.js +6 -4
  36. package/dist/lib/daemon-target.d.ts +103 -0
  37. package/dist/lib/daemon-target.js +110 -0
  38. package/dist/lib/dedicated-copy.d.ts +92 -2
  39. package/dist/lib/dedicated-copy.js +223 -18
  40. package/dist/lib/dedicated-lease-command.d.ts +7 -1
  41. package/dist/lib/dedicated-lease-command.js +16 -3
  42. package/dist/lib/dedicated-machines.d.ts +130 -8
  43. package/dist/lib/dedicated-machines.js +274 -15
  44. package/dist/lib/dedicated-terminal.d.ts +5 -25
  45. package/dist/lib/dedicated-terminal.js +45 -73
  46. package/dist/lib/dedicated-wait.d.ts +10 -0
  47. package/dist/lib/dedicated-wait.js +52 -0
  48. package/dist/lib/device-code.d.ts +12 -1
  49. package/dist/lib/device-code.js +44 -9
  50. package/dist/lib/harnesses.d.ts +13 -0
  51. package/dist/lib/harnesses.js +24 -0
  52. package/dist/lib/login.js +8 -7
  53. package/dist/lib/sky-code-broker.d.ts +46 -5
  54. package/dist/lib/sky-code-broker.js +96 -26
  55. package/dist/lib/sky-code.d.ts +33 -0
  56. package/dist/lib/sky-code.js +45 -7
  57. package/dist/lib/task-instruction-offer.js +12 -0
  58. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refresh.d.ts +67 -1
  59. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refresh.js +124 -12
  60. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refresh.d.ts +67 -1
  61. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refresh.js +123 -11
  62. package/dist/node_modules/@skrr-ai/auth-core/package.json +1 -1
  63. package/dist/node_modules/@skrr-ai/data-provider/index.js +3061 -2876
  64. package/dist/node_modules/@skrr-ai/data-provider/package.json +1 -1
  65. package/oclif.manifest.json +2930 -2828
  66. package/package.json +1 -1
@@ -367,6 +367,21 @@ export declare function withSubject(subject: string | undefined, message: string
367
367
  * Exported for tests.
368
368
  */
369
369
  export declare function describeHarnessUnavailable(body: Record<string, unknown> | null, bin?: string): string;
370
+ /**
371
+ * Frame a typed `GITHUB_UNAVAILABLE` failure as GitHub being unreachable rather
372
+ * than as a control-plane outage.
373
+ *
374
+ * The CLI's own sentence, keyed on the code, because the server's cannot be
375
+ * relied on: in production the 5xx redaction boundary keeps `code` and replaces
376
+ * the message with "An unexpected error occurred." Same meaning as the web's
377
+ * copy for this code (`hostedJobTaskFailureMessage`) — GitHub, not skrr, and
378
+ * waiting is the fix.
379
+ *
380
+ * Exported for tests.
381
+ */
382
+ export declare function describeGitHubUnavailable(): string;
383
+ /** A deployment setting cannot recover through a client retry. */
384
+ export declare function describeGitHubAppNotConfigured(): string;
370
385
  export declare function describeResourceNotFound(serverMessage: string | undefined, bin?: string, baseURL?: string): string | null;
371
386
  export declare function describeAssigneeNotInSpace(serverMessage: string | undefined, bin?: string): string;
372
387
  export declare function describeForbidden(bodyMessage?: string | null, method?: string | null): string;
@@ -5,6 +5,8 @@ exports.assigneePayloadNeedsResolution = assigneePayloadNeedsResolution;
5
5
  exports.parseGlobalFlags = parseGlobalFlags;
6
6
  exports.withSubject = withSubject;
7
7
  exports.describeHarnessUnavailable = describeHarnessUnavailable;
8
+ exports.describeGitHubUnavailable = describeGitHubUnavailable;
9
+ exports.describeGitHubAppNotConfigured = describeGitHubAppNotConfigured;
8
10
  exports.describeResourceNotFound = describeResourceNotFound;
9
11
  exports.describeAssigneeNotInSpace = describeAssigneeNotInSpace;
10
12
  exports.describeForbidden = describeForbidden;
@@ -851,6 +853,32 @@ class BaseCommand extends core_1.Command {
851
853
  details: body ?? undefined,
852
854
  });
853
855
  }
856
+ else if (bodyCode === 'GITHUB_APP_NOT_CONFIGURED') {
857
+ this.failWithCliError({
858
+ message: say(describeGitHubAppNotConfigured()),
859
+ code: bodyCode,
860
+ status: e.status,
861
+ exit: 2,
862
+ retryable: false,
863
+ details: body ?? undefined,
864
+ });
865
+ }
866
+ else if (bodyCode === 'GITHUB_UNAVAILABLE') {
867
+ // GitHub could not be reached — also a 503, also not a platform outage:
868
+ // the API answered, with a typed code. The body has no `cause` or
869
+ // `retryable: false`, so without this it reached the outage branch and
870
+ // told the user skrr was down, and recorded an incident saying so.
871
+ // Unlike an offline runtime, retrying IS the remedy, so the outage
872
+ // branch's exit code and verdict stay.
873
+ this.failWithCliError({
874
+ message: say(describeGitHubUnavailable()),
875
+ code: bodyCode,
876
+ status: e.status,
877
+ exit: 5,
878
+ retryable: true,
879
+ details: body ?? undefined,
880
+ });
881
+ }
854
882
  else if (bodyCode === 'ASSIGNEE_NOT_IN_SPACE') {
855
883
  // Code-specific enrichment takes precedence over the status heuristic:
856
884
  // the server rejects a non-member assignee by id with no next step, so
@@ -1325,6 +1353,27 @@ function describeHarnessUnavailable(body, bin = 'skrr') {
1325
1353
  `\`${bin} agents attach-harness\`.`;
1326
1354
  return `${lead}\n → ${fix}`;
1327
1355
  }
1356
+ /**
1357
+ * Frame a typed `GITHUB_UNAVAILABLE` failure as GitHub being unreachable rather
1358
+ * than as a control-plane outage.
1359
+ *
1360
+ * The CLI's own sentence, keyed on the code, because the server's cannot be
1361
+ * relied on: in production the 5xx redaction boundary keeps `code` and replaces
1362
+ * the message with "An unexpected error occurred." Same meaning as the web's
1363
+ * copy for this code (`hostedJobTaskFailureMessage`) — GitHub, not skrr, and
1364
+ * waiting is the fix.
1365
+ *
1366
+ * Exported for tests.
1367
+ */
1368
+ function describeGitHubUnavailable() {
1369
+ return ('GitHub is temporarily unreachable, so this request could not be completed. ' +
1370
+ 'skrr itself answered, so this is not a skrr outage — try again shortly.');
1371
+ }
1372
+ /** A deployment setting cannot recover through a client retry. */
1373
+ function describeGitHubAppNotConfigured() {
1374
+ return ('The GitHub App is not configured on this deployment. ' +
1375
+ 'Retrying will not help — ask the deployment operator to configure it.');
1376
+ }
1328
1377
  /**
1329
1378
  * Enrich the server's `ASSIGNEE_NOT_IN_SPACE` rejection with the same
1330
1379
  * copy-pasteable remedy the by-name resolution failure already gives (OSK-4864).
@@ -43,6 +43,8 @@ export declare const STREAM_LOST_EXIT_CODE = 75;
43
43
  export declare function interruptedDetachMessage(input: {
44
44
  sessionId: string;
45
45
  conversationId?: string | null;
46
+ /** The second Ctrl-C: the CLI stopped waiting rather than running out the grace period. */
47
+ forced?: boolean;
46
48
  }): string;
47
49
  /**
48
50
  * What the durable record says about a turn, read from its messages.
@@ -7,6 +7,7 @@ exports.inspectTurnCommand = inspectTurnCommand;
7
7
  exports.noOutputFailureMessage = noOutputFailureMessage;
8
8
  exports.noStreamJsonPayload = noStreamJsonPayload;
9
9
  exports.chatStreamHeaderLines = chatStreamHeaderLines;
10
+ const node_fs_1 = require("node:fs");
10
11
  const core_1 = require("@oclif/core");
11
12
  const data_provider_1 = require("@skrr-ai/data-provider");
12
13
  const base_command_1 = require("../../base-command");
@@ -53,8 +54,12 @@ exports.STREAM_LOST_EXIT_CODE = 75;
53
54
  * cancelled. This says what was asked, what is not guaranteed, and how to look.
54
55
  */
55
56
  function interruptedDetachMessage(input) {
56
- return (`Cancel requested for run ${input.sessionId}, but no terminal event arrived before the ` +
57
- 'grace period ended. The runtime MAY STILL BE EXECUTING — a tool call already running on ' +
57
+ const waited = input.forced
58
+ ? `Cancel requested for run ${input.sessionId}; stopped waiting for it on the second Ctrl-C. `
59
+ : `Cancel requested for run ${input.sessionId}, but no terminal event arrived before the ` +
60
+ 'grace period ended. ';
61
+ return (waited +
62
+ 'The runtime MAY STILL BE EXECUTING — a tool call already running on ' +
58
63
  'your machine is not guaranteed to stop. ' +
59
64
  `Check what it did with: ${inspectTurnCommand(input.conversationId)}` +
60
65
  (process.platform === 'win32'
@@ -454,12 +459,20 @@ class AgentsChat extends base_command_1.BaseCommand {
454
459
  // event, and if it does not arrive detach explicitly while naming the
455
460
  // session so the run can be re-attached or cancelled. A second Ctrl-C
456
461
  // detaches immediately rather than being a blind kill.
462
+ //
463
+ // None of this ran until OSK-8708: auth-core's lock handler, installed
464
+ // before any command, exited the process on the FIRST Ctrl-C. It now defers
465
+ // to this listener — and exits the process itself on a SECOND signal, as
466
+ // soon as that signal's listeners have run. So the second Ctrl-C's detach
467
+ // notice is written here, synchronously, straight to the descriptor: the
468
+ // probe and the message the detach path prints after it would never happen.
457
469
  const CANCEL_GRACE_MS = 10_000;
458
470
  let detach = null;
459
471
  const detached = new Promise((resolve) => {
460
472
  detach = () => resolve('detached');
461
473
  });
462
474
  let interrupts = 0;
475
+ let forcedDetachAnnounced = false;
463
476
  let graceTimer = null;
464
477
  const onInterrupt = () => {
465
478
  interrupts += 1;
@@ -474,6 +487,27 @@ class AgentsChat extends base_command_1.BaseCommand {
474
487
  graceTimer.unref?.();
475
488
  return;
476
489
  }
490
+ if (!forcedDetachAnnounced) {
491
+ forcedDetachAnnounced = true;
492
+ const message = interruptedDetachMessage({
493
+ sessionId: started.sessionId,
494
+ conversationId: started.conversationId,
495
+ forced: true,
496
+ });
497
+ if (flags.jsonl) {
498
+ (0, node_fs_1.writeSync)(process.stdout.fd, `${JSON.stringify({
499
+ type: 'run.detached',
500
+ sessionId: started.sessionId,
501
+ conversationId: started.conversationId,
502
+ interrupted: true,
503
+ message,
504
+ })}\n`);
505
+ }
506
+ else {
507
+ (0, node_fs_1.writeSync)(process.stderr.fd, `\n${message}\n`);
508
+ }
509
+ process.exitCode = exports.DETACH_EXIT_CODE;
510
+ }
477
511
  detach?.();
478
512
  };
479
513
  process.on('SIGINT', onInterrupt);
@@ -495,6 +529,10 @@ class AgentsChat extends base_command_1.BaseCommand {
495
529
  // turns the server had already finalised as errors — "it may still be
496
530
  // executing" is the most expensive thing to be wrong about here, because
497
531
  // it is what stops a caller retrying or alerting (OSK-7595, OSK-7660).
532
+ if (outcome === 'detached' && forcedDetachAnnounced) {
533
+ // Already said, and the exit code already set, on the second Ctrl-C.
534
+ return;
535
+ }
498
536
  let reconciled = null;
499
537
  if (outcome === 'detached') {
500
538
  if (await client.probeTerminal('detach'))
@@ -3,9 +3,8 @@ import { type RuntimeMonthlySpend } from '../../lib/harnesses';
3
3
  /**
4
4
  * The hosted-runtime monthly cap rows, as lines.
5
5
  *
6
- * Pure so it can be tested the way `renderUsageSummary` is: this screen's job
7
- * is to stop a user reading headroom off the wrong cap, and the wording is the
8
- * fix, so the wording is what gets pinned.
6
+ * Pure because this screen must stop a user reading headroom off the wrong cap;
7
+ * the wording is part of the observable contract and is pinned directly.
9
8
  */
10
9
  export declare function renderRuntimeSpend(spend: RuntimeMonthlySpend[], log: (line: string) => void): void;
11
10
  /**
@@ -10,9 +10,8 @@ const usd = (cents) => (typeof cents === 'number' ? `$${(cents / 100).toFixed(2)
10
10
  /**
11
11
  * The hosted-runtime monthly cap rows, as lines.
12
12
  *
13
- * Pure so it can be tested the way `renderUsageSummary` is: this screen's job
14
- * is to stop a user reading headroom off the wrong cap, and the wording is the
15
- * fix, so the wording is what gets pinned.
13
+ * Pure because this screen must stop a user reading headroom off the wrong cap;
14
+ * the wording is part of the observable contract and is pinned directly.
16
15
  */
17
16
  function renderRuntimeSpend(spend, log) {
18
17
  const capped = spend.filter((row) => row.hasLimit);
@@ -1,10 +1,7 @@
1
+ import type { TSkyCodeUsageSnapshot } from '@skrr-ai/data-provider';
1
2
  import { BaseCommand } from '../../base-command';
2
3
  /**
3
- * Aggregate usage summary.
4
- *
5
- * The shape is owned server-side by `getUsageSummary` and is not a stable
6
- * contract this command should reshape, so the default output pretty-prints
7
- * what the server sent rather than inventing a table over fields that may move.
4
+ * Sky Code's versioned monthly and five-hour usage snapshot.
8
5
  */
9
6
  export default class BalanceUsage extends BaseCommand {
10
7
  static description: string;
@@ -14,9 +11,4 @@ export default class BalanceUsage extends BaseCommand {
14
11
  };
15
12
  run(): Promise<void>;
16
13
  }
17
- export declare function renderUsageSummary(summary: Record<string, unknown>, log: (line: string) => void): void;
18
- /**
19
- * Why `--workspace` is refused here rather than silently dropped, and where a
20
- * workspace's figures actually live.
21
- */
22
- export declare function usageNotWorkspaceScopedMessage(workspaceId: string, bin: string): string;
14
+ export declare function renderSkyCodeUsage(usage: TSkyCodeUsageSnapshot, log: (line: string) => void): void;
@@ -1,16 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.renderUsageSummary = renderUsageSummary;
4
- exports.usageNotWorkspaceScopedMessage = usageNotWorkspaceScopedMessage;
3
+ exports.renderSkyCodeUsage = renderSkyCodeUsage;
5
4
  const core_1 = require("@oclif/core");
6
5
  const base_command_1 = require("../../base-command");
7
6
  const balance_1 = require("../../lib/balance");
8
7
  /**
9
- * Aggregate usage summary.
10
- *
11
- * The shape is owned server-side by `getUsageSummary` and is not a stable
12
- * contract this command should reshape, so the default output pretty-prints
13
- * what the server sent rather than inventing a table over fields that may move.
8
+ * Sky Code's versioned monthly and five-hour usage snapshot.
14
9
  */
15
10
  class BalanceUsage extends base_command_1.BaseCommand {
16
11
  static description = 'Usage summary against your hosted usage balance';
@@ -19,24 +14,10 @@ class BalanceUsage extends base_command_1.BaseCommand {
19
14
  async run() {
20
15
  this.requireAuth();
21
16
  const { flags } = await this.parse(BalanceUsage);
22
- // `--workspace` is a global flag, so it parsed here and was then ignored:
23
- // `balance usage --workspace <id>` printed the caller's PERSONAL summary
24
- // with exit 0, which a member following a workspace refusal read as the
25
- // workspace's (OSK-4820). There is no workspace-scoped usage read behind
26
- // this command, so the honest answer is a refusal that names the reads
27
- // that do exist.
28
17
  const workspaceId = flags.workspace;
29
- if (workspaceId) {
30
- this.failWithCliError({
31
- message: usageNotWorkspaceScopedMessage(workspaceId, this.config.bin),
32
- code: 'BALANCE_USAGE_NOT_WORKSPACE_SCOPED',
33
- exit: 2,
34
- retryable: false,
35
- });
36
- }
37
18
  let summary;
38
19
  try {
39
- summary = await (0, balance_1.getBalanceUsage)();
20
+ summary = await (0, balance_1.getSkyCodeUsage)(workspaceId);
40
21
  }
41
22
  catch (err) {
42
23
  this.handleApiError(err);
@@ -49,58 +30,24 @@ class BalanceUsage extends base_command_1.BaseCommand {
49
30
  this.log('No usage recorded.');
50
31
  return;
51
32
  }
52
- renderUsageSummary(summary, (line) => this.log(line));
33
+ renderSkyCodeUsage(summary, (line) => this.log(line));
53
34
  }
54
35
  }
55
36
  exports.default = BalanceUsage;
56
- /**
57
- * Every monetary field here is in the credit/micros unit (CREDITS_PER_USD),
58
- * despite names like `tokenLimit` — a limit of 10,000,000 is $10.00 per period,
59
- * not ten million tokens (see lib/balance.ts, OSK-4664). The command used to
60
- * dump the raw response, so those figures printed unitless and `tokenLimit`
61
- * read as tokens — the single most misleading number on the surface (OSK-4826).
62
- * Known money fields are formatted with credits + USD; anything unrecognised is
63
- * still printed raw so a server-side field addition is never hidden.
64
- */
65
- const MONEY_FIELDS = {
66
- creditBalance: 'Ledger balance (before holds)',
67
- monthlyUsage: 'Used this month',
68
- monthlyAllowance: 'Monthly allowance',
69
- tokenLimit: 'Period limit',
70
- periodTokensUsed: 'Used this period',
71
- projectedPeriodUsage: 'Projected period usage',
72
- };
73
- function renderUsageSummary(summary, log) {
74
- const label = (key) => (MONEY_FIELDS[key] ?? key).padEnd(24);
75
- for (const [key, value] of Object.entries(summary)) {
76
- if (key === 'runRateNudge' && value != null && typeof value === 'object') {
77
- log(' Run-rate nudge');
78
- for (const [k, v] of Object.entries(value)) {
79
- const rendered = k === 'projectedPeriodUsage' && typeof v === 'number'
80
- ? (0, balance_1.formatCreditsWithUsd)(v)
81
- : String(v ?? '-');
82
- log(` ${k.padEnd(22)} ${rendered}`);
83
- }
84
- continue;
85
- }
86
- if (key in MONEY_FIELDS && typeof value === 'number') {
87
- log(` ${label(key)} ${(0, balance_1.formatCreditsWithUsd)(value)}`);
88
- continue;
89
- }
90
- const rendered = value != null && typeof value === 'object' ? JSON.stringify(value) : String(value ?? '-');
91
- log(` ${label(key)} ${rendered}`);
37
+ function renderSkyCodeUsage(usage, log) {
38
+ log(`Sky Code ${usage.plan ?? 'current'} plan · ${usage.ownerType} ${usage.ownerId}`);
39
+ log(` Monthly used ${(0, balance_1.formatCreditsWithUsd)(usage.monthly.usedUsageMicros)}`);
40
+ log(` Monthly reserved ${(0, balance_1.formatCreditsWithUsd)(usage.monthly.reservedUsageMicros)}`);
41
+ log(` Monthly remaining ${(0, balance_1.formatCreditsWithUsd)(usage.monthly.remainingUsageMicros)}`);
42
+ log(` Monthly reset ${usage.monthly.resetsAt ?? 'unavailable'}`);
43
+ log(` 5-hour used ${(0, balance_1.formatCreditsWithUsd)(usage.fiveHour.usedUsageMicros)}`);
44
+ log(` 5-hour reserved ${(0, balance_1.formatCreditsWithUsd)(usage.fiveHour.reservedUsageMicros)}`);
45
+ log(` 5-hour limit ${(0, balance_1.formatCreditsWithUsd)(usage.fiveHour.limitUsageMicros)}`);
46
+ log(` 5-hour reset ${usage.fiveHour.resetsAt ?? 'unavailable'}`);
47
+ log(` Concurrent sessions ${usage.fiveHour.activeSessions}/${usage.fiveHour.maxConcurrentSessions}`);
48
+ log(` Available overage ${(0, balance_1.formatCreditsWithUsd)(usage.balance.availableMicros)}`);
49
+ log(` Overage consent ${usage.overageEnabled ? 'enabled' : 'disabled'}`);
50
+ if (usage.dataStatus === 'partial') {
51
+ log(` Reconciliation partial (${usage.unresolvedUsageCount} unresolved)`);
92
52
  }
93
- log('');
94
- log('Figures are in credits, with the dollar value alongside.');
95
- log('`skrr balance show` reports the authoritative available balance used to admit a turn (holds excluded).');
96
- }
97
- /**
98
- * Why `--workspace` is refused here rather than silently dropped, and where a
99
- * workspace's figures actually live.
100
- */
101
- function usageNotWorkspaceScopedMessage(workspaceId, bin) {
102
- return (`\`${bin} balance usage\` summarises your PERSONAL account and cannot be scoped to ` +
103
- `workspace ${workspaceId} — it would have shown your own figures under that id. ` +
104
- `For the workspace, use \`${bin} balance show --workspace ${workspaceId}\` (any member) ` +
105
- `or \`${bin} balance statement --workspace ${workspaceId}\` (owner).`);
106
53
  }
@@ -25,6 +25,10 @@ export default class Code extends Command {
25
25
  * engine exit into a generic CLI failure would make "the task failed" and "the
26
26
  * CLI could not start it" indistinguishable in CI.
27
27
  *
28
+ * So the codes this function owns are the ones where no engine ran: `127`, the
29
+ * engine is not installed, and `77`, the account was refused before the engine
30
+ * could start (OSK-8822).
31
+ *
28
32
  * ── Why `managed` is opt-IN per command ────────────────────────────────────
29
33
  *
30
34
  * `skrr code acp` must NOT take the managed path: the daemon that drives it already
@@ -63,6 +63,10 @@ exports.default = Code;
63
63
  * engine exit into a generic CLI failure would make "the task failed" and "the
64
64
  * CLI could not start it" indistinguishable in CI.
65
65
  *
66
+ * So the codes this function owns are the ones where no engine ran: `127`, the
67
+ * engine is not installed, and `77`, the account was refused before the engine
68
+ * could start (OSK-8822).
69
+ *
66
70
  * ── Why `managed` is opt-IN per command ────────────────────────────────────
67
71
  *
68
72
  * `skrr code acp` must NOT take the managed path: the daemon that drives it already
@@ -107,6 +111,11 @@ async function forwardToEngine(cmd, subcommand, passthrough, options = {}) {
107
111
  cmd.exit(code);
108
112
  }
109
113
  catch (err) {
114
+ if (err instanceof sky_code_1.EngineStartRefusedError) {
115
+ // The engine never ran, so there is no engine code to pass through (OSK-8822).
116
+ // Silent here: the hook that refused already said why, once, on stderr.
117
+ cmd.exit(sky_code_1.ENGINE_START_REFUSED_EXIT_CODE);
118
+ }
110
119
  if (err instanceof Error && err.message === 'sky-code-not-installed') {
111
120
  cmd.log((0, sky_code_1.notInstalledMessage)());
112
121
  cmd.exit(127); // conventional "command not found"
@@ -31,15 +31,23 @@ export default class CodeInstall extends BaseCommand {
31
31
  json: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
32
32
  };
33
33
  run(): Promise<void>;
34
+ /** Which daemon the install targets, by the rule every daemon-picking command shares. */
35
+ private resolveTarget;
34
36
  /**
35
- * Which daemon does the install.
37
+ * The daemon id to install through, or the refusal.
36
38
  *
37
39
  * Refuses rather than guessing when several are active. Picking one would mean
38
40
  * installing a hundred megabytes onto a machine the user was not thinking
39
41
  * about, and the list this reads is the same one `skrr list-daemons` prints —
40
42
  * so the error can name the exact values that work.
43
+ *
44
+ * A Dedicated Runtime's own machine is refused here, before any request. The
45
+ * daemon and the API refuse it too; saying so locally is what keeps the answer
46
+ * the same whether or not this machine's daemon is reachable, and whether or
47
+ * not it has published its id. Another of the owner's machines, named with
48
+ * `--daemon`, is not this machine and goes to the API like anywhere else.
41
49
  */
42
- private resolveDaemon;
50
+ private installTarget;
43
51
  /**
44
52
  * What the daemon says about this provider, when it started no job.
45
53
  *
@@ -4,6 +4,7 @@ const core_1 = require("@oclif/core");
4
4
  const base_command_1 = require("../../base-command");
5
5
  const api_fetch_1 = require("../../lib/api-fetch");
6
6
  const daemon_ref_1 = require("../../lib/daemon-ref");
7
+ const daemon_target_1 = require("../../lib/daemon-target");
7
8
  const sky_code_1 = require("../../lib/sky-code");
8
9
  /**
9
10
  * `skrr code install` — install the engine from the terminal.
@@ -37,7 +38,7 @@ class CodeInstall extends base_command_1.BaseCommand {
37
38
  ];
38
39
  static flags = {
39
40
  daemon: core_1.Flags.string({
40
- description: 'Which daemon installs it. Defaults to the only active one; required when you have several.',
41
+ description: 'Which daemon installs it. Defaults to this machine on a Dedicated Runtime, otherwise to the only active one; required when you have several.',
41
42
  }),
42
43
  wait: core_1.Flags.boolean({
43
44
  default: true,
@@ -48,19 +49,7 @@ class CodeInstall extends base_command_1.BaseCommand {
48
49
  };
49
50
  async run() {
50
51
  const { flags } = await this.parse(CodeInstall);
51
- // Refused locally, before choosing a daemon. The daemon and the API refuse
52
- // too, but with several active daemons this command would first ask which
53
- // one — and "pick a machine" is the wrong question to put to someone whose
54
- // answer is "none of them can do this here".
55
- if ((0, sky_code_1.isDedicatedRuntimeWorkload)()) {
56
- this.failWithCliError({
57
- message: sky_code_1.DEDICATED_RUNTIME_ENGINE_MESSAGE,
58
- code: 'DEDICATED_RUNTIME_ENGINE_SHIPS_WITH_IMAGE',
59
- exit: 1,
60
- retryable: false,
61
- });
62
- }
63
- const daemonId = await this.resolveDaemon(flags.daemon);
52
+ const daemonId = this.installTarget(await this.resolveTarget(flags.daemon));
64
53
  let started;
65
54
  try {
66
55
  started = await (0, api_fetch_1.apiFetch)(`/api/daemons/${encodeURIComponent(daemonId)}/cli-installers/${PROVIDER}/install`, { method: 'POST', body: {}, credential: this.resolvedCredential });
@@ -136,28 +125,48 @@ class CodeInstall extends base_command_1.BaseCommand {
136
125
  this.log(`skrr Code installed. Run \`${this.config.bin} code\` to start it.`);
137
126
  }
138
127
  }
128
+ /** Which daemon the install targets, by the rule every daemon-picking command shares. */
129
+ async resolveTarget(explicit) {
130
+ try {
131
+ return await (0, daemon_target_1.resolveDaemonTarget)({
132
+ explicit,
133
+ local: (0, daemon_target_1.localMachine)({ baseURL: this.cliConfig?.baseURL }),
134
+ listActiveDaemons: () => (0, daemon_target_1.listActiveDaemons)(this.resolvedCredential),
135
+ });
136
+ }
137
+ catch (err) {
138
+ this.handleApiError(err);
139
+ }
140
+ }
139
141
  /**
140
- * Which daemon does the install.
142
+ * The daemon id to install through, or the refusal.
141
143
  *
142
144
  * Refuses rather than guessing when several are active. Picking one would mean
143
145
  * installing a hundred megabytes onto a machine the user was not thinking
144
146
  * about, and the list this reads is the same one `skrr list-daemons` prints —
145
147
  * so the error can name the exact values that work.
148
+ *
149
+ * A Dedicated Runtime's own machine is refused here, before any request. The
150
+ * daemon and the API refuse it too; saying so locally is what keeps the answer
151
+ * the same whether or not this machine's daemon is reachable, and whether or
152
+ * not it has published its id. Another of the owner's machines, named with
153
+ * `--daemon`, is not this machine and goes to the API like anywhere else.
146
154
  */
147
- async resolveDaemon(explicit) {
148
- if (explicit)
149
- return (0, daemon_ref_1.normalizeDaemonRef)(explicit);
150
- let res;
151
- try {
152
- res = await (0, api_fetch_1.apiFetch)('/api/daemon-resources?status=active&limit=50', { credential: this.resolvedCredential });
153
- }
154
- catch (err) {
155
- this.handleApiError(err);
155
+ installTarget(target) {
156
+ if (target.kind === 'daemon')
157
+ return target.daemonId;
158
+ if (target.kind === 'this-machine') {
159
+ this.failWithCliError({
160
+ message: target.explicit
161
+ ? sky_code_1.DEDICATED_RUNTIME_ENGINE_MESSAGE
162
+ : `${sky_code_1.DEDICATED_RUNTIME_ENGINE_MESSAGE} This command installs on the machine it runs on; ` +
163
+ 'to install on another of your machines, name it with --daemon.',
164
+ code: 'DEDICATED_RUNTIME_ENGINE_SHIPS_WITH_IMAGE',
165
+ exit: 1,
166
+ retryable: false,
167
+ });
156
168
  }
157
- const daemons = (res.daemons ?? []).filter((d) => d.isActive !== false);
158
- if (daemons.length === 1)
159
- return daemons[0].id;
160
- if (daemons.length === 0) {
169
+ if (target.kind === 'none') {
161
170
  this.failWithCliError({
162
171
  message: 'No active daemon is registered for this account, so nothing can perform the install. ' +
163
172
  `Start the local runtime with \`${this.config.bin} daemon start\`, then run this again.`,
@@ -166,6 +175,7 @@ class CodeInstall extends base_command_1.BaseCommand {
166
175
  retryable: false,
167
176
  });
168
177
  }
178
+ const { daemons } = target;
169
179
  const choices = daemons
170
180
  .slice(0, 10)
171
181
  .map((d) => ` ${(0, daemon_ref_1.formatDaemonRef)(d.id)} ${d.name || '—'}`)
@@ -68,12 +68,15 @@ class HarnessesLeasesShow extends base_command_1.BaseCommand {
68
68
  if (os || arch)
69
69
  line('Resources:', [os, arch].filter(Boolean).join('/'));
70
70
  // `assignable` is the question a reader has about health, and the reasons
71
- // are why — a bare status word would drop the actionable half.
71
+ // are why — a bare status word would drop the actionable half. The reasons
72
+ // are printed as sentences, judged against the lease state, in the words the
73
+ // machine page uses; `--json` keeps the codes (OSK-8768).
72
74
  const assignable = nested(lease.health, 'assignable');
73
75
  if (assignable !== undefined) {
74
- const reasons = lease.health?.reasons;
75
- const why = Array.isArray(reasons) && reasons.length > 0 ? ` (${reasons.join(', ')})` : '';
76
- line('Assignable:', `${assignable === true ? 'yes' : 'no'}${why}`);
76
+ const why = assignable === true
77
+ ? ''
78
+ : (0, harnesses_1.describeHealthReasonSentences)(lease.health?.reasons, lease.state);
79
+ line('Assignable:', `${assignable === true ? 'yes' : 'no'}${why ? ` — ${why}` : ''}`);
77
80
  }
78
81
  line('Created:', lease.createdAt);
79
82
  line('Updated:', lease.updatedAt);
@@ -1,4 +1,19 @@
1
1
  import { BaseCommand } from '../../base-command';
2
+ /**
3
+ * Stop `--follow` on Ctrl-C or SIGTERM, and end with the status a shell
4
+ * reports for that signal (130 / 143).
5
+ *
6
+ * The listener only started running with OSK-8708 — auth-core's lock handler
7
+ * used to exit the process first, with 130 for SIGINT. Now the follow loop
8
+ * stops and the command returns normally, which on its own would report
9
+ * success (0) for an interrupted follow and change what a script reads.
10
+ */
11
+ export declare function listenForFollowStop(proc?: Pick<NodeJS.Process, 'once' | 'removeListener'> & {
12
+ exitCode?: number | string;
13
+ }): {
14
+ stopped: () => boolean;
15
+ dispose: () => void;
16
+ };
2
17
  /**
3
18
  * `skrr inbox` / `skrr inbox --follow` — what have my agents said to me?
4
19
  *