@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
- const message = bareBackfillMessage();
25
- const retryCommand = backfillRetryCommand(command);
26
- writeLine(io.stderr, message);
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(command, io);
50
- if (command.json) {
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 bareBackfillMessage() {
513
+ function defaultBackfillWindowNotice() {
512
514
  return [
513
- "cockpit backfill needs an explicit window.",
514
- "Use `cockpit backfill --since-days N` for post-pairing history; the effective start is capped at the collector paired_at timestamp.",
515
- "Use `cockpit backfill --all` only after reviewing a dry-run; on headless agent runs add `--yes`.",
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
- repoRoot: optionalNonEmpty(values.flags.get("--workspace")),
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) {
@@ -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
- return installLocalCollector({
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
- if (/collection.root|workspace|repo|worktree/iu.test(message)) {
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
- if (!command || command === "--help" || command === "-h") {
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.8");
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
  "",
@@ -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. [y/N]: `;
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
- return answer === "y" || answer === "yes";
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.8",
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.14"
29
+ "@bli-cockpit/telemetry-core": "0.1.15"
30
30
  }
31
31
  }