@skrr-ai/cli 0.1.8 → 0.1.10

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 (170) hide show
  1. package/dist/base-command.d.ts +27 -0
  2. package/dist/base-command.js +83 -11
  3. package/dist/commands/agent/message/send.js +1 -1
  4. package/dist/commands/agents/chat.d.ts +1 -1
  5. package/dist/commands/agents/chat.js +1 -1
  6. package/dist/commands/browser/install.d.ts +1 -1
  7. package/dist/commands/browser/install.js +10 -2
  8. package/dist/commands/browser/mcp-config.js +3 -1
  9. package/dist/commands/browser/skill/show.js +1 -1
  10. package/dist/commands/browser/status.js +4 -1
  11. package/dist/commands/browser/uninstall.d.ts +1 -1
  12. package/dist/commands/browser/uninstall.js +10 -2
  13. package/dist/commands/browser.d.ts +1 -1
  14. package/dist/commands/browser.js +3 -3
  15. package/dist/commands/code/index.js +6 -3
  16. package/dist/commands/code/install.d.ts +61 -0
  17. package/dist/commands/code/install.js +247 -0
  18. package/dist/commands/code/oversky-agent.d.ts +2 -1
  19. package/dist/commands/code/oversky-agent.js +12 -8
  20. package/dist/commands/commands.js +1 -1
  21. package/dist/commands/context.js +1 -1
  22. package/dist/commands/create-token.js +1 -1
  23. package/dist/commands/daemon/byok.d.ts +1 -0
  24. package/dist/commands/daemon/byok.js +14 -1
  25. package/dist/commands/daemon/index.js +18 -7
  26. package/dist/commands/daemon/install.d.ts +1 -0
  27. package/dist/commands/daemon/install.js +22 -3
  28. package/dist/commands/daemon/logs.d.ts +1 -0
  29. package/dist/commands/daemon/logs.js +4 -0
  30. package/dist/commands/daemon/start.d.ts +1 -0
  31. package/dist/commands/daemon/start.js +15 -2
  32. package/dist/commands/daemon/status.d.ts +1 -0
  33. package/dist/commands/daemon/status.js +14 -1
  34. package/dist/commands/daemon/stop.d.ts +1 -0
  35. package/dist/commands/daemon/stop.js +14 -1
  36. package/dist/commands/daemon/uninstall.d.ts +1 -0
  37. package/dist/commands/daemon/uninstall.js +15 -2
  38. package/dist/commands/daemon/usage.d.ts +1 -0
  39. package/dist/commands/daemon/usage.js +5 -1
  40. package/dist/commands/doctor.js +2 -2
  41. package/dist/commands/github/allow-tools.d.ts +1 -0
  42. package/dist/commands/github/app/install.d.ts +1 -0
  43. package/dist/commands/github/app/status.d.ts +1 -0
  44. package/dist/commands/github/connect.d.ts +1 -0
  45. package/dist/commands/github/connections.d.ts +1 -0
  46. package/dist/commands/github/disconnect.d.ts +1 -0
  47. package/dist/commands/github/finalize.d.ts +1 -0
  48. package/dist/commands/github/reconnect.d.ts +1 -0
  49. package/dist/commands/github/run.d.ts +1 -0
  50. package/dist/commands/github/tools.d.ts +1 -0
  51. package/dist/commands/goals/create.js +2 -2
  52. package/dist/commands/goals/show.js +5 -0
  53. package/dist/commands/harnesses/list.js +14 -3
  54. package/dist/commands/harnesses/show.js +14 -0
  55. package/dist/commands/instructions/activate.js +1 -1
  56. package/dist/commands/instructions/archive-version.js +1 -1
  57. package/dist/commands/instructions/delete.js +1 -1
  58. package/dist/commands/instructions/diff.js +1 -1
  59. package/dist/commands/instructions/install.js +7 -1
  60. package/dist/commands/instructions/restore.js +1 -1
  61. package/dist/commands/instructions/show.js +17 -1
  62. package/dist/commands/instructions/update.js +1 -1
  63. package/dist/commands/integrations/connections/connect.d.ts +1 -0
  64. package/dist/commands/integrations/connections/disconnect.d.ts +1 -0
  65. package/dist/commands/integrations/connections/finalize.d.ts +1 -0
  66. package/dist/commands/integrations/connections/list.d.ts +1 -0
  67. package/dist/commands/integrations/connections/reconnect.d.ts +1 -0
  68. package/dist/commands/integrations/execute.d.ts +1 -0
  69. package/dist/commands/integrations/search.d.ts +1 -0
  70. package/dist/commands/integrations/toolkits.d.ts +1 -0
  71. package/dist/commands/integrations/tools/allow.d.ts +1 -0
  72. package/dist/commands/integrations/tools/list.d.ts +1 -0
  73. package/dist/commands/list-daemons.d.ts +2 -7
  74. package/dist/commands/list-daemons.js +13 -13
  75. package/dist/commands/login.js +78 -33
  76. package/dist/commands/logout.js +52 -1
  77. package/dist/commands/machines/hosted/start.js +1 -1
  78. package/dist/commands/pair.js +2 -2
  79. package/dist/commands/projects/create.js +6 -1
  80. package/dist/commands/projects/tasks.js +6 -1
  81. package/dist/commands/revoke-daemon.js +6 -2
  82. package/dist/commands/set-capabilities.js +6 -2
  83. package/dist/commands/skills/install.js +1 -1
  84. package/dist/commands/skills/library.js +2 -2
  85. package/dist/commands/spaces/create.js +6 -2
  86. package/dist/commands/tasks/complete.d.ts +1 -1
  87. package/dist/commands/tasks/complete.js +21 -8
  88. package/dist/commands/tasks/create.js +4 -1
  89. package/dist/commands/tasks/current.js +1 -1
  90. package/dist/commands/tasks/delete.d.ts +1 -0
  91. package/dist/commands/tasks/delete.js +9 -0
  92. package/dist/commands/tasks/export.js +5 -1
  93. package/dist/commands/tasks/follow-up.js +2 -1
  94. package/dist/commands/tasks/list.js +5 -1
  95. package/dist/commands/tasks/pr.js +1 -1
  96. package/dist/commands/tasks/show.js +10 -1
  97. package/dist/commands/tasks/triage.js +2 -1
  98. package/dist/commands/tasks/update.js +4 -1
  99. package/dist/commands/tasks/upsert.js +4 -1
  100. package/dist/commands/usage.js +1 -1
  101. package/dist/commands/whoami.js +4 -1
  102. package/dist/lib/agentic-stream.js +21 -1
  103. package/dist/lib/api-fetch.d.ts +1 -1
  104. package/dist/lib/api-fetch.js +1 -1
  105. package/dist/lib/auth-core-init.js +23 -3
  106. package/dist/lib/auth-storage.d.ts +0 -12
  107. package/dist/lib/auth-storage.js +37 -2
  108. package/dist/lib/config.d.ts +43 -3
  109. package/dist/lib/config.js +391 -31
  110. package/dist/lib/created-line.d.ts +22 -0
  111. package/dist/lib/created-line.js +25 -0
  112. package/dist/lib/daemon-binding.d.ts +29 -0
  113. package/dist/lib/daemon-binding.js +63 -0
  114. package/dist/lib/daemon-ref.d.ts +31 -0
  115. package/dist/lib/daemon-ref.js +47 -0
  116. package/dist/lib/daemonBroker.js +1 -1
  117. package/dist/lib/delegated-cli.js +3 -3
  118. package/dist/lib/exec-oversky.d.ts +18 -1
  119. package/dist/lib/exec-oversky.js +21 -4
  120. package/dist/lib/html-text.d.ts +15 -0
  121. package/dist/lib/html-text.js +40 -0
  122. package/dist/lib/keychain.d.ts +43 -0
  123. package/dist/lib/keychain.js +145 -4
  124. package/dist/lib/login.js +3 -3
  125. package/dist/lib/node-adapter.js +1 -1
  126. package/dist/lib/oauthLogin.js +17 -6
  127. package/dist/lib/sky-code-agent.d.ts +1 -1
  128. package/dist/lib/sky-code-agent.js +8 -8
  129. package/dist/lib/sky-code-broker.js +18 -2
  130. package/dist/lib/sky-code-doctor.js +120 -4
  131. package/dist/lib/sky-code-managed.d.ts +12 -0
  132. package/dist/lib/sky-code-managed.js +21 -7
  133. package/dist/lib/sky-code.d.ts +16 -1
  134. package/dist/lib/sky-code.js +110 -7
  135. package/dist/lib/task-execution.js +4 -11
  136. package/dist/lib/tasks.d.ts +1 -0
  137. package/dist/lib/tasks.js +9 -6
  138. package/dist/lib/update-check.js +1 -1
  139. package/dist/lib/usage-discovery.js +1 -1
  140. package/dist/lib/web-url.js +12 -1
  141. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialResolver.d.ts +1 -1
  142. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialResolver.js +2 -2
  143. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +1 -0
  144. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +13 -1
  145. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/linux.js +16 -3
  146. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/legacyStatePreflight.d.ts +0 -9
  147. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/legacyStatePreflight.js +18 -1
  148. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/loginLocalhost.js +3 -3
  149. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/messages.js +3 -3
  150. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/runtime.d.ts +7 -3
  151. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/skyCodeChannels.d.ts +96 -0
  152. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/skyCodeChannels.js +115 -0
  153. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialResolver.d.ts +1 -1
  154. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialResolver.js +2 -2
  155. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +1 -0
  156. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +1 -0
  157. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/linux.js +16 -3
  158. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/legacyStatePreflight.d.ts +0 -9
  159. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/legacyStatePreflight.js +18 -1
  160. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/loginLocalhost.js +3 -3
  161. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/messages.js +3 -3
  162. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/runtime.d.ts +7 -3
  163. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/skyCodeChannels.d.ts +96 -0
  164. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/skyCodeChannels.js +107 -0
  165. package/dist/node_modules/@skrr-ai/auth-core/package.json +1 -1
  166. package/dist/node_modules/@skrr-ai/data-provider/index.js +4206 -4191
  167. package/dist/node_modules/@skrr-ai/inference-broker/dist/cjs/managed-inference-broker.js +2 -2
  168. package/dist/node_modules/@skrr-ai/inference-broker/dist/esm/managed-inference-broker.js +2 -2
  169. package/oclif.manifest.json +7969 -1397
  170. package/package.json +22 -19
