@nurix/nustack 0.10.0-dev.18 → 0.10.0-dev.20

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.
@@ -0,0 +1,55 @@
1
+ import { answerLogscope, listLogscopeFunctions } from "../lib/logs/answer.js";
2
+ import { loadLogscope } from "../lib/logs/library.js";
3
+ import { allowSources, forgetCorpusCommand, indexLogs, listAllowed, listCorpora, pruneCorpora, revokeSources, } from "../lib/logs/lifecycle.js";
4
+ export function logscopeArgv(rawArgv) {
5
+ const args = rawArgv.slice(2);
6
+ const at = args.indexOf("logscope");
7
+ return at === -1 ? [] : args.slice(at + 1);
8
+ }
9
+ const VALUED = ["--root", "--corpus", "--budget", "--retain", "--since", "--until", "--revision", "--note"];
10
+ const BARE = ["--json", "--examples", "--no-redact", "--dry-run"];
11
+ export function functionArgv(argv, fn) {
12
+ const valued = fn !== undefined && fn.args["limit"] !== undefined ? VALUED : [...VALUED, "--limit"];
13
+ const out = [];
14
+ for (let i = 0; i < argv.length; i++) {
15
+ const token = argv[i] ?? "";
16
+ if (valued.includes(token)) {
17
+ i++;
18
+ continue;
19
+ }
20
+ if (valued.some((flag) => token.startsWith(`${flag}=`)))
21
+ continue;
22
+ if (BARE.includes(token))
23
+ continue;
24
+ out.push(token);
25
+ }
26
+ return out;
27
+ }
28
+ function positionals(argv) {
29
+ return functionArgv(argv)
30
+ .slice(1)
31
+ .filter((token) => !token.startsWith("--"));
32
+ }
33
+ export async function runLogscope(rawArgv, options, cwd = process.cwd()) {
34
+ const argv = logscopeArgv(rawArgv);
35
+ const fn = argv.find((token) => !token.startsWith("--"));
36
+ if (fn === undefined || fn === "functions")
37
+ return listLogscopeFunctions(options);
38
+ if (fn === "index")
39
+ return indexLogs(positionals(argv), options, cwd);
40
+ if (fn === "corpora")
41
+ return listCorpora(options);
42
+ if (fn === "allow")
43
+ return allowSources(positionals(argv), options, cwd);
44
+ if (fn === "allowed")
45
+ return listAllowed(options, cwd);
46
+ if (fn === "revoke")
47
+ return revokeSources(positionals(argv), options, cwd);
48
+ if (fn === "prune")
49
+ return pruneCorpora(options, cwd);
50
+ if (fn === "forget")
51
+ return forgetCorpusCommand(positionals(argv)[0], options);
52
+ const library = await loadLogscope();
53
+ const rest = functionArgv(argv, library.findFn(fn));
54
+ return answerLogscope(fn, rest.slice(1), options, cwd);
55
+ }
@@ -8,16 +8,30 @@ const EXIT_FOR = {
8
8
  none: 1,
9
9
  unreachable: 2,
10
10
  };
11
+ const latestAnswered = (items) => items.find((item) => item.answer?.verdict === "signed" || item.answer?.verdict === "refused");
11
12
  export function decisionStateOf(items) {
12
- const latest = items.find((item) => item.answer?.verdict === "signed" || item.answer?.verdict === "refused");
13
+ const latest = latestAnswered(items);
13
14
  if (!latest)
14
15
  return "none";
15
16
  return latest.answer?.verdict === "signed" ? "signed" : "refused";
16
17
  }
