@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
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.listenForFollowStop = listenForFollowStop;
3
4
  const core_1 = require("@oclif/core");
4
5
  const socket_io_client_1 = require("socket.io-client");
5
6
  const data_provider_1 = require("@skrr-ai/data-provider");
@@ -7,6 +8,33 @@ const base_command_1 = require("../../base-command");
7
8
  const inbox_1 = require("../../lib/inbox");
8
9
  const format_1 = require("../../lib/format");
9
10
  const agentic_stream_1 = require("../../lib/agentic-stream");
11
+ /**
12
+ * Stop `--follow` on Ctrl-C or SIGTERM, and end with the status a shell
13
+ * reports for that signal (130 / 143).
14
+ *
15
+ * The listener only started running with OSK-8708 — auth-core's lock handler
16
+ * used to exit the process first, with 130 for SIGINT. Now the follow loop
17
+ * stops and the command returns normally, which on its own would report
18
+ * success (0) for an interrupted follow and change what a script reads.
19
+ */
20
+ function listenForFollowStop(proc = process) {
21
+ let stopped = false;
22
+ const stopOn = (status) => () => {
23
+ stopped = true;
24
+ proc.exitCode = status;
25
+ };
26
+ const onSigint = stopOn(130);
27
+ const onSigterm = stopOn(143);
28
+ proc.once('SIGINT', onSigint);
29
+ proc.once('SIGTERM', onSigterm);
30
+ return {
31
+ stopped: () => stopped,
32
+ dispose: () => {
33
+ proc.removeListener('SIGINT', onSigint);
34
+ proc.removeListener('SIGTERM', onSigterm);
35
+ },
36
+ };
37
+ }
10
38
  /**
11
39
  * `skrr inbox` / `skrr inbox --follow` — what have my agents said to me?
12
40
  *
@@ -157,12 +185,7 @@ class Inbox extends base_command_1.BaseCommand {
157
185
  */