@@ -35,8 +35,10 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.ENVIRONMENT_BASE_URLS = exports.LEGACY_PROD_BASE_URL = exports.LEGACY_DEV_BASE_URL = exports.CANONICAL_PROD_BASE_URL = exports.CANONICAL_DEV_BASE_URL = void 0;
37
37
  exports.environmentForBaseURL = environmentForBaseURL;
38
+ exports.describeDeployment = describeDeployment;
38
39
  exports.canonicalizeKnownBaseURL = canonicalizeKnownBaseURL;
39
40
  exports.isEnvAuthOverride = isEnvAuthOverride;
41
+ exports.configRoot = configRoot;
40
42
  exports.loadConfig = loadConfig;
41
43
  exports.saveConfig = saveConfig;
42
44
  exports.getConfigPath = getConfigPath;
@@ -48,6 +50,7 @@ exports.saveRawConfig = saveRawConfig;
48
50
  const fs = __importStar(require("node:fs"));
49
51
  const os = __importStar(require("node:os"));
50
52
  const path = __importStar(require("node:path"));
53
+ const keychain_1 = require("./keychain");
51
54
  const publicEndpoints_generated_1 = require("./publicEndpoints.generated");
52
55
  // Both from config/public-endpoints.json, the one domain contract, rather than a
