@skrr-ai/cli 0.1.10 → 0.1.12
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 +1 -13
- package/dist/base-command.js +78 -1
- package/dist/commands/browser/skill/show.js +7 -1
- package/dist/commands/code/index.d.ts +1 -0
- package/dist/commands/code/index.js +9 -1
- package/dist/commands/commitments/analytics/index.js +2 -0
- package/dist/commands/daemon/byok.d.ts +1 -0
- package/dist/commands/daemon/byok.js +2 -1
- package/dist/commands/daemon/install.d.ts +15 -0
- package/dist/commands/daemon/install.js +39 -2
- package/dist/commands/daemon/login.d.ts +31 -0
- package/dist/commands/daemon/login.js +56 -0
- package/dist/commands/daemon/restart.d.ts +9 -0
- package/dist/commands/daemon/restart.js +36 -0
- package/dist/commands/daemon/start.d.ts +1 -0
- package/dist/commands/daemon/start.js +2 -1
- package/dist/commands/daemon/status.d.ts +1 -0
- package/dist/commands/daemon/status.js +2 -1
- package/dist/commands/daemon/stop.d.ts +1 -0
- package/dist/commands/daemon/stop.js +2 -1
- package/dist/commands/daemon/uninstall.d.ts +1 -0
- package/dist/commands/daemon/uninstall.js +2 -1
- package/dist/commands/daemon/unlock.d.ts +9 -0
- package/dist/commands/daemon/unlock.js +33 -0
- package/dist/commands/goals/key-results/create.js +32 -1
- package/dist/commands/goals/key-results/update.d.ts +11 -0
- package/dist/commands/goals/key-results/update.js +80 -2
- package/dist/commands/goals/plan-now.d.ts +54 -2
- package/dist/commands/goals/plan-now.js +175 -18
- package/dist/commands/goals/planner-config.d.ts +60 -9
- package/dist/commands/goals/planner-config.js +82 -34
- package/dist/commands/goals/revisions.js +17 -0
- package/dist/commands/goals/show.d.ts +17 -0
- package/dist/commands/goals/show.js +90 -3
- package/dist/commands/login.js +53 -0
- package/dist/commands/spaces/create.js +2 -1
- package/dist/commands/spaces/index.js +9 -1
- package/dist/commands/spaces/list.d.ts +18 -0
- package/dist/commands/spaces/list.js +57 -7
- package/dist/commands/spaces/show.js +4 -1
- package/dist/commands/spaces/summary.d.ts +4 -0
- package/dist/commands/spaces/summary.js +77 -1
- package/dist/commands/spaces/update.d.ts +4 -0
- package/dist/commands/spaces/update.js +38 -1
- package/dist/commands/tasks/actionability.js +40 -1
- package/dist/commands/tasks/activity.d.ts +29 -0
- package/dist/commands/tasks/activity.js +47 -0
- package/dist/commands/tasks/complete.d.ts +47 -0
- package/dist/commands/tasks/complete.js +159 -12
- package/dist/commands/tasks/create.d.ts +26 -0
- package/dist/commands/tasks/create.js +60 -1
- package/dist/commands/tasks/events/append.d.ts +2 -0
- package/dist/commands/tasks/events/append.js +41 -10
- package/dist/commands/tasks/events/list.js +60 -9
- package/dist/commands/tasks/output.js +22 -2
- package/dist/commands/tasks/ready.d.ts +38 -0
- package/dist/commands/tasks/ready.js +37 -0
- package/dist/commands/tasks/runs.d.ts +22 -0
- package/dist/commands/tasks/runs.js +110 -2
- package/dist/commands/tasks/show.d.ts +28 -0
- package/dist/commands/tasks/show.js +61 -0
- package/dist/commands/tasks/timeline.d.ts +7 -0
- package/dist/commands/tasks/timeline.js +34 -3
- package/dist/commands/tasks/update.d.ts +20 -0
- package/dist/commands/tasks/update.js +38 -1
- package/dist/commands/whoami.d.ts +40 -0
- package/dist/commands/whoami.js +57 -9
- package/dist/commands/wiki/ls.d.ts +23 -0
- package/dist/commands/wiki/ls.js +63 -10
- package/dist/commands/wiki/mv.d.ts +70 -0
- package/dist/commands/wiki/mv.js +198 -4
- package/dist/commands/wiki/rm.js +12 -1
- package/dist/commands/wiki/write.js +35 -3
- package/dist/help.d.ts +27 -0
- package/dist/help.js +48 -0
- package/dist/hooks/command-not-found.d.ts +31 -0
- package/dist/hooks/command-not-found.js +12 -80
- package/dist/lib/command-miss.d.ts +60 -0
- package/dist/lib/command-miss.js +128 -0
- package/dist/lib/commitment-analytics.d.ts +10 -0
- package/dist/lib/commitment-analytics.js +10 -0
- package/dist/lib/config.js +6 -2
- package/dist/lib/daemon-installer.d.ts +62 -0
- package/dist/lib/daemon-installer.js +247 -0
- package/dist/lib/daemon-setup.d.ts +49 -0
- package/dist/lib/daemon-setup.js +103 -0
- package/dist/lib/daemonBroker.d.ts +7 -1
- package/dist/lib/daemonBroker.js +71 -0
- package/dist/lib/daemonHandoff.d.ts +47 -0
- package/dist/lib/daemonHandoff.js +304 -0
- package/dist/lib/exec-oversky.d.ts +30 -0
- package/dist/lib/exec-oversky.js +41 -0
- package/dist/lib/format.d.ts +7 -0
- package/dist/lib/format.js +27 -5
- package/dist/lib/login.js +22 -2
- package/dist/lib/sky-code.js +1 -1
- package/dist/lib/task-transcript.d.ts +9 -0
- package/dist/lib/task-transcript.js +13 -2
- package/dist/lib/tasks.d.ts +34 -0
- package/dist/lib/tasks.js +70 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/configRoot.d.ts +50 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/configRoot.js +67 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +3 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +21 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/releaseKeys.d.ts +87 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/releaseKeys.js +94 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/releaseManifest.d.ts +161 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/releaseManifest.js +235 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/configRoot.d.ts +50 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/configRoot.js +59 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +3 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +10 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/releaseKeys.d.ts +87 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/releaseKeys.js +91 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/releaseManifest.d.ts +161 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/releaseManifest.js +227 -0
- package/dist/node_modules/@skrr-ai/auth-core/package.json +1 -1
- package/dist/node_modules/@skrr-ai/data-provider/index.js +15 -4
- package/oclif.manifest.json +1831 -1598
- package/package.json +5 -3
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.offerDaemonSetup = offerDaemonSetup;
|
|
4
|
+
/**
|
|
5
|
+
* daemon-setup.ts — offer to set this machine up, at the moment we can.
|
|
6
|
+
*
|
|
7
|
+
* ## Why login and not `npm install`
|
|
8
|
+
*
|
|
9
|
+
* The obvious way to remove a step is a `postinstall` that installs the daemon
|
|
10
|
+
* when the CLI is installed. It cannot work, and the reason is specific rather
|
|
11
|
+
* than aesthetic: installing the daemon BINDS it to a deployment and profile,
|
|
12
|
+
* taken from the CLI's session (`bindToCliSession`, OSK-279 — a daemon on a
|
|
13
|
+
* different deployment than its CLI is the failure that binding exists to
|
|
14
|
+
* prevent). At `npm install` time there is no session.
|
|
15
|
+
*
|
|
16
|
+
* It would not even fail loudly. With no config on disk `loadConfig()` returns
|
|
17
|
+
* DEFAULT_CONFIG, whose baseURL is the DEV deployment — so a postinstall would
|
|
18
|
+
* quietly bind the machine to dev, and the first `skrr login` against
|
|
19
|
+
* production would produce exactly the split. That is the default path on a
|
|
20
|
+
* clean machine, not an edge case.
|
|
21
|
+
*
|
|
22
|
+
* Login is the first moment the answer exists. It is also where the credential
|
|
23
|
+
* handoff already runs, so this is not a new path through the program; it is
|
|
24
|
+
* the branch where that path used to give up.
|
|
25
|
+
*
|
|
26
|
+
* ## Why it asks
|
|
27
|
+
*
|
|
28
|
+
* Signing in and "run a process on this machine that can execute bash, read and
|
|
29
|
+
* write files, and drive a browser" are not the same size of decision. Doing
|
|
30
|
+
* the second as a silent consequence of the first is the class of surprise this
|
|
31
|
+
* whole area has been removing. So: asked when there is someone to ask, skipped
|
|
32
|
+
* with an instruction when there is not, and never on the opt-out.
|
|
33
|
+
*/
|
|
34
|
+
const prompt_1 = require("./prompt");
|
|
35
|
+
const daemon_binding_1 = require("./daemon-binding");
|
|
36
|
+
const exec_oversky_1 = require("./exec-oversky");
|
|
37
|
+
const daemon_installer_1 = require("./daemon-installer");
|
|
38
|
+
const ssh_detect_1 = require("./ssh-detect");
|
|
39
|
+
/**
|
|
40
|
+
* Ask, then install. Returns without touching anything when there is nobody to
|
|
41
|
+
* ask or the operator has opted out.
|
|
42
|
+
*/
|
|
43
|
+
async function offerDaemonSetup(opts = {}) {
|
|
44
|
+
const env = opts.env ?? process.env;
|
|
45
|
+
const log = opts.log ?? ((line) => console.log(line));
|
|
46
|
+
// The same switch that turns the credential handoff off. An operator who has
|
|
47
|
+
// said "do not touch my daemon" has not asked a narrower question, and a
|
|
48
|
+
// second variable for the same intent is a second thing to remember.
|
|
49
|
+
if (env.OVERSKY_SKIP_DAEMON_HANDOFF === '1' || env.SKRR_SKIP_DAEMON_HANDOFF === '1') {
|
|
50
|
+
return { status: 'skipped', detail: 'skipped by OVERSKY_SKIP_DAEMON_HANDOFF' };
|
|
51
|
+
}
|
|
52
|
+
const interactive = opts.interactive ?? !(0, ssh_detect_1.isNonInteractive)();
|
|
53
|
+
if (!interactive) {
|
|
54
|
+
// A prompt with no terminal hangs forever, which is worse than the extra
|
|
55
|
+
// step. Say which command does it instead.
|
|
56
|
+
return {
|
|
57
|
+
status: 'skipped',
|
|
58
|
+
detail: 'no terminal to ask — run `skrr daemon install` to set this machine up',
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
const confirmImpl = opts.confirmImpl ?? prompt_1.confirm;
|
|
62
|
+
log('');
|
|
63
|
+
log(' This machine has no skrr runtime. It is what lets your agents work here —');
|
|
64
|
+
log(' running commands, editing files, and driving a browser on this machine.');
|
|
65
|
+
const yes = await confirmImpl(' Set it up now?', false);
|
|
66
|
+
if (!yes) {
|
|
67
|
+
return {
|
|
68
|
+
status: 'declined',
|
|
69
|
+
detail: 'run `skrr daemon install` when you want it',
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
// Fetch the signed binary only when there is none. When the binary is already
|
|
73
|
+
// present and only the service is missing, this step is skipped entirely —
|
|
74
|
+
// downloading over a working runtime is not what "set it up" asked for.
|
|
75
|
+
const binaryPresent = opts.binaryPresent ?? Boolean((0, exec_oversky_1.findOverskyBinary)());
|
|
76
|
+
if (!binaryPresent) {
|
|
77
|
+
log(' Fetching the signed release...');
|
|
78
|
+
const fetched = await (0, daemon_installer_1.installDaemon)({ env });
|
|
79
|
+
if (!fetched.ok) {
|
|
80
|
+
return { status: 'failed', detail: fetched.error };
|
|
81
|
+
}
|
|
82
|
+
log(` Installed skrrd ${fetched.version} (${fetched.platformKey})`);
|
|
83
|
+
}
|
|
84
|
+
// Register the OS service, bound to the deployment and profile of the session
|
|
85
|
+
// that just authenticated — which is the whole reason this runs here and not
|
|
86
|
+
// at install time.
|
|
87
|
+
try {
|
|
88
|
+
const code = await (0, exec_oversky_1.execOversky)(['install', ...(0, daemon_binding_1.bindToCliSession)([])]);
|
|
89
|
+
if (code !== 0) {
|
|
90
|
+
return { status: 'failed', detail: `\`skrrd install\` exited ${code}` };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
if (err.message === 'oversky-not-installed') {
|
|
95
|
+
return {
|
|
96
|
+
status: 'failed',
|
|
97
|
+
detail: 'the runtime vanished between fetching and installing it',
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
return { status: 'failed', detail: err.message };
|
|
101
|
+
}
|
|
102
|
+
return { status: 'installed', detail: 'this machine is set up' };
|
|
103
|
+
}
|
|
@@ -80,6 +80,12 @@ export interface DaemonBrokerResult {
|
|
|
80
80
|
cliId: string;
|
|
81
81
|
userId: string;
|
|
82
82
|
email: string | null;
|
|
83
|
+
/**
|
|
84
|
+
* The refresh family this credential belongs to, named so the caller can
|
|
85
|
+
* retire its predecessors AFTER persisting it. Absent on servers that predate
|
|
86
|
+
* the mint/confirm split, where the mint still revoked eagerly.
|
|
87
|
+
*/
|
|
88
|
+
familyId?: string;
|
|
83
89
|
}
|
|
84
90
|
/**
|
|
85
91
|
* Explicit reasons the broker path could fail. These exist to keep the
|
|
@@ -87,7 +93,7 @@ export interface DaemonBrokerResult {
|
|
|
87
93
|
* fall through to PKCE; `ci_token_refused` is the one we surface
|
|
88
94
|
* because re-driving it via PKCE would not change the outcome.
|
|
89
95
|
*/
|
|
90
|
-
export type DaemonBrokerErrorReason = 'no_bootstrap' | 'stale_bootstrap' | 'bootstrap_parse' | 'base_url_mismatch' | 'daemon_not_authed' | 'ci_token_refused' | 'server_error' | 'network' | 'timeout' | 'unknown';
|
|
96
|
+
export type DaemonBrokerErrorReason = 'no_bootstrap' | 'stale_bootstrap' | 'bootstrap_parse' | 'base_url_mismatch' | 'daemon_not_authed' | 'ci_token_refused' | 'rate_limited' | 'server_error' | 'network' | 'timeout' | 'unknown';
|
|
91
97
|
export interface DaemonBrokerFailure {
|
|
92
98
|
ok: false;
|
|
93
99
|
reason: DaemonBrokerErrorReason;
|
package/dist/lib/daemonBroker.js
CHANGED
|
@@ -351,6 +351,22 @@ async function attemptDaemonBrokerLogin(opts) {
|
|
|
351
351
|
if (response.status === 503 && code === 'DAEMON_NOT_AUTHED') {
|
|
352
352
|
return { ok: false, reason: 'daemon_not_authed', detail: message, status: 503, code };
|
|
353
353
|
}
|
|
354
|
+
// 429 is TRANSIENT and its remedy is time, which makes it the one failure
|
|
355
|
+
// here that a fall-through cannot substitute for. Left in the `unknown`
|
|
356
|
+
// bucket it fell silently through to PKCE, and in a non-interactive shell
|
|
357
|
+
// that surfaced as "skrr login requires an interactive terminal" — a
|
|
358
|
+
// sentence about the wrong thing entirely. Reachable in the ordinary way: a
|
|
359
|
+
// cold machine running several skrr commands at once has every process
|
|
360
|
+
// brokering, because none of them has a token yet.
|
|
361
|
+
if (response.status === 429) {
|
|
362
|
+
return {
|
|
363
|
+
ok: false,
|
|
364
|
+
reason: 'rate_limited',
|
|
365
|
+
detail: message,
|
|
366
|
+
status: 429,
|
|
367
|
+
...(code ? { code } : {}),
|
|
368
|
+
};
|
|
369
|
+
}
|
|
354
370
|
if (response.status === 400 && code === 'BASE_URL_MISMATCH') {
|
|
355
371
|
// Older daemon binary that lacked the bootstrap.serverUrl field
|
|
356
372
|
// ended up here despite the CLI sending baseURL. Treat as the
|
|
@@ -397,8 +413,52 @@ async function attemptDaemonBrokerLogin(opts) {
|
|
|
397
413
|
cliId: parsedBody.cliId,
|
|
398
414
|
userId: parsedBody.userId,
|
|
399
415
|
email: parsedBody.email ?? null,
|
|
416
|
+
...(typeof parsedBody.familyId === 'string' ? { familyId: parsedBody.familyId } : {}),
|
|
400
417
|
};
|
|
401
418
|
}
|
|
419
|
+
/** Bound the confirm so a slow server cannot stall a login that already worked. */
|
|
420
|
+
const CONFIRM_TIMEOUT_MS = 10_000;
|
|
421
|
+
/**
|
|
422
|
+
* Retire the cli credentials this one replaces — after it is on disk, never
|
|
423
|
+
* before.
|
|
424
|
+
*
|
|
425
|
+
* The mint deliberately no longer does this. Between the server's response and
|
|
426
|
+
* the credential being usable there are two more hops (the daemon's loopback
|
|
427
|
+
* broker forwarding the body, this process writing it to the keychain), and a
|
|
428
|
+
* failure at either one used to leave the user with no working cli credential at
|
|
429
|
+
* all, recoverable only by a full browser login.
|
|
430
|
+
*
|
|
431
|
+
* Called with the NEW access token explicitly rather than through the shared
|
|
432
|
+
* client. That is the point rather than a detail: the route refuses unless the
|
|
433
|
+
* bearer's own `did` claim is the cliId being swept, so this call is not a claim
|
|
434
|
+
* that the delivery happened — it is a demonstration that it did.
|
|
435
|
+
*
|
|
436
|
+
* Best-effort and silent. The credential is already persisted and working by the
|
|
437
|
+
* time this runs; a failure leaves one extra live family, which the server's
|
|
438
|
+
* session cap bounds and the next confirm sweeps.
|
|
439
|
+
*/
|
|
440
|
+
async function confirmCliHandoff(opts) {
|
|
441
|
+
const controller = new AbortController();
|
|
442
|
+
const timer = setTimeout(() => controller.abort(), CONFIRM_TIMEOUT_MS);
|
|
443
|
+
try {
|
|
444
|
+
await fetch(`${opts.serverOrigin}/api/daemons/cli-handoff/confirm`, {
|
|
445
|
+
method: 'POST',
|
|
446
|
+
headers: {
|
|
447
|
+
Authorization: `Bearer ${opts.accessToken}`,
|
|
448
|
+
'Content-Type': 'application/json',
|
|
449
|
+
Accept: 'application/json',
|
|
450
|
+
},
|
|
451
|
+
body: JSON.stringify({ cliId: opts.cliId, familyId: opts.familyId }),
|
|
452
|
+
signal: controller.signal,
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
catch {
|
|
456
|
+
/* bookkeeping — the credential is already delivered */
|
|
457
|
+
}
|
|
458
|
+
finally {
|
|
459
|
+
clearTimeout(timer);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
402
462
|
/**
|
|
403
463
|
* Helper: coerce an ISO-string or numeric expiry to epoch ms for
|
|
404
464
|
* `writeToBackend`. Returns undefined on missing / unparseable input.
|
|
@@ -436,6 +496,17 @@ async function attemptDaemonBrokerLoginAndPersist(opts) {
|
|
|
436
496
|
? { refreshExpiresAt: expiryToEpochMs(outcome.refreshExpiresAt) }
|
|
437
497
|
: {}),
|
|
438
498
|
}, { serverOrigin, clearReauth: true });
|
|
499
|
+
// Only now: the credential is on disk, so "the predecessor dies iff the
|
|
500
|
+
// replacement is in place" holds. A server that predates the split ships no
|
|
501
|
+
// `familyId` and has already revoked eagerly — nothing to do there.
|
|
502
|
+
if (outcome.familyId) {
|
|
503
|
+
await confirmCliHandoff({
|
|
504
|
+
serverOrigin,
|
|
505
|
+
accessToken: outcome.accessToken,
|
|
506
|
+
cliId: outcome.cliId,
|
|
507
|
+
familyId: outcome.familyId,
|
|
508
|
+
});
|
|
509
|
+
}
|
|
439
510
|
return outcome;
|
|
440
511
|
}
|
|
441
512
|
// ---------------------------------------------------------------------
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hand the local daemon a credential of its own, right after `skrr login`.
|
|
3
|
+
*
|
|
4
|
+
* The inverse of the daemon-as-broker path that already ships. That one lets an
|
|
5
|
+
* authenticated DAEMON mint a cli-scope credential so the CLI can skip the
|
|
6
|
+
* browser; this lets an authenticated HUMAN mint a daemon-scope one so the
|
|
7
|
+
* machine is logged in by the same act. The direction added here is the safer of
|
|
8
|
+
* the two: daemon scope is deliberately lower-privileged than cli scope
|
|
9
|
+
* (`requireHumanScope` refuses it from billing, account management, password
|
|
10
|
+
* change and deletion), so this is a de-escalation, while the shipping direction
|
|
11
|
+
* is an escalation.
|
|
12
|
+
*
|
|
13
|
+
* Design record: docs/architecture/machine-login-broker-inversion-2026-09-03.md
|
|
14
|
+
*
|
|
15
|
+
* Three properties are deliberate:
|
|
16
|
+
*
|
|
17
|
+
* - **Best-effort, never fatal.** `skrr login` has already SUCCEEDED by the time
|
|
18
|
+
* this runs. A failure here must not fail the login or discard the credential
|
|
19
|
+
* the user just earned — it degrades to exactly the old behaviour, with a hint.
|
|
20
|
+
* - **Skipped when no daemon service is installed.** There is nothing to hand a
|
|
21
|
+
* credential to, and minting one anyway leaves an orphaned daemon-scope family
|
|
22
|
+
* on the account. A binary on PATH is not a service, so this is decided by the
|
|
23
|
+
* daemon's own answer about where its id came from — not by whether `skrrd`
|
|
24
|
+
* exists.
|
|
25
|
+
* - **The bundle goes over STDIN**, never a file. It is a live credential; a temp
|
|
26
|
+
* file would put it at rest with a lifetime nobody owns.
|
|
27
|
+
* - **The predecessor is retired only after the new credential is written.** The
|
|
28
|
+
* mint no longer revokes anything; `confirmDaemonHandoff` does, once the daemon
|
|
29
|
+
* has accepted. So a handoff that fails leaves the machine exactly as it was,
|
|
30
|
+
* rather than signed out.
|
|
31
|
+
*/
|
|
32
|
+
export interface DaemonHandoffOutcome {
|
|
33
|
+
status: 'delivered' | 'skipped' | 'failed';
|
|
34
|
+
detail: string;
|
|
35
|
+
/**
|
|
36
|
+
* Why it skipped, for a caller that must act on the difference.
|
|
37
|
+
*
|
|
38
|
+
* `detail` is a sentence for a human and is expected to be reworded; the
|
|
39
|
+
* caller deciding whether to OFFER TO INSTALL a daemon cannot key off that.
|
|
40
|
+
* The two absent cases are separated because they need different work — one
|
|
41
|
+
* needs a binary fetched, the other only needs the service registered.
|
|
42
|
+
*/
|
|
43
|
+
reason?: 'opted-out' | 'brokered' | 'no-binary' | 'unsupported-daemon' | 'no-service';
|
|
44
|
+
}
|
|
45
|
+
export declare function handOffToLocalDaemon(env?: NodeJS.ProcessEnv, opts?: {
|
|
46
|
+
loginFlow?: string;
|
|
47
|
+
}): Promise<DaemonHandoffOutcome>;
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.handOffToLocalDaemon = handOffToLocalDaemon;
|
|
4
|
+
const node_child_process_1 = require("node:child_process");
|
|
5
|
+
const data_provider_1 = require("@skrr-ai/data-provider");
|
|
6
|
+
const config_1 = require("./config");
|
|
7
|
+
const exec_oversky_1 = require("./exec-oversky");
|
|
8
|
+
const keychain_1 = require("./keychain");
|
|
9
|
+
/**
|
|
10
|
+
* Ceilings, not expectations. Both calls finish in milliseconds when anything is
|
|
11
|
+
* working; these exist so that when something is NOT working the login ends.
|
|
12
|
+
*
|
|
13
|
+
* The failure this prevents is the worst one available here: `skrr login` has
|
|
14
|
+
* already printed "Login successful", and an unbounded wait on a child process
|
|
15
|
+
* leaves the user staring at a terminal that will never return, with no way to
|
|
16
|
+
* tell a hung handoff from a slow one. A blocked keychain prompt, a daemon
|
|
17
|
+
* binary wedged on a lock, an NFS home directory — none of them are exotic, and
|
|
18
|
+
* all of them used to hang the command.
|
|
19
|
+
*
|
|
20
|
+
* The accept side gets the longer budget because it may have to unlock a
|
|
21
|
+
* keychain. Killing it mid-write is the cost, and it is the lesser one: the
|
|
22
|
+
* credential write is a few milliseconds at the end of that window, so a timeout
|
|
23
|
+
* overwhelmingly lands BEFORE it rather than during it, and `skrr daemon login`
|
|
24
|
+
* recovers either way.
|
|
25
|
+
*/
|
|
26
|
+
const PROBE_TIMEOUT_MS = 15_000;
|
|
27
|
+
const ACCEPT_TIMEOUT_MS = 60_000;
|
|
28
|
+
function runOversky(binary, args, opts) {
|
|
29
|
+
const { input, timeoutMs } = opts;
|
|
30
|
+
return new Promise((resolve) => {
|
|
31
|
+
// A Windows npm-global install ships a `.cmd` shim, which `spawn` cannot
|
|
32
|
+
// execute without a shell — Node fires ENOENT instead. `execOversky` has
|
|
33
|
+
// handled this since it was written; this function did not, so the handoff
|
|
34
|
+
// failed on EVERY Windows login and reported it as the daemon refusing the
|
|
35
|
+
// bundle. Restricted to the shim case so real binaries keep the argv-array
|
|
36
|
+
// exec path, which needs no quoting rules to be right.
|
|
37
|
+
const isWinShim = process.platform === 'win32' && /\.(cmd|bat)$/i.test(binary);
|
|
38
|
+
const child = (0, node_child_process_1.spawn)(binary, args, {
|
|
39
|
+
// stderr is CAPTURED, not discarded. Discarding it cost a debugging cycle
|
|
40
|
+
// on the first real run: the daemon exited non-zero with
|
|
41
|
+
// `OVERSKY_KEK_REQUIRED`, and all the user saw was "the daemon refused the
|
|
42
|
+
// handoff" — an outcome with its reason thrown away. A wrapper that hides
|
|
43
|
+
// the wrapped program's error is worse than one that does not wrap.
|
|
44
|
+
stdio: [input === undefined ? 'ignore' : 'pipe', 'pipe', 'pipe'],
|
|
45
|
+
...(isWinShim ? { shell: true } : {}),
|
|
46
|
+
});
|
|
47
|
+
let stdout = '';
|
|
48
|
+
let stderr = '';
|
|
49
|
+
let settled = false;
|
|
50
|
+
const settle = (value) => {
|
|
51
|
+
if (settled)
|
|
52
|
+
return;
|
|
53
|
+
settled = true;
|
|
54
|
+
clearTimeout(timer);
|
|
55
|
+
resolve(value);
|
|
56
|
+
};
|
|
57
|
+
const timer = setTimeout(() => {
|
|
58
|
+
child.kill('SIGKILL');
|
|
59
|
+
settle({
|
|
60
|
+
ok: false,
|
|
61
|
+
stdout,
|
|
62
|
+
stderr: `timed out after ${Math.round(timeoutMs / 1000)}s`,
|
|
63
|
+
});
|
|
64
|
+
}, timeoutMs);
|
|
65
|
+
// Never hold the event loop open on the timer alone.
|
|
66
|
+
if (typeof timer.unref === 'function')
|
|
67
|
+
timer.unref();
|
|
68
|
+
child.stdout?.on('data', (c) => (stdout += String(c)));
|
|
69
|
+
child.stderr?.on('data', (c) => (stderr += String(c)));
|
|
70
|
+
child.on('error', (err) => settle({ ok: false, stdout, stderr: err.message }));
|
|
71
|
+
child.on('close', (code) => settle({ ok: code === 0, stdout, stderr }));
|
|
72
|
+
if (input !== undefined) {
|
|
73
|
+
// An EPIPE here is the child having already exited — its own exit code and
|
|
74
|
+
// stderr are the real diagnosis, and an unhandled stream error would
|
|
75
|
+
// instead crash the CLI one line after a successful login.
|
|
76
|
+
child.stdin?.on('error', () => {
|
|
77
|
+
/* reported by the close handler */
|
|
78
|
+
});
|
|
79
|
+
child.stdin?.end(input);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The profile flags to pass to `skrrd`, so both spawns land in the same tenancy
|
|
85
|
+
* the CLI is signed in to.
|
|
86
|
+
*
|
|
87
|
+
* A profile is the daemon's tenancy boundary — separate config, state, keychain
|
|
88
|
+
* namespace and service label. The CLI resolves its own; the daemon resolves its
|
|
89
|
+
* own, from `--profile` → `OVERSKY_PROFILE` → its `profile.lock` → `default`.
|
|
90
|
+
* Passing nothing let those two answers differ, and the failure was silent in the
|
|
91
|
+
* damaging direction: `skrr --profile work login` would mint against the id
|
|
92
|
+
* printed by whichever profile the daemon's lock happened to name, then write a
|
|
93
|
+
* work-profile credential over THAT profile's store.
|
|
94
|
+
*
|
|
95
|
+
* `skrr daemon login` already binds the profile this way (`bindToCliSession`);
|
|
96
|
+
* this is the same rule for the path that runs without being asked.
|
|
97
|
+
*/
|
|
98
|
+
function profileArgs() {
|
|
99
|
+
const profile = (0, keychain_1.getActiveProfile)();
|
|
100
|
+
return profile && profile !== keychain_1.DEFAULT_PROFILE ? ['--profile', profile] : [];
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Parse the machine-readable line `accept-handoff --print-id` writes to stderr.
|
|
104
|
+
*
|
|
105
|
+
* Absent on a daemon that predates it, which is a normal state during a rollout
|
|
106
|
+
* and not an error — the caller degrades to the older, less careful behaviour
|
|
107
|
+
* rather than refusing.
|
|
108
|
+
*/
|
|
109
|
+
function parseIdProvenance(stderr) {
|
|
110
|
+
const line = stderr.split('\n').find((l) => l.includes('skrrd:accept-handoff'));
|
|
111
|
+
if (!line)
|
|
112
|
+
return {};
|
|
113
|
+
const idSource = /id-source=([A-Za-z0-9._:-]+)/.exec(line)?.[1];
|
|
114
|
+
const machineUuid = /machine-uuid=([A-Za-z0-9._:-]+)/.exec(line)?.[1];
|
|
115
|
+
// Any non-space run: this one is a URL, and a URL contains characters the
|
|
116
|
+
// other two never do.
|
|
117
|
+
const daemonServer = /server=(\S+)/.exec(line)?.[1];
|
|
118
|
+
return { idSource, machineUuid, daemonServer };
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* What actually changed on this machine, said out loud.
|
|
122
|
+
*
|
|
123
|
+
* A handoff can re-point the machine to a different ACCOUNT (you signed in as
|
|
124
|
+
* someone else) or a different DEPLOYMENT (your daemon was on dev, your CLI is
|
|
125
|
+
* on prod). Both are the feature working — the machine follows the human, and
|
|
126
|
+
* moving a dev-bound daemon to prod is the exact story this path was written for
|
|
127
|
+
* — and both are state changes an operator should watch happen rather than
|
|
128
|
+
* discover later, when a daemon they thought was on dev answers from prod.
|
|
129
|
+
*
|
|
130
|
+
* Degrades cleanly: an older server sends no account, an older daemon reports no
|
|
131
|
+
* server, and the sentence simply says less rather than guessing.
|
|
132
|
+
*/
|
|
133
|
+
function describeDelivery(opts) {
|
|
134
|
+
const parts = [];
|
|
135
|
+
parts.push(opts.accountLabel
|
|
136
|
+
? `now signed in as ${opts.accountLabel} on ${(0, config_1.describeDeployment)(opts.cliBaseURL)}`
|
|
137
|
+
: 'the local daemon now has its own credential');
|
|
138
|
+
if (opts.daemonServer &&
|
|
139
|
+
canonicalServer(opts.daemonServer) !== canonicalServer(opts.cliBaseURL)) {
|
|
140
|
+
parts.push(`moved from ${(0, config_1.describeDeployment)(opts.daemonServer)}`);
|
|
141
|
+
}
|
|
142
|
+
return parts.join(', ');
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Compare deployments, not spellings.
|
|
146
|
+
*
|
|
147
|
+
* `https://oversky.ai` and `https://skrr.ai` are one deployment under two names,
|
|
148
|
+
* and the first is still what every daemon installed before the cutover carries.
|
|
149
|
+
* Trimming and lowercasing alone would report a move on every login from every
|
|
150
|
+
* one of those machines — a warning that is wrong for the largest group of users
|
|
151
|
+
* who would see it, which is how a signal becomes noise. `canonicalizeKnownBaseURL`
|
|
152
|
+
* is the fold the rest of the CLI already compares through.
|
|
153
|
+
*/
|
|
154
|
+
function canonicalServer(url) {
|
|
155
|
+
return (0, config_1.canonicalizeKnownBaseURL)(url).trim().replace(/\/+$/, '').toLowerCase();
|
|
156
|
+
}
|
|
157
|
+
/** The most useful line of a failure, so a remedy can name the cause. */
|
|
158
|
+
function firstMeaningfulLine(text) {
|
|
159
|
+
const line = text
|
|
160
|
+
.split('\n')
|
|
161
|
+
.map((l) => l.trim())
|
|
162
|
+
.find((l) => l &&
|
|
163
|
+
!l.startsWith('at ') &&
|
|
164
|
+
!/^[{}]/.test(l) &&
|
|
165
|
+
// The provenance line is a protocol between these two programs, not a
|
|
166
|
+
// diagnosis. Reporting it as one would answer "why did this fail?" with
|
|
167
|
+
// a machine's own bookkeeping.
|
|
168
|
+
!l.includes('skrrd:accept-handoff'));
|
|
169
|
+
return line ? line.slice(0, 160) : 'no output';
|
|
170
|
+
}
|
|
171
|
+
async function handOffToLocalDaemon(env = process.env, opts = {}) {
|
|
172
|
+
// Mirrors `OVERSKY_SKIP_DAEMON_BROKER` on the other direction. An operator
|
|
173
|
+
// deliberately running a daemon against a different deployment than their CLI is
|
|
174
|
+
// doing a legitimate thing, and this must not overrule it.
|
|
175
|
+
if (env.OVERSKY_SKIP_DAEMON_HANDOFF === '1' || env.SKRR_SKIP_DAEMON_HANDOFF === '1') {
|
|
176
|
+
return {
|
|
177
|
+
status: 'skipped',
|
|
178
|
+
reason: 'opted-out',
|
|
179
|
+
detail: 'skipped by OVERSKY_SKIP_DAEMON_HANDOFF',
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
// The daemon brokered this login, which means it just used its OWN credential
|
|
183
|
+
// against this server and that credential works. There is nothing to fix.
|
|
184
|
+
//
|
|
185
|
+
// Running anyway is not merely redundant, it is destructive: it mints a
|
|
186
|
+
// replacement, writes it over the working one, revokes the family the daemon is
|
|
187
|
+
// authenticated with, and restarts the process — killing whatever sessions it
|
|
188
|
+
// was running. `skrr login` on a healthy machine is the path where the least
|
|
189
|
+
// should happen, and it was the path where the most did.
|
|
190
|
+
if (opts.loginFlow === 'daemon-broker') {
|
|
191
|
+
return {
|
|
192
|
+
status: 'skipped',
|
|
193
|
+
reason: 'brokered',
|
|
194
|
+
detail: 'the local daemon brokered this login, so it already has a working credential',
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
const binary = (0, exec_oversky_1.findOverskyBinary)();
|
|
198
|
+
if (!binary) {
|
|
199
|
+
return { status: 'skipped', reason: 'no-binary', detail: 'no local daemon installed' };
|
|
200
|
+
}
|
|
201
|
+
// An older daemon has no `accept-handoff`, and spawning it would fail with
|
|
202
|
+
// commander's "unknown command" rather than anything a reader could act on. A
|
|
203
|
+
// CLI that outruns the daemon on the machine is the normal case during a
|
|
204
|
+
// staged rollout, not an error.
|
|
205
|
+
const supported = await (0, exec_oversky_1.overskySupportsSubcommand)('accept-handoff');
|
|
206
|
+
if (supported === false) {
|
|
207
|
+
return {
|
|
208
|
+
status: 'skipped',
|
|
209
|
+
reason: 'unsupported-daemon',
|
|
210
|
+
detail: 'the installed daemon predates `accept-handoff`; run `skrr daemon login` once',
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
// Ask the daemon which id it will bind, rather than deriving it here. The id
|
|
214
|
+
// comes from cwd + profile, and a second copy of that derivation in the CLI
|
|
215
|
+
// would drift — producing a credential minted for a daemon that does not exist,
|
|
216
|
+
// which is indistinguishable from a login that simply did not work.
|
|
217
|
+
const printed = await runOversky(binary, ['accept-handoff', '--print-id', ...profileArgs()], {
|
|
218
|
+
timeoutMs: PROBE_TIMEOUT_MS,
|
|
219
|
+
});
|
|
220
|
+
const daemonId = printed.ok ? printed.stdout.trim() : '';
|
|
221
|
+
if (!daemonId) {
|
|
222
|
+
return {
|
|
223
|
+
status: 'failed',
|
|
224
|
+
detail: `could not read the local daemon id — ${firstMeaningfulLine(printed.stderr)}`,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
const { idSource, machineUuid, daemonServer } = parseIdProvenance(printed.stderr);
|
|
228
|
+
// A well-formed id is not the same as a real daemon. With no service installed
|
|
229
|
+
// the daemon derives one from whatever directory this process is in, so the id
|
|
230
|
+
// above names a daemon that does not exist and will not exist under that id if
|
|
231
|
+
// one is ever installed from a different directory. Minting for it leaves an
|
|
232
|
+
// orphaned daemon-scope family on the account and hands the credential to
|
|
233
|
+
// nothing — which is the case the design record says to skip, and which the
|
|
234
|
+
// binary-presence check alone never detected because a binary on PATH is not a
|
|
235
|
+
// service.
|
|
236
|
+
//
|
|
237
|
+
// Only a daemon that reports its provenance can be skipped this way. An older
|
|
238
|
+
// one says nothing, and is left on the previous behaviour rather than refused.
|
|
239
|
+
if (idSource === 'cwd') {
|
|
240
|
+
return {
|
|
241
|
+
status: 'skipped',
|
|
242
|
+
reason: 'no-service',
|
|
243
|
+
detail: 'no daemon service is installed on this machine',
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
let bundle;
|
|
247
|
+
try {
|
|
248
|
+
bundle = (await data_provider_1.dataService.mintDaemonHandoff({
|
|
249
|
+
daemonId,
|
|
250
|
+
...(machineUuid ? { machineUuid } : {}),
|
|
251
|
+
}));
|
|
252
|
+
}
|
|
253
|
+
catch (err) {
|
|
254
|
+
return { status: 'failed', detail: err.message };
|
|
255
|
+
}
|
|
256
|
+
const payload = JSON.stringify({
|
|
257
|
+
refreshToken: bundle.refreshToken,
|
|
258
|
+
refreshExpiresAt: bundle.refreshExpiresAt,
|
|
259
|
+
serverUrl: (0, config_1.loadConfig)().baseURL,
|
|
260
|
+
daemonId,
|
|
261
|
+
});
|
|
262
|
+
// No restart from here. `accept-handoff` runs the runtime's own
|
|
263
|
+
// `restartDisconnectedServiceAfterLogin` after it writes the credential — the
|
|
264
|
+
// same reconcile every other credential-write path in the daemon uses — and it
|
|
265
|
+
// is the better of the two: it checks that a service is installed and running,
|
|
266
|
+
// refuses to yank a daemon another profile owns, and gives the in-process
|
|
267
|
+
// watcher a window to converge before reaching for the heavy hammer.
|
|
268
|
+
//
|
|
269
|
+
// A second restart out here does not add a guarantee, it adds an outage. Each
|
|
270
|
+
// one kills in-flight Claude/Codex sessions and browser CDP attachments, and
|
|
271
|
+
// the second would land on a process that is seconds into starting up.
|
|
272
|
+
const accepted = await runOversky(binary, ['accept-handoff', ...profileArgs()], {
|
|
273
|
+
input: payload,
|
|
274
|
+
timeoutMs: ACCEPT_TIMEOUT_MS,
|
|
275
|
+
});
|
|
276
|
+
if (!accepted.ok) {
|
|
277
|
+
// Deliberately NOT confirming. The machine still holds the credential it had
|
|
278
|
+
// before this ran, and it still works — the mint no longer revokes anything,
|
|
279
|
+
// so the cost of a refused handoff is one unused family that the next
|
|
280
|
+
// successful login sweeps, rather than a signed-out machine.
|
|
281
|
+
return { status: 'failed', detail: firstMeaningfulLine(accepted.stderr) };
|
|
282
|
+
}
|
|
283
|
+
// Only now is the predecessor retired: the daemon has written the new
|
|
284
|
+
// credential to its own store, so "the old one dies iff the new one is in
|
|
285
|
+
// place" holds. Best-effort — a confirm that does not land leaves one extra
|
|
286
|
+
// live family, bounded by the server's concurrent-session cap and swept by the
|
|
287
|
+
// next confirm, which is a far cheaper failure than the one it replaces.
|
|
288
|
+
if (bundle.familyId) {
|
|
289
|
+
try {
|
|
290
|
+
await data_provider_1.dataService.confirmDaemonHandoff({ daemonId, familyId: bundle.familyId });
|
|
291
|
+
}
|
|
292
|
+
catch {
|
|
293
|
+
/* the credential is delivered; the sweep is bookkeeping */
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return {
|
|
297
|
+
status: 'delivered',
|
|
298
|
+
detail: describeDelivery({
|
|
299
|
+
accountLabel: bundle.accountLabel,
|
|
300
|
+
daemonServer,
|
|
301
|
+
cliBaseURL: (0, config_1.loadConfig)().baseURL,
|
|
302
|
+
}),
|
|
303
|
+
};
|
|
304
|
+
}
|
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
/** Runtime binary names, current first. */
|
|
2
2
|
export declare const RUNTIME_BINARY_NAMES: readonly ["skrrd", "oversky"];
|
|
3
|
+
/**
|
|
4
|
+
* The sentence a `skrr daemon` pass-through appends to its description.
|
|
5
|
+
*
|
|
6
|
+
* OSK-296 made these commands forward their flags to `skrrd` instead of
|
|
7
|
+
* rejecting them, and stopped there. oclif renders the FLAGS section of help
|
|
8
|
+
* from a command's declared flags, and a pass-through declares none — so help
|
|
9
|
+
* kept describing the old, broken behaviour where these commands took nothing,
|
|
10
|
+
* while `skrr daemon status --profile work` quietly worked. The entire flag
|
|
11
|
+
* surface of the wrapped binary became reachable and undiscoverable in the
|
|
12
|
+
* same commit.
|
|
13
|
+
*
|
|
14
|
+
* `skrr code` hit this first and fixed BOTH halves: its description says out
|
|
15
|
+
* loud that engine flags are not listed there and where to find them. This is
|
|
16
|
+
* that sentence, for the other wrapper, in one place so eight commands cannot
|
|
17
|
+
* drift into eight wordings.
|
|
18
|
+
*
|
|
19
|
+
* It names `skrrd --help` separately because `--profile` is a GLOBAL option on
|
|
20
|
+
* the runtime, so a reader sent only to the subcommand's help would not find
|
|
21
|
+
* the flag they are most likely to want.
|
|
22
|
+
*
|
|
23
|
+
* This is a command's `description`, and each command keeps a short `summary`
|
|
24
|
+
* beside it. oclif renders `summary` in the topic listing and `description`
|
|
25
|
+
* only in the command's own help, so the note reaches someone asking about
|
|
26
|
+
* THIS command without turning `skrr daemon --help` into eight paragraphs of
|
|
27
|
+
* the same boilerplate — which is what it did when this text was appended to
|
|
28
|
+
* `description` alone.
|
|
29
|
+
*/
|
|
30
|
+
export declare function forwardedFlagsNote(verb: string, opts?: {
|
|
31
|
+
binds?: boolean;
|
|
32
|
+
}): string;
|
|
3
33
|
/** Resolve the absolute path of `oversky` if it exists, else return null. */
|
|
4
34
|
export declare function findOverskyBinary(): string | null;
|
|
5
35
|
/**
|
package/dist/lib/exec-oversky.js
CHANGED
|
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.RUNTIME_BINARY_NAMES = void 0;
|
|
7
|
+
exports.forwardedFlagsNote = forwardedFlagsNote;
|
|
7
8
|
exports.findOverskyBinary = findOverskyBinary;
|
|
8
9
|
exports.notInstalledMessage = notInstalledMessage;
|
|
9
10
|
exports.execOversky = execOversky;
|
|
@@ -31,6 +32,46 @@ const node_fs_1 = require("node:fs");
|
|
|
31
32
|
const node_path_1 = __importDefault(require("node:path"));
|
|
32
33
|
/** Runtime binary names, current first. */
|
|
33
34
|
exports.RUNTIME_BINARY_NAMES = ['skrrd', 'oversky'];
|
|
35
|
+
/**
|
|
36
|
+
* The sentence a `skrr daemon` pass-through appends to its description.
|
|
37
|
+
*
|
|
38
|
+
* OSK-296 made these commands forward their flags to `skrrd` instead of
|
|
39
|
+
* rejecting them, and stopped there. oclif renders the FLAGS section of help
|
|
40
|
+
* from a command's declared flags, and a pass-through declares none — so help
|
|
41
|
+
* kept describing the old, broken behaviour where these commands took nothing,
|
|
42
|
+
* while `skrr daemon status --profile work` quietly worked. The entire flag
|
|
43
|
+
* surface of the wrapped binary became reachable and undiscoverable in the
|
|
44
|
+
* same commit.
|
|
45
|
+
*
|
|
46
|
+
* `skrr code` hit this first and fixed BOTH halves: its description says out
|
|
47
|
+
* loud that engine flags are not listed there and where to find them. This is
|
|
48
|
+
* that sentence, for the other wrapper, in one place so eight commands cannot
|
|
49
|
+
* drift into eight wordings.
|
|
50
|
+
*
|
|
51
|
+
* It names `skrrd --help` separately because `--profile` is a GLOBAL option on
|
|
52
|
+
* the runtime, so a reader sent only to the subcommand's help would not find
|
|
53
|
+
* the flag they are most likely to want.
|
|
54
|
+
*
|
|
55
|
+
* This is a command's `description`, and each command keeps a short `summary`
|
|
56
|
+
* beside it. oclif renders `summary` in the topic listing and `description`
|
|
57
|
+
* only in the command's own help, so the note reaches someone asking about
|
|
58
|
+
* THIS command without turning `skrr daemon --help` into eight paragraphs of
|
|
59
|
+
* the same boilerplate — which is what it did when this text was appended to
|
|
60
|
+
* `description` alone.
|
|
61
|
+
*/
|
|
62
|
+
function forwardedFlagsNote(verb, opts = {}) {
|
|
63
|
+
const base = `Flags are forwarded to \`skrrd ${verb}\` unchanged, so they are not listed here —` +
|
|
64
|
+
` run \`skrrd ${verb} --help\` for this verb's own flags, or \`skrrd --help\` for` +
|
|
65
|
+
' global ones such as `--profile`.';
|
|
66
|
+
// `install` and `login` are not pure pass-throughs: they fill in the server
|
|
67
|
+
// and profile from the CLI's own session when the caller named neither, so a
|
|
68
|
+
// daemon cannot silently end up on a different deployment than the CLI that
|
|
69
|
+
// installed it (OSK-279). Saying so is the difference between a flag you may
|
|
70
|
+
// pass and a flag that is already being passed for you.
|
|
71
|
+
return opts.binds
|
|
72
|
+
? `${base} \`--server\` and \`--profile\` are filled in from your CLI session unless you pass them.`
|
|
73
|
+
: base;
|
|
74
|
+
}
|
|
34
75
|
/** Extra well-known install locations to check when PATH lookup misses. */
|
|
35
76
|
const FALLBACK_PATHS = exports.RUNTIME_BINARY_NAMES.flatMap((name) => [
|
|
36
77
|
`/opt/homebrew/bin/${name}`,
|