@tryaura/aura-cli 0.2.0 → 0.2.1

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.
@@ -1,4 +1,4 @@
1
- import { A as renderRemoveDiff, B as errorMessage$1, C as canonicalizeManagedSnippet, D as renderArchiveDiff, E as renderRedactedWriteDiff, F as assertAuraManifestWritable, H as resolveAuraManifestPath, I as createAuraManifestWriteOperation, K as pluralize, L as createEmptyAuraManifest, M as FILE_MODES, N as MAX_MUTABLE_FILE_BYTES, O as renderConflict, P as MAX_RETAINED_PLAN_BYTES, R as parseAuraManifest, S as readManagedBlock, T as hashManagedSnippet, U as AuraManifestError, V as isRecord$4, W as SHARED_INSTRUCTIONS_TEMPLATE, _ as planSharedSkillTreeUpdate, a as rememberMcpSecretPlanner, d as createAppMcpConvergence, g as managedContentRevisionStatus, j as renderSymlinkDiff, k as renderMoveDiff, m as sharedSkillsRoot, p as planSkillDeployment, q as displayPath, r as createAppMcpSecretPlanner, s as planDesiredMcpConvergence, t as planSharedInstructionLink, u as rememberMcpConvergence, v as reconcileParsedManagedBlock, w as hashCanonicalManagedSnippet, x as managedSnippetContentProblems, y as diffManagedSnippet, z as errorCode } from "./shared-link-plan-DVgOyqP9.js";
1
+ import { A as renderRemoveDiff, B as errorMessage$1, C as canonicalizeManagedSnippet, D as renderArchiveDiff, E as renderRedactedWriteDiff, F as assertAuraManifestWritable, H as resolveAuraManifestPath, I as createAuraManifestWriteOperation, K as pluralize, L as createEmptyAuraManifest, M as FILE_MODES, N as MAX_MUTABLE_FILE_BYTES, O as renderConflict, P as MAX_RETAINED_PLAN_BYTES, R as parseAuraManifest, S as readManagedBlock, T as hashManagedSnippet, U as AuraManifestError, V as isRecord$4, W as SHARED_INSTRUCTIONS_TEMPLATE, _ as planSharedSkillTreeUpdate, a as rememberMcpSecretPlanner, d as createAppMcpConvergence, g as managedContentRevisionStatus, j as renderSymlinkDiff, k as renderMoveDiff, m as sharedSkillsRoot, p as planSkillDeployment, q as displayPath, r as createAppMcpSecretPlanner, s as planDesiredMcpConvergence, t as planSharedInstructionLink, u as rememberMcpConvergence, v as reconcileParsedManagedBlock, w as hashCanonicalManagedSnippet, x as managedSnippetContentProblems, y as diffManagedSnippet, z as errorCode } from "./shared-link-plan-D8fxdFYv.js";
2
2
  import { COMMAND_NOT_FOUND_EXIT_CODE, DEFAULT_EXEC_TIMEOUT_MS, DEFAULT_HTTP_TIMEOUT_MS, MAX_EXEC_OUTPUT_CHARACTERS, MAX_EXEC_TIMEOUT_MS, MAX_HTTP_RESPONSE_BYTES, MAX_HTTP_TIMEOUT_MS, NOT_EXECUTABLE_EXIT_CODE, OUTPUT_LIMIT_EXIT_CODE, SHARED_INSTRUCTIONS_TEMPLATE_TOKEN, TIMEOUT_EXIT_CODE, defineOwnProperty, detectExecutable, hasMcpRedaction, jsonPropertyPath, mcpEnvironmentVariableNames, mcpServerNameProblem, parseMcpServerDefinition, parseMcpServerManifest, parseSkillFrontmatter, parseSkillReferences, resolveMcpSecretNameCollisions, resolveSkillDirectory, splitSourceLines } from "@tryaura/aura-sdk";
3
3
  import { basename, delimiter, dirname, isAbsolute, join, posix, relative, resolve, sep, win32 } from "node:path";
4
4
  import { Buffer as Buffer$1, isUtf8 } from "node:buffer";
@@ -64,8 +64,9 @@ function createExec(options) {
64
64
  return (request) => execute(request, options);
65
65
  }