53
56
  // fourth hand-copied pair. The CLI previously knew only about dev — there was no
@@ -96,6 +99,22 @@ function environmentForBaseURL(value) {
96
99
  * display and not harmless for routing, because two spellings of prod are two
97
100
  * different keys to anything comparing base URLs.
98
101
  */
102
+ /**
103
+ * A deployment, written so a human can tell which one it is.
104
+ *
105
+ * A published `npm i -g @skrr-ai/cli` defaults to the DEV deployment, and nothing
106
+ * in the login flow said so — you got "Signed in via browser" and no indication
107
+ * of which backend now holds your account (OSK-301). Naming the environment as
108
+ * well as the URL matters because `oversky.dev` and `skrr.ai` do not look like a
109
+ * dev/prod pair to anyone who has not read this file.
110
+ *
111
+ * Falls back to the bare URL for a deployment we do not recognise — a
112
+ * self-hosted or local server is legitimate, and labelling it would be a guess.
113
+ */
114
+ function describeDeployment(baseURL) {
115
+ const env = environmentForBaseURL(baseURL);
116
+ return env ? `${env} (${baseURL})` : baseURL;
117
+ }
99
118
  function canonicalizeKnownBaseURL(value) {
100
119
  const trimmed = value.trim().replace(/\/+$/, '');
101
120
  const normalized = trimmed.toLowerCase().replace(/^http:\/\//, 'https://');
@@ -112,10 +131,27 @@ function canonicalizeKnownBaseURL(value) {
112
131
  return value;
113
132
  }
114
133
  const DEFAULT_CONFIG = {
115
- // Dev remains the default for an unconfigured CLI, matching the daemon
116
- // (daemon/src/config.ts → ENVIRONMENT_URLS.dev). Reach prod with
117
- // `skrr login --env prod`, `--base-url https://oversky.ai`, or OVERSKY_BASE_URL.
118
- baseURL: exports.CANONICAL_DEV_BASE_URL,
134
+ // PROD for an unconfigured CLI, matching the daemon
135
+ // (daemon/src/config.ts → ENVIRONMENT_URLS.prod). Reach dev with
136
+ // `skrr login --env dev`, `--base-url https://oversky.dev`, or OVERSKY_BASE_URL.
137
+ //
138
+ // This was dev, which was right when the CLI was something the team installed
139
+ // from the repo. `@skrr-ai/cli` is a PUBLIC npm package now, and the default
140
+ // decides where a stranger's first `skrr login` creates their account,
141
+ // workspace and agents. `oversky.dev` is the integration environment: it
142
+ // deploys on every push to `dev` and, by design, without waiting for the api or
143
+ // client suites. A reasonable place for the team to live, a poor place to send
144
+ // someone who typed `npm install` (OSK-301).
145
+ //
146
+ // It moves with the daemon or not at all. The two defaults are ONE decision:
147
+ // changing this alone would put every fresh machine into the CLI-on-prod /
148
+ // daemon-on-dev split that OSK-279 exists to prevent — which is exactly the
149
+ // state this machine was found in. `cli/src/__tests__/default-deployment.spec.ts`
150
+ // reads the daemon's own source and fails if they diverge.
151
+ //
152
+ // Nobody's existing setup moves: a stored `cli-config.json` wins over this, so
153
+ // only an install with no configuration at all is affected.
154
+ baseURL: exports.CANONICAL_PROD_BASE_URL,
119
155
  };
120
156
  /**
121
157
  * Environment-variable overlay for headless use (CI, Secrets Manager, EC2
@@ -138,18 +174,46 @@ const ENV_VAR = {
138
174
  refreshToken: 'OVERSKY_REFRESH_TOKEN',
139
175
  cliId: 'OVERSKY_CLI_ID',
140
176
  };
177
+ /**
178
+ * Read one overlay variable under either prefix.
179
+ *
180
+ * The ~921 `OVERSKY_*` variables are RETAINED by decision
181
+ * (docs/product/skrr-env-inventory.md), so this migrates nothing and changes no
182
+ * existing environment: the legacy spelling still wins when both are set. What
183
+ * it stops is the renamed product SILENTLY IGNORING the prefix its own
184
+ * contributor docs tell new code to use. `SKRR_BASE_URL=https://oversky.dev`
185
+ * previously did nothing at all — the command ran happily against production
186
+ * and reported success, which is the worst available outcome for a variable
187
+ * whose entire job is choosing the deployment.
188
+ *
189
+ * A disagreement is reported rather than resolved quietly, because two
190
+ * different values for one setting is a mistake either way.
191
+ */
192
+ let warnedEnvPrefixConflict = false;
193
+ function envOverlayValue(key) {
194
+ const legacyName = ENV_VAR[key];
195
+ const renamedName = legacyName.replace(/^OVERSKY_/, 'SKRR_');
196
+ const legacy = process.env[legacyName]?.trim();
197
+ const renamed = process.env[renamedName]?.trim();
198
+ if (legacy && renamed && legacy !== renamed && !warnedEnvPrefixConflict) {
199
+ warnedEnvPrefixConflict = true;
200
+ process.stderr.write(`warning: ${legacyName} and ${renamedName} are both set and disagree. ` +
201
+ `Using ${legacyName}=${legacy}.\n`);
202
+ }
203
+ return legacy && legacy.length > 0 ? legacy : renamed && renamed.length > 0 ? renamed : undefined;
204
+ }
141
205
  function overlayEnv(cfg) {
142
206
  const out = { ...cfg };
143
- const bu = process.env[ENV_VAR.baseURL] || process.env[ENV_VAR.serverUrl];
207
+ const bu = envOverlayValue('baseURL') || envOverlayValue('serverUrl');
144
208
  if (bu && bu.length > 0)
145
209
  out.baseURL = canonicalizeKnownBaseURL(bu);
146
- const tok = process.env[ENV_VAR.token];
210
+ const tok = envOverlayValue('token');
147
211
  if (tok && tok.length > 0)
148
212
  out.token = tok;
149
- const rt = process.env[ENV_VAR.refreshToken];
213
+ const rt = envOverlayValue('refreshToken');
150
214
  if (rt && rt.length > 0)
151
215
  out.refreshToken = rt;
152
- const cid = process.env[ENV_VAR.cliId];
216
+ const cid = envOverlayValue('cliId');
153
217
  if (cid && cid.length > 0)
154
218
  out.cliId = cid;
155
219
  return out;
@@ -161,8 +225,9 @@ function overlayEnv(cfg) {
161
225
  * us persist a rotated pair that the next `loadConfig()` would shadow.
162
226
  */
163
227
  function isEnvAuthOverride() {
164
- return !!((process.env[ENV_VAR.token] && process.env[ENV_VAR.token].length > 0) ||
165
- (process.env[ENV_VAR.refreshToken] && process.env[ENV_VAR.refreshToken].length > 0));
228
+ // Both prefixes, or a credential injected as SKRR_TOKEN would be used for the
229
+ // request while the process still believed it could refresh and persist.
230
+ return !!(envOverlayValue('token') || envOverlayValue('refreshToken'));
166
231
  }
167
232
  /**
168
233
  * Storage layout (Phase H+).
@@ -175,12 +240,127 @@ function isEnvAuthOverride() {
175
240
  * The CLI shares the daemon's config root so `auth.lock` and
176
241
  * `needs-reauth.json` are coordinated across binaries.
177
242
  */
243
+ /**
244
+ * The skrr root for this process — the ONE place every root-derived path comes
245
+ * from.
246
+ *
247
+ * It is exported and takes `env` because the alternative is what this replaced:
248
+ * two functions each deriving the root themselves, one honouring the override
249
+ * and one not, disagreeing only when someone relocated their root (OSK-300).
250
+ * Fixing that by teaching the second function the same two variable names would
251
+ * have re-created the split the moment a third name appeared — and did, briefly,
252
+ * when `SKRR_CONFIG_DIR` was added to one side only.
253
+ *
254
+ * `SKRR_CONFIG_DIR` is the spelling for anything new (root CLAUDE.md) and wins
255
+ * when both are set. `OVERSKY_CONFIG_DIR` keeps working: the daemon, the engine
256
+ * resolver and every existing install read it.
257
+ */
258
+ function configRoot(env = process.env) {
259
+ const root = (env.SKRR_CONFIG_DIR || env.OVERSKY_CONFIG_DIR)?.trim();
260
+ return root || path.join(os.homedir(), '.skrr');
261
+ }
178
262
  function newConfigDir() {
179
- return path.join(os.homedir(), '.skrr');
263
+ return configRoot();
264
+ }
265
+ /**
266
+ * Fields that belong to the MACHINE, not to a profile.
267
+ *
268
+ * `cliId` is the sharp one. Its own doc says it is presented at login and at
269
+ * every refresh rotation, and that the server binds it into the JWT `did` claim
270
+ * and rejects a rotation against a different one. Split it per profile and a
271
+ * second profile does not degrade — it fails to stay logged in.
272
+ *
273
+ * `credentials.store` is a machine posture (keychain / file / env-only), and
274
+ * `migrated` is a breadcrumb about this machine's DISK LAYOUT. Neither is a
275
+ * property of which deployment you are addressing.
276
+ *
277
+ * Everything else follows the profile, because everything else IS the
278
+ * deployment: `baseURL`, who you are there (`userId`, `email`), and what you
279
+ * chose there (`workspaceId`, `skyCodeAgentId`).
280
+ */
281
+ const MACHINE_SCOPED_KEYS = ['cliId', 'credentials', 'migrated'];
282
+ /**
283
+ * Where this profile's config lives.
284
+ *
285
+ * The DEFAULT profile keeps `~/.skrr/cli-config.json` — the exact path it has
286
+ * always used. That is the whole reason this change needs no migration and
287
+ * carries almost no risk: profiles are ADDITIVE. An existing install is the
288
+ * default profile, its file does not move, and nothing about it is rewritten.
289
+ *
290
+ * A named profile gets `~/.skrr/profiles/<name>/cli-config.json`, sharing the
291
+ * tree the daemon already uses for the same idea. Sharing rather than inventing
292
+ * `cli-profiles/` is deliberate: today's worst failure is the CLI and the daemon
293
+ * pointing at different deployments (OSK-279), and one profile namespace makes
294
+ * that harder to produce, not easier.
295
+ */
296
+ function profileConfigDir() {
297
+ const profile = (0, keychain_1.getActiveProfile)();
298
+ if (!profile || profile === keychain_1.DEFAULT_PROFILE)
299
+ return newConfigDir();
300
+ return path.join(newConfigDir(), 'profiles', profile);
180
301
  }
181
302
  function newConfigFile() {
303
+ return path.join(profileConfigDir(), 'cli-config.json');
304
+ }
305
+ /** The machine-scoped file, which never moves with the profile. */
306
+ function machineConfigFile() {
182
307
  return path.join(newConfigDir(), 'cli-config.json');
183
308
  }
309
+ /** Split a config into the half that follows the profile and the half that does not. */
310
+ function splitByScope(config) {
311
+ const machine = {};
312
+ const profile = {};
313
+ for (const [key, value] of Object.entries(config)) {
314
+ if (MACHINE_SCOPED_KEYS.includes(key))
315
+ machine[key] = value;
316
+ else
317
+ profile[key] = value;
318
+ }
319
+ return { machine, profile };
320
+ }
321
+ /**
322
+ * Machine-scoped fields, read from the machine file regardless of profile.
323
+ *
324
+ * Returns an empty object rather than throwing: a named profile on a machine
325
+ * that has never run the default one has no machine file yet, and that is a
326
+ * first run, not a fault.
327
+ */
328
+ function readMachineScoped() {
329
+ const p = machineConfigFile();
330
+ if (!fs.existsSync(p))
331
+ return {};
332
+ try {
333
+ return splitByScope(readJsonOrThrow(p)).machine;
334
+ }
335
+ catch {
336
+ return {};
337
+ }
338
+ }
339
+ /**
340
+ * The PRE-RENAME config root — the generation between `.sky` and `.skrr`.
341
+ *
342
+ * This was missing from the fallback chain, which went straight from `.skrr` to
343
+ * `.sky` and skipped the directory that every current install actually has.
344
+ * The effect is not a missing preference: with no config found, `loadConfig`
345
+ * returns DEFAULT_CONFIG, whose baseURL is the DEV deployment — so a CLI that
346
+ * had been pointed at production silently starts addressing dev, and its
347
+ * `workspaceId` is gone. Nothing errors; commands just answer about a different
348
+ * backend.
349
+ *
350
+ * This read "the production apex", and said the drift ran the other way. It did
351
+ * not: DEFAULT_CONFIG has been dev since it was written, forty lines further up.
352
+ * The direction matters here more than most stale comments, because it is the
353
+ * difference between a lost session and one that silently addresses production.
354
+ *
355
+ * Note the filename differs by generation: `.sky` used `config.json`, `.oversky`
356
+ * and `.skrr` use `cli-config.json`.
357
+ */
358
+ function priorConfigDir() {
359
+ return path.join(os.homedir(), '.oversky');
360
+ }
361
+ function priorConfigFile() {
362
+ return path.join(priorConfigDir(), 'cli-config.json');
363
+ }
184
364
  function legacyConfigDir() {
185
365
  return path.join(os.homedir(), '.sky');
186
366
  }
@@ -188,41 +368,221 @@ function legacyConfigFile() {
188
368
  return path.join(legacyConfigDir(), 'config.json');
189
369
  }
190
370
  /**
191
- * Read the config file. Returns defaults if missing. Throws on corrupt
192
- * JSON. Performs a one-shot legacy → new copy when only the legacy file
193
- * exists, so existing users don't have to re-login.
371
+ * Breadcrumb proving the carry-forward already ran on this machine.
372
+ *
373
+ * Deliberately OUTSIDE the file it produces. The migration's own record used to
374
+ * be the config it wrote, which made it one-shot only for as long as that file
375
+ * survived: any moment where `cli-config.json` was absent looked exactly like a
376
+ * first run, and the pre-rename config was adopted again — `baseURL` included.
377
+ *
378
+ * Follows the `.migrated-*` convention already used by the daemon's
379
+ * profile-migration and keychain-hygiene passes.
380
+ */
381
+ const CARRY_FORWARD_BREADCRUMB = '.migrated-2026-09-02-cli-config-carry-forward';
382
+ function carryForwardBreadcrumbPath() {
383
+ return path.join(newConfigDir(), CARRY_FORWARD_BREADCRUMB);
384
+ }
385
+ function hasCarriedForward() {
386
+ try {
387
+ return fs.existsSync(carryForwardBreadcrumbPath());
388
+ }
389
+ catch {
390
+ return false;
391
+ }
392
+ }
393
+ function markCarriedForward() {
394
+ try {
395
+ const dir = newConfigDir();
396
+ if (!fs.existsSync(dir))
397
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
398
+ fs.writeFileSync(carryForwardBreadcrumbPath(), `${new Date().toISOString()}\n`, {
399
+ mode: 0o600,
400
+ });
401
+ }
402
+ catch {
403
+ // Best effort. Failing to record it means the migration may run again,
404
+ // which is the old behaviour — not worse.
405
+ }
406
+ }
407
+ let announcedAdoptedDeployment = false;
408
+ /**
409
+ * Say when the CLI adopts a deployment the user did not name in this command.
410
+ *
411
+ * Whatever the source, changing which DEPLOYMENT a session addresses is not a
412
+ * detail: the failure it produces is an empty list, not an error, because a
413
+ * successful `200 {"data": []}` from the wrong deployment is indistinguishable
414
+ * from having no rows.
415
+ */
416
+ function announceAdoptedDeployment(baseURL, source) {
417
+ if (announcedAdoptedDeployment)
418
+ return;
419
+ announcedAdoptedDeployment = true;
420
+ process.stderr.write(`note: no CLI config found; using ${baseURL} (from ${source}).\n` +
421
+ ` If that is the wrong deployment: skrr login --base-url <url>\n`);
422
+ }
423
+ /**
424
+ * Read the config file. Returns defaults if missing. Throws on corrupt JSON.
425
+ * Performs a one-shot legacy → new copy when only the legacy file exists, so
426
+ * existing users don't have to re-login.
194
427
  */
195
428
  function loadConfig() {
196
429
  const newPath = newConfigFile();
430
+ const named = newPath !== machineConfigFile();
197
431
  if (fs.existsSync(newPath)) {
198
- return overlayEnv(migrateLegacyBaseURL(readJsonOrThrow(newPath)));
432
+ const current = migrateLegacyBaseURL(readJsonOrThrow(newPath));
433
+ warnIfStrandedPriorConfig(current);
434
+ // A named profile's file holds only its own half; the machine half is read
435
+ // from the machine file so `cliId` is one value for this machine no matter
436
+ // how many profiles exist.
437
+ return overlayEnv(named ? { ...current, ...readMachineScoped() } : current);
199
438
  }
200
- const legacyPath = legacyConfigFile();
201
- if (fs.existsSync(legacyPath)) {
202
- const legacy = migrateLegacyBaseURL(readJsonOrThrow(legacyPath));
203
- try {
204
- saveConfig(legacy);
205
- }
206
- catch {
207
- /* swallow caller still has a usable config in memory */
439
+ // A named profile that has never been used is a FIRST RUN for that profile,
440
+ // not a machine without config. It must not adopt the default profile's
441
+ // deployment that is how "switch to dev" would silently keep talking to
442
+ // prod — so it starts from the documented default, carrying only the machine
443
+ // half forward. The generation carry-forward below is deliberately skipped:
444
+ // there is no `.sky`/`.oversky` ancestor of a profile that did not exist then.
445
+ if (named) {
446
+ return overlayEnv({ ...DEFAULT_CONFIG, ...readMachineScoped() });
447
+ }
448
+ // Newest first, and ONCE per machine. Each generation is copied forward so a
449
+ // user never has to re-login because a directory was renamed — but only on a
450
+ // genuine first run. After the breadcrumb exists, a missing config means the
451
+ // file went away, not that this machine is pre-rename, and re-adopting the
452
+ // old file would silently move the session to the old DEPLOYMENT.
453
+ if (!hasCarriedForward()) {
454
+ for (const carriedPath of [priorConfigFile(), legacyConfigFile()]) {
455
+ if (!fs.existsSync(carriedPath))
456
+ continue;
457
+ const carried = migrateLegacyBaseURL(readJsonOrThrow(carriedPath));
458
+ try {
459
+ saveConfig(carried);
460
+ }
461
+ catch {
462
+ /* swallow — caller still has a usable config in memory */
463
+ }
464
+ markCarriedForward();
465
+ announceAdoptedDeployment(carried.baseURL, carriedPath);
466
+ return overlayEnv(carried);
208
467
  }
209
- return overlayEnv(legacy);
210
468
  }
469
+ // Nothing to carry: fall through to the documented default. Deliberately NOT
470
+ // derived from the stored credential's origin — making config resolution read
471
+ // the keychain would let ambient machine state override a documented default,
472
+ // and a fresh install defaulting to dev is intended behaviour, not a bug.
211
473
  return overlayEnv({ ...DEFAULT_CONFIG });
212
474
  }
475
+ /**
476
+ * Surface the one case this fix cannot repair on its own.
477
+ *
478
+ * A CLI that ran after the directory rename but before this migration existed
479
+ * already wrote a `.skrr` config from DEFAULT_CONFIG. The copy-forward above
480
+ * will not fire for it, and it must not: once a config exists, "the user chose
481
+ * this backend" and "the default landed here" are indistinguishable, and
482
+ * silently rewriting a deliberate choice is worse than the bug.
483
+ *
484
+ * So it says so instead of guessing. Warned once per process, to stderr, and
485
+ * only when the two disagree — agreement needs no warning.
486
+ */
487
+ let warnedStrandedPriorConfig = false;
488
+ function warnIfStrandedPriorConfig(current) {
489
+ if (warnedStrandedPriorConfig)
490
+ return;
491
+ // On EVERY command, forever, was the complaint — and it is fair: once you have
492
+ // read it and meant the deployment you are on, it is pure noise burying real
493
+ // stderr. Deleting the old file is one exit; this is the other, for a machine
494
+ // that wants to keep it.
495
+ if ((process.env[QUIET_ENV] ?? '').trim().length > 0)
496
+ return;
497
+ const priorPath = priorConfigFile();
498
+ if (!fs.existsSync(priorPath))
499
+ return;
500
+ let prior;
501
+ try {
502
+ prior = readJsonOrThrow(priorPath);
503
+ }
504
+ catch {
505
+ return; // an unreadable prior config is not evidence of anything
506
+ }
507
+ const priorBase = canonicalizeKnownBaseURL(prior.baseURL);
508
+ if (!priorBase || priorBase === canonicalizeKnownBaseURL(current.baseURL))
509
+ return;
510
+ warnedStrandedPriorConfig = true;
511
+ const stranded = strandedSettings(prior);
512
+ const alsoHolds = stranded.length > 0 ? ` (it also holds ${stranded.join(', ')})` : '';
513
+ process.stderr.write(`warning: ${priorPath} points at ${priorBase}; this CLI is using ${current.baseURL}.\n` +
514
+ ` Use it instead: skrr login --base-url ${priorBase}\n` +
515
+ ` Or silence this: set ${QUIET_ENV}=1, or delete that file${alsoHolds}\n`);
516
+ }
517
+ /** Opt out once you have read the warning and meant the deployment you are on. */
518
+ const QUIET_ENV = 'SKRR_QUIET_CONFIG_WARNING';
519
+ /**
520
+ * The settings that deleting the prior config would discard.
521
+ *
522
+ * The migration did not carry these forward, so the old file is their only
523
+ * copy — naming them is the difference between an instruction and a trap.
524
+ */
525
+ function strandedSettings(prior) {
526
+ const carried = new Set(['baseURL', 'migrated', 'cliId']);
527
+ return Object.keys(prior)
528
+ .filter((k) => !carried.has(k) && prior[k] !== undefined)
529
+ .sort();
530
+ }
531
+ function newPath() {
532
+ return newConfigFile();
533
+ }
213
534
  /** Write the config file with mode 0600 inside a 0700 directory. */
214
535
  function saveConfig(config) {
215
- const dir = newConfigDir();
216
- if (!fs.existsSync(dir)) {
217
- fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
218
- }
219
- const p = newConfigFile();
220
536
  const canonical = {
221
537
  ...config,
222
538
  baseURL: canonicalizeKnownBaseURL(config.baseURL),
223
539
  };
224
- fs.writeFileSync(p, JSON.stringify(canonical, null, 2), { mode: 0o600 });
225
- fs.chmodSync(p, 0o600);
540
+ const target = newConfigFile();
541
+ const machineFile = machineConfigFile();
542
+ // Default profile: one file, exactly as before. Nothing is split, nothing
543
+ // moves, and an existing install cannot notice this change.
544
+ if (target === machineFile) {
545
+ writeConfigFile(target, canonical);
546
+ return;
547
+ }
548
+ const { machine, profile } = splitByScope(canonical);
549
+ writeConfigFile(target, profile);
550
+ // Machine-scoped fields are MERGED into the machine file rather than
551
+ // replacing it: the default profile's deployment, workspace and account live
552
+ // in that same file, and a named profile writing its own half over the top
553
+ // would sign the default profile out.
554
+ const existingMachine = fs.existsSync(machineFile) ? readJsonOrThrow(machineFile) : {};
555
+ const merged = { ...existingMachine, ...machine };
556
+ if (JSON.stringify(merged) !== JSON.stringify(existingMachine)) {
557
+ writeConfigFile(machineFile, merged);
558
+ }
559
+ }
560
+ /**
561
+ * Warn once when a write happens before the profile was chosen.
562
+ *
563
+ * Such a write lands in the default profile's file whatever `--profile` said,
564
+ * because the choice had not been made yet. It is silent, it is permanent, and
565
+ * the only way anyone finds it is by wondering why a named profile keeps
566
+ * forgetting things. The daemon carries the same guard for the same reason.
567
+ */
568
+ let warnedUnresolvedProfile = false;
569
+ function warnIfProfileUnresolved(target) {
570
+ if ((0, keychain_1.isProfileResolved)() || warnedUnresolvedProfile)
571
+ return;
572
+ warnedUnresolvedProfile = true;
573
+ process.stderr.write(`[profile] config written to ${target} before the profile was resolved; ` +
574
+ 'it landed in the default profile. If this was not the crash path, it is a ' +
575
+ 'regression — a writer is running before BaseCommand.init().\n');
576
+ }
577
+ /** Write one config file with the directory created and 0600 enforced. */
578
+ function writeConfigFile(target, contents) {
579
+ warnIfProfileUnresolved(target);
580
+ const dir = path.dirname(target);
581
+ if (!fs.existsSync(dir)) {
582
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
583
+ }
584
+ fs.writeFileSync(target, JSON.stringify(contents, null, 2), { mode: 0o600 });
585
+ fs.chmodSync(target, 0o600);
226
586
  }
227
587
  /**
228
588
  * One-shot migration for clients installed before oversky.dev became the dev
@@ -0,0 +1,22 @@
1
+ /**
2
+ * One shape for "you just created a thing".
3
+ *
4
+ * The four core entities each printed a different confirmation: the identifier
5
+ * was a human ref for a project, a raw uuid for a goal and a space, and both for
6
+ * a task; the separator was an em dash for two and a hyphen for the other two;
7
+ * and goals and spaces appended context (`in space <uuid>`,
8
+ * `(subspace of <uuid>)`) that the link on the next line already carries.
9
+ *
10
+ * Reading four of these in a row, you cannot tell what you are being handed —
11
+ * which is what makes the difference between "did this work" and "what do I
12
+ * paste into the next command" harder than it should be.
13
+ *
14
+ * The rule: prefer the ref a person can type, fall back to the id, and put the
15
+ * title after a single em dash. The `View it at <url>` line that follows is
16
+ * already consistent and is left alone.
17
+ */
18
+ export declare function createdLine(entity: string, opts: {
19
+ ref?: string | null;
20
+ id?: string | null;
21
+ title?: string | null;
22
+ }): string;
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createdLine = createdLine;
4
+ /**
5
+ * One shape for "you just created a thing".
6
+ *
7
+ * The four core entities each printed a different confirmation: the identifier
8
+ * was a human ref for a project, a raw uuid for a goal and a space, and both for
9
+ * a task; the separator was an em dash for two and a hyphen for the other two;
10
+ * and goals and spaces appended context (`in space <uuid>`,
11
+ * `(subspace of <uuid>)`) that the link on the next line already carries.
12
+ *
13
+ * Reading four of these in a row, you cannot tell what you are being handed —
14
+ * which is what makes the difference between "did this work" and "what do I
15
+ * paste into the next command" harder than it should be.
16
+ *
17
+ * The rule: prefer the ref a person can type, fall back to the id, and put the
18
+ * title after a single em dash. The `View it at <url>` line that follows is
19
+ * already consistent and is left alone.
20
+ */
21
+ function createdLine(entity, opts) {
22
+ const label = (opts.ref ?? '').trim() || (opts.id ?? '').trim() || '?';
23
+ const title = (opts.title ?? '').trim() || '(no title)';
24
+ return `Created ${entity} ${label} — ${title}`;
25
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Defaults for the flags that decide WHICH deployment and WHICH profile a
3
+ * newly installed daemon binds to.
4
+ *
5
+ * `skrr daemon *` are pass-throughs to the `skrrd` binary, and the two
6
+ * programs keep SEPARATE configuration: the CLI's deployment and profile live
7
+ * in `cli-config.json`, the daemon's in `daemon.json`. Nothing has ever made
8
+ * one follow the other, so the ordinary way to end up split is to do nothing
9
+ * unusual at all — log the CLI into one deployment and leave the daemon on
10
+ * whichever it was installed against. That is OSK-279, observed on this
11
+ * machine as a daemon on dev serving a CLI on prod.
12
+ *
13
+ * The fix is deliberately scoped to INSTALL, the moment a daemon is created
14
+ * and the only moment its binding is not yet somebody's decision. `start` is
15
+ * left alone on purpose: it starts a daemon that already has a binding, and
16
+ * silently overriding it would break the operator who split the two knowingly
17
+ * — running a dev daemon against a prod CLI is a legitimate thing to do, and
18
+ * it is not this function's place to overrule it. `skrr daemon status` reports
19
+ * a split instead of correcting one.
20
+ */
21
+ /** Whether `argv` already carries `--name` or `--name=value` (or `-alias`). */
22
+ export declare function hasFlag(argv: readonly string[], name: string, alias?: string): boolean;
23
+ /**
24
+ * Append `--server` / `--profile` describing the CLI's own session, for any
25
+ * the caller did not pass. An explicit flag always wins — this only fills a
26
+ * silence, so `skrr daemon install --env prod` still installs against prod
27
+ * while the CLI sits on dev.
28
+ */
29
+ export declare function bindToCliSession(argv: readonly string[]): string[];
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.hasFlag = hasFlag;
4
+ exports.bindToCliSession = bindToCliSession;
5
+ const config_1 = require("./config");
6
+ const keychain_1 = require("./keychain");
7
+ /**
8
+ * Defaults for the flags that decide WHICH deployment and WHICH profile a
9
+ * newly installed daemon binds to.
10
+ *
11
+ * `skrr daemon *` are pass-throughs to the `skrrd` binary, and the two
12
+ * programs keep SEPARATE configuration: the CLI's deployment and profile live
13
+ * in `cli-config.json`, the daemon's in `daemon.json`. Nothing has ever made
14
+ * one follow the other, so the ordinary way to end up split is to do nothing
15
+ * unusual at all — log the CLI into one deployment and leave the daemon on
16
+ * whichever it was installed against. That is OSK-279, observed on this
17
+ * machine as a daemon on dev serving a CLI on prod.
18
+ *
19
+ * The fix is deliberately scoped to INSTALL, the moment a daemon is created
20
+ * and the only moment its binding is not yet somebody's decision. `start` is
21
+ * left alone on purpose: it starts a daemon that already has a binding, and
22
+ * silently overriding it would break the operator who split the two knowingly
23
+ * — running a dev daemon against a prod CLI is a legitimate thing to do, and
24
+ * it is not this function's place to overrule it. `skrr daemon status` reports
25
+ * a split instead of correcting one.
26
+ */
27
+ /** Whether `argv` already carries `--name` or `--name=value` (or `-alias`). */
28
+ function hasFlag(argv, name, alias) {
29
+ return argv.some((arg) => {
30
+ if (arg === '--')
31
+ return false;
32
+ return (arg === name ||
33
+ arg.startsWith(`${name}=`) ||
34
+ (alias !== undefined && (arg === alias || arg.startsWith(`${alias}=`))));
35
+ });
36
+ }
37
+ /**
38
+ * Append `--server` / `--profile` describing the CLI's own session, for any
39
+ * the caller did not pass. An explicit flag always wins — this only fills a
40
+ * silence, so `skrr daemon install --env prod` still installs against prod
41
+ * while the CLI sits on dev.
42
+ */
43
+ function bindToCliSession(argv) {
44
+ const extra = [];
45
+ // `--env` resolves to a server URL inside the daemon, so passing one is
46
+ // passing the other. Adding `--server` beside it would be an ambiguous
47
+ // instruction where the daemon's own precedence, not ours, decides.
48
+ if (!hasFlag(argv, '--server', '-s') && !hasFlag(argv, '--env', '-e')) {
49
+ const { baseURL } = (0, config_1.loadConfig)();
50
+ if (baseURL)
51
+ extra.push('--server', baseURL);
52
+ }
53
+ if (!hasFlag(argv, '--profile')) {
54
+ // The CLI's own profile, resolved the same way `BaseCommand` resolves it.
55
+ // These commands extend `Command`, so `init()` never ran and the module
56
+ // state `getActiveProfile()` reads was never set — asking it here would
57
+ // report `default` no matter what the user typed.
58
+ const profile = (0, keychain_1.resolveProfileFromArgv)(argv) ?? process.env.OVERSKY_PROFILE ?? null;
59
+ if (profile && profile !== keychain_1.DEFAULT_PROFILE)
60
+ extra.push('--profile', profile);
61
+ }
62
+ return [...argv, ...extra];
63
+ }