@staix/agent-mem 0.12.0 → 0.13.0
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/.claude-plugin/plugin.json +1 -1
- package/README.md +50 -43
- package/package.json +4 -2
- package/src/cli-agent-reads.ts +105 -0
- package/src/cli.ts +211 -55
- package/src/commands-table.ts +9 -9
- package/src/index.ts +1 -0
- package/src/ingest.ts +21 -4
- package/src/mcp.ts +8 -8
- package/src/memory/candidates.ts +15 -13
- package/src/memory/capacity-split.ts +4 -1
- package/src/memory/capacity.ts +48 -4
- package/src/memory/cli-attribution.ts +23 -0
- package/src/memory/commands.ts +59 -11
- package/src/memory/consolidate.ts +3 -2
- package/src/memory/decay-review.ts +4 -4
- package/src/memory/doctor.ts +23 -4
- package/src/memory/metrics.ts +20 -8
- package/src/memory/migrate.ts +30 -0
- package/src/memory/note-add.ts +4 -4
- package/src/memory/prune.ts +28 -1
- package/src/memory/shard-migration.ts +38 -0
- package/src/memory/store.ts +5 -4
- package/src/memory/style.ts +3 -0
- package/src/memory/vault.ts +31 -0
- package/src/setup.ts +59 -31
package/src/setup.ts
CHANGED
|
@@ -1,22 +1,28 @@
|
|
|
1
|
+
import { cliAttribution } from "./memory/cli-attribution.ts";
|
|
2
|
+
import { configuredPolicy } from "./memory/config.ts";
|
|
1
3
|
import { SETUP_CONFIG_BYTES, updateTomlArray } from "./setup-toml.ts";
|
|
2
4
|
import { managedAfter, mcpRecord, putMcpRecord, restoreOwned, sameImage, type ConfigImage } from "./setup-owned.ts";
|
|
3
5
|
import { spawnSync } from "node:child_process";
|
|
4
6
|
import { createHash, randomUUID } from "node:crypto";
|
|
5
7
|
import { closeSync, constants, fchmodSync, fstatSync, fsyncSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readSync, renameSync, rmdirSync, rmSync, writeFileSync } from "node:fs";
|
|
6
8
|
import { homedir, tmpdir } from "node:os";
|
|
7
|
-
import { createInterface } from "node:readline/promises";
|
|
8
9
|
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
9
10
|
import { fileURLToPath } from "node:url";
|
|
10
11
|
import { isDeepStrictEqual } from "node:util";
|
|
11
12
|
import { HOOK_EVENTS } from "./hook-contract.ts";
|
|
12
13
|
import { memoryHome } from "./memory/home.ts";
|
|
13
|
-
import { label, PLAIN, type Style } from "./memory/style.ts";
|
|
14
|
+
import { label, PLAIN, singleLine, type Style } from "./memory/style.ts";
|
|
14
15
|
import { canonicalPath, memoryGitEnv, memoryGitSync, processLiveness, processSignature, realPath } from "./memory/platform.ts";
|
|
15
16
|
|
|
16
17
|
export const SETUP_TARGETS = ["claude", "codex", "kimi", "pi", "qwen", "jcode", "agy"] as const;
|
|
17
18
|
/** Workspace-scoped Antigravity setup is always selected explicitly. */
|
|
18
19
|
export const SETUP_ALL_TARGETS = SETUP_TARGETS.filter(target => target !== "agy");
|
|
19
20
|
export type SetupTarget = typeof SETUP_TARGETS[number];
|
|
21
|
+
// Only recorded layout verification versions belong here; null means no version was recorded.
|
|
22
|
+
const SETUP_VERIFIED_VERSIONS: Readonly<Record<SetupTarget, string | null>> = {
|
|
23
|
+
claude: null, codex: "0.165.0", kimi: null, pi: null, qwen: "0.26.1", jcode: "0.99.1", agy: null,
|
|
24
|
+
};
|
|
25
|
+
interface ClientVersion { output: string | null; verified: string | null; matches: boolean | null }
|
|
20
26
|
type Env = Readonly<Record<string, string | undefined>>;
|
|
21
27
|
type Image = ConfigImage;
|
|
22
28
|
interface PlannedHook { event: string; command: string; timeout: number }
|
|
@@ -25,7 +31,7 @@ interface DirectoryIdentity { dev: number; ino: number }
|
|
|
25
31
|
interface Receipt { createdDirectory?: DirectoryIdentity; version: 1; target: SetupTarget; intent: string; phase: "prepared" | "applied"; changes: Change[] }
|
|
26
32
|
export interface SetupContext {
|
|
27
33
|
env?: Env; bun?: string; entry?: string; stdinTTY?: boolean; cwd?: string;
|
|
28
|
-
/**
|
|
34
|
+
/** @deprecated Retained for source compatibility; explicit apply/undo modes do not prompt. */
|
|
29
35
|
confirm?: (plan: SetupPlan) => Promise<boolean>;
|
|
30
36
|
/** Host-only race fixture, called after temporary-file fsync and before the final comparison/rename. */
|
|
31
37
|
beforePublish?: (file: string) => void;
|
|
@@ -34,7 +40,7 @@ export interface SetupContext {
|
|
|
34
40
|
}
|
|
35
41
|
export interface SetupPlan {
|
|
36
42
|
targets: SetupTarget[]; mode: "preview" | "apply" | "undo";
|
|
37
|
-
actions: { target: SetupTarget; files: string[]; receipt: string; cwd?: string; registration: string[] | null; lifecycle: string; available: boolean; hooks: PlannedHook[]; mcpChange: "add" | "replace" | "restore" | "unchanged"; commands: string[][]; state: string }[];
|
|
43
|
+
actions: { target: SetupTarget; version?: ClientVersion; files: string[]; receipt: string; cwd?: string; registration: string[] | null; lifecycle: string; available: boolean; hooks: PlannedHook[]; mcpChange: "add" | "replace" | "restore" | "unchanged"; commands: string[][]; state: string }[];
|
|
38
44
|
notes: string[];
|
|
39
45
|
}
|
|
40
46
|
const MAX = SETUP_CONFIG_BYTES, JOURNAL_MAX = 12 * MAX;
|
|
@@ -163,7 +169,7 @@ function assertJcodeRuntime(c: ReturnType<typeof context>): void {
|
|
|
163
169
|
assertJcodeHome(c.env);
|
|
164
170
|
if ([c.bun, c.entry].some(value => value.includes("${"))) throw Error("Jcode interpolates environment placeholders in MCP command paths; setup cannot register this runtime exactly");
|
|
165
171
|
}
|
|
166
|
-
function intent(c: ReturnType<typeof context>, target: SetupTarget): string { return digest([1, target, c.paths[target], c.bun, c.entry, ...(target === "agy" ? [c.workspace!.root] : [])]); }
|
|
172
|
+
function intent(c: ReturnType<typeof context>, target: SetupTarget): string { return digest([1, target, c.paths[target], c.bun, c.entry, ...(["codex", "kimi", "qwen"].includes(target) ? ["capture-json-v1"] : []), ...(target === "agy" ? [c.workspace!.root] : [])]); }
|
|
167
173
|
function journal(c: ReturnType<typeof context>, target: SetupTarget): string { return join(c.state, target === "agy" ? `agy-${digest(c.workspace!.root)}.json` : `${target}.json`); }
|
|
168
174
|
function receipt(c: ReturnType<typeof context>, target: SetupTarget, requireRuntime = false): Receipt | undefined {
|
|
169
175
|
const raw = read(journal(c, target), JOURNAL_MAX); if (raw.bytes === null) return;
|
|
@@ -186,7 +192,7 @@ function plannedHooks(c: ReturnType<typeof context>, target: SetupTarget): Plann
|
|
|
186
192
|
if (target === "jcode" || target === "agy") return [];
|
|
187
193
|
const launch = `${quote(c.bun)} --smol ${quote(c.entry)}`;
|
|
188
194
|
return target === "claude" ? HOOK_EVENTS.map(event => ({ event, command: `${launch} hook ${event}`, timeout: 1 }))
|
|
189
|
-
: [{ event: target === "pi" ? "session_shutdown" : "SessionEnd", command: `${launch} ingest ${target}`, timeout: target === "pi" ? 2.5 : 3 }];
|
|
195
|
+
: [{ event: target === "pi" ? "session_shutdown" : "SessionEnd", command: `${launch} ingest ${target}${target === "pi" ? "" : " --json"}`, timeout: target === "pi" ? 2.5 : 3 }];
|
|
190
196
|
}
|
|
191
197
|
const lifecycle = (target: SetupTarget) => target === "jcode" || target === "agy" ? "MCP only; native capture unavailable" : target === "claude" ? "seven bounded native hooks" : target === "pi" ? "session_shutdown incremental ingest" : "SessionEnd incremental ingest";
|
|
192
198
|
export function planSetup(target: string, mode: SetupPlan["mode"] = "preview", options: SetupContext = {}): SetupPlan {
|
|
@@ -219,8 +225,43 @@ export function planSetup(target: string, mode: SetupPlan["mode"] = "preview", o
|
|
|
219
225
|
const hasAmem = target === "jcode" || target === "agy" ? mcpRecord(target, original) !== undefined : Object.hasOwn(record(document[target === "codex" ? "mcp_servers" : "mcpServers"] ?? {}), "amem");
|
|
220
226
|
const unchanged = !!prior && prior.phase === "applied" && prior.intent === intent(c, target) && prior.changes.every((change, index) => { try { const live = read(change.path); restoreChange(prior, change, index, live); return managedAfter(target, index, change.before, change.after, live); } catch { return false; } });
|
|
221
227
|
const add = command(target, c.bun, c.entry), remove = [target, "mcp", "remove", ...(target === "claude" || target === "qwen" ? ["--scope", "user"] : []), "amem"];
|
|
222
|
-
|
|
223
|
-
|
|
228
|
+
const installed = available(c, target, options), version = installed && target !== "agy" && mode !== "undo" ? probeClientVersion(c, target, options) : undefined;
|
|
229
|
+
if (version?.matches === false) pluginNotes.push(`WARNING: ${target} version ${version.output}; layout verified on ${version.verified}. Apply remains guarded by native configuration validation.`);
|
|
230
|
+
return { target, available: installed, ...(version ? { version } : {}), files: c.paths[target], receipt: journal(c, target), ...(target === "agy" ? { cwd: c.workspace!.root } : {}), registration: mode === "undo" ? null : add, lifecycle: mode === "undo" ? "restore original MCP/hook files" : lifecycle(target), hooks: mode === "undo" || unchanged ? [] : plannedHooks(c, target), mcpChange: mode === "undo" ? prior ? "restore" : "unchanged" : unchanged ? "unchanged" : hasAmem ? "replace" : "add", commands: mode === "undo" || prior?.intent === intent(c, target) || !add || !installed ? [] : [...(hasAmem ? [remove] : []), add], state: prior && prior.intent !== intent(c, target) ? "runtime changed; managed entries will update" : prior?.phase ?? "not installed" };
|
|
231
|
+
}), notes: [...pluginNotes, ...(/\/[^/]*\d+\.\d+[^/]*\/(?:bin\/)?bun$/.test(c.bun) ? ["this bun path is version-specific; rerun setup --apply after changing Bun"] : []), "Preview only unless --apply or --undo is passed; explicit modes apply without a prompt.", "Native registration commands run in a private staging HOME; only the listed configuration files are published.", "Original file bytes restore exactly when untouched; otherwise undo preserves CLI-owned changes and restores only managed entries, refusing edits to those entries.", "No AGENTS.md, CLAUDE.md or rule files are edited.", `A private ${join(c.state, "setup.lock")} serializes mutations; temporary ${join(realPath(tmpdir()), "amem-setup-*")} staging homes are removed.`, "Codex requires its native /hooks review before newly registered hooks run; setup does not grant hook trust. Runtime registrations use the PATH-resolved absolute Bun path without resolving its stable symlink; setup all reports and skips missing clients."] };
|
|
232
|
+
}
|
|
233
|
+
/** Native probes and registration share one private HOME, cwd and configuration layout. */
|
|
234
|
+
function stagingEnvironment(c: ReturnType<typeof context>, target: SetupTarget, options: SetupContext, staging: string) {
|
|
235
|
+
const env: Record<string, string | undefined> = { ...c.env, HOME: staging, AMEM_HOME: join(staging, "memory"), AGENTHUB_HOME: join(staging, "hub"), CODEX_HOME: join(staging, ".codex"), KIMI_CODE_HOME: join(staging, ".kimi-code"), PI_CODING_AGENT_DIR: join(staging, ".pi", "agent"), QWEN_HOME: join(staging, ".qwen"), JCODE_HOME: join(staging, ".jcode") };
|
|
236
|
+
Object.assign(env, { XDG_CONFIG_HOME: join(staging, ".config"), XDG_DATA_HOME: join(staging, ".local", "share"), XDG_CACHE_HOME: join(staging, ".cache"), XDG_STATE_HOME: join(staging, ".local", "state"), TMPDIR: join(staging, "tmp"), TMP: join(staging, "tmp"), TEMP: join(staging, "tmp") });
|
|
237
|
+
env.QWEN_RUNTIME_DIR = join(staging, "qwen-runtime");
|
|
238
|
+
env.QWEN_CODE_SYSTEM_SETTINGS_PATH = join(staging, "qwen-system-settings.json");
|
|
239
|
+
env.QWEN_CODE_SYSTEM_DEFAULTS_PATH = join(staging, "qwen-system-defaults.json");
|
|
240
|
+
mkdirSync(env.TMPDIR!, { mode: 0o700 });
|
|
241
|
+
if (c.env.CLAUDE_CONFIG_DIR?.trim()) env.CLAUDE_CONFIG_DIR = join(staging, ".claude"); else delete env.CLAUDE_CONFIG_DIR;
|
|
242
|
+
// The staging workspace is a trusted private binding, not a Git repository or the real caller cwd.
|
|
243
|
+
const staged = context({ ...options, env, cwd: staging }, target === "agy" ? { root: staging, ignored: true, assertCurrent() {} } : undefined);
|
|
244
|
+
// Codex refuses an explicit CODEX_HOME that does not exist, even for mcp add.
|
|
245
|
+
mkdirSync(dirname(staged.paths[target][0]!), { recursive: true, mode: 0o700 });
|
|
246
|
+
return { env, staged };
|
|
247
|
+
}
|
|
248
|
+
/** Advisory only: never queried by apply/undo or persisted in the ownership receipt. */
|
|
249
|
+
function probeClientVersion(c: ReturnType<typeof context>, target: SetupTarget, options: SetupContext): ClientVersion {
|
|
250
|
+
const verified = SETUP_VERIFIED_VERSIONS[target];
|
|
251
|
+
let output: string | null = null;
|
|
252
|
+
// Like the help probe, trusted native fixtures do not invoke installed executables.
|
|
253
|
+
if (!options.native) {
|
|
254
|
+
let staging: string | undefined;
|
|
255
|
+
try {
|
|
256
|
+
staging = mkdtempSync(join(realPath(tmpdir()), "amem-setup-"));
|
|
257
|
+
const { env } = stagingEnvironment(c, target, options, staging);
|
|
258
|
+
const result = spawnSync(target, ["--version"], { cwd: staging, env: { ...env }, encoding: "utf8", timeout: 1000, maxBuffer: 4096, stdio: ["ignore", "pipe", "pipe"] });
|
|
259
|
+
if (result.status === 0 && !result.error) output = Array.from(singleLine(result.stdout).trim()).slice(0, 256).join("") || null;
|
|
260
|
+
} catch { /* An absent, failed or timed-out native version never prevents setup. */ }
|
|
261
|
+
finally { if (staging) try { rmSync(staging, { recursive: true, force: true }); } catch { output = null; } }
|
|
262
|
+
}
|
|
263
|
+
const observed = output?.match(/(?:^|\s)v?(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)(?=\s|$)/)?.[1];
|
|
264
|
+
return { output, verified, matches: observed === undefined || verified === null ? null : observed === verified };
|
|
224
265
|
}
|
|
225
266
|
function verifyNativeMcp(target: SetupTarget, env: Env, cwd: string): void {
|
|
226
267
|
// In older free-form Pi CLIs, an unknown subcommand can be treated as a model prompt.
|
|
@@ -268,17 +309,7 @@ function prepare(c: ReturnType<typeof context>, target: SetupTarget, options: Se
|
|
|
268
309
|
const changes: Change[] = c.paths[target].map((path, i) => ({ key: `${target}:${i}`, path, before: restored[i]!, after: restored[i]!, start: expected[i]! }));
|
|
269
310
|
const staging = mkdtempSync(join(realPath(tmpdir()), "amem-setup-"));
|
|
270
311
|
try {
|
|
271
|
-
const
|
|
272
|
-
Object.assign(env, { XDG_CONFIG_HOME: join(staging, ".config"), XDG_DATA_HOME: join(staging, ".local", "share"), XDG_CACHE_HOME: join(staging, ".cache"), XDG_STATE_HOME: join(staging, ".local", "state"), TMPDIR: join(staging, "tmp"), TMP: join(staging, "tmp"), TEMP: join(staging, "tmp") });
|
|
273
|
-
env.QWEN_RUNTIME_DIR = join(staging, "qwen-runtime");
|
|
274
|
-
env.QWEN_CODE_SYSTEM_SETTINGS_PATH = join(staging, "qwen-system-settings.json");
|
|
275
|
-
env.QWEN_CODE_SYSTEM_DEFAULTS_PATH = join(staging, "qwen-system-defaults.json");
|
|
276
|
-
mkdirSync(env.TMPDIR!, { mode: 0o700 });
|
|
277
|
-
if (c.env.CLAUDE_CONFIG_DIR?.trim()) env.CLAUDE_CONFIG_DIR = join(staging, ".claude"); else delete env.CLAUDE_CONFIG_DIR;
|
|
278
|
-
// The staging workspace is a trusted private binding, not a Git repository or the real caller cwd.
|
|
279
|
-
const staged = context({ ...options, env, cwd: staging }, target === "agy" ? { root: staging, ignored: true, assertCurrent() {} } : undefined);
|
|
280
|
-
// Codex refuses an explicit CODEX_HOME that does not exist, even for mcp add.
|
|
281
|
-
mkdirSync(dirname(staged.paths[target][0]!), { recursive: true, mode: 0o700 });
|
|
312
|
+
const { env, staged } = stagingEnvironment(c, target, options, staging);
|
|
282
313
|
for (let i = 0; i < changes.length; i++) if (expected[i]!.bytes !== null) write(staged.paths[target][i]!, expected[i]!);
|
|
283
314
|
const argv = command(target, c.bun, c.entry);
|
|
284
315
|
if (argv && prior?.intent === intent(c, target)) {
|
|
@@ -335,7 +366,7 @@ function prepare(c: ReturnType<typeof context>, target: SetupTarget, options: Se
|
|
|
335
366
|
servers.amem = { command: c.bun, args: ["--smol", c.entry, "mcp"] }; document.mcpServers = servers;
|
|
336
367
|
changes[0]!.after = objectImage(document, changes[0]!.before);
|
|
337
368
|
}
|
|
338
|
-
const capture = (kind: string) => `${quote(c.bun)} --smol ${quote(c.entry)} ingest ${kind}`;
|
|
369
|
+
const capture = (kind: string) => `${quote(c.bun)} --smol ${quote(c.entry)} ingest ${kind} --json`;
|
|
339
370
|
if (target === "claude") changes[1]!.after = hookDocument(changes[1]!.before, HOOK_EVENTS, event => `${quote(c.bun)} --smol ${quote(c.entry)} hook ${event}`);
|
|
340
371
|
if (target === "codex") changes[1]!.after = hookDocument(changes[1]!.before, ["SessionEnd"], () => capture("codex"), 3);
|
|
341
372
|
// Qwen's user MCP and hooks share settings.json: publish and journal one combined image.
|
|
@@ -391,7 +422,7 @@ function contextForPlan(plan: SetupPlan, options: SetupContext) {
|
|
|
391
422
|
function publishGuard(c: ReturnType<typeof context>, target: SetupTarget, options: SetupContext) {
|
|
392
423
|
return target === "agy" ? (file: string) => { options.beforePublish?.(file); c.workspace!.assertCurrent(); } : options.beforePublish;
|
|
393
424
|
}
|
|
394
|
-
/**
|
|
425
|
+
/** CLI-owned unrelated fields remain live. */
|
|
395
426
|
function applyUnlocked(plan: SetupPlan, options: SetupContext = {}): SetupApplyResult {
|
|
396
427
|
const c = contextForPlan(plan, options), skipped: { target: SetupTarget; reason: string }[] = [];
|
|
397
428
|
const targets = plan.targets.filter(target => { if (available(c, target, options)) return true; if (plan.targets.length === 1) throw Error(`${target} CLI is not installed`); skipped.push({ target, reason: "CLI not installed" }); return false; });
|
|
@@ -461,6 +492,10 @@ export function setupPlanText(plan: SetupPlan, s: Style = PLAIN): string {
|
|
|
461
492
|
const state = action.mcpChange === "unchanged" ? "ok" : "attention";
|
|
462
493
|
const what = action.mcpChange === "unchanged" ? plan.mode === "undo" ? "no setup receipt; nothing to restore" : "already set up; no change" : action.mcpChange === "restore" ? `restore original MCP/hook files${action.available ? "" : " (CLI not installed)"}` : `MCP ${action.mcpChange}`;
|
|
463
494
|
lines.push(`${label(s, state)} ${s.bold(action.target)}: ${what} ${s.dim(`(${action.state})`)}`);
|
|
495
|
+
if (action.version) {
|
|
496
|
+
const reported = action.version.output === null ? "unknown" : action.version.verified && action.version.matches === null ? `unrecognised (${action.version.output})` : action.version.output;
|
|
497
|
+
lines.push(` version ${reported}; ${action.version.verified ? `verified ${action.version.verified}` : "unverified (no verified version recorded)"}`);
|
|
498
|
+
}
|
|
464
499
|
for (const file of action.files) lines.push(` file ${file}`);
|
|
465
500
|
if (action.cwd) lines.push(` cwd ${action.cwd}`);
|
|
466
501
|
lines.push(` receipt ${action.receipt}`);
|
|
@@ -473,7 +508,7 @@ export function setupPlanText(plan: SetupPlan, s: Style = PLAIN): string {
|
|
|
473
508
|
if (skipped.length) lines.push("", `Skipped clients: ${skipped.map(action => action.target).join(", ")} (not installed)`);
|
|
474
509
|
if (warnings.length) lines.push("", "Warnings:", ...warnings.map(note => ` - ${s.attention(note)}`));
|
|
475
510
|
lines.push("", "Notes:", ...notes.map(note => ` - ${note}`));
|
|
476
|
-
if (plan.mode === "preview") lines.push("", `Next: amem setup ${plan.targets.length === 1 ? plan.targets[0] : "all"} --apply
|
|
511
|
+
if (plan.mode === "preview") lines.push("", `Next: amem setup ${plan.targets.length === 1 ? plan.targets[0] : "all"} --apply writes without a prompt; --json prints the full plan`);
|
|
477
512
|
return lines.join("\n");
|
|
478
513
|
}
|
|
479
514
|
|
|
@@ -483,20 +518,13 @@ function setupResultText(result: SetupApplyResult | { restored: SetupTarget[] },
|
|
|
483
518
|
...(result.skipped ?? []).map(item => `${label(s, "skipped")} ${item.target}: ${item.reason}`)].join("\n");
|
|
484
519
|
}
|
|
485
520
|
|
|
486
|
-
/**
|
|
521
|
+
/** Compatibility formatter for confirmation-style callers: the raw plan under `--json`, the readable plan otherwise. */
|
|
487
522
|
export function setupConfirmationText(plan: SetupPlan, json: boolean, s: Style = PLAIN): string { return json ? JSON.stringify(plan, null, 2) : setupPlanText(plan, s); }
|
|
488
523
|
|
|
489
524
|
export async function runSetup(target: string, mode: SetupPlan["mode"], options: SetupContext & { json?: boolean; style?: Style } = {}): Promise<string> {
|
|
490
525
|
const plan = planSetup(target, mode, options), s = options.json ? PLAIN : options.style ?? PLAIN;
|
|
526
|
+
if (cliAttribution()?.actor === "agent" && !configuredPolicy(memoryHome(options.env ?? process.env), options.cwd ?? process.cwd()).screenStandalone(JSON.parse(JSON.stringify(plan)))) throw Error("Memory operation unavailable or refused; no partial result is shown.");
|
|
491
527
|
if (mode === "preview") return options.json ? JSON.stringify(plan, null, 2) : setupPlanText(plan, s);
|
|
492
|
-
if (!(options.stdinTTY ?? process.stdin.isTTY)) throw Error("setup --apply/--undo require a terminal on stdin");
|
|
493
|
-
const confirm = options.confirm ?? (async (shown: SetupPlan) => {
|
|
494
|
-
process.stdout.write(setupConfirmationText(shown, options.json === true, s) + "\n\n");
|
|
495
|
-
const terminal = createInterface({ input: process.stdin, output: process.stdout });
|
|
496
|
-
try { return (await terminal.question(`Type yes to ${mode} the listed configuration changes: `)).trim() === "yes"; }
|
|
497
|
-
finally { terminal.close(); }
|
|
498
|
-
});
|
|
499
|
-
if (!await confirm(plan)) return options.json ? JSON.stringify({ cancelled: true }) : "setup cancelled; nothing changed";
|
|
500
528
|
const result = mode === "apply" ? applySetup(plan, options) : undoSetup(plan, options);
|
|
501
529
|
return options.json ? JSON.stringify(result, null, 2) : setupResultText(result, s);
|
|
502
530
|
}
|