@skrr-ai/cli 0.1.39 → 0.1.41
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/README.md +1 -1
- package/dist/base-command.d.ts +9 -0
- package/dist/base-command.js +26 -3
- package/dist/commands/agent-shares/list-for.js +4 -11
- package/dist/commands/agent-shares/received.js +4 -11
- package/dist/commands/agent-shares/sent.js +4 -11
- package/dist/commands/agent-shares/with.js +3 -10
- package/dist/commands/agents/activity.js +1 -1
- package/dist/commands/agents/api-actions/add.js +6 -0
- package/dist/commands/agents/chat.js +6 -1
- package/dist/commands/agents/create.d.ts +7 -0
- package/dist/commands/agents/create.js +19 -2
- package/dist/commands/agents/manager/ensure.d.ts +18 -0
- package/dist/commands/agents/manager/ensure.js +56 -0
- package/dist/commands/agents/show.js +4 -0
- package/dist/commands/agents/tools/auth.d.ts +6 -0
- package/dist/commands/agents/tools/auth.js +27 -1
- package/dist/commands/agents/tools/index.js +11 -1
- package/dist/commands/agents/triggers/policy.d.ts +1 -0
- package/dist/commands/agents/triggers/policy.js +32 -0
- package/dist/commands/agents/update.js +10 -4
- package/dist/commands/code/install.d.ts +4 -2
- package/dist/commands/code/install.js +12 -10
- package/dist/commands/convos/list.js +1 -3
- package/dist/commands/convos/show.js +4 -11
- package/dist/commands/daemon/index.js +3 -3
- package/dist/commands/daemon/install.js +4 -4
- package/dist/commands/daemon/login.js +1 -1
- package/dist/commands/daemon/logs.d.ts +3 -1
- package/dist/commands/daemon/logs.js +16 -1
- package/dist/commands/daemon/start.js +3 -3
- package/dist/commands/daemon/status.d.ts +9 -0
- package/dist/commands/daemon/status.js +22 -0
- package/dist/commands/dm/messages.js +1 -11
- package/dist/commands/followups/cancel.js +1 -1
- package/dist/commands/followups/observability.js +6 -2
- package/dist/commands/followups/remind.js +5 -2
- package/dist/commands/followups/reschedule.js +2 -1
- package/dist/commands/followups/resolve.js +1 -1
- package/dist/commands/followups/show.js +20 -0
- package/dist/commands/followups/watch.d.ts +1 -0
- package/dist/commands/followups/watch.js +9 -3
- package/dist/commands/harnesses/forget.js +20 -3
- package/dist/commands/harnesses/install.js +18 -6
- package/dist/commands/harnesses/installers.d.ts +1 -1
- package/dist/commands/harnesses/installers.js +36 -12
- package/dist/commands/harnesses/list.d.ts +74 -2
- package/dist/commands/harnesses/list.js +231 -15
- package/dist/commands/harnesses/ping.d.ts +2 -0
- package/dist/commands/harnesses/ping.js +21 -0
- package/dist/commands/harnesses/show.d.ts +22 -0
- package/dist/commands/harnesses/show.js +116 -17
- package/dist/commands/harnesses/update.d.ts +8 -0
- package/dist/commands/harnesses/update.js +25 -1
- package/dist/commands/harnesses/usage.d.ts +2 -0
- package/dist/commands/harnesses/usage.js +7 -2
- package/dist/commands/instructions/status.js +13 -0
- package/dist/commands/instructions/update-target.d.ts +1 -0
- package/dist/commands/instructions/update-target.js +11 -3
- package/dist/commands/login.js +5 -5
- package/dist/commands/machines/list.js +8 -9
- package/dist/commands/machines/show.js +46 -17
- package/dist/commands/messages/show.js +3 -10
- package/dist/commands/shares/get.js +2 -9
- package/dist/commands/shares/list.js +1 -9
- package/dist/commands/shortcuts/show.js +4 -11
- package/dist/commands/skills/local.js +6 -0
- package/dist/commands/spaces/actions/run.js +3 -4
- package/dist/commands/spaces/briefs/get.js +2 -9
- package/dist/commands/spaces/briefs/list.js +1 -9
- package/dist/commands/spaces/conversations.js +1 -9
- package/dist/commands/spaces/heartbeats/list.js +1 -9
- package/dist/commands/spaces/heartbeats/status.js +2 -9
- package/dist/commands/tasks/activity.js +1 -10
- package/dist/commands/tasks/approvals/list.js +1 -9
- package/dist/commands/tasks/attachments/list.js +1 -9
- package/dist/commands/tasks/comments/list.js +1 -9
- package/dist/commands/tasks/dri-status.js +2 -9
- package/dist/commands/tasks/events/append.js +9 -7
- package/dist/commands/tasks/execution-timeline.js +1 -9
- package/dist/commands/tasks/lifecycle/detach.js +26 -5
- package/dist/commands/tasks/monitor.js +1 -9
- package/dist/commands/tasks/result/submit.js +9 -0
- package/dist/commands/tasks/runs.js +1 -9
- package/dist/commands/tasks/show.js +5 -12
- package/dist/commands/token/list.d.ts +8 -0
- package/dist/commands/token/list.js +51 -6
- package/dist/help.js +8 -12
- package/dist/lib/agent-home-workspace.d.ts +19 -0
- package/dist/lib/agent-home-workspace.js +39 -0
- package/dist/lib/agent-resolver.js +2 -2
- package/dist/lib/agentic-stream.d.ts +22 -1
- package/dist/lib/agentic-stream.js +48 -7
- package/dist/lib/cli-installers.d.ts +84 -2
- package/dist/lib/cli-installers.js +146 -13
- package/dist/lib/command-miss.d.ts +22 -0
- package/dist/lib/command-miss.js +78 -0
- package/dist/lib/daemon-setup.js +2 -2
- package/dist/lib/daemon-target.d.ts +14 -0
- package/dist/lib/daemon-target.js +32 -0
- package/dist/lib/daemonBroker.js +1 -1
- package/dist/lib/daemonHandoff.js +1 -1
- package/dist/lib/dedicated-lease-command.js +4 -1
- package/dist/lib/dedicated-machines.d.ts +8 -1
- package/dist/lib/dedicated-machines.js +22 -6
- package/dist/lib/dedicated-wait.js +13 -0
- package/dist/lib/exec-runtime-binary.js +2 -2
- package/dist/lib/followups.d.ts +18 -0
- package/dist/lib/followups.js +14 -1
- package/dist/lib/format.d.ts +11 -0
- package/dist/lib/format.js +21 -0
- package/dist/lib/harness-labels.d.ts +39 -0
- package/dist/lib/harness-labels.js +60 -0
- package/dist/lib/harnesses.d.ts +6 -0
- package/dist/lib/instruction-input.d.ts +13 -0
- package/dist/lib/instruction-input.js +20 -1
- package/dist/lib/label-ref.d.ts +8 -1
- package/dist/lib/label-ref.js +12 -2
- package/dist/lib/local-skills.d.ts +17 -1
- package/dist/lib/local-skills.js +20 -2
- package/dist/lib/login.js +5 -5
- package/dist/lib/machines.d.ts +6 -0
- package/dist/lib/machines.js +68 -2
- package/dist/lib/option-hint.d.ts +15 -0
- package/dist/lib/option-hint.js +75 -0
- package/dist/lib/runtime-tool-support.d.ts +34 -0
- package/dist/lib/runtime-tool-support.js +77 -0
- package/dist/lib/share-rows.d.ts +17 -0
- package/dist/lib/share-rows.js +35 -0
- package/dist/lib/workspaces.d.ts +3 -0
- package/dist/lib/workspaces.js +38 -0
- package/dist/node_modules/@skrr-ai/data-provider/index.js +5029 -5007
- package/oclif.manifest.json +34255 -34142
- package/package.json +10 -5
package/dist/lib/command-miss.js
CHANGED
|
@@ -22,10 +22,44 @@
|
|
|
22
22
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
23
|
exports.editDistance = editDistance;
|
|
24
24
|
exports.nearestCommands = nearestCommands;
|
|
25
|
+
exports.helpSubjectId = helpSubjectId;
|
|
25
26
|
exports.describeCommandMiss = describeCommandMiss;
|
|
26
27
|
exports.lookupFromConfig = lookupFromConfig;
|
|
27
28
|
/** A uuid-ish token, which is what a stray space id looks like. */
|
|
28
29
|
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;
|
|
30
|
+
const DAEMON_READINESS = [
|
|
31
|
+
{
|
|
32
|
+
id: 'harnesses:installers',
|
|
33
|
+
usage: 'harnesses installers --daemon <id>',
|
|
34
|
+
answers: 'whether each coding-agent CLI on that machine is installed, and signed in where the daemon can tell',
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: 'daemon:status',
|
|
38
|
+
usage: 'daemon status',
|
|
39
|
+
answers: 'whether the daemon itself is running and connected',
|
|
40
|
+
},
|
|
41
|
+
];
|
|
42
|
+
const ANSWERED_ELSEWHERE = {
|
|
43
|
+
daemon: {
|
|
44
|
+
health: DAEMON_READINESS,
|
|
45
|
+
readiness: DAEMON_READINESS,
|
|
46
|
+
ready: DAEMON_READINESS,
|
|
47
|
+
doctor: DAEMON_READINESS,
|
|
48
|
+
check: DAEMON_READINESS,
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
// `skrr daemons <sub>` is the same topic.
|
|
52
|
+
const ANSWERED_ELSEWHERE_BY_COMMAND = { ...ANSWERED_ELSEWHERE, daemons: ANSWERED_ELSEWHERE.daemon };
|
|
53
|
+
function describeAnsweredElsewhere(config, resolved, extras) {
|
|
54
|
+
if (extras.length !== 1)
|
|
55
|
+
return null;
|
|
56
|
+
const targets = (ANSWERED_ELSEWHERE_BY_COMMAND[resolved]?.[extras[0].toLowerCase()] ?? []).filter((target) => config.findCommand(target.id) !== undefined);
|
|
57
|
+
if (targets.length === 0)
|
|
58
|
+
return null;
|
|
59
|
+
const spoken = `${config.bin} ${resolved.split(':').join(' ')}`;
|
|
60
|
+
return (`\`${spoken}\` has no \`${extras[0]}\`. Depending on what you want to know:\n` +
|
|
61
|
+
targets.map((t) => ` \`${config.bin} ${t.usage}\` — ${t.answers}`).join('\n'));
|
|
62
|
+
}
|
|
29
63
|
/** Cheap edit distance, bounded — this runs only on a miss. */
|
|
30
64
|
function editDistance(a, b) {
|
|
31
65
|
const rows = a.length + 1;
|
|
@@ -60,6 +94,47 @@ function nearestCommands(id, all, limit = 3) {
|
|
|
60
94
|
.slice(0, limit)
|
|
61
95
|
.map(({ candidate }) => candidate));
|
|
62
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* The command id a `--help` request is about, resolved the way oclif resolves
|
|
99
|
+
* the same words when they are RUN.
|
|
100
|
+
*
|
|
101
|
+
* The help override used to join every leading word into the id, so
|
|
102
|
+
* `skrr harnesses install devin --help` asked for `harnesses:install:devin`,
|
|
103
|
+
* found no such command, and reported that `harnesses install` "does not take
|
|
104
|
+
* the positional argument `devin`" — the positional its own help lists
|
|
105
|
+
* (OSK-10099). Running the same words works, because oclif stops collecting id
|
|
106
|
+
* words at the first resolved command that takes arguments: from there on a
|
|
107
|
+
* word is an argument, not part of the name. Adding `--help` to a command you
|
|
108
|
+
* are about to run is the commonest way to check it, so the two must agree.
|
|
109
|
+
*
|
|
110
|
+
* Mirrors `collateSpacedCmdIDFromArgs` in `@oclif/core/lib/help/util` (not a
|
|
111
|
+
* public export): a word extends the id while the longer id is a command or a
|
|
112
|
+
* prefix of one; otherwise it is appended only when the id so far is not a
|
|
113
|
+
* command that accepts arguments, so a genuinely wrong word under a command
|
|
114
|
+
* that takes none (`daemon reload`) still reaches the miss message.
|
|
115
|
+
*/
|
|
116
|
+
function helpSubjectId(tokens, config) {
|
|
117
|
+
const usable = new Set();
|
|
118
|
+
for (const id of [...config.commandIDs, ...config.topicNames]) {
|
|
119
|
+
const parts = id.split(':');
|
|
120
|
+
for (let i = 1; i <= parts.length; i += 1)
|
|
121
|
+
usable.add(parts.slice(0, i).join(':'));
|
|
122
|
+
}
|
|
123
|
+
const taken = [];
|
|
124
|
+
for (const token of tokens) {
|
|
125
|
+
if (token.startsWith('-') || token.includes('='))
|
|
126
|
+
break;
|
|
127
|
+
if (usable.has([...taken, token].join(':'))) {
|
|
128
|
+
taken.push(token);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
const current = taken.length > 0 ? config.findCommand(taken.join(':')) : undefined;
|
|
132
|
+
if (current && (current.strict === false || Object.keys(current.args ?? {}).length > 0))
|
|
133
|
+
break;
|
|
134
|
+
taken.push(token);
|
|
135
|
+
}
|
|
136
|
+
return taken.join(':');
|
|
137
|
+
}
|
|
63
138
|
/**
|
|
64
139
|
* The message for a colon-joined id that did not resolve.
|
|
65
140
|
*
|
|
@@ -81,6 +156,9 @@ function describeCommandMiss(config, id) {
|
|
|
81
156
|
if (!found)
|
|
82
157
|
continue;
|
|
83
158
|
const extras = parts.slice(take);
|
|
159
|
+
const redirect = describeAnsweredElsewhere(config, parts.slice(0, take).join(':'), extras);
|
|
160
|
+
if (redirect)
|
|
161
|
+
return redirect;
|
|
84
162
|
const spoken = `${config.bin} ${parts.slice(0, take).join(' ')}`;
|
|
85
163
|
// Suggest the flag only when the command actually has it, so the hint can
|
|
86
164
|
// never point at something that does not exist.
|
package/dist/lib/daemon-setup.js
CHANGED
|
@@ -62,7 +62,7 @@ async function offerDaemonSetup(opts = {}) {
|
|
|
62
62
|
}
|
|
63
63
|
const confirmImpl = opts.confirmImpl ?? prompt_1.confirm;
|
|
64
64
|
log('');
|
|
65
|
-
log(' This
|
|
65
|
+
log(' This computer has no skrr background service. It is what lets your agents work here —');
|
|
66
66
|
log(' running commands, editing files, and driving a browser on this machine.');
|
|
67
67
|
// This creates a persistent service with broad local capabilities, so a
|
|
68
68
|
// bare Enter must visibly and behaviorally decline it. `confirm` names its
|
|
@@ -100,7 +100,7 @@ async function offerDaemonSetup(opts = {}) {
|
|
|
100
100
|
if (err.message === 'runtime-binary-not-installed') {
|
|
101
101
|
return {
|
|
102
102
|
status: 'failed',
|
|
103
|
-
detail: 'the
|
|
103
|
+
detail: 'the background service vanished between fetching and installing it',
|
|
104
104
|
};
|
|
105
105
|
}
|
|
106
106
|
return { status: 'failed', detail: err.message };
|
|
@@ -99,5 +99,19 @@ export interface ResolveDaemonTargetInput {
|
|
|
99
99
|
listActiveDaemons: () => Promise<ActiveDaemonRow[]>;
|
|
100
100
|
}
|
|
101
101
|
export declare function resolveDaemonTarget(input: ResolveDaemonTargetInput): Promise<DaemonTarget>;
|
|
102
|
+
/**
|
|
103
|
+
* Daemons that are CONNECTED right now, under the id the daemon-control API
|
|
104
|
+
* takes (`daemon_…`, `dedicated-runtime-…`).
|
|
105
|
+
*
|
|
106
|
+
* `listActiveDaemons` returns Daemon RESOURCE rows: "active" means not revoked,
|
|
107
|
+
* and `id` is the resource's own id. The installer routes relay an RPC to a
|
|
108
|
+
* connected daemon by its connection id, so that list could not be used to pick
|
|
109
|
+
* one: its refusal counted 50 "active daemons" — most of them sandboxes gone for
|
|
110
|
+
* weeks — and suggested values like `d_69eb2d1e...` that `--daemon` then failed
|
|
111
|
+
* to find, and a single "active" daemon would have been auto-picked by an id no
|
|
112
|
+
* route accepts (OSK-9993). Online harness rows carry exactly the ids and the
|
|
113
|
+
* liveness needed, one row per (daemon, provider).
|
|
114
|
+
*/
|
|
115
|
+
export declare function listConnectedDaemons(credential: ResolvedCredential): Promise<ActiveDaemonRow[]>;
|
|
102
116
|
/** The same list `skrr list-daemons` prints, so a refusal can name values that work. */
|
|
103
117
|
export declare function listActiveDaemons(credential: ResolvedCredential): Promise<ActiveDaemonRow[]>;
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
46
46
|
exports.localMachine = localMachine;
|
|
47
47
|
exports.resolveDaemonTarget = resolveDaemonTarget;
|
|
48
|
+
exports.listConnectedDaemons = listConnectedDaemons;
|
|
48
49
|
exports.listActiveDaemons = listActiveDaemons;
|
|
49
50
|
const api_fetch_1 = require("./api-fetch");
|
|
50
51
|
const daemonBroker_1 = require("./daemonBroker");
|
|
@@ -103,6 +104,37 @@ async function resolveDaemonTarget(input) {
|
|
|
103
104
|
return { kind: 'none' };
|
|
104
105
|
return { kind: 'ambiguous', daemons };
|
|
105
106
|
}
|
|
107
|
+
/**
|
|
108
|
+
* Daemons that are CONNECTED right now, under the id the daemon-control API
|
|
109
|
+
* takes (`daemon_…`, `dedicated-runtime-…`).
|
|
110
|
+
*
|
|
111
|
+
* `listActiveDaemons` returns Daemon RESOURCE rows: "active" means not revoked,
|
|
112
|
+
* and `id` is the resource's own id. The installer routes relay an RPC to a
|
|
113
|
+
* connected daemon by its connection id, so that list could not be used to pick
|
|
114
|
+
* one: its refusal counted 50 "active daemons" — most of them sandboxes gone for
|
|
115
|
+
* weeks — and suggested values like `d_69eb2d1e...` that `--daemon` then failed
|
|
116
|
+
* to find, and a single "active" daemon would have been auto-picked by an id no
|
|
117
|
+
* route accepts (OSK-9993). Online harness rows carry exactly the ids and the
|
|
118
|
+
* liveness needed, one row per (daemon, provider).
|
|
119
|
+
*/
|
|
120
|
+
async function listConnectedDaemons(credential) {
|
|
121
|
+
const res = await (0, api_fetch_1.apiFetch)('/api/harnesses?status=online', { credential });
|
|
122
|
+
const byDaemon = new Map();
|
|
123
|
+
for (const row of res.harnesses ?? []) {
|
|
124
|
+
if (row.status !== 'online' || !row.daemonId || byDaemon.has(row.daemonId))
|
|
125
|
+
continue;
|
|
126
|
+
// A harness is named "<provider> — <machine>"; the machine half names the daemon.
|
|
127
|
+
const separator = typeof row.name === 'string' ? row.name.indexOf(' — ') : -1;
|
|
128
|
+
byDaemon.set(row.daemonId, {
|
|
129
|
+
id: row.daemonId,
|
|
130
|
+
name: separator === -1 ? undefined : row.name.slice(separator + 3),
|
|
131
|
+
isActive: true,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
// Stable order: the server returns rows most-recently-seen first, so two
|
|
135
|
+
// commands refusing one ambiguity listed the same daemons in different orders.
|
|
136
|
+
return [...byDaemon.values()].sort((a, b) => a.id.localeCompare(b.id));
|
|
137
|
+
}
|
|
106
138
|
/** The same list `skrr list-daemons` prints, so a refusal can name values that work. */
|
|
107
139
|
async function listActiveDaemons(credential) {
|
|
108
140
|
const res = await (0, api_fetch_1.apiFetch)('/api/daemon-resources?status=active&limit=50', { credential });
|
package/dist/lib/daemonBroker.js
CHANGED
|
@@ -311,7 +311,7 @@ async function attemptDaemonBrokerLogin(opts) {
|
|
|
311
311
|
return {
|
|
312
312
|
ok: false,
|
|
313
313
|
reason: 'base_url_mismatch',
|
|
314
|
-
detail: `
|
|
314
|
+
detail: `The skrr background service on this computer is not logged in to ${opts.baseURL}; it is logged in to ${mismatchedBootstrap.serverUrl}.`,
|
|
315
315
|
daemonServerUrl: mismatchedBootstrap.serverUrl,
|
|
316
316
|
};
|
|
317
317
|
}
|
|
@@ -208,7 +208,7 @@ async function handOffToLocalDaemon(env = process.env, opts = {}) {
|
|
|
208
208
|
return {
|
|
209
209
|
status: 'skipped',
|
|
210
210
|
reason: 'legacy-binary',
|
|
211
|
-
detail: 'a legacy oversky
|
|
211
|
+
detail: 'a legacy oversky background service is installed; bootstrap a current skrr one',
|
|
212
212
|
};
|
|
213
213
|
}
|
|
214
214
|
// An older daemon has no `accept-handoff`, and spawning it would fail with
|
|
@@ -127,7 +127,10 @@ class DedicatedLeaseCommand extends base_command_1.BaseCommand {
|
|
|
127
127
|
return await fn();
|
|
128
128
|
}
|
|
129
129
|
catch (err) {
|
|
130
|
-
const message = (0, dedicated_machines_1.formatDedicatedRuntimeApiError)(err
|
|
130
|
+
const message = (0, dedicated_machines_1.formatDedicatedRuntimeApiError)(err, {
|
|
131
|
+
command: this.id,
|
|
132
|
+
bin: this.config.bin,
|
|
133
|
+
});
|
|
131
134
|
if (message) {
|
|
132
135
|
this.failWithCliError({
|
|
133
136
|
message,
|
|
@@ -643,5 +643,12 @@ export declare function dedicatedRuntimeApiErrorCode(err: unknown): string | und
|
|
|
643
643
|
* second list would only drift from the first.
|
|
644
644
|
*/
|
|
645
645
|
export declare function dedicatedRuntimeApiErrorDetails(err: unknown): Record<string, unknown> | undefined;
|
|
646
|
-
|
|
646
|
+
/** What a refused command knows about itself, so advice can be one it can follow. */
|
|
647
|
+
export interface DedicatedApiErrorContext {
|
|
648
|
+
/** oclif command id, e.g. `machines:dedicated:create`. */
|
|
649
|
+
command?: string;
|
|
650
|
+
bin?: string;
|
|
651
|
+
leaseId?: string;
|
|
652
|
+
}
|
|
653
|
+
export declare function formatDedicatedRuntimeApiError(err: unknown, context?: DedicatedApiErrorContext): string | null;
|
|
647
654
|
export {};
|
|
@@ -1134,9 +1134,9 @@ function dedicatedRuntimeApiErrorDetails(err) {
|
|
|
1134
1134
|
* say nothing to the person running it (OSK-8768); `--json` keeps them in
|
|
1135
1135
|
* `error.details.reasons`.
|
|
1136
1136
|
*/
|
|
1137
|
-
function describeRefusalReasons(err) {
|
|
1137
|
+
function describeRefusalReasons(err, context = {}) {
|
|
1138
1138
|
const details = dedicatedRuntimeApiErrorDetails(err);
|
|
1139
|
-
const spend = describeSpendingCapRefusal(err.body, details);
|
|
1139
|
+
const spend = describeSpendingCapRefusal(err.body, details, context);
|
|
1140
1140
|
if (spend)
|
|
1141
1141
|
return spend;
|
|
1142
1142
|
const sentences = (0, harnesses_1.describeHealthReasonSentences)(details?.reasons, details?.state);
|
|
@@ -1148,7 +1148,7 @@ function describeRefusalReasons(err) {
|
|
|
1148
1148
|
* the cap. Without them "would exceed the monthly cap" left a user guessing by
|
|
1149
1149
|
* how much, and which cap (OSK-9552).
|
|
1150
1150
|
*/
|
|
1151
|
-
function describeSpendingCapRefusal(body, details) {
|
|
1151
|
+
function describeSpendingCapRefusal(body, details, context = {}) {
|
|
1152
1152
|
let code;
|
|
1153
1153
|
try {
|
|
1154
1154
|
code = JSON.parse(body || '{}').code;
|
|
@@ -1172,9 +1172,25 @@ function describeSpendingCapRefusal(body, details) {
|
|
|
1172
1172
|
].filter(Boolean);
|
|
1173
1173
|
if (parts.length === 0)
|
|
1174
1174
|
return '';
|
|
1175
|
-
return ` ${parts.join('; ')}.
|
|
1175
|
+
return ` ${parts.join('; ')}. ${raiseCapHint(context)}`;
|
|
1176
1176
|
}
|
|
1177
|
-
|
|
1177
|
+
/**
|
|
1178
|
+
* How to get past a cap refusal, for the command that was refused. A create or
|
|
1179
|
+
* a restore makes a NEW machine and takes its cap as a flag; only an existing
|
|
1180
|
+
* machine has a lease id to raise. Every refusal used to say
|
|
1181
|
+
* `machines dedicated spending-limit <lease-id>`, which a user refused on create
|
|
1182
|
+
* had no lease id to run (OSK-9552).
|
|
1183
|
+
*/
|
|
1184
|
+
function raiseCapHint(context) {
|
|
1185
|
+
const bin = context.bin || 'skrr';
|
|
1186
|
+
const creates = context.command === 'machines:dedicated:create' ||
|
|
1187
|
+
context.command === 'machines:dedicated:restore';
|
|
1188
|
+
if (creates) {
|
|
1189
|
+
return 'Pass a higher --spending-limit-cents: the cap is compared with the whole month, not with this machine alone.';
|
|
1190
|
+
}
|
|
1191
|
+
return `Raise the cap with \`${bin} machines dedicated spending-limit ${context.leaseId || '<lease-id>'} --cents <n>\`.`;
|
|
1192
|
+
}
|
|
1193
|
+
function formatDedicatedRuntimeApiError(err, context = {}) {
|
|
1178
1194
|
if (!(err instanceof api_fetch_1.ApiFetchError) || !err.body) {
|
|
1179
1195
|
return null;
|
|
1180
1196
|
}
|
|
@@ -1202,7 +1218,7 @@ function formatDedicatedRuntimeApiError(err) {
|
|
|
1202
1218
|
// Machine noun even though the caller named a Dedicated Runtime. Do not
|
|
1203
1219
|
// repeat that contradictory product name while a mixed deployment rolls.
|
|
1204
1220
|
const sentence = (parsed.message || parsed.error || parsed.code)?.replace(/(?:skrr )?Hosted Machine/g, 'Dedicated Runtime');
|
|
1205
|
-
return sentence ? `${sentence}${describeRefusalReasons(err)}` : null;
|
|
1221
|
+
return sentence ? `${sentence}${describeRefusalReasons(err, context)}` : null;
|
|
1206
1222
|
}
|
|
1207
1223
|
catch {
|
|
1208
1224
|
return err.body.trim() || null;
|
|
@@ -17,8 +17,21 @@ function backupIsNewer(current, previous) {
|
|
|
17
17
|
const b = Date.parse(previous);
|
|
18
18
|
return Number.isFinite(a) && Number.isFinite(b) ? a > b : current !== previous;
|
|
19
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* `recovering` is also the state an image move replaces the machine through, on
|
|
22
|
+
* purpose. Reading every `recovering` as a failed action being retried told an
|
|
23
|
+
* owner watching an ordinary `update-image --wait` that destroy was available
|
|
24
|
+
* "if it never settles".
|
|
25
|
+
*/
|
|
26
|
+
const PLANNED_REPLACEMENT_PROGRESS = {
|
|
27
|
+
dedicated_runtime_image_update: 'replacing the machine',
|
|
28
|
+
dedicated_runtime_image_update_rolling_back: 'replacing the machine again',
|
|
29
|
+
};
|
|
20
30
|
function progressOf(lease) {
|
|
21
31
|
const state = (0, dedicated_machines_1.dedicatedLeaseState)(lease);
|
|
32
|
+
const planned = lease.stateReason ? PLANNED_REPLACEMENT_PROGRESS[lease.stateReason] : undefined;
|
|
33
|
+
if (state === 'recovering' && planned)
|
|
34
|
+
return planned;
|
|
22
35
|
if (state === 'recovering') {
|
|
23
36
|
// `recovering` is where a failed provision or action lands, retried with
|
|
24
37
|
// backoff. It can settle — or never, which is why destroy is allowed from it.
|
|
@@ -64,7 +64,7 @@ exports.RUNTIME_BINARY_NAMES = ['skrrd', 'oversky'];
|
|
|
64
64
|
*/
|
|
65
65
|
function forwardedFlagsNote(verb, opts = {}) {
|
|
66
66
|
const base = opts.bootstrapsRuntime
|
|
67
|
-
? `If
|
|
67
|
+
? `If the background service is not on this computer yet, this command first fetches the signed release. After it is ` +
|
|
68
68
|
`installed, flags are forwarded to \`skrrd ${verb}\` unchanged; run \`skrrd ${verb} --help\` ` +
|
|
69
69
|
`for this verb's own flags, or \`skrrd --help\` for global ones such as \`--profile\`.`
|
|
70
70
|
: `Flags are forwarded to \`skrrd ${verb}\` unchanged, so they are not listed here —` +
|
|
@@ -196,7 +196,7 @@ function isExecutable(p) {
|
|
|
196
196
|
*/
|
|
197
197
|
function notInstalledMessage() {
|
|
198
198
|
return [
|
|
199
|
-
'The skrr
|
|
199
|
+
'The skrr background service is not installed on this computer.',
|
|
200
200
|
'',
|
|
201
201
|
'Install:',
|
|
202
202
|
' macOS: brew install dush1023/oversky/oversky',
|
package/dist/lib/followups.d.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { withQuery } from './triggers';
|
|
2
2
|
type Query = Parameters<typeof withQuery>[1];
|
|
3
3
|
type Json = Record<string, unknown>;
|
|
4
|
+
/**
|
|
5
|
+
* The line a settle verb prints. A follow-up that had already ended is said to
|
|
6
|
+
* have ended, with the terminal it actually has, rather than reported as the
|
|
7
|
+
* thing this command would have done (OSK-10038).
|
|
8
|
+
*/
|
|
9
|
+
export declare function settledLine(id: string, result: Record<string, unknown>, done: string): string;
|
|
4
10
|
/**
|
|
5
11
|
* The `id` argument every follow-up command that addresses one takes.
|
|
6
12
|
*
|
|
@@ -34,7 +40,13 @@ export interface FollowUpRow {
|
|
|
34
40
|
terminal?: string | null;
|
|
35
41
|
terminalAt?: string | null;
|
|
36
42
|
terminalReason?: string | null;
|
|
43
|
+
/** Who settled it: `trigger_fire` for the runtime, otherwise the actor. */
|
|
44
|
+
terminalBy?: string | null;
|
|
37
45
|
escalatedInitiativeId?: string | null;
|
|
46
|
+
/** The gate the fire passes: exempt_silent, exempt_user_directed, exempt_system, user_directed_guarded, autonomy_ladder, approval_ladder. */
|
|
47
|
+
gate?: string | null;
|
|
48
|
+
/** Set while its fire waits on an offline machine; it escalates after 60 minutes. */
|
|
49
|
+
parkedAt?: string | null;
|
|
38
50
|
capsule?: unknown;
|
|
39
51
|
nextRunAt?: string | null;
|
|
40
52
|
deadlineAt?: string | null;
|
|
@@ -62,6 +74,12 @@ export interface FollowUpObservability {
|
|
|
62
74
|
terminal: Record<string, number>;
|
|
63
75
|
escalationRate: number | null;
|
|
64
76
|
fires: number;
|
|
77
|
+
/** USD from the fires' UsageLog rows; null when the ledger could not be read. */
|
|
78
|
+
spend?: {
|
|
79
|
+
today: number;
|
|
80
|
+
total: number;
|
|
81
|
+
currency: string;
|
|
82
|
+
} | null;
|
|
65
83
|
budget: {
|
|
66
84
|
pool: string | null;
|
|
67
85
|
used: number;
|
package/dist/lib/followups.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.followupsApi = exports.WATCH_WAIT_KINDS = exports.FOLLOWUP_RESOLVE_OUTCOMES = exports.FOLLOWUP_STATES = exports.FOLLOWUP_KINDS = exports.followUpIdArg = void 0;
|
|
4
|
+
exports.settledLine = settledLine;
|
|
4
5
|
exports.resolveFollowUpAgent = resolveFollowUpAgent;
|
|
5
6
|
exports.parseSubjectRef = parseSubjectRef;
|
|
6
7
|
exports.followupSummaryLine = followupSummaryLine;
|
|
@@ -26,6 +27,18 @@ const triggers_1 = require("./triggers");
|
|
|
26
27
|
const agent_resolver_1 = require("./agent-resolver");
|
|
27
28
|
const MOUNT = '/api/followups';
|
|
28
29
|
const base = (id) => `${MOUNT}/${encodeURIComponent(id)}`;
|
|
30
|
+
/**
|
|
31
|
+
* The line a settle verb prints. A follow-up that had already ended is said to
|
|
32
|
+
* have ended, with the terminal it actually has, rather than reported as the
|
|
33
|
+
* thing this command would have done (OSK-10038).
|
|
34
|
+
*/
|
|
35
|
+
function settledLine(id, result, done) {
|
|
36
|
+
const followup = result.followup;
|
|
37
|
+
if (result.alreadySettled === true) {
|
|
38
|
+
return `Follow-up ${id} was already settled${followup?.terminal ? ` (${followup.terminal})` : ''}; nothing changed.`;
|
|
39
|
+
}
|
|
40
|
+
return `Follow-up ${id} ${done}.`;
|
|
41
|
+
}
|
|
29
42
|
/**
|
|
30
43
|
* The `id` argument every follow-up command that addresses one takes.
|
|
31
44
|
*
|
|
@@ -79,7 +92,7 @@ function resolveFollowUpAgent(explicitAgentId, env = process.env) {
|
|
|
79
92
|
if (explicitAgentId && explicitAgentId !== sessionAgent.id) {
|
|
80
93
|
throw new agent_resolver_1.AgentResolutionError({
|
|
81
94
|
code: 'AGENT_CONTEXT_CONFLICT',
|
|
82
|
-
message: 'This
|
|
95
|
+
message: 'This session is bound to its session agent. Do not override it with another agent ID.',
|
|
83
96
|
suggestion: 'Use the bound session agent, or start a new daemon session for a different agent.',
|
|
84
97
|
details: { explicitAgentId, sessionAgentId: sessionAgent.id, source: sessionAgent.source },
|
|
85
98
|
});
|
package/dist/lib/format.d.ts
CHANGED
|
@@ -78,6 +78,17 @@ export declare function renderTable<T extends Record<string, string>>(rows: T[],
|
|
|
78
78
|
* should print it in the reader's zone and name that zone once.
|
|
79
79
|
*/
|
|
80
80
|
export declare function localStamp(value: unknown): string;
|
|
81
|
+
/**
|
|
82
|
+
* One timestamp that carries its own zone: `2026-08-21 17:54:03 PDT`.
|
|
83
|
+
*
|
|
84
|
+
* For key/value output and any row that cannot share a header naming the zone.
|
|
85
|
+
* About 25 commands formatted with `toISOString().slice(0, 19)` — UTC with no
|
|
86
|
+
* marker, which reads as local time — while `harnesses list` and `machines`
|
|
87
|
+
* printed local time with the zone named, so one CLI kept two clocks
|
|
88
|
+
* (OSK-10041). Seconds, because the commands that print these (task runs,
|
|
89
|
+
* activity, monitor) order events seconds apart. Never throws.
|
|
90
|
+
*/
|
|
91
|
+
export declare function localTime(value: unknown): string;
|
|
81
92
|
/**
|
|
82
93
|
* `PDT` — the zone `localStamp` renders in, to be named once per command
|
|
83
94
|
* rather than repeated on every row.
|
package/dist/lib/format.js
CHANGED
|
@@ -7,6 +7,7 @@ exports.formatCell = formatCell;
|
|
|
7
7
|
exports.collapseWhitespace = collapseWhitespace;
|
|
8
8
|
exports.renderTable = renderTable;
|
|
9
9
|
exports.localStamp = localStamp;
|
|
10
|
+
exports.localTime = localTime;
|
|
10
11
|
exports.localZone = localZone;
|
|
11
12
|
/**
|
|
12
13
|
* Code points that occupy TWO terminal columns (Unicode East Asian Wide and
|
|
@@ -212,6 +213,26 @@ function localStamp(value) {
|
|
|
212
213
|
return (`${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ` +
|
|
213
214
|
`${pad(date.getHours())}:${pad(date.getMinutes())}`);
|
|
214
215
|
}
|
|
216
|
+
/**
|
|
217
|
+
* One timestamp that carries its own zone: `2026-08-21 17:54:03 PDT`.
|
|
218
|
+
*
|
|
219
|
+
* For key/value output and any row that cannot share a header naming the zone.
|
|
220
|
+
* About 25 commands formatted with `toISOString().slice(0, 19)` — UTC with no
|
|
221
|
+
* marker, which reads as local time — while `harnesses list` and `machines`
|
|
222
|
+
* printed local time with the zone named, so one CLI kept two clocks
|
|
223
|
+
* (OSK-10041). Seconds, because the commands that print these (task runs,
|
|
224
|
+
* activity, monitor) order events seconds apart. Never throws.
|
|
225
|
+
*/
|
|
226
|
+
function localTime(value) {
|
|
227
|
+
if (value === null || value === undefined || value === '')
|
|
228
|
+
return '-';
|
|
229
|
+
const date = value instanceof Date ? value : new Date(typeof value === 'number' ? value : String(value));
|
|
230
|
+
if (Number.isNaN(date.getTime()))
|
|
231
|
+
return String(value);
|
|
232
|
+
const zone = localZone(date);
|
|
233
|
+
const seconds = `${date.getSeconds()}`.padStart(2, '0');
|
|
234
|
+
return `${localStamp(date)}:${seconds}${zone ? ` ${zone}` : ''}`;
|
|
235
|
+
}
|
|
215
236
|
/**
|
|
216
237
|
* `PDT` — the zone `localStamp` renders in, to be named once per command
|
|
217
238
|
* rather than repeated on every row.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a harness row's provider and status read in human output — ONE rule for
|
|
3
|
+
* `harnesses list` and `harnesses show`.
|
|
4
|
+
*
|
|
5
|
+
* Both findings this closes were the same defect on two columns: the rule lived
|
|
6
|
+
* in one command and not the other.
|
|
7
|
+
*
|
|
8
|
+
* - Status (OSK-9610): `list` printed `superseded` for a row a later
|
|
9
|
+
* registration replaced; `show` printed `offline` for the same id. The server
|
|
10
|
+
* sends `superseded` on both routes — `show` never read it.
|
|
11
|
+
* - Provider (OSK-9649 / OSK-9290): the web renders the placeholder row of a
|
|
12
|
+
* machine with no coding engine as "No coding agent installed" and folds the
|
|
13
|
+
* first-party harness's older spelling into its current one. The CLI printed
|
|
14
|
+
* the stored value — `oversky` on 111 rows, and the first-party harness's
|
|
15
|
+
* older spelling beside its current one — so one product had three names here.
|
|
16
|
+
*
|
|
17
|
+
* `--json` keeps the stored values: it is the API contract, not a rendering.
|
|
18
|
+
*/
|
|
19
|
+
/** What the PROVIDER column says for a machine that has no coding engine. */
|
|
20
|
+
export declare const NO_CODING_AGENT_PROVIDER_LABEL = "none";
|
|
21
|
+
export declare function harnessStatusLabel(row: {
|
|
22
|
+
status?: string | null;
|
|
23
|
+
superseded?: boolean | null;
|
|
24
|
+
}): string;
|
|
25
|
+
export declare function isNoCodingAgentProvider(provider: string | null | undefined): boolean;
|
|
26
|
+
export declare function harnessProviderLabel(provider: string | null | undefined): string;
|
|
27
|
+
/**
|
|
28
|
+
* YOUR alias is what `name` shows and it differs from the name everyone else
|
|
29
|
+
* sees. The server sends both (`aliased`, `sharedName`); every human surface
|
|
30
|
+
* asks this one question of them, so `list`, `show` and `update --name` cannot
|
|
31
|
+
* disagree about whether a row is aliased (OSK-10004).
|
|
32
|
+
*/
|
|
33
|
+
export declare function isAliasCoveringSharedName(row: {
|
|
34
|
+
name?: string | null;
|
|
35
|
+
sharedName?: string | null;
|
|
36
|
+
aliased?: boolean | null;
|
|
37
|
+
}): boolean;
|
|
38
|
+
/** The longer phrase, for a single-row view with room for it. */
|
|
39
|
+
export declare function harnessProviderDescription(provider: string | null | undefined): string;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* How a harness row's provider and status read in human output — ONE rule for
|
|
4
|
+
* `harnesses list` and `harnesses show`.
|
|
5
|
+
*
|
|
6
|
+
* Both findings this closes were the same defect on two columns: the rule lived
|
|
7
|
+
* in one command and not the other.
|
|
8
|
+
*
|
|
9
|
+
* - Status (OSK-9610): `list` printed `superseded` for a row a later
|
|
10
|
+
* registration replaced; `show` printed `offline` for the same id. The server
|
|
11
|
+
* sends `superseded` on both routes — `show` never read it.
|
|
12
|
+
* - Provider (OSK-9649 / OSK-9290): the web renders the placeholder row of a
|
|
13
|
+
* machine with no coding engine as "No coding agent installed" and folds the
|
|
14
|
+
* first-party harness's older spelling into its current one. The CLI printed
|
|
15
|
+
* the stored value — `oversky` on 111 rows, and the first-party harness's
|
|
16
|
+
* older spelling beside its current one — so one product had three names here.
|
|
17
|
+
*
|
|
18
|
+
* `--json` keeps the stored values: it is the API contract, not a rendering.
|
|
19
|
+
*/
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
exports.NO_CODING_AGENT_PROVIDER_LABEL = void 0;
|
|
22
|
+
exports.harnessStatusLabel = harnessStatusLabel;
|
|
23
|
+
exports.isNoCodingAgentProvider = isNoCodingAgentProvider;
|
|
24
|
+
exports.harnessProviderLabel = harnessProviderLabel;
|
|
25
|
+
exports.isAliasCoveringSharedName = isAliasCoveringSharedName;
|
|
26
|
+
exports.harnessProviderDescription = harnessProviderDescription;
|
|
27
|
+
const data_provider_1 = require("@skrr-ai/data-provider");
|
|
28
|
+
/** What the PROVIDER column says for a machine that has no coding engine. */
|
|
29
|
+
exports.NO_CODING_AGENT_PROVIDER_LABEL = 'none';
|
|
30
|
+
function harnessStatusLabel(row) {
|
|
31
|
+
if (row.superseded)
|
|
32
|
+
return 'superseded';
|
|
33
|
+
return typeof row.status === 'string' && row.status.length > 0 ? row.status : 'unknown';
|
|
34
|
+
}
|
|
35
|
+
function isNoCodingAgentProvider(provider) {
|
|
36
|
+
return typeof provider === 'string' && (0, data_provider_1.isManagedProductFallbackProvider)(provider);
|
|
37
|
+
}
|
|
38
|
+
function harnessProviderLabel(provider) {
|
|
39
|
+
if (typeof provider !== 'string' || provider.length === 0)
|
|
40
|
+
return 'unknown';
|
|
41
|
+
if (isNoCodingAgentProvider(provider))
|
|
42
|
+
return exports.NO_CODING_AGENT_PROVIDER_LABEL;
|
|
43
|
+
return String((0, data_provider_1.canonicalHarnessProvider)(provider) ?? provider);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* YOUR alias is what `name` shows and it differs from the name everyone else
|
|
47
|
+
* sees. The server sends both (`aliased`, `sharedName`); every human surface
|
|
48
|
+
* asks this one question of them, so `list`, `show` and `update --name` cannot
|
|
49
|
+
* disagree about whether a row is aliased (OSK-10004).
|
|
50
|
+
*/
|
|
51
|
+
function isAliasCoveringSharedName(row) {
|
|
52
|
+
return Boolean(row.aliased && row.sharedName && row.sharedName !== row.name);
|
|
53
|
+
}
|
|
54
|
+
/** The longer phrase, for a single-row view with room for it. */
|
|
55
|
+
function harnessProviderDescription(provider) {
|
|
56
|
+
if (isNoCodingAgentProvider(provider)) {
|
|
57
|
+
return `${exports.NO_CODING_AGENT_PROVIDER_LABEL} — ${(0, data_provider_1.managedProductFallbackLabel)().toLowerCase()} on this machine`;
|
|
58
|
+
}
|
|
59
|
+
return harnessProviderLabel(provider);
|
|
60
|
+
}
|
package/dist/lib/harnesses.d.ts
CHANGED
|
@@ -51,6 +51,12 @@ export interface Harness {
|
|
|
51
51
|
sharedName?: string;
|
|
52
52
|
aliased?: boolean;
|
|
53
53
|
status?: string;
|
|
54
|
+
/**
|
|
55
|
+
* A later registration on the same machine replaced this row. Sent by both
|
|
56
|
+
* the list and the single-row routes; human output shows it instead of the
|
|
57
|
+
* stored status (`harnessStatusLabel`, OSK-9610).
|
|
58
|
+
*/
|
|
59
|
+
superseded?: boolean;
|
|
54
60
|
harnessLocation?: 'local' | 'cloud';
|
|
55
61
|
daemonId?: string;
|
|
56
62
|
machineUuid?: string;
|
|
@@ -76,3 +76,16 @@ export declare function describeTargetSync(target: {
|
|
|
76
76
|
updatedAt?: string;
|
|
77
77
|
driftCount?: number;
|
|
78
78
|
}, now?: number, liveDaemonIds?: string[]): string;
|
|
79
|
+
/**
|
|
80
|
+
* The command that re-binds a stalled target to the daemon now running on its
|
|
81
|
+
* machine, or null when no such daemon is live.
|
|
82
|
+
*
|
|
83
|
+
* A daemon started a different way on the same machine (launchd → cli) gets a
|
|
84
|
+
* new id, and a target stays bound to the old one. Placement is the machine, not
|
|
85
|
+
* the daemon, so the recovery is `update-target --daemon`, never a re-install,
|
|
86
|
+
* which the placement index refuses (OSK-9961).
|
|
87
|
+
*/
|
|
88
|
+
export declare function rebindCommandFor(target: {
|
|
89
|
+
id: string;
|
|
90
|
+
daemonId?: string | null;
|
|
91
|
+
}, liveDaemonIds: readonly string[]): string | null;
|
|
@@ -7,6 +7,7 @@ exports.resolveBundleLabel = resolveBundleLabel;
|
|
|
7
7
|
exports.parseDaemonId = parseDaemonId;
|
|
8
8
|
exports.sameMachineDaemon = sameMachineDaemon;
|
|
9
9
|
exports.describeTargetSync = describeTargetSync;
|
|
10
|
+
exports.rebindCommandFor = rebindCommandFor;
|
|
10
11
|
const promises_1 = require("node:fs/promises");
|
|
11
12
|
async function readStdin() {
|
|
12
13
|
const chunks = [];
|
|
@@ -180,7 +181,25 @@ function describeTargetSync(target, now = Date.now(), liveDaemonIds = []) {
|
|
|
180
181
|
const live = parseDaemonId(elsewhere);
|
|
181
182
|
const pinned = parseDaemonId(target.daemonId || '');
|
|
182
183
|
// Kept under the STATUS column's `maxWidth: 78`, same constraint as below.
|
|
183
|
-
|
|
184
|
+
// Re-bind, not re-install: install refuses a second target on the same
|
|
185
|
+
// placement, so "re-install" was a remedy that could not succeed (OSK-9961).
|
|
186
|
+
// The full command does not fit the column; status prints it below.
|
|
187
|
+
return `${base} — stalled ${days}d; daemon here now runs as ${live?.source}, not ${pinned?.source}: re-bind below`;
|
|
184
188
|
}
|
|
185
189
|
return `${base} — stalled ${days}d; daemon may be gone: remove, then purge-target --force`;
|
|
186
190
|
}
|
|
191
|
+
/**
|
|
192
|
+
* The command that re-binds a stalled target to the daemon now running on its
|
|
193
|
+
* machine, or null when no such daemon is live.
|
|
194
|
+
*
|
|
195
|
+
* A daemon started a different way on the same machine (launchd → cli) gets a
|
|
196
|
+
* new id, and a target stays bound to the old one. Placement is the machine, not
|
|
197
|
+
* the daemon, so the recovery is `update-target --daemon`, never a re-install,
|
|
198
|
+
* which the placement index refuses (OSK-9961).
|
|
199
|
+
*/
|
|
200
|
+
function rebindCommandFor(target, liveDaemonIds) {
|
|
201
|
+
if (!target.daemonId || liveDaemonIds.includes(target.daemonId))
|
|
202
|
+
return null;
|
|
203
|
+
const live = sameMachineDaemon(target.daemonId, [...liveDaemonIds]);
|
|
204
|
+
return live ? `skrr instructions update-target ${target.id} --daemon ${live}` : null;
|
|
205
|
+
}
|