@bli-cockpit/cli 0.2.8 → 0.2.10
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.
|
@@ -19,19 +19,21 @@ const BACKFILL_MAX_CONSECUTIVE_FAILURES = 3;
|
|
|
19
19
|
const ALL_BACKFILL_SINCE_MINUTES = 20 * 365 * 24 * 60;
|
|
20
20
|
const DEFAULT_DISCOVERY_MAX_DEPTH = 3;
|
|
21
21
|
const DEFAULT_DISCOVERY_MAX_REPOS = 50;
|
|
22
|
+
// Window a bare `cockpit backfill` uses. Wide enough to cover a new machine's
|
|
23
|
+
// recent history and an intern who went quiet for a few weeks, narrow enough
|
|
24
|
+
// that it is not the whole-history scan `--all` deliberately gates.
|
|
25
|
+
export const DEFAULT_BACKFILL_SINCE_DAYS = 30;
|
|
22
26
|
export async function runBackfillCommand(command, io) {
|
|
27
|
+
// A bare `cockpit backfill` used to refuse and print three lines telling the
|
|
28
|
+
// operator to pick a window. That put a mandatory flag on the command that
|
|
29
|
+
// matters most for coverage, so the honest default is to run the common case
|
|
30
|
+
// and say plainly which window was chosen. `--all` stays explicit because it
|
|
31
|
+
// is the expensive, whole-history scan.
|
|
32
|
+
let effective = command;
|
|
23
33
|
if (!command.all && command.sinceDays === undefined) {
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
writeLine(io.stderr,
|
|
27
|
-
if (command.json) {
|
|
28
|
-
writeLine(io.stdout, JSON.stringify({
|
|
29
|
-
status: "blocked",
|
|
30
|
-
reason: "missing_window",
|
|
31
|
-
retry_command: retryCommand,
|
|
32
|
-
}, null, 2));
|
|
33
|
-
}
|
|
34
|
-
return 1;
|
|
34
|
+
effective = { ...command, sinceDays: DEFAULT_BACKFILL_SINCE_DAYS };
|
|
35
|
+
const notice = defaultBackfillWindowNotice();
|
|
36
|
+
writeLine(command.json ? io.stderr : io.stdout, notice);
|
|
35
37
|
}
|
|
36
38
|
if (command.all && !command.yes && !isInteractiveStdin(io)) {
|
|
37
39
|
const message = "--all requires TTY confirmation; pass --yes for agent runs.";
|
|
@@ -46,8 +48,8 @@ export async function runBackfillCommand(command, io) {
|
|
|
46
48
|
}
|
|
47
49
|
return 1;
|
|
48
50
|
}
|
|
49
|
-
const result = await runBackfill(
|
|
50
|
-
if (
|
|
51
|
+
const result = await runBackfill(effective, io);
|
|
52
|
+
if (effective.json) {
|
|
51
53
|
writeLine(io.stdout, JSON.stringify(result, null, 2));
|
|
52
54
|
return result.status === "complete" ? 0 : 1;
|
|
53
55
|
}
|
|
@@ -508,11 +510,11 @@ export async function runBackfill(command, io) {
|
|
|
508
510
|
await lock.handle.release();
|
|
509
511
|
}
|
|
510
512
|
}
|
|
511
|
-
function
|
|
513
|
+
function defaultBackfillWindowNotice() {
|
|
512
514
|
return [
|
|
513
|
-
|
|
514
|
-
"
|
|
515
|
-
"Use `cockpit backfill --all`
|
|
515
|
+
`No window given — backfilling the last ${DEFAULT_BACKFILL_SINCE_DAYS} days.`,
|
|
516
|
+
"The effective start is capped at the collector paired_at timestamp.",
|
|
517
|
+
"Use `cockpit backfill --since-days N` for a different window, or `cockpit backfill --all` for the full local history (review a dry-run first; add `--yes` on headless agent runs).",
|
|
516
518
|
].join("\n");
|
|
517
519
|
}
|
|
518
520
|
/**
|
|
@@ -7,6 +7,18 @@ import { DEFAULT_DASHBOARD_URL } from "../local-state.js";
|
|
|
7
7
|
import { DEFAULT_AUTOSTART_INTERVAL_SECONDS } from "../autostart.js";
|
|
8
8
|
import { IntentSourceSchema, WorkIntentSchema, WorkPhaseSchema, } from "@bli-cockpit/telemetry-core";
|
|
9
9
|
const WORK_ROOT_FLAGS = ["--repo", "--workspace"];
|
|
10
|
+
// The six human "set my machine up" doors. They are one thing wearing six
|
|
11
|
+
// hats, so they all run the convergence command — but they keep accepting the
|
|
12
|
+
// flags they always accepted, because DMs, runbooks and AGENTS.md rules across
|
|
13
|
+
// the fleet still spell them out.
|
|
14
|
+
export const DOCTOR_SETUP_ALIASES = [
|
|
15
|
+
"install",
|
|
16
|
+
"onboard",
|
|
17
|
+
"login",
|
|
18
|
+
"pair",
|
|
19
|
+
"update",
|
|
20
|
+
"upgrade",
|
|
21
|
+
];
|
|
10
22
|
export function parseLocalArgs(argv) {
|
|
11
23
|
const command = argv[0];
|
|
12
24
|
switch (command) {
|
|
@@ -15,6 +27,10 @@ export function parseLocalArgs(argv) {
|
|
|
15
27
|
case "update":
|
|
16
28
|
case "upgrade":
|
|
17
29
|
return parseUpdateArgs(command, argv.slice(1));
|
|
30
|
+
// NOT yet aliased to the convergence command. `onboard --ticket <id>` binds
|
|
31
|
+
// work to a ticket, and routing it here would accept the flag and drop the
|
|
32
|
+
// binding with no error — the silent breakage BLI-2490 explicitly forbids.
|
|
33
|
+
// Aliasing lands once the convergence run honours --ticket. See BLI-2494.
|
|
18
34
|
case "do-everything":
|
|
19
35
|
case "fix":
|
|
20
36
|
return parseDoctorArgs(command, argv.slice(1));
|
|
@@ -114,15 +130,46 @@ function parseUpdateArgs(alias, args) {
|
|
|
114
130
|
};
|
|
115
131
|
}
|
|
116
132
|
function parseDoctorArgs(alias, args) {
|
|
133
|
+
// The allowed set is the UNION of what the six setup doors used to accept.
|
|
134
|
+
// An alias that silently rejected a flag its own docs told people to pass
|
|
135
|
+
// would be a worse dead end than the one we are removing.
|
|
117
136
|
const values = parseNamedArgs(args, {
|
|
118
137
|
allowedFlags: [
|
|
138
|
+
"--home",
|
|
139
|
+
"--repo",
|
|
119
140
|
"--workspace",
|
|
120
141
|
"--dashboard-url",
|
|
121
142
|
"--update-tag",
|
|
122
143
|
"--dry-run",
|
|
123
144
|
"--json",
|
|
145
|
+
"--allow-home-root",
|
|
146
|
+
"--max-depth",
|
|
147
|
+
"--max-repos",
|
|
148
|
+
// Accepted and ignored: the convergence run works these out itself.
|
|
149
|
+
// Rejecting them would break existing DMs and runbooks for no gain.
|
|
150
|
+
"--email",
|
|
151
|
+
"--device-name",
|
|
152
|
+
"--ticket",
|
|
153
|
+
"--branch",
|
|
154
|
+
"--no-auth",
|
|
155
|
+
"--poll-interval-ms",
|
|
156
|
+
"--timeout-ms",
|
|
157
|
+
],
|
|
158
|
+
valueFlags: [
|
|
159
|
+
"--home",
|
|
160
|
+
"--repo",
|
|
161
|
+
"--workspace",
|
|
162
|
+
"--dashboard-url",
|
|
163
|
+
"--update-tag",
|
|
164
|
+
"--max-depth",
|
|
165
|
+
"--max-repos",
|
|
166
|
+
"--email",
|
|
167
|
+
"--device-name",
|
|
168
|
+
"--ticket",
|
|
169
|
+
"--branch",
|
|
170
|
+
"--poll-interval-ms",
|
|
171
|
+
"--timeout-ms",
|
|
124
172
|
],
|
|
125
|
-
valueFlags: ["--workspace", "--dashboard-url", "--update-tag"],
|
|
126
173
|
});
|
|
127
174
|
assertNoPositionals(values.positionals, alias);
|
|
128
175
|
const updateTag = optionalNonEmpty(values.flags.get("--update-tag"));
|
|
@@ -132,11 +179,16 @@ function parseDoctorArgs(alias, args) {
|
|
|
132
179
|
return {
|
|
133
180
|
kind: "doctor",
|
|
134
181
|
alias,
|
|
135
|
-
|
|
182
|
+
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
183
|
+
repoRoot: optionalNonEmpty(workRootFlagValue(values)),
|
|
184
|
+
collectionRoots: optionalNonEmptyList(workRootFlagValues(values)),
|
|
136
185
|
dashboardUrl: normalizeUrl(values.flags.get("--dashboard-url") ?? DEFAULT_DASHBOARD_URL),
|
|
137
186
|
updateTag,
|
|
138
187
|
dryRun: values.booleans.has("--dry-run"),
|
|
139
188
|
json: values.booleans.has("--json"),
|
|
189
|
+
allowHomeRoot: values.booleans.has("--allow-home-root"),
|
|
190
|
+
maxDepth: optionalPositiveInteger(values.flags.get("--max-depth"), "--max-depth"),
|
|
191
|
+
maxRepos: optionalPositiveInteger(values.flags.get("--max-repos"), "--max-repos"),
|
|
140
192
|
};
|
|
141
193
|
}
|
|
142
194
|
function parseInstallArgs(args) {
|
package/dist/commands/local.js
CHANGED
|
@@ -10,6 +10,7 @@ import { inspectBackfillLock } from "../backfill-lock.js";
|
|
|
10
10
|
import { parseLocalArgs, normalizeUrl } from "./local-args.js";
|
|
11
11
|
import { autostartStatus, installAutostartAgent, uninstallAutostartAgent, } from "../autostart.js";
|
|
12
12
|
import { DEFAULT_DASHBOARD_URL, ensureLocalCollectorConfig, getCollectorRuntimePaths, inspectLocalCollectorStatus, installLocalCollector, logoutLocalCollector, pairLocalCollector, LOCAL_COLLECTOR_VERSION, readLocalCollectorConfig, readLocalCollectorSessionFile, readLocalSessionReference, startLocalWorkContext, } from "../local-state.js";
|
|
13
|
+
import { redactSecretLikeContent, } from "@bli-cockpit/telemetry-core";
|
|
13
14
|
import { CODEX_ATTRIBUTION_SCAN_WINDOW_SESSION_LIMIT, CODEX_ATTRIBUTION_SCAN_WINDOW_MINUTES, defaultCodexSessionDirs, scanAndAttributeCodexSessions, } from "../adapters/codex-attribution.js";
|
|
14
15
|
import { scanAndAttributeClaudeSessions } from "../adapters/claude-attribution.js";
|
|
15
16
|
import { backfillCompletionCovers, emptyBackfillCursorState, prepareBackfillCursorForScope, readBackfillCompletionMarker, readBackfillCursor, } from "../cursors/backfill-cursor.js";
|
|
@@ -367,6 +368,9 @@ async function runInstall(command, io) {
|
|
|
367
368
|
addInstallEvent(installEvents, "home_root_optin", "ok");
|
|
368
369
|
}
|
|
369
370
|
const result = await installLocalCollector(resolved.command);
|
|
371
|
+
// Same invariant as the onboarding path: never report a successful install
|
|
372
|
+
// over a config that saved no usable collection root.
|
|
373
|
+
await assertCollectionRootPersisted(resolved.command.homeDir);
|
|
370
374
|
addInstallEvent(installEvents, "install", "ok");
|
|
371
375
|
if (command.json) {
|
|
372
376
|
writeLine(io.stdout, JSON.stringify(result, null, 2));
|
|
@@ -707,6 +711,15 @@ export async function reportInstallEventsBestEffort(options) {
|
|
|
707
711
|
...(event.error_code
|
|
708
712
|
? { error_code: sanitizeInstallErrorCode(event.error_code) }
|
|
709
713
|
: {}),
|
|
714
|
+
// Already redacted and capped at the point it was produced; bounded
|
|
715
|
+
// again here because this mapping is what the server contract sees.
|
|
716
|
+
...(event.error_detail
|
|
717
|
+
? {
|
|
718
|
+
error_detail: event.error_detail
|
|
719
|
+
.trim()
|
|
720
|
+
.slice(0, SYNC_ERROR_DETAIL_MAX_CHARS),
|
|
721
|
+
}
|
|
722
|
+
: {}),
|
|
710
723
|
...(event.at ? { at: event.at } : {}),
|
|
711
724
|
})),
|
|
712
725
|
});
|
|
@@ -1199,7 +1212,7 @@ async function resolveOnboardingRootsForCommand(command, io) {
|
|
|
1199
1212
|
};
|
|
1200
1213
|
}
|
|
1201
1214
|
async function persistOnboardingRootConfig(command, resolution) {
|
|
1202
|
-
|
|
1215
|
+
const result = await installLocalCollector({
|
|
1203
1216
|
homeDir: command.homeDir,
|
|
1204
1217
|
repoRoot: resolution.primaryRoot,
|
|
1205
1218
|
repoRoots: resolution.collectionRoots,
|
|
@@ -1207,6 +1220,88 @@ async function persistOnboardingRootConfig(command, resolution) {
|
|
|
1207
1220
|
dashboardUrl: command.dashboardUrl,
|
|
1208
1221
|
deviceName: command.deviceName,
|
|
1209
1222
|
});
|
|
1223
|
+
await assertCollectionRootPersisted(command.homeDir);
|
|
1224
|
+
return result;
|
|
1225
|
+
}
|
|
1226
|
+
/**
|
|
1227
|
+
* Setup does not get to claim success on its own say-so.
|
|
1228
|
+
*
|
|
1229
|
+
* Onboarding used to write the config and report success without ever reading
|
|
1230
|
+
* it back. Savina's onboard did exactly that, persisted nothing, and every
|
|
1231
|
+
* scheduled sync afterwards threw `collection_root_required` into a log nobody
|
|
1232
|
+
* reads — twelve consecutive failures, six days at 3 uploaded of 195, found
|
|
1233
|
+
* only by hand-querying the database. BLI-1986 fixed one path into that state;
|
|
1234
|
+
* this closes the state itself.
|
|
1235
|
+
*
|
|
1236
|
+
* So we read the config back through the SAME resolution the scheduled sync
|
|
1237
|
+
* will use, and fail here — in front of a human who can still fix it — rather
|
|
1238
|
+
* than silently handing back a machine that will never collect.
|
|
1239
|
+
*/
|
|
1240
|
+
export async function assertCollectionRootPersisted(homeDir) {
|
|
1241
|
+
const config = await readLocalCollectorConfig(getCollectorRuntimePaths(homeDir)).catch(() => null);
|
|
1242
|
+
const saved = normalizeCollectionRoots(config?.default_repo_paths ?? []);
|
|
1243
|
+
if (saved.length === 0) {
|
|
1244
|
+
throw new Error(`${COLLECTION_ROOT_REQUIRED}: ${collectionRootNotPersistedMessage(homeDir)}`);
|
|
1245
|
+
}
|
|
1246
|
+
// Present in the file is not the same as usable. A root that no longer
|
|
1247
|
+
// exists on disk resolves to nothing at sync time, which is the same silent
|
|
1248
|
+
// dead end arriving one step later.
|
|
1249
|
+
const usable = [];
|
|
1250
|
+
for (const root of saved) {
|
|
1251
|
+
if (await directoryExists(root))
|
|
1252
|
+
usable.push(root);
|
|
1253
|
+
}
|
|
1254
|
+
if (usable.length === 0) {
|
|
1255
|
+
throw new Error(`${COLLECTION_ROOT_REQUIRED}: ${collectionRootMissingOnDiskMessage(saved, homeDir)}`);
|
|
1256
|
+
}
|
|
1257
|
+
return usable;
|
|
1258
|
+
}
|
|
1259
|
+
// Placeholders like <path-to-your-work-folder> make a person stop and think.
|
|
1260
|
+
// These print real, paste-able commands with this machine's actual paths in
|
|
1261
|
+
// them, so the fix is a copy away rather than a puzzle.
|
|
1262
|
+
function collectionRootNotPersistedMessage(homeDir) {
|
|
1263
|
+
const home = path.resolve(homeDir ?? os.homedir());
|
|
1264
|
+
return [
|
|
1265
|
+
"Setup finished without saving a collection root, so this machine would never collect anything.",
|
|
1266
|
+
"Nothing was saved, so nothing is broken — setup just did not finish.",
|
|
1267
|
+
"",
|
|
1268
|
+
"Fix it by running ONE of these:",
|
|
1269
|
+
"",
|
|
1270
|
+
" # Sync everything on this machine (what most people want on a work laptop)",
|
|
1271
|
+
" cockpit do-everything --allow-home-root",
|
|
1272
|
+
"",
|
|
1273
|
+
" # Or sync one folder — replace the path with where your projects live",
|
|
1274
|
+
` cockpit do-everything --workspace ${path.join(home, "BLI")}`,
|
|
1275
|
+
"",
|
|
1276
|
+
" # Or answer the folder question interactively",
|
|
1277
|
+
" cockpit do-everything",
|
|
1278
|
+
"",
|
|
1279
|
+
"Then check it worked:",
|
|
1280
|
+
" cockpit status",
|
|
1281
|
+
].join("\n");
|
|
1282
|
+
}
|
|
1283
|
+
function collectionRootMissingOnDiskMessage(saved, homeDir) {
|
|
1284
|
+
const home = path.resolve(homeDir ?? os.homedir());
|
|
1285
|
+
return [
|
|
1286
|
+
"Cockpit is set up to collect from a folder that is not on this machine:",
|
|
1287
|
+
...saved.map((root) => ` ${root}`),
|
|
1288
|
+
"",
|
|
1289
|
+
"That usually means the folder was renamed, moved, or deleted since setup.",
|
|
1290
|
+
"",
|
|
1291
|
+
"Fix it by running ONE of these:",
|
|
1292
|
+
"",
|
|
1293
|
+
" # Point Cockpit at where your projects actually live now",
|
|
1294
|
+
` cockpit do-everything --workspace ${path.join(home, "BLI")}`,
|
|
1295
|
+
"",
|
|
1296
|
+
" # Or sync everything on this machine and stop worrying about the path",
|
|
1297
|
+
" cockpit do-everything --allow-home-root",
|
|
1298
|
+
"",
|
|
1299
|
+
"Not sure where your projects are? This lists the folders Cockpit can see:",
|
|
1300
|
+
" cockpit status",
|
|
1301
|
+
].join("\n");
|
|
1302
|
+
}
|
|
1303
|
+
async function directoryExists(dir) {
|
|
1304
|
+
return stat(dir).then((stats) => stats.isDirectory(), () => false);
|
|
1210
1305
|
}
|
|
1211
1306
|
async function runDoctorLogin(command, io) {
|
|
1212
1307
|
return runLogin({
|
|
@@ -1218,9 +1313,15 @@ async function runDoctorLogin(command, io) {
|
|
|
1218
1313
|
}
|
|
1219
1314
|
async function resolveAndSaveDoctorRoots(command, io) {
|
|
1220
1315
|
const rootCommand = {
|
|
1316
|
+
homeDir: command.homeDir,
|
|
1221
1317
|
repoRoot: command.repoRoot,
|
|
1318
|
+
collectionRoots: command.collectionRoots,
|
|
1222
1319
|
dashboardUrl: command.dashboardUrl,
|
|
1223
1320
|
json: command.json,
|
|
1321
|
+
// Interactively this is not needed — the convergence run reaches the
|
|
1322
|
+
// home-folder consent prompt and Enter accepts it. The flag is the
|
|
1323
|
+
// headless equivalent for scripted and scheduled runs.
|
|
1324
|
+
allowHomeRoot: command.allowHomeRoot,
|
|
1224
1325
|
};
|
|
1225
1326
|
const resolution = await resolveOnboardingRootsForCommand(rootCommand, io);
|
|
1226
1327
|
await persistOnboardingRootConfig(rootCommand, resolution);
|
|
@@ -1974,6 +2075,7 @@ async function runSync(command, io) {
|
|
|
1974
2075
|
step: "sync_complete",
|
|
1975
2076
|
status: "fail",
|
|
1976
2077
|
error_code: classifySyncHealthError(error),
|
|
2078
|
+
error_detail: redactedSyncErrorDetail(error),
|
|
1977
2079
|
},
|
|
1978
2080
|
],
|
|
1979
2081
|
json: command.json,
|
|
@@ -2048,7 +2150,7 @@ async function runSyncWithHealthReceipt(command, io) {
|
|
|
2048
2150
|
await lock.handle.release();
|
|
2049
2151
|
}
|
|
2050
2152
|
}
|
|
2051
|
-
function classifySyncHealthError(error) {
|
|
2153
|
+
export function classifySyncHealthError(error) {
|
|
2052
2154
|
const message = errorMessage(error);
|
|
2053
2155
|
if (/auth|token|session|unauthorized|forbidden|401|403/iu.test(message)) {
|
|
2054
2156
|
return "auth_failed";
|
|
@@ -2056,11 +2158,33 @@ function classifySyncHealthError(error) {
|
|
|
2056
2158
|
if (/fetch|network|enotfound|econnrefused|timeout/iu.test(message)) {
|
|
2057
2159
|
return "network_failed";
|
|
2058
2160
|
}
|
|
2059
|
-
|
|
2161
|
+
// Anchored on the code the collector actually throws rather than on loose
|
|
2162
|
+
// vocabulary. The old test matched /collection.root|workspace|repo|worktree/
|
|
2163
|
+
// against the message, so any failure that merely mentioned a repo was filed
|
|
2164
|
+
// as a collection-root failure and the real reason was lost (BLI-2492).
|
|
2165
|
+
if (message.includes(COLLECTION_ROOT_REQUIRED) ||
|
|
2166
|
+
/collection root/iu.test(message)) {
|
|
2060
2167
|
return "collection_root_failed";
|
|
2061
2168
|
}
|
|
2062
2169
|
return "sync_failed";
|
|
2063
2170
|
}
|
|
2171
|
+
// The bucket above is for aggregation. This is the reason — the actual message,
|
|
2172
|
+
// redacted on the machine that produced it, before it ever leaves.
|
|
2173
|
+
//
|
|
2174
|
+
// Error text can carry absolute paths and, on some auth failures, token-shaped
|
|
2175
|
+
// fragments. It goes through the same deterministic redaction the collector
|
|
2176
|
+
// already applies to evidence, and is capped so one pathological stack trace
|
|
2177
|
+
// cannot dominate a health receipt.
|
|
2178
|
+
export const SYNC_ERROR_DETAIL_MAX_CHARS = 600;
|
|
2179
|
+
export function redactedSyncErrorDetail(error) {
|
|
2180
|
+
const message = errorMessage(error).replace(/\s+/gu, " ").trim();
|
|
2181
|
+
const { text } = redactSecretLikeContent(message, {
|
|
2182
|
+
appliedBy: "local_collector",
|
|
2183
|
+
});
|
|
2184
|
+
return text.length > SYNC_ERROR_DETAIL_MAX_CHARS
|
|
2185
|
+
? `${text.slice(0, SYNC_ERROR_DETAIL_MAX_CHARS - 1)}…`
|
|
2186
|
+
: text;
|
|
2187
|
+
}
|
|
2064
2188
|
async function runSyncLocked(command, io) {
|
|
2065
2189
|
const collectionRoots = await resolveSyncCollectionRoots(command);
|
|
2066
2190
|
const worktrees = await discoverCommandWorktrees(collectionRoots, {
|
|
@@ -2,13 +2,20 @@ import { localCommandHelp, runLocalCockpitCli, rootCommandNames } from "./local.
|
|
|
2
2
|
|
|
3
3
|
export async function runCockpitCli(argv, io) {
|
|
4
4
|
const command = argv[0];
|
|
5
|
-
|
|
5
|
+
// Bare `cockpit` runs the convergence command instead of printing a
|
|
6
|
+
// usage wall (BLI-2490). This file is GENERATED and does not import
|
|
7
|
+
// commands/root.ts, so the routing has to be mirrored here or the fix
|
|
8
|
+
// ships to nobody.
|
|
9
|
+
if (!command) {
|
|
10
|
+
return runLocalCockpitCli(["do-everything"], io);
|
|
11
|
+
}
|
|
12
|
+
if (command === "--help" || command === "-h") {
|
|
6
13
|
writeLine(io?.stdout ?? process.stdout, cockpitHelp());
|
|
7
14
|
return 0;
|
|
8
15
|
}
|
|
9
16
|
|
|
10
17
|
if (command === "--version" || command === "-V" || command === "version") {
|
|
11
|
-
writeLine(io?.stdout ?? process.stdout, "0.2.
|
|
18
|
+
writeLine(io?.stdout ?? process.stdout, "0.2.10");
|
|
12
19
|
return 0;
|
|
13
20
|
}
|
|
14
21
|
|
|
@@ -24,6 +31,11 @@ export async function runCockpitCli(argv, io) {
|
|
|
24
31
|
|
|
25
32
|
function cockpitHelp() {
|
|
26
33
|
return [
|
|
34
|
+
"Just run `cockpit do-everything` — it takes this machine from nothing to collecting and asks you anything it needs.",
|
|
35
|
+
"(Running plain `cockpit` does the same thing.)",
|
|
36
|
+
"",
|
|
37
|
+
"Everything below is for scripting and agents.",
|
|
38
|
+
"",
|
|
27
39
|
"Usage:",
|
|
28
40
|
localCommandHelp(),
|
|
29
41
|
"",
|
package/dist/onboarding-roots.js
CHANGED
|
@@ -6,7 +6,7 @@ import { isSamePath, normalizeCollectionRoots, } from "./root-normalization.js";
|
|
|
6
6
|
export const COLLECTION_ROOT_REQUIRED = "collection_root_required";
|
|
7
7
|
export function homeRootConsentPrompt(homeDirInput) {
|
|
8
8
|
const homeDir = path.resolve(homeDirInput ?? os.homedir());
|
|
9
|
-
return `You're in your home folder (${homeDir}).\n Sync ALL projects on this machine? Every git repo under here gets captured now and in the future.\n This is a work machine — exclude personal projects yourself if needed. [
|
|
9
|
+
return `You're in your home folder (${homeDir}).\n Sync ALL projects on this machine? Every git repo under here gets captured now and in the future.\n This is a work machine — exclude personal projects yourself if needed.\n Press Enter to sync everything, or answer n to name one folder instead. [Y/n]: `;
|
|
10
10
|
}
|
|
11
11
|
export const HOME_ROOT_DECLINE_PATH_PROMPT = "Okay — which folder should I sync? Enter the full path to your work directory: ";
|
|
12
12
|
export const HOME_ROOT_NO_FOLDER_CHOSEN_MESSAGE = "No folder chosen. Re-run: cockpit onboard --workspace <path-to-your-work-folder>";
|
|
@@ -223,9 +223,20 @@ async function promptForHomeRootOptIn(options, rejections) {
|
|
|
223
223
|
}
|
|
224
224
|
return promptForDeclinedHomeRoot(options, homeDir);
|
|
225
225
|
}
|
|
226
|
+
// Bare Enter is the answer most people give, so it has to mean the thing that
|
|
227
|
+
// collects. It used to mean decline, and the decline path then asks for a
|
|
228
|
+
// folder that is not home — which a person who keeps every project directly in
|
|
229
|
+
// their home folder cannot answer. They finished onboarding with zero roots and
|
|
230
|
+
// every session on the machine was legitimately out of scope forever after.
|
|
231
|
+
// Anything that isn't recognisably yes still routes to the decline prompt,
|
|
232
|
+
// where a typed path is recoverable rather than lost.
|
|
226
233
|
function isHomeRootYes(raw) {
|
|
227
234
|
const answer = raw.trim().split(/\s+/u)[0]?.toLowerCase() ?? "";
|
|
228
|
-
|
|
235
|
+
if (answer === "")
|
|
236
|
+
return true;
|
|
237
|
+
if (answer.startsWith("n"))
|
|
238
|
+
return false;
|
|
239
|
+
return answer.startsWith("y");
|
|
229
240
|
}
|
|
230
241
|
async function promptForDeclinedHomeRoot(options, homeDir) {
|
|
231
242
|
const prompt = requirePrompt(options);
|
|
@@ -23,6 +23,11 @@ export async function enqueueInstallEventEntry(paths, options) {
|
|
|
23
23
|
step: event.step,
|
|
24
24
|
status: event.status,
|
|
25
25
|
...(event.error_code ? { error_code: event.error_code } : {}),
|
|
26
|
+
// Spooling must not quietly downgrade a receipt. This mapping copies
|
|
27
|
+
// named fields, so a new one has to be added here too — otherwise a
|
|
28
|
+
// failure that could not be delivered immediately loses its reason on
|
|
29
|
+
// the way to disk and replays as a bare bucket (BLI-2492).
|
|
30
|
+
...(event.error_detail ? { error_detail: event.error_detail } : {}),
|
|
26
31
|
at: event.at ?? createdAt,
|
|
27
32
|
})),
|
|
28
33
|
};
|
|
@@ -157,6 +162,9 @@ function parseEvent(value) {
|
|
|
157
162
|
...(stringValue(record["error_code"])
|
|
158
163
|
? { error_code: stringValue(record["error_code"]) }
|
|
159
164
|
: {}),
|
|
165
|
+
...(stringValue(record["error_detail"])
|
|
166
|
+
? { error_detail: stringValue(record["error_detail"]) }
|
|
167
|
+
: {}),
|
|
160
168
|
at,
|
|
161
169
|
};
|
|
162
170
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bli-cockpit/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.10",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -23,9 +23,9 @@
|
|
|
23
23
|
"pretypecheck": "npm run build",
|
|
24
24
|
"typecheck": "node -e \"await import('./dist/commands/public-root.js')\"",
|
|
25
25
|
"pretest": "npm run build",
|
|
26
|
-
"test": "node dist/cli.js --help && node ../../scripts/assert-public-package-pack.mjs --workspace=@bli-cockpit/cli"
|
|
26
|
+
"test": "node dist/cli.js --help && node ../../scripts/assert-public-cli-routing.mjs && node ../../scripts/assert-public-package-pack.mjs --workspace=@bli-cockpit/cli"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@bli-cockpit/telemetry-core": "0.1.
|
|
29
|
+
"@bli-cockpit/telemetry-core": "0.1.15"
|
|
30
30
|
}
|
|
31
31
|
}
|