17
- function checkLine(state, commit) {
18
+ export function waivedKeysOf(items) {
19
+ const latest = latestAnswered(items);
20
+ if (latest?.answer?.verdict !== "signed")
21
+ return [];
22
+ const waivers = latest.answer.waivers;
23
+ if (!Array.isArray(waivers))
24
+ return [];
25
+ return waivers
26
+ .map((waiver) => waiver?.criterionKey)
27
+ .filter((key) => typeof key === "string" && key.length > 0);
28
+ }
29
+ function checkLine(state, commit, waived) {
18
30
  switch (state) {
19
31
  case "signed":
20
- return `review check: ${commit} is signed off\n`;
32
+ return waived.length
33
+ ? `review check: ${commit} is signed off — ${waived.length} ${waived.length === 1 ? "criterion" : "criteria"} waived: ${waived.join(", ")}\n`
34
+ : `review check: ${commit} is signed off\n`;
21
35
  case "refused":
22
36
  return `review check: ${commit} was refused\n`;
23
37
  case "none":
@@ -34,11 +48,11 @@ export async function runReviewCheck(options, globals) {
34
48
  process.exitCode = 2;
35
49
  return;
36
50
  }
37
- const report = (state) => {
51
+ const report = (state, waived = []) => {
38
52
  if (json)
39
- printJsonSuccess("review check", { commit, state, exitCode: EXIT_FOR[state] });
53
+ printJsonSuccess("review check", { commit, state, exitCode: EXIT_FOR[state], waived });
40
54
  else
41
- process.stdout.write(checkLine(state, commit));
55
+ process.stdout.write(checkLine(state, commit, waived));
42
56
  process.exitCode = EXIT_FOR[state];
43
57
  };
44
58
  const baseUrl = resolveNuStackUrl(globals);
@@ -47,7 +61,7 @@ export async function runReviewCheck(options, globals) {
47
61
  return report("unreachable");
48
62
  try {
49
63
  const items = await makeReviewClient(baseUrl, deviceAuth.accessToken).readDecisions(commit, options.repository?.trim() || null);
50
- report(decisionStateOf(items));
64
+ report(decisionStateOf(items), waivedKeysOf(items));
51
65
  }
52
66
  catch (error) {
53
67
  if (error instanceof ReviewUnreachableError)
@@ -77,7 +91,16 @@ function readSubject(raw) {
77
91
  type: "tool_permission",
78
92
  toolName,
79
93
  displayName,
80
- toolInput: row.toolInput ?? null,
94
+ ...(typeof row.target === "string" || row.target === null ? { target: row.target } : {}),
95
+ ...(typeof row.inputBytes === "number" && Number.isFinite(row.inputBytes)
96
+ ? { inputBytes: row.inputBytes }
97
+ : {}),
98
+ ...(Array.isArray(row.inputFields) && row.inputFields.every((field) => typeof field === "string")
99
+ ? { inputFields: row.inputFields }
100
+ : {}),
101
+ ...(typeof row.redactionVersion === "number" && Number.isFinite(row.redactionVersion)
102
+ ? { redactionVersion: row.redactionVersion }
103
+ : {}),
81
104
  ...(typeof row.description === "string" ? { description: row.description } : {}),
82
105
  };
83
106
  }
@@ -140,9 +163,9 @@ export function registerReviewCommands(reviewCommand, getGlobalOptions) {
140
163
  .action(async (options) => runReviewCheck(options, getGlobalOptions()));
141
164
  reviewCommand
142
165
  .command("mirror")
143
- .description("Reflect a lane's permission prompt, or its answer, into the Review pane")
144
- .option("--session <laneId>", "the lane's session id")
145
- .option("--project <id>", "the Project the lane's folder is bound to (required with --state open)")
166
+ .description("Reflect a session's permission prompt, or its answer, into the Review pane")
167
+ .option("--session <sessionId>", "the session's id")
168
+ .option("--project <id>", "the Project the session's folder is bound to (required with --state open)")
146
169
  .option("--state <state>", "open or answered")
147
170
  .option("--subject <json>", "the tool-permission subject, as JSON (required with --state open)")
148
171
  .option("--verdict <verdict>", "allow or deny (required with --state answered)")
@@ -1,9 +1,25 @@
1
1
  import { readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import * as p from "@clack/prompts";
2
4
  import { ensureAccessToken } from "../lib/auth_grant.js";
5
+ import { isInteractive } from "../lib/interactive.js";
3
6
  import { resolveNuStackUrl } from "../lib/nustack_client.js";
4
- import { makeSessionsClient } from "../lib/sessions/client.js";
7
+ import { makeSessionsClient, sessionsClientFromEnv } from "../lib/sessions/client.js";
5
8
  import { recordOutsideSessions, } from "../lib/sessions/outsideSessions.js";
6
- import { printJsonSuccess } from "../lib/json_output.js";
9
+ import { inspectSession, readOrphanedWorktrees, reclaimSession, } from "../lib/session-engine/prune.js";
10
+ import { readSessionRegistry, sessionRegistryPath } from "../lib/session-engine/registry.js";
11
+ import { SessionSupervisor } from "../lib/session-engine/supervisor.js";
12
+ import { printJsonError, printJsonSuccess } from "../lib/json_output.js";
13
+ export async function runSessionsServe(engineVersion) {
14
+ const supervisor = new SessionSupervisor({
15
+ input: process.stdin,
16
+ output: process.stdout,
17
+ engineVersion,
18
+ sessions: sessionsClientFromEnv(process.env),
19
+ });
20
+ await supervisor.run();
21
+ process.exit(0);
22
+ }
7
23
  async function readScanArgument(raw) {
8
24
  const body = raw.startsWith("@") ? await readFile(raw.slice(1), "utf8") : raw;
9
25
  return JSON.parse(body);
@@ -46,6 +62,10 @@ export async function runSessionsRecord(options, globals) {
46
62
  if (!options.scan)
47
63
  return report("sessions record", EMPTY, json, "no scan payload");
48
64
  const payload = (await readScanArgument(options.scan).catch(() => null));
65
+ if (payload && "laneSessionIds" in payload) {
66
+ process.stderr.write("sessions record: this NuStack desktop is older than this CLI: it sent laneSessionIds, which is now engineSessionIds. Update both.\n");
67
+ process.exit(1);
68
+ }
49
69
  const transcripts = readTranscripts(payload?.sessions);
50
70
  if (transcripts.length === 0)
51
71
  return report("sessions record", EMPTY, json, "nothing to record");
@@ -54,9 +74,140 @@ export async function runSessionsRecord(options, globals) {
54
74
  return report("sessions record", EMPTY, json, "signed out");
55
75
  const summary = await recordOutsideSessions(makeSessionsClient(baseUrl, deviceAuth.accessToken), {
56
76
  transcripts,
57
- laneSessionIds: Array.isArray(payload?.laneSessionIds)
58
- ? payload.laneSessionIds.filter((id) => typeof id === "string")
77
+ engineSessionIds: Array.isArray(payload?.engineSessionIds)
78
+ ? payload.engineSessionIds.filter((id) => typeof id === "string")
59
79
  : [],
60
80
  });
61
81
  report("sessions record", summary, json);
62
82
  }
83
+ async function readSessions() {
84
+ const { records, dropped } = await readSessionRegistry();
85
+ const orphans = await readOrphanedWorktrees();
86
+ const sessions = [];
87
+ for (const record of [...records, ...orphans.records]) {
88
+ sessions.push({ record, facts: await inspectSession(record) });
89
+ }
90
+ return { sessions, unreadable: dropped, undecipherableOrphans: orphans.undecipherable };
91
+ }
92
+ function statusWord(entry) {
93
+ if (entry.facts.orphaned)
94
+ return "orphaned";
95
+ if (entry.facts.stale)
96
+ return "stale";
97
+ return entry.record.status;
98
+ }
99
+ function formatSessionRow(entry) {
100
+ const checkout = path.basename(entry.record.projectPath);
101
+ return (`${entry.record.sessionId.padEnd(38)} ${checkout.padEnd(22)} ${(entry.record.branch ?? "—").padEnd(31)} ` +
102
+ `${statusWord(entry).padEnd(10)} ${entry.facts.reason}`);
103
+ }
104
+ function toJsonView(entry) {
105
+ const { record, facts } = entry;
106
+ return {
107
+ sessionId: record.sessionId,
108
+ projectPath: record.projectPath,
109
+ worktreePath: record.worktreePath,
110
+ branch: record.branch,
111
+ status: record.status,
112
+ nustack: record.nustack,
113
+ createdAt: record.createdAt,
114
+ closedAt: record.closedAt ?? null,
115
+ permissionMode: record.permissionMode ?? null,
116
+ baseBranch: record.baseBranch ?? null,
117
+ isolated: facts.isolated,
118
+ checkoutPresent: facts.checkoutPresent,
119
+ worktreePresent: facts.worktreePresent,
120
+ branchExists: facts.branchExists,
121
+ reading: facts.reading,
122
+ dirty: facts.dirty,
123
+ stale: facts.stale,
124
+ orphaned: facts.orphaned,
125
+ reclaimable: facts.reclaimable,
126
+ reason: facts.reason,
127
+ };
128
+ }
129
+ export async function runSessionsList(options) {
130
+ const json = options.json === true;
131
+ const { sessions, unreadable, undecipherableOrphans } = await readSessions();
132
+ if (json) {
133
+ printJsonSuccess("sessions list", {
134
+ registryPath: sessionRegistryPath(),
135
+ unreadable,
136
+ undecipherableOrphans,
137
+ sessions: sessions.map(toJsonView),
138
+ });
139
+ return;
140
+ }
141
+ if (sessions.length === 0 && unreadable === 0 && undecipherableOrphans.length === 0) {
142
+ console.log("No engine-opened sessions on this machine.");
143
+ return;
144
+ }
145
+ if (sessions.length > 0) {
146
+ console.log(`${"SESSION".padEnd(38)} ${"CHECKOUT".padEnd(22)} ${"BRANCH".padEnd(31)} ${"STATUS".padEnd(10)} HOLDS`);
147
+ for (const entry of sessions)
148
+ console.log(formatSessionRow(entry));
149
+ }
150
+ if (unreadable > 0)
151
+ console.log(`${unreadable} row(s) in the registry could not be read and were skipped.`);
152
+ for (const orphan of undecipherableOrphans) {
153
+ console.log(`1 worktree under the pre-rename engine home names no checkout and must be removed by hand: ${orphan}.`);
154
+ }
155
+ }
156
+ function previewLine(entry) {
157
+ const verb = entry.facts.reclaimable ? "reclaim" : "refuse";
158
+ return ` ${verb} ${entry.record.sessionId} ${entry.record.branch ?? "—"} — ${entry.facts.reason}`;
159
+ }
160
+ export async function runSessionsPrune(options) {
161
+ const json = options.json === true;
162
+ const force = options.force === true;
163
+ const { sessions } = await readSessions();
164
+ const scoped = options.session ? sessions.filter((entry) => entry.record.sessionId === options.session) : sessions;
165
+ if (options.session && scoped.length === 0) {
166
+ const message = `No session ${options.session} in this machine's registry — pass one from \`nustack sessions list\`.`;
167
+ if (json)
168
+ return printJsonError("sessions prune", { code: "SESSION_NOT_FOUND", message });
169
+ p.log.error(message);
170
+ process.exitCode = 1;
171
+ return;
172
+ }
173
+ const candidates = scoped.filter((entry) => force || entry.facts.reclaimable);
174
+ if (options.yes !== true) {
175
+ const plan = candidates.map((entry) => ({
176
+ sessionId: entry.record.sessionId,
177
+ branch: entry.record.branch,
178
+ worktreePath: entry.record.worktreePath,
179
+ orphaned: entry.facts.orphaned,
180
+ reclaimable: entry.facts.reclaimable,
181
+ reason: entry.facts.reason,
182
+ }));
183
+ if (json)
184
+ return printJsonSuccess("sessions prune", { applied: false, plan, note: "preview only — pass --yes to apply" });
185
+ if (plan.length === 0) {
186
+ console.log("Nothing to reclaim.");
187
+ return;
188
+ }
189
+ console.log(`Would reclaim ${plan.length} session(s):`);
190
+ for (const entry of candidates)
191
+ console.log(previewLine(entry));
192
+ if (!isInteractive()) {
193
+ console.log("Preview only — re-run with --yes to apply.");
194
+ return;
195
+ }
196
+ const answer = await p.confirm({ message: "Remove these worktrees and delete their branches?" });
197
+ if (p.isCancel(answer) || !answer) {
198
+ p.outro("Cancelled — nothing reclaimed.");
199
+ return;
200
+ }
201
+ }
202
+ const reclaimed = [];
203
+ const refused = [];
204
+ for (const entry of candidates) {
205
+ const outcome = await reclaimSession(entry.record, { force });
206
+ (outcome.reason === null ? reclaimed : refused).push(outcome);
207
+ }
208
+ if (json)
209
+ return printJsonSuccess("sessions prune", { applied: true, reclaimed, refused });
210
+ console.log(`Reclaimed ${reclaimed.length} session(s); refused ${refused.length}.`);
211
+ for (const outcome of refused)
212
+ console.log(` refused ${outcome.sessionId} — ${outcome.reason ?? "unknown"}`);
213
+ }
package/dist/index.js CHANGED
@@ -9,6 +9,7 @@ import { runSync } from "./commands/sync.js";
9
9
  import { runDescribe } from "./commands/describe.js";
10
10
  import { runValidate } from "./commands/validate.js";
11
11
  import { runGraph } from "./commands/graph.js";
12
+ import { runLogscope } from "./commands/logscope.js";
12
13
  import { runDoctor } from "./commands/doctor.js";
13
14
  import { runTokenCreate, runTokenList, runTokenRevoke } from "./commands/token.js";
14
15
  import { runOpen } from "./commands/open.js";
@@ -23,8 +24,7 @@ import { runOrganizationList, runOrganizationCreate, runOrganizationUse } from "
23
24
  import { runDocsList, runDocsShow, runDocsStatus } from "./commands/docs.js";
24
25
  import { runHandoffList, runHandoffShow } from "./commands/handoff.js";
25
26
  import { runToolkitStatus, runToolkitReconcile } from "./commands/toolkit.js";
26
- import { runLaneServe } from "./commands/lane.js";
27
- import { runSessionsRecord } from "./commands/sessions.js";
27
+ import { runSessionsList, runSessionsPrune, runSessionsRecord, runSessionsServe, } from "./commands/sessions.js";
28
28
  import { registerReviewCommands } from "./commands/review.js";
29
29
  import { runAgentContext, runAgentHookInstall, runAgentHookRemove } from "./commands/agent.js";
30
30
  import { printJsonError } from "./lib/json_output.js";
@@ -96,6 +96,25 @@ program
96
96
  .allowUnknownOption()
97
97
  .allowExcessArguments()
98
98
  .action(async (_fn, _args, options) => runGraph(process.argv, options));
99
+ program
100
+ .command("logscope [fn] [args...]")
101
+ .description("Log tracking: ask this machine's log corpus a question, or `index` / `corpora` / `allow` / `prune`")
102
+ .option("--root <dir>", "the project this command is about (default: the working tree's root)")
103
+ .option("--corpus <ref>", "a corpus id, or a path belonging to one")
104
+ .option("--budget <n>", "token ceiling for the answer; 0 means unbounded")
105
+ .option("--limit <n>", "rows requested — beats a default budget, never an explicit one")
106
+ .option("--retain <days>", "retention for `index`; 0 keeps the corpus until it is forgotten")
107
+ .option("--examples", "keep up to three exemplar log lines per template (off by default)")
108
+ .option("--no-redact", "store exemplars unredacted (loud, recorded on the corpus)")
109
+ .option("--since <ts>", "window start for container sources")
110
+ .option("--until <ts>", "window end for container sources")
111
+ .option("--revision <sha>", "the source revision these logs were written by")
112
+ .option("--note <text>", "a note recorded with an `allow` grant")
113
+ .option("--dry-run", "for `prune`: report what would be dropped and delete nothing")
114
+ .option("--json", "emit the versioned machine envelope with the answer as data")
115
+ .allowUnknownOption()
116
+ .allowExcessArguments()
117
+ .action(async (_fn, _args, options) => runLogscope(process.argv, options));
99
118
  program
100
119
  .command("doctor")
101
120
  .description("Run the full local diagnostics matrix (identity, repository, manifests, toolkit, telemetry, credentials)")
@@ -230,18 +249,32 @@ handoff
230
249
  .option("--targets", "list each target's path, operation, and base semantics")
231
250
  .option("--json", "emit the versioned machine envelope")
232
251
  .action(async (id, options) => runHandoffShow(id, options, program.opts()));
233
- const lane = program.command("lane").description("The desktop build room's lane engine");
234
- lane
252
+ const sessions = program
253
+ .command("sessions")
254
+ .description("Sessions on this machine — the engine the desktop drives, and the record of what ran");
255
+ sessions
235
256
  .command("serve")
236
- .description("Run the long-lived lane supervisor: JSONL frames on stdin/stdout (used by the NuStack desktop)")
237
- .action(async () => runLaneServe(version));
238
- const sessions = program.command("sessions").description("The session record over this machine's agent work");
257
+ .description("Run the long-lived session supervisor: JSONL frames on stdin/stdout (used by the NuStack desktop)")
258
+ .action(async () => runSessionsServe(version));
239
259
  sessions
240
260
  .command("record")
241
261
  .description("Record the sessions one desktop scan found in a bound folder (used by the NuStack desktop)")
242
262
  .option("--scan <json>", "the scan payload as JSON, or @<path> to read it from a file")
243
263
  .option("--json", "emit the versioned machine envelope")
244
264
  .action(async (options) => runSessionsRecord(options, program.opts()));
265
+ sessions
266
+ .command("list")
267
+ .description("List the sessions this machine's engine opened, and what each one's worktree and branch still hold")
268
+ .option("--json", "emit the versioned machine envelope")
269
+ .action(async (options) => runSessionsList(options));
270
+ sessions
271
+ .command("prune")
272
+ .description("Reclaim the worktree and branch of a session that has ended (preview unless --yes)")
273
+ .option("--session <id>", "reclaim one session instead of every reclaimable one")
274
+ .option("--yes", "apply the preview without prompting")
275
+ .option("--force", "also reclaim an unmerged or dirty session — never one whose engine is running")
276
+ .option("--json", "emit the versioned machine envelope")
277
+ .action(async (options) => runSessionsPrune(options));
245
278
  const toolkit = program.command("toolkit").description("Inspect and reconcile the Project's toolkit pin");
246
279
  toolkit
247
280
  .command("status")
@@ -0,0 +1,99 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { resolve } from "node:path";
4
+ import { printJsonSuccess } from "../json_output.js";
5
+ import { loadLogscope } from "./library.js";
6
+ export class LogsNoRepository extends Error {
7
+ code = "LOGS_NO_REPOSITORY";
8
+ constructor(where) {
9
+ super(`not a git repository: ${where} — pass --root <path> or run inside a checkout`);
10
+ this.name = "LogsNoRepository";
11
+ }
12
+ }
13
+ export class LogsUnavailable extends Error {
14
+ code = "LOGS_UNAVAILABLE";
15
+ constructor(reason) {
16
+ super(reason);
17
+ this.name = "LogsUnavailable";
18
+ }
19
+ }
20
+ export class LogsRefused extends Error {
21
+ code = "LOGS_REFUSED";
22
+ constructor(reason) {
23
+ super(reason);
24
+ this.name = "LogsRefused";
25
+ }
26
+ }
27
+ export class LogsNotFound extends Error {
28
+ code = "LOGS_NOT_FOUND";
29
+ constructor(reason) {
30
+ super(reason);
31
+ this.name = "LogsNotFound";
32
+ }
33
+ }
34
+ export function resolveRoot(root, cwd) {
35
+ if (root !== undefined) {
36
+ const path = resolve(cwd, root);
37
+ if (!existsSync(path))
38
+ throw new LogsNoRepository(root);
39
+ return path;
40
+ }
41
+ return gitRoot(cwd);
42
+ }
43
+ function gitRoot(from) {
44
+ try {
45
+ return execFileSync("git", ["rev-parse", "--show-toplevel"], { cwd: from, stdio: ["ignore", "pipe", "ignore"] })
46
+ .toString("utf8")
47
+ .trim();
48
+ }
49
+ catch {
50
+ throw new LogsNoRepository(from);
51
+ }
52
+ }
53
+ export const progress = (line) => {
54
+ process.stderr.write(`${line}\n`);
55
+ };
56
+ export async function answerLogscope(fn, argv, options, cwd) {
57
+ const library = await loadLogscope();
58
+ const known = library.findFn(fn);
59
+ if (known === undefined) {
60
+ throw new LogsUnavailable(`unknown logscope function '${fn}' — run \`nustack logscope functions\` for the list`);
61
+ }
62
+ const args = library.parseArgs(known, argv);
63
+ if (known.args["root"] !== undefined && args["root"] === undefined) {
64
+ args["root"] = resolveRoot(options.root, cwd);
65
+ }
66
+ if (args["limit"] === undefined && options.limit !== undefined)
67
+ args["limit"] = Number(options.limit);
68
+ const limit = args["limit"];
69
+ const budget = options.budget !== undefined ? Number(options.budget) : limit !== undefined ? 0 : library.DEFAULT_BUDGET;
70
+ const result = library.dispatch({ fn: known.name, args, corpus: options.corpus, budget });
71
+ if (result.answer.unavailable !== undefined)
72
+ throw new LogsUnavailable(result.answer.unavailable);
73
+ if (options.json === true) {
74
+ printJsonSuccess(`logscope ${known.name}`, result.answer);
75
+ return;
76
+ }
77
+ console.log(result.rendered);
78
+ }
79
+ export async function listLogscopeFunctions(options) {
80
+ const library = await loadLogscope();
81
+ const budget = options.budget !== undefined ? Number(options.budget) : 0;
82
+ const result = library.dispatch({ fn: "functions", args: {}, budget });
83
+ if (options.json === true) {
84
+ printJsonSuccess("logscope functions", result.answer);
85
+ return;
86
+ }
87
+ console.log(result.rendered);
88
+ console.log([
89
+ "",
90
+ "lifecycle — `nustack logscope <verb>`",
91
+ " allow <targets...> grant a log source; nothing is read before this",
92
+ " allowed the grants covering this project",
93
+ " revoke <targets...> withdraw grants",
94
+ " index <sources...> ingest or refresh a corpus from granted sources",
95
+ " corpora every corpus indexed on this machine",
96
+ " prune [--dry-run] drop every corpus past its retention",
97
+ " forget <id> drop one corpus and its store",
98
+ ].join("\n"));
99
+ }
@@ -0,0 +1,19 @@
1
+ export class LogsLibraryMissing extends Error {
2
+ code = "LOGS_LIBRARY_MISSING";
3
+ constructor() {
4
+ super("the log-tracking library is not installed with this nustack — install it beside the CLI (`npm i -g @nurix/logscope`) or run nustack from a checkout of the monorepo.");
5
+ this.name = "LogsLibraryMissing";
6
+ }
7
+ }
8
+ let cached = null;
9
+ export async function loadLogscope() {
10
+ if (cached !== null)
11
+ return cached;
12
+ try {
13
+ cached = (await import("@nurix/logscope"));
14
+ return cached;
15
+ }
16
+ catch {
17
+ throw new LogsLibraryMissing();
18
+ }
19
+ }