66
66
  function execute(request, options) {
67
+ if (request.signal?.aborted === true) return Promise.reject(abortReason(request.signal));
67
68
  const timeoutMs = normalizeTimeout(request.timeoutMs);
68
- return new Promise((resolve) => {
69
+ return new Promise((resolve, reject) => {
69
70
  const plan = planSpawn(request.command, request.args ?? [], options.platform);
70
71
  const child = spawn(plan.command, [...plan.args], {
71
72
  cwd: request.cwd ?? options.cwd,
@@ -78,7 +79,27 @@ function execute(request, options) {
78
79
  let settled = false;
79
80
  let stderr = "";
80
81
  let stdout = "";
81
- const timeout = setTimeout(() => {
82
+ let timeout;
83
+ let abortOnSignal;
84
+ const cleanUp = () => {
85
+ if (timeout !== void 0) clearTimeout(timeout);
86
+ if (abortOnSignal !== void 0) request.signal?.removeEventListener("abort", abortOnSignal);
87
+ child.stdout.destroy();
88
+ child.stderr.destroy();
89
+ };
90
+ const finish = (result) => {
91
+ if (settled) return;
92
+ settled = true;
93
+ cleanUp();
94
+ resolve(result);
95
+ };
96
+ const fail = (error) => {
97
+ if (settled) return;
98
+ settled = true;
99
+ cleanUp();
100
+ reject(error);
101
+ };
102
+ timeout = setTimeout(() => {
82
103
  terminate(child, options.platform);
83
104
  finish({
84
105
  exitCode: TIMEOUT_EXIT_CODE,
@@ -86,13 +107,9 @@ function execute(request, options) {
86
107
  stdout
87
108
  });
88
109
  }, timeoutMs);
89
- const finish = (result) => {
90
- if (settled) return;
91
- settled = true;
92
- clearTimeout(timeout);
93
- child.stdout.destroy();
94
- child.stderr.destroy();
95
- resolve(result);
110
+ abortOnSignal = () => {
111
+ terminate(child, options.platform);
112
+ fail(abortReason(request.signal));
96
113
  };
97
114
  const abortOnOverflow = () => {
98
115
  terminate(child, options.platform);
@@ -128,8 +145,14 @@ function execute(request, options) {
128
145
  });
129
146
  });
130
147
  child.stdin.end(request.input);
148
+ request.signal?.addEventListener("abort", abortOnSignal, { once: true });
149
+ if (request.signal?.aborted === true) abortOnSignal();
131
150
  });
132
151
  }
152
+ /** The caller-selected reason, with the platform-standard fallback for a bare abort. */
153
+ function abortReason(signal) {
154
+ return signal?.reason ?? new DOMException("The command was aborted.", "AbortError");
155
+ }
133
156
  /**
134
157
  * Kills the command and every process it started.
135
158
  *
@@ -5022,6 +5045,7 @@ async function parseAdapter(adapter, detection, discovery, context) {
5022
5045
  cwd: context.environment.cwd,
5023
5046
  detection,
5024
5047
  files: discovery.files,
5048
+ gitMainWorktreeRoot: await context.gitMainWorktreeRoot,
5025
5049
  homeDir: context.environment.homeDir,
5026
5050
  projectRoot: await context.projectRoot
5027
5051
  });
@@ -5502,6 +5526,26 @@ async function findProjectRoot(cwd, reader) {
5502
5526
  directory = parent;
5503
5527
  }
5504
5528
  }
5529
+ /**
5530
+ * Resolves the primary checkout Git uses as the trust boundary for a repository.
5531
+ *
5532
+ * A normal checkout keeps a `.git` directory and is its own primary checkout. A linked worktree
5533
+ * keeps a `gitdir:` file pointing at `<primary>/.git/worktrees/<id>` instead, so the primary
5534
+ * checkout is recovered from that standard Git layout without requiring the Git executable.
5535
+ */
5536
+ async function findGitMainWorktreeRoot(projectRoot, reader) {
5537
+ const marker = await reader.read(join(projectRoot, ".git"));
5538
+ if (marker.problem !== void 0 || !marker.exists) return;
5539
+ if (marker.isDirectory) return projectRoot;
5540
+ if (marker.content === void 0) return;
5541
+ const pointer = marker.content.trim().match(/^gitdir:\s*(.+)$/u)?.[1]?.trim();
5542
+ if (pointer === void 0 || pointer.length === 0) return;
5543
+ const gitDirectory = resolve(projectRoot, pointer);
5544
+ const worktreesDirectory = dirname(gitDirectory);
5545
+ if (basename(worktreesDirectory) !== "worktrees") return;
5546
+ const mainWorktreeRoot = dirname(dirname(worktreesDirectory));
5547
+ return await reader.realPath(mainWorktreeRoot) ?? mainWorktreeRoot;
5548
+ }
5505
5549
  //#endregion
5506
5550
  //#region ../core/src/workspace/repository.ts
5507
5551
  const GIT_TIMEOUT_MS = 5e3;
@@ -5668,6 +5712,24 @@ async function readTrackedPackageManifestPaths(command, projectRoot, environment
5668
5712
  return [...new Set(result.stdout.split("\0").filter((path) => path.length > 0))].sort().slice(0, MAX_PACKAGE_MANIFESTS);
5669
5713
  }
5670
5714
  //#endregion
5715
+ //#region ../core/src/workspace/scan-cancellation.ts
5716
+ /** Injects one scan's cancellation into every command an adapter starts. */
5717
+ function withScanCancellation(environment, signal) {
5718
+ if (signal === void 0) return environment;
5719
+ return Object.freeze({
5720
+ ...environment,
5721
+ exec: async (request) => {
5722
+ signal.throwIfAborted();
5723
+ const result = await environment.exec({
5724
+ ...request,
5725
+ signal: request.signal === void 0 ? signal : AbortSignal.any([request.signal, signal])
5726
+ });
5727
+ signal.throwIfAborted();
5728
+ return result;
5729
+ }
5730
+ });
5731
+ }
5732
+ //#endregion
5671
5733
  //#region ../core/src/skills/limits.ts
5672
5734
  /**
5673
5735
  * The largest directory index Aura will read, in bytes.
@@ -6004,11 +6066,15 @@ const skipMcpProbes = (servers) => Promise.resolve(servers);
6004
6066
  */
6005
6067
  async function buildWorkspaceModel(options) {
6006
6068
  const reader = createCachingReader(options.reader ?? createFileReader());
6007
- const environment = await canonicalizeEnvironmentPaths(options.environment, reader);
6069
+ options.signal?.throwIfAborted();
6070
+ const environment = withScanCancellation(await canonicalizeEnvironmentPaths(options.environment, reader), options.signal);
6071
+ options.signal?.throwIfAborted();
6008
6072
  const projectRoot = findProjectRoot(environment.cwd, reader);
6073
+ const gitMainWorktreeRoot = projectRoot.then((root) => root === void 0 ? void 0 : findGitMainWorktreeRoot(root, reader));
6009
6074
  const context = {
6010
6075
  documents: createDocumentResolver(reader),
6011
6076
  environment,
6077
+ gitMainWorktreeRoot,
6012
6078
  projectBoundary: resolveProjectBoundary(projectRoot, environment.cwd, reader),
6013
6079
  projectRoot,
6014
6080
  probeMcpServers: options.mcpProbes === void 0 ? skipMcpProbes : createMcpProber({
@@ -6020,7 +6086,7 @@ async function buildWorkspaceModel(options) {
6020
6086
  };
6021
6087
  const sharedPath = sharedInstructionsPath(environment);
6022
6088
  const manifestPath = resolveAuraManifestPath(environment.homeDir);
6023
- const scansPending = Promise.all(options.adapters.map((adapter) => scanAdapter(adapter, context)));
6089
+ const scansPending = Promise.all(options.adapters.map((adapter) => reportingScan(adapter, context, options.onAdapterScan, options.signal)));
6024
6090
  const repositoryPending = projectRoot.then((found) => found === void 0 ? void 0 : scanRepository(found, environment, reader));
6025
6091
  const sharedContentsPending = reader.read(sharedPath);
6026
6092
  const manifestContentsPending = reader.read(manifestPath);
@@ -6030,6 +6096,7 @@ async function buildWorkspaceModel(options) {
6030
6096
  const sharedSkillsPending = scanSharedSkills(environment, reader);
6031
6097
  const scans = await scansPending;
6032
6098
  const root = await projectRoot;
6099
+ const mainWorktreeRoot = await gitMainWorktreeRoot;
6033
6100
  const repository = await repositoryPending;
6034
6101
  const sharedContents = await sharedContentsPending;
6035
6102
  const manifestContents = await manifestContentsPending;
@@ -6037,6 +6104,7 @@ async function buildWorkspaceModel(options) {
6037
6104
  const resolvedCatalog = await resolvedCatalogPending;
6038
6105
  const resolvedSkills = await resolvedSkillsPending;
6039
6106
  const sharedSkills = await sharedSkillsPending;
6107
+ options.signal?.throwIfAborted();
6040
6108
  const apps = [];
6041
6109
  const diagnostics = [];
6042
6110
  const skipped = [];
@@ -6058,6 +6126,7 @@ async function buildWorkspaceModel(options) {
6058
6126
  availableSnippets: resolvedSnippets.values,
6059
6127
  apps,
6060
6128
  cwd: environment.cwd,
6129
+ ...mainWorktreeRoot === void 0 ? {} : { gitMainWorktreeRoot: mainWorktreeRoot },
6061
6130
  homeDir: environment.homeDir,
6062
6131
  instructionFiles: apps.flatMap((app) => app.instructionFiles),
6063
6132
  manifest,
@@ -6074,6 +6143,24 @@ async function buildWorkspaceModel(options) {
6074
6143
  skipped
6075
6144
  };
6076
6145
  }
6146
+ /** One adapter's scan bracketed by progress reports; `finally` keeps a throwing scan honest. */
6147
+ async function reportingScan(adapter, context, report, signal) {
6148
+ reportScan(report, adapter.id, "active");
6149
+ try {
6150
+ signal?.throwIfAborted();
6151
+ const scan = await scanAdapter(adapter, context);
6152
+ signal?.throwIfAborted();
6153
+ return scan;
6154
+ } finally {
6155
+ reportScan(report, adapter.id, "complete");
6156
+ }
6157
+ }
6158
+ /** A progress sink is an observer, so its own failure cannot change the scan it watches. */
6159
+ function reportScan(report, adapterId, status) {
6160
+ try {
6161
+ report?.(adapterId, status);
6162
+ } catch {}
6163
+ }
6077
6164
  function environmentVariableStates(manifest, environment) {
6078
6165
  if (manifest.status !== "ready") return [];
6079
6166
  return [...new Set(manifest.value.mcpServers.flatMap((server) => mcpEnvironmentVariableNames(server.transport)))].sort().map((name) => Object.freeze({
@@ -8070,24 +8157,45 @@ function hashRepoPreset(content) {
8070
8157
  }
8071
8158
  /** Reads and validates the repository preset below the invoking directory. */
8072
8159
  async function readRepoPreset(environment, reader = createFileReader()) {
8073
- const path = resolveTeamPresetPath(environment.cwd);
8074
- const state = await readTeamPreset(environment.cwd, reader);
8160
+ const cwd = await reader.realPath(environment.cwd) ?? environment.cwd;
8161
+ const path = resolveTeamPresetPath(cwd);
8162
+ const state = await readTeamPreset(cwd, reader);
8075
8163
  if (state.status !== "ready" || state.preset === void 0 || state.content === void 0) return {
8076
8164
  diagnostics: state.diagnostics,
8077
8165
  path,
8078
8166
  status: state.status
8079
8167
  };
8168
+ const mainWorktreePath = await resolveMainWorktreePresetPath(cwd, path, reader);
8080
8169
  return {
8081
8170
  diagnostics: state.diagnostics,
8082
8171
  hash: hashRepoPreset(state.content),
8172
+ ...mainWorktreePath === void 0 ? {} : { mainWorktreePath },
8083
8173
  path,
8084
8174
  preset: state.preset,
8085
8175
  status: "ready"
8086
8176
  };
8087
8177
  }
8088
- /** Whether the manifest records an acceptance of exactly these repository preset contents. */
8089
- function isRepoPresetTrusted(manifest, path, hash) {
8090
- return (manifest?.trustedRepoPresets ?? []).some((entry) => entry.path === path && entry.hash === hash);
8178
+ /** Whether the manifest records an acceptance of exactly these contents for this repository. */
8179
+ function isRepoPresetTrusted(manifest, identity, hash) {
8180
+ return (manifest?.trustedRepoPresets ?? []).some((entry) => entry.hash === hash && (entry.path === identity.path || identity.mainWorktreePath !== void 0 && (entry.path === identity.mainWorktreePath || entry.mainWorktreePath === identity.mainWorktreePath)));
8181
+ }
8182
+ /**
8183
+ * Resolves the same preset path inside the repository's primary Git checkout.
8184
+ *
8185
+ * The preset keeps its position within the repository, so a run below the worktree root maps onto
8186
+ * the matching subdirectory of the primary checkout rather than onto its root. Returns undefined
8187
+ * outside a Git checkout, inside the primary checkout itself, and for any layout Git's own
8188
+ * `.git`-file convention does not describe.
8189
+ */
8190
+ async function resolveMainWorktreePresetPath(cwd, path, reader) {
8191
+ const projectRoot = await findProjectRoot(cwd, reader);
8192
+ if (projectRoot === void 0) return;
8193
+ const mainWorktreeRoot = await findGitMainWorktreeRoot(projectRoot, reader);
8194
+ if (mainWorktreeRoot === void 0) return;
8195
+ const within = relative(projectRoot, cwd);
8196
+ if (within.startsWith("..") || isAbsolute(within)) return;
8197
+ const candidate = resolveTeamPresetPath(join(mainWorktreeRoot, within));
8198
+ return candidate === path ? void 0 : candidate;
8091
8199
  }
8092
8200
  //#endregion
8093
8201
  //#region ../core/src/preset/required-mcp.ts
@@ -8439,7 +8547,7 @@ async function resolveRuntimeConfig(input) {
8439
8547
  message: `${repo.diagnostics[0]?.message ?? `Repository preset ${repo.path} cannot be read.`} Fix or remove the file to continue.`,
8440
8548
  status: "invalid"
8441
8549
  };
8442
- const repoTrusted = repo.status === "ready" && repo.hash !== void 0 && (repo.hash === input.acceptedRepoPresetHash || isRepoPresetTrusted(manifest, repo.path, repo.hash));
8550
+ const repoTrusted = repo.status === "ready" && repo.hash !== void 0 && (repo.hash === input.acceptedRepoPresetHash || isRepoPresetTrusted(manifest, repo, repo.hash));
8443
8551
  const resolved = resolveEffectiveConfig({
8444
8552
  checks: input.registry.checks,
8445
8553
  cli: input.cliLayer,
@@ -8477,6 +8585,7 @@ async function resolveRuntimeConfig(input) {
8477
8585
  presetOrigin: loaded.status === "ready" ? loaded.origin : policyPreset?.name ?? ".aura/preset.json",
8478
8586
  ...repo.status === "ready" && repo.hash !== void 0 ? { repoPreset: {
8479
8587
  hash: repo.hash,
8588
+ ...repo.mainWorktreePath === void 0 ? {} : { mainWorktreePath: repo.mainWorktreePath },
8480
8589
  path: repo.path,
8481
8590
  status: repoTrusted ? "applied" : "held"
8482
8591
  } } : {},
@@ -8797,7 +8906,7 @@ function resolveStatus(findings, diagnostics, checks, forcedExitCode) {
8797
8906
  /** Describes the remediation mode and the command that can act on it. */
8798
8907
  const FIXABILITY_DESCRIPTIONS = Object.freeze({
8799
8908
  auto: "auto — apply with check --fix",
8800
- guided: "guided — walk through choices with check --fix --interactive",
8909
+ guided: "guided — choose from the options check --fix puts to you",
8801
8910
  manual: "manual — follow the guidance below"
8802
8911
  });
8803
8912
  function renderExplanation(check, config, branding, output, configuration) {
@@ -8863,7 +8972,7 @@ function firstExplainConflict(options) {
8863
8972
  return options.only ? "--only" : void 0;
8864
8973
  }
8865
8974
  function fixOptionRejection(options) {
8866
- return missingFixRejection(options) ?? contradictoryFixRejection(options) ?? interactiveTerminalRejection(options);
8975
+ return missingFixRejection(options) ?? contradictoryFixRejection(options);
8867
8976
  }
8868
8977
  function missingFixRejection(options) {
8869
8978
  const flag = firstFixDependentFlag(options.dryRun, options.interactive, options.yes);
@@ -8871,10 +8980,7 @@ function missingFixRejection(options) {
8871
8980
  }
8872
8981
  function contradictoryFixRejection(options) {
8873
8982
  if (options.dryRun && options.yes) return "--dry-run and --yes contradict each other: one stops at the preview, the other applies without asking.";
8874
- if (options.interactive && options.yes) return "--interactive and --yes contradict each other: one asks for guided choices, the other forbids questions.";
8875
- }
8876
- function interactiveTerminalRejection(options) {
8877
- return options.interactive && (!options.stdinTerminal || !options.stdoutTerminal) ? "stdin and prompt output must both be terminals for --interactive." : void 0;
8983
+ return options.interactive && options.yes ? "--interactive and --yes contradict each other: the deprecated alias asks for guided choices, while --yes forbids questions." : void 0;
8878
8984
  }
8879
8985
  function firstFixDependentFlag(dryRun, interactive, yes) {
8880
8986
  if (dryRun) return "--dry-run";
@@ -8911,8 +9017,6 @@ function rejectInvalidFixOptions(options) {
8911
9017
  dryRun: options.dryRun,
8912
9018
  fix: options.fix,
8913
9019
  interactive: options.interactive,
8914
- stdinTerminal: isTerminal(options.stdin),
8915
- stdoutTerminal: isTerminal(options.stdout),
8916
9020
  yes: options.yes
8917
9021
  });
8918
9022
  }
@@ -9258,16 +9362,36 @@ function editFreeText(keypress, state) {
9258
9362
  return true;
9259
9363
  }
9260
9364
  /**
9261
- * Toggles the multi-select option on `row`; a no-op on any other kind of row.
9365
+ * Marks the option on `row` without answering the question: a multiselect toggles it, a select
9366
+ * moves its single mark to it.
9367
+ *
9368
+ * Space stops at marking on purpose — the row shows what would stand, and ↵ is still the only key
9369
+ * that answers the question and moves on.
9370
+ */
9371
+ function markRow(state, row, options = state.question.options) {
9372
+ const marked = state.question.kind === "multiselect" ? toggle(state, row, options) : choose(state, row, options);
9373
+ if (marked) state.answeredWithText = false;
9374
+ return marked;
9375
+ }
9376
+ /**
9377
+ * Toggles the multi-select option on `row`.
9262
9378
  *
9263
9379
  * A disabled option can be cleared but never selected. Refusing both directions would strand any
9264
9380
  * selection that was seeded before the option became unavailable, with no way to give it up.
9265
9381
  */
9266
- function toggleRow(state, row, options = state.question.options) {
9382
+ function toggle(state, row, options) {
9383
+ const option = options[row];
9384
+ if (option === void 0 || option.disabled === true && !state.selected.has(option.value)) return false;
9385
+ if (state.selected.has(option.value)) state.selected.delete(option.value);
9386
+ else state.selected.add(option.value);
9387
+ return true;
9388
+ }
9389
+ /** Moves a select's single mark to `row`; a disabled option can never take it. */
9390
+ function choose(state, row, options) {
9267
9391
  const option = options[row];
9268
- if (option === void 0 || state.question.kind !== "multiselect") return false;
9269
- if (option.disabled === true && !state.selected.has(option.value)) return false;
9270
- toggle(state.selected, option.value);
9392
+ if (option === void 0 || option.disabled === true) return false;
9393
+ state.selected.clear();
9394
+ state.selected.add(option.value);
9271
9395
  return true;
9272
9396
  }
9273
9397
  /** Answers the question with the row the cursor is on: free text, or a select's chosen option. */
@@ -9333,10 +9457,6 @@ function visibleOptions(state) {
9333
9457
  const visibleValues = new Set(initial.map((option) => option.value));
9334
9458
  return [...initial, ...state.question.options.filter((option) => state.selected.has(option.value) && !visibleValues.has(option.value))];
9335
9459
  }
9336
- function toggle(selected, value) {
9337
- if (selected.has(value)) selected.delete(value);
9338
- else selected.add(value);
9339
- }
9340
9460
  //#endregion
9341
9461
  //#region src/setup/wizard-form.ts
9342
9462
  function createFormSession(questions, flow) {
@@ -9446,11 +9566,11 @@ function createFormSession(questions, flow) {
9446
9566
  preview = openPreview(option.preview, option.label);
9447
9567
  return true;
9448
9568
  }
9449
- if (keypress.name === "space") return toggleRow(state, cursorRow, options);
9569
+ if (keypress.name === "space") return markRow(state, cursorRow, options);
9450
9570
  const digit = digitRow(keypress, state.question, options.length);
9451
9571
  if (digit === void 0) return false;
9452
9572
  cursorRow = digit;
9453
- toggleRow(state, digit, options);
9573
+ markRow(state, digit, options);
9454
9574
  return true;
9455
9575
  };
9456
9576
  return {
@@ -9842,7 +9962,10 @@ function renderSubmitBody(questions, submitLocked, style) {
9842
9962
  function renderHint(question, submitLocked, searching) {
9843
9963
  if (question === void 0) return submitLocked ? " ←/→ steps · esc cancel" : " ↵ submit · ←/→ steps · esc cancel";
9844
9964
  if (searching) return " type to filter · ↑/↓ move · ↵ results · esc clear search";
9845
- return ` ↑/↓ move${question.kind === "multiselect" ? " · space toggle" : ""}${question.options.some((option) => option.preview !== void 0) ? " · p preview" : ""}${question.search === void 0 ? "" : " · / search"} · ←/→ steps · ↵ select · esc cancel`;
9965
+ const multi = question.kind === "multiselect";
9966
+ const mark = multi ? " · space toggle" : " · space select";
9967
+ const commit = multi ? "↵ select" : "↵ confirm";
9968
+ return ` ↑/↓ move${mark}${question.options.some((option) => option.preview !== void 0) ? " · p preview" : ""}${question.search === void 0 ? "" : " · / search"} · ←/→ steps · ${commit} · esc cancel`;
9846
9969
  }
9847
9970
  /**
9848
9971
  * Shows one screenful of the body, never more.
@@ -10195,17 +10318,37 @@ function keyText(key, field) {
10195
10318
  //#endregion
10196
10319
  //#region src/fix-confirmation.ts
10197
10320
  /**
10321
+ * Whether a check run would put guided questions on screen — the telemetry sense of interactive.
10322
+ *
10323
+ * `--json` promises one machine-readable document and a machine cannot answer, so that run declines
10324
+ * the questions it is otherwise able to ask.
10325
+ */
10326
+ function runAsksGuided(options) {
10327
+ return options.fix && !options.json && canPrompt(options, void 0);
10328
+ }
10329
+ /**
10330
+ * Whether this run can put a question on screen and read the answer.
10331
+ *
10332
+ * The guided step and the confirmation step ask the same thing of the same streams, so they read
10333
+ * the capability from here rather than each deciding for itself — a run that may not ask which
10334
+ * resolution to take may not ask whether to apply one either.
10335
+ */
10336
+ function canPrompt(request, wizard) {
10337
+ if (request.yes) return false;
10338
+ return wizard !== void 0 || isTerminal(request.stdin) && isTerminal(request.stdout);
10339
+ }
10340
+ /**
10198
10341
  * One confirmation idiom for every fix path.
10199
10342
  *
10200
- * A plain `check --fix` confirms through the same Apply/Cancel wizard form that setup, undo, and
10201
- * `--fix --interactive` use, instead of presenting a second `[y/N]` dialect for the same decision.
10202
- * A caller that already holds a wizard (the interactive branch, or a test's scripted seam) keeps
10203
- * it; otherwise one is built here — only when both streams are terminals, so non-TTY runs still
10204
- * report the prompt unavailable rather than hanging.
10343
+ * `check --fix` confirms through the same Apply/Cancel wizard form that setup, undo, and the guided
10344
+ * questions use, instead of presenting a second `[y/N]` dialect for the same decision. A caller
10345
+ * that already holds a wizard (the guided branch, or a test's scripted seam) keeps it; otherwise
10346
+ * one is built here — only when both streams are terminals, so non-TTY runs still report the prompt
10347
+ * unavailable rather than hanging.
10205
10348
  */
10206
10349
  async function confirmFixes(request, wizard) {
10207
10350
  if (request.yes) return "accepted";
10208
- if (wizard === void 0 && (!isTerminal(request.stdin) || !isTerminal(request.stdout))) return "unavailable";
10351
+ if (!canPrompt(request, wizard)) return "unavailable";
10209
10352
  return await (wizard ?? createInteractiveWizardIo({
10210
10353
  colorDepth: request.colorDepth,
10211
10354
  stdin: request.stdin,
@@ -10261,10 +10404,23 @@ function reportFixes(prepared, status, withDetail, message) {
10261
10404
  * Candidates survive the preparation only when their plan does something, so candidates without a
10262
10405
  * single operation are plans made entirely of steps the user has to take. Reporting those as no
10263
10406
  * available fix contradicts the choice the user just made and hides the steps below it.
10407
+ *
10408
+ * `unasked` guided findings get no message at all here: {@link guidedNotice} is the whole sentence
10409
+ * for that run, and "no executable fixes are available" would deny the findings it is about to name.
10264
10410
  */
10265
- function fixlessMessage(candidates, findings) {
10411
+ function fixlessMessage(candidates, findings, unasked) {
10266
10412
  if (candidates > 0) return "Nothing to write: what these fixes need is listed below.\n\n";
10267
- return findings === 0 ? "Nothing to fix.\n\n" : "No executable fixes are available for the findings below.\n\n";
10413
+ if (findings === 0) return "Nothing to fix.\n\n";
10414
+ return unasked > 0 ? "" : "No executable fixes are available for the findings below.\n\n";
10415
+ }
10416
+ /**
10417
+ * Why a run that cannot ask questions left the guided fixes where they were.
10418
+ *
10419
+ * Every route to this sentence is one the user chose — `--yes`, `--json`, or a shell with no
10420
+ * terminal — so it names them rather than describing the run's state back at them.
10421
+ */
10422
+ function guidedNotice(branding, unasked) {
10423
+ return `Left ${String(unasked)} guided ${pluralize(unasked, "finding", "findings")} alone: this run cannot ask for the choices they need. Run ${branding.command} check --fix in a terminal, without --yes or --json.\n\n`;
10268
10424
  }
10269
10425
  function reportUnpreparedFixes(candidates) {
10270
10426
  return candidates.flatMap((candidate) => {
@@ -10292,6 +10448,67 @@ function operationPaths(operation) {
10292
10448
  return [operation.path];
10293
10449
  }
10294
10450
  //#endregion
10451
+ //#region src/preview-render.ts
10452
+ /**
10453
+ * Shows what applying the plan would do; only the shape of each change unless `withDetail`.
10454
+ *
10455
+ * Attribution goes through {@link operationsForCandidate} — the same way the report reads it — so
10456
+ * a coalesced operation prints under every check that asked for it. Grouping them under the check
10457
+ * that proposed each change is what lets the user judge the plan: the findings themselves have not
10458
+ * been printed yet at this point in the flow.
10459
+ */
10460
+ function renderFixPreview(plan, withDetail, output) {
10461
+ output.write(`Fix preview: ${safe(plan.prepared.preview.summary)}\n`);
10462
+ for (const [candidateIndex, candidate] of plan.candidates.entries()) {
10463
+ const operations = operationsForCandidate(plan, candidateIndex);
10464
+ if (operations.every((operation) => operation.effect === "noop")) continue;
10465
+ output.write(` [${safe(candidate.checkId)}] ${safe(candidate.plan.summary)}\n`);
10466
+ renderOperationPreviews(operations, withDetail, output, " ");
10467
+ }
10468
+ if (!withDetail) output.write("\nRe-run with --detail to see the full diff of every change.\n");
10469
+ renderManualSteps(plan.manualSteps, output);
10470
+ }
10471
+ /**
10472
+ * Prints the shape of each changed operation, one line per change.
10473
+ *
10474
+ * The diffs only under `withDetail`: a diff quotes the file it rewrites, and an instruction file is
10475
+ * exactly the kind of place a user pastes an API token, so the contents sit behind the same
10476
+ * `--detail` flag that gates a plugin's own error text.
10477
+ */
10478
+ function renderOperationPreviews(operations, withDetail, output, indent = " ") {
10479
+ for (const operation of operations) {
10480
+ if (operation.effect === "noop") continue;
10481
+ output.write(`${indent}${operation.effect} ${operation.paths.map(safe).join(" -> ")}\n`);
10482
+ if (operation.conflict !== void 0) output.write(`${indent} blocked: ${safe(operation.conflict)}\n`);
10483
+ if (withDetail) output.write(`\n${safeMultiline(operation.diff)}\n`);
10484
+ }
10485
+ }
10486
+ /** Prints what the plan cannot do for the user, which is otherwise lost between preview and report. */
10487
+ function renderManualSteps(steps, output) {
10488
+ if (steps.length === 0) return;
10489
+ output.write("\nSteps to take yourself:\n");
10490
+ for (const step of steps) output.write(` - ${safe(step)}\n`);
10491
+ }
10492
+ //#endregion
10493
+ //#region src/fix-fixless.ts
10494
+ /**
10495
+ * Ends a run that wrote nothing, saying what it left rather than only what it could not do.
10496
+ *
10497
+ * A run that may not ask its guided questions has fixes it never attempted, and reporting those as
10498
+ * unavailable would contradict the report printed directly below it.
10499
+ */
10500
+ function finishWithoutFixes(request, diagnostics, fixDiagnostics, outcome) {
10501
+ request.stdout.write(outcome.message);
10502
+ renderManualSteps(outcome.manualSteps, request.stdout);
10503
+ if (outcome.unasked > 0) request.stdout.write(guidedNotice(request.branding, outcome.unasked));
10504
+ return {
10505
+ applied: false,
10506
+ diagnostics,
10507
+ fixDiagnostics,
10508
+ fixes: []
10509
+ };
10510
+ }
10511
+ //#endregion
10295
10512
  //#region src/setup/wizard-types.ts
10296
10513
  /** The answer a question proposes on its own, used by `--yes` and untouched forms. */
10297
10514
  function defaultAnswer(question) {
@@ -10333,9 +10550,13 @@ function foldDecisions(previous, answers, prefix, fallback) {
10333
10550
  }
10334
10551
  //#endregion
10335
10552
  //#region src/guided-fix.ts
10553
+ /** Keeps simultaneous path probes and retained preview buffers within a small fixed ceiling. */
10554
+ const MAX_CONCURRENT_GUIDED_PREVIEWS = 4;
10336
10555
  async function gatherGuidedFixes(request, wizard, diagnostics) {
10337
10556
  const checks = new Map(request.checks.map((check) => [check.id, check]));
10557
+ const limitPreview = createLimiter(MAX_CONCURRENT_GUIDED_PREVIEWS);
10338
10558
  const selected = [];
10559
+ const presented = /* @__PURE__ */ new Set();
10339
10560
  for (const [index, finding] of request.findings.entries()) {
10340
10561
  const check = checks.get(finding.checkId);
10341
10562
  if (check?.fixability !== "guided" || finding.fixability === "manual") continue;
@@ -10351,8 +10572,9 @@ async function gatherGuidedFixes(request, wizard, diagnostics) {
10351
10572
  });
10352
10573
  continue;
10353
10574
  }
10354
- if (choices.length === 0) continue;
10355
- const previewed = await Promise.all(choices.map(async (choice, choiceIndex) => {
10575
+ if (choices.every((choice) => presented.has(choice.plan))) continue;
10576
+ for (const choice of choices) presented.add(choice.plan);
10577
+ const previewed = await Promise.all(choices.map((choice, choiceIndex) => limitPreview(async () => {
10356
10578
  try {
10357
10579
  return { option: await guidedOption(check, finding, choice, choiceIndex, request) };
10358
10580
  } catch (error) {
@@ -10361,7 +10583,7 @@ async function gatherGuidedFixes(request, wizard, diagnostics) {
10361
10583
  error
10362
10584
  };
10363
10585
  }
10364
- }));
10586
+ })));
10365
10587
  const options = [];
10366
10588
  for (const outcome of previewed) {
10367
10589
  if ("option" in outcome) {
@@ -10399,6 +10621,11 @@ async function gatherGuidedFixes(request, wizard, diagnostics) {
10399
10621
  }
10400
10622
  return Object.freeze(selected);
10401
10623
  }
10624
+ /** How many findings a run would have to ask about before it could fix them. */
10625
+ function guidedFindingCount(checks, findings) {
10626
+ const guided = new Set(checks.filter((check) => check.fixability === "guided").map((check) => check.id));
10627
+ return findings.filter((finding) => guided.has(finding.checkId) && finding.fixability !== "manual").length;
10628
+ }
10402
10629
  function orderCandidates(candidates, findings) {
10403
10630
  const order = new Map(findings.map((finding, index) => [`${finding.checkId}\0${finding.id}`, index]));
10404
10631
  return [...candidates].sort((left, right) => (order.get(`${left.checkId}\0${left.findingId}`) ?? Number.MAX_SAFE_INTEGER) - (order.get(`${right.checkId}\0${right.findingId}`) ?? Number.MAX_SAFE_INTEGER));
@@ -10439,60 +10666,6 @@ async function guidedOption(check, finding, choice, choiceIndex, request) {
10439
10666
  };
10440
10667
  }
10441
10668
  //#endregion
10442
- //#region src/preview-render.ts
10443
- /**
10444
- * Shows what applying the plan would do; only the shape of each change unless `withDetail`.
10445
- *
10446
- * Attribution goes through {@link operationsForCandidate} — the same way the report reads it — so
10447
- * a coalesced operation prints under every check that asked for it. Grouping them under the check
10448
- * that proposed each change is what lets the user judge the plan: the findings themselves have not
10449
- * been printed yet at this point in the flow.
10450
- */
10451
- function renderFixPreview(plan, withDetail, output) {
10452
- output.write(`Fix preview: ${safe(plan.prepared.preview.summary)}\n`);
10453
- for (const [candidateIndex, candidate] of plan.candidates.entries()) {
10454
- const operations = operationsForCandidate(plan, candidateIndex);
10455
- if (operations.every((operation) => operation.effect === "noop")) continue;
10456
- output.write(` [${safe(candidate.checkId)}] ${safe(candidate.plan.summary)}\n`);
10457
- renderOperationPreviews(operations, withDetail, output, " ");
10458
- }
10459
- if (!withDetail) output.write("\nRe-run with --detail to see the full diff of every change.\n");
10460
- renderManualSteps(plan.manualSteps, output);
10461
- }
10462
- /**
10463
- * Names `--interactive` when it is the thing that would have helped.
10464
- *
10465
- * `--fix` only builds plans for `auto` checks, so a workspace whose findings are all `guided`
10466
- * otherwise reads as "nothing can be done" when in fact the next flag along does exactly what the
10467
- * user asked for.
10468
- */
10469
- function renderGuidedHint(checks, findings, branding, output) {
10470
- const guided = new Set(checks.filter((check) => check.fixability === "guided").map((check) => check.id));
10471
- if (!findings.some((finding) => guided.has(finding.checkId))) return;
10472
- output.write(`Some of these findings offer guided resolutions. Run ${branding.command} check --fix --interactive to choose one.\n\n`);
10473
- }
10474
- /**
10475
- * Prints the shape of each changed operation, one line per change.
10476
- *
10477
- * The diffs only under `withDetail`: a diff quotes the file it rewrites, and an instruction file is
10478
- * exactly the kind of place a user pastes an API token, so the contents sit behind the same
10479
- * `--detail` flag that gates a plugin's own error text.
10480
- */
10481
- function renderOperationPreviews(operations, withDetail, output, indent = " ") {
10482
- for (const operation of operations) {
10483
- if (operation.effect === "noop") continue;
10484
- output.write(`${indent}${operation.effect} ${operation.paths.map(safe).join(" -> ")}\n`);
10485
- if (operation.conflict !== void 0) output.write(`${indent} blocked: ${safe(operation.conflict)}\n`);
10486
- if (withDetail) output.write(`\n${safeMultiline(operation.diff)}\n`);
10487
- }
10488
- }
10489
- /** Prints what the plan cannot do for the user, which is otherwise lost between preview and report. */
10490
- function renderManualSteps(steps, output) {
10491
- if (steps.length === 0) return;
10492
- output.write("\nSteps to take yourself:\n");
10493
- for (const step of steps) output.write(` - ${safe(step)}\n`);
10494
- }
10495
- //#endregion
10496
10669
  //#region src/fix.ts
10497
10670
  async function runFixes(request) {
10498
10671
  const automatic = collectAutomaticFixCandidates({
@@ -10504,7 +10677,8 @@ async function runFixes(request) {
10504
10677
  let candidates = [...automatic.candidates];
10505
10678
  let wizard = request.wizard;
10506
10679
  let guidedAborted = false;
10507
- if (request.interactive) {
10680
+ const asksGuided = request.guidedChoices && canPrompt(request, wizard);
10681
+ if (asksGuided) {
10508
10682
  wizard ??= createInteractiveWizardIo({
10509
10683
  colorDepth: request.colorDepth,
10510
10684
  stdin: request.stdin,
@@ -10543,8 +10717,20 @@ async function runFixes(request) {
10543
10717
  fixDiagnostics,
10544
10718
  fixes: reportFixes(prepared, "planned", request.withDetail, "Aborted before confirmation. Nothing was changed.")
10545
10719
  };
10546
- if (prepared.prepared === void 0) return finishWithoutFixes(request, automatic.diagnostics, fixDiagnostics, prepared.manualSteps, fixlessMessage(prepared.candidates.length, request.findings.length));
10547
- if (prepared.prepared.preview.changedOperationCount === 0 && prepared.prepared.preview.conflictedOperationCount === 0) return finishWithoutFixes(request, automatic.diagnostics, fixDiagnostics, prepared.manualSteps, "The planned fixes already match the current file contents.\n\n");
10720
+ const unasked = asksGuided ? 0 : guidedFindingCount(request.checks, request.findings);
10721
+ if (prepared.prepared === void 0) {
10722
+ const message = fixlessMessage(prepared.candidates.length, request.findings.length, unasked);
10723
+ return finishWithoutFixes(request, automatic.diagnostics, fixDiagnostics, {
10724
+ manualSteps: prepared.manualSteps,
10725
+ message,
10726
+ unasked
10727
+ });
10728
+ }
10729
+ if (prepared.prepared.preview.changedOperationCount === 0 && prepared.prepared.preview.conflictedOperationCount === 0) return finishWithoutFixes(request, automatic.diagnostics, fixDiagnostics, {
10730
+ manualSteps: prepared.manualSteps,
10731
+ message: "The planned fixes already match the current file contents.\n\n",
10732
+ unasked
10733
+ });
10548
10734
  renderFixPreview(prepared, request.withDetail, request.stdout);
10549
10735
  if (prepared.prepared.preview.conflictedOperationCount > 0) {
10550
10736
  request.stderr.write(`${request.branding.displayName}: fixes are blocked by the current state of these files; nothing was changed.\n`);
@@ -10642,17 +10828,6 @@ async function runFixes(request) {
10642
10828
  };
10643
10829
  }
10644
10830
  }
10645
- function finishWithoutFixes(request, diagnostics, fixDiagnostics, manualSteps, message) {
10646
- request.stdout.write(message);
10647
- renderManualSteps(manualSteps, request.stdout);
10648
- if (!request.interactive) renderGuidedHint(request.checks, request.findings, request.branding, request.stdout);
10649
- return {
10650
- applied: false,
10651
- diagnostics,
10652
- fixDiagnostics,
10653
- fixes: []
10654
- };
10655
- }
10656
10831
  //#endregion
10657
10832
  //#region src/render.ts
10658
10833
  function renderJson(report, output) {
@@ -10982,7 +11157,7 @@ function renderRecommendation(report, branding, output, context) {
10982
11157
  if (fixable.length === 0) return;
10983
11158
  const automatic = fixable.filter((finding) => finding.fixability === "auto").length;
10984
11159
  const guided = fixable.length - automatic;
10985
- const command = `${branding.command} check --fix${guided > 0 ? " --interactive" : ""}`;
11160
+ const command = `${branding.command} check --fix`;
10986
11161
  const modes = [...automatic > 0 ? [`${String(automatic)} automatic`] : [], ...guided > 0 ? [`${String(guided)} guided`] : []];
10987
11162
  renderSection("▶", "Recommended next step", [
10988
11163
  command,
@@ -11063,9 +11238,8 @@ var CheckCommand = class extends Command {
11063
11238
  ["Explain one check without scanning", "$0 check --explain ENV-001"],
11064
11239
  ["Explain one check as JSON", "$0 check --explain ENV-001 --json"],
11065
11240
  ["See what fixing would change, without writing", "$0 check --fix --dry-run"],
11066
- ["Apply fixes after confirming them", "$0 check --fix"],
11067
- ["Apply fixes without being asked", "$0 check --fix --yes"],
11068
- ["Choose guided resolutions", "$0 check --fix --interactive"]
11241
+ ["Choose resolutions, then apply them", "$0 check --fix"],
11242
+ ["Apply automatic fixes without being asked", "$0 check --fix --yes"]
11069
11243
  ]
11070
11244
  });
11071
11245
  detail = Option.Boolean("--detail", false, { description: "Include the failing plugin's own error text. May contain file contents." });
@@ -11073,20 +11247,29 @@ var CheckCommand = class extends Command {
11073
11247
  dryRun = Option.Boolean("--dry-run", false, { description: "With --fix, show what would change and write nothing." });
11074
11248
  explain = Option.String("--explain", { description: "Explain a check without scanning." });
11075
11249
  enable = enableOption();
11076
- fix = Option.Boolean("--fix", false, { description: "Preview fixes and apply them after confirmation." });
11250
+ fix = Option.Boolean("--fix", false, { description: "Choose any guided resolutions, preview, and apply after confirmation." });
11077
11251
  home = homeOption();
11078
11252
  json = Option.Boolean("--json", false, { description: "Emit JSON instead of human output." });
11079
11253
  jsonVersion = Option.String("--json-version", { description: "Select the machine-readable contract version. Supported: 1." });
11254
+ legacyInteractive = Option.Boolean("--interactive", false, { hidden: true });
11080
11255
  only = Option.Array("--only", [], { description: "Run only a check ID, category, or application. Repeatable." });
11081
11256
  online = Option.Boolean("--online", false, { description: "Probe remote MCP URLs with bounded network requests." });
11082
11257
  noCache = noCacheOption();
11083
- interactive = Option.Boolean("--interactive", false, { description: "With --fix, walk through guided remediation choices." });
11084
11258
  pathValue = pathOption();
11085
11259
  preset = presetOption();
11086
11260
  severity = severityOption();
11087
11261
  threshold = thresholdOption();
11088
11262
  verbose = Option.Boolean("--verbose", false, { description: "Expand occurrences and locations; add passed checks and applications." });
11089
- yes = Option.Boolean("--yes", false, { description: "Apply fixes without asking. Required when stdin is not a terminal." });
11263
+ yes = Option.Boolean("--yes", false, { description: "Apply automatic fixes without asking. Required when stdin is not a terminal." });
11264
+ get interactive() {
11265
+ return runAsksGuided({
11266
+ fix: this.fix,
11267
+ json: this.json,
11268
+ stdin: this.context.stdin,
11269
+ stdout: this.context.stdout,
11270
+ yes: this.yes
11271
+ });
11272
+ }
11090
11273
  async execute() {
11091
11274
  const rejection = rejectInvalidCheckOptions({
11092
11275
  detail: this.detail,
@@ -11094,14 +11277,12 @@ var CheckCommand = class extends Command {
11094
11277
  explaining: this.explain !== void 0,
11095
11278
  fix: this.fix,
11096
11279
  home: this.home,
11097
- interactive: this.interactive,
11280
+ interactive: this.legacyInteractive,
11098
11281
  json: this.json,
11099
11282
  jsonVersion: this.jsonVersion,
11100
11283
  online: this.online,
11101
11284
  only: this.only,
11102
11285
  pathValue: this.pathValue,
11103
- stdin: this.context.stdin,
11104
- stdout: this.context.stdout,
11105
11286
  verbose: this.verbose,
11106
11287
  yes: this.yes
11107
11288
  });
@@ -11166,7 +11347,7 @@ var CheckCommand = class extends Command {
11166
11347
  dryRun: this.dryRun,
11167
11348
  environment,
11168
11349
  findings: run.findings,
11169
- interactive: this.interactive,
11350
+ guidedChoices: !this.json,
11170
11351
  model,
11171
11352
  stderr: this.context.stderr,
11172
11353
  stdin: this.context.stdin,
@@ -11319,11 +11500,8 @@ function renderCheckHelp(branding) {
11319
11500
  },
11320
11501
  {
11321
11502
  rows: [{
11322
- term: "--interactive",
11323
- text: "With --fix, walk through guided remediation choices"
11324
- }, {
11325
11503
  term: "--yes",
11326
- text: "Apply without asking; required when stdin is not a terminal"
11504
+ text: "Apply automatic fixes without asking; required when stdin is not a terminal"
11327
11505
  }],
11328
11506
  title: "Fixing behavior"
11329
11507
  },
@@ -11382,7 +11560,11 @@ function renderSetupHelp(branding, addKinds) {
11382
11560
  rows: advancedRows(),
11383
11561
  title: "Advanced"
11384
11562
  }
11385
- ], [`After setup, run '${bin} check' to verify the machine converged`, "Exit codes: 0 applied/converged/dry-run/declined · 1 aborted or warnings · 2 conflicts/errors/invalid usage · 3 operational failures"]);
11563
+ ], [
11564
+ `Consolidating moves the instruction files you select; '${bin} undo' restores them`,
11565
+ `After setup, run '${bin} check' to verify the machine converged`,
11566
+ "Exit codes: 0 applied/converged/dry-run/declined · 1 aborted or warnings · 2 conflicts/errors/invalid usage · 3 operational failures"
11567
+ ]);
11386
11568
  }
11387
11569
  function renderUndoHelp(branding) {
11388
11570
  const bin = branding.command;
@@ -11656,11 +11838,15 @@ function repoPresetNotices(context) {
11656
11838
  kind: "repo",
11657
11839
  message: `${AURA_TEAM_PRESET_PATH} was not applied. Run setup interactively to review and trust it.`
11658
11840
  }];
11659
- return repo.checkSummary.map((message, index) => ({
11841
+ return [...repo.recorded ? [{
11842
+ heading: "Repository preset trust recorded before this plan:",
11843
+ kind: "repo",
11844
+ message: `Trust of ${AURA_TEAM_PRESET_PATH} is already saved; declining or aborting this plan keeps it.`
11845
+ }] : [], ...repo.checkSummary.map((message, index) => ({
11660
11846
  ...index === 0 ? { heading: `Effective check policy from the repository preset ${AURA_TEAM_PRESET_PATH} (not copied into your manifest):` } : {},
11661
11847
  kind: "repo",
11662
11848
  message
11663
- }));
11849
+ }))];
11664
11850
  }
11665
11851
  /** The check values one layer supplied, in the wording the policy summaries share. */
11666
11852
  function presetCheckSummary(config, layer) {
@@ -11690,10 +11876,11 @@ async function establishRepoPresetTrust(options) {
11690
11876
  const repo = await readRepoPreset(options.environment);
11691
11877
  if (repo.status !== "ready" || repo.hash === void 0) return { kind: "resolved" };
11692
11878
  const manifest = options.manifest.status === "ready" ? options.manifest.value : void 0;
11693
- if (isRepoPresetTrusted(manifest, repo.path, repo.hash) || !options.interactive) return { kind: "resolved" };
11879
+ if (isRepoPresetTrusted(manifest, repo, repo.hash) || !options.interactive) return { kind: "resolved" };
11694
11880
  const name = repo.preset?.name;
11695
11881
  options.io.note(name === void 0 ? `This repository provides a preset at ${AURA_TEAM_PRESET_PATH}.` : `This repository provides the preset "${safe(name)}" at ${AURA_TEAM_PRESET_PATH}.`);
11696
11882
  if (repo.preset !== void 0) options.io.note(repoPresetTrustPreview(repo.preset));
11883
+ if (repo.mainWorktreePath !== void 0) options.io.note("This directory is a linked worktree, so trusting these contents also applies them in every other worktree of the same checkout.");
11697
11884
  const changed = (manifest?.trustedRepoPresets ?? []).some((entry) => entry.path === repo.path);
11698
11885
  const confirmation = await options.io.confirm(changed ? `The repository preset at ${AURA_TEAM_PRESET_PATH} changed since you trusted it. Trust the new contents?` : `Trust the repository preset at ${AURA_TEAM_PRESET_PATH}? Its settings apply to every Aura run in this repository until the file changes.`);
11699
11886
  if (confirmation === "aborted") return { kind: "aborted" };
@@ -11714,9 +11901,16 @@ function repoPresetTrustPreview(preset) {
11714
11901
  ].filter((line) => line !== void 0);
11715
11902
  return ["Review these repository-controlled settings before trusting:", ...settings.length === 0 ? ["No check, MCP, skill, or snippet settings."] : settings].join("\n");
11716
11903
  }
11904
+ /** The exact check settings the repository preset asks the user to trust. */
11717
11905
  function checksPreview(checks) {
11718
11906
  if (checks === void 0) return;
11719
- return `Checks: ${safe(JSON.stringify(checks) ?? "{}")}`;
11907
+ const settings = [
11908
+ ...(checks.disabled ?? []).map((id) => `${safe(id)}: disabled`),
11909
+ ...(checks.enabled ?? []).map((id) => `${safe(id)}: enabled`),
11910
+ ...Object.entries(checks.severity ?? {}).map(([id, severity]) => `${safe(id)}: severity ${safe(severity)}`),
11911
+ ...Object.entries(checks.thresholds ?? {}).map(([id, thresholds]) => `${safe(id)}: thresholds ${safe(JSON.stringify(thresholds))}`)
11912
+ ].sort();
11913
+ return `Checks: ${settings.length === 0 ? "(none)" : settings.join("; ")}`;
11720
11914
  }
11721
11915
  function directoryPreview(source) {
11722
11916
  const token = source.kind === "private-directory" ? `; token ${safe(source.tokenEnv)}` : "";
@@ -11728,41 +11922,186 @@ function listPreview(label, values) {
11728
11922
  function listValues(values) {
11729
11923
  return values.length === 0 ? "(none)" : values.map(safe).join(", ");
11730
11924
  }
11925
+ /**
11926
+ * Whether this run's prompt accepted exactly the contents the resolved layer applied.
11927
+ *
11928
+ * The two reads of the file — the prompt's and configuration resolution's — are independent, so a
11929
+ * write that lands between them leaves the layer held. Recording trust for a layer that did not
11930
+ * apply would claim consent for settings this run never used.
11931
+ */
11932
+ function acceptedRepoPreset(configured, acceptedHash) {
11933
+ const repo = configured.repoPreset;
11934
+ return repo !== void 0 && repo.status === "applied" && repo.hash === acceptedHash;
11935
+ }
11731
11936
  /** The repository-preset slice steps and the planner read, absent when no file exists. */
11732
- function setupRepoPresetContext(configured, acceptedHash) {
11937
+ function setupRepoPresetContext(configured, acceptedHash, recorded) {
11733
11938
  const repo = configured.repoPreset;
11734
11939
  if (repo === void 0) return;
11735
11940
  return Object.freeze({
11736
- accepted: repo.status === "applied" && repo.hash === acceptedHash,
11941
+ accepted: acceptedRepoPreset(configured, acceptedHash),
11737
11942
  checkSummary: repo.status === "applied" ? presetCheckSummary(configured.config, "repo") : Object.freeze([]),
11738
11943
  hash: repo.hash,
11944
+ ...repo.mainWorktreePath === void 0 ? {} : { mainWorktreePath: repo.mainWorktreePath },
11739
11945
  path: repo.path,
11946
+ recorded,
11740
11947
  status: repo.status
11741
11948
  });
11742
11949
  }
11743
- /** Records one accepted repository preset, replacing any earlier acceptance for the same path. */
11950
+ /** Records one accepted repository preset while retaining earlier accepted contents. */
11744
11951
  function withTrustedRepoPreset(manifest, record) {
11745
- const entries = [...(manifest.trustedRepoPresets ?? []).filter((entry) => entry.path !== record.path), record].slice(-64);
11952
+ const entries = [...(manifest.trustedRepoPresets ?? []).filter((entry) => entry.path !== record.path || entry.hash !== record.hash), record].slice(-64);
11746
11953
  return {
11747
11954
  ...manifest,
11748
11955
  trustedRepoPresets: entries
11749
11956
  };
11750
11957
  }
11751
11958
  //#endregion
11959
+ //#region src/setup/scan-loading.ts
11960
+ /** What the loading frame says while the machine scan finishes. */
11961
+ const SCAN_PROMPT = "Scanning this machine…";
11962
+ /**
11963
+ * Starts the boot scan immediately and defers the waiting to a wizard loading frame.
11964
+ *
11965
+ * The scan begins before any prompt so its slowest probes overlap the user's reading time, but a
11966
+ * loading frame can only exist once the wizard is ready to paint one. This tracker holds the gap
11967
+ * between the two: adapter progress reported before the frame opens is buffered and replayed into
11968
+ * it, progress after flows through live, and a scan that settles before the wizard needs it skips
11969
+ * the frame entirely — the same contract as a memoized skill listing.
11970
+ *
11971
+ * The returned settle function is the only consumer of the scan's outcome. A rejection while a
11972
+ * prompt is still open is held for it rather than crashing the process as unhandled; settle
11973
+ * rethrows it where boot's caller already handles scan failures.
11974
+ */
11975
+ function trackBootScan(adapters, start) {
11976
+ const statuses = /* @__PURE__ */ new Map();
11977
+ let frame;
11978
+ let settled = false;
11979
+ const pending = start((adapterId, status) => {
11980
+ statuses.set(adapterId, status);
11981
+ frame?.(adapterId, status);
11982
+ });
11983
+ pending.then(() => {
11984
+ settled = true;
11985
+ }, () => {
11986
+ settled = true;
11987
+ });
11988
+ return (io) => {
11989
+ if (settled || adapters.length === 0) return pending;
11990
+ return io.load({
11991
+ items: adapters.map((adapter) => ({
11992
+ id: adapter.id,
11993
+ label: adapter.displayName
11994
+ })),
11995
+ prompt: SCAN_PROMPT
11996
+ }, (update) => {
11997
+ for (const [adapterId, status] of statuses) update(adapterId, status);
11998
+ frame = update;
11999
+ return pending;
12000
+ });
12001
+ };
12002
+ }
12003
+ //#endregion
12004
+ //#region src/setup/trust-record.ts
12005
+ /**
12006
+ * Persists an accepted repository preset before the wizard opens.
12007
+ *
12008
+ * Consent is not a configuration choice. A user who reviews the file, trusts it, and then backs
12009
+ * out of a wizard step has still answered the security question, and asking it again on the next
12010
+ * run is how a person learns to accept without reading. So the acceptance is written the moment it
12011
+ * is established, as its own one-operation plan, rather than riding along with the plan the wizard
12012
+ * confirms — which a run that never reaches the confirmation would discard.
12013
+ */
12014
+ async function bootRepoPresetTrust(request, configured, acceptedHash, scan) {
12015
+ const repo = configured.repoPreset;
12016
+ if (repo === void 0 || !acceptedRepoPreset(configured, acceptedHash)) return {
12017
+ repoPreset: setupRepoPresetContext(configured, acceptedHash, false),
12018
+ scan
12019
+ };
12020
+ if (request.dryRun) {
12021
+ request.io.note(`Dry run: the acceptance of ${AURA_TEAM_PRESET_PATH} was not recorded, so the next run asks again.`);
12022
+ return {
12023
+ repoPreset: setupRepoPresetContext(configured, acceptedHash, false),
12024
+ scan
12025
+ };
12026
+ }
12027
+ const recorded = await recordRepoPresetTrust({
12028
+ environment: request.environment,
12029
+ hash: repo.hash,
12030
+ mainWorktreePath: repo.mainWorktreePath,
12031
+ model: scan.model,
12032
+ path: repo.path,
12033
+ stateHomeDir: request.stateHomeDir
12034
+ });
12035
+ if (recorded.manifest === void 0) {
12036
+ request.stderr.write(`${request.branding.displayName}: could not record your trust of ${AURA_TEAM_PRESET_PATH} yet (${safe(recorded.problem)}). It applies to this run and is recorded when you apply the plan.\n`);
12037
+ return {
12038
+ repoPreset: setupRepoPresetContext(configured, acceptedHash, false),
12039
+ scan
12040
+ };
12041
+ }
12042
+ return {
12043
+ repoPreset: setupRepoPresetContext(configured, acceptedHash, true),
12044
+ scan: {
12045
+ ...scan,
12046
+ model: {
12047
+ ...scan.model,
12048
+ manifest: recorded.manifest
12049
+ }
12050
+ }
12051
+ };
12052
+ }
12053
+ /** Writes one accepted repository preset to the manifest through the fix-plan kernel. */
12054
+ async function recordRepoPresetTrust(options) {
12055
+ const state = options.model.manifest;
12056
+ try {
12057
+ const desired = withTrustedRepoPreset(state.status === "ready" ? state.value : createEmptyAuraManifest(), {
12058
+ hash: options.hash,
12059
+ ...options.mainWorktreePath === void 0 ? {} : { mainWorktreePath: options.mainWorktreePath },
12060
+ path: options.path
12061
+ });
12062
+ await applyFixPlan(await prepareFixPlan({
12063
+ model: options.model,
12064
+ plan: {
12065
+ operations: [createAuraManifestWriteOperation(state, desired)],
12066
+ summary: "Record the repository preset you trusted."
12067
+ }
12068
+ }), {
12069
+ now: options.environment.now,
12070
+ stateHomeDir: options.stateHomeDir
12071
+ });
12072
+ return { manifest: readAuraManifest(state.path, await createFileReader().read(state.path)) };
12073
+ } catch (error) {
12074
+ if (error instanceof FixPlanError || error instanceof AuraManifestError) return { problem: error.message };
12075
+ throw error;
12076
+ }
12077
+ }
12078
+ //#endregion
11752
12079
  //#region src/setup/boot.ts
11753
12080
  /**
11754
12081
  * Resolves configuration, scans the machine, and projects preset requirements onto the model.
11755
12082
  *
11756
12083
  * All of it happens before the first wizard prompt so the wizard never asks a question it will
11757
12084
  * then have to take back — an unusable manifest or an unresolvable preset stops the run while
11758
- * nothing has been shown and nothing has been written. The one exception is the repository preset
11759
- * trust confirmation: its answer decides whether the repo layer joins the configuration at all, so
11760
- * it must precede resolution, and the read-only manifest check runs before it so it is never asked
11761
- * for a run that is already dead.
12085
+ * nothing has been shown and nothing has been written. Repository preset trust is the exception on
12086
+ * both counts: its answer decides whether the repo layer joins the configuration at all, so the
12087
+ * confirmation must precede resolution, and an accepted answer is written here, before the wizard
12088
+ * opens, so that backing out of a wizard step does not discard a security decision the user has
12089
+ * already made and get them asked again. The read-only manifest check runs before the prompt so it
12090
+ * is never asked for a run that is already dead.
12091
+ *
12092
+ * The full scan starts the moment the trust prompt resolves and is awaited last, behind a wizard
12093
+ * loading frame, so nothing user-visible ever waits on it silently. The prompt needs only the
12094
+ * manifest read and therefore appears instantly; the scan's slowest probes (an adapter execing a
12095
+ * companion CLI can take many seconds) then overlap configuration resolution, and whatever
12096
+ * remains is spent on an animated per-adapter frame rather than a dead terminal. Starting after
12097
+ * the prompt keeps an aborted run's guarantee intact — backing out of the trust question still
12098
+ * means no adapter ever ran — while an invalid configuration cancels the speculative scan.
11762
12099
  */
11763
12100
  async function bootSetup(request, environment) {
11764
- const manifestPath = resolveAuraManifestPath(environment.homeDir);
11765
- const manifest = readAuraManifest(manifestPath, await createFileReader().read(manifestPath));
12101
+ const reader = createFileReader();
12102
+ const homeDir = await reader.realPath(environment.homeDir) ?? environment.homeDir;
12103
+ const manifestPath = resolveAuraManifestPath(homeDir);
12104
+ const manifest = readAuraManifest(manifestPath, await reader.read(manifestPath));
11766
12105
  if (manifest.status === "read-only") return {
11767
12106
  message: manifest.problem.message,
11768
12107
  status: "invalid"
@@ -11774,6 +12113,16 @@ async function bootSetup(request, environment) {
11774
12113
  manifest
11775
12114
  });
11776
12115
  if (trust.kind === "aborted") return { status: "aborted" };
12116
+ const scanCancellation = new AbortController();
12117
+ const settleScan = trackBootScan(request.registry.adapters, (report) => buildWorkspaceModel({
12118
+ adapters: request.registry.adapters,
12119
+ environment,
12120
+ mcpCatalog: request.registry.mcpServers,
12121
+ onAdapterScan: report,
12122
+ signal: scanCancellation.signal,
12123
+ snippets: request.registry.snippets,
12124
+ skills: request.registry.skills
12125
+ }));
11777
12126
  const configured = await resolveRuntimeConfig({
11778
12127
  acceptedRepoPresetHash: trust.acceptedHash,
11779
12128
  cliLayer: request.cliLayer,
@@ -11786,27 +12135,25 @@ async function bootSetup(request, environment) {
11786
12135
  online: true,
11787
12136
  registry: request.registry
11788
12137
  });
11789
- if (configured.status === "invalid") return configured;
11790
- const scan = await buildWorkspaceModel({
11791
- adapters: request.registry.adapters,
11792
- environment,
11793
- mcpCatalog: request.registry.mcpServers,
11794
- snippets: request.registry.snippets,
11795
- skills: request.registry.skills
11796
- });
11797
- const projected = applyRequiredMcpServers(scan.model, configured.config);
12138
+ if (configured.status === "invalid") {
12139
+ scanCancellation.abort();
12140
+ return configured;
12141
+ }
12142
+ const scan = await settleScan(request.io);
12143
+ const trusted = await bootRepoPresetTrust(request, configured, trust.acceptedHash, scan);
12144
+ const projected = applyRequiredMcpServers(trusted.scan.model, configured.config);
11798
12145
  return {
11799
- ...trust.acceptedHash === void 0 ? {} : { acceptedRepoPresetHash: trust.acceptedHash },
11800
12146
  activeChecks: enabledChecks(request.registry.checks, configured.config),
11801
12147
  configured,
11802
12148
  effectiveModel: projected.model,
11803
12149
  effectiveScan: {
11804
- ...scan,
11805
- diagnostics: [...scan.diagnostics, ...projected.diagnostics],
12150
+ ...trusted.scan,
12151
+ diagnostics: [...trusted.scan.diagnostics, ...projected.diagnostics],
11806
12152
  model: projected.model
11807
12153
  },
11808
12154
  projected,
11809
- scan,
12155
+ ...trusted.repoPreset === void 0 ? {} : { repoPreset: trusted.repoPreset },
12156
+ scan: trusted.scan,
11810
12157
  status: "ready"
11811
12158
  };
11812
12159
  }
@@ -12013,6 +12360,123 @@ function withIgnoredAppSelections(manifest, previous, apps, appCatalog) {
12013
12360
  };
12014
12361
  }
12015
12362
  //#endregion
12363
+ //#region src/setup/managed-apps.ts
12364
+ /** Managed ids from this wizard pass, falling back to the persisted selection for targeted runs. */
12365
+ function managedAppIdList(context) {
12366
+ if (context.selections.apps !== void 0) return context.selections.apps.managed;
12367
+ return context.manifest.status === "ready" ? Object.entries(context.manifest.value.apps).filter(([, app]) => app.managed).map(([id]) => id) : [];
12368
+ }
12369
+ //#endregion
12370
+ //#region src/setup/instruction-links.ts
12371
+ /** Wires every managed application to the scope targets that survived planning. */
12372
+ function planLinks$1(context, scopeSelections, archived, ownership, manualSteps) {
12373
+ const managedIds = new Set(managedAppIdList(context));
12374
+ const operations = [];
12375
+ for (const app of context.model.apps) {
12376
+ if (app.synthetic === true || !managedIds.has(app.adapterId)) continue;
12377
+ operations.push(...planAppLinks(context, app, scopeSelections, archived, ownership, manualSteps));
12378
+ }
12379
+ return operations;
12380
+ }
12381
+ function planAppLinks(context, app, scopeSelections, archived, ownership, manualSteps) {
12382
+ return scopeSelections.flatMap((selection) => {
12383
+ const link = selection.scope === "global" ? app.sharedLink : app.projectSharedLink;
12384
+ if (link === void 0) return [];
12385
+ const outcome = planSharedInstructionLink(app, context.model, {
12386
+ link,
12387
+ ...archived.has(resolve(link.entryPath)) ? { sourceContent: "" } : {},
12388
+ symlinkTarget: selection.targetPath
12389
+ });
12390
+ if ("blocked" in outcome) {
12391
+ manualSteps.push(`Aura could not wire ${link.entryPath}: ${outcome.blocked}`);
12392
+ return [];
12393
+ }
12394
+ manualSteps.push(...outcome.plan.manualSteps ?? []);
12395
+ const files = ownership.get(app.adapterId) ?? [];
12396
+ files.push(link.entryPath);
12397
+ ownership.set(app.adapterId, files);
12398
+ return [...outcome.plan.operations];
12399
+ });
12400
+ }
12401
+ //#endregion
12402
+ //#region src/setup/instruction-merge.ts
12403
+ /**
12404
+ * Merges the selected sources into what the target should contain.
12405
+ *
12406
+ * Convergent by construction, because setup's contract is that the fifth run is the first run. The
12407
+ * existing target is carried through verbatim as the base rather than nested under a provenance
12408
+ * heading of its own, and a source whose heading is already in that base is left out. That keeps a
12409
+ * source restored from an archive from being appended again on a later run.
12410
+ *
12411
+ * What that skip cannot decide is whether the file is still safe to archive; {@link unmergedSources}
12412
+ * answers that from the same sections.
12413
+ */
12414
+ function composeConsolidatedInstructions(sources, selection, clusters, model, existingTarget) {
12415
+ const base = existingTarget?.content ?? "";
12416
+ const sections = mergeSections(sources, selection, clusters, model).filter((section) => section.text.trim().length > 0).filter((section) => !base.includes(`${section.heading}\n`)).map((section) => `${section.heading}\n\n${section.text}`);
12417
+ const body = [...base.length === 0 ? [] : [base], ...sections].join("\n\n---\n\n");
12418
+ return body.endsWith("\n") ? body : `${body}\n`;
12419
+ }
12420
+ /**
12421
+ * Selected sources whose current text the merge leaves out, so archiving them would lose it.
12422
+ *
12423
+ * A section is dropped when its heading is already in the target, which is what stops a restored
12424
+ * source from being appended a second time. That is safe only while the target still holds what the
12425
+ * file says today: once it has been edited since the merge, the dropped section is the only copy,
12426
+ * and the archive consolidation performs would take it off disk without a line of it ever reaching
12427
+ * the target. Losing every paragraph to a duplicate winner is not that case — the text is in the
12428
+ * target already, under the winner's heading.
12429
+ */
12430
+ function unmergedSources(sources, selection, clusters, model, existingTarget) {
12431
+ const base = existingTarget?.content ?? "";
12432
+ return mergeSections(sources, selection, clusters, model).filter((section) => section.text.trim().length > 0 && base.includes(`${section.heading}\n`) && !base.includes(`${section.heading}\n\n${section.text}`)).map((section) => section.path);
12433
+ }
12434
+ /** The blocks a merge considers, before either caller decides what to do with them. */
12435
+ function mergeSections(sources, selection, clusters, model) {
12436
+ const selected = new Set(selection.selectedSources.map((path) => resolve(path)));
12437
+ const removals = loserRanges(selection, clusters);
12438
+ return sources.filter((source) => selected.has(resolve(source.path))).sort((left, right) => left.path.localeCompare(right.path)).map((source) => ({
12439
+ heading: provenanceHeading(source.path, selection.scope, model),
12440
+ path: resolve(source.path),
12441
+ text: removeRanges(source.content, removals.get(resolve(source.path)) ?? [])
12442
+ }));
12443
+ }
12444
+ /**
12445
+ * Names where a merged block came from, without carrying the machine into the file.
12446
+ *
12447
+ * A project target is committed, so an absolute path there would publish the developer's username
12448
+ * and resolve to nothing on anyone else's checkout. The home tilde does the same job for a global
12449
+ * target, which is at least private but no more portable.
12450
+ */
12451
+ function provenanceHeading(path, scope, model) {
12452
+ const root = scope === "global" ? model.homeDir : model.projectRoot ?? model.cwd;
12453
+ const child = relative(root, path);
12454
+ return `# Instructions from ${child.length > 0 && !child.split(/[\\/]/u).includes("..") ? `${scope === "global" ? "~/" : ""}${child.replaceAll("\\", "/")}` : resolve(path)}`;
12455
+ }
12456
+ function loserRanges(selection, clusters) {
12457
+ const selected = new Set(selection.selectedSources.map((path) => resolve(path)));
12458
+ const byPath = /* @__PURE__ */ new Map();
12459
+ for (const cluster of clusters) {
12460
+ const members = cluster.members.filter((member) => selected.has(resolve(member.path)));
12461
+ if (members.length < 2) continue;
12462
+ const winner = selection.duplicateWinners[cluster.id] ?? members[0]?.id;
12463
+ for (const member of members) {
12464
+ if (member.id === winner) continue;
12465
+ const path = resolve(member.path);
12466
+ const ranges = byPath.get(path) ?? [];
12467
+ ranges.push(member);
12468
+ byPath.set(path, ranges);
12469
+ }
12470
+ }
12471
+ return byPath;
12472
+ }
12473
+ function removeRanges(content, ranges) {
12474
+ if (ranges.length === 0) return content;
12475
+ const removed = /* @__PURE__ */ new Set();
12476
+ for (const range of ranges) for (let line = range.startLine; line <= range.endLine; line += 1) removed.add(line);
12477
+ return [...splitSourceLines(content)].filter((line) => !removed.has(line.number)).map((line) => `${line.text}${line.ending}`).join("");
12478
+ }
12479
+ //#endregion
12016
12480
  //#region src/setup/instructions.ts
12017
12481
  function instructionTargets(model) {
12018
12482
  return {
@@ -12039,8 +12503,7 @@ function instructionInventory(model) {
12039
12503
  /** How much text one source carries, measured only for the sources a form actually shows. */
12040
12504
  function describeInstructionSource(source) {
12041
12505
  const lineCount = [...splitSourceLines(source.content)].length;
12042
- const size = Buffer$1.byteLength(source.content, "utf8");
12043
- return `${String(lineCount)} lines, ${String(size)} bytes`;
12506
+ return `${String(lineCount)} ${pluralize(lineCount, "line")}`;
12044
12507
  }
12045
12508
  function instructionTargetSource(model, scope, path) {
12046
12509
  const content = scope === "global" && resolve(path) === resolve(model.sharedInstructions.path) ? model.sharedInstructions.content : model.instructionFiles.find((document) => resolve(document.path) === resolve(path))?.content;
@@ -12078,66 +12541,12 @@ function duplicateClusters(findings) {
12078
12541
  }];
12079
12542
  }).sort((left, right) => left.id.localeCompare(right.id));
12080
12543
  }
12081
- /**
12082
- * Merges the selected sources into what the target should contain.
12083
- *
12084
- * Convergent by construction, because setup's contract is that the fifth run is the first run. The
12085
- * existing target is carried through verbatim as the base rather than nested under a provenance
12086
- * heading of its own, and a source whose heading is already in that base is left out: without both,
12087
- * keeping the originals in place means every re-run appends the same guidance one level deeper.
12088
- */
12089
- function composeConsolidatedInstructions(sources, selection, clusters, model, existingTarget) {
12090
- const selected = new Set(selection.selectedSources.map((path) => resolve(path)));
12091
- const removals = loserRanges(selection, clusters);
12092
- const base = existingTarget?.content ?? "";
12093
- const sections = sources.filter((source) => selected.has(resolve(source.path))).sort((left, right) => left.path.localeCompare(right.path)).map((source) => ({
12094
- heading: provenanceHeading(source.path, selection.scope, model),
12095
- text: removeRanges(source.content, removals.get(resolve(source.path)) ?? [])
12096
- })).filter((section) => section.text.trim().length > 0).filter((section) => !base.includes(`${section.heading}\n`)).map((section) => `${section.heading}\n\n${section.text}`);
12097
- const body = [...base.length === 0 ? [] : [base], ...sections].join("\n\n---\n\n");
12098
- return body.endsWith("\n") ? body : `${body}\n`;
12099
- }
12100
- /**
12101
- * Names where a merged block came from, without carrying the machine into the file.
12102
- *
12103
- * A project target is committed, so an absolute path there would publish the developer's username
12104
- * and resolve to nothing on anyone else's checkout. The home tilde does the same job for a global
12105
- * target, which is at least private but no more portable.
12106
- */
12107
- function provenanceHeading(path, scope, model) {
12108
- const root = scope === "global" ? model.homeDir : model.projectRoot ?? model.cwd;
12109
- const child = relative(root, path);
12110
- return `# Instructions from ${child.length > 0 && !child.split(/[\\/]/u).includes("..") ? `${scope === "global" ? "~/" : ""}${child.replaceAll("\\", "/")}` : resolve(path)}`;
12111
- }
12112
12544
  function archiveRelativePath(path, scope, model) {
12113
12545
  const root = scope === "global" ? model.homeDir : model.projectRoot ?? model.cwd;
12114
12546
  const child = relative(root, path);
12115
12547
  if (child.length === 0 || child.split(/[\\/]/u).some((part) => part === "..")) return;
12116
12548
  return `${scope === "global" ? "home" : "project"}/${child.replaceAll("\\", "/")}`;
12117
12549
  }
12118
- function loserRanges(selection, clusters) {
12119
- const selected = new Set(selection.selectedSources.map((path) => resolve(path)));
12120
- const byPath = /* @__PURE__ */ new Map();
12121
- for (const cluster of clusters) {
12122
- const members = cluster.members.filter((member) => selected.has(resolve(member.path)));
12123
- if (members.length < 2) continue;
12124
- const winner = selection.duplicateWinners[cluster.id] ?? members[0]?.id;
12125
- for (const member of members) {
12126
- if (member.id === winner) continue;
12127
- const path = resolve(member.path);
12128
- const ranges = byPath.get(path) ?? [];
12129
- ranges.push(member);
12130
- byPath.set(path, ranges);
12131
- }
12132
- }
12133
- return byPath;
12134
- }
12135
- function removeRanges(content, ranges) {
12136
- if (ranges.length === 0) return content;
12137
- const removed = /* @__PURE__ */ new Set();
12138
- for (const range of ranges) for (let line = range.startLine; line <= range.endLine; line += 1) removed.add(line);
12139
- return [...splitSourceLines(content)].filter((line) => !removed.has(line.number)).map((line) => `${line.text}${line.ending}`).join("");
12140
- }
12141
12550
  function parseMember(value) {
12142
12551
  if (!isRecord(value) || typeof value["path"] !== "string" || typeof value["startLine"] !== "number" || typeof value["endLine"] !== "number") return [];
12143
12552
  const path = resolve(value["path"]);
@@ -12154,13 +12563,6 @@ function isRecord(value) {
12154
12563
  return typeof value === "object" && value !== null && !Array.isArray(value);
12155
12564
  }
12156
12565
  //#endregion
12157
- //#region src/setup/managed-apps.ts
12158
- /** Managed ids from this wizard pass, falling back to the persisted selection for targeted runs. */
12159
- function managedAppIdList(context) {
12160
- if (context.selections.apps !== void 0) return context.selections.apps.managed;
12161
- return context.manifest.status === "ready" ? Object.entries(context.manifest.value.apps).filter(([, app]) => app.managed).map(([id]) => id) : [];
12162
- }
12163
- //#endregion
12164
12566
  //#region src/setup/instruction-planner.ts
12165
12567
  /**
12166
12568
  * Mode for an instruction file Aura composes.
@@ -12236,11 +12638,12 @@ function planScope(context, selection, inventory, clusters, state) {
12236
12638
  return false;
12237
12639
  }
12238
12640
  planConsolidatedTarget(context, selection, existing, content, state);
12239
- planOriginals(context, selection, chosen, state);
12641
+ planOriginals(context, selection, chosen, new Set(unmergedSources(chosen, selection, clusters, context.model, existing)), state);
12240
12642
  return true;
12241
12643
  }
12242
12644
  function planConsolidatedTarget(context, selection, existing, content, state) {
12243
- if (existing !== void 0 && selection.archiveOriginals) {
12645
+ if (existing !== void 0) {
12646
+ if (existing.content === content) return;
12244
12647
  const relativePath = archiveRelativePath(existing.path, selection.scope, context.model);
12245
12648
  if (relativePath === void 0) state.blockers.push({
12246
12649
  path: existing.path,
@@ -12264,17 +12667,21 @@ function planConsolidatedTarget(context, selection, existing, content, state) {
12264
12667
  });
12265
12668
  }
12266
12669
  /**
12267
- * Decides what happens to the files the merged text came from.
12670
+ * Archives the sources the merged text came from, and leaves behind the ones it did not.
12268
12671
  *
12269
- * A source left in place produces no manual step. Keeping originals is the default answer, so a
12270
- * line per untouched file would appear on every run forever including the converged one, where
12271
- * "Steps to take yourself" would name work nobody has to do. What that choice actually leaves
12272
- * behind is guidance living in two places, and INS-003 reports exactly that against what is on disk
12273
- * once setup ends on green.
12672
+ * Consolidation is a migration, so a source that reached the target is backed up and taken off disk
12673
+ * in the same plan, before app links replace or remove what is left. A source the merge left out is
12674
+ * the one case where that would delete the only copy of what the file says: its heading is already
12675
+ * in the target, so nothing new was appended, and the text on disk is not text the target holds. It
12676
+ * stays where it is and the run names it. That step repeats until the divergence is resolved, which
12677
+ * is work someone does have to do — unlike a line per untouched file on a converged machine.
12274
12678
  */
12275
- function planOriginals(context, selection, chosen, state) {
12276
- if (!selection.archiveOriginals) return;
12679
+ function planOriginals(context, selection, chosen, unmerged, state) {
12277
12680
  for (const source of chosen) {
12681
+ if (unmerged.has(resolve(source.path))) {
12682
+ state.manualSteps.push(`${source.path} changed since Aura merged it into ${selection.targetPath}, so Aura left the file in place rather than archiving text the target does not have. Move what is new into ${selection.targetPath}, then delete ${source.path}.`);
12683
+ continue;
12684
+ }
12278
12685
  const relativePath = archiveRelativePath(source.path, source.scope, context.model);
12279
12686
  if (relativePath === void 0) state.blockers.push({
12280
12687
  path: source.path,
@@ -12286,35 +12693,6 @@ function planOriginals(context, selection, chosen, state) {
12286
12693
  });
12287
12694
  }
12288
12695
  }
12289
- function planLinks$1(context, scopeSelections, archived, ownership, manualSteps) {
12290
- const managedIds = new Set(managedAppIdList(context));
12291
- const operations = [];
12292
- for (const app of context.model.apps) {
12293
- if (app.synthetic === true || !managedIds.has(app.adapterId)) continue;
12294
- operations.push(...planAppLinks(context, app, scopeSelections, archived, ownership, manualSteps));
12295
- }
12296
- return operations;
12297
- }
12298
- function planAppLinks(context, app, scopeSelections, archived, ownership, manualSteps) {
12299
- return scopeSelections.flatMap((selection) => {
12300
- const link = selection.scope === "global" ? app.sharedLink : app.projectSharedLink;
12301
- if (link === void 0) return [];
12302
- const outcome = planSharedInstructionLink(app, context.model, {
12303
- link,
12304
- ...archived.has(resolve(link.entryPath)) ? { sourceContent: "" } : {},
12305
- symlinkTarget: selection.targetPath
12306
- });
12307
- if ("blocked" in outcome) {
12308
- manualSteps.push(`Aura could not wire ${link.entryPath}: ${outcome.blocked}`);
12309
- return [];
12310
- }
12311
- manualSteps.push(...outcome.plan.manualSteps ?? []);
12312
- const files = ownership.get(app.adapterId) ?? [];
12313
- files.push(link.entryPath);
12314
- ownership.set(app.adapterId, files);
12315
- return [...outcome.plan.operations];
12316
- });
12317
- }
12318
12696
  function primaryPath(operation) {
12319
12697
  return operation.type === "move" ? operation.sourcePath : operation.path;
12320
12698
  }
@@ -12913,6 +13291,7 @@ function desiredManifest(state, apps, appCatalog, ownershipUpdates, snippets, sk
12913
13291
  const repo = context.repoPreset;
12914
13292
  return repo?.accepted === true ? withTrustedRepoPreset(withPreset, {
12915
13293
  hash: repo.hash,
13294
+ ...repo.mainWorktreePath === void 0 ? {} : { mainWorktreePath: repo.mainWorktreePath },
12916
13295
  path: repo.path
12917
13296
  }) : withPreset;
12918
13297
  }
@@ -13034,6 +13413,7 @@ async function runPass(request, steps, stepContext, scan, start, activeChecks, c
13034
13413
  const gathered = await gatherSelections(steps, stepContext, io, start);
13035
13414
  if (gathered.status === "invalid-dependency") {
13036
13415
  request.stderr.write(`${branding.displayName}: the ${safe(gathered.stepTitle)} step needs ${safe(gathered.missing)}. Run ${branding.command} setup to establish it, then retry this command.\n`);
13416
+ if (stepContext.repoPreset?.recorded === true) stdout.write(leftUnchanged(stepContext));
13037
13417
  return {
13038
13418
  code: 2,
13039
13419
  kind: "exit",
@@ -13041,7 +13421,7 @@ async function runPass(request, steps, stepContext, scan, start, activeChecks, c
13041
13421
  };
13042
13422
  }
13043
13423
  if (gathered.status === "aborted") {
13044
- stdout.write("\nLeft everything as it was.\n");
13424
+ stdout.write(leftUnchanged(stepContext));
13045
13425
  return {
13046
13426
  code: 1,
13047
13427
  kind: "exit",
@@ -13053,18 +13433,24 @@ async function runPass(request, steps, stepContext, scan, start, activeChecks, c
13053
13433
  ...stepContext,
13054
13434
  selections
13055
13435
  });
13056
- if (planned.kind === "converged") return {
13057
- code: endOnGreen(request, scan, activeChecks, config),
13058
- kind: "exit",
13059
- manifest: planned.manifest,
13060
- outcome: "converged"
13061
- };
13062
- if (planned.kind === "blocked") return {
13063
- code: 2,
13064
- kind: "exit",
13065
- manifest: planned.manifest,
13066
- outcome: "blocked"
13067
- };
13436
+ if (planned.kind === "converged") {
13437
+ if (stepContext.repoPreset?.recorded === true) stdout.write(leftUnchanged(stepContext));
13438
+ return {
13439
+ code: endOnGreen(request, scan, activeChecks, config),
13440
+ kind: "exit",
13441
+ manifest: planned.manifest,
13442
+ outcome: "converged"
13443
+ };
13444
+ }
13445
+ if (planned.kind === "blocked") {
13446
+ if (stepContext.repoPreset?.recorded === true) stdout.write(leftUnchanged(stepContext));
13447
+ return {
13448
+ code: 2,
13449
+ kind: "exit",
13450
+ manifest: planned.manifest,
13451
+ outcome: "blocked"
13452
+ };
13453
+ }
13068
13454
  if (request.dryRun) {
13069
13455
  stdout.write("\nDry run: nothing was written.\n");
13070
13456
  return {
@@ -13088,7 +13474,7 @@ async function runPass(request, steps, stepContext, scan, start, activeChecks, c
13088
13474
  }
13089
13475
  };
13090
13476
  if (confirmation !== "accepted") {
13091
- stdout.write("\nLeft everything as it was.\n");
13477
+ stdout.write(leftUnchanged(stepContext));
13092
13478
  return confirmation === "aborted" ? {
13093
13479
  code: 1,
13094
13480
  kind: "exit",
@@ -13107,6 +13493,15 @@ async function runPass(request, steps, stepContext, scan, start, activeChecks, c
13107
13493
  prepared: planned.prepared
13108
13494
  };
13109
13495
  }
13496
+ /**
13497
+ * The closing line for a pass that applied nothing.
13498
+ *
13499
+ * A run that recorded repository preset trust during boot did write one file, and "left everything
13500
+ * as it was" is the one sentence a user checks against their own filesystem.
13501
+ */
13502
+ function leftUnchanged(stepContext) {
13503
+ return stepContext.repoPreset?.recorded === true ? `\nRecorded your trust of ${AURA_TEAM_PRESET_PATH}. Left everything else as it was.\n` : "\nLeft everything as it was.\n";
13504
+ }
13110
13505
  /** Plans the gathered selections, renders the summary, and classifies what can happen next. */
13111
13506
  async function previewPlan(request, inputs) {
13112
13507
  const { stdout } = request;
@@ -13125,7 +13520,8 @@ async function previewPlan(request, inputs) {
13125
13520
  stdout.write("\n");
13126
13521
  renderSetupSummary(prepared.preview, outcome.blockers, outcome.notices, request.withDetail, stdout);
13127
13522
  if (prepared.preview.conflictedOperationCount > 0 || outcome.blockers.length > 0) {
13128
- request.stderr.write(`${request.branding.displayName}: the plan is blocked by the current state of these files; nothing was changed.\n`);
13523
+ const recorded = inputs.repoPreset?.recorded === true;
13524
+ request.stderr.write(`${request.branding.displayName}: the plan is blocked by the current state of these files; ${recorded ? "the repository preset trust record was the only change" : "nothing was changed"}.\n`);
13129
13525
  return {
13130
13526
  kind: "blocked",
13131
13527
  manifest: outcome.manifest
@@ -13929,7 +14325,6 @@ function scopeStages(input) {
13929
14325
  const fallback = options[0]?.value ?? "template";
13930
14326
  const actionId = `${input.scope}-instruction-action`;
13931
14327
  const sourcesId = `${input.scope}-instruction-sources`;
13932
- const archiveId = `${input.scope}-archive-originals`;
13933
14328
  const draft = (state) => state[input.scope];
13934
14329
  const update = (state, patch) => ({
13935
14330
  ...state,
@@ -13981,27 +14376,6 @@ function scopeStages(input) {
13981
14376
  const questions = duplicateQuestions(input.scope, relevant, input.sources, draft(state).duplicateWinners ?? {});
13982
14377
  return questions.length === 0 ? void 0 : questions;
13983
14378
  }
13984
- },
13985
- {
13986
- isApplicable: consolidating,
13987
- label: "Archive",
13988
- apply: (state, answers) => update(state, { archiveOriginals: selectedValues(answers[archiveId]).includes("archive") }),
13989
- questions: (state) => consolidating(state) ? [{
13990
- id: archiveId,
13991
- initial: [draft(state).archiveOriginals === true ? "archive" : "keep"],
13992
- kind: "select",
13993
- label: "Archive",
13994
- options: [{
13995
- description: "Leave every original source in place after creating the shared file.",
13996
- label: "Keep originals in place",
13997
- value: "keep"
13998
- }, {
13999
- description: "Preserve exact originals in Aura's undo journal, then replace or remove them.",
14000
- label: "Archive originals",
14001
- value: "archive"
14002
- }],
14003
- prompt: "What should Aura do with the selected originals after consolidation?"
14004
- }] : void 0
14005
14379
  }
14006
14380
  ];
14007
14381
  }
@@ -14034,12 +14408,12 @@ function actionOptions(input) {
14034
14408
  value: "keep"
14035
14409
  });
14036
14410
  if (input.sources.length > 0) options.push({
14037
- description: "Merge selected sources with provenance and optional archival.",
14038
- label: "Consolidate found instructions",
14411
+ description: `Move instructions from the files you select into this ${basename(input.targetPath)} file. Aura backs up the originals for undo.`,
14412
+ label: "Combine found instructions",
14039
14413
  value: CONSOLIDATE_VALUE
14040
14414
  });
14041
14415
  options.push({
14042
- description: "Start with Aura's minimal official instruction template.",
14416
+ description: `Create this ${basename(input.targetPath)} file with Aura's basic instructions.`,
14043
14417
  label: "Use starter template",
14044
14418
  value: TEMPLATE_VALUE
14045
14419
  });
@@ -14055,10 +14429,10 @@ function actionOptions(input) {
14055
14429
  /**
14056
14430
  * The instructions step: one back-navigable chain of forms across both scopes.
14057
14431
  *
14058
- * Each scope contributes action → sources → duplicate review → archive stages; a stage whose
14059
- * precondition no longer holds (a non-consolidate action, no duplicated paragraphs left) simply
14060
- * disappears from the chain. The chain runner owns ← navigation between the forms and re-seeds
14061
- * re-asked questions with their previous answers.
14432
+ * Each scope contributes action → sources → duplicate review stages; a stage whose precondition no
14433
+ * longer holds (a non-consolidate action, no duplicated paragraphs left) simply disappears from
14434
+ * the chain. The chain runner owns ← navigation between the forms and re-seeds re-asked questions
14435
+ * with their previous answers.
14062
14436
  */
14063
14437
  const instructionsStep = {
14064
14438
  gather: async (context, io) => {
@@ -14118,11 +14492,9 @@ function scopeSelection(input, draft) {
14118
14492
  if (draft.action !== "consolidate") return inactiveSelection(input, inactiveAction(draft.action, input.scope));
14119
14493
  const selectedSources = draft.selectedSources ?? [];
14120
14494
  const relevant = relevantDuplicateClusters(selectedSources, input.clusters);
14121
- const duplicateWinners = Object.fromEntries(Object.entries(draft.duplicateWinners ?? {}).filter(([id]) => relevant.some((cluster) => cluster.id === id)));
14122
14495
  return {
14123
14496
  action: "consolidate",
14124
- archiveOriginals: draft.archiveOriginals === true,
14125
- duplicateWinners,
14497
+ duplicateWinners: Object.fromEntries(Object.entries(draft.duplicateWinners ?? {}).filter(([id]) => relevant.some((cluster) => cluster.id === id))),
14126
14498
  scope: input.scope,
14127
14499
  selectedSources,
14128
14500
  targetPath: input.targetPath
@@ -14142,7 +14514,6 @@ function inactiveAction(action, scope) {
14142
14514
  function inactiveSelection(input, action) {
14143
14515
  return {
14144
14516
  action,
14145
- archiveOriginals: false,
14146
14517
  duplicateWinners: {},
14147
14518
  scope: input.scope,
14148
14519
  selectedSources: [],
@@ -14161,7 +14532,6 @@ function toDraft(selection) {
14161
14532
  if (selection === void 0 || selection.action === "blocked") return {};
14162
14533
  return {
14163
14534
  action: selection.action,
14164
- archiveOriginals: selection.archiveOriginals,
14165
14535
  duplicateWinners: selection.duplicateWinners,
14166
14536
  selectedSources: selection.action === "consolidate" ? selection.selectedSources : void 0
14167
14537
  };
@@ -15391,9 +15761,11 @@ function selectSetupSteps(addKind) {
15391
15761
  * The `setup` flow: scan, gather selections, plan, confirm once, apply, end on green.
15392
15762
  *
15393
15763
  * Steps never write and the plan applies through the fix-plan kernel after one confirmation, so
15394
- * backing out anywhere before that leaves the filesystem untouched by construction. A machine that
15395
- * already matches the desired state produces an empty operation plan and skips both confirmation
15396
- * and the journal the fifth run is the first run.
15764
+ * backing out anywhere before that leaves the filesystem as the run found it — with one exception:
15765
+ * an accepted repository preset trust is recorded during boot, because consent the user has already
15766
+ * given should not be discarded by a change of mind about the wizard. The closing line names it
15767
+ * when that happened. A machine that already matches the desired state produces an empty operation
15768
+ * plan and skips both confirmation and the journal — the fifth run is the first run.
15397
15769
  */
15398
15770
  async function runSetup(request) {
15399
15771
  const { branding, environment, io, stdout } = request;
@@ -15433,7 +15805,7 @@ async function runSetup(request) {
15433
15805
  registry: request.registry
15434
15806
  });
15435
15807
  const preset = setupPresetContext(request, configured.config, configured.preset);
15436
- const repoPreset = setupRepoPresetContext(configured, booted.acceptedRepoPresetHash);
15808
+ const repoPreset = booted.repoPreset;
15437
15809
  const stepContext = {
15438
15810
  appCatalog: buildAppCatalog(request.registry.adapters, model, scan.skipped),
15439
15811
  ...initialFindings === void 0 ? {} : { findings: initialFindings },