158
186
  async follow(flags) {
159
187
  const cursors = new Map();
160
- let stopped = false;
161
- const stop = () => {
162
- stopped = true;
163
- };
164
- process.once('SIGINT', stop);
165
- process.once('SIGTERM', stop);
188
+ const stop = listenForFollowStop();
166
189
  const emit = (message) => {
167
190
  if (flags.json) {
168
191
  this.log(JSON.stringify(message));
@@ -184,7 +207,7 @@ class Inbox extends base_command_1.BaseCommand {
184
207
  return;
185
208
  }
186
209
  for (const dm of dms) {
187
- if (stopped)
210
+ if (stop.stopped())
188
211
  break;
189
212
  const seen = cursors.get(dm.conversationId);
190
213
  // Skip the fetch entirely when the DM has not advanced. Without this,
@@ -233,7 +256,11 @@ class Inbox extends base_command_1.BaseCommand {
233
256
  });
234
257
  const received = new Set();
235
258
  socket.on('agent:user_message', (event) => {
236
- if (stopped || !event.agentId || !event.conversationId || !event.createdAt || !event.text)
259
+ if (stop.stopped() ||
260
+ !event.agentId ||
261
+ !event.conversationId ||
262
+ !event.createdAt ||
263
+ !event.text)
237
264
  return;
238
265
  if (flags.agent && event.agentId !== flags.agent)
239
266
  return;
@@ -251,20 +278,25 @@ class Inbox extends base_command_1.BaseCommand {
251
278
  if (!flags.json)
252
279
  this.warn(`inbox push disconnected: ${error.message}`);
253
280
  });
254
- await sync(true);
255
- if (!flags.json) {
256
- const label = flags.agent ? `agent ${flags.agent}` : 'all agents';
257
- this.log(`— watching ${label}, authenticated push with cursor backfill (Ctrl-C to stop) —`);
281
+ try {
282
+ await sync(true);
283
+ if (!flags.json) {
284
+ const label = flags.agent ? `agent ${flags.agent}` : 'all agents';
285
+ this.log(`— watching ${label}, authenticated push with cursor backfill (Ctrl-C to stop) —`);
286
+ }
287
+ while (!stop.stopped()) {
288
+ // Socket.IO reconnects independently. This low-frequency reconciliation
289
+ // also covers an older server that has not yet emitted the live event.
290
+ await sleep((0, inbox_1.clampInterval)(flags.interval) * 1000, stop.stopped);
291
+ if (!socket.connected)
292
+ await sync(false);
293
+ }
258
294
  }
259
- while (!stopped) {
260
- // Socket.IO reconnects independently. This low-frequency reconciliation
261
- // also covers an older server that has not yet emitted the live event.
262
- await sleep((0, inbox_1.clampInterval)(flags.interval) * 1000, () => stopped);
263
- if (!socket.connected)
264
- await sync(false);
295
+ finally {
296
+ socket.removeAllListeners();
297
+ socket.disconnect();
298
+ stop.dispose();
265
299
  }
266
- socket.removeAllListeners();
267
- socket.disconnect();
268
300
  }
269
301
  }
270
302
  exports.default = Inbox;
@@ -14,4 +14,16 @@ export default class InstructionsInstall extends BaseCommand {
14
14
  json: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
15
15
  };
16
16
  run(): Promise<void>;
17
+ /**
18
+ * The daemon the targets are created on, by the rule every daemon-picking
19
+ * command shares (`lib/daemon-target.ts`, OSK-8759).
20
+ *
21
+ * On a Dedicated Runtime guest that is the guest's own daemon, and unlike an
22
+ * engine install it is not refused: the API accepts any daemon with a live
23
+ * daemon session on the account (`ManagedInstructions.requireOwnedDaemon`),
24
+ * and the guest daemon reconciles a project target by writing its files as the
25
+ * workload (`daemon/src/managed-instructions/sync.ts`). Off a guest the rule is
26
+ * the one this command always had — the only daemon online, or `--daemon`.
27
+ */
28
+ private resolveDaemon;
17
29
  }
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const core_1 = require("@oclif/core");
4
4
  const data_provider_1 = require("@skrr-ai/data-provider");
5
5
  const base_command_1 = require("../../base-command");
6
+ const daemon_target_1 = require("../../lib/daemon-target");
6
7
  const prompt_1 = require("../../lib/prompt");
7
8
  class InstructionsInstall extends base_command_1.BaseCommand {
8
9
  static description = 'Opt in a daemon target to ongoing managed instruction reconciliation';
@@ -14,7 +15,9 @@ class InstructionsInstall extends base_command_1.BaseCommand {
14
15
  description: 'Instruction ID (the id `skrr instructions list` prints)',
15
16
  required: true,
16
17
  }),
17
- daemon: core_1.Flags.string({ description: 'Daemon ID; inferred when exactly one daemon is online' }),
18
+ daemon: core_1.Flags.string({
19
+ description: 'Daemon ID; defaults to this machine on a Dedicated Runtime, otherwise inferred when exactly one daemon is online',
20
+ }),
18
21
  provider: core_1.Flags.string({
19
22
  description: 'Provider to manage (repeatable)',
20
23
  options: ['claude', 'codex', 'sky-code'],
@@ -86,19 +89,7 @@ class InstructionsInstall extends base_command_1.BaseCommand {
86
89
  const confirmed = await (0, prompt_1.confirmDestructive)(`Authorize continuous ${flags.provider.join(', ')} reconciliation on this target?${teamWarning}`, flags.yes);
87
90
  if (!confirmed)
88
91
  return this.log('Aborted.');
89
- let daemonId = flags.daemon;
90
- if (!daemonId) {
91
- try {
92
- const daemons = await data_provider_1.dataService.getDaemons();
93
- if (daemons.active.length !== 1) {
94
- this.error('Pass --daemon when zero or multiple daemons are online', { exit: 2 });
95
- }
96
- daemonId = daemons.active[0].daemonId;
97
- }
98
- catch (error) {
99
- this.handleApiError(error);
100
- }
101
- }
92
+ const daemonId = await this.resolveDaemon(flags.daemon);
102
93
  const results = [];
103
94
  try {
104
95
  for (const provider of flags.provider) {
@@ -124,5 +115,59 @@ class InstructionsInstall extends base_command_1.BaseCommand {
124
115
  this.log(`Install queued: ${result.target.provider} ${result.target.scope}${result.target.projectPath ? ` ${result.target.projectPath}` : ''} (${result.target.id})`);
125
116
  }
126
117
  }
118
+ /**
119
+ * The daemon the targets are created on, by the rule every daemon-picking
120
+ * command shares (`lib/daemon-target.ts`, OSK-8759).
121
+ *
122
+ * On a Dedicated Runtime guest that is the guest's own daemon, and unlike an
123
+ * engine install it is not refused: the API accepts any daemon with a live
124
+ * daemon session on the account (`ManagedInstructions.requireOwnedDaemon`),
125
+ * and the guest daemon reconciles a project target by writing its files as the
126
+ * workload (`daemon/src/managed-instructions/sync.ts`). Off a guest the rule is
127
+ * the one this command always had — the only daemon online, or `--daemon`.
128
+ */
129
+ async resolveDaemon(explicit) {
130
+ let target;
131
+ try {
132
+ target = await (0, daemon_target_1.resolveDaemonTarget)({
133
+ explicit,
134
+ local: (0, daemon_target_1.localMachine)({ baseURL: this.cliConfig?.baseURL }),
135
+ // ONLINE daemons, as before — not the registered list `skrr code install`
136
+ // reads: a target is reconciled by a connected daemon.
137
+ listActiveDaemons: async () => (await data_provider_1.dataService.getDaemons()).active.map((daemon) => ({
138
+ id: daemon.daemonId,
139
+ ...(daemon.deviceName ? { name: daemon.deviceName } : {}),
140
+ })),
141
+ });
142
+ }
143
+ catch (error) {
144
+ this.handleApiError(error);
145
+ }
146
+ if (target.kind === 'daemon')
147
+ return target.daemonId;
148
+ if (target.kind === 'this-machine') {
149
+ if (target.daemonId)
150
+ return target.daemonId;
151
+ // A guest whose daemon has published no id this process can read: it is
152
+ // stopped, or the operator narrowed the CLI surface. Naming another machine
153
+ // would be a guess, and the account list is exactly the wrong one.
154
+ this.failWithCliError({
155
+ message: "This machine's daemon has not published its id, so it cannot be targeted by default " +
156
+ `(it may be stopped). Pass --daemon with its id from \`${this.config.bin} list-daemons\`.`,
157
+ code: 'LOCAL_DAEMON_UNPUBLISHED',
158
+ exit: 1,
159
+ retryable: false,
160
+ });
161
+ }
162
+ // Exactly as this refusal has always reached a user: it was raised inside the
163
+ // listing's try, so `handleApiError` rendered it as `CLI_ERROR`, exit 1 —
164
+ // not the `exit: 2` its `this.error` asked for. Kept, so a laptop sees no change.
165
+ this.failWithCliError({
166
+ message: 'Pass --daemon when zero or multiple daemons are online',
167
+ code: 'CLI_ERROR',
168
+ exit: 1,
169
+ retryable: false,
170
+ });
171
+ }
127
172
  }
128
173
  exports.default = InstructionsInstall;
@@ -9,6 +9,7 @@ const daemon_setup_1 = require("../lib/daemon-setup");
9
9
  const config_1 = require("../lib/config");
10
10
  const keychain_1 = require("../lib/keychain");
11
11
  const login_1 = require("../lib/login");
12
+ const device_code_1 = require("../lib/device-code");
12
13
  const api_fetch_1 = require("../lib/api-fetch");
13
14
  const auth_storage_1 = require("../lib/auth-storage");
14
15
  /**
@@ -220,6 +221,11 @@ class Login extends base_command_1.BaseCommand {
220
221
  }
221
222
  }
222
223
  catch (err) {
224
+ // Ctrl-C while waiting for device-code approval: a person's decision,
225
+ // not a failure, and 130 as a shell reports an interrupt.
226
+ if (err instanceof device_code_1.DeviceCodeLoginCancelledError) {
227
+ this.error(err.message, { exit: err.exitCode, code: err.code });
228
+ }
223
229
  // Everything structured in login.ts throws Error; surface the
224
230
  // message through the standard handler.
225
231
  this.handleApiError(err);
@@ -40,7 +40,7 @@ class DedicatedMachinesAttach extends dedicated_lease_command_1.DedicatedLeaseCo
40
40
  description: 'Agent to attach. Inside an agent session it defaults to that agent (OVERSKY_SESSION_AGENT_ID).',
41
41
  }),
42
42
  engine: core_1.Flags.string({
43
- description: 'Which engine row to attach when the machine registered several (claude, codex, sky-code, oversky). Required only when it is ambiguous.',
43
+ description: 'Which engine row to attach when the machine registered several (claude, codex, sky-code). Required only when it is ambiguous.',
44
44
  }),
45
45
  'rebind-tasks': core_1.Flags.boolean({
46
46
  description: 'Publish the updated agent release and move dormant assigned task pins to it. Running and finished tasks are never changed.',
@@ -8,6 +8,7 @@ import { BaseCommand } from '../../../base-command';
8
8
  * half-written.
9
9
  */
10
10
  export default class DedicatedMachinesCp extends BaseCommand {
11
+ static summary: string;
11
12
  static description: string;
12
13
  static examples: string[];
13
14
  static args: {
@@ -4,6 +4,7 @@ const core_1 = require("@oclif/core");
4
4
  const base_command_1 = require("../../../base-command");
5
5
  const dedicated_machines_1 = require("../../../lib/dedicated-machines");
6
6
  const dedicated_copy_1 = require("../../../lib/dedicated-copy");
7
+ const LIMIT_MIB = dedicated_machines_1.DEDICATED_FILE_MAX_BYTES / (1024 * 1024);
7
8
  /**
8
9
  * `skrr machines dedicated cp` — copy ONE file to or from a Dedicated Runtime.
9
10
  *
@@ -13,9 +14,26 @@ const dedicated_copy_1 = require("../../../lib/dedicated-copy");
13
14
  * half-written.
14
15
  */
15
16
  class DedicatedMachinesCp extends base_command_1.BaseCommand {
16
- static description = 'Copy a file to or from a Dedicated Runtime machine';
17
+ static summary = 'Copy a file to or from a Dedicated Runtime machine';
18
+ // What the copy accepts, in the help read before trying it (OSK-8714).
19
+ static description = [
20
+ `One file of at most ${LIMIT_MIB} MiB, in either direction; exactly one side is <lease-id>:<path>. ` +
21
+ 'Push anything larger through Git or object storage.',
22
+ 'On the machine a path is relative to /workspace (/workspace/... works too) and must be inside ' +
23
+ 'repos/ or user-data/: the rest of the workspace belongs to the runtime. A path that leads ' +
24
+ 'outside the workspace through a symbolic link is refused, and so is a name that holds ' +
25
+ 'credentials (.env, keys, .ssh/, .npmrc, .git/config and the like). A missing directory on the ' +
26
+ 'machine is created.',
27
+ 'As with cp and scp, a destination that ends in / or is an existing directory receives the file ' +
28
+ 'under its source name.',
29
+ 'Neither side is left half-written: the file is staged beside its destination and renamed into ' +
30
+ 'place at the end. A file that changes while it is copied is refused, not spliced. Ctrl-C removes ' +
31
+ 'the partial copy and exits 130 (143 for SIGTERM, 129 when the terminal closes). Progress is ' +
32
+ 'shown on a terminal, but not with --json or under CI.',
33
+ ].join('\n\n');
17
34
  static examples = [
18
35
  '<%= config.bin %> machines dedicated cp ./dist/app.tar.gz rtl_abc123:repos/app/app.tar.gz',
36
+ '<%= config.bin %> machines dedicated cp ./notes.md rtl_abc123:user-data/',
19
37
  '<%= config.bin %> machines dedicated cp rtl_abc123:repos/app/coverage/report.html ./report.html',
20
38
  ];
21
39
  // `ignoreStdin: true` on both, per the CLI's own hygiene rule: without it oclif
@@ -23,12 +41,12 @@ class DedicatedMachinesCp extends base_command_1.BaseCommand {
23
41
  // typed with one argument hangs on a pipe instead of saying what it needs.
24
42
  static args = {
25
43
  source: core_1.Args.string({
26
- description: 'Local path, or <lease-id>:<path> on the machine',
44
+ description: 'Local path, or <lease-id>:<path> on the machine (relative to /workspace, inside repos/ or user-data/)',
27
45
  required: true,
28
46
  ignoreStdin: true,
29
47
  }),
30
48
  destination: core_1.Args.string({
31
- description: 'Local path, or <lease-id>:<path> on the machine',
49
+ description: 'Local path, or <lease-id>:<path> on the machine; a directory (or a path ending in /) receives the file under its own name',
32
50
  required: true,
33
51
  ignoreStdin: true,
34
52
  }),
@@ -48,24 +66,60 @@ class DedicatedMachinesCp extends base_command_1.BaseCommand {
48
66
  this.error(err.message, { exit: 2 });
49
67
  }
50
68
  const scope = { workspaceId: flags.workspace };
69
+ // Progress is drawn on stderr, for a person at a terminal: never into a
70
+ // --json document, and never into a CI log as a stream of carriage returns.
71
+ const progress = !flags.json && process.stderr.isTTY && !(0, dedicated_copy_1.isContinuousIntegration)()
72
+ ? (0, dedicated_copy_1.createCopyProgress)({
73
+ label: plan.direction === 'upload' ? 'Uploading' : 'Downloading',
74
+ write: (text) => process.stderr.write(text),
75
+ })
76
+ : null;
77
+ const onProgress = progress ? progress.update : undefined;
78
+ // Ctrl-C, SIGTERM and a closed terminal cancel the copy, which removes its
79
+ // partial before it rejects. An ordinary listener: the auth lock's handler
80
+ // defers to it, and exits the process itself on a second signal.
81
+ const interrupt = (0, dedicated_copy_1.listenForCopyInterrupt)();
51
82
  let result;
52
83
  try {
84
+ const { signal } = interrupt;
53
85
  result =
54
86
  plan.direction === 'upload'
55
- ? await (0, dedicated_copy_1.uploadToDedicatedRuntime)(plan.localPath, plan.leaseId, plan.remotePath, scope)
56
- : await (0, dedicated_copy_1.downloadFromDedicatedRuntime)(plan.leaseId, plan.remotePath, plan.localPath, scope);
87
+ ? await (0, dedicated_copy_1.uploadToDedicatedRuntime)(plan.localPath, plan.leaseId, plan.remotePath, scope, {
88
+ signal,
89
+ onProgress,
90
+ })
91
+ : await (0, dedicated_copy_1.downloadFromDedicatedRuntime)(plan.leaseId, plan.remotePath, plan.localPath, scope, { signal, onProgress });
57
92
  }
58
- catch (err) {
93
+ catch (thrown) {
94
+ progress?.clear();
95
+ // Whatever the loop threw on its way out, a signal is why it stopped.
96
+ const interruptedBy = interrupt.interruptedBy();
97
+ const err = interruptedBy ? new dedicated_copy_1.DedicatedCopyCancelledError(interruptedBy) : thrown;
98
+ if (err instanceof dedicated_copy_1.DedicatedCopyCancelledError) {
99
+ this.error(plan.direction === 'upload'
100
+ ? `${err.message} ${plan.leaseId}:${plan.remotePath} was not replaced; the partial copy staged beside it is removed now or, if the machine could not be reached, by a later upload into that directory.`
101
+ : `${err.message} Nothing was written to ${plan.localPath}.`, { exit: err.exitCode, code: err.code });
102
+ }
103
+ // Refusals the CLI makes itself carry the code the server gives the same
104
+ // cause, so a script branches on one code whichever side noticed.
105
+ if (err instanceof dedicated_copy_1.DedicatedCopyError) {
106
+ this.error(err.message, { exit: 1, code: err.code });
107
+ }
59
108
  const message = (0, dedicated_machines_1.formatDedicatedRuntimeApiError)(err);
60
109
  if (message)
61
110
  this.error(message, { exit: 1, code: (0, dedicated_machines_1.dedicatedRuntimeApiErrorCode)(err) });
62
- // A local failure (missing file, too large, changed mid-copy) is ours to
63
- // explain; an API failure without a readable body goes to the shared path.
111
+ // Any other local failure (a destination that cannot be written, say) is
112
+ // ours to explain; an API failure without a readable body goes to the
113
+ // shared path.
64
114
  if (err instanceof Error && typeof err.status !== 'number') {
65
- this.error(err.message, { exit: 1 });
115
+ this.error(err.message, { exit: 1, code: 'DEDICATED_RUNTIME_FILE_TRANSFER_FAILED' });
66
116
  }
67
117
  this.handleApiError(err);
68
118
  }
119
+ finally {
120
+ interrupt.dispose();
121
+ }
122
+ progress?.clear();
69
123
  if (flags.json) {
70
124
  this.log(JSON.stringify({ direction: plan.direction, ...result }, null, 2));
71
125
  return;
@@ -12,7 +12,7 @@ export default class DedicatedMachinesCreate extends DedicatedLeaseCommand {
12
12
  'storage-gb': import("@oclif/core/lib/interfaces").OptionFlag<number | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
13
13
  'retention-class': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
14
14
  'backup-profile': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
15
- 'spending-limit-cents': import("@oclif/core/lib/interfaces").OptionFlag<number, import("@oclif/core/lib/interfaces").CustomOptions>;
15
+ 'spending-limit-cents': import("@oclif/core/lib/interfaces").OptionFlag<number | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
16
16
  repository: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
17
17
  workspace: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
18
18
  'request-id': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
@@ -7,6 +7,7 @@ const dedicated_machines_1 = require("../../../lib/dedicated-machines");
7
7
  class DedicatedMachinesCreate extends dedicated_lease_command_1.DedicatedLeaseCommand {
8
8
  static description = 'Request a Dedicated Runtime lease. See `machines dedicated catalog` for sizes, storage bounds and prices.';
9
9
  static examples = [
10
+ '<%= config.bin %> machines dedicated create --name dev-box --size-preset standard',
10
11
  '<%= config.bin %> machines dedicated create --name dev-box --size-preset standard --spending-limit-cents 20000',
11
12
  '<%= config.bin %> machines dedicated create --name dev-box --size-preset standard --spending-limit-cents 20000 --repository acme/app --wait --json',
12
13
  ];
@@ -38,9 +39,13 @@ class DedicatedMachinesCreate extends dedicated_lease_command_1.DedicatedLeaseCo
38
39
  'backup-profile': core_1.Flags.string({
39
40
  description: 'crash-consistent (AWS Backup) or application-consistent (guest fsfreeze while the daemon is reachable; scheduled DLM when that plan is enabled). Defaults to the product catalog value.',
40
41
  }),
42
+ // Not required, and no client-side value either. Every lease has a cap:
43
+ // when create names none, the server applies the product's default cap,
44
+ // the "default monthly cap" `machines dedicated catalog` shows. Marking the
45
+ // flag required contradicted that catalog line (OSK-8691), and restating
46
+ // the default here would be the `--storage-gb` bug above.
41
47
  'spending-limit-cents': core_1.Flags.integer({
42
- description: 'Monthly spending limit in cents. Required; change it later with `machines dedicated spending-limit`.',
43
- required: true,
48
+ description: "Monthly spending cap in cents. Omit it to take the product's default cap (see `machines dedicated catalog`); change it later with `machines dedicated spending-limit`.",
44
49
  }),
45
50
  repository: core_1.Flags.string({
46
51
  description: 'Default GitHub repository (owner/repo) for git credentials on the machine. Requires the GitHub App installed for it; a clone of another repository the App covers still works.',
@@ -1,4 +1,25 @@
1
1
  import { BaseCommand } from '../../../base-command';
2
+ /**
3
+ * The command string to send, or why the invocation is ambiguous.
4
+ *
5
+ * `--command` is one shell string; the words after the lease id are argv,
6
+ * quoted one by one. Either way of giving it used to drop words without a word
7
+ * (OSK-8703): `exec <lease> --command 'echo A' -- echo B` ran `echo A` and exited
8
+ * 0, and words between the lease id and `--` were discarded in favour of the
9
+ * words after it. Both are refused, because only the caller knows which part
10
+ * they meant.
11
+ */
12
+ export declare function resolveDedicatedExecCommand(input: {
13
+ commandFlag: string | undefined;
14
+ /** Everything after the first `--`, or null when none was typed. */
15
+ tail: string[] | null;
16
+ /** Positional words after the lease id, before any `--`. */
17
+ positional: string[];
18
+ }): {
19
+ command: string;
20
+ } | {
21
+ error: string;
22
+ };
2
23
  /**
3
24
  * `skrr machines dedicated exec` — the Run Command half of EC2 parity.
4
25
  *
@@ -13,12 +34,13 @@ import { BaseCommand } from '../../../base-command';
13
34
  * run for its whole timeout whatever sits between the CLI and the API.
14
35
  */
15
36
  export default class DedicatedMachinesExec extends BaseCommand {
37
+ static summary: string;
16
38
  static description: string;
17
- static strict: boolean;
18
39
  static examples: string[];
19
40
  static usage: string;
20
41
  static args: {
21
42
  id: import("@oclif/core/lib/interfaces").Arg<string, Record<string, unknown>>;
43
+ command: import("@oclif/core/lib/interfaces").Arg<string[] | undefined, Record<string, unknown>>;
22
44
  };
23
45
  static flags: {
24
46
  command: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveDedicatedExecCommand = resolveDedicatedExecCommand;
3
4
  const core_1 = require("@oclif/core");
4
5
  const base_command_1 = require("../../../base-command");
5
6
  const shell_quote_1 = require("../../../lib/shell-quote");
@@ -7,6 +8,35 @@ const hosted_machines_1 = require("../../../lib/hosted-machines");
7
8
  const dedicated_machines_1 = require("../../../lib/dedicated-machines");
8
9
  const dedicated_exec_1 = require("../../../lib/dedicated-exec");
9
10
  const exec_1 = require("../hosted/exec");
11
+ /**
12
+ * The command string to send, or why the invocation is ambiguous.
13
+ *
14
+ * `--command` is one shell string; the words after the lease id are argv,
15
+ * quoted one by one. Either way of giving it used to drop words without a word
16
+ * (OSK-8703): `exec <lease> --command 'echo A' -- echo B` ran `echo A` and exited
17
+ * 0, and words between the lease id and `--` were discarded in favour of the
18
+ * words after it. Both are refused, because only the caller knows which part
19
+ * they meant.
20
+ */
21
+ function resolveDedicatedExecCommand(input) {
22
+ const { commandFlag, tail, positional } = input;
23
+ const argv = [...positional, ...(tail ?? [])];
24
+ if (commandFlag !== undefined) {
25
+ if (argv.length > 0) {
26
+ return {
27
+ error: "Give the command once: --command '<shell string>', or the words after --, not both " +
28
+ `(${(0, shell_quote_1.shellQuoteArgv)(argv)} would not have run).`,
29
+ };
30
+ }
31
+ return { command: commandFlag };
32
+ }
33
+ if (tail !== null && tail.length > 0 && positional.length > 0) {
34
+ return {
35
+ error: `Put the whole command after --: ${(0, shell_quote_1.shellQuoteArgv)(positional)} came before it and would not have run.`,
36
+ };
37
+ }
38
+ return { command: (0, shell_quote_1.shellQuoteArgv)(argv) };
39
+ }
10
40
  /**
11
41
  * `skrr machines dedicated exec` — the Run Command half of EC2 parity.
12
42
  *
@@ -21,8 +51,21 @@ const exec_1 = require("../hosted/exec");
21
51
  * run for its whole timeout whatever sits between the CLI and the API.
22
52
  */
23
53
  class DedicatedMachinesExec extends base_command_1.BaseCommand {
24
- static description = 'Run one command on a Dedicated Runtime machine and return its output and exit code';
25
- static strict = false;
54
+ static summary = 'Run one command on a Dedicated Runtime machine and return its output and exit code';
55
+ // What a script depends on, in the help a script author reads (OSK-8703).
56
+ static description = [
57
+ 'Runs the command once, as the workload user, in /workspace or --cwd, through a shell with no TTY. ' +
58
+ 'Give it after -- as words (each one quoted for you), or as one shell string with --command; not both. ' +
59
+ 'The agent shell policy does not apply. A start whose answer is lost is followed up under the same ' +
60
+ 'request id, never run a second time.',
61
+ "Exit status: the command's own exit code, and 124 when --timeout passes (its process group is killed; " +
62
+ 'what it printed before then is still shown). When skrr refuses or cannot reach the machine it exits 1 ' +
63
+ '(2 for a usage mistake) and prints a Code: line on stderr.',
64
+ 'Output: stdout and stderr together. Each keeps its own order, but they are read as two streams, so a ' +
65
+ "line from one can land before a line the other printed first; use --command '{ ...; } 2>&1' when that " +
66
+ 'order matters. The machine returns at most 1 MB and the last 256 KiB of it is kept; when output was ' +
67
+ 'cut, stderr says how much.',
68
+ ].join('\n\n');
26
69
  static examples = [
27
70
  '<%= config.bin %> machines dedicated exec <lease-id> -- git -C repos/app status',
28
71
  '<%= config.bin %> machines dedicated exec <lease-id> --cwd repos/app --timeout 5m -- npm test',
@@ -30,16 +73,26 @@ class DedicatedMachinesExec extends base_command_1.BaseCommand {
30
73
  '<%= config.bin %> machines dedicated exec <lease-id> --request-id deploy-42 -- ./deploy.sh',
31
74
  ];
32
75
  static usage = 'machines dedicated exec <lease-id> [flags] -- <command...>';
76
+ // The words after the lease id are a declared VARIADIC argument, not a
77
+ // `strict = false` command: oclif renders every argument of a non-strict
78
+ // command as variadic, so help read `ID...` — "one or more lease ids" — for a
79
+ // command that takes exactly one (OSK-8703).
33
80
  static args = {
34
81
  id: core_1.Args.string({
35
82
  description: 'Dedicated Runtime lease id',
36
83
  required: true,
37
84
  ignoreStdin: true,
38
85
  }),
86
+ command: core_1.Args.string({
87
+ description: 'The command and its arguments, after --',
88
+ multiple: true,
89
+ required: false,
90
+ ignoreStdin: true,
91
+ }),
39
92
  };
40
93
  static flags = {
41
94
  command: core_1.Flags.string({
42
- description: 'Command to run as one shell string, instead of trailing argv',
95
+ description: 'Command to run as one shell string, instead of the words after --',
43
96
  }),
44
97
  cwd: core_1.Flags.string({
45
98
  description: 'Working directory, relative to /workspace (for example repos/app)',
@@ -58,13 +111,20 @@ class DedicatedMachinesExec extends base_command_1.BaseCommand {
58
111
  // Same argv contract as `machines hosted exec`: everything after the FIRST
59
112
  // `--` is the remote argv, taken from the raw line so a second `--` survives.
60
113
  const { head, tail } = (0, exec_1.splitExecArgv)(this.argv);
61
- const { argv, flags } = await this.parse(DedicatedMachinesExec, head);
62
- const raw = argv;
63
- const id = raw[0];
64
- const command = flags.command || (0, shell_quote_1.shellQuoteArgv)(tail ?? raw.slice(1));
114
+ const { args, flags } = await this.parse(DedicatedMachinesExec, head);
115
+ const id = args.id;
65
116
  if (!id) {
66
117
  this.error('Lease id is required.', { exit: 2 });
67
118
  }
119
+ const resolved = resolveDedicatedExecCommand({
120
+ commandFlag: flags.command,
121
+ tail,
122
+ positional: args.command ?? [],
123
+ });
124
+ if ('error' in resolved) {
125
+ this.error(resolved.error, { exit: 2 });
126
+ }
127
+ const { command } = resolved;
68
128
  if (!command.trim()) {
69
129
  this.error('Command is required. Use --command or pass it after --.', { exit: 2 });
70
130
  }
@@ -22,6 +22,7 @@ class DedicatedMachines extends core_1.Command {
22
22
  '<%= config.bin %> machines dedicated stop <lease-id> --wait',
23
23
  '<%= config.bin %> machines dedicated start <lease-id> --wait',
24
24
  '<%= config.bin %> machines dedicated restart <lease-id>',
25
+ '<%= config.bin %> machines dedicated update-image <lease-id> --wait',
25
26
  '<%= config.bin %> machines dedicated snapshot <lease-id> --wait',
26
27
  '<%= config.bin %> machines dedicated grow <lease-id> --storage-gb 200',
27
28
  '<%= config.bin %> machines dedicated spending-limit <lease-id> --cents 30000',
@@ -44,6 +45,7 @@ class DedicatedMachines extends core_1.Command {
44
45
  ['start', 'start a stopped runtime'],
45
46
  ['stop', 'stop compute and keep the disk'],
46
47
  ['restart', 'restart the instance; files stay'],
48
+ ['update-image', 'move onto the current root image now; files stay'],
47
49
  ['snapshot', 'create a recovery point'],
48
50
  ['grow', 'increase workspace storage; shrink is unsupported'],
49
51
  ['spending-limit', 'change the monthly spending cap'],
@@ -6,6 +6,12 @@ import { DedicatedLeaseCommand } from '../../../lib/dedicated-lease-command';
6
6
  * by default), because whoever deleted it may have been an agent acting with
7
7
  * the owner's authority. This builds a NEW machine from that point, with the
8
8
  * deleted one's files; the deleted lease stays deleted.
9
+ *
10
+ * It also restores a workspace whose unpaid deletion was STOPPED after its
11
+ * machine was already shut down (`deletion: withheld`): the files were kept in
12
+ * the archived lease, and no start path exists for a machine that is gone, so a
13
+ * new machine is built from the same final recovery point. That one has no
14
+ * window.
9
15
  */
10
16
  export default class DedicatedMachinesRestore extends DedicatedLeaseCommand {
11
17
  static description: string;
@@ -11,9 +11,15 @@ const dedicated_machines_1 = require("../../../lib/dedicated-machines");
11
11
  * by default), because whoever deleted it may have been an agent acting with
12
12
  * the owner's authority. This builds a NEW machine from that point, with the
13
13
  * deleted one's files; the deleted lease stays deleted.
14
+ *
15
+ * It also restores a workspace whose unpaid deletion was STOPPED after its
16
+ * machine was already shut down (`deletion: withheld`): the files were kept in
17
+ * the archived lease, and no start path exists for a machine that is gone, so a
18
+ * new machine is built from the same final recovery point. That one has no
19
+ * window.
14
20
  */
15
21
  class DedicatedMachinesRestore extends dedicated_lease_command_1.DedicatedLeaseCommand {
16
- static description = 'Restore a deleted Dedicated Runtime into a new machine, from the recovery point its deletion kept. Only inside the recovery window, and once.';
22
+ static description = 'Restore a deleted Dedicated Runtime into a new machine, from the recovery point its deletion kept: inside the recovery window, or at any time for one whose deletion was stopped after its machine was shut down. Once.';
17
23
  static examples = [
18
24
  '<%= config.bin %> machines dedicated restore <deleted-lease-id>',
19
25
  '<%= config.bin %> machines dedicated restore <deleted-lease-id> --name research-box --wait',