@bli-cockpit/cli 0.2.5 → 0.2.7
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 +19 -7
- package/dist/adapters/attribution-core.js +152 -31
- package/dist/adapters/claude-attribution.js +14 -7
- package/dist/adapters/codex-attribution.js +8 -2
- package/dist/adapters/raw-evidence.js +33 -12
- package/dist/autostart.js +460 -20
- package/dist/commands/backfill.js +582 -87
- package/dist/commands/doctor.js +72 -60
- package/dist/commands/local-args.js +19 -2
- package/dist/commands/local.js +401 -229
- package/dist/commands/public-root.js +6 -1
- package/dist/commands/session-sync.js +259 -54
- package/dist/cursors/backfill-cursor.js +169 -1
- package/dist/cursors/raw-evidence-cursor.js +16 -2
- package/dist/evidence-upload-client.js +139 -107
- package/dist/local-state.js +71 -5
- package/dist/onboarding-roots.js +42 -17
- package/dist/process-runner.js +103 -0
- package/dist/raw-evidence-attribution-policy.js +25 -0
- package/dist/repo-identity.js +142 -26
- package/dist/root-normalization.js +29 -16
- package/dist/spool/local-spool.js +335 -58
- package/dist/upload.js +309 -66
- package/package.json +2 -2
package/dist/commands/local.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { execFile
|
|
2
|
-
import {
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { readdir, readFile, stat } from "node:fs/promises";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { createCollectorServer } from "../server.js";
|
|
6
6
|
import { inspectAgentRules, installAgentRules, uninstallAgentRules, } from "../agent-rules.js";
|
|
7
|
-
import { runBackfillCommand } from "./backfill.js";
|
|
7
|
+
import { backfillRetryCommand, runBackfill, runBackfillCommand, } from "./backfill.js";
|
|
8
8
|
import { runDoctor } from "./doctor.js";
|
|
9
9
|
import { inspectBackfillLock } from "../backfill-lock.js";
|
|
10
10
|
import { parseLocalArgs, normalizeUrl } from "./local-args.js";
|
|
@@ -12,13 +12,15 @@ import { autostartStatus, installAutostartAgent, uninstallAutostartAgent, } from
|
|
|
12
12
|
import { DEFAULT_DASHBOARD_URL, getCollectorRuntimePaths, inspectLocalCollectorStatus, installLocalCollector, logoutLocalCollector, pairLocalCollector, LOCAL_COLLECTOR_VERSION, readLocalCollectorConfig, readLocalCollectorSessionFile, readLocalSessionReference, startLocalWorkContext, } from "../local-state.js";
|
|
13
13
|
import { CODEX_ATTRIBUTION_SCAN_WINDOW_SESSION_LIMIT, CODEX_ATTRIBUTION_SCAN_WINDOW_MINUTES, defaultCodexSessionDirs, scanAndAttributeCodexSessions, } from "../adapters/codex-attribution.js";
|
|
14
14
|
import { scanAndAttributeClaudeSessions } from "../adapters/claude-attribution.js";
|
|
15
|
-
import {
|
|
15
|
+
import { backfillCompletionCovers, emptyBackfillCursorState, prepareBackfillCursorForScope, readBackfillCompletionMarker, readBackfillCursor, } from "../cursors/backfill-cursor.js";
|
|
16
16
|
import { acquireSyncLock } from "../sync-lock.js";
|
|
17
|
-
import {
|
|
18
|
-
import { runAttributedWorktreeSync, } from "./session-sync.js";
|
|
17
|
+
import { collectionRootPathAliases, discoverGitWorktreesInRootsWithStatus, } from "../repo-identity.js";
|
|
18
|
+
import { runAttributedWorktreeSync, matchesLiveSyncWorktree, } from "./session-sync.js";
|
|
19
19
|
import { COLLECTION_ROOT_REQUIRED, missingCollectionRootMessage, normalizeRootsDetailed, resolveOnboardingRoots, rootRejectionExplanation, } from "../onboarding-roots.js";
|
|
20
20
|
import { rawEvidenceGcSummary, runRawEvidenceLocalGc, } from "../raw-evidence-gc.js";
|
|
21
21
|
import { enqueueInstallEventEntry, readPendingInstallEventEntries, recordInstallEventAttemptFailure, removeInstallEventEntry, } from "../spool/install-event-outbox.js";
|
|
22
|
+
import { createCapturedExecRunner, createInteractiveExecRunner, } from "../process-runner.js";
|
|
23
|
+
import { normalizeCollectionRoots } from "../root-normalization.js";
|
|
22
24
|
export const rootCommandNames = new Set([
|
|
23
25
|
"onboard",
|
|
24
26
|
"update",
|
|
@@ -108,7 +110,7 @@ export function localCommandHelp(command) {
|
|
|
108
110
|
" cockpit onboard [--ticket <id>] [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--workspace <path>] [--allow-home-root] [--branch <name>] [--no-auth] [--max-depth <n>] [--max-repos <n>] [--json]",
|
|
109
111
|
" cockpit update [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--workspace <path>] [--allow-home-root] [--no-auth] [--json]",
|
|
110
112
|
" cockpit upgrade [same flags as update]",
|
|
111
|
-
" cockpit do-everything [--workspace <path>] [--dashboard-url <url>] [--dry-run] [--json]",
|
|
113
|
+
" cockpit do-everything [--workspace <path>] [--dashboard-url <url>] [--update-tag <tag>] [--dry-run] [--json]",
|
|
112
114
|
" cockpit fix [same flags as do-everything]",
|
|
113
115
|
" cockpit install [--dashboard-url <url>] [--workspace <path>] [--allow-home-root] [--json]",
|
|
114
116
|
" cockpit login [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--no-auth] [--json]",
|
|
@@ -117,7 +119,7 @@ export function localCommandHelp(command) {
|
|
|
117
119
|
" cockpit start [--ticket <id>|--clear-ticket] [--topic <label>] [--intent <intent>] [--phase <phase>] [--workspace <path>] [--branch <name>] [--max-depth <n>] [--max-repos <n>] [--json]",
|
|
118
120
|
" cockpit sync [--workspace <path>] [--dashboard-url <url>] [--max-depth <n>] [--max-repos <n>] [--json]",
|
|
119
121
|
" cockpit analyze [--workspace <path>] [--dashboard-url <url>] [--max-depth <n>] [--max-repos <n>] [--json]",
|
|
120
|
-
" cockpit backfill (--since-days <n>|--all) [--source codex|claude] [--dry-run] [--max-files <n>] [--yes] [--workspace <path>] [--json]",
|
|
122
|
+
" cockpit backfill (--since-days <n>|--all) [--source codex|claude] [--dry-run] [--max-files <n>] [--max-depth <n>] [--max-repos <n>] [--yes] [--workspace <path>] [--json]",
|
|
121
123
|
" cockpit status [--workspace <path>] [--max-depth <n>] [--max-repos <n>] [--json]",
|
|
122
124
|
" cockpit sessions [--source codex|claude] [--since-days <n>|--all] [--workspace <path>] [--max-depth <n>] [--max-repos <n>] [--json]",
|
|
123
125
|
" cockpit serve [--port <port>] [--workspace <path>]",
|
|
@@ -133,9 +135,10 @@ function localSubcommandHelp(command) {
|
|
|
133
135
|
[
|
|
134
136
|
"onboard",
|
|
135
137
|
[
|
|
136
|
-
"Usage: cockpit onboard [--ticket <id>] [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--workspace <path>] [--allow-home-root] [--branch <name>] [--no-auth] [--json]",
|
|
138
|
+
"Usage: cockpit onboard [--ticket <id>] [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--workspace <path>] [--allow-home-root] [--branch <name>] [--no-auth] [--max-depth <n>] [--max-repos <n>] [--json]",
|
|
137
139
|
"",
|
|
138
|
-
"Installs, pairs, starts work context(s), syncs once,
|
|
140
|
+
"Installs, pairs, starts work context(s), syncs once, completes all-history",
|
|
141
|
+
"Codex and Claude backfill for the saved roots, and then prints readiness proof.",
|
|
139
142
|
"If --workspace is a parent folder, scans child git repos/worktrees and rolls them up by repo.",
|
|
140
143
|
"`--repo <path>` remains supported as a backward-compatible alias.",
|
|
141
144
|
`Omit --dashboard-url for normal production setup (${DEFAULT_DASHBOARD_URL}).`,
|
|
@@ -160,8 +163,8 @@ function localSubcommandHelp(command) {
|
|
|
160
163
|
"Usage: cockpit update [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--workspace <path>] [--allow-home-root] [--no-auth] [--json]",
|
|
161
164
|
"",
|
|
162
165
|
"Updates the global public CLI from npm, then reruns `cockpit onboard`",
|
|
163
|
-
"with the same setup flags so pairing, saved roots,
|
|
164
|
-
"autostart, and the initial sync are refreshed in one command.",
|
|
166
|
+
"with the same setup flags so pairing, saved roots, all-history backfill,",
|
|
167
|
+
"agent rules, autostart, and the initial sync are refreshed in one command.",
|
|
165
168
|
"`cockpit upgrade` is an alias.",
|
|
166
169
|
],
|
|
167
170
|
],
|
|
@@ -176,10 +179,11 @@ function localSubcommandHelp(command) {
|
|
|
176
179
|
[
|
|
177
180
|
"do-everything",
|
|
178
181
|
[
|
|
179
|
-
"Usage: cockpit do-everything [--workspace <path>] [--dashboard-url <url>] [--dry-run] [--json]",
|
|
182
|
+
"Usage: cockpit do-everything [--workspace <path>] [--dashboard-url <url>] [--update-tag <tag>] [--dry-run] [--json]",
|
|
180
183
|
"",
|
|
181
|
-
"Converges
|
|
184
|
+
"Converges a blank, existing, or reused intern machine: latest CLI, interactive auth and collection-root recovery when needed, saved roots, autostart, all-history backfill, raw-evidence GC, and sync freshness.",
|
|
182
185
|
"`cockpit fix` is an alias.",
|
|
186
|
+
"Maintainer canary: use `--update-tag next` so self-update and re-exec stay on the prerelease candidate.",
|
|
183
187
|
"--dry-run prints the checks and would-fix steps without writing config, plists, cursors, or install telemetry.",
|
|
184
188
|
],
|
|
185
189
|
],
|
|
@@ -255,12 +259,14 @@ function localSubcommandHelp(command) {
|
|
|
255
259
|
[
|
|
256
260
|
"backfill",
|
|
257
261
|
[
|
|
258
|
-
"Usage: cockpit backfill (--since-days <n>|--all) [--source codex|claude] [--dry-run] [--max-files <n>] [--yes] [--workspace <path>] [--json]",
|
|
262
|
+
"Usage: cockpit backfill (--since-days <n>|--all) [--source codex|claude] [--dry-run] [--max-files <n>] [--max-depth <n>] [--max-repos <n>] [--yes] [--workspace <path>] [--json]",
|
|
259
263
|
"",
|
|
260
264
|
"Backfills historical Codex and Claude Code session evidence using the saved collection roots from local config when --workspace is omitted.",
|
|
261
265
|
"Omit --source to scan both sources; there is no --source all literal.",
|
|
262
266
|
"The non-all window is capped at the collector session paired_at timestamp.",
|
|
263
267
|
"--all requires a dry-run review and TTY confirmation; pass --yes for headless agent runs.",
|
|
268
|
+
"Repo discovery scans 3 folder levels and up to 50 repos by default.",
|
|
269
|
+
"Raise --max-depth or --max-repos when a partial result reports a discovery cap.",
|
|
264
270
|
"--dry-run validates the paired collector and dashboard reachability, prints the same summary tables, and writes nothing.",
|
|
265
271
|
],
|
|
266
272
|
],
|
|
@@ -300,12 +306,15 @@ function localSubcommandHelp(command) {
|
|
|
300
306
|
[
|
|
301
307
|
"Usage: cockpit autostart [install|uninstall|status] [--workspace <path>] [--dashboard-url <url>] [--interval-seconds <n>] [--json]",
|
|
302
308
|
"",
|
|
303
|
-
"Installs
|
|
304
|
-
"
|
|
309
|
+
"Installs background sync through launchd on Apple Silicon macOS or a",
|
|
310
|
+
"per-user Task Scheduler task on native Windows.",
|
|
311
|
+
"Runs `cockpit sync` every 15 min by default and survives reboots;",
|
|
312
|
+
"the Windows task runs while the user is signed in.",
|
|
305
313
|
"Action defaults to `install`. `--workspace` is the parent work folder to sync.",
|
|
306
314
|
"`--repo <path>` remains supported as a backward-compatible alias.",
|
|
307
315
|
"Omit --dashboard-url for production; pass it only for staging/custom dashboards.",
|
|
308
|
-
"
|
|
316
|
+
"See docs/runbooks/cockpit-launchd-sync.md and",
|
|
317
|
+
"docs/runbooks/cockpit-windows-task-scheduler-sync.md.",
|
|
309
318
|
],
|
|
310
319
|
],
|
|
311
320
|
[
|
|
@@ -418,7 +427,7 @@ async function runUpdate(command, io) {
|
|
|
418
427
|
writeLine(io.stdout, JSON.stringify({
|
|
419
428
|
status: "blocked",
|
|
420
429
|
step: "npm_install",
|
|
421
|
-
command: `npm ${
|
|
430
|
+
command: `npm ${selfUpdateInstallArgs().join(" ")}`,
|
|
422
431
|
exit_code: error.result.code,
|
|
423
432
|
}, null, 2));
|
|
424
433
|
}
|
|
@@ -443,12 +452,6 @@ async function runUpdate(command, io) {
|
|
|
443
452
|
addInstallEvent(installEvents, "onboard_rerun", onboard.code === 0 ? "ok" : "fail", onboard.code === 0 ? undefined : updateOnboardFailureCode(onboard));
|
|
444
453
|
return finish(onboard.code);
|
|
445
454
|
}
|
|
446
|
-
const SELF_UPDATE_INSTALL_ARGS = [
|
|
447
|
-
"install",
|
|
448
|
-
"-g",
|
|
449
|
-
"@bli-cockpit/cli@latest",
|
|
450
|
-
"--prefer-online",
|
|
451
|
-
];
|
|
452
455
|
export class SelfUpdateError extends Error {
|
|
453
456
|
result;
|
|
454
457
|
eacces;
|
|
@@ -461,16 +464,25 @@ export class SelfUpdateError extends Error {
|
|
|
461
464
|
}
|
|
462
465
|
export async function runSelfUpdate(io, options = {}) {
|
|
463
466
|
const exec = io.exec ?? defaultExec();
|
|
467
|
+
const tag = options.tag ?? "latest";
|
|
464
468
|
if (!options.json) {
|
|
465
|
-
writeLine(io.stdout,
|
|
469
|
+
writeLine(io.stdout, `Updating Cockpit CLI from npm (${tag})...`);
|
|
466
470
|
}
|
|
467
|
-
const install = await exec("npm",
|
|
471
|
+
const install = await exec("npm", selfUpdateInstallArgs(tag));
|
|
468
472
|
writeExecOutput(io, install, { stdout: !options.json, stderr: true });
|
|
469
473
|
if (install.code !== 0) {
|
|
470
474
|
throw new SelfUpdateError(install);
|
|
471
475
|
}
|
|
472
476
|
return { updated: true, version: LOCAL_COLLECTOR_VERSION };
|
|
473
477
|
}
|
|
478
|
+
function selfUpdateInstallArgs(tag = "latest") {
|
|
479
|
+
return [
|
|
480
|
+
"install",
|
|
481
|
+
"-g",
|
|
482
|
+
`@bli-cockpit/cli@${tag}`,
|
|
483
|
+
"--prefer-online",
|
|
484
|
+
];
|
|
485
|
+
}
|
|
474
486
|
async function runRelease(command, io) {
|
|
475
487
|
const releaseRoot = await findPublicReleaseRoot(process.cwd());
|
|
476
488
|
if (!releaseRoot) {
|
|
@@ -644,6 +656,17 @@ function addAutostartInstallEvent(events, result) {
|
|
|
644
656
|
}
|
|
645
657
|
addInstallEvent(events, "autostart", result.loaded === false ? "fail" : "ok", result.loaded === false ? "autostart_load_failed" : undefined);
|
|
646
658
|
}
|
|
659
|
+
function onboardAutostartFailed(result) {
|
|
660
|
+
return (result !== null &&
|
|
661
|
+
result.status !== "unsupported" &&
|
|
662
|
+
result.loaded === false);
|
|
663
|
+
}
|
|
664
|
+
function writeOnboardAutostartBlocker(io, result) {
|
|
665
|
+
writeLine(io.stderr, "BLOCKED: initial collection succeeded, but recurring background collection is not running.");
|
|
666
|
+
if (result?.message)
|
|
667
|
+
writeLine(io.stderr, `Failure: ${result.message}`);
|
|
668
|
+
writeLine(io.stderr, "Next: run `cockpit autostart install`, then require `cockpit autostart status --json` to exit 0.");
|
|
669
|
+
}
|
|
647
670
|
function onboardFailureStep(blocker, events) {
|
|
648
671
|
if (blocker === COLLECTION_ROOT_REQUIRED)
|
|
649
672
|
return "install";
|
|
@@ -1023,11 +1046,7 @@ async function readGitConfigEmail(root) {
|
|
|
1023
1046
|
});
|
|
1024
1047
|
});
|
|
1025
1048
|
}
|
|
1026
|
-
/**
|
|
1027
|
-
* After a successful onboard, refreshes the launchd autostart agent so intern
|
|
1028
|
-
* machines keep syncing without anyone re-running Cockpit. Runtimes without an
|
|
1029
|
-
* exec runner skip this because launchd installation cannot be attempted there.
|
|
1030
|
-
*/
|
|
1049
|
+
/** Refreshes the host autostart backend after successful onboarding. */
|
|
1031
1050
|
async function refreshOnboardAutostart(command, roots, io) {
|
|
1032
1051
|
if (!io.exec)
|
|
1033
1052
|
return null;
|
|
@@ -1046,8 +1065,8 @@ async function refreshOnboardAutostart(command, roots, io) {
|
|
|
1046
1065
|
}
|
|
1047
1066
|
writeLine(io.stdout, result.loaded
|
|
1048
1067
|
? "Background autostart refreshed; Cockpit syncs at login and every 15 min."
|
|
1049
|
-
: "Background autostart refreshed, but
|
|
1050
|
-
writeLine(io.stdout,
|
|
1068
|
+
: "Background autostart refreshed, but the operating-system scheduler reported a problem; check `cockpit autostart status`.");
|
|
1069
|
+
writeLine(io.stdout, autostartLocationLine(result));
|
|
1051
1070
|
return result;
|
|
1052
1071
|
}
|
|
1053
1072
|
async function refreshOnboardAgentRules(command, roots, io) {
|
|
@@ -1088,21 +1107,66 @@ function writeOnboardLiveStatus(io, command, roots, options) {
|
|
|
1088
1107
|
(options.status?.session_state === "valid" ? "existing valid session" : "pending")}`);
|
|
1089
1108
|
writeLine(io.stdout, `Background sync: ${backgroundSyncLine(options.autostart)}`);
|
|
1090
1109
|
writeLine(io.stdout, `Initial sync: ${options.initialSyncOk ? options.sync?.status ?? "uploaded" : "blocked"}`);
|
|
1110
|
+
writeLine(io.stdout, `Historical backfill: complete (${options.backfill.counts.backfilled} uploaded, ${options.backfill.counts.skipped} explicit skip(s))`);
|
|
1091
1111
|
writeLine(io.stdout, `Agent rules: ${options.agentRules ? onboardAgentRulesInstallLine(options.agentRules) : "not refreshed"}`);
|
|
1092
1112
|
writeLine(io.stdout, `Dashboard: ${command.dashboardUrl}/my-work`);
|
|
1093
|
-
if (options.backfillHint)
|
|
1094
|
-
writeLine(io.stdout, options.backfillHint);
|
|
1095
1113
|
writeLine(io.stdout, "Next: cockpit status");
|
|
1096
1114
|
}
|
|
1115
|
+
function requiredOnboardBackfillCommand(command) {
|
|
1116
|
+
return {
|
|
1117
|
+
homeDir: command.homeDir,
|
|
1118
|
+
all: true,
|
|
1119
|
+
dryRun: false,
|
|
1120
|
+
maxDepth: command.maxDepth,
|
|
1121
|
+
maxRepos: command.maxRepos,
|
|
1122
|
+
yes: true,
|
|
1123
|
+
json: true,
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
1126
|
+
async function runRequiredOnboardBackfill(command, io) {
|
|
1127
|
+
const backfillCommand = requiredOnboardBackfillCommand(command);
|
|
1128
|
+
const retryCommand = backfillRetryCommand(backfillCommand);
|
|
1129
|
+
try {
|
|
1130
|
+
const result = await runBackfill(backfillCommand, io);
|
|
1131
|
+
return {
|
|
1132
|
+
status: result.status,
|
|
1133
|
+
failureReason: result.status === "complete"
|
|
1134
|
+
? null
|
|
1135
|
+
: result.failure_reason ?? `backfill_${result.status}`,
|
|
1136
|
+
retryCommand: result.retry_command,
|
|
1137
|
+
result,
|
|
1138
|
+
};
|
|
1139
|
+
}
|
|
1140
|
+
catch (error) {
|
|
1141
|
+
return {
|
|
1142
|
+
status: "failed",
|
|
1143
|
+
failureReason: errorMessage(error),
|
|
1144
|
+
retryCommand,
|
|
1145
|
+
result: null,
|
|
1146
|
+
};
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
function onboardBackfillPayload(outcome) {
|
|
1150
|
+
return (outcome.result ?? {
|
|
1151
|
+
status: "blocked",
|
|
1152
|
+
failure_reason: outcome.failureReason,
|
|
1153
|
+
retry_command: outcome.retryCommand,
|
|
1154
|
+
});
|
|
1155
|
+
}
|
|
1156
|
+
function writeOnboardBackfillBlocker(io, outcome) {
|
|
1157
|
+
writeLine(io.stderr, "BLOCKED: all-history Codex and Claude backfill is incomplete for the approved collection roots.");
|
|
1158
|
+
writeLine(io.stderr, `Failure: ${outcome.failureReason ?? `backfill_${outcome.status}`}`);
|
|
1159
|
+
writeLine(io.stderr, `Retry: ${outcome.retryCommand}`);
|
|
1160
|
+
}
|
|
1097
1161
|
function backgroundSyncLine(result) {
|
|
1098
1162
|
if (!result)
|
|
1099
|
-
return "not refreshed (
|
|
1163
|
+
return "not refreshed (autostart runner unavailable)";
|
|
1100
1164
|
if (result.status === "unsupported")
|
|
1101
1165
|
return `unsupported (${result.message})`;
|
|
1102
1166
|
if (result.status === "installed" && result.loaded)
|
|
1103
1167
|
return "installed";
|
|
1104
1168
|
if (result.status === "installed") {
|
|
1105
|
-
return `installed with warning (${result.message ?? "
|
|
1169
|
+
return `installed with warning (${result.message ?? "operating-system scheduler load failed"})`;
|
|
1106
1170
|
}
|
|
1107
1171
|
return result.status;
|
|
1108
1172
|
}
|
|
@@ -1184,18 +1248,17 @@ async function runOnboard(command, io) {
|
|
|
1184
1248
|
let rootsResult = null;
|
|
1185
1249
|
let agentRules = null;
|
|
1186
1250
|
let autostart = null;
|
|
1187
|
-
let backfillHint = null;
|
|
1188
1251
|
try {
|
|
1189
1252
|
if (!command.json) {
|
|
1190
1253
|
writeLine(io.stdout, "Cockpit harvest onboarding");
|
|
1191
1254
|
writeLine(io.stdout, `Dashboard: ${command.dashboardUrl}`);
|
|
1192
1255
|
writeLine(io.stdout, `Ticket: ${command.activeTicketId ?? "general ambient"}`);
|
|
1193
1256
|
}
|
|
1194
|
-
backfillHint = await onboardBackfillHint(command.homeDir);
|
|
1195
1257
|
const resolvedRoots = await resolveOnboardingRootsForCommand(command, io);
|
|
1196
1258
|
const existingConfig = resolvedRoots.existingConfig;
|
|
1197
1259
|
rootsResult = resolvedRoots.rootsResult;
|
|
1198
1260
|
const collectionRoots = rootsResult.roots;
|
|
1261
|
+
const attributionCollectionRoots = await collectionRootConsentAliases(collectionRoots);
|
|
1199
1262
|
const primaryRoot = resolvedRoots.primaryRoot;
|
|
1200
1263
|
const resolvedCommand = {
|
|
1201
1264
|
...command,
|
|
@@ -1263,31 +1326,81 @@ async function runOnboard(command, io) {
|
|
|
1263
1326
|
writeLine(io.stdout, `Device: ${pair.session.device_name ?? pair.session.device_id ?? "unknown"}`);
|
|
1264
1327
|
}
|
|
1265
1328
|
}
|
|
1266
|
-
const worktrees = await discoverCommandWorktrees(collectionRoots, {
|
|
1329
|
+
const worktrees = await discoverCommandWorktrees(collectionRoots, {
|
|
1330
|
+
maxDepth: command.maxDepth,
|
|
1331
|
+
maxRepos: command.maxRepos,
|
|
1332
|
+
allowEmpty: true,
|
|
1333
|
+
}, io);
|
|
1267
1334
|
if (worktrees.length > 1) {
|
|
1268
1335
|
const multi = await runMultiRepoOnboard(resolvedCommand, io, worktrees);
|
|
1269
1336
|
addInstallEvent(installEvents, "work_context", "ok");
|
|
1270
1337
|
addInstallEvent(installEvents, "sync", multi.ok ? "ok" : "fail", multi.ok ? undefined : "sync_blocked");
|
|
1271
|
-
if (multi.ok) {
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1338
|
+
if (!multi.ok) {
|
|
1339
|
+
if (command.json) {
|
|
1340
|
+
writeLine(io.stdout, JSON.stringify({
|
|
1341
|
+
...onboardResult("blocked", resolvedCommand, install, pair, null, null),
|
|
1342
|
+
collection_roots: collectionRoots,
|
|
1343
|
+
root_resolution: rootsResult,
|
|
1344
|
+
mode: "multi_repo",
|
|
1345
|
+
repos: multi.results,
|
|
1346
|
+
codex_sessions: multi.codex_sessions,
|
|
1347
|
+
blocker: "sync_blocked",
|
|
1348
|
+
next_step: "Run `cockpit sync --json` until every root returns an uploaded receipt.",
|
|
1349
|
+
}, null, 2));
|
|
1350
|
+
}
|
|
1351
|
+
return finish(1);
|
|
1276
1352
|
}
|
|
1353
|
+
const backfill = await runRequiredOnboardBackfill(resolvedCommand, io);
|
|
1354
|
+
const completedBackfill = backfill.status === "complete" ? backfill.result : null;
|
|
1355
|
+
addInstallEvent(installEvents, "backfill", completedBackfill ? "ok" : "fail", completedBackfill
|
|
1356
|
+
? undefined
|
|
1357
|
+
: backfill.failureReason ?? "backfill_incomplete");
|
|
1358
|
+
if (!completedBackfill) {
|
|
1359
|
+
if (command.json) {
|
|
1360
|
+
writeLine(io.stdout, JSON.stringify({
|
|
1361
|
+
...onboardResult("blocked", resolvedCommand, install, pair, null, null),
|
|
1362
|
+
collection_roots: collectionRoots,
|
|
1363
|
+
root_resolution: rootsResult,
|
|
1364
|
+
mode: "multi_repo",
|
|
1365
|
+
repos: multi.results,
|
|
1366
|
+
codex_sessions: multi.codex_sessions,
|
|
1367
|
+
backfill: onboardBackfillPayload(backfill),
|
|
1368
|
+
blocker: "backfill_incomplete",
|
|
1369
|
+
backfill_failure_reason: backfill.failureReason,
|
|
1370
|
+
next_step: backfill.retryCommand,
|
|
1371
|
+
}, null, 2));
|
|
1372
|
+
}
|
|
1373
|
+
else {
|
|
1374
|
+
writeOnboardBackfillBlocker(io, backfill);
|
|
1375
|
+
}
|
|
1376
|
+
return finish(1);
|
|
1377
|
+
}
|
|
1378
|
+
agentRules = await refreshOnboardAgentRules(resolvedCommand, collectionRoots, io);
|
|
1379
|
+
addInstallEvent(installEvents, "agent_rules", "ok");
|
|
1380
|
+
autostart = await refreshOnboardAutostart(resolvedCommand, collectionRoots, io);
|
|
1381
|
+
addAutostartInstallEvent(installEvents, autostart);
|
|
1382
|
+
const onboardOk = !onboardAutostartFailed(autostart);
|
|
1277
1383
|
if (command.json) {
|
|
1278
1384
|
writeLine(io.stdout, JSON.stringify({
|
|
1279
|
-
...onboardResult(
|
|
1385
|
+
...onboardResult(onboardOk ? "pass" : "blocked", resolvedCommand, install, pair, null, null),
|
|
1280
1386
|
collection_roots: collectionRoots,
|
|
1281
1387
|
root_resolution: rootsResult,
|
|
1282
1388
|
agent_rules: agentRules,
|
|
1283
1389
|
autostart,
|
|
1284
|
-
|
|
1390
|
+
backfill: completedBackfill,
|
|
1285
1391
|
mode: "multi_repo",
|
|
1286
1392
|
repos: multi.results,
|
|
1287
1393
|
codex_sessions: multi.codex_sessions,
|
|
1394
|
+
...(!onboardOk && multi.ok
|
|
1395
|
+
? {
|
|
1396
|
+
blocker: "autostart_load_failed",
|
|
1397
|
+
next_step: "Run `cockpit autostart install`, then require `cockpit autostart status --json` to exit 0.",
|
|
1398
|
+
}
|
|
1399
|
+
: {}),
|
|
1288
1400
|
}, null, 2));
|
|
1289
1401
|
}
|
|
1290
|
-
if (
|
|
1402
|
+
if (onboardOk && !command.json) {
|
|
1403
|
+
writeLine(io.stdout, "PASS: Cockpit collector is ready for harvest.");
|
|
1291
1404
|
writeOnboardLiveStatus(io, resolvedCommand, collectionRoots, {
|
|
1292
1405
|
pair,
|
|
1293
1406
|
status,
|
|
@@ -1295,10 +1408,13 @@ async function runOnboard(command, io) {
|
|
|
1295
1408
|
agentRules,
|
|
1296
1409
|
autostart,
|
|
1297
1410
|
initialSyncOk: true,
|
|
1298
|
-
|
|
1411
|
+
backfill: completedBackfill,
|
|
1299
1412
|
});
|
|
1300
1413
|
}
|
|
1301
|
-
|
|
1414
|
+
else if (multi.ok && !command.json) {
|
|
1415
|
+
writeOnboardAutostartBlocker(io, autostart);
|
|
1416
|
+
}
|
|
1417
|
+
return finish(onboardOk ? 0 : 1);
|
|
1302
1418
|
}
|
|
1303
1419
|
const worktreeRoot = worktrees[0]?.repo_root ?? primaryRoot;
|
|
1304
1420
|
const context = await startLocalWorkContext({
|
|
@@ -1318,6 +1434,7 @@ async function runOnboard(command, io) {
|
|
|
1318
1434
|
const run = await runAttributedWorktreeSync({
|
|
1319
1435
|
homeDir: command.homeDir,
|
|
1320
1436
|
dashboardUrl: command.dashboardUrl,
|
|
1437
|
+
collectionRoots: attributionCollectionRoots,
|
|
1321
1438
|
startContexts: false,
|
|
1322
1439
|
worktrees,
|
|
1323
1440
|
fetchImpl: io.fetch,
|
|
@@ -1331,39 +1448,76 @@ async function runOnboard(command, io) {
|
|
|
1331
1448
|
repoRoot: worktreeRoot,
|
|
1332
1449
|
branch: command.branch,
|
|
1333
1450
|
});
|
|
1334
|
-
if (sync.status !== "uploaded") {
|
|
1451
|
+
if (!run.ok || sync.status !== "uploaded") {
|
|
1335
1452
|
addInstallEvent(installEvents, "sync", "fail", "sync_blocked");
|
|
1453
|
+
const runStatus = attributedSyncRunStatus(run);
|
|
1336
1454
|
if (command.json) {
|
|
1337
1455
|
writeLine(io.stdout, JSON.stringify({
|
|
1338
1456
|
...onboardResult("blocked", resolvedCommand, install, pair, sync, status),
|
|
1339
1457
|
collection_roots: collectionRoots,
|
|
1340
1458
|
root_resolution: rootsResult,
|
|
1341
1459
|
codex_sessions: run.summary,
|
|
1460
|
+
collection_status: runStatus,
|
|
1342
1461
|
}, null, 2));
|
|
1343
1462
|
}
|
|
1344
1463
|
else {
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1464
|
+
const syncFailureReason = sync.status === "spooled" ? sync.failure_reason : null;
|
|
1465
|
+
const retryCommand = sync.status === "spooled"
|
|
1466
|
+
? sync.retry_command
|
|
1467
|
+
: `cockpit sync --workspace ${JSON.stringify(worktreeRoot)}`;
|
|
1468
|
+
writeLine(io.stderr, "BLOCKED: initial collection is incomplete; retry until every eligible session has a durable receipt or explicit terminal reason.");
|
|
1469
|
+
writeLine(io.stderr, `Failure: ${syncFailureReason ?? (run.summary.report_posted ? runStatus : run.summary.report_reason)}`);
|
|
1470
|
+
writeAgentSessionSummary(io, run.summary);
|
|
1471
|
+
writeLine(io.stderr, `Retry: ${retryCommand}`);
|
|
1348
1472
|
}
|
|
1349
1473
|
return finish(1);
|
|
1350
1474
|
}
|
|
1351
1475
|
addInstallEvent(installEvents, "sync", "ok");
|
|
1476
|
+
const backfill = await runRequiredOnboardBackfill(resolvedCommand, io);
|
|
1477
|
+
const completedBackfill = backfill.status === "complete" ? backfill.result : null;
|
|
1478
|
+
addInstallEvent(installEvents, "backfill", completedBackfill ? "ok" : "fail", completedBackfill
|
|
1479
|
+
? undefined
|
|
1480
|
+
: backfill.failureReason ?? "backfill_incomplete");
|
|
1481
|
+
if (!completedBackfill) {
|
|
1482
|
+
if (command.json) {
|
|
1483
|
+
writeLine(io.stdout, JSON.stringify({
|
|
1484
|
+
...onboardResult("blocked", resolvedCommand, install, pair, sync, status),
|
|
1485
|
+
collection_roots: collectionRoots,
|
|
1486
|
+
root_resolution: rootsResult,
|
|
1487
|
+
codex_sessions: run.summary,
|
|
1488
|
+
backfill: onboardBackfillPayload(backfill),
|
|
1489
|
+
blocker: "backfill_incomplete",
|
|
1490
|
+
backfill_failure_reason: backfill.failureReason,
|
|
1491
|
+
next_step: backfill.retryCommand,
|
|
1492
|
+
}, null, 2));
|
|
1493
|
+
}
|
|
1494
|
+
else {
|
|
1495
|
+
writeOnboardBackfillBlocker(io, backfill);
|
|
1496
|
+
}
|
|
1497
|
+
return finish(1);
|
|
1498
|
+
}
|
|
1352
1499
|
if (command.json) {
|
|
1353
1500
|
agentRules = await refreshOnboardAgentRules(resolvedCommand, collectionRoots, io);
|
|
1354
1501
|
addInstallEvent(installEvents, "agent_rules", "ok");
|
|
1355
1502
|
autostart = await refreshOnboardAutostart(resolvedCommand, collectionRoots, io);
|
|
1356
1503
|
addAutostartInstallEvent(installEvents, autostart);
|
|
1504
|
+
const onboardOk = !onboardAutostartFailed(autostart);
|
|
1357
1505
|
writeLine(io.stdout, JSON.stringify({
|
|
1358
|
-
...onboardResult("pass", resolvedCommand, install, pair, sync, status),
|
|
1506
|
+
...onboardResult(onboardOk ? "pass" : "blocked", resolvedCommand, install, pair, sync, status),
|
|
1359
1507
|
collection_roots: collectionRoots,
|
|
1360
1508
|
root_resolution: rootsResult,
|
|
1361
1509
|
agent_rules: agentRules,
|
|
1362
1510
|
autostart,
|
|
1363
|
-
|
|
1511
|
+
backfill: completedBackfill,
|
|
1364
1512
|
codex_sessions: run.summary,
|
|
1513
|
+
...(!onboardOk
|
|
1514
|
+
? {
|
|
1515
|
+
blocker: "autostart_load_failed",
|
|
1516
|
+
next_step: "Run `cockpit autostart install`, then require `cockpit autostart status --json` to exit 0.",
|
|
1517
|
+
}
|
|
1518
|
+
: {}),
|
|
1365
1519
|
}, null, 2));
|
|
1366
|
-
return finish(0);
|
|
1520
|
+
return finish(onboardOk ? 0 : 1);
|
|
1367
1521
|
}
|
|
1368
1522
|
writeLine(io.stdout, "4/5 Ambient metadata uploaded.");
|
|
1369
1523
|
writeLine(io.stdout, `HTTP: ${sync.http_status}`);
|
|
@@ -1375,12 +1529,16 @@ async function runOnboard(command, io) {
|
|
|
1375
1529
|
writeAgentSessionSummary(io, run.summary);
|
|
1376
1530
|
writeLine(io.stdout, "5/5 Status ready.");
|
|
1377
1531
|
writeLine(io.stdout, `Upload state: ${status.upload_state}`);
|
|
1378
|
-
writeLine(io.stdout, "PASS: Cockpit collector is ready for harvest.");
|
|
1379
1532
|
writeLine(io.stdout, `Open: ${command.dashboardUrl}/my-work`);
|
|
1380
1533
|
agentRules = await refreshOnboardAgentRules(resolvedCommand, collectionRoots, io);
|
|
1381
1534
|
addInstallEvent(installEvents, "agent_rules", "ok");
|
|
1382
1535
|
autostart = await refreshOnboardAutostart(resolvedCommand, collectionRoots, io);
|
|
1383
1536
|
addAutostartInstallEvent(installEvents, autostart);
|
|
1537
|
+
if (onboardAutostartFailed(autostart)) {
|
|
1538
|
+
writeOnboardAutostartBlocker(io, autostart);
|
|
1539
|
+
return finish(1);
|
|
1540
|
+
}
|
|
1541
|
+
writeLine(io.stdout, "PASS: Cockpit collector is ready for harvest.");
|
|
1384
1542
|
writeOnboardLiveStatus(io, resolvedCommand, collectionRoots, {
|
|
1385
1543
|
pair,
|
|
1386
1544
|
status,
|
|
@@ -1388,7 +1546,7 @@ async function runOnboard(command, io) {
|
|
|
1388
1546
|
agentRules,
|
|
1389
1547
|
autostart,
|
|
1390
1548
|
initialSyncOk: true,
|
|
1391
|
-
|
|
1549
|
+
backfill: completedBackfill,
|
|
1392
1550
|
});
|
|
1393
1551
|
return finish(0);
|
|
1394
1552
|
}
|
|
@@ -1456,7 +1614,8 @@ function shortSha(value) {
|
|
|
1456
1614
|
return value ? value.slice(0, 12) : "unknown";
|
|
1457
1615
|
}
|
|
1458
1616
|
function sourceFunnelLine(label, counts) {
|
|
1459
|
-
|
|
1617
|
+
const readFailures = counts.read_failures > 0 ? `, read_failures ${counts.read_failures}` : "";
|
|
1618
|
+
return `${label} sessions: attributed ${counts.attributed}, fallback ${counts.attributed_fallback}, ambiguous ${counts.ambiguous}, unattributed ${counts.unattributed}, skipped ${counts.skipped}, stale ${counts.stale}${readFailures}`;
|
|
1460
1619
|
}
|
|
1461
1620
|
function attributionReportLine(summary) {
|
|
1462
1621
|
return summary.report_posted
|
|
@@ -1511,7 +1670,10 @@ function rawEvidenceSyncLine(sync) {
|
|
|
1511
1670
|
const failures = sync.raw_evidence_failure_reasons.length > 0
|
|
1512
1671
|
? ` failures: ${sync.raw_evidence_failure_reasons.join(",")}`
|
|
1513
1672
|
: "";
|
|
1514
|
-
|
|
1673
|
+
const retries = sync.raw_evidence_retry_reasons.length > 0
|
|
1674
|
+
? ` retry_required: ${sync.raw_evidence_retry_reasons.join(",")}`
|
|
1675
|
+
: "";
|
|
1676
|
+
return `Raw evidence: uploaded ${sync.raw_evidence_uploaded_object_count} object(s) in ${sync.raw_evidence_uploaded_chunk_count} chunk(s), reused ${sync.raw_evidence_reused_count}, failed ${sync.raw_evidence_failed_count}${failures}${retries}`;
|
|
1515
1677
|
}
|
|
1516
1678
|
function cursorStatusLine(sync) {
|
|
1517
1679
|
return `Cursor: ${sync.cursor_tracked_object_count} durable object(s) tracked`;
|
|
@@ -1521,75 +1683,69 @@ const DEFAULT_DISCOVERY_MAX_REPOS = 50;
|
|
|
1521
1683
|
const ALL_SESSION_SCAN_WINDOW_MINUTES = 20 * 365 * 24 * 60;
|
|
1522
1684
|
const SESSION_SCAN_OVERRIDE_LIMIT = 10_000;
|
|
1523
1685
|
async function discoverCommandWorktrees(repoRoot, discovery = {}, io) {
|
|
1524
|
-
if (Array.isArray(repoRoot)) {
|
|
1525
|
-
const discovered = [];
|
|
1526
|
-
for (const root of repoRoot) {
|
|
1527
|
-
discovered.push(...(await discoverCommandWorktrees(root, discovery, io)));
|
|
1528
|
-
}
|
|
1529
|
-
return dedupeWorktrees(discovered);
|
|
1530
|
-
}
|
|
1531
1686
|
const maxWorktrees = discovery.maxRepos ?? DEFAULT_DISCOVERY_MAX_REPOS;
|
|
1532
|
-
const
|
|
1687
|
+
const roots = Array.isArray(repoRoot)
|
|
1688
|
+
? repoRoot
|
|
1689
|
+
: [repoRoot ?? process.cwd()];
|
|
1690
|
+
const result = await discoverGitWorktreesInRootsWithStatus(roots, {
|
|
1533
1691
|
maxDepth: discovery.maxDepth ?? DEFAULT_DISCOVERY_MAX_DEPTH,
|
|
1534
1692
|
maxWorktrees,
|
|
1535
1693
|
});
|
|
1536
|
-
|
|
1537
|
-
|
|
1694
|
+
const worktrees = result.worktrees;
|
|
1695
|
+
if (!result.complete) {
|
|
1696
|
+
const reasons = result.incomplete_reasons.join(",");
|
|
1697
|
+
if (io) {
|
|
1698
|
+
writeLine(io.stderr, `BLOCKED: repo discovery is incomplete (${reasons}); no collection cursor was advanced.`);
|
|
1699
|
+
}
|
|
1700
|
+
throw new Error(`repo discovery incomplete: ${reasons}. Raise --max-depth or --max-repos until every approved-root repo is included.`);
|
|
1538
1701
|
}
|
|
1539
|
-
if (
|
|
1540
|
-
|
|
1702
|
+
if (worktrees.length === 0 && !discovery.allowEmpty) {
|
|
1703
|
+
throw new Error("No git repos found. Run from a git repo, or from a parent folder containing git repos.");
|
|
1541
1704
|
}
|
|
1542
1705
|
return worktrees;
|
|
1543
1706
|
}
|
|
1544
|
-
function dedupeWorktrees(worktrees) {
|
|
1545
|
-
const seen = new Set();
|
|
1546
|
-
const deduped = [];
|
|
1547
|
-
for (const worktree of worktrees) {
|
|
1548
|
-
const key = worktree.worktree_fingerprint || path.resolve(worktree.repo_root);
|
|
1549
|
-
if (seen.has(key))
|
|
1550
|
-
continue;
|
|
1551
|
-
seen.add(key);
|
|
1552
|
-
deduped.push(worktree);
|
|
1553
|
-
}
|
|
1554
|
-
return deduped;
|
|
1555
|
-
}
|
|
1556
1707
|
async function runMultiRepoOnboard(command, io, worktrees) {
|
|
1557
1708
|
if (!command.json) {
|
|
1558
1709
|
writeLine(io.stdout, `3/5 Parent folder mode: discovered ${worktrees.length} git worktree(s).`);
|
|
1559
1710
|
}
|
|
1711
|
+
const collectionRoots = await collectionRootConsentAliases(command.collectionRoots ??
|
|
1712
|
+
(command.repoRoot ? [command.repoRoot] : []));
|
|
1560
1713
|
const run = await runAttributedWorktreeSync({
|
|
1561
1714
|
homeDir: command.homeDir,
|
|
1562
1715
|
dashboardUrl: command.dashboardUrl,
|
|
1563
1716
|
branch: command.branch,
|
|
1564
1717
|
activeTicketId: command.activeTicketId,
|
|
1718
|
+
collectionRoots,
|
|
1565
1719
|
startContexts: true,
|
|
1566
1720
|
worktrees,
|
|
1567
1721
|
fetchImpl: io.fetch,
|
|
1568
1722
|
});
|
|
1569
1723
|
const results = run.outcomes.map((outcome) => worktreeSyncRow(outcome, run));
|
|
1724
|
+
const runStatus = attributedSyncRunStatus(run);
|
|
1570
1725
|
if (!command.json) {
|
|
1571
1726
|
for (const [index, outcome] of run.outcomes.entries()) {
|
|
1572
1727
|
const row = results[index];
|
|
1573
1728
|
if (!row)
|
|
1574
1729
|
continue;
|
|
1575
1730
|
const uploaded = outcome.sync.status === "uploaded";
|
|
1576
|
-
writeLine(uploaded ? io.stdout : io.stderr, `${uploaded ? "
|
|
1731
|
+
writeLine(uploaded ? io.stdout : io.stderr, `${uploaded ? "SYNCED" : "BLOCKED"} ${row["repo_label"]}/${row["worktree_label"]} (${row["branch"]}) head:${shortSha(outcome.sync.head_sha ?? outcome.worktree.head_sha)} ${outcome.sync.status} objects:${outcome.sync.raw_evidence_uploaded_object_count} chunks:${outcome.sync.raw_evidence_uploaded_chunk_count} reused:${outcome.sync.raw_evidence_reused_count} failed:${outcome.sync.raw_evidence_failed_count} sessions:${row["attributed_session_count"]}${row["failure_reason"] ? ` reason:${row["failure_reason"]}` : ""}`);
|
|
1577
1732
|
}
|
|
1578
1733
|
writeLine(io.stdout, "4/5 Parent worktree sync complete.");
|
|
1579
1734
|
writeLine(io.stdout, `Uploaded: ${results.filter((row) => row["upload_status"] === "uploaded").length}/${results.length}`);
|
|
1580
1735
|
writeAgentSessionSummary(io, run.summary);
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1736
|
+
if (run.ok) {
|
|
1737
|
+
writeLine(io.stdout, "Live sync receipts complete; verifying all-history Codex and Claude backfill.");
|
|
1738
|
+
}
|
|
1739
|
+
else {
|
|
1740
|
+
writeLine(io.stderr, `BLOCKED: Cockpit collection is ${runStatus}; retry until every eligible session has a durable receipt or explicit terminal reason.`);
|
|
1741
|
+
}
|
|
1585
1742
|
writeLine(io.stdout, `Open: ${command.dashboardUrl}/my-work`);
|
|
1586
1743
|
}
|
|
1587
1744
|
return { ok: run.ok, results, codex_sessions: run.summary };
|
|
1588
1745
|
}
|
|
1589
1746
|
function worktreeSyncRow(outcome, run) {
|
|
1590
1747
|
const { worktree, context, sync } = outcome;
|
|
1591
|
-
const matchesWorktree = (result) => result
|
|
1592
|
-
result.worktree?.worktree_fingerprint === worktree.worktree_fingerprint;
|
|
1748
|
+
const matchesWorktree = (result) => matchesLiveSyncWorktree(result, worktree);
|
|
1593
1749
|
const codexSessionCount = run.codexAttribution.results.filter(matchesWorktree).length;
|
|
1594
1750
|
const claudeSessionCount = run.claudeAttribution.results.filter(matchesWorktree).length;
|
|
1595
1751
|
const attributedSessionCount = codexSessionCount + claudeSessionCount;
|
|
@@ -1608,6 +1764,8 @@ function worktreeSyncRow(outcome, run) {
|
|
|
1608
1764
|
raw_evidence_reused_count: sync.raw_evidence_reused_count,
|
|
1609
1765
|
raw_evidence_failed_count: sync.raw_evidence_failed_count,
|
|
1610
1766
|
raw_evidence_failure_reasons: sync.raw_evidence_failure_reasons,
|
|
1767
|
+
raw_evidence_retry_required: sync.raw_evidence_retry_required,
|
|
1768
|
+
raw_evidence_retry_reasons: sync.raw_evidence_retry_reasons,
|
|
1611
1769
|
attributed_session_count: attributedSessionCount,
|
|
1612
1770
|
codex_session_count: codexSessionCount,
|
|
1613
1771
|
claude_session_count: claudeSessionCount,
|
|
@@ -1790,22 +1948,16 @@ async function runSync(command, io) {
|
|
|
1790
1948
|
io,
|
|
1791
1949
|
});
|
|
1792
1950
|
try {
|
|
1793
|
-
const
|
|
1951
|
+
const result = await runSyncWithHealthReceipt(command, io);
|
|
1794
1952
|
await reportInstallEventsBestEffort({
|
|
1795
1953
|
homeDir: command.homeDir,
|
|
1796
1954
|
dashboardUrl,
|
|
1797
1955
|
command: "sync",
|
|
1798
|
-
events: [
|
|
1799
|
-
{
|
|
1800
|
-
step: "sync_complete",
|
|
1801
|
-
status: code === 0 ? "ok" : "fail",
|
|
1802
|
-
...(code === 0 ? {} : { error_code: "sync_failed" }),
|
|
1803
|
-
},
|
|
1804
|
-
],
|
|
1956
|
+
events: [result.completion],
|
|
1805
1957
|
json: command.json,
|
|
1806
1958
|
io,
|
|
1807
1959
|
});
|
|
1808
|
-
return
|
|
1960
|
+
return result.exitCode;
|
|
1809
1961
|
}
|
|
1810
1962
|
catch (error) {
|
|
1811
1963
|
await reportInstallEventsBestEffort({
|
|
@@ -1833,27 +1985,59 @@ async function runSyncWithHealthReceipt(command, io) {
|
|
|
1833
1985
|
status: "live_sync_paused_during_backfill",
|
|
1834
1986
|
reason: "live sync paused during backfill",
|
|
1835
1987
|
held_since: backfillLock.held_since,
|
|
1988
|
+
collection_complete: false,
|
|
1989
|
+
upload_state: "not_uploaded",
|
|
1990
|
+
retryable: true,
|
|
1836
1991
|
}, null, 2));
|
|
1837
1992
|
}
|
|
1838
1993
|
else {
|
|
1839
1994
|
writeLine(io.stdout, "live sync paused during backfill");
|
|
1840
1995
|
}
|
|
1841
|
-
return
|
|
1996
|
+
return {
|
|
1997
|
+
exitCode: 0,
|
|
1998
|
+
completion: {
|
|
1999
|
+
step: "sync_complete",
|
|
2000
|
+
status: "skipped",
|
|
2001
|
+
error_code: "live_sync_paused_during_backfill",
|
|
2002
|
+
},
|
|
2003
|
+
};
|
|
1842
2004
|
}
|
|
1843
2005
|
// Single-flight: a launchd timer and a manual sync must not interleave the
|
|
1844
2006
|
// cursor read-modify-write. A blocked invocation exits cleanly (B.4 §7).
|
|
1845
2007
|
const lock = await acquireSyncLock(getCollectorRuntimePaths(command.homeDir));
|
|
1846
2008
|
if (!lock.acquired) {
|
|
1847
2009
|
if (command.json) {
|
|
1848
|
-
writeLine(io.stdout, JSON.stringify({
|
|
2010
|
+
writeLine(io.stdout, JSON.stringify({
|
|
2011
|
+
status: "sync_already_running",
|
|
2012
|
+
reason: "another sync owns the collection lock",
|
|
2013
|
+
held_since: lock.held_since,
|
|
2014
|
+
collection_complete: false,
|
|
2015
|
+
upload_state: "not_uploaded",
|
|
2016
|
+
retryable: true,
|
|
2017
|
+
}, null, 2));
|
|
1849
2018
|
}
|
|
1850
2019
|
else {
|
|
1851
2020
|
writeLine(io.stdout, "Cockpit sync already running; skipping this run.");
|
|
1852
2021
|
}
|
|
1853
|
-
return
|
|
2022
|
+
return {
|
|
2023
|
+
exitCode: 0,
|
|
2024
|
+
completion: {
|
|
2025
|
+
step: "sync_complete",
|
|
2026
|
+
status: "skipped",
|
|
2027
|
+
error_code: "sync_already_running",
|
|
2028
|
+
},
|
|
2029
|
+
};
|
|
1854
2030
|
}
|
|
1855
2031
|
try {
|
|
1856
|
-
|
|
2032
|
+
const exitCode = await runSyncLocked(command, io);
|
|
2033
|
+
return {
|
|
2034
|
+
exitCode,
|
|
2035
|
+
completion: {
|
|
2036
|
+
step: "sync_complete",
|
|
2037
|
+
status: exitCode === 0 ? "ok" : "fail",
|
|
2038
|
+
...(exitCode === 0 ? {} : { error_code: "sync_failed" }),
|
|
2039
|
+
},
|
|
2040
|
+
};
|
|
1857
2041
|
}
|
|
1858
2042
|
finally {
|
|
1859
2043
|
await lock.handle.release();
|
|
@@ -1873,21 +2057,29 @@ function classifySyncHealthError(error) {
|
|
|
1873
2057
|
return "sync_failed";
|
|
1874
2058
|
}
|
|
1875
2059
|
async function runSyncLocked(command, io) {
|
|
1876
|
-
const
|
|
2060
|
+
const collectionRoots = await resolveSyncCollectionRoots(command);
|
|
2061
|
+
const worktrees = await discoverCommandWorktrees(collectionRoots, {
|
|
2062
|
+
maxDepth: command.maxDepth,
|
|
2063
|
+
maxRepos: command.maxRepos,
|
|
2064
|
+
allowEmpty: true,
|
|
2065
|
+
}, io);
|
|
1877
2066
|
const run = await runAttributedWorktreeSync({
|
|
1878
2067
|
homeDir: command.homeDir,
|
|
1879
2068
|
dashboardUrl: command.dashboardUrl,
|
|
2069
|
+
collectionRoots,
|
|
1880
2070
|
startContexts: false,
|
|
1881
2071
|
worktrees,
|
|
1882
2072
|
fetchImpl: io.fetch,
|
|
1883
2073
|
});
|
|
1884
|
-
if (
|
|
2074
|
+
if (run.outcomes.length > 1) {
|
|
1885
2075
|
const rows = run.outcomes.map((outcome) => worktreeSyncRow(outcome, run));
|
|
2076
|
+
const runStatus = attributedSyncRunStatus(run);
|
|
1886
2077
|
const gc = run.ok ? await runSyncRawEvidenceGc(command, io) : null;
|
|
1887
2078
|
if (command.json) {
|
|
1888
2079
|
writeLine(io.stdout, JSON.stringify({
|
|
1889
2080
|
mode: "multi_repo",
|
|
1890
|
-
status:
|
|
2081
|
+
status: runStatus,
|
|
2082
|
+
collection_complete: run.ok,
|
|
1891
2083
|
results: run.outcomes.map((outcome) => outcome.sync),
|
|
1892
2084
|
repos: rows,
|
|
1893
2085
|
codex_sessions: run.summary,
|
|
@@ -1895,7 +2087,7 @@ async function runSyncLocked(command, io) {
|
|
|
1895
2087
|
}, null, 2));
|
|
1896
2088
|
return run.ok ? 0 : 1;
|
|
1897
2089
|
}
|
|
1898
|
-
writeLine(run.ok ? io.stdout : io.stderr, `Cockpit parent sync ${
|
|
2090
|
+
writeLine(run.ok ? io.stdout : io.stderr, `Cockpit parent sync ${runStatus} ${run.outcomes.filter((outcome) => outcome.sync.status === "uploaded").length}/${run.outcomes.length} worktree(s).`);
|
|
1899
2091
|
for (const outcome of run.outcomes) {
|
|
1900
2092
|
const { worktree, sync } = outcome;
|
|
1901
2093
|
const uploaded = sync.status === "uploaded";
|
|
@@ -1911,12 +2103,19 @@ async function runSyncLocked(command, io) {
|
|
|
1911
2103
|
if (!result) {
|
|
1912
2104
|
throw new Error("Sync produced no result for the repo worktree.");
|
|
1913
2105
|
}
|
|
1914
|
-
const
|
|
2106
|
+
const runStatus = attributedSyncRunStatus(run);
|
|
2107
|
+
const gc = run.ok ? await runSyncRawEvidenceGc(command, io) : null;
|
|
1915
2108
|
if (command.json) {
|
|
1916
|
-
writeLine(io.stdout, JSON.stringify({
|
|
1917
|
-
|
|
2109
|
+
writeLine(io.stdout, JSON.stringify({
|
|
2110
|
+
...result,
|
|
2111
|
+
status: runStatus,
|
|
2112
|
+
collection_complete: run.ok,
|
|
2113
|
+
codex_sessions: run.summary,
|
|
2114
|
+
raw_evidence_gc: gc,
|
|
2115
|
+
}, null, 2));
|
|
2116
|
+
return run.ok ? 0 : 1;
|
|
1918
2117
|
}
|
|
1919
|
-
if (
|
|
2118
|
+
if (run.ok) {
|
|
1920
2119
|
writeLine(io.stdout, "Cockpit ambient envelope uploaded.");
|
|
1921
2120
|
writeLine(io.stdout, `Ticket: ${displayTicketId(result.ticket_id)}`);
|
|
1922
2121
|
writeLine(io.stdout, `Context: ${result.work_context_id}`);
|
|
@@ -1931,12 +2130,43 @@ async function runSyncLocked(command, io) {
|
|
|
1931
2130
|
writeLine(io.stdout, rawEvidenceGcSummary(gc));
|
|
1932
2131
|
return 0;
|
|
1933
2132
|
}
|
|
2133
|
+
if (result.status === "uploaded") {
|
|
2134
|
+
writeLine(io.stderr, "Cockpit ambient upload was accepted, but session collection is partial; retry `cockpit sync`.");
|
|
2135
|
+
writeAgentSessionSummary(io, run.summary);
|
|
2136
|
+
return 1;
|
|
2137
|
+
}
|
|
1934
2138
|
writeLine(io.stderr, "Cockpit ambient upload failed; safe retry metadata was spooled.");
|
|
1935
2139
|
writeLine(io.stderr, `Ticket: ${displayTicketId(result.ticket_id)}`);
|
|
1936
2140
|
writeLine(io.stderr, `Failure: ${result.failure_reason}`);
|
|
1937
2141
|
writeLine(io.stderr, `Retry: ${result.retry_command}`);
|
|
1938
2142
|
return 1;
|
|
1939
2143
|
}
|
|
2144
|
+
async function resolveSyncCollectionRoots(command) {
|
|
2145
|
+
const explicitRoots = normalizeCollectionRoots(command.repoRoot ? [command.repoRoot] : []);
|
|
2146
|
+
if (explicitRoots.length > 0) {
|
|
2147
|
+
return collectionRootConsentAliases(explicitRoots);
|
|
2148
|
+
}
|
|
2149
|
+
const config = await readLocalCollectorConfig(getCollectorRuntimePaths(command.homeDir)).catch(() => null);
|
|
2150
|
+
const savedRoots = normalizeCollectionRoots(config?.default_repo_paths ?? []);
|
|
2151
|
+
if (savedRoots.length > 0) {
|
|
2152
|
+
return collectionRootConsentAliases(savedRoots);
|
|
2153
|
+
}
|
|
2154
|
+
throw new Error(`${COLLECTION_ROOT_REQUIRED}: no explicit or saved collection root is available.`);
|
|
2155
|
+
}
|
|
2156
|
+
async function collectionRootConsentAliases(roots) {
|
|
2157
|
+
// Keep both spellings when an existing root is reached through a filesystem
|
|
2158
|
+
// alias such as macOS `/var` -> `/private/var`. Deleted child paths cannot be
|
|
2159
|
+
// realpathed later, so either transcript spelling must remain inside the same
|
|
2160
|
+
// operator-approved physical root.
|
|
2161
|
+
return normalizeCollectionRoots(await collectionRootPathAliases(roots));
|
|
2162
|
+
}
|
|
2163
|
+
function attributedSyncRunStatus(run) {
|
|
2164
|
+
if (run.ok)
|
|
2165
|
+
return "uploaded";
|
|
2166
|
+
return run.outcomes.every((outcome) => outcome.sync.status === "uploaded")
|
|
2167
|
+
? "partial"
|
|
2168
|
+
: "spooled";
|
|
2169
|
+
}
|
|
1940
2170
|
async function runAnalyze(command, io) {
|
|
1941
2171
|
const syncStdout = [];
|
|
1942
2172
|
const syncStderr = [];
|
|
@@ -2127,39 +2357,20 @@ function displayWorkLabel(status) {
|
|
|
2127
2357
|
return `${status.work_label} (${status.work_id})`;
|
|
2128
2358
|
return status.work_label ?? status.work_id ?? "no active work context";
|
|
2129
2359
|
}
|
|
2130
|
-
async function fileExists(filePath) {
|
|
2131
|
-
try {
|
|
2132
|
-
await access(filePath);
|
|
2133
|
-
return true;
|
|
2134
|
-
}
|
|
2135
|
-
catch {
|
|
2136
|
-
return false;
|
|
2137
|
-
}
|
|
2138
|
-
}
|
|
2139
|
-
async function onboardBackfillHint(homeDir) {
|
|
2140
|
-
const paths = getCollectorRuntimePaths(homeDir);
|
|
2141
|
-
if (await fileExists(backfillCompletionMarkerPath(paths)))
|
|
2142
|
-
return null;
|
|
2143
|
-
const hasArchivedSessions = await hasAnyJsonlFile([
|
|
2144
|
-
path.join(homeDir ?? os.homedir(), ".codex", "archived_sessions"),
|
|
2145
|
-
]);
|
|
2146
|
-
return hasArchivedSessions
|
|
2147
|
-
? "Backfill: archived sessions found. Run `cockpit backfill --all` to include older local history."
|
|
2148
|
-
: null;
|
|
2149
|
-
}
|
|
2150
2360
|
async function inspectBackfillCursor(homeDir) {
|
|
2151
2361
|
const paths = getCollectorRuntimePaths(homeDir);
|
|
2362
|
+
const roots = await currentBackfillRoots(homeDir);
|
|
2152
2363
|
const marker = await readBackfillCompletionMarker(paths);
|
|
2153
|
-
if (marker) {
|
|
2364
|
+
if (backfillCompletionCovers(marker, roots, ["codex", "claude_code"])) {
|
|
2154
2365
|
return {
|
|
2155
2366
|
state: "done",
|
|
2156
2367
|
remaining_count: 0,
|
|
2157
|
-
updated_at: marker
|
|
2158
|
-
completed_at: marker
|
|
2159
|
-
sources: summarizeBackfillCursorSources(marker
|
|
2368
|
+
updated_at: marker?.cursor.updated_at ?? null,
|
|
2369
|
+
completed_at: marker?.completed_at ?? null,
|
|
2370
|
+
sources: summarizeBackfillCursorSources(marker?.cursor ?? emptyBackfillCursorState()),
|
|
2160
2371
|
};
|
|
2161
2372
|
}
|
|
2162
|
-
const cursor = await readBackfillCursor(paths);
|
|
2373
|
+
const cursor = (prepareBackfillCursorForScope(await readBackfillCursor(paths), roots, ["codex", "claude_code"])).cursor;
|
|
2163
2374
|
if (!cursor.updated_at) {
|
|
2164
2375
|
return {
|
|
2165
2376
|
state: "never_run",
|
|
@@ -2177,60 +2388,9 @@ async function inspectBackfillCursor(homeDir) {
|
|
|
2177
2388
|
sources: summarizeBackfillCursorSources(cursor),
|
|
2178
2389
|
};
|
|
2179
2390
|
}
|
|
2180
|
-
async function
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
if (typeof raw.completed_at !== "string")
|
|
2184
|
-
return null;
|
|
2185
|
-
const cursor = raw.cursor && typeof raw.cursor === "object"
|
|
2186
|
-
? normalizeBackfillCursor(raw.cursor)
|
|
2187
|
-
: await readBackfillCursor(paths);
|
|
2188
|
-
return { completed_at: raw.completed_at, cursor };
|
|
2189
|
-
}
|
|
2190
|
-
catch {
|
|
2191
|
-
return null;
|
|
2192
|
-
}
|
|
2193
|
-
}
|
|
2194
|
-
function normalizeBackfillCursor(value) {
|
|
2195
|
-
const record = value;
|
|
2196
|
-
return {
|
|
2197
|
-
schema_version: "cockpit-backfill-cursor.v1",
|
|
2198
|
-
updated_at: typeof record.updated_at === "string" && record.updated_at
|
|
2199
|
-
? record.updated_at
|
|
2200
|
-
: null,
|
|
2201
|
-
sources: {
|
|
2202
|
-
codex: {
|
|
2203
|
-
oldest_mtime_ms_processed: typeof record.sources?.codex?.oldest_mtime_ms_processed === "number"
|
|
2204
|
-
? record.sources.codex.oldest_mtime_ms_processed
|
|
2205
|
-
: null,
|
|
2206
|
-
oldest_mtime_processed: typeof record.sources?.codex?.oldest_mtime_processed === "string"
|
|
2207
|
-
? record.sources.codex.oldest_mtime_processed
|
|
2208
|
-
: null,
|
|
2209
|
-
state_counts: numberRecord(record.sources?.codex?.state_counts),
|
|
2210
|
-
reason_counts: numberRecord(record.sources?.codex?.reason_counts),
|
|
2211
|
-
},
|
|
2212
|
-
claude_code: {
|
|
2213
|
-
oldest_mtime_ms_processed: typeof record.sources?.claude_code?.oldest_mtime_ms_processed === "number"
|
|
2214
|
-
? record.sources.claude_code.oldest_mtime_ms_processed
|
|
2215
|
-
: null,
|
|
2216
|
-
oldest_mtime_processed: typeof record.sources?.claude_code?.oldest_mtime_processed === "string"
|
|
2217
|
-
? record.sources.claude_code.oldest_mtime_processed
|
|
2218
|
-
: null,
|
|
2219
|
-
state_counts: numberRecord(record.sources?.claude_code?.state_counts),
|
|
2220
|
-
reason_counts: numberRecord(record.sources?.claude_code?.reason_counts),
|
|
2221
|
-
},
|
|
2222
|
-
},
|
|
2223
|
-
};
|
|
2224
|
-
}
|
|
2225
|
-
function numberRecord(value) {
|
|
2226
|
-
if (!value || typeof value !== "object")
|
|
2227
|
-
return {};
|
|
2228
|
-
const out = {};
|
|
2229
|
-
for (const [key, raw] of Object.entries(value)) {
|
|
2230
|
-
if (typeof raw === "number" && Number.isFinite(raw))
|
|
2231
|
-
out[key] = raw;
|
|
2232
|
-
}
|
|
2233
|
-
return out;
|
|
2391
|
+
async function currentBackfillRoots(homeDir) {
|
|
2392
|
+
const config = await readLocalCollectorConfig(getCollectorRuntimePaths(homeDir)).catch(() => null);
|
|
2393
|
+
return normalizeCollectionRoots(config?.default_repo_paths ?? []);
|
|
2234
2394
|
}
|
|
2235
2395
|
function summarizeBackfillCursorSources(cursor) {
|
|
2236
2396
|
return {
|
|
@@ -2289,14 +2449,6 @@ async function countClaudeMainFilesBeforeCursor(projectsDir, oldestProcessedMs)
|
|
|
2289
2449
|
});
|
|
2290
2450
|
return count;
|
|
2291
2451
|
}
|
|
2292
|
-
async function hasAnyJsonlFile(roots) {
|
|
2293
|
-
let found = false;
|
|
2294
|
-
await walkFiles(roots, async (_filePath, entryName) => {
|
|
2295
|
-
if (entryName.endsWith(".jsonl"))
|
|
2296
|
-
found = true;
|
|
2297
|
-
}, () => found);
|
|
2298
|
-
return found;
|
|
2299
|
-
}
|
|
2300
2452
|
async function walkFiles(roots, onFile, shouldStop = () => false) {
|
|
2301
2453
|
const stack = [...roots];
|
|
2302
2454
|
while (stack.length > 0 && !shouldStop()) {
|
|
@@ -2486,23 +2638,51 @@ async function runServe(command, io) {
|
|
|
2486
2638
|
}
|
|
2487
2639
|
async function runAutostart(command, io) {
|
|
2488
2640
|
const exec = io.exec ?? defaultExec();
|
|
2641
|
+
const repoRoots = command.action === "install" || command.action === "status"
|
|
2642
|
+
? await resolveAutostartRoots(command.homeDir, command.repoRoot)
|
|
2643
|
+
: [];
|
|
2489
2644
|
const result = command.action === "install"
|
|
2490
2645
|
? await installAutostartAgent({
|
|
2491
2646
|
homeDir: command.homeDir,
|
|
2492
|
-
repoRoot:
|
|
2647
|
+
repoRoot: repoRoots[0],
|
|
2648
|
+
repoRoots,
|
|
2493
2649
|
dashboardUrl: command.dashboardUrl,
|
|
2494
2650
|
intervalSeconds: command.intervalSeconds,
|
|
2495
2651
|
exec,
|
|
2496
2652
|
})
|
|
2497
2653
|
: command.action === "uninstall"
|
|
2498
2654
|
? await uninstallAutostartAgent({ homeDir: command.homeDir, exec })
|
|
2499
|
-
: await autostartStatus({
|
|
2655
|
+
: await autostartStatus({
|
|
2656
|
+
homeDir: command.homeDir,
|
|
2657
|
+
repoRoot: repoRoots[0],
|
|
2658
|
+
repoRoots,
|
|
2659
|
+
dashboardUrl: command.dashboardUrl,
|
|
2660
|
+
intervalSeconds: command.intervalSeconds,
|
|
2661
|
+
exec,
|
|
2662
|
+
});
|
|
2500
2663
|
if (command.json) {
|
|
2501
2664
|
writeLine(io.stdout, JSON.stringify(result, null, 2));
|
|
2502
|
-
return
|
|
2665
|
+
return autostartExitCode(command.action, result);
|
|
2503
2666
|
}
|
|
2504
2667
|
writeAutostartResult(io, result);
|
|
2505
|
-
return
|
|
2668
|
+
return autostartExitCode(command.action, result);
|
|
2669
|
+
}
|
|
2670
|
+
function autostartExitCode(action, result) {
|
|
2671
|
+
if (result.status === "unsupported")
|
|
2672
|
+
return 1;
|
|
2673
|
+
if (action === "status")
|
|
2674
|
+
return result.status === "loaded" ? 0 : 1;
|
|
2675
|
+
if (action === "install" && result.loaded === false)
|
|
2676
|
+
return 1;
|
|
2677
|
+
if (action === "uninstall" && result.status === "not_loaded")
|
|
2678
|
+
return 1;
|
|
2679
|
+
return 0;
|
|
2680
|
+
}
|
|
2681
|
+
async function resolveAutostartRoots(homeDir, explicitRoot) {
|
|
2682
|
+
if (explicitRoot)
|
|
2683
|
+
return [explicitRoot];
|
|
2684
|
+
const config = await readLocalCollectorConfig(getCollectorRuntimePaths(homeDir)).catch(() => null);
|
|
2685
|
+
return config?.default_repo_paths ?? [];
|
|
2506
2686
|
}
|
|
2507
2687
|
async function runAgentRules(command, io) {
|
|
2508
2688
|
const hosts = agentRuleHosts(command.host);
|
|
@@ -2552,9 +2732,9 @@ function writeAutostartResult(io, result) {
|
|
|
2552
2732
|
case "installed":
|
|
2553
2733
|
writeLine(io.stdout, result.loaded
|
|
2554
2734
|
? "Cockpit autostart installed and loaded."
|
|
2555
|
-
: "Cockpit autostart installed (
|
|
2735
|
+
: "Cockpit autostart installed (the operating-system scheduler reported a problem).");
|
|
2556
2736
|
writeLine(io.stdout, `Label: ${result.label}`);
|
|
2557
|
-
writeLine(io.stdout,
|
|
2737
|
+
writeLine(io.stdout, autostartLocationLine(result));
|
|
2558
2738
|
writeLine(io.stdout, `Interval: every ${result.interval_seconds}s`);
|
|
2559
2739
|
writeLine(io.stdout, `Repo: ${result.work_dir}`);
|
|
2560
2740
|
writeLine(io.stdout, `Dashboard: ${result.dashboard_url}`);
|
|
@@ -2563,45 +2743,37 @@ function writeAutostartResult(io, result) {
|
|
|
2563
2743
|
return;
|
|
2564
2744
|
case "uninstalled":
|
|
2565
2745
|
writeLine(io.stdout, "Cockpit autostart removed.");
|
|
2566
|
-
writeLine(io.stdout,
|
|
2746
|
+
writeLine(io.stdout, autostartLocationLine(result));
|
|
2567
2747
|
return;
|
|
2568
2748
|
case "absent":
|
|
2569
2749
|
writeLine(io.stdout, "Cockpit autostart is not installed.");
|
|
2570
|
-
writeLine(io.stdout,
|
|
2750
|
+
writeLine(io.stdout, autostartLocationLine(result));
|
|
2571
2751
|
return;
|
|
2572
2752
|
case "loaded":
|
|
2573
2753
|
writeLine(io.stdout, "Cockpit autostart is installed and loaded.");
|
|
2574
|
-
writeLine(io.stdout,
|
|
2754
|
+
writeLine(io.stdout, autostartLocationLine(result));
|
|
2575
2755
|
return;
|
|
2576
2756
|
case "not_loaded":
|
|
2577
|
-
writeLine(io.stdout, "Cockpit autostart
|
|
2578
|
-
writeLine(io.stdout,
|
|
2757
|
+
writeLine(io.stdout, "Cockpit autostart exists but is disabled or not loaded; run `cockpit autostart install` to repair it.");
|
|
2758
|
+
writeLine(io.stdout, autostartLocationLine(result));
|
|
2759
|
+
if (result.message)
|
|
2760
|
+
writeLine(io.stderr, result.message);
|
|
2579
2761
|
return;
|
|
2580
2762
|
case "unsupported":
|
|
2581
2763
|
writeLine(io.stderr, `Autostart unsupported: ${result.message}`);
|
|
2582
2764
|
return;
|
|
2583
2765
|
}
|
|
2584
2766
|
}
|
|
2767
|
+
function autostartLocationLine(result) {
|
|
2768
|
+
return result.task_name
|
|
2769
|
+
? `Task: ${result.task_name}`
|
|
2770
|
+
: `Plist: ${result.plist_path}`;
|
|
2771
|
+
}
|
|
2585
2772
|
function defaultExec() {
|
|
2586
|
-
return (
|
|
2587
|
-
execFile(cmd, args, { encoding: "utf8" }, (err, stdout, stderr) => {
|
|
2588
|
-
// execFile's error `.code` is the exit code when numeric, but a string
|
|
2589
|
-
// (e.g. "ENOENT") for spawn failures — treat those as a generic 1.
|
|
2590
|
-
const code = err == null ? 0 : typeof err.code === "number" ? err.code : 1;
|
|
2591
|
-
resolve({ code, stdout: stdout ?? "", stderr: stderr ?? "" });
|
|
2592
|
-
});
|
|
2593
|
-
});
|
|
2773
|
+
return createCapturedExecRunner();
|
|
2594
2774
|
}
|
|
2595
2775
|
function defaultInteractiveExec() {
|
|
2596
|
-
return (
|
|
2597
|
-
const child = spawn(cmd, args, { stdio: "inherit" });
|
|
2598
|
-
child.on("error", (error) => {
|
|
2599
|
-
resolve({ code: 1, stdout: "", stderr: errorMessage(error) });
|
|
2600
|
-
});
|
|
2601
|
-
child.on("close", (code) => {
|
|
2602
|
-
resolve({ code: code ?? 1, stdout: "", stderr: "" });
|
|
2603
|
-
});
|
|
2604
|
-
});
|
|
2776
|
+
return createInteractiveExecRunner();
|
|
2605
2777
|
}
|
|
2606
2778
|
function defaultIo() {
|
|
2607
2779
|
if (!globalThis.fetch) {
|