@bli-cockpit/cli 0.2.109 → 0.2.111
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/claude-attribution-score.js +2 -1
- package/dist/adapters/codex-attribution.js +2 -1
- package/dist/commands/collection-roots.js +4 -0
- package/dist/commands/doctor-access.js +42 -0
- package/dist/commands/doctor-report.js +1 -0
- package/dist/commands/doctor.js +4 -1
- package/dist/commands/heartbeat.js +1 -0
- package/dist/commands/local-args-tower-usage.js +8 -6
- package/dist/commands/local-args.js +2 -0
- package/dist/commands/local-discovery.js +18 -0
- package/dist/commands/local-help-commands-tower.js +4 -1
- package/dist/commands/local-help-commands.js +2 -0
- package/dist/commands/local-help.js +6 -1
- package/dist/commands/local.js +2 -0
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/roots.js +41 -0
- package/dist/commands/session-sync-health.js +5 -0
- package/dist/commands/session-sync-scan.js +2 -0
- package/dist/commands/session-sync.js +1 -0
- package/dist/commands/sync-receipt.js +1 -0
- package/dist/commands/sync-report.js +1 -0
- package/dist/commands/usage.js +34 -0
- package/dist/local-state-config.js +2 -0
- package/dist/onboarding-roots.js +4 -0
- package/dist/root-work.js +70 -0
- package/package.json +4 -4
|
@@ -88,7 +88,8 @@ export async function attributeOneSession(session, worktrees, collectionRoots) {
|
|
|
88
88
|
}
|
|
89
89
|
const primaryCwd = signals.cwds[0] ?? null;
|
|
90
90
|
if (primaryCwd) {
|
|
91
|
-
base.
|
|
91
|
+
base.local_cwd = primaryCwd;
|
|
92
|
+
base.cwd_basename = (path.win32.isAbsolute(primaryCwd) ? path.win32 : path.posix).basename(primaryCwd) || null;
|
|
92
93
|
base.cwd_hash = shortHash(primaryCwd);
|
|
93
94
|
}
|
|
94
95
|
if (signals.line_count === 0 || signals.parse_error_count === signals.line_count) {
|
|
@@ -199,7 +199,8 @@ async function attributeOneSession(file, worktrees, collectionRoots) {
|
|
|
199
199
|
}
|
|
200
200
|
const primaryCwd = signals.cwds[0] ?? null;
|
|
201
201
|
if (primaryCwd) {
|
|
202
|
-
base.
|
|
202
|
+
base.local_cwd = primaryCwd;
|
|
203
|
+
base.cwd_basename = (path.win32.isAbsolute(primaryCwd) ? path.win32 : path.posix).basename(primaryCwd) || null;
|
|
203
204
|
base.cwd_hash = shortHash(primaryCwd);
|
|
204
205
|
}
|
|
205
206
|
if (signals.line_count === 0 || signals.parse_error_count === signals.line_count) {
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* Split out of commands/local.ts (BLI-3104); moved verbatim.
|
|
9
9
|
*/
|
|
10
10
|
import os from "node:os";
|
|
11
|
+
import { scanWorkDirectories } from "./local-discovery.js";
|
|
11
12
|
import path from "node:path";
|
|
12
13
|
import { stat } from "node:fs/promises";
|
|
13
14
|
import { isInteractiveStdin, readLine, writeLine, yesByDefault } from "./cli-io.js";
|
|
@@ -26,7 +27,10 @@ export async function resolveOnboardingRootsForCommand(command, io) {
|
|
|
26
27
|
const paths = getCollectorRuntimePaths(command.homeDir);
|
|
27
28
|
const existingConfig = await readLocalCollectorConfig(paths).catch(() => null);
|
|
28
29
|
const interactive = !command.json && isInteractiveStdin(io);
|
|
30
|
+
const discovered = interactive && !command.collectionRoots?.length && !command.repoRoot && !existingConfig?.default_repo_paths.length
|
|
31
|
+
? await scanWorkDirectories(command.homeDir ?? os.homedir()) : null;
|
|
29
32
|
const rootsResult = await resolveOnboardingRoots({
|
|
33
|
+
suggestedRoots: discovered?.directories.map(entry => entry.directory),
|
|
30
34
|
homeDir: command.homeDir,
|
|
31
35
|
explicitRoots: command.collectionRoots ?? (command.repoRoot ? [command.repoRoot] : []),
|
|
32
36
|
config: existingConfig,
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import { readRootWorkScan, outsideWorkDirectories, readRootApprovals, rootsAddCommand, rootPathApi } from "../root-work.js";
|
|
3
|
+
import { writeJsonFile } from "../local-state-files.js";
|
|
4
|
+
import { containsPath } from "../root-normalization.js";
|
|
5
|
+
import { scanWorkDirectories } from "./local-discovery.js";
|
|
1
6
|
import { sendCollectorHeartbeatBestEffort } from "./heartbeat.js";
|
|
2
7
|
import { isInteractiveDoctorFix } from "./doctor-report.js";
|
|
3
8
|
import { describeError } from "../health-detail.js";
|
|
@@ -150,4 +155,41 @@ function shellQuote(value) {
|
|
|
150
155
|
if (value === "$PWD")
|
|
151
156
|
return '"$PWD"';
|
|
152
157
|
return `'${value.replace(/'/gu, "'\\''")}'`;
|
|
158
|
+
}
|
|
159
|
+
/** The last scan names work missed by the saved boundary. Diagnosis never grants consent. */
|
|
160
|
+
export async function readRootsCoverWork(context) {
|
|
161
|
+
const homeDir = context.command.homeDir ?? os.homedir();
|
|
162
|
+
const paths = getCollectorRuntimePaths(homeDir);
|
|
163
|
+
const scan = await readRootWorkScan(paths) ?? await scanWorkDirectories(homeDir, false);
|
|
164
|
+
const outside = outsideWorkDirectories(scan, (await readLocalCollectorConfig(paths))?.default_repo_paths ?? []);
|
|
165
|
+
const count = outside.reduce((sum, entry) => sum + entry.count, 0);
|
|
166
|
+
if (!count)
|
|
167
|
+
return ok("roots-cover-work", "work_covered", `${scan.observed} sessions seen; no known working directories outside approved roots.`);
|
|
168
|
+
return {
|
|
169
|
+
...needsFix("roots-cover-work", "work_outside_roots", `${count} of ${scan.observed} sessions have working directories outside every approved root (scan ${scan.generated_at}):\n${outside.map(entry => `${entry.count}: ${entry.directory}\n ${rootsAddCommand(entry.directory, process.platform, homeDir)}`).join("\n")}`),
|
|
170
|
+
nextAction: rootsAddCommand(outside[0].directory, process.platform, homeDir),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
export async function fixRootsCoverWork(context) {
|
|
174
|
+
const homeDir = context.command.homeDir ?? os.homedir();
|
|
175
|
+
const paths = getCollectorRuntimePaths(homeDir);
|
|
176
|
+
const config = await readLocalCollectorConfig(paths);
|
|
177
|
+
if (!config)
|
|
178
|
+
return readRootsCoverWork(context);
|
|
179
|
+
const scan = await readRootWorkScan(paths) ?? await scanWorkDirectories(homeDir);
|
|
180
|
+
const approvals = await readRootApprovals(paths);
|
|
181
|
+
const outside = outsideWorkDirectories(scan, config.default_repo_paths);
|
|
182
|
+
const additions = outside.filter(entry => approvals.some(root => containsPath(root, entry.directory, rootPathApi())));
|
|
183
|
+
if (additions.length) {
|
|
184
|
+
// Restore the precise approved directories, preserving every other config option.
|
|
185
|
+
const roots = normalizeCollectionRoots([...config.default_repo_paths, ...additions.map(entry => entry.directory)], rootPathApi());
|
|
186
|
+
await writeJsonFile(paths.config_file, { ...config, default_repo_paths: roots });
|
|
187
|
+
console.error("[doctor] roots-cover-work repaired", JSON.stringify({ added: additions.length }));
|
|
188
|
+
await scanWorkDirectories(homeDir);
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
console.error("[doctor] roots-cover-work needs approval", JSON.stringify({ outside: outside.length, added: 0 }));
|
|
192
|
+
}
|
|
193
|
+
const checked = await readRootsCoverWork(context);
|
|
194
|
+
return { ...checked, fixed: additions.length > 0 && checked.status === "ok" };
|
|
153
195
|
}
|
|
@@ -137,6 +137,7 @@ export function doctorNeedsPerson(rows, receipt) {
|
|
|
137
137
|
"cli-latest": "npm i -g @bli-cockpit/cli && cockpit doctor",
|
|
138
138
|
"authed": "cockpit doctor",
|
|
139
139
|
"roots-ok": "cockpit doctor",
|
|
140
|
+
"roots-cover-work": "cockpit doctor",
|
|
140
141
|
"single-install": "Run npm uninstall -g @bli-cockpit/cli with the Node installation that owns the extra CLI listed above.",
|
|
141
142
|
"autostart-alive": "cockpit doctor",
|
|
142
143
|
"memory-registered": "cockpit doctor",
|
package/dist/commands/doctor.js
CHANGED
|
@@ -2,7 +2,7 @@ import { LOCAL_COLLECTOR_VERSION } from "../local-state.js";
|
|
|
2
2
|
import { isSemverBelow } from "../scheduled-self-update.js";
|
|
3
3
|
import { describeError } from "../health-detail.js";
|
|
4
4
|
import { withStage } from "../crash-guard.js";
|
|
5
|
-
import { checkSingleInstallState, fixAuthState, fixRootState, readAuthState, readRootState, } from "./doctor-access.js";
|
|
5
|
+
import { checkSingleInstallState, readRootsCoverWork, fixRootsCoverWork, fixAuthState, fixRootState, readAuthState, readRootState, } from "./doctor-access.js";
|
|
6
6
|
import { backfillCompletionStepState, backfillFixVerdict, checkBackfillState, checkDiskState, checkGcState, checkSyncState, fixBackfillState, fixDiskState, fixGcState, fixSyncState, syncBacklogDrainingVerdict, syncStandAsideVerdict, } from "./doctor-pipeline.js";
|
|
7
7
|
import { checkMcpAnswersState } from "./doctor-mcp.js";
|
|
8
8
|
import { checkMemoryDaemonState } from "./doctor-memory-daemon.js";
|
|
@@ -119,6 +119,7 @@ function doctorInvariants() {
|
|
|
119
119
|
fix: fixRootState,
|
|
120
120
|
requiresInteractiveFix: true,
|
|
121
121
|
},
|
|
122
|
+
{ id: "roots-cover-work", check: context => context.deps.readRootsCoverWork(context), fix: context => context.deps.fixRootsCoverWork(context) },
|
|
122
123
|
// Deliberately has NO fix: uninstalling software the operator did not ask
|
|
123
124
|
// to have uninstalled is not a repair (BLI-3218/BLI-3553). Doctor names the
|
|
124
125
|
// other install and the exact command; the person decides.
|
|
@@ -192,6 +193,8 @@ function defaultDoctorDeps(hooks) {
|
|
|
192
193
|
readAuth: readAuthState,
|
|
193
194
|
runLogin: (context) => hooks.runLogin(context.command, context.io),
|
|
194
195
|
readRoots: readRootState,
|
|
196
|
+
readRootsCoverWork,
|
|
197
|
+
fixRootsCoverWork,
|
|
195
198
|
resolveAndSaveRoots: (context) => hooks.resolveAndSaveRoots(context.command, context.io),
|
|
196
199
|
checkSingleInstall: checkSingleInstallState,
|
|
197
200
|
checkAutostart: checkAutostartState,
|
|
@@ -102,6 +102,7 @@ export function buildCollectorHeartbeat(options) {
|
|
|
102
102
|
os_platform: platform,
|
|
103
103
|
roots: collectionRootLabels(options.roots, platform),
|
|
104
104
|
last_sync_status: options.facts.status,
|
|
105
|
+
...(options.facts.outsideRootDirectories ? { outside_root_directories: options.facts.outsideRootDirectories } : {}),
|
|
105
106
|
...(options.facts.reason ? { last_sync_reason: options.facts.reason } : {}),
|
|
106
107
|
...(typeof options.facts.sessionsObserved === "number"
|
|
107
108
|
? { sessions_observed: options.facts.sessionsObserved }
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { optionalNonEmpty, optionalUrl, parseNamedArgs } from "./local-arg-values.js";
|
|
2
2
|
export function parseUsageArgs(args) {
|
|
3
|
-
const values = parseNamedArgs(args, { allowedFlags: ["--min-confidence", "--by-topic", "--by-repo", "--person", "--all", "--since", "--until", "--include-automated", "--detail", "--home", "--dashboard-url", "--json"], valueFlags: ["--min-confidence", "--person", "--since", "--until", "--home", "--dashboard-url"] });
|
|
4
|
-
if (
|
|
5
|
-
throw new Error("--by-repo and --by-topic cannot be used together.");
|
|
3
|
+
const values = parseNamedArgs(args, { allowedFlags: ["--by-subject", "--subject", "--repo", "--session", "--from-window", "--min-confidence", "--by-topic", "--by-repo", "--person", "--all", "--since", "--until", "--include-automated", "--detail", "--home", "--dashboard-url", "--json"], valueFlags: ["--subject", "--repo", "--session", "--from-window", "--min-confidence", "--person", "--since", "--until", "--home", "--dashboard-url"] });
|
|
4
|
+
if (["--by-repo", "--by-topic", "--by-subject"].filter(flag => values.booleans.has(flag)).length > 1)
|
|
5
|
+
throw new Error("--by-repo and --by-topic cannot be used together or with --by-subject.");
|
|
6
6
|
const minConfidence = values.flags.get("--min-confidence");
|
|
7
7
|
if (minConfidence !== undefined && !["high", "medium", "low"].includes(minConfidence))
|
|
8
8
|
throw new Error("--min-confidence accepts high, medium or low.");
|
|
9
9
|
const action = values.positionals[0] ?? "people";
|
|
10
|
-
if (
|
|
11
|
-
throw new Error("usage takes
|
|
12
|
-
|
|
10
|
+
if (!["people", "sessions"].includes(action) || values.positionals.length > 1)
|
|
11
|
+
throw new Error("usage takes people or sessions.");
|
|
12
|
+
if (action === "sessions" && !values.flags.get("--subject") && !values.flags.get("--session"))
|
|
13
|
+
throw new Error("usage sessions requires --subject or --session.");
|
|
14
|
+
return { bySubject: values.booleans.has("--by-subject"), subject: optionalNonEmpty(values.flags.get("--subject")), repo: optionalNonEmpty(values.flags.get("--repo")), session: optionalNonEmpty(values.flags.get("--session")), fromWindow: optionalNonEmpty(values.flags.get("--from-window")), kind: "usage", ...(minConfidence ? { minConfidence: minConfidence } : {}), byTopic: values.booleans.has("--by-topic"), byRepo: values.booleans.has("--by-repo"), person: optionalNonEmpty(values.flags.get("--person")), all: values.booleans.has("--all"), action: action, since: optionalNonEmpty(values.flags.get("--since")) ?? "30d", until: optionalNonEmpty(values.flags.get("--until")), detail: values.booleans.has("--detail"), includeAutomated: values.booleans.has("--include-automated"), homeDir: optionalNonEmpty(values.flags.get("--home")), dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")), json: values.booleans.has("--json") };
|
|
13
15
|
}
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* its own rate. Behavior-preserving extraction throughout: functions moved
|
|
16
16
|
* verbatim, no logic change.
|
|
17
17
|
*/
|
|
18
|
+
import { parseRootsArgs } from "./roots.js";
|
|
18
19
|
import { parseAgentRulesArgs, parseAnalyzeArgs, parseAutostartArgs, parseBackfillArgs, parseCleanArgs, parseDoctorArgs, parseInstallArgs, parseLoginArgs, parseMemoryArgs, parseLogoutArgs, parseOnboardArgs, parseReleaseArgs, parseServeArgs, parseSessionsArgs, parseStartArgs, parseStatusArgs, parseSyncArgs, parseUpdateArgs, } from "./local-args-collector.js";
|
|
19
20
|
import { parseBriefArgs, parseCorrectArgs, parseDocsArgs, parseIssueArgs, parseJarvisArgs, parseMailArgs, parseCalArgs, parseModelArgs, parseMsgArgs, parseNotesArgs, parseOpsArgs, parseProjectArgs, parseModelsArgs, parseScoutArgs, parseSearchArgs, parseSettingsArgs, parseSlackArgs, parseTeamArgs, parseWorkbookArgs, parseUsageArgs, parseCareersArgs, } from "./local-args-tower.js";
|
|
20
21
|
// `normalizeUrl` has always been part of this module's surface — `local.ts` and
|
|
@@ -30,6 +31,7 @@ export { SCOUT_MIN_PREFIX_LENGTH, SLACK_WORKSPACE_KEYS, WORKBOOK_MIN_WIDTH, ISSU
|
|
|
30
31
|
export function parseLocalArgs(argv) {
|
|
31
32
|
const command = argv[0];
|
|
32
33
|
switch (command) {
|
|
34
|
+
case "roots": return parseRootsArgs(argv.slice(1));
|
|
33
35
|
case "onboard":
|
|
34
36
|
return parseOnboardArgs(argv.slice(1));
|
|
35
37
|
case "update":
|
|
@@ -112,4 +112,22 @@ function incompleteDiscoveryMessage(input) {
|
|
|
112
112
|
"",
|
|
113
113
|
"If the scan is still partial, raise the numbers again, or point --workspace at the specific project folders instead of a parent.",
|
|
114
114
|
].join("\n");
|
|
115
|
+
}
|
|
116
|
+
/** Reuse the session adapters for onboarding/doctor. No transcript bodies leave them. */
|
|
117
|
+
export async function scanWorkDirectories(homeDir, persist = true) {
|
|
118
|
+
const [{ scanAndAttributeCodexSessions, defaultCodexSessionDirs }, { scanAndAttributeClaudeSessions }, { readLocalCollectorConfig, getCollectorRuntimePaths }, { saveRootWorkScan, workDirectories }] = await Promise.all([
|
|
119
|
+
import("../adapters/codex-attribution.js"), import("../adapters/claude-attribution.js"), import("../local-state.js"), import("../root-work.js"),
|
|
120
|
+
]);
|
|
121
|
+
const paths = getCollectorRuntimePaths(homeDir);
|
|
122
|
+
const config = await readLocalCollectorConfig(paths).catch(error => { if (error.code === "ENOENT")
|
|
123
|
+
return null; throw error; });
|
|
124
|
+
const now = new Date();
|
|
125
|
+
const options = { worktrees: [], collectionRoots: config?.default_repo_paths ?? [], now, sinceMinutes: 14 * 24 * 60, limit: Number.MAX_SAFE_INTEGER };
|
|
126
|
+
const codex = await scanAndAttributeCodexSessions({ ...options, sessionsDirs: defaultCodexSessionDirs(homeDir) });
|
|
127
|
+
const claude = config?.collect_claude_jsonl === false ? null : await scanAndAttributeClaudeSessions({ ...options, projectsDir: (await import("node:path")).default.join(homeDir, ".claude", "projects") });
|
|
128
|
+
if (codex.directory_read_failed_count || codex.stat_failed_count || claude?.project_dir_read_failed_count || claude?.session_stat_failed_count) {
|
|
129
|
+
throw new Error("work_directory_scan_incomplete");
|
|
130
|
+
}
|
|
131
|
+
const results = [...codex.results, ...(claude?.results ?? [])];
|
|
132
|
+
return persist ? saveRootWorkScan(paths, results, now) : { generated_at: now.toISOString(), observed: results.length, directories: workDirectories(results) };
|
|
115
133
|
}
|
|
@@ -143,7 +143,10 @@ export const TOWER_COMMAND_HELP = [
|
|
|
143
143
|
[
|
|
144
144
|
"usage",
|
|
145
145
|
[
|
|
146
|
-
"Usage: cockpit usage people [--by-repo | --by-topic] [--min-confidence high|medium|low] [--person <email|me>] [--all] [--since <n>d|<n>h|<iso>] [--detail] [--until <iso>] [--include-automated] [--json]",
|
|
146
|
+
"Usage: cockpit usage people [--by-repo | --by-topic | --by-subject] [--min-confidence high|medium|low] [--person <email|me>] [--all] [--since <n>d|<n>h|<iso>] [--detail] [--until <iso>] [--include-automated] [--json]",
|
|
147
|
+
" cockpit usage sessions --subject <name> [--repo <label>] [--since <window>] [--json]",
|
|
148
|
+
" cockpit usage sessions --session <id> [--from-window <n>] [--json]",
|
|
149
|
+
" --by-topic groups work types; --by-subject groups open subjects from session summaries.",
|
|
147
150
|
"",
|
|
148
151
|
"--by-topic adds topic rows, including (unlabelled). Choose only one grouping. --by-repo adds project rows under each person (top 10; --all shows every repo). --person me uses your signed-in email.",
|
|
149
152
|
"Claude Code and Codex usage per person: sessions observed and extracted, tokens (total, output,",
|
|
@@ -21,6 +21,7 @@ import { localCommandHelp } from "./local-help.js";
|
|
|
21
21
|
import { TOWER_COMMAND_HELP } from "./local-help-commands-tower.js";
|
|
22
22
|
export function localSubcommandHelp(command) {
|
|
23
23
|
const helpByCommand = new Map([
|
|
24
|
+
["roots", ["Usage: cockpit roots add <dir> [--allow-home-root] [--json]", "Approves a work directory, saves an approval receipt, and adds it to collection. Run cockpit doctor afterward."]],
|
|
24
25
|
// BLI-3709: the Tower nouns live next door, split along the same seam the
|
|
25
26
|
// argument parsers already use — see `local-help-commands-tower.ts`.
|
|
26
27
|
...TOWER_COMMAND_HELP,
|
|
@@ -89,6 +90,7 @@ export function localSubcommandHelp(command) {
|
|
|
89
90
|
"",
|
|
90
91
|
"Diagnose, repair, and verify this machine.",
|
|
91
92
|
"Repairs CLI, background sync, sign-in, roots, memory and hooks, agent rules, catch-up, uploads, and cleanup.",
|
|
93
|
+
"roots-cover-work names sessions outside saved roots and gives cockpit roots add commands. Previously approved directories are restored automatically.",
|
|
92
94
|
"--check or --no-repair diagnoses without repairs. --dry-run previews repairs.",
|
|
93
95
|
"--backfill-budget defaults to 900 seconds; catch-up repeats chunks until complete, with progress every 30 seconds.",
|
|
94
96
|
"--lock-wait defaults to 600 seconds; progress prints every 30 seconds.",
|
|
@@ -11,6 +11,7 @@ import { DEFAULT_DASHBOARD_URL } from "../local-state.js";
|
|
|
11
11
|
import { SEARCH_KINDS } from "./local-args-tower-search.js";
|
|
12
12
|
import { localSubcommandHelp } from "./local-help-commands.js";
|
|
13
13
|
export const rootCommandNames = new Set([
|
|
14
|
+
"roots",
|
|
14
15
|
"onboard",
|
|
15
16
|
"update",
|
|
16
17
|
"upgrade",
|
|
@@ -61,6 +62,7 @@ export function localCommandHelp(command) {
|
|
|
61
62
|
if (command)
|
|
62
63
|
return localSubcommandHelp(command);
|
|
63
64
|
return [
|
|
65
|
+
" cockpit roots add <dir> [--allow-home-root] [--json]",
|
|
64
66
|
" 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]",
|
|
65
67
|
" cockpit update [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--workspace <path>] [--allow-home-root] [--no-auth] [--json]",
|
|
66
68
|
" cockpit upgrade [same flags as update]",
|
|
@@ -102,7 +104,10 @@ export function localCommandHelp(command) {
|
|
|
102
104
|
` cockpit search "<words>" [--kind ${SEARCH_KINDS.join(",")}] [--limit <n>] [--dashboard-url <url>] [--json]`,
|
|
103
105
|
" cockpit release [--dry-run] [--skip-checks] [--no-floor] [--tag <tag>] [--access <public|restricted>] [--otp <code>]",
|
|
104
106
|
" cockpit careers [list|show <id>|rescreen <id>] [--role <slug>] [--min-score <n>] [--since <date>] [--json]",
|
|
105
|
-
" cockpit usage
|
|
107
|
+
" cockpit usage sessions --subject <name> [--repo <label>] [--since <window>] [--json]",
|
|
108
|
+
" cockpit usage sessions --session <id> [--from-window <n>] [--json]",
|
|
109
|
+
" --by-topic groups work types; --by-subject groups open subjects from session summaries.",
|
|
110
|
+
" cockpit usage people [--by-repo | --by-topic | --by-subject] [--min-confidence high|medium|low] [--person <email|me>] [--all] [--detail] [--since <n>d|<n>h|<iso>] [--until <iso>] [--include-automated] [--dashboard-url <url>] [--json]",
|
|
106
111
|
"",
|
|
107
112
|
`Default dashboard: ${DEFAULT_DASHBOARD_URL}. Omit --dashboard-url for normal production use; pass it only for staging/custom dashboards or to force a different pairing.`,
|
|
108
113
|
].join("\n");
|
package/dist/commands/local.js
CHANGED
|
@@ -54,6 +54,7 @@ export { buildCollectorHeartbeat, collectionRootLabel, collectionRootLabels, sen
|
|
|
54
54
|
export { classifySyncFailureRecords, classifySyncHealthError, redactedSyncErrorDetail, reportInstallEventsBestEffort, SYNC_ERROR_DETAIL_MAX_CHARS, } from "./install-receipts.js";
|
|
55
55
|
export { assertCollectionRootPersisted } from "./collection-roots.js";
|
|
56
56
|
export { runSelfUpdate, SelfUpdateError, } from "./install-update.js";
|
|
57
|
+
import { runRoots } from "./roots.js";
|
|
57
58
|
export async function runLocalCockpitCli(argv, io = defaultIo()) {
|
|
58
59
|
if (isLocalHelpRequest(argv)) {
|
|
59
60
|
writeLine(io.stdout, localCommandHelp(argv[0]));
|
|
@@ -85,6 +86,7 @@ export async function runLocalCockpitCli(argv, io = defaultIo()) {
|
|
|
85
86
|
beginStage(`command:${command.kind}`);
|
|
86
87
|
try {
|
|
87
88
|
switch (command.kind) {
|
|
89
|
+
case "roots": return await runRoots(command, io);
|
|
88
90
|
case "install":
|
|
89
91
|
return await runInstall(command, io);
|
|
90
92
|
case "onboard":
|
|
@@ -15,7 +15,7 @@ export async function runCockpitCli(argv, io) {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
if (command === "--version" || command === "-V" || command === "version") {
|
|
18
|
-
writeLine(io?.stdout ?? process.stdout, "0.2.
|
|
18
|
+
writeLine(io?.stdout ?? process.stdout, "0.2.111");
|
|
19
19
|
return 0;
|
|
20
20
|
}
|
|
21
21
|
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import { stat } from "node:fs/promises";
|
|
3
|
+
import { getCollectorRuntimePaths, ensureLocalCollectorConfig } from "../local-state.js";
|
|
4
|
+
import { normalizeRootsDetailed } from "../onboarding-roots.js";
|
|
5
|
+
import { normalizeCollectionRoots } from "../root-normalization.js";
|
|
6
|
+
import { recordRootApprovals } from "../root-work.js";
|
|
7
|
+
import { writeJsonFile } from "../local-state-files.js";
|
|
8
|
+
import { writeLine } from "./cli-io.js";
|
|
9
|
+
export function parseRootsArgs(args) {
|
|
10
|
+
const [action, directory, ...flags] = args;
|
|
11
|
+
if (action !== "add" || !directory || directory.startsWith("--"))
|
|
12
|
+
throw new Error("Usage: cockpit roots add <dir> [--allow-home-root] [--json]");
|
|
13
|
+
let homeDir;
|
|
14
|
+
for (let index = 0; index < flags.length; index++) {
|
|
15
|
+
const flag = flags[index];
|
|
16
|
+
if (flag === "--home-dir" && flags[index + 1]) {
|
|
17
|
+
homeDir = flags[++index];
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
if (flag !== "--json" && flag !== "--allow-home-root")
|
|
21
|
+
throw new Error(`Unknown roots flag: ${flag}`);
|
|
22
|
+
}
|
|
23
|
+
return { kind: "roots", action, directory, homeDir, json: flags.includes("--json"), allowHomeRoot: flags.includes("--allow-home-root") };
|
|
24
|
+
}
|
|
25
|
+
export async function runRoots(command, io) {
|
|
26
|
+
const homeDir = command.homeDir ?? os.homedir();
|
|
27
|
+
const { roots, rejected } = normalizeRootsDetailed([command.directory], { homeDir, allowHomeRoot: command.allowHomeRoot });
|
|
28
|
+
if (!roots.length || rejected.length)
|
|
29
|
+
throw new Error("Choose a work directory, or pass --allow-home-root to approve your home folder.");
|
|
30
|
+
for (const root of roots)
|
|
31
|
+
if (!(await stat(root)).isDirectory())
|
|
32
|
+
throw new Error("Collection root must be a directory.");
|
|
33
|
+
const paths = getCollectorRuntimePaths(homeDir);
|
|
34
|
+
const config = (await ensureLocalCollectorConfig({ homeDir })).config;
|
|
35
|
+
const updated = { ...config, default_repo_paths: normalizeCollectionRoots([...config.default_repo_paths, ...roots]) };
|
|
36
|
+
await recordRootApprovals(paths, [...config.default_repo_paths, ...roots]);
|
|
37
|
+
await writeJsonFile(paths.config_file, updated);
|
|
38
|
+
console.error("[roots] added", JSON.stringify({ added: roots.length, roots: updated.default_repo_paths.length }));
|
|
39
|
+
writeLine(io.stdout, command.json ? JSON.stringify({ ok: true, roots: updated.default_repo_paths }) : `Approved roots: ${updated.default_repo_paths.join(", ")}. Run cockpit doctor to collect the work there.`);
|
|
40
|
+
return 0;
|
|
41
|
+
}
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* that writes the closed-registry LABEL the health receipt is classified by.
|
|
10
10
|
* Nothing here parses a rendered sentence back apart.
|
|
11
11
|
*/
|
|
12
|
+
import { collectionRootLabel } from "./heartbeat.js";
|
|
13
|
+
import { workDirectories } from "../root-work.js";
|
|
12
14
|
import { createSyncFailureLedger, recordSourceScanFailures, recordUnexplainedFailure, recordUnpostedSessionReportFailure, recordWorktreeDeliveryFailures, } from "./session-sync-failures.js";
|
|
13
15
|
import { countSessionsNewThisTick, countSessionsPendingUpload, } from "./session-sync-counters.js";
|
|
14
16
|
import { logDeliveryHold, summarizeDeliveryHold } from "./session-sync-hold.js";
|
|
@@ -60,6 +62,9 @@ export function decideSyncHealth(options) {
|
|
|
60
62
|
notice,
|
|
61
63
|
hold,
|
|
62
64
|
sessions_outside_root: sessionsOutsideRoot,
|
|
65
|
+
outside_root_directories: workDirectories([...(options.scan.codexAttribution.results ?? []), ...(options.scan.claudeAttribution.results ?? [])].filter(result => OUTSIDE_APPROVED_ROOT_REASONS.has(result.reason)))
|
|
66
|
+
.map(entry => ({ ...collectionRootLabel(entry.directory), count: entry.count }))
|
|
67
|
+
.filter(entry => entry.basename.length > 0 && !/[\\/]/u.test(entry.basename)).slice(0, 40),
|
|
63
68
|
sessions_new_this_tick: countSessionsNewThisTick({
|
|
64
69
|
sessions: options.sessions,
|
|
65
70
|
codexCursorBefore: options.plan.codexCursorBefore,
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* `repo_not_on_disk` widens the next window and never fails a sync.
|
|
9
9
|
*/
|
|
10
10
|
import path from "node:path";
|
|
11
|
+
import { saveRootWorkScan } from "../root-work.js";
|
|
11
12
|
import { CODEX_ATTRIBUTION_SCAN_WINDOW_SESSION_LIMIT, CODEX_ATTRIBUTION_SCAN_WINDOW_MINUTES, defaultCodexSessionDirs, scanAndAttributeCodexSessions, } from "../adapters/codex-attribution.js";
|
|
12
13
|
import { scanAndAttributeClaudeSessions, } from "../adapters/claude-attribution.js";
|
|
13
14
|
import { CLAUDE_CURSOR_FILENAME } from "../cursors/raw-evidence-cursor.js";
|
|
@@ -61,6 +62,7 @@ export async function scanAndAttributeBothSources(options) {
|
|
|
61
62
|
reason: sourceScanRetryReason("claude_code", claudeAttribution),
|
|
62
63
|
});
|
|
63
64
|
}
|
|
65
|
+
await saveRootWorkScan(options.paths, [...codexAttribution.results, ...claudeAttribution.results], options.now);
|
|
64
66
|
return { codexAttribution, claudeAttribution, firstRunBackfill };
|
|
65
67
|
}
|
|
66
68
|
/**
|
|
@@ -133,6 +133,7 @@ export async function runAttributedWorktreeSync(options) {
|
|
|
133
133
|
hold: health.hold,
|
|
134
134
|
sessions_observed: sessions.length,
|
|
135
135
|
sessions_outside_root: health.sessions_outside_root,
|
|
136
|
+
outside_root_directories: health.outside_root_directories,
|
|
136
137
|
sessions_new_this_tick: health.sessions_new_this_tick,
|
|
137
138
|
sessions_pending_upload: health.sessions_pending_upload,
|
|
138
139
|
outcomes: worktreePass.outcomes,
|
|
@@ -147,6 +147,7 @@ function heartbeatCounts(run) {
|
|
|
147
147
|
return {
|
|
148
148
|
sessionsObserved: run.sessionsObserved,
|
|
149
149
|
sessionsOutsideRoot: run.sessionsOutsideRoot,
|
|
150
|
+
outsideRootDirectories: run.outsideRootDirectories,
|
|
150
151
|
sessionsNewThisTick: run.sessionsNewThisTick,
|
|
151
152
|
sessionsPendingUpload: run.sessionsPendingUpload,
|
|
152
153
|
};
|
|
@@ -155,6 +155,7 @@ function syncResult(run) {
|
|
|
155
155
|
holdNotice: run.hold?.notice ?? null,
|
|
156
156
|
sessionsObserved: run.sessions_observed,
|
|
157
157
|
sessionsOutsideRoot: run.sessions_outside_root,
|
|
158
|
+
outsideRootDirectories: run.outside_root_directories,
|
|
158
159
|
sessionsNewThisTick: run.sessions_new_this_tick,
|
|
159
160
|
sessionsPendingUpload: run.sessions_pending_upload,
|
|
160
161
|
};
|
package/dist/commands/usage.js
CHANGED
|
@@ -5,6 +5,8 @@ import { writeLine } from "./cli-io.js";
|
|
|
5
5
|
export async function runUsage(command, io) {
|
|
6
6
|
const door = await openAgentDoor("usage", command, io);
|
|
7
7
|
const query = new URLSearchParams({ since: command.since });
|
|
8
|
+
if (command.bySubject)
|
|
9
|
+
query.set("groupBy", "topic");
|
|
8
10
|
if (command.minConfidence)
|
|
9
11
|
query.set("minConfidence", command.minConfidence);
|
|
10
12
|
if (command.byTopic)
|
|
@@ -15,6 +17,21 @@ export async function runUsage(command, io) {
|
|
|
15
17
|
query.set("until", command.until);
|
|
16
18
|
if (command.includeAutomated)
|
|
17
19
|
query.set("include_automated", "1");
|
|
20
|
+
if (command.action === "sessions") {
|
|
21
|
+
for (const [key, value] of [["subject", command.subject], ["repo", command.repo], ["session", command.session], ["fromWindow", command.fromWindow]])
|
|
22
|
+
if (value)
|
|
23
|
+
query.set(key, value);
|
|
24
|
+
const response = await askAgentDoor(door, { path: `/api/usage/sessions?${query}`, method: "GET", label: "usage sessions", timeoutMs: 30_000 });
|
|
25
|
+
if (!response.ok)
|
|
26
|
+
return failAgentDoor(door, "[usage]", response.reason, response.detail);
|
|
27
|
+
if (door.json || command.session)
|
|
28
|
+
return emitAgentDoor(door, response.body);
|
|
29
|
+
const sessions = response.body.sessions ?? [];
|
|
30
|
+
for (const session of sessions)
|
|
31
|
+
writeLine(io.stdout, `${session.session_id} ${formatCount(session.tokens)} ${session.task_type} ${session.title}`);
|
|
32
|
+
writeLine(io.stdout, `${sessions.length} sessions`);
|
|
33
|
+
return 0;
|
|
34
|
+
}
|
|
18
35
|
const answer = await askAgentDoor(door, { path: `/api/usage/people?${query}`, method: "GET", label: "usage people", timeoutMs: 30_000 });
|
|
19
36
|
if (!answer.ok)
|
|
20
37
|
return failAgentDoor(door, "[usage]", answer.reason, answer.detail);
|
|
@@ -31,6 +48,8 @@ export async function runUsage(command, io) {
|
|
|
31
48
|
}
|
|
32
49
|
if (command.byTopic && body.people?.some((person) => !Array.isArray(person.task_types)))
|
|
33
50
|
return failAgentDoor(door, "[usage]", "topic_grouping_unavailable", "The dashboard returned person totals without topic rows. Deploy the dashboard topic split before using --by-topic.");
|
|
51
|
+
if (command.bySubject && body.people?.some(person => !Array.isArray(person.subjects)))
|
|
52
|
+
return failAgentDoor(door, "[usage]", "subject_grouping_unavailable", "Deploy the dashboard subject split before using --by-subject.");
|
|
34
53
|
if (door.json)
|
|
35
54
|
return emitAgentDoor(door, body);
|
|
36
55
|
let nameWidth = 20;
|
|
@@ -41,6 +60,13 @@ export async function runUsage(command, io) {
|
|
|
41
60
|
nameWidth = Math.max(nameWidth, repo.repo_label.length + 2);
|
|
42
61
|
}
|
|
43
62
|
}
|
|
63
|
+
if (command.bySubject)
|
|
64
|
+
for (const person of body.people ?? [])
|
|
65
|
+
for (const subject of person.subjects ?? []) {
|
|
66
|
+
nameWidth = Math.max(nameWidth, subject.repo_label.length + subject.topic.length + 5);
|
|
67
|
+
for (const sub of subject.children ?? [])
|
|
68
|
+
nameWidth = Math.max(nameWidth, sub.subtopic.length + 4);
|
|
69
|
+
}
|
|
44
70
|
const widths = [nameWidth, 8, 15, 12, 10, 10, 12, 14];
|
|
45
71
|
const printRow = (cells) => writeLine(io.stdout, cells.map((cell, index) => index === 0 ? cell.padEnd(widths[index]) : cell.padStart(widths[index])).join(" ").trimEnd());
|
|
46
72
|
printRow(["Person", "Tokens", "List equivalent", "Coverage", ...(command.detail ? ["Output", "Input", "Cache read", "Cache creation"] : [])]);
|
|
@@ -52,6 +78,12 @@ export async function runUsage(command, io) {
|
|
|
52
78
|
`${row.sessions_extracted}/${row.sessions_observed}`,
|
|
53
79
|
...(command.detail ? [row.output_tokens, row.input_tokens, row.cache_read_input_tokens, row.cache_creation_input_tokens].map(formatCount) : []),
|
|
54
80
|
]);
|
|
81
|
+
if (command.bySubject)
|
|
82
|
+
for (const subject of row.subjects ?? []) {
|
|
83
|
+
printRow([` ${subject.repo_label} / ${subject.topic}`, formatCount(subject.tokens), formatUsageDollars(subject.api_list_price_equivalent_usd), `${subject.extracted_sessions}/${subject.sessions}`, ...(command.detail ? [subject.output, subject.input, subject.cache_read, subject.cache_creation].map(formatCount) : [])]);
|
|
84
|
+
for (const sub of subject.children ?? [])
|
|
85
|
+
printRow([` ${sub.subtopic}`, formatCount(sub.tokens), formatUsageDollars(sub.api_list_price_equivalent_usd), `${sub.extracted_sessions}/${sub.sessions}`, ...(command.detail ? [sub.output, sub.input, sub.cache_read, sub.cache_creation].map(formatCount) : [])]);
|
|
86
|
+
}
|
|
55
87
|
if (command.byRepo || command.byTopic) {
|
|
56
88
|
const repos = (command.byTopic ? row.task_types : row.repos) ?? [];
|
|
57
89
|
for (const repo of command.all ? repos : repos.slice(0, 10)) {
|
|
@@ -66,6 +98,8 @@ export async function runUsage(command, io) {
|
|
|
66
98
|
writeLine(io.stdout, "");
|
|
67
99
|
writeLine(io.stdout, body.api_list_price_equivalent_label ?? "API list-price equivalent (not actual spend)");
|
|
68
100
|
writeLine(io.stdout, `${body.coverage?.sessions_extracted ?? 0} of ${body.coverage?.sessions_observed ?? 0} sessions extracted`);
|
|
101
|
+
if (command.bySubject)
|
|
102
|
+
writeLine(io.stdout, `${(body.people ?? []).reduce((n, person) => n + (person.sessions_summarized ?? 0), 0)} of ${body.coverage?.sessions_observed ?? 0} sessions summarized`);
|
|
69
103
|
if (command.byTopic)
|
|
70
104
|
writeLine(io.stdout, `${body.coverage?.sessions_labelled ?? 0} of ${body.coverage?.sessions_observed ?? 0} sessions labelled by topic`);
|
|
71
105
|
return 0;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { LocalCollectorConfigSchema, } from "@bli-cockpit/telemetry-core";
|
|
2
2
|
import crypto from "node:crypto";
|
|
3
|
+
import { recordRootApprovals } from "./root-work.js";
|
|
3
4
|
import { readFileSync } from "node:fs";
|
|
4
5
|
import os from "node:os";
|
|
5
6
|
import path from "node:path";
|
|
@@ -44,6 +45,7 @@ export async function installLocalCollector(options = {}) {
|
|
|
44
45
|
session_file_path: paths.session_file,
|
|
45
46
|
state_dir_path: paths.state_dir,
|
|
46
47
|
});
|
|
48
|
+
await recordRootApprovals(paths, defaultRepoPaths, options.replaceRepoRoots);
|
|
47
49
|
await writeJsonFile(paths.config_file, config);
|
|
48
50
|
return {
|
|
49
51
|
config,
|
package/dist/onboarding-roots.js
CHANGED
|
@@ -64,6 +64,10 @@ export async function resolveOnboardingRoots(options) {
|
|
|
64
64
|
}
|
|
65
65
|
return promptForRoots(options, "Collection root(s), comma-separated: ");
|
|
66
66
|
}
|
|
67
|
+
const suggestions = normalizeRootsDetailed(options.suggestedRoots ?? [], { homeDir: options.homeDir, allowHomeRoot: options.allowHomeRoot }).roots;
|
|
68
|
+
if (options.interactive && suggestions.length > 0 && await requirePrompt(options).confirm(`Sessions were found in:\n${suggestions.join("\n")}\nCollect from these folders? [Y/n] `)) {
|
|
69
|
+
return resolvedRoots(options, suggestions, "prompt", true);
|
|
70
|
+
}
|
|
67
71
|
const cwd = path.resolve(options.cwd ?? process.cwd());
|
|
68
72
|
if (isHomeRoot(cwd, options.homeDir)) {
|
|
69
73
|
const homeDir = path.resolve(options.homeDir ?? os.homedir());
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/** Local directory evidence and explicit root approvals. Full paths stay here. */
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { readJsonFile, writeJsonFile, isMissingFileError } from "./local-state-files.js";
|
|
5
|
+
import { containsPath, normalizeCollectionRoots } from "./root-normalization.js";
|
|
6
|
+
const DirectorySchema = z.object({ directory: z.string(), count: z.number().int().nonnegative() });
|
|
7
|
+
const ScanSchema = z.object({ generated_at: z.string(), observed: z.number(), directories: z.array(DirectorySchema) });
|
|
8
|
+
export const rootPathApi = (platform = process.platform) => platform === "win32" ? path.win32 : path.posix;
|
|
9
|
+
export function workDirectories(results, platform = process.platform) {
|
|
10
|
+
const api = rootPathApi(platform);
|
|
11
|
+
const counts = new Map();
|
|
12
|
+
for (const result of results) {
|
|
13
|
+
if (!result.local_cwd || !api.isAbsolute(result.local_cwd))
|
|
14
|
+
continue;
|
|
15
|
+
const directory = api.resolve(result.local_cwd);
|
|
16
|
+
const key = platform === "win32" ? directory.toLowerCase() : directory;
|
|
17
|
+
const entry = counts.get(key) ?? { directory, count: 0 };
|
|
18
|
+
entry.count += 1;
|
|
19
|
+
counts.set(key, entry);
|
|
20
|
+
}
|
|
21
|
+
return [...counts.values()].sort((a, b) => b.count - a.count || a.directory.localeCompare(b.directory));
|
|
22
|
+
}
|
|
23
|
+
export function outsideWorkDirectories(scan, roots, platform = process.platform) {
|
|
24
|
+
return scan.directories.filter(({ directory }) => !roots.some(root => containsPath(root, directory, rootPathApi(platform))));
|
|
25
|
+
}
|
|
26
|
+
export async function saveRootWorkScan(paths, results, now) {
|
|
27
|
+
const scan = { generated_at: now.toISOString(), observed: results.length, directories: workDirectories(results) };
|
|
28
|
+
try {
|
|
29
|
+
await writeJsonFile(path.join(paths.state_dir, "root-work-scan.json"), scan);
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
console.error("[roots] work scan cache failed", JSON.stringify({ reason: "scan_cache_write_failed", error_name: error instanceof Error ? error.name : "unknown" }));
|
|
33
|
+
return scan;
|
|
34
|
+
}
|
|
35
|
+
console.error("[roots] work scan recorded", JSON.stringify({ observed: scan.observed, directories: scan.directories.length }));
|
|
36
|
+
return scan;
|
|
37
|
+
}
|
|
38
|
+
export async function readRootWorkScan(paths) {
|
|
39
|
+
try {
|
|
40
|
+
return ScanSchema.parse(await readJsonFile(path.join(paths.state_dir, "root-work-scan.json")));
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
if (isMissingFileError(error))
|
|
44
|
+
return null;
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const ApprovalsSchema = z.object({ approved_at: z.string(), roots: z.array(z.string()) });
|
|
49
|
+
export async function readRootApprovals(paths) {
|
|
50
|
+
try {
|
|
51
|
+
return ApprovalsSchema.parse(await readJsonFile(path.join(paths.config_dir, "root-approvals.json"))).roots;
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
if (isMissingFileError(error))
|
|
55
|
+
return [];
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export async function recordRootApprovals(paths, roots, replace = false) {
|
|
60
|
+
const previous = replace ? [] : await readRootApprovals(paths);
|
|
61
|
+
const approved = normalizeCollectionRoots([...previous, ...roots]);
|
|
62
|
+
await writeJsonFile(path.join(paths.config_dir, "root-approvals.json"), { approved_at: new Date().toISOString(), roots: approved });
|
|
63
|
+
console.error("[roots] approval recorded", JSON.stringify({ roots: approved.length, replaced: replace }));
|
|
64
|
+
}
|
|
65
|
+
export function rootsAddCommand(directory, platform = process.platform, homeDir) {
|
|
66
|
+
// Native Windows command is for PowerShell; POSIX quoting is for zsh/bash.
|
|
67
|
+
const escaped = platform === "win32" ? directory.replace(/'/gu, "''") : directory.replace(/'/gu, "'\\''");
|
|
68
|
+
const homeFlag = homeDir && rootPathApi(platform).relative(homeDir, directory) === "" ? " --allow-home-root" : "";
|
|
69
|
+
return `cockpit roots add '${escaped}'${homeFlag}`;
|
|
70
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bli-cockpit/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.111",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -27,8 +27,8 @@
|
|
|
27
27
|
"test": "node dist/cli.js --help && node ../../scripts/assert-public-cli-routing.mjs && node ../../scripts/assert-public-cli-verb-help.mjs && node ../../scripts/assert-public-cli-runtime-files.mjs && node ../../scripts/assert-public-cli-exit-contract.mjs && node ../../scripts/assert-public-cli-no-fleet-posts.mjs && node ../../scripts/assert-public-package-pack.mjs --workspace=@bli-cockpit/cli"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@bli-cockpit/memory-mcp": "0.1.
|
|
31
|
-
"@bli-cockpit/mcp": "0.1.
|
|
32
|
-
"@bli-cockpit/telemetry-core": "0.1.
|
|
30
|
+
"@bli-cockpit/memory-mcp": "0.1.29",
|
|
31
|
+
"@bli-cockpit/mcp": "0.1.39",
|
|
32
|
+
"@bli-cockpit/telemetry-core": "0.1.45"
|
|
33
33
|
}
|
|
34
34
|
}
|