@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,128 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* What to say when a command id does not resolve — ONE description, shared by
|
|
4
|
+
* every surface that can produce a miss.
|
|
5
|
+
*
|
|
6
|
+
* There are two such surfaces, and they used to disagree. `skrr daemon reload`
|
|
7
|
+
* goes through the `command_not_found` hook and gets a message naming the real
|
|
8
|
+
* command, the mistake, and the next step. `skrr daemon reload --help` goes
|
|
9
|
+
* through oclif's HELP command, which does its own lookup and never reaches
|
|
10
|
+
* that hook — so it answered `Command daemon:reload not found.`: raw
|
|
11
|
+
* boilerplate naming an internal colon-form id the user never typed and cannot
|
|
12
|
+
* type, with no route forward.
|
|
13
|
+
*
|
|
14
|
+
* The audience is what makes that the wrong way round. Someone exploring a
|
|
15
|
+
* service-manager CLI types `--help` on a guessed subcommand FIRST. So the
|
|
16
|
+
* readers most likely to hit a miss were exactly the readers the good message
|
|
17
|
+
* was written for, and they systematically got the bad one.
|
|
18
|
+
*
|
|
19
|
+
* Keeping the text here rather than in either caller is the point: a fix to one
|
|
20
|
+
* phrasing reaches both, and neither surface can quietly drift from the other.
|
|
21
|
+
*/
|
|
22
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
+
exports.editDistance = editDistance;
|
|
24
|
+
exports.nearestCommands = nearestCommands;
|
|
25
|
+
exports.describeCommandMiss = describeCommandMiss;
|
|
26
|
+
exports.lookupFromConfig = lookupFromConfig;
|
|
27
|
+
/** A uuid-ish token, which is what a stray space id looks like. */
|
|
28
|
+
const LOOKS_LIKE_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
29
|
+
/** Cheap edit distance, bounded — this runs only on a miss. */
|
|
30
|
+
function editDistance(a, b) {
|
|
31
|
+
const rows = a.length + 1;
|
|
32
|
+
const cols = b.length + 1;
|
|
33
|
+
let prev = Array.from({ length: cols }, (_, j) => j);
|
|
34
|
+
for (let i = 1; i < rows; i += 1) {
|
|
35
|
+
const cur = [i, ...Array(cols - 1).fill(0)];
|
|
36
|
+
for (let j = 1; j < cols; j += 1) {
|
|
37
|
+
cur[j] = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
38
|
+
}
|
|
39
|
+
prev = cur;
|
|
40
|
+
}
|
|
41
|
+
return prev[cols - 1];
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The closest real command ids, so a typo gets a route forward rather than
|
|
45
|
+
* only a verdict.
|
|
46
|
+
*/
|
|
47
|
+
function nearestCommands(id, all, limit = 3) {
|
|
48
|
+
// Deduped, because oclif registers a TOPIC for a command path as well as the
|
|
49
|
+
// command itself — `daemon:status` is in `commandIDs` and in `topics`, so the
|
|
50
|
+
// combined candidate list held it twice and a near-miss was answered with
|
|
51
|
+
// "Did you mean: `skrr daemon status`, `skrr daemon status`, …?", spending
|
|
52
|
+
// two of the three suggestion slots saying one thing.
|
|
53
|
+
return ([...new Set(all)]
|
|
54
|
+
.map((candidate) => ({ candidate, d: editDistance(id, candidate) }))
|
|
55
|
+
// A third of the length keeps this to genuine near-misses; without a bound
|
|
56
|
+
// every miss suggests three unrelated commands, which is noise wearing the
|
|
57
|
+
// shape of help.
|
|
58
|
+
.filter(({ d }) => d <= Math.max(2, Math.floor(id.length / 3)))
|
|
59
|
+
.sort((x, y) => x.d - y.d)
|
|
60
|
+
.slice(0, limit)
|
|
61
|
+
.map(({ candidate }) => candidate));
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The message for a colon-joined id that did not resolve.
|
|
65
|
+
*
|
|
66
|
+
* Two shapes, in order:
|
|
67
|
+
*
|
|
68
|
+
* 1. A PREFIX of the id is a real command, so the extra tokens are arguments
|
|
69
|
+
* it did not want. Longest prefix first, because `a:b:c` should report
|
|
70
|
+
* `a b`, not `a`.
|
|
71
|
+
* 2. Nothing resolved, so the command genuinely is not there — say so, and
|
|
72
|
+
* suggest the nearest siblings under the same topic before falling back to
|
|
73
|
+
* the whole command list.
|
|
74
|
+
*/
|
|
75
|
+
function describeCommandMiss(config, id) {
|
|
76
|
+
const parts = String(id ?? '')
|
|
77
|
+
.split(':')
|
|
78
|
+
.filter(Boolean);
|
|
79
|
+
for (let take = parts.length - 1; take >= 1; take -= 1) {
|
|
80
|
+
const found = config.findCommand(parts.slice(0, take).join(':'));
|
|
81
|
+
if (!found)
|
|
82
|
+
continue;
|
|
83
|
+
const extras = parts.slice(take);
|
|
84
|
+
const spoken = `${config.bin} ${parts.slice(0, take).join(' ')}`;
|
|
85
|
+
// Suggest the flag only when the command actually has it, so the hint can
|
|
86
|
+
// never point at something that does not exist.
|
|
87
|
+
const flags = Object.keys(found.flags ?? {});
|
|
88
|
+
const hint = extras.some((e) => LOOKS_LIKE_ID.test(e)) && flags.includes('space')
|
|
89
|
+
? ` Pass it as \`--space ${extras[0]}\`.`
|
|
90
|
+
: flags.includes('space')
|
|
91
|
+
? ' It takes `--space <id>`.'
|
|
92
|
+
: '';
|
|
93
|
+
const plural = extras.length === 1 ? 'argument' : 'arguments';
|
|
94
|
+
return (`\`${spoken}\` exists, but does not take the positional ${plural} ` +
|
|
95
|
+
`${extras.map((e) => `\`${e}\``).join(' ')}.${hint}\n` +
|
|
96
|
+
`See \`${spoken} --help\`.`);
|
|
97
|
+
}
|
|
98
|
+
// Topic names are candidates too, so a near-miss on a bare topic gets routed
|
|
99
|
+
// to the topic rather than nothing (OSK-4869): `skrr harness` (singular) is
|
|
100
|
+
// one edit from the `harnesses` topic, but that topic owns no command whose
|
|
101
|
+
// id is close to `harness`, so a command-id-only search suggests nothing.
|
|
102
|
+
//
|
|
103
|
+
// Siblings first: `commitments:archive:<id>` is a long way from every real
|
|
104
|
+
// command id, so a whole-string comparison finds nothing and the reader is
|
|
105
|
+
// told only that they are wrong — when the useful answer is "that topic
|
|
106
|
+
// exists and here is what it does have".
|
|
107
|
+
const siblings = config.commandIDs.filter((c) => c.startsWith(`${parts[0]}:`));
|
|
108
|
+
const suggestions = siblings.length
|
|
109
|
+
? nearestCommands(parts.slice(0, 2).join(':'), siblings)
|
|
110
|
+
: nearestCommands(parts.join(':'), [...config.commandIDs, ...config.topicNames]);
|
|
111
|
+
return (`\`${config.bin} ${parts.join(' ')}\` is not a command.` +
|
|
112
|
+
(suggestions.length
|
|
113
|
+
? `\nDid you mean: ${suggestions
|
|
114
|
+
.map((s) => `\`${config.bin} ${s.split(':').join(' ')}\``)
|
|
115
|
+
.join(', ')}?`
|
|
116
|
+
: `\nRun \`${config.bin} --help\` to see the topics.`));
|
|
117
|
+
}
|
|
118
|
+
/** Adapt an oclif `Config` to the lookup shape above. */
|
|
119
|
+
function lookupFromConfig(config) {
|
|
120
|
+
return {
|
|
121
|
+
bin: config.bin,
|
|
122
|
+
findCommand: (id) => config.findCommand(id),
|
|
123
|
+
commandIDs: config.commandIDs.map((c) => String(c)),
|
|
124
|
+
topicNames: Array.isArray(config.topics)
|
|
125
|
+
? config.topics.map((t) => String(t?.name ?? '')).filter(Boolean)
|
|
126
|
+
: [],
|
|
127
|
+
};
|
|
128
|
+
}
|
|
@@ -22,10 +22,18 @@ export declare const ANALYTICS_REPORTS: {
|
|
|
22
22
|
readonly path: "/api/commitment-analytics/distribution";
|
|
23
23
|
readonly summary: "How commitments are spread across agents, tiers and cadences";
|
|
24
24
|
};
|
|
25
|
+
readonly 'distribution-tripwire': {
|
|
26
|
+
readonly path: "/api/commitment-analytics/distribution/tripwire";
|
|
27
|
+
readonly summary: "Persistent weekly confirmation of portfolio overload risk";
|
|
28
|
+
};
|
|
25
29
|
readonly 'attention-budget': {
|
|
26
30
|
readonly path: "/api/commitment-analytics/attention-budget";
|
|
27
31
|
readonly summary: "How much user attention the fleet is actually spending";
|
|
28
32
|
};
|
|
33
|
+
readonly 'decision-inbox': {
|
|
34
|
+
readonly path: "/api/commitment-analytics/decision-inbox";
|
|
35
|
+
readonly summary: "One ranked owner inbox across approvals, blockers, attention and held surfaces";
|
|
36
|
+
};
|
|
29
37
|
readonly 'action-adapters-audit': {
|
|
30
38
|
readonly path: "/api/commitment-analytics/action-adapters/audit";
|
|
31
39
|
readonly summary: "Which action adapters are installed and what they are permitted to do";
|
|
@@ -54,7 +62,9 @@ export declare const commitmentAnalyticsApi: {
|
|
|
54
62
|
operationsQueue: (query?: Query) => Promise<Record<string, unknown>>;
|
|
55
63
|
governanceExceptions: (query?: Query) => Promise<Record<string, unknown>>;
|
|
56
64
|
distribution: (query?: Query) => Promise<Record<string, unknown>>;
|
|
65
|
+
distributionTripwire: (query?: Query) => Promise<Record<string, unknown>>;
|
|
57
66
|
attentionBudget: (query?: Query) => Promise<Record<string, unknown>>;
|
|
67
|
+
decisionInbox: () => Promise<Record<string, unknown>>;
|
|
58
68
|
actionAdapterAudit: () => Promise<Record<string, unknown>>;
|
|
59
69
|
recoveryMetrics: (query?: Query) => Promise<Record<string, unknown>>;
|
|
60
70
|
recovery: (query?: Query) => Promise<Record<string, unknown>>;
|
|
@@ -41,10 +41,18 @@ exports.ANALYTICS_REPORTS = {
|
|
|
41
41
|
path: `${MOUNT}/distribution`,
|
|
42
42
|
summary: 'How commitments are spread across agents, tiers and cadences',
|
|
43
43
|
},
|
|
44
|
+
'distribution-tripwire': {
|
|
45
|
+
path: `${MOUNT}/distribution/tripwire`,
|
|
46
|
+
summary: 'Persistent weekly confirmation of portfolio overload risk',
|
|
47
|
+
},
|
|
44
48
|
'attention-budget': {
|
|
45
49
|
path: `${MOUNT}/attention-budget`,
|
|
46
50
|
summary: 'How much user attention the fleet is actually spending',
|
|
47
51
|
},
|
|
52
|
+
'decision-inbox': {
|
|
53
|
+
path: `${MOUNT}/decision-inbox`,
|
|
54
|
+
summary: 'One ranked owner inbox across approvals, blockers, attention and held surfaces',
|
|
55
|
+
},
|
|
48
56
|
'action-adapters-audit': {
|
|
49
57
|
path: `${MOUNT}/action-adapters/audit`,
|
|
50
58
|
summary: 'Which action adapters are installed and what they are permitted to do',
|
|
@@ -72,7 +80,9 @@ exports.commitmentAnalyticsApi = {
|
|
|
72
80
|
operationsQueue: (query = {}) => data_provider_1.request.get((0, triggers_1.withQuery)(`${MOUNT}/operations/queue`, query)),
|
|
73
81
|
governanceExceptions: (query = {}) => data_provider_1.request.get((0, triggers_1.withQuery)(`${MOUNT}/governance/exceptions`, query)),
|
|
74
82
|
distribution: (query = {}) => data_provider_1.request.get((0, triggers_1.withQuery)(`${MOUNT}/distribution`, query)),
|
|
83
|
+
distributionTripwire: (query = {}) => data_provider_1.request.get((0, triggers_1.withQuery)(`${MOUNT}/distribution/tripwire`, query)),
|
|
75
84
|
attentionBudget: (query = {}) => data_provider_1.request.get((0, triggers_1.withQuery)(`${MOUNT}/attention-budget`, query)),
|
|
85
|
+
decisionInbox: () => data_provider_1.request.get(`${MOUNT}/decision-inbox`),
|
|
76
86
|
actionAdapterAudit: () => data_provider_1.request.get(`${MOUNT}/action-adapters/audit`),
|
|
77
87
|
recoveryMetrics: (query = {}) => data_provider_1.request.get((0, triggers_1.withQuery)(`${MOUNT}/recovery-metrics`, query)),
|
|
78
88
|
recovery: (query = {}) => data_provider_1.request.get((0, triggers_1.withQuery)(`${MOUNT}/recovery`, query)),
|
package/dist/lib/config.js
CHANGED
|
@@ -51,6 +51,7 @@ const fs = __importStar(require("node:fs"));
|
|
|
51
51
|
const os = __importStar(require("node:os"));
|
|
52
52
|
const path = __importStar(require("node:path"));
|
|
53
53
|
const keychain_1 = require("./keychain");
|
|
54
|
+
const auth_core_1 = require("@skrr-ai/auth-core");
|
|
54
55
|
const publicEndpoints_generated_1 = require("./publicEndpoints.generated");
|
|
55
56
|
// Both from config/public-endpoints.json, the one domain contract, rather than a
|
|
56
57
|
// fourth hand-copied pair. The CLI previously knew only about dev — there was no
|
|
@@ -256,8 +257,11 @@ function isEnvAuthOverride() {
|
|
|
256
257
|
* resolver and every existing install read it.
|
|
257
258
|
*/
|
|
258
259
|
function configRoot(env = process.env) {
|
|
259
|
-
|
|
260
|
-
|
|
260
|
+
// Delegates. The variable list lives in ONE place (`@skrr-ai/auth-core`)
|
|
261
|
+
// because every copy of it became a place `skrr` and `skrrd` could disagree —
|
|
262
|
+
// twice in one day, each time as a correct local fix that created the next
|
|
263
|
+
// defect. `scripts/config-root-single-source.spec` fails if this is re-derived.
|
|
264
|
+
return (0, auth_core_1.resolveConfigRoot)(env);
|
|
261
265
|
}
|
|
262
266
|
function newConfigDir() {
|
|
263
267
|
return configRoot();
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { type DaemonVersionManifest, type ReleaseKey } from '@skrr-ai/auth-core';
|
|
2
|
+
/** Public CloudFront feed. Mirrors the daemon's own `CLOUDFRONT_FEED_BASE`. */
|
|
3
|
+
export declare const DEFAULT_DAEMON_FEED_BASE = "https://updates.oversky.ai/daemon";
|
|
4
|
+
/** Same override the daemon honours, so one env var steers both. */
|
|
5
|
+
export declare const DAEMON_FEED_BASE_ENV = "OVERSKY_DAEMON_UPDATE_FEED_BASE";
|
|
6
|
+
export type DaemonInstallFailureCode = 'unsupported_platform' | 'manifest_unreachable' | 'manifest_malformed' | 'manifest_unsigned' | 'platform_missing' | 'untrusted_artifact_origin' | 'download_failed' | 'checksum_mismatch' | 'artifact_unsigned' | 'write_failed';
|
|
7
|
+
export type DaemonInstallResult = {
|
|
8
|
+
ok: true;
|
|
9
|
+
version: string;
|
|
10
|
+
platformKey: string;
|
|
11
|
+
binaryPath: string;
|
|
12
|
+
} | {
|
|
13
|
+
ok: false;
|
|
14
|
+
code: DaemonInstallFailureCode;
|
|
15
|
+
error: string;
|
|
16
|
+
};
|
|
17
|
+
export interface DaemonInstallOptions {
|
|
18
|
+
env?: NodeJS.ProcessEnv;
|
|
19
|
+
/** Override the feed root. Tests point this at a local fixture server. */
|
|
20
|
+
feedBase?: string;
|
|
21
|
+
/** Injected for tests; defaults to global fetch. */
|
|
22
|
+
fetchImpl?: typeof fetch;
|
|
23
|
+
/**
|
|
24
|
+
* Trust anchors. Defaults to the pinned production set; tests substitute a
|
|
25
|
+
* generated keypair, exactly as the sky-code installer does. Passing an EMPTY
|
|
26
|
+
* set does not disable verification here — every non-`ok` verdict is a
|
|
27
|
+
* refusal, so an unconfigured trust root turns installs off rather than
|
|
28
|
+
* turning checking off.
|
|
29
|
+
*/
|
|
30
|
+
keys?: readonly ReleaseKey[];
|
|
31
|
+
}
|
|
32
|
+
export declare function daemonFeedBase(env?: NodeJS.ProcessEnv): string;
|
|
33
|
+
/**
|
|
34
|
+
* Where the fetched binary lands. `exec-oversky.ts` already looks here, so a
|
|
35
|
+
* daemon installed by this path is found by every `skrr daemon *` command
|
|
36
|
+
* without the user touching PATH.
|
|
37
|
+
*/
|
|
38
|
+
export declare function daemonBinaryPath(env?: NodeJS.ProcessEnv): string;
|
|
39
|
+
/**
|
|
40
|
+
* `<platform>-<arch>`, the manifest's `platforms` key. Returns null for a
|
|
41
|
+
* combination the release does not build, which is a clearer refusal than a
|
|
42
|
+
* missing-key lookup one step later.
|
|
43
|
+
*/
|
|
44
|
+
export declare function daemonPlatformKey(platform?: string, arch?: string): string | null;
|
|
45
|
+
/**
|
|
46
|
+
* True when `candidate` may serve an artifact for a feed rooted at `feedBase`.
|
|
47
|
+
* Same rule as the daemon's `isTrustedArtifactOrigin`: a signed manifest still
|
|
48
|
+
* does not get to point the download at another origin.
|
|
49
|
+
*/
|
|
50
|
+
export declare function isTrustedArtifactOrigin(candidate: string, feedBase: string): boolean;
|
|
51
|
+
/** Fetch `<feedBase>/latest/daemon-version.json` and verify its signature. */
|
|
52
|
+
export declare function fetchVerifiedDaemonManifest(opts?: DaemonInstallOptions): Promise<{
|
|
53
|
+
ok: true;
|
|
54
|
+
manifest: DaemonVersionManifest;
|
|
55
|
+
} | (DaemonInstallResult & {
|
|
56
|
+
ok: false;
|
|
57
|
+
})>;
|
|
58
|
+
/**
|
|
59
|
+
* Download, verify and install the daemon binary. Returns a tagged result; it
|
|
60
|
+
* never throws, so the caller can render one refusal for every failure mode.
|
|
61
|
+
*/
|
|
62
|
+
export declare function installDaemon(opts?: DaemonInstallOptions): Promise<DaemonInstallResult>;
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.DAEMON_FEED_BASE_ENV = exports.DEFAULT_DAEMON_FEED_BASE = void 0;
|
|
7
|
+
exports.daemonFeedBase = daemonFeedBase;
|
|
8
|
+
exports.daemonBinaryPath = daemonBinaryPath;
|
|
9
|
+
exports.daemonPlatformKey = daemonPlatformKey;
|
|
10
|
+
exports.isTrustedArtifactOrigin = isTrustedArtifactOrigin;
|
|
11
|
+
exports.fetchVerifiedDaemonManifest = fetchVerifiedDaemonManifest;
|
|
12
|
+
exports.installDaemon = installDaemon;
|
|
13
|
+
/**
|
|
14
|
+
* daemon-installer.ts — fetch, verify and place the signed `skrrd` binary.
|
|
15
|
+
*
|
|
16
|
+
* ## Why this exists
|
|
17
|
+
*
|
|
18
|
+
* `skrr daemon install` forwarded to `skrrd install`, and `findOverskyBinary()`
|
|
19
|
+
* refused with "install it with brew" when there was no `skrrd` to forward to.
|
|
20
|
+
* So the one command whose job is to set the machine up required the machine to
|
|
21
|
+
* already be set up, and onboarding's first step was a second install from a
|
|
22
|
+
* second distribution channel — before the user had signed in or seen anything
|
|
23
|
+
* work.
|
|
24
|
+
*
|
|
25
|
+
* npm is the front door now. `npm i -g @skrr-ai/cli` gives you `skrr`, and
|
|
26
|
+
* `skrr daemon install` gets the daemon itself.
|
|
27
|
+
*
|
|
28
|
+
* ## Why it downloads a binary instead of shipping one
|
|
29
|
+
*
|
|
30
|
+
* The daemon is a compiled per-platform executable that is codesigned,
|
|
31
|
+
* notarized, and auto-updates itself in the field from a signed CloudFront feed
|
|
32
|
+
* with rollout cohorts. Vendoring five platform binaries into an npm tarball
|
|
33
|
+
* would bloat every install to serve one platform, and — worse — would freeze a
|
|
34
|
+
* copy that the daemon's own updater cannot replace, because that updater swaps
|
|
35
|
+
* a binary in its release slot, not a file inside a node_modules tree. Fetching
|
|
36
|
+
* from the same feed the updater uses keeps ONE distribution channel for the
|
|
37
|
+
* daemon and leaves its update story intact.
|
|
38
|
+
*
|
|
39
|
+
* ## Trust
|
|
40
|
+
*
|
|
41
|
+
* This is the CLI downloading an executable and putting it on disk, so every
|
|
42
|
+
* gate the daemon's own self-updater applies is applied here too, against the
|
|
43
|
+
* SAME pinned keys (`@skrr-ai/auth-core`, moved there for this reason — a
|
|
44
|
+
* mirrored trust root is a second array that can be emptied):
|
|
45
|
+
*
|
|
46
|
+
* 1. the manifest's ed25519 signature, fail-closed;
|
|
47
|
+
* 2. the artifact URL must share the feed's origin. The signature already
|
|
48
|
+
* covers the URL, so this is not about forgery — it bounds where a
|
|
49
|
+
* LEGITIMATELY signed manifest can send us, which is what protects against
|
|
50
|
+
* operator error and against a signer coerced into signing a redirect;
|
|
51
|
+
* 3. sha256 of the received bytes;
|
|
52
|
+
* 4. the artifact's own ed25519 signature over (platform, version, sha256, url).
|
|
53
|
+
*
|
|
54
|
+
* TLS alone is explicitly not enough: the sha256 travels in the same document
|
|
55
|
+
* over the same channel, so it defends against corruption in transit, not
|
|
56
|
+
* against a spoofed feed. (3) without (4) would be the same mistake.
|
|
57
|
+
*
|
|
58
|
+
* The write is staged then renamed, so an interrupted download can never leave
|
|
59
|
+
* a half-written executable at the path the CLI will later exec.
|
|
60
|
+
*/
|
|
61
|
+
const node_crypto_1 = require("node:crypto");
|
|
62
|
+
const node_fs_1 = require("node:fs");
|
|
63
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
64
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
65
|
+
const auth_core_1 = require("@skrr-ai/auth-core");
|
|
66
|
+
/** Public CloudFront feed. Mirrors the daemon's own `CLOUDFRONT_FEED_BASE`. */
|
|
67
|
+
exports.DEFAULT_DAEMON_FEED_BASE = 'https://updates.oversky.ai/daemon';
|
|
68
|
+
/** Same override the daemon honours, so one env var steers both. */
|
|
69
|
+
exports.DAEMON_FEED_BASE_ENV = 'OVERSKY_DAEMON_UPDATE_FEED_BASE';
|
|
70
|
+
/** Refuse a manifest larger than this. It is a small JSON document. */
|
|
71
|
+
const MAX_MANIFEST_BYTES = 1_024 * 1_024;
|
|
72
|
+
/** Refuse an artifact larger than this regardless of what the manifest claims. */
|
|
73
|
+
const MAX_ARTIFACT_BYTES = 256 * 1_024 * 1_024;
|
|
74
|
+
const MANIFEST_TIMEOUT_MS = 30_000;
|
|
75
|
+
const ARTIFACT_TIMEOUT_MS = 10 * 60_000;
|
|
76
|
+
function daemonFeedBase(env = process.env) {
|
|
77
|
+
return (env[exports.DAEMON_FEED_BASE_ENV]?.trim() || exports.DEFAULT_DAEMON_FEED_BASE).replace(/\/+$/, '');
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Where the fetched binary lands. `exec-oversky.ts` already looks here, so a
|
|
81
|
+
* daemon installed by this path is found by every `skrr daemon *` command
|
|
82
|
+
* without the user touching PATH.
|
|
83
|
+
*/
|
|
84
|
+
function daemonBinaryPath(env = process.env) {
|
|
85
|
+
const home = env.HOME || env.USERPROFILE || node_os_1.default.homedir();
|
|
86
|
+
const exe = process.platform === 'win32' ? 'skrrd.exe' : 'skrrd';
|
|
87
|
+
return node_path_1.default.join(home, '.skrr', 'bin', exe);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* `<platform>-<arch>`, the manifest's `platforms` key. Returns null for a
|
|
91
|
+
* combination the release does not build, which is a clearer refusal than a
|
|
92
|
+
* missing-key lookup one step later.
|
|
93
|
+
*/
|
|
94
|
+
function daemonPlatformKey(platform = process.platform, arch = process.arch) {
|
|
95
|
+
const supported = new Set([
|
|
96
|
+
'darwin-arm64',
|
|
97
|
+
'darwin-x64',
|
|
98
|
+
'linux-arm64',
|
|
99
|
+
'linux-x64',
|
|
100
|
+
'win32-x64',
|
|
101
|
+
]);
|
|
102
|
+
const key = `${platform}-${arch}`;
|
|
103
|
+
return supported.has(key) ? key : null;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* True when `candidate` may serve an artifact for a feed rooted at `feedBase`.
|
|
107
|
+
* Same rule as the daemon's `isTrustedArtifactOrigin`: a signed manifest still
|
|
108
|
+
* does not get to point the download at another origin.
|
|
109
|
+
*/
|
|
110
|
+
function isTrustedArtifactOrigin(candidate, feedBase) {
|
|
111
|
+
try {
|
|
112
|
+
return new URL(candidate).origin === new URL(feedBase).origin;
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function fail(code, error) {
|
|
119
|
+
return { ok: false, code, error };
|
|
120
|
+
}
|
|
121
|
+
async function getWithTimeout(url, timeoutMs, fetchImpl) {
|
|
122
|
+
const controller = new AbortController();
|
|
123
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
124
|
+
try {
|
|
125
|
+
return await fetchImpl(url, {
|
|
126
|
+
signal: controller.signal,
|
|
127
|
+
headers: { 'User-Agent': 'skrr-cli (daemon install)' },
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
finally {
|
|
131
|
+
clearTimeout(timer);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/** Fetch `<feedBase>/latest/daemon-version.json` and verify its signature. */
|
|
135
|
+
async function fetchVerifiedDaemonManifest(opts = {}) {
|
|
136
|
+
const env = opts.env ?? process.env;
|
|
137
|
+
const feedBase = opts.feedBase ?? daemonFeedBase(env);
|
|
138
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
139
|
+
const keys = opts.keys ?? auth_core_1.OVERSKY_RELEASE_PUBLIC_KEYS;
|
|
140
|
+
const url = `${feedBase}/latest/daemon-version.json`;
|
|
141
|
+
let res;
|
|
142
|
+
try {
|
|
143
|
+
res = await getWithTimeout(url, MANIFEST_TIMEOUT_MS, fetchImpl);
|
|
144
|
+
}
|
|
145
|
+
catch (err) {
|
|
146
|
+
return fail('manifest_unreachable', `${url}: ${err.message}`);
|
|
147
|
+
}
|
|
148
|
+
if (!res.ok) {
|
|
149
|
+
return fail('manifest_unreachable', `${url} returned HTTP ${res.status}`);
|
|
150
|
+
}
|
|
151
|
+
const text = await res.text();
|
|
152
|
+
if (text.length > MAX_MANIFEST_BYTES) {
|
|
153
|
+
return fail('manifest_malformed', 'release manifest is implausibly large');
|
|
154
|
+
}
|
|
155
|
+
let manifest;
|
|
156
|
+
try {
|
|
157
|
+
manifest = JSON.parse(text);
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return fail('manifest_malformed', `${url} did not return JSON`);
|
|
161
|
+
}
|
|
162
|
+
if (!manifest || typeof manifest.version !== 'string' || !manifest.platforms) {
|
|
163
|
+
return fail('manifest_malformed', 'release manifest is missing version/platforms');
|
|
164
|
+
}
|
|
165
|
+
// Fail-closed by construction: the pinned key set is non-empty, so `disabled`
|
|
166
|
+
// cannot be reached here. Treating it as a refusal anyway means emptying that
|
|
167
|
+
// array turns installs OFF rather than turning verification off silently.
|
|
168
|
+
const verdict = (0, auth_core_1.verifyManifest)(manifest, keys);
|
|
169
|
+
if (!('ok' in verdict) || verdict.ok !== true) {
|
|
170
|
+
const reason = 'reason' in verdict ? verdict.reason : 'no release signing key is pinned';
|
|
171
|
+
return fail('manifest_unsigned', `release manifest failed verification: ${reason}`);
|
|
172
|
+
}
|
|
173
|
+
return { ok: true, manifest };
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Download, verify and install the daemon binary. Returns a tagged result; it
|
|
177
|
+
* never throws, so the caller can render one refusal for every failure mode.
|
|
178
|
+
*/
|
|
179
|
+
async function installDaemon(opts = {}) {
|
|
180
|
+
const env = opts.env ?? process.env;
|
|
181
|
+
const feedBase = opts.feedBase ?? daemonFeedBase(env);
|
|
182
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
183
|
+
const keys = opts.keys ?? auth_core_1.OVERSKY_RELEASE_PUBLIC_KEYS;
|
|
184
|
+
const platformKey = daemonPlatformKey();
|
|
185
|
+
if (!platformKey) {
|
|
186
|
+
return fail('unsupported_platform', `no daemon build for ${process.platform}-${process.arch}`);
|
|
187
|
+
}
|
|
188
|
+
const fetched = await fetchVerifiedDaemonManifest({ env, feedBase, fetchImpl, keys });
|
|
189
|
+
if (!('manifest' in fetched))
|
|
190
|
+
return fetched;
|
|
191
|
+
const { manifest } = fetched;
|
|
192
|
+
const artifact = manifest.platforms[platformKey];
|
|
193
|
+
if (!artifact?.url || !artifact.sha256) {
|
|
194
|
+
return fail('platform_missing', `release ${manifest.version} publishes no artifact for ${platformKey}`);
|
|
195
|
+
}
|
|
196
|
+
if (!isTrustedArtifactOrigin(artifact.url, feedBase)) {
|
|
197
|
+
return fail('untrusted_artifact_origin', `manifest points ${platformKey} at ${artifact.url}, which is not on the feed's origin`);
|
|
198
|
+
}
|
|
199
|
+
const artifactVerdict = (0, auth_core_1.verifyArtifact)(platformKey, manifest.version, artifact, keys);
|
|
200
|
+
if (!('ok' in artifactVerdict) || artifactVerdict.ok !== true) {
|
|
201
|
+
const reason = 'reason' in artifactVerdict ? artifactVerdict.reason : 'no key pinned';
|
|
202
|
+
return fail('artifact_unsigned', `${platformKey} artifact failed verification: ${reason}`);
|
|
203
|
+
}
|
|
204
|
+
if (typeof artifact.size === 'number' && artifact.size > MAX_ARTIFACT_BYTES) {
|
|
205
|
+
return fail('download_failed', `manifest declares an implausible size (${artifact.size} bytes)`);
|
|
206
|
+
}
|
|
207
|
+
let bytes;
|
|
208
|
+
try {
|
|
209
|
+
const res = await getWithTimeout(artifact.url, ARTIFACT_TIMEOUT_MS, fetchImpl);
|
|
210
|
+
if (!res.ok) {
|
|
211
|
+
return fail('download_failed', `${artifact.url} returned HTTP ${res.status}`);
|
|
212
|
+
}
|
|
213
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
214
|
+
if (buf.byteLength > MAX_ARTIFACT_BYTES) {
|
|
215
|
+
return fail('download_failed', 'downloaded artifact exceeds the size ceiling');
|
|
216
|
+
}
|
|
217
|
+
bytes = buf;
|
|
218
|
+
}
|
|
219
|
+
catch (err) {
|
|
220
|
+
return fail('download_failed', `${artifact.url}: ${err.message}`);
|
|
221
|
+
}
|
|
222
|
+
const actual = (0, node_crypto_1.createHash)('sha256').update(bytes).digest('hex');
|
|
223
|
+
if (actual !== artifact.sha256.toLowerCase()) {
|
|
224
|
+
return fail('checksum_mismatch', `sha256 mismatch for ${platformKey}: expected ${artifact.sha256}, received ${actual}`);
|
|
225
|
+
}
|
|
226
|
+
// Staged then renamed. A rename within one directory is atomic, so the path
|
|
227
|
+
// the CLI later execs is either the previous binary or the fully-written new
|
|
228
|
+
// one — never a truncated download wearing the executable bit.
|
|
229
|
+
const binaryPath = daemonBinaryPath(env);
|
|
230
|
+
const stagingPath = `${binaryPath}.staging-${process.pid}`;
|
|
231
|
+
try {
|
|
232
|
+
(0, node_fs_1.mkdirSync)(node_path_1.default.dirname(binaryPath), { recursive: true });
|
|
233
|
+
(0, node_fs_1.writeFileSync)(stagingPath, bytes, { mode: 0o755 });
|
|
234
|
+
(0, node_fs_1.chmodSync)(stagingPath, 0o755);
|
|
235
|
+
(0, node_fs_1.renameSync)(stagingPath, binaryPath);
|
|
236
|
+
}
|
|
237
|
+
catch (err) {
|
|
238
|
+
try {
|
|
239
|
+
(0, node_fs_1.rmSync)(stagingPath, { force: true });
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
/* best-effort staging cleanup */
|
|
243
|
+
}
|
|
244
|
+
return fail('write_failed', `could not install to ${binaryPath}: ${err.message}`);
|
|
245
|
+
}
|
|
246
|
+
return { ok: true, version: manifest.version, platformKey, binaryPath };
|
|
247
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* daemon-setup.ts — offer to set this machine up, at the moment we can.
|
|
3
|
+
*
|
|
4
|
+
* ## Why login and not `npm install`
|
|
5
|
+
*
|
|
6
|
+
* The obvious way to remove a step is a `postinstall` that installs the daemon
|
|
7
|
+
* when the CLI is installed. It cannot work, and the reason is specific rather
|
|
8
|
+
* than aesthetic: installing the daemon BINDS it to a deployment and profile,
|
|
9
|
+
* taken from the CLI's session (`bindToCliSession`, OSK-279 — a daemon on a
|
|
10
|
+
* different deployment than its CLI is the failure that binding exists to
|
|
11
|
+
* prevent). At `npm install` time there is no session.
|
|
12
|
+
*
|
|
13
|
+
* It would not even fail loudly. With no config on disk `loadConfig()` returns
|
|
14
|
+
* DEFAULT_CONFIG, whose baseURL is the DEV deployment — so a postinstall would
|
|
15
|
+
* quietly bind the machine to dev, and the first `skrr login` against
|
|
16
|
+
* production would produce exactly the split. That is the default path on a
|
|
17
|
+
* clean machine, not an edge case.
|
|
18
|
+
*
|
|
19
|
+
* Login is the first moment the answer exists. It is also where the credential
|
|
20
|
+
* handoff already runs, so this is not a new path through the program; it is
|
|
21
|
+
* the branch where that path used to give up.
|
|
22
|
+
*
|
|
23
|
+
* ## Why it asks
|
|
24
|
+
*
|
|
25
|
+
* Signing in and "run a process on this machine that can execute bash, read and
|
|
26
|
+
* write files, and drive a browser" are not the same size of decision. Doing
|
|
27
|
+
* the second as a silent consequence of the first is the class of surprise this
|
|
28
|
+
* whole area has been removing. So: asked when there is someone to ask, skipped
|
|
29
|
+
* with an instruction when there is not, and never on the opt-out.
|
|
30
|
+
*/
|
|
31
|
+
import { confirm } from './prompt';
|
|
32
|
+
export interface DaemonSetupOutcome {
|
|
33
|
+
status: 'installed' | 'declined' | 'skipped' | 'failed';
|
|
34
|
+
detail: string;
|
|
35
|
+
}
|
|
36
|
+
export interface DaemonSetupOptions {
|
|
37
|
+
env?: NodeJS.ProcessEnv;
|
|
38
|
+
/** True when only the OS service is missing and the binary is already here. */
|
|
39
|
+
binaryPresent?: boolean;
|
|
40
|
+
/** Injected by tests. */
|
|
41
|
+
confirmImpl?: typeof confirm;
|
|
42
|
+
interactive?: boolean;
|
|
43
|
+
log?: (line: string) => void;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Ask, then install. Returns without touching anything when there is nobody to
|
|
47
|
+
* ask or the operator has opted out.
|
|
48
|
+
*/
|
|
49
|
+
export declare function offerDaemonSetup(opts?: DaemonSetupOptions): Promise<DaemonSetupOutcome>;
|