@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
@@ -1,242 +1,163 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
3
36
  const core_1 = require("@oclif/core");
4
37
  const auth_core_1 = require("@skrr-ai/auth-core");
5
- const base_command_1 = require("../../base-command");
6
- const cli_installers_1 = require("../../lib/cli-installers");
7
- const daemon_target_1 = require("../../lib/daemon-target");
8
- const first_party_harness_1 = require("../../lib/first-party-harness");
9
- /**
10
- * `skrr code update` — pull the engine's signed release down through the daemon.
11
- *
12
- * ── Why this command exists ────────────────────────────────────────────────
13
- *
14
- * The update verb has been API-exposed and daemon-implemented since OSK-10096 —
15
- * `POST /api/daemons/:id/cli-installers/:provider/update` re-fetches the signed
16
- * manifest and installs whatever the channel points at. But the only CLI that
17
- * reached it was `skrr harnesses install <provider> --update`: a flag on a
18
- * command a `code` user has no reason to read. On stable that left no
19
- * discoverable path at all — `code install` answered "Already installed." with
20
- * no next step, and the channel moving past the installed build was invisible
21
- * from the surface that owns the engine (OSK-10650).
22
- *
23
- * So this adds a SURFACE and no mechanism. Same route `harnesses install
24
- * --update` posts, same inventory pre-check, same 504 recovery — one update
25
- * path, spelled where the engine's users look.
26
- */
27
- class CodeUpdate extends base_command_1.BaseCommand {
28
- static description = `Update the ${auth_core_1.FIRST_PARTY_HARNESS.displayName} engine on this machine, through the daemon`;
38
+ const first_party_harness_update_1 = require("../../lib/first-party-harness-update");
39
+ const first_party_harness_startup_update_1 = require("../../lib/first-party-harness-startup-update");
40
+ /** The default target is this computer; an explicit daemon keeps the remote API path. */
41
+ class CodeUpdate extends core_1.Command {
42
+ static description = `Check or update the local ${auth_core_1.FIRST_PARTY_HARNESS.displayName} signed release; use --daemon to update another machine`;
29
43
  static examples = [
30
44
  '<%= config.bin %> code update',
45
+ '<%= config.bin %> code update --check',
31
46
  '<%= config.bin %> code update --daemon <daemon-id>',
32
- '<%= config.bin %> code update --json',
47
+ '<%= config.bin %> code update --rollback',
33
48
  ];
34
49
  static flags = {
35
- daemon: core_1.Flags.string({
36
- description: 'Which daemon runs the update. Defaults to this machine on a Dedicated Runtime, otherwise to the only active one; required when you have several.',
37
- }),
50
+ daemon: core_1.Flags.string({ description: 'Run the update on this daemon instead of this computer' }),
38
51
  wait: core_1.Flags.boolean({
39
52
  default: true,
40
53
  allowNo: true,
41
- description: 'Follow the update job until it finishes. --no-wait returns the job id.',
54
+ description: 'Wait for completion. --no-wait returns the local process or remote job.',
42
55
  }),
43
56
  json: core_1.Flags.boolean({ description: 'Emit the result as JSON' }),
57
+ check: core_1.Flags.boolean({
58
+ description: 'Check the signed release channel without downloading or changing the engine',
59
+ }),
60
+ 'target-version': core_1.Flags.string({ description: 'Install this exact signed release' }),
61
+ 'allow-downgrade': core_1.Flags.boolean({
62
+ description: 'Allow an explicitly pinned older signed release',
63
+ }),
64
+ rollback: core_1.Flags.boolean({ description: 'Restore the previous verified release' }),
65
+ 'startup-checks': core_1.Flags.string({
66
+ options: ['on', 'off'],
67
+ description: 'Enable or disable new-session update checks without updating the engine',
68
+ }),
44
69
  };
45
70
  async run() {
46
- const { flags } = await this.parse(CodeUpdate);
47
- const daemonId = this.updateTarget(await this.resolveTarget(flags.daemon));
48
- // Ask the inventory before posting: a daemon older than managed updates has
49
- // no handler for the request and would leave it to time out, and an engine
50
- // that updates some other way has a reason the daemon already words.
51
- let inventory;
52
- try {
53
- inventory = await (0, cli_installers_1.listCliInstallers)(daemonId, this.resolvedCredential);
54
- }
55
- catch (err) {
56
- this.handleApiError(err);
57
- }
58
- const row = (inventory.providers ?? []).find((p) => (0, auth_core_1.isFirstPartyHarnessProvider)(p.provider));
59
- if (!row) {
60
- this.failWithCliError({
61
- message: `${auth_core_1.FIRST_PARTY_HARNESS.displayName} is not installed on ${daemonId}. ` +
62
- `Install it with \`${this.config.bin} code install\`.`,
63
- code: 'NOT_INSTALLED',
64
- exit: 1,
65
- retryable: false,
66
- });
67
- }
68
- if (row.updatable === undefined) {
69
- this.failWithCliError({
70
- message: `The daemon ${daemonId} (${inventory.daemonVersion ?? 'version not reported'}) cannot update ${auth_core_1.FIRST_PARTY_HARNESS.displayName}: it predates managed updates. ` +
71
- `Update the daemon, then run this again.`,
72
- code: 'DAEMON_UPDATE_UNSUPPORTED',
73
- exit: 1,
74
- retryable: false,
75
- });
76
- }
77
- if (!row.updatable) {
78
- this.failWithCliError({
79
- message: row.updateDisabledReason?.trim() ||
80
- `${auth_core_1.FIRST_PARTY_HARNESS.displayName}: the daemon cannot update it here.`,
81
- code: 'CLI_NOT_UPDATABLE',
82
- exit: 1,
83
- retryable: false,
84
- });
71
+ // Preserve all authenticated remote flags (--profile, --workspace, --token)
72
+ // without importing or initializing the auth stack for a local operation.
73
+ if (this.argv.some((arg) => arg === '--daemon' || arg.startsWith('--daemon='))) {
74
+ const { default: RemoteCodeUpdate } = await Promise.resolve().then(() => __importStar(require('../../lib/first-party-harness-remote-update')));
75
+ await this.parse(RemoteCodeUpdate);
76
+ await RemoteCodeUpdate.run(this.argv, this.config);
77
+ return;
85
78
  }
86
- const start = await this.start(daemonId, flags.json === true);
87
- if (!start)
79
+ const { flags } = await this.parse(CodeUpdate);
80
+ if (flags['startup-checks']) {
81
+ if (flags.daemon ||
82
+ flags.check ||
83
+ flags.rollback ||
84
+ flags['target-version'] ||
85
+ flags['allow-downgrade'] ||
86
+ !flags.wait)
87
+ this.error('--startup-checks changes only the local preference; use it without other update controls.');
88
+ const setting = (0, first_party_harness_startup_update_1.configureCodeStartupChecks)(flags['startup-checks'] === 'on');
89
+ if (flags.json)
90
+ this.log(JSON.stringify({
91
+ component: 'code',
92
+ state: 'configured',
93
+ startupChecks: setting.enabled,
94
+ preferencesPath: setting.preferencesPath,
95
+ }, null, 2));
96
+ else
97
+ this.log(`${auth_core_1.FIRST_PARTY_HARNESS.displayName} startup update checks ${setting.enabled ? 'enabled' : 'disabled'}. Change this with \`skrr code update --startup-checks ${setting.enabled ? 'off' : 'on'}\`.`);
88
98
  return;
89
- const jobId = start.job?.id;
90
- if (!start.started || !jobId) {
91
- if (flags.json) {
92
- this.log(JSON.stringify({ daemonId, ...start }, null, 2));
99
+ }
100
+ if (flags.rollback &&
101
+ (flags.check || flags['target-version'] || flags['allow-downgrade'] || !flags.wait))
102
+ this.error('--rollback cannot be combined with --check, --target-version, --allow-downgrade or --no-wait.');
103
+ if (flags['allow-downgrade'] && !flags['target-version'])
104
+ this.error('--allow-downgrade requires --target-version.');
105
+ const options = {
106
+ version: flags['target-version'],
107
+ allowDowngrade: flags['allow-downgrade'],
108
+ force: true,
109
+ };
110
+ try {
111
+ const current = (0, first_party_harness_update_1.getLocalCodeUpdateStatus)();
112
+ if (!flags.check && !flags.rollback && !current.activePath && current.owner === 'managed') {
113
+ const missing = {
114
+ ...current,
115
+ state: 'failed',
116
+ code: 'NOT_INSTALLED',
117
+ message: `${auth_core_1.FIRST_PARTY_HARNESS.displayName} is not installed on this computer. Run \`skrr code install\`.`,
118
+ };
119
+ if (flags.json)
120
+ this.log(JSON.stringify(missing, null, 2));
121
+ else
122
+ this.log((0, first_party_harness_update_1.formatLocalCodeUpdate)(missing));
123
+ process.exitCode = 1;
93
124
  return;
94
125
  }
95
- this.log(start.alreadyRunning
96
- ? `${auth_core_1.FIRST_PARTY_HARNESS.displayName}: an install or update is already running (job ${start.job?.id ?? 'unknown'}).`
97
- : `${auth_core_1.FIRST_PARTY_HARNESS.displayName}: ${start.provider?.updateDisabledReason?.trim() || 'the daemon started no update job.'}`);
98
- return;
99
- }
100
- if (!flags.wait) {
101
- if (flags.json) {
102
- this.log(JSON.stringify({ daemonId, ...start }, null, 2));
126
+ if (!flags.check && !flags.rollback && !flags.wait) {
127
+ const queued = await (0, first_party_harness_update_1.startLocalCodeUpdate)(options);
128
+ if (flags.json)
129
+ this.log(JSON.stringify(queued, null, 2));
130
+ else
131
+ this.log(`${auth_core_1.FIRST_PARTY_HARNESS.displayName} update queued (process ${queued.pid}). Results: ${queued.logPath}; status: skrr code doctor.`);
103
132
  return;
104
133
  }
105
- this.log(`Update started on ${daemonId} — job ${jobId}.`);
106
- return;
107
- }
108
- if (!flags.json) {
109
- this.logToStderr(`Updating ${auth_core_1.FIRST_PARTY_HARNESS.displayName} ${row.version ? `${row.version} ` : ''}on ${daemonId}…`);
110
- }
111
- const job = await this.follow(daemonId, jobId, flags.json === true);
112
- if (!job)
113
- return;
114
- if (flags.json) {
115
- this.log(JSON.stringify({ daemonId, job }, null, 2));
116
- if (job.status !== 'succeeded')
134
+ const result = flags.check
135
+ ? await (0, first_party_harness_update_1.checkLocalCodeUpdate)(options)
136
+ : flags.rollback
137
+ ? (0, first_party_harness_update_1.rollbackLocalCode)()
138
+ : await (0, first_party_harness_update_1.updateLocalCode)({
139
+ ...options,
140
+ onProgress: flags.json ? undefined : (line) => this.logToStderr(line),
141
+ });
142
+ if (flags.json)
143
+ this.log(JSON.stringify(result, null, 2));
144
+ else
145
+ this.log((0, first_party_harness_update_1.formatLocalCodeUpdate)(result));
146
+ if (result.state === 'failed' || result.state === 'check-failed')
117
147
  process.exitCode = 1;
118
- return;
119
- }
120
- if (job.status === 'failed') {
121
- this.failWithCliError({
122
- message: job.error || 'The update failed and the daemon gave no reason.',
123
- code: 'UPDATE_FAILED',
124
- exit: 1,
125
- retryable: false,
126
- });
127
148
  }
128
- this.log((0, cli_installers_1.updateOutcomeLine)(job) ??
129
- `${auth_core_1.FIRST_PARTY_HARNESS.displayName} updated${job.installedVersion ? ` to ${job.installedVersion}` : ''}.`);
130
- }
131
- /** Which daemon the update targets, by the rule every daemon-picking command shares. */
132
- async resolveTarget(explicit) {
133
- try {
134
- return await (0, daemon_target_1.resolveDaemonTarget)({
135
- explicit,
136
- local: (0, daemon_target_1.localMachine)({ baseURL: this.cliConfig?.baseURL }),
137
- listActiveDaemons: () => (0, daemon_target_1.listConnectedDaemons)(this.resolvedCredential),
138
- });
139
- }
140
- catch (err) {
141
- this.handleApiError(err);
142
- }
143
- }
144
- /**
145
- * The daemon id to update through, or the refusal — identical rules to
146
- * `code install`: a Dedicated Runtime's own machine is image-managed, several
147
- * connected daemons get named rather than guessed.
148
- */
149
- updateTarget(target) {
150
- if (target.kind === 'daemon')
151
- return target.daemonId;
152
- if (target.kind === 'this-machine') {
153
- this.failWithCliError({
154
- message: target.explicit
155
- ? first_party_harness_1.DEDICATED_RUNTIME_ENGINE_MESSAGE
156
- : `${first_party_harness_1.DEDICATED_RUNTIME_ENGINE_MESSAGE} This command updates the machine it runs on; ` +
157
- 'to update another of your machines, name it with --daemon.',
158
- code: 'DEDICATED_RUNTIME_ENGINE_SHIPS_WITH_IMAGE',
159
- exit: 1,
160
- retryable: false,
161
- });
162
- }
163
- if (target.kind === 'none') {
164
- this.failWithCliError({
165
- message: 'No daemon is connected for this account, so nothing can perform the update. ' +
166
- `Start the background service on this computer with \`${this.config.bin} daemon start\`, then run this again.`,
167
- code: 'NO_DAEMON',
168
- exit: 1,
169
- retryable: false,
170
- });
171
- }
172
- const { daemons } = target;
173
- const choices = daemons
174
- .slice(0, 10)
175
- .map((d) => ` ${d.id} ${d.name || '—'}`)
176
- .join('\n');
177
- this.failWithCliError({
178
- message: `${daemons.length} connected daemons — say which machine should update the engine:\n${choices}\n` +
179
- `Pass one with --daemon, or see each daemon id in the DAEMON ID column of \`${this.config.bin} harnesses list\`.`,
180
- code: 'AMBIGUOUS_DAEMON',
181
- exit: 1,
182
- retryable: false,
183
- });
184
- }
185
- /**
186
- * Start the update, and survive a gateway that cut the request off — the same
187
- * 504 recovery `harnesses install --update` uses: the relay can drop the
188
- * answer while the daemon runs the job it started, so on a server-side
189
- * failure we ask the daemon and follow the job it reports.
190
- */
191
- async start(daemonId, json) {
192
- try {
193
- return await (0, cli_installers_1.startManagedCliUpdate)(daemonId, PROVIDER, this.resolvedCredential);
194
- }
195
- catch (err) {
196
- const status = err.status ?? 0;
197
- if (status >= 500 || status === 408) {
198
- const job = await (0, cli_installers_1.jobStartedDespiteTimeout)(daemonId, PROVIDER, this.resolvedCredential);
199
- if (job?.id) {
200
- if (!json) {
201
- this.logToStderr(`The request to ${daemonId} did not come back (HTTP ${status}), but the daemon started job ${job.id}. Following it.`);
202
- }
203
- return { started: true, job };
204
- }
149
+ catch (error) {
150
+ if (flags.json) {
151
+ this.log(JSON.stringify({
152
+ component: 'code',
153
+ state: 'failed',
154
+ error: error instanceof Error ? error.message : String(error),
155
+ }));
156
+ process.exitCode = 1;
205
157
  }
206
- this.handleApiError(err);
207
- return undefined;
208
- }
209
- }
210
- /** Follow a job to its end, or report why it could not be followed (and return nothing). */
211
- async follow(daemonId, jobId, json) {
212
- const outcome = await (0, cli_installers_1.followManagedCliInstallJob)({
213
- daemonId,
214
- jobId,
215
- credential: this.resolvedCredential,
216
- onLogLine: json ? undefined : (line) => this.logToStderr(` ${line}`),
217
- });
218
- if (outcome.kind === 'missing') {
219
- this.failWithCliError({
220
- message: `The daemon no longer has update job ${jobId}.`,
221
- code: 'JOB_MISSING',
222
- exit: 1,
223
- retryable: false,
224
- });
225
- return undefined;
226
- }
227
- if (outcome.kind === 'timeout') {
228
- this.failWithCliError({
229
- message: `Still updating after ${Math.round(cli_installers_1.INSTALL_MAX_WAIT_MS / 60000)} minutes — the daemon is ` +
230
- `still working on job ${jobId}. Check \`${this.config.bin} code doctor\` in a while.`,
231
- code: 'UPDATE_PENDING',
232
- exit: 1,
233
- retryable: true,
234
- });
235
- return undefined;
158
+ else
159
+ this.error(error instanceof Error ? error : String(error));
236
160
  }
237
- return outcome.job;
238
161
  }
239
162
  }
240
163
  exports.default = CodeUpdate;
241
- /** Same canonical provider id `code install` posts — read from the identity module. */
242
- const PROVIDER = auth_core_1.FIRST_PARTY_HARNESS.provider;
@@ -84,6 +84,10 @@ class DaemonInstall extends core_1.Command {
84
84
  // is how a CLI and its daemon end up on different deployments
85
85
  // (OSK-279) without anyone doing anything wrong.
86
86
  const code = await (0, exec_runtime_binary_1.execRuntimeBinary)(['install', ...(0, daemon_binding_1.bindToCliSession)(argv)]);
87
+ if (code === 0 &&
88
+ !argv.some((arg) => ['--json', '--help', '-h'].includes(arg))) {
89
+ this.log('Coding-agent instructions: sign in with `skrr login`, or review installation choices with `skrr instructions setup`.');
90
+ }
87
91
  this.exit(code);
88
92
  }
89
93
  catch (err) {
@@ -6,7 +6,9 @@ import { BaseCommand } from '../base-command';
6
6
  * POST /api/auth/recovery/generate. The formatted code is printed
7
7
  * exactly once to STDOUT; a warning goes to STDERR.
8
8
  *
9
- * Requires an interactive user session (NOT a CI token).
9
+ * Requires a user sign-in session (NOT a CI token or PAT). When the server
10
+ * requests a fresh proof, this command verifies it for recovery_code_generate
11
+ * on the same session before retrying the generation exactly once.
10
12
  * On success: any previously-active recovery code is revoked server-side
11
13
  * and the new one is returned.
12
14
  *
@@ -19,6 +21,16 @@ export default class GenerateRecoveryCode extends BaseCommand {
19
21
  static examples: string[];
20
22
  static flags: {
21
23
  json: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
24
+ otp: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
25
+ 'backup-code': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
26
+ 'email-code': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
22
27
  };
23
28
  run(): Promise<void>;
29
+ private generate;
30
+ private proofFor;
31
+ private nonemptyCode;
32
+ private refuseUnavailableMethod;
33
+ private refusePasskeyOnly;
34
+ private refuseStepUp;
35
+ private handleGenerationError;
24
36
  }
@@ -1,8 +1,16 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const core_1 = require("@oclif/core");
4
+ const data_provider_1 = require("@skrr-ai/data-provider");
4
5
  const base_command_1 = require("../base-command");
5
6
  const api_fetch_1 = require("../lib/api-fetch");
7
+ const prompt_1 = require("../lib/prompt");
8
+ // Include passkey even though this terminal cannot run a WebAuthn ceremony.
9
+ // The server uses this header when choosing the account's strongest proof:
10
+ // omitting passkey would make a passkey-only account fall back to email.
11
+ // A passkey-only challenge is refused below with a first-party web handoff.
12
+ const STEP_UP_HEADERS = { 'X-Step-Up-Methods': 'passkey,totp,backup_code,email_code' };
13
+ const PURPOSE = data_provider_1.RECOVERY_CODE_GENERATE_STEP_UP_PURPOSE;
6
14
  /**
7
15
  * `skrr generate-recovery-code`
8
16
  *
@@ -10,7 +18,9 @@ const api_fetch_1 = require("../lib/api-fetch");
10
18
  * POST /api/auth/recovery/generate. The formatted code is printed
11
19
  * exactly once to STDOUT; a warning goes to STDERR.
12
20
  *
13
- * Requires an interactive user session (NOT a CI token).
21
+ * Requires a user sign-in session (NOT a CI token or PAT). When the server
22
+ * requests a fresh proof, this command verifies it for recovery_code_generate
23
+ * on the same session before retrying the generation exactly once.
14
24
  * On success: any previously-active recovery code is revoked server-side
15
25
  * and the new one is returned.
16
26
  *
@@ -22,6 +32,8 @@ class GenerateRecoveryCode extends base_command_1.BaseCommand {
22
32
  static description = 'Generate a printable account-recovery code. Shown once — store it safely. Recovery codes use letters A-Z and digits 2-7.';
23
33
  static examples = [
24
34
  '<%= config.bin %> generate-recovery-code',
35
+ '<%= config.bin %> generate-recovery-code --otp 123456',
36
+ '<%= config.bin %> generate-recovery-code --backup-code <unused-code>',
25
37
  '<%= config.bin %> generate-recovery-code --json',
26
38
  ];
27
39
  static flags = {
@@ -29,28 +41,60 @@ class GenerateRecoveryCode extends base_command_1.BaseCommand {
29
41
  description: 'Output as JSON (for scripting). Code still printed only once.',
30
42
  default: false,
31
43
  }),
44
+ otp: core_1.Flags.string({
45
+ description: 'Current code from your authenticator app when a fresh proof is required',
46
+ exclusive: ['backup-code', 'email-code'],
47
+ }),
48
+ 'backup-code': core_1.Flags.string({
49
+ description: 'Unused two-factor backup code when a fresh proof is required (it is spent)',
50
+ exclusive: ['otp', 'email-code'],
51
+ }),
52
+ 'email-code': core_1.Flags.string({
53
+ description: 'Previously requested one-time email code when a fresh proof is required',
54
+ exclusive: ['otp', 'backup-code'],
55
+ }),
32
56
  };
33
57
  async run() {
34
58
  this.requireAuth();
35
59
  const { flags } = await this.parse(GenerateRecoveryCode);
36
- // Guard: CI tokens cannot generate recovery codes (server also enforces this).
37
- if (this.resolvedCredential.kind === 'ci') {
38
- this.error('CI tokens cannot generate recovery codes. Run this command from an interactive session (`skrr login`).', { exit: 2 });
60
+ // Static credentials cannot mint account recovery credentials.
61
+ if (this.resolvedCredential.kind === 'ci' || this.resolvedCredential.kind === 'pat') {
62
+ this.error('CI and personal access tokens cannot generate recovery codes. Sign in with `skrr login` first.', { exit: 2 });
39
63
  }
40
64
  let result;
41
65
  try {
42
- result = await (0, api_fetch_1.apiFetch)('/api/auth/recovery/generate', {
43
- method: 'POST',
44
- credential: this.resolvedCredential,
45
- });
66
+ result = await this.generate();
46
67
  }
47
68
  catch (err) {
48
- if (err instanceof api_fetch_1.ApiFetchError) {
49
- if (err.status === 401 || err.status === 403) {
50
- this.error('Not authorized to generate a recovery code. Ensure you are signed in with a user-scope session.', { exit: 1 });
51
- }
69
+ const challenge = err instanceof api_fetch_1.ApiFetchError && err.status === 403 ? (0, data_provider_1.stepUpChallengeFromError)(err) : null;
70
+ if (!challenge)
71
+ this.handleGenerationError(err);
72
+ if (challenge.purpose !== PURPOSE) {
73
+ this.error('The server asked for verification of a different action. Nothing changed.', {
74
+ code: 'STEP_UP_PURPOSE_MISMATCH',
75
+ exit: 2,
76
+ });
77
+ }
78
+ if (challenge.reason !== 'no_recent_step_up')
79
+ this.refuseStepUp(challenge);
80
+ const proof = await this.proofFor(challenge, flags);
81
+ try {
82
+ await (0, api_fetch_1.apiFetch)('/api/auth/step-up', {
83
+ method: 'POST',
84
+ credential: this.resolvedCredential,
85
+ headers: STEP_UP_HEADERS,
86
+ body: { purpose: PURPOSE, ...proof },
87
+ });
88
+ }
89
+ catch (verifyError) {
90
+ this.handleApiError(verifyError);
91
+ }
92
+ try {
93
+ result = await this.generate();
94
+ }
95
+ catch (retryError) {
96
+ this.handleGenerationError(retryError);
52
97
  }
53
- this.handleApiError(err);
54
98
  }
55
99
  if (flags.json) {
56
100
  // JSON mode: print structured output to stdout for scripting.
@@ -75,5 +119,94 @@ class GenerateRecoveryCode extends base_command_1.BaseCommand {
75
119
  process.stderr.write(` Generated at: ${new Date(result.createdAt).toISOString()}\n`);
76
120
  process.stderr.write('\n');
77
121
  }
122
+ generate() {
123
+ return (0, api_fetch_1.apiFetch)('/api/auth/recovery/generate', {
124
+ method: 'POST',
125
+ credential: this.resolvedCredential,
126
+ headers: STEP_UP_HEADERS,
127
+ });
128
+ }
129
+ async proofFor(challenge, flags) {
130
+ if (challenge.methods.includes('passkey') &&
131
+ !challenge.methods.includes('totp') &&
132
+ !challenge.methods.includes('backup_code')) {
133
+ this.refusePasskeyOnly();
134
+ }
135
+ if (flags.otp !== undefined) {
136
+ if (!challenge.methods.includes('totp'))
137
+ this.refuseUnavailableMethod();
138
+ return { token: this.nonemptyCode(flags.otp) };
139
+ }
140
+ if (flags['backup-code'] !== undefined) {
141
+ if (!challenge.methods.includes('backup_code'))
142
+ this.refuseUnavailableMethod();
143
+ return { backupCode: this.nonemptyCode(flags['backup-code']) };
144
+ }
145
+ if (flags['email-code'] !== undefined) {
146
+ if (!challenge.methods.includes('email_code'))
147
+ this.refuseUnavailableMethod();
148
+ return { emailCode: this.nonemptyCode(flags['email-code']) };
149
+ }
150
+ if (!process.stdin.isTTY) {
151
+ this.error('A fresh identity check is required. Run this command at a terminal, or pass --otp, --backup-code, or a previously requested --email-code. Nothing changed.', { code: 'STEP_UP_PROOF_REQUIRED', exit: 2 });
152
+ }
153
+ if (challenge.methods.includes('totp')) {
154
+ const entered = await (0, prompt_1.prompt)('Authenticator code: ', {
155
+ hidden: true,
156
+ output: process.stderr,
157
+ });
158
+ return { token: this.nonemptyCode(entered) };
159
+ }
160
+ if (challenge.methods.includes('backup_code')) {
161
+ const entered = await (0, prompt_1.prompt)('Unused two-factor backup code: ', {
162
+ hidden: true,
163
+ output: process.stderr,
164
+ });
165
+ return { backupCode: this.nonemptyCode(entered) };
166
+ }
167
+ if (challenge.methods.includes('email_code')) {
168
+ try {
169
+ await (0, api_fetch_1.apiFetch)('/api/auth/step-up/email-code', {
170
+ method: 'POST',
171
+ credential: this.resolvedCredential,
172
+ headers: STEP_UP_HEADERS,
173
+ body: { purpose: PURPOSE },
174
+ });
175
+ }
176
+ catch (requestError) {
177
+ this.handleApiError(requestError);
178
+ }
179
+ process.stderr.write('A one-time code was sent to your account email.\n');
180
+ const entered = await (0, prompt_1.prompt)('Email code: ', { hidden: true, output: process.stderr });
181
+ return { emailCode: this.nonemptyCode(entered) };
182
+ }
183
+ this.refuseUnavailableMethod();
184
+ }
185
+ nonemptyCode(value) {
186
+ const code = value.trim();
187
+ if (!code)
188
+ this.error('A verification code cannot be empty. Nothing changed.', { exit: 2 });
189
+ return code;
190
+ }
191
+ refuseUnavailableMethod() {
192
+ this.error('This login has no identity-check method the CLI can complete. Use Settings → Account in the web app, or enroll an authenticator and try again. Nothing changed.', { code: 'STEP_UP_METHOD_UNAVAILABLE', exit: 2 });
193
+ }
194
+ refusePasskeyOnly() {
195
+ this.error('This account requires a passkey for this identity check. Open Settings → Account in the web app to generate a recovery code. Nothing changed.', { code: 'STEP_UP_PASSKEY_REQUIRED', exit: 2 });
196
+ }
197
+ refuseStepUp(challenge) {
198
+ if (challenge.reason === 'credential_not_session_bound') {
199
+ this.error('This credential has no sign-in session for an identity check. Run `skrr login` and try again. Nothing changed.', { code: 'STEP_UP_SESSION_UNBOUND', exit: 2 });
200
+ }
201
+ this.refuseUnavailableMethod();
202
+ }
203
+ handleGenerationError(err) {
204
+ if (err instanceof api_fetch_1.ApiFetchError && (err.status === 401 || err.status === 403)) {
205
+ this.error('Not authorized to generate a recovery code. Sign in with a user-scope session.', {
206
+ exit: 1,
207
+ });
208
+ }
209
+ this.handleApiError(err);
210
+ }
78
211
  }
79
212
  exports.default = GenerateRecoveryCode;
@@ -0,0 +1,20 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class InstructionsGuide extends Command {
3
+ static baseFlags: {
4
+ bare: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
5
+ token: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
6
+ workspace: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
7
+ profile: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
8
+ raw: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
9
+ };
10
+ static description: string;
11
+ static args: {
12
+ name: import("@oclif/core/lib/interfaces").Arg<string, Record<string, unknown>>;
13
+ };
14
+ static flags: {
15
+ json: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
16
+ offline: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
17
+ };
18
+ static examples: string[];
19
+ run(): Promise<void>;
20
+ }