@skrr-ai/cli 0.1.8 → 0.1.9
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.
- package/dist/base-command.d.ts +27 -0
- package/dist/base-command.js +83 -11
- package/dist/commands/agent/message/send.js +1 -1
- package/dist/commands/agents/chat.d.ts +1 -1
- package/dist/commands/agents/chat.js +1 -1
- package/dist/commands/browser/install.d.ts +1 -1
- package/dist/commands/browser/install.js +10 -2
- package/dist/commands/browser/mcp-config.js +3 -1
- package/dist/commands/browser/skill/show.js +1 -1
- package/dist/commands/browser/status.js +4 -1
- package/dist/commands/browser/uninstall.d.ts +1 -1
- package/dist/commands/browser/uninstall.js +10 -2
- package/dist/commands/browser.d.ts +1 -1
- package/dist/commands/browser.js +3 -3
- package/dist/commands/code/index.js +6 -3
- package/dist/commands/code/install.d.ts +61 -0
- package/dist/commands/code/install.js +247 -0
- package/dist/commands/code/oversky-agent.d.ts +2 -1
- package/dist/commands/code/oversky-agent.js +12 -8
- package/dist/commands/commands.js +1 -1
- package/dist/commands/context.js +1 -1
- package/dist/commands/create-token.js +1 -1
- package/dist/commands/daemon/byok.d.ts +1 -0
- package/dist/commands/daemon/byok.js +14 -1
- package/dist/commands/daemon/index.js +18 -7
- package/dist/commands/daemon/install.d.ts +1 -0
- package/dist/commands/daemon/install.js +22 -3
- package/dist/commands/daemon/logs.d.ts +1 -0
- package/dist/commands/daemon/logs.js +4 -0
- package/dist/commands/daemon/start.d.ts +1 -0
- package/dist/commands/daemon/start.js +15 -2
- package/dist/commands/daemon/status.d.ts +1 -0
- package/dist/commands/daemon/status.js +14 -1
- package/dist/commands/daemon/stop.d.ts +1 -0
- package/dist/commands/daemon/stop.js +14 -1
- package/dist/commands/daemon/uninstall.d.ts +1 -0
- package/dist/commands/daemon/uninstall.js +15 -2
- package/dist/commands/daemon/usage.d.ts +1 -0
- package/dist/commands/daemon/usage.js +5 -1
- package/dist/commands/doctor.js +2 -2
- package/dist/commands/github/allow-tools.d.ts +1 -0
- package/dist/commands/github/app/install.d.ts +1 -0
- package/dist/commands/github/app/status.d.ts +1 -0
- package/dist/commands/github/connect.d.ts +1 -0
- package/dist/commands/github/connections.d.ts +1 -0
- package/dist/commands/github/disconnect.d.ts +1 -0
- package/dist/commands/github/finalize.d.ts +1 -0
- package/dist/commands/github/reconnect.d.ts +1 -0
- package/dist/commands/github/run.d.ts +1 -0
- package/dist/commands/github/tools.d.ts +1 -0
- package/dist/commands/goals/create.js +2 -2
- package/dist/commands/goals/show.js +5 -0
- package/dist/commands/harnesses/list.js +14 -3
- package/dist/commands/harnesses/show.js +14 -0
- package/dist/commands/instructions/activate.js +1 -1
- package/dist/commands/instructions/archive-version.js +1 -1
- package/dist/commands/instructions/delete.js +1 -1
- package/dist/commands/instructions/diff.js +1 -1
- package/dist/commands/instructions/install.js +7 -1
- package/dist/commands/instructions/restore.js +1 -1
- package/dist/commands/instructions/show.js +17 -1
- package/dist/commands/instructions/update.js +1 -1
- package/dist/commands/integrations/connections/connect.d.ts +1 -0
- package/dist/commands/integrations/connections/disconnect.d.ts +1 -0
- package/dist/commands/integrations/connections/finalize.d.ts +1 -0
- package/dist/commands/integrations/connections/list.d.ts +1 -0
- package/dist/commands/integrations/connections/reconnect.d.ts +1 -0
- package/dist/commands/integrations/execute.d.ts +1 -0
- package/dist/commands/integrations/search.d.ts +1 -0
- package/dist/commands/integrations/toolkits.d.ts +1 -0
- package/dist/commands/integrations/tools/allow.d.ts +1 -0
- package/dist/commands/integrations/tools/list.d.ts +1 -0
- package/dist/commands/list-daemons.d.ts +2 -7
- package/dist/commands/list-daemons.js +13 -13
- package/dist/commands/login.js +73 -31
- package/dist/commands/logout.js +52 -1
- package/dist/commands/machines/hosted/start.js +1 -1
- package/dist/commands/pair.js +2 -2
- package/dist/commands/projects/create.js +6 -1
- package/dist/commands/projects/tasks.js +6 -1
- package/dist/commands/revoke-daemon.js +6 -2
- package/dist/commands/set-capabilities.js +6 -2
- package/dist/commands/skills/install.js +1 -1
- package/dist/commands/skills/library.js +2 -2
- package/dist/commands/spaces/create.js +6 -2
- package/dist/commands/tasks/complete.d.ts +1 -1
- package/dist/commands/tasks/complete.js +21 -8
- package/dist/commands/tasks/create.js +4 -1
- package/dist/commands/tasks/current.js +1 -1
- package/dist/commands/tasks/delete.d.ts +1 -0
- package/dist/commands/tasks/delete.js +9 -0
- package/dist/commands/tasks/export.js +5 -1
- package/dist/commands/tasks/follow-up.js +2 -1
- package/dist/commands/tasks/list.js +5 -1
- package/dist/commands/tasks/pr.js +1 -1
- package/dist/commands/tasks/show.js +10 -1
- package/dist/commands/tasks/triage.js +2 -1
- package/dist/commands/tasks/update.js +4 -1
- package/dist/commands/tasks/upsert.js +4 -1
- package/dist/commands/usage.js +1 -1
- package/dist/lib/agentic-stream.js +21 -1
- package/dist/lib/api-fetch.d.ts +1 -1
- package/dist/lib/api-fetch.js +1 -1
- package/dist/lib/auth-core-init.js +23 -3
- package/dist/lib/auth-storage.d.ts +0 -12
- package/dist/lib/auth-storage.js +37 -2
- package/dist/lib/config.d.ts +3 -3
- package/dist/lib/config.js +327 -26
- package/dist/lib/created-line.d.ts +22 -0
- package/dist/lib/created-line.js +25 -0
- package/dist/lib/daemon-binding.d.ts +29 -0
- package/dist/lib/daemon-binding.js +63 -0
- package/dist/lib/daemon-ref.d.ts +31 -0
- package/dist/lib/daemon-ref.js +47 -0
- package/dist/lib/daemonBroker.js +1 -1
- package/dist/lib/delegated-cli.js +3 -3
- package/dist/lib/exec-oversky.d.ts +18 -1
- package/dist/lib/exec-oversky.js +21 -4
- package/dist/lib/html-text.d.ts +15 -0
- package/dist/lib/html-text.js +40 -0
- package/dist/lib/keychain.d.ts +43 -0
- package/dist/lib/keychain.js +145 -4
- package/dist/lib/login.js +3 -3
- package/dist/lib/node-adapter.js +1 -1
- package/dist/lib/oauthLogin.js +17 -6
- package/dist/lib/sky-code-agent.d.ts +1 -1
- package/dist/lib/sky-code-agent.js +8 -8
- package/dist/lib/sky-code-broker.js +18 -2
- package/dist/lib/sky-code-doctor.js +120 -4
- package/dist/lib/sky-code-managed.d.ts +12 -0
- package/dist/lib/sky-code-managed.js +21 -7
- package/dist/lib/sky-code.d.ts +16 -1
- package/dist/lib/sky-code.js +102 -4
- package/dist/lib/task-execution.js +4 -11
- package/dist/lib/tasks.d.ts +1 -0
- package/dist/lib/tasks.js +9 -6
- package/dist/lib/update-check.js +1 -1
- package/dist/lib/usage-discovery.js +1 -1
- package/dist/lib/web-url.js +12 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialResolver.d.ts +1 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialResolver.js +2 -2
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +1 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +13 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/linux.js +16 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/legacyStatePreflight.d.ts +0 -9
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/legacyStatePreflight.js +18 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/loginLocalhost.js +3 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/messages.js +3 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/runtime.d.ts +7 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/skyCodeChannels.d.ts +96 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/skyCodeChannels.js +115 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialResolver.d.ts +1 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialResolver.js +2 -2
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +1 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +1 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/linux.js +16 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/legacyStatePreflight.d.ts +0 -9
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/legacyStatePreflight.js +18 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/loginLocalhost.js +3 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/messages.js +3 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/runtime.d.ts +7 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/skyCodeChannels.d.ts +96 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/skyCodeChannels.js +107 -0
- package/dist/node_modules/@skrr-ai/auth-core/package.json +1 -1
- package/dist/node_modules/@skrr-ai/data-provider/index.js +4206 -4191
- package/dist/node_modules/@skrr-ai/inference-broker/dist/cjs/managed-inference-broker.js +2 -2
- package/dist/node_modules/@skrr-ai/inference-broker/dist/esm/managed-inference-broker.js +2 -2
- package/oclif.manifest.json +9924 -3352
- package/package.json +22 -19
|
@@ -4,6 +4,7 @@ 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
6
|
const task_resolver_1 = require("../../lib/task-resolver");
|
|
7
|
+
const tasks_1 = require("../../lib/tasks");
|
|
7
8
|
/**
|
|
8
9
|
* A deliberately thin decision façade. Triage has no state machine of its own:
|
|
9
10
|
* accepting and declining are ordinary TYPE-resolved moves, duplicate is the
|
|
@@ -29,7 +30,7 @@ class TasksTriage extends base_command_1.BaseCommand {
|
|
|
29
30
|
static flags = {
|
|
30
31
|
reason: core_1.Flags.string({ description: 'Reason returned to the task source when declining' }),
|
|
31
32
|
priority: core_1.Flags.string({
|
|
32
|
-
options: [
|
|
33
|
+
options: [...tasks_1.TASK_PRIORITIES],
|
|
33
34
|
description: 'Priority to set before accepting',
|
|
34
35
|
}),
|
|
35
36
|
of: core_1.Flags.string({ description: 'Canonical task for the duplicate decision' }),
|
|
@@ -45,7 +45,10 @@ class TasksUpdate extends base_command_1.BaseCommand {
|
|
|
45
45
|
description: 'Input format for --description or from-json description',
|
|
46
46
|
options: ['html', 'markdown', 'plain'],
|
|
47
47
|
}),
|
|
48
|
-
priority: core_1.Flags.string({
|
|
48
|
+
priority: core_1.Flags.string({
|
|
49
|
+
description: 'New priority',
|
|
50
|
+
options: [...tasks_1.TASK_PRIORITIES],
|
|
51
|
+
}),
|
|
49
52
|
'start-date': core_1.Flags.string({ description: 'New planned start date (ISO string)' }),
|
|
50
53
|
'due-date': core_1.Flags.string({ description: 'New due date (ISO string)' }),
|
|
51
54
|
estimate: core_1.Flags.integer({ description: 'New estimate value from the Space estimate scale' }),
|
|
@@ -31,7 +31,10 @@ class TasksUpsert extends base_command_1.BaseCommand {
|
|
|
31
31
|
description: 'Input format for --description or from-json description',
|
|
32
32
|
options: ['html', 'markdown', 'plain'],
|
|
33
33
|
}),
|
|
34
|
-
priority: core_1.Flags.string({
|
|
34
|
+
priority: core_1.Flags.string({
|
|
35
|
+
description: 'Task priority',
|
|
36
|
+
options: [...tasks_1.TASK_PRIORITIES],
|
|
37
|
+
}),
|
|
35
38
|
parent: core_1.Flags.string({ description: 'Parent task ID' }),
|
|
36
39
|
space: core_1.Flags.string({ description: 'Space ID' }),
|
|
37
40
|
goal: core_1.Flags.string({ description: 'Goal ID' }),
|
package/dist/commands/usage.js
CHANGED
|
@@ -5,7 +5,7 @@ const base_command_1 = require("../base-command");
|
|
|
5
5
|
const command_manifest_1 = require("../lib/command-manifest");
|
|
6
6
|
const usage_discovery_1 = require("../lib/usage-discovery");
|
|
7
7
|
class Usage extends base_command_1.BaseCommand {
|
|
8
|
-
static description = 'Progressively discover
|
|
8
|
+
static description = 'Progressively discover skrr commands from the manifest, one token-budgeted domain at a time.';
|
|
9
9
|
static examples = [
|
|
10
10
|
'<%= config.bin %> usage',
|
|
11
11
|
'<%= config.bin %> usage tasks',
|
|
@@ -8,6 +8,8 @@ exports.toWebSocketOrigin = toWebSocketOrigin;
|
|
|
8
8
|
const node_readline_1 = require("node:readline");
|
|
9
9
|
const socket_io_client_1 = require("socket.io-client");
|
|
10
10
|
const data_provider_1 = require("@skrr-ai/data-provider");
|
|
11
|
+
const config_1 = require("./config");
|
|
12
|
+
const publicEndpoints_generated_1 = require("./publicEndpoints.generated");
|
|
11
13
|
/**
|
|
12
14
|
* Small Node-side adapter for the existing Agentic Socket.IO contract.
|
|
13
15
|
* It deliberately normalizes only lifecycle/text/tool events; the server's
|
|
@@ -458,9 +460,27 @@ class AgenticStreamClient {
|
|
|
458
460
|
}
|
|
459
461
|
}
|
|
460
462
|
exports.AgenticStreamClient = AgenticStreamClient;
|
|
463
|
+
/**
|
|
464
|
+
* Web apexes whose realtime traffic lives on their `api.` subdomain.
|
|
465
|
+
*
|
|
466
|
+
* Derived from the endpoint contract rather than written out, because the two
|
|
467
|
+
* hostnames used to be literals here and the domain cutover moved one of them:
|
|
468
|
+
* a CLI logged into the new prod apex fell through to `wss://<apex>`, which
|
|
469
|
+
* CloudFront cannot upgrade. Nothing errors at the call site — the socket just
|
|
470
|
+
* never connects, which reads as a network problem rather than a stale list.
|
|
471
|
+
*
|
|
472
|
+
* The retired prod apex stays, for the reverse reason: a config file written
|
|
473
|
+
* before the cutover still names it, and dropping it would strand exactly the
|
|
474
|
+
* sessions the entry exists to carry.
|
|
475
|
+
*/
|
|
476
|
+
const REALTIME_APEXES = new Set([
|
|
477
|
+
new URL(publicEndpoints_generated_1.OVERSKY_ENVIRONMENT_ENDPOINTS.prod.appUrl).hostname,
|
|
478
|
+
new URL(publicEndpoints_generated_1.OVERSKY_ENVIRONMENT_ENDPOINTS.dev.appUrl).hostname,
|
|
479
|
+
new URL(config_1.LEGACY_PROD_BASE_URL).hostname,
|
|
480
|
+
]);
|
|
461
481
|
function toWebSocketOrigin(baseURL) {
|
|
462
482
|
const url = new URL(baseURL);
|
|
463
|
-
if (
|
|
483
|
+
if (REALTIME_APEXES.has(url.hostname)) {
|
|
464
484
|
url.hostname = `api.${url.hostname}`;
|
|
465
485
|
}
|
|
466
486
|
url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
|
package/dist/lib/api-fetch.d.ts
CHANGED
|
@@ -32,7 +32,7 @@ export declare class ApiFetchError extends Error {
|
|
|
32
32
|
constructor(message: string, status: number, body?: string, code?: string);
|
|
33
33
|
}
|
|
34
34
|
/**
|
|
35
|
-
* Call
|
|
35
|
+
* Call a skrr API endpoint with the current credential. Throws
|
|
36
36
|
* `ApiFetchError` on any non-2xx status so callers can branch on
|
|
37
37
|
* `err.status` / `err.code`.
|
|
38
38
|
*/
|
package/dist/lib/api-fetch.js
CHANGED
|
@@ -56,7 +56,7 @@ class ApiFetchError extends Error {
|
|
|
56
56
|
}
|
|
57
57
|
exports.ApiFetchError = ApiFetchError;
|
|
58
58
|
/**
|
|
59
|
-
* Call
|
|
59
|
+
* Call a skrr API endpoint with the current credential. Throws
|
|
60
60
|
* `ApiFetchError` on any non-2xx status so callers can branch on
|
|
61
61
|
* `err.status` / `err.code`.
|
|
62
62
|
*/
|
|
@@ -85,6 +85,26 @@ function ensureAuthCoreConfigured() {
|
|
|
85
85
|
// Phase D: share the daemon's config root so auth.lock and the
|
|
86
86
|
// needs-reauth flag are coordinated across binaries.
|
|
87
87
|
configDir: () => node_path_1.default.join(node_os_1.default.homedir(), '.skrr'),
|
|
88
|
+
/**
|
|
89
|
+
* The machine root, PINNED — never the active profile's directory.
|
|
90
|
+
*
|
|
91
|
+
* `machineConfigDir` defaults to `configDir()`, and while the CLI's
|
|
92
|
+
* `configDir` was unconditionally `~/.skrr` that default was correct and
|
|
93
|
+
* this line would have been noise. It stops being correct the moment
|
|
94
|
+
* `configDir` becomes profile-aware: machine identity would start drifting
|
|
95
|
+
* per profile, and the server binds `cliId` into the JWT `did` claim and
|
|
96
|
+
* REFUSES a rotation presented against a different one. A second profile
|
|
97
|
+
* would not degrade — it would fail to stay logged in.
|
|
98
|
+
*
|
|
99
|
+
* The daemon already learned this the expensive way. OSK-1478: pinning the
|
|
100
|
+
* machine UUID to a profile-scoped `configDir` silently killed the
|
|
101
|
+
* `legacyDaemonUuids()` fold safety net, because it scanned
|
|
102
|
+
* `<profileDir>/profiles/*`, which never exists.
|
|
103
|
+
*
|
|
104
|
+
* Wired ahead of the profile work rather than with it, so the landmine is
|
|
105
|
+
* gone before anything can step on it.
|
|
106
|
+
*/
|
|
107
|
+
machineConfigDir: () => node_path_1.default.join(node_os_1.default.homedir(), '.skrr'),
|
|
88
108
|
logger: cliLogger,
|
|
89
109
|
killSwitches: {
|
|
90
110
|
// CLI shares the same kill-switch surface as the daemon for ops parity.
|
|
@@ -139,13 +159,13 @@ async function settlesWithin(promise, timeoutMs, keepTimerRef = false) {
|
|
|
139
159
|
function encryptedCredentialInitError(timedOut, cause) {
|
|
140
160
|
const state = (0, cred_envelope_1.describeCredEnvelopeState)();
|
|
141
161
|
if (timedOut) {
|
|
142
|
-
return new CliCredentialInitializationError('Stored
|
|
162
|
+
return new CliCredentialInitializationError('Stored skrr credentials are encrypted, but the local credential store did not ' +
|
|
143
163
|
`initialize within ${ENCRYPTED_CREDENTIAL_INIT_TIMEOUT_MS / 1000} seconds. ` +
|
|
144
|
-
'The credential remains protected; wait for other
|
|
164
|
+
'The credential remains protected; wait for other skrr processes to finish and retry.');
|
|
145
165
|
}
|
|
146
166
|
const detail = state.kind === 'disabled' && state.reason ? `${state.kind} (${state.reason})` : state.kind;
|
|
147
167
|
const causeMessage = cause instanceof Error ? cause.message.trim().slice(0, 200) : '';
|
|
148
|
-
return new CliCredentialInitializationError(`Stored
|
|
168
|
+
return new CliCredentialInitializationError(`Stored skrr credentials are encrypted, but the local credential store is ${detail}. ` +
|
|
149
169
|
(causeMessage ? `Initialization failed: ${causeMessage}. ` : '') +
|
|
150
170
|
'The credential remains protected; unlock or repair the OS credential store and retry.');
|
|
151
171
|
}
|
|
@@ -33,18 +33,6 @@ export interface WriteOptions {
|
|
|
33
33
|
* function returns whatever is persisted.
|
|
34
34
|
*/
|
|
35
35
|
export declare function readFromBackend(expectedServerOrigin?: string): ReadResult;
|
|
36
|
-
/**
|
|
37
|
-
* Persist a token bundle to the preferred backend.
|
|
38
|
-
*
|
|
39
|
-
* Writes to keychain when available, file otherwise. If keychain writes
|
|
40
|
-
* fail mid-flight (e.g., user canceled the unlock prompt), we fall back
|
|
41
|
-
* to the file backend so the CLI remains functional.
|
|
42
|
-
*
|
|
43
|
-
* Returns the backend that actually accepted the write. Callers wanting
|
|
44
|
-
* a specific backend (bare mode, store-mode overrides) should call the
|
|
45
|
-
* lower-level write helpers directly — not exported here to keep the
|
|
46
|
-
* default path honest.
|
|
47
|
-
*/
|
|
48
36
|
export declare function writeToBackend(bundle: AuthBundle, options?: WriteOptions): AuthBackend;
|
|
49
37
|
/**
|
|
50
38
|
* Clear both backends. Called on `skrr logout` (and internally during
|
package/dist/lib/auth-storage.js
CHANGED
|
@@ -357,6 +357,37 @@ function readFromBackend(expectedServerOrigin) {
|
|
|
357
357
|
* lower-level write helpers directly — not exported here to keep the
|
|
358
358
|
* default path honest.
|
|
359
359
|
*/
|
|
360
|
+
/**
|
|
361
|
+
* Clear the DAEMON's needs-reauth latch as well as the CLI's own.
|
|
362
|
+
*
|
|
363
|
+
* `clearReauthState()` resolves `getAuthConfigDir()`, and the two binaries wire
|
|
364
|
+
* that differently: the CLI points auth-core at the machine root (`~/.skrr`),
|
|
365
|
+
* the daemon at its PROFILE dir (`~/.skrr/profiles/<profile>`). So the daemon
|
|
366
|
+
* latched at one path while `skrr login` cleared another — a file that on most
|
|
367
|
+
* machines does not exist at all, which made the clear a silent no-op.
|
|
368
|
+
*
|
|
369
|
+
* The visible cost was a loop with no exit: `skrr daemon status` reported
|
|
370
|
+
* "Cause: TOKEN_INVALID · Remedy: run `skrr login`", the login succeeded, and
|
|
371
|
+
* the flag never moved, so the daemon's realtime channel stayed down and it
|
|
372
|
+
* could not execute tasks. `auth-core-init.ts` states the intent — "share the
|
|
373
|
+
* daemon's config root so auth.lock and the needs-reauth flag are coordinated
|
|
374
|
+
* across binaries" — which stopped being true when the daemon became
|
|
375
|
+
* profile-scoped.
|
|
376
|
+
*
|
|
377
|
+
* The ACTIVE profile only, deliberately: a per-profile latch is correct because
|
|
378
|
+
* different profiles can hold different credentials, and a login re-authorises
|
|
379
|
+
* exactly the one it just wrote.
|
|
380
|
+
*/
|
|
381
|
+
function clearDaemonProfileReauthState() {
|
|
382
|
+
try {
|
|
383
|
+
const target = path.join(os.homedir(), '.skrr', 'profiles', keychain.getActiveProfile(), 'needs-reauth.json');
|
|
384
|
+
fs.rmSync(target, { force: true });
|
|
385
|
+
}
|
|
386
|
+
catch {
|
|
387
|
+
// Best effort: the CLI's own latch is already cleared, and failing to
|
|
388
|
+
// remove the daemon's must not fail a login that otherwise succeeded.
|
|
389
|
+
}
|
|
390
|
+
}
|
|
360
391
|
function writeToBackend(bundle, options = {}) {
|
|
361
392
|
const requestedOrigin = options.serverOrigin ?? bundle.serverOrigin;
|
|
362
393
|
let persistedBundle = bundle;
|
|
@@ -373,8 +404,10 @@ function writeToBackend(bundle, options = {}) {
|
|
|
373
404
|
// Remove any stale file copy so we don't have two sources of truth.
|
|
374
405
|
deleteFileBackend();
|
|
375
406
|
backend = 'keychain';
|
|
376
|
-
if (options.clearReauth)
|
|
407
|
+
if (options.clearReauth) {
|
|
377
408
|
(0, auth_core_1.clearReauthState)();
|
|
409
|
+
clearDaemonProfileReauthState();
|
|
410
|
+
}
|
|
378
411
|
return backend;
|
|
379
412
|
}
|
|
380
413
|
// Keychain failed — fall through to the file backend rather than
|
|
@@ -386,8 +419,10 @@ function writeToBackend(bundle, options = {}) {
|
|
|
386
419
|
}
|
|
387
420
|
writeToFile(persistedBundle);
|
|
388
421
|
backend = 'file';
|
|
389
|
-
if (options.clearReauth)
|
|
422
|
+
if (options.clearReauth) {
|
|
390
423
|
(0, auth_core_1.clearReauthState)();
|
|
424
|
+
clearDaemonProfileReauthState();
|
|
425
|
+
}
|
|
391
426
|
return backend;
|
|
392
427
|
}
|
|
393
428
|
/**
|
package/dist/lib/config.d.ts
CHANGED
|
@@ -128,9 +128,9 @@ export declare function canonicalizeKnownBaseURL(value: string): string;
|
|
|
128
128
|
*/
|
|
129
129
|
export declare function isEnvAuthOverride(): boolean;
|
|
130
130
|
/**
|
|
131
|
-
* Read the config file. Returns defaults if missing. Throws on corrupt
|
|
132
|
-
*
|
|
133
|
-
*
|
|
131
|
+
* Read the config file. Returns defaults if missing. Throws on corrupt JSON.
|
|
132
|
+
* Performs a one-shot legacy → new copy when only the legacy file exists, so
|
|
133
|
+
* existing users don't have to re-login.
|
|
134
134
|
*/
|
|
135
135
|
export declare function loadConfig(): CliConfig;
|
|
136
136
|
/** Write the config file with mode 0600 inside a 0700 directory. */
|
package/dist/lib/config.js
CHANGED
|
@@ -48,6 +48,7 @@ exports.saveRawConfig = saveRawConfig;
|
|
|
48
48
|
const fs = __importStar(require("node:fs"));
|
|
49
49
|
const os = __importStar(require("node:os"));
|
|
50
50
|
const path = __importStar(require("node:path"));
|
|
51
|
+
const keychain_1 = require("./keychain");
|
|
51
52
|
const publicEndpoints_generated_1 = require("./publicEndpoints.generated");
|
|
52
53
|
// Both from config/public-endpoints.json, the one domain contract, rather than a
|
|
53
54
|
// fourth hand-copied pair. The CLI previously knew only about dev — there was no
|
|
@@ -138,18 +139,46 @@ const ENV_VAR = {
|
|
|
138
139
|
refreshToken: 'OVERSKY_REFRESH_TOKEN',
|
|
139
140
|
cliId: 'OVERSKY_CLI_ID',
|
|
140
141
|
};
|
|
142
|
+
/**
|
|
143
|
+
* Read one overlay variable under either prefix.
|
|
144
|
+
*
|
|
145
|
+
* The ~921 `OVERSKY_*` variables are RETAINED by decision
|
|
146
|
+
* (docs/product/skrr-env-inventory.md), so this migrates nothing and changes no
|
|
147
|
+
* existing environment: the legacy spelling still wins when both are set. What
|
|
148
|
+
* it stops is the renamed product SILENTLY IGNORING the prefix its own
|
|
149
|
+
* contributor docs tell new code to use. `SKRR_BASE_URL=https://oversky.dev`
|
|
150
|
+
* previously did nothing at all — the command ran happily against production
|
|
151
|
+
* and reported success, which is the worst available outcome for a variable
|
|
152
|
+
* whose entire job is choosing the deployment.
|
|
153
|
+
*
|
|
154
|
+
* A disagreement is reported rather than resolved quietly, because two
|
|
155
|
+
* different values for one setting is a mistake either way.
|
|
156
|
+
*/
|
|
157
|
+
let warnedEnvPrefixConflict = false;
|
|
158
|
+
function envOverlayValue(key) {
|
|
159
|
+
const legacyName = ENV_VAR[key];
|
|
160
|
+
const renamedName = legacyName.replace(/^OVERSKY_/, 'SKRR_');
|
|
161
|
+
const legacy = process.env[legacyName]?.trim();
|
|
162
|
+
const renamed = process.env[renamedName]?.trim();
|
|
163
|
+
if (legacy && renamed && legacy !== renamed && !warnedEnvPrefixConflict) {
|
|
164
|
+
warnedEnvPrefixConflict = true;
|
|
165
|
+
process.stderr.write(`warning: ${legacyName} and ${renamedName} are both set and disagree. ` +
|
|
166
|
+
`Using ${legacyName}=${legacy}.\n`);
|
|
167
|
+
}
|
|
168
|
+
return legacy && legacy.length > 0 ? legacy : renamed && renamed.length > 0 ? renamed : undefined;
|
|
169
|
+
}
|
|
141
170
|
function overlayEnv(cfg) {
|
|
142
171
|
const out = { ...cfg };
|
|
143
|
-
const bu =
|
|
172
|
+
const bu = envOverlayValue('baseURL') || envOverlayValue('serverUrl');
|
|
144
173
|
if (bu && bu.length > 0)
|
|
145
174
|
out.baseURL = canonicalizeKnownBaseURL(bu);
|
|
146
|
-
const tok =
|
|
175
|
+
const tok = envOverlayValue('token');
|
|
147
176
|
if (tok && tok.length > 0)
|
|
148
177
|
out.token = tok;
|
|
149
|
-
const rt =
|
|
178
|
+
const rt = envOverlayValue('refreshToken');
|
|
150
179
|
if (rt && rt.length > 0)
|
|
151
180
|
out.refreshToken = rt;
|
|
152
|
-
const cid =
|
|
181
|
+
const cid = envOverlayValue('cliId');
|
|
153
182
|
if (cid && cid.length > 0)
|
|
154
183
|
out.cliId = cid;
|
|
155
184
|
return out;
|
|
@@ -161,8 +190,9 @@ function overlayEnv(cfg) {
|
|
|
161
190
|
* us persist a rotated pair that the next `loadConfig()` would shadow.
|
|
162
191
|
*/
|
|
163
192
|
function isEnvAuthOverride() {
|
|
164
|
-
|
|
165
|
-
|
|
193
|
+
// Both prefixes, or a credential injected as SKRR_TOKEN would be used for the
|
|
194
|
+
// request while the process still believed it could refresh and persist.
|
|
195
|
+
return !!(envOverlayValue('token') || envOverlayValue('refreshToken'));
|
|
166
196
|
}
|
|
167
197
|
/**
|
|
168
198
|
* Storage layout (Phase H+).
|
|
@@ -178,9 +208,100 @@ function isEnvAuthOverride() {
|
|
|
178
208
|
function newConfigDir() {
|
|
179
209
|
return path.join(os.homedir(), '.skrr');
|
|
180
210
|
}
|
|
211
|
+
/**
|
|
212
|
+
* Fields that belong to the MACHINE, not to a profile.
|
|
213
|
+
*
|
|
214
|
+
* `cliId` is the sharp one. Its own doc says it is presented at login and at
|
|
215
|
+
* every refresh rotation, and that the server binds it into the JWT `did` claim
|
|
216
|
+
* and rejects a rotation against a different one. Split it per profile and a
|
|
217
|
+
* second profile does not degrade — it fails to stay logged in.
|
|
218
|
+
*
|
|
219
|
+
* `credentials.store` is a machine posture (keychain / file / env-only), and
|
|
220
|
+
* `migrated` is a breadcrumb about this machine's DISK LAYOUT. Neither is a
|
|
221
|
+
* property of which deployment you are addressing.
|
|
222
|
+
*
|
|
223
|
+
* Everything else follows the profile, because everything else IS the
|
|
224
|
+
* deployment: `baseURL`, who you are there (`userId`, `email`), and what you
|
|
225
|
+
* chose there (`workspaceId`, `skyCodeAgentId`).
|
|
226
|
+
*/
|
|
227
|
+
const MACHINE_SCOPED_KEYS = ['cliId', 'credentials', 'migrated'];
|
|
228
|
+
/**
|
|
229
|
+
* Where this profile's config lives.
|
|
230
|
+
*
|
|
231
|
+
* The DEFAULT profile keeps `~/.skrr/cli-config.json` — the exact path it has
|
|
232
|
+
* always used. That is the whole reason this change needs no migration and
|
|
233
|
+
* carries almost no risk: profiles are ADDITIVE. An existing install is the
|
|
234
|
+
* default profile, its file does not move, and nothing about it is rewritten.
|
|
235
|
+
*
|
|
236
|
+
* A named profile gets `~/.skrr/profiles/<name>/cli-config.json`, sharing the
|
|
237
|
+
* tree the daemon already uses for the same idea. Sharing rather than inventing
|
|
238
|
+
* `cli-profiles/` is deliberate: today's worst failure is the CLI and the daemon
|
|
239
|
+
* pointing at different deployments (OSK-279), and one profile namespace makes
|
|
240
|
+
* that harder to produce, not easier.
|
|
241
|
+
*/
|
|
242
|
+
function profileConfigDir() {
|
|
243
|
+
const profile = (0, keychain_1.getActiveProfile)();
|
|
244
|
+
if (!profile || profile === keychain_1.DEFAULT_PROFILE)
|
|
245
|
+
return newConfigDir();
|
|
246
|
+
return path.join(newConfigDir(), 'profiles', profile);
|
|
247
|
+
}
|
|
181
248
|
function newConfigFile() {
|
|
249
|
+
return path.join(profileConfigDir(), 'cli-config.json');
|
|
250
|
+
}
|
|
251
|
+
/** The machine-scoped file, which never moves with the profile. */
|
|
252
|
+
function machineConfigFile() {
|
|
182
253
|
return path.join(newConfigDir(), 'cli-config.json');
|
|
183
254
|
}
|
|
255
|
+
/** Split a config into the half that follows the profile and the half that does not. */
|
|
256
|
+
function splitByScope(config) {
|
|
257
|
+
const machine = {};
|
|
258
|
+
const profile = {};
|
|
259
|
+
for (const [key, value] of Object.entries(config)) {
|
|
260
|
+
if (MACHINE_SCOPED_KEYS.includes(key))
|
|
261
|
+
machine[key] = value;
|
|
262
|
+
else
|
|
263
|
+
profile[key] = value;
|
|
264
|
+
}
|
|
265
|
+
return { machine, profile };
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Machine-scoped fields, read from the machine file regardless of profile.
|
|
269
|
+
*
|
|
270
|
+
* Returns an empty object rather than throwing: a named profile on a machine
|
|
271
|
+
* that has never run the default one has no machine file yet, and that is a
|
|
272
|
+
* first run, not a fault.
|
|
273
|
+
*/
|
|
274
|
+
function readMachineScoped() {
|
|
275
|
+
const p = machineConfigFile();
|
|
276
|
+
if (!fs.existsSync(p))
|
|
277
|
+
return {};
|
|
278
|
+
try {
|
|
279
|
+
return splitByScope(readJsonOrThrow(p)).machine;
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
return {};
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* The PRE-RENAME config root — the generation between `.sky` and `.skrr`.
|
|
287
|
+
*
|
|
288
|
+
* This was missing from the fallback chain, which went straight from `.skrr` to
|
|
289
|
+
* `.sky` and skipped the directory that every current install actually has.
|
|
290
|
+
* The effect is not a missing preference: with no config found, `loadConfig`
|
|
291
|
+
* returns DEFAULT_CONFIG, whose baseURL is the production apex — so a CLI that
|
|
292
|
+
* had been pointed at the dev deployment silently starts addressing PRODUCTION,
|
|
293
|
+
* and its `workspaceId` is gone. Nothing errors; commands just answer about a
|
|
294
|
+
* different backend.
|
|
295
|
+
*
|
|
296
|
+
* Note the filename differs by generation: `.sky` used `config.json`, `.oversky`
|
|
297
|
+
* and `.skrr` use `cli-config.json`.
|
|
298
|
+
*/
|
|
299
|
+
function priorConfigDir() {
|
|
300
|
+
return path.join(os.homedir(), '.oversky');
|
|
301
|
+
}
|
|
302
|
+
function priorConfigFile() {
|
|
303
|
+
return path.join(priorConfigDir(), 'cli-config.json');
|
|
304
|
+
}
|
|
184
305
|
function legacyConfigDir() {
|
|
185
306
|
return path.join(os.homedir(), '.sky');
|
|
186
307
|
}
|
|
@@ -188,41 +309,221 @@ function legacyConfigFile() {
|
|
|
188
309
|
return path.join(legacyConfigDir(), 'config.json');
|
|
189
310
|
}
|
|
190
311
|
/**
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
312
|
+
* Breadcrumb proving the carry-forward already ran on this machine.
|
|
313
|
+
*
|
|
314
|
+
* Deliberately OUTSIDE the file it produces. The migration's own record used to
|
|
315
|
+
* be the config it wrote, which made it one-shot only for as long as that file
|
|
316
|
+
* survived: any moment where `cli-config.json` was absent looked exactly like a
|
|
317
|
+
* first run, and the pre-rename config was adopted again — `baseURL` included.
|
|
318
|
+
*
|
|
319
|
+
* Follows the `.migrated-*` convention already used by the daemon's
|
|
320
|
+
* profile-migration and keychain-hygiene passes.
|
|
321
|
+
*/
|
|
322
|
+
const CARRY_FORWARD_BREADCRUMB = '.migrated-2026-09-02-cli-config-carry-forward';
|
|
323
|
+
function carryForwardBreadcrumbPath() {
|
|
324
|
+
return path.join(newConfigDir(), CARRY_FORWARD_BREADCRUMB);
|
|
325
|
+
}
|
|
326
|
+
function hasCarriedForward() {
|
|
327
|
+
try {
|
|
328
|
+
return fs.existsSync(carryForwardBreadcrumbPath());
|
|
329
|
+
}
|
|
330
|
+
catch {
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
function markCarriedForward() {
|
|
335
|
+
try {
|
|
336
|
+
const dir = newConfigDir();
|
|
337
|
+
if (!fs.existsSync(dir))
|
|
338
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
339
|
+
fs.writeFileSync(carryForwardBreadcrumbPath(), `${new Date().toISOString()}\n`, {
|
|
340
|
+
mode: 0o600,
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
catch {
|
|
344
|
+
// Best effort. Failing to record it means the migration may run again,
|
|
345
|
+
// which is the old behaviour — not worse.
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
let announcedAdoptedDeployment = false;
|
|
349
|
+
/**
|
|
350
|
+
* Say when the CLI adopts a deployment the user did not name in this command.
|
|
351
|
+
*
|
|
352
|
+
* Whatever the source, changing which DEPLOYMENT a session addresses is not a
|
|
353
|
+
* detail: the failure it produces is an empty list, not an error, because a
|
|
354
|
+
* successful `200 {"data": []}` from the wrong deployment is indistinguishable
|
|
355
|
+
* from having no rows.
|
|
356
|
+
*/
|
|
357
|
+
function announceAdoptedDeployment(baseURL, source) {
|
|
358
|
+
if (announcedAdoptedDeployment)
|
|
359
|
+
return;
|
|
360
|
+
announcedAdoptedDeployment = true;
|
|
361
|
+
process.stderr.write(`note: no CLI config found; using ${baseURL} (from ${source}).\n` +
|
|
362
|
+
` If that is the wrong deployment: skrr login --base-url <url>\n`);
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Read the config file. Returns defaults if missing. Throws on corrupt JSON.
|
|
366
|
+
* Performs a one-shot legacy → new copy when only the legacy file exists, so
|
|
367
|
+
* existing users don't have to re-login.
|
|
194
368
|
*/
|
|
195
369
|
function loadConfig() {
|
|
196
370
|
const newPath = newConfigFile();
|
|
371
|
+
const named = newPath !== machineConfigFile();
|
|
197
372
|
if (fs.existsSync(newPath)) {
|
|
198
|
-
|
|
373
|
+
const current = migrateLegacyBaseURL(readJsonOrThrow(newPath));
|
|
374
|
+
warnIfStrandedPriorConfig(current);
|
|
375
|
+
// A named profile's file holds only its own half; the machine half is read
|
|
376
|
+
// from the machine file so `cliId` is one value for this machine no matter
|
|
377
|
+
// how many profiles exist.
|
|
378
|
+
return overlayEnv(named ? { ...current, ...readMachineScoped() } : current);
|
|
199
379
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
380
|
+
// A named profile that has never been used is a FIRST RUN for that profile,
|
|
381
|
+
// not a machine without config. It must not adopt the default profile's
|
|
382
|
+
// deployment — that is how "switch to dev" would silently keep talking to
|
|
383
|
+
// prod — so it starts from the documented default, carrying only the machine
|
|
384
|
+
// half forward. The generation carry-forward below is deliberately skipped:
|
|
385
|
+
// there is no `.sky`/`.oversky` ancestor of a profile that did not exist then.
|
|
386
|
+
if (named) {
|
|
387
|
+
return overlayEnv({ ...DEFAULT_CONFIG, ...readMachineScoped() });
|
|
388
|
+
}
|
|
389
|
+
// Newest first, and ONCE per machine. Each generation is copied forward so a
|
|
390
|
+
// user never has to re-login because a directory was renamed — but only on a
|
|
391
|
+
// genuine first run. After the breadcrumb exists, a missing config means the
|
|
392
|
+
// file went away, not that this machine is pre-rename, and re-adopting the
|
|
393
|
+
// old file would silently move the session to the old DEPLOYMENT.
|
|
394
|
+
if (!hasCarriedForward()) {
|
|
395
|
+
for (const carriedPath of [priorConfigFile(), legacyConfigFile()]) {
|
|
396
|
+
if (!fs.existsSync(carriedPath))
|
|
397
|
+
continue;
|
|
398
|
+
const carried = migrateLegacyBaseURL(readJsonOrThrow(carriedPath));
|
|
399
|
+
try {
|
|
400
|
+
saveConfig(carried);
|
|
401
|
+
}
|
|
402
|
+
catch {
|
|
403
|
+
/* swallow — caller still has a usable config in memory */
|
|
404
|
+
}
|
|
405
|
+
markCarriedForward();
|
|
406
|
+
announceAdoptedDeployment(carried.baseURL, carriedPath);
|
|
407
|
+
return overlayEnv(carried);
|
|
208
408
|
}
|
|
209
|
-
return overlayEnv(legacy);
|
|
210
409
|
}
|
|
410
|
+
// Nothing to carry: fall through to the documented default. Deliberately NOT
|
|
411
|
+
// derived from the stored credential's origin — making config resolution read
|
|
412
|
+
// the keychain would let ambient machine state override a documented default,
|
|
413
|
+
// and a fresh install defaulting to dev is intended behaviour, not a bug.
|
|
211
414
|
return overlayEnv({ ...DEFAULT_CONFIG });
|
|
212
415
|
}
|
|
416
|
+
/**
|
|
417
|
+
* Surface the one case this fix cannot repair on its own.
|
|
418
|
+
*
|
|
419
|
+
* A CLI that ran after the directory rename but before this migration existed
|
|
420
|
+
* already wrote a `.skrr` config from DEFAULT_CONFIG. The copy-forward above
|
|
421
|
+
* will not fire for it, and it must not: once a config exists, "the user chose
|
|
422
|
+
* this backend" and "the default landed here" are indistinguishable, and
|
|
423
|
+
* silently rewriting a deliberate choice is worse than the bug.
|
|
424
|
+
*
|
|
425
|
+
* So it says so instead of guessing. Warned once per process, to stderr, and
|
|
426
|
+
* only when the two disagree — agreement needs no warning.
|
|
427
|
+
*/
|
|
428
|
+
let warnedStrandedPriorConfig = false;
|
|
429
|
+
function warnIfStrandedPriorConfig(current) {
|
|
430
|
+
if (warnedStrandedPriorConfig)
|
|
431
|
+
return;
|
|
432
|
+
// On EVERY command, forever, was the complaint — and it is fair: once you have
|
|
433
|
+
// read it and meant the deployment you are on, it is pure noise burying real
|
|
434
|
+
// stderr. Deleting the old file is one exit; this is the other, for a machine
|
|
435
|
+
// that wants to keep it.
|
|
436
|
+
if ((process.env[QUIET_ENV] ?? '').trim().length > 0)
|
|
437
|
+
return;
|
|
438
|
+
const priorPath = priorConfigFile();
|
|
439
|
+
if (!fs.existsSync(priorPath))
|
|
440
|
+
return;
|
|
441
|
+
let prior;
|
|
442
|
+
try {
|
|
443
|
+
prior = readJsonOrThrow(priorPath);
|
|
444
|
+
}
|
|
445
|
+
catch {
|
|
446
|
+
return; // an unreadable prior config is not evidence of anything
|
|
447
|
+
}
|
|
448
|
+
const priorBase = canonicalizeKnownBaseURL(prior.baseURL);
|
|
449
|
+
if (!priorBase || priorBase === canonicalizeKnownBaseURL(current.baseURL))
|
|
450
|
+
return;
|
|
451
|
+
warnedStrandedPriorConfig = true;
|
|
452
|
+
const stranded = strandedSettings(prior);
|
|
453
|
+
const alsoHolds = stranded.length > 0 ? ` (it also holds ${stranded.join(', ')})` : '';
|
|
454
|
+
process.stderr.write(`warning: ${priorPath} points at ${priorBase}; this CLI is using ${current.baseURL}.\n` +
|
|
455
|
+
` Use it instead: skrr login --base-url ${priorBase}\n` +
|
|
456
|
+
` Or silence this: set ${QUIET_ENV}=1, or delete that file${alsoHolds}\n`);
|
|
457
|
+
}
|
|
458
|
+
/** Opt out once you have read the warning and meant the deployment you are on. */
|
|
459
|
+
const QUIET_ENV = 'SKRR_QUIET_CONFIG_WARNING';
|
|
460
|
+
/**
|
|
461
|
+
* The settings that deleting the prior config would discard.
|
|
462
|
+
*
|
|
463
|
+
* The migration did not carry these forward, so the old file is their only
|
|
464
|
+
* copy — naming them is the difference between an instruction and a trap.
|
|
465
|
+
*/
|
|
466
|
+
function strandedSettings(prior) {
|
|
467
|
+
const carried = new Set(['baseURL', 'migrated', 'cliId']);
|
|
468
|
+
return Object.keys(prior)
|
|
469
|
+
.filter((k) => !carried.has(k) && prior[k] !== undefined)
|
|
470
|
+
.sort();
|
|
471
|
+
}
|
|
472
|
+
function newPath() {
|
|
473
|
+
return newConfigFile();
|
|
474
|
+
}
|
|
213
475
|
/** Write the config file with mode 0600 inside a 0700 directory. */
|
|
214
476
|
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
477
|
const canonical = {
|
|
221
478
|
...config,
|
|
222
479
|
baseURL: canonicalizeKnownBaseURL(config.baseURL),
|
|
223
480
|
};
|
|
224
|
-
|
|
225
|
-
|
|
481
|
+
const target = newConfigFile();
|
|
482
|
+
const machineFile = machineConfigFile();
|
|
483
|
+
// Default profile: one file, exactly as before. Nothing is split, nothing
|
|
484
|
+
// moves, and an existing install cannot notice this change.
|
|
485
|
+
if (target === machineFile) {
|
|
486
|
+
writeConfigFile(target, canonical);
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
const { machine, profile } = splitByScope(canonical);
|
|
490
|
+
writeConfigFile(target, profile);
|
|
491
|
+
// Machine-scoped fields are MERGED into the machine file rather than
|
|
492
|
+
// replacing it: the default profile's deployment, workspace and account live
|
|
493
|
+
// in that same file, and a named profile writing its own half over the top
|
|
494
|
+
// would sign the default profile out.
|
|
495
|
+
const existingMachine = fs.existsSync(machineFile) ? readJsonOrThrow(machineFile) : {};
|
|
496
|
+
const merged = { ...existingMachine, ...machine };
|
|
497
|
+
if (JSON.stringify(merged) !== JSON.stringify(existingMachine)) {
|
|
498
|
+
writeConfigFile(machineFile, merged);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Warn once when a write happens before the profile was chosen.
|
|
503
|
+
*
|
|
504
|
+
* Such a write lands in the default profile's file whatever `--profile` said,
|
|
505
|
+
* because the choice had not been made yet. It is silent, it is permanent, and
|
|
506
|
+
* the only way anyone finds it is by wondering why a named profile keeps
|
|
507
|
+
* forgetting things. The daemon carries the same guard for the same reason.
|
|
508
|
+
*/
|
|
509
|
+
let warnedUnresolvedProfile = false;
|
|
510
|
+
function warnIfProfileUnresolved(target) {
|
|
511
|
+
if ((0, keychain_1.isProfileResolved)() || warnedUnresolvedProfile)
|
|
512
|
+
return;
|
|
513
|
+
warnedUnresolvedProfile = true;
|
|
514
|
+
process.stderr.write(`[profile] config written to ${target} before the profile was resolved; ` +
|
|
515
|
+
'it landed in the default profile. If this was not the crash path, it is a ' +
|
|
516
|
+
'regression — a writer is running before BaseCommand.init().\n');
|
|
517
|
+
}
|
|
518
|
+
/** Write one config file with the directory created and 0600 enforced. */
|
|
519
|
+
function writeConfigFile(target, contents) {
|
|
520
|
+
warnIfProfileUnresolved(target);
|
|
521
|
+
const dir = path.dirname(target);
|
|
522
|
+
if (!fs.existsSync(dir)) {
|
|
523
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
524
|
+
}
|
|
525
|
+
fs.writeFileSync(target, JSON.stringify(contents, null, 2), { mode: 0o600 });
|
|
526
|
+
fs.chmodSync(target, 0o600);
|
|
226
527
|
}
|
|
227
528
|
/**
|
|
228
529
|
* One-shot migration for clients installed before oversky.dev became the dev
|