@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.
- package/dist/bin/aura.js +3 -3
- package/dist/index.js +1 -1
- package/dist/plugins/index.js +1 -1
- package/dist/{plugins-SZegBVXF.js → plugins-Dbasvr1u.js} +37 -31
- package/dist/{run.boundary-BtMqQAEQ.js → run.boundary-CF5kZTH3.js} +687 -315
- package/dist/{shared-link-plan-DVgOyqP9.js → shared-link-plan-D8fxdFYv.js} +21 -5
- package/package.json +3 -3
|
@@ -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-
|
|
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
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
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) =>
|
|
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
|
|
8074
|
-
const
|
|
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
|
|
8089
|
-
function isRepoPresetTrusted(manifest,
|
|
8090
|
-
return (manifest?.trustedRepoPresets ?? []).some((entry) => entry.path === path && entry.
|
|
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
|
|
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 —
|
|
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)
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
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 ||
|
|
9269
|
-
|
|
9270
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
10201
|
-
*
|
|
10202
|
-
*
|
|
10203
|
-
*
|
|
10204
|
-
*
|
|
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 (
|
|
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
|
-
|
|
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.
|
|
10355
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
10547
|
-
if (prepared.prepared
|
|
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
|
|
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
|
-
["
|
|
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: "
|
|
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.
|
|
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
|
-
|
|
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
|
-
], [
|
|
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.
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
|
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.
|
|
11759
|
-
*
|
|
11760
|
-
*
|
|
11761
|
-
*
|
|
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
|
|
11765
|
-
const
|
|
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")
|
|
11790
|
-
|
|
11791
|
-
|
|
11792
|
-
|
|
11793
|
-
|
|
11794
|
-
|
|
11795
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
*
|
|
12670
|
+
* Archives the sources the merged text came from, and leaves behind the ones it did not.
|
|
12268
12671
|
*
|
|
12269
|
-
*
|
|
12270
|
-
*
|
|
12271
|
-
*
|
|
12272
|
-
*
|
|
12273
|
-
*
|
|
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(
|
|
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")
|
|
13057
|
-
|
|
13058
|
-
|
|
13059
|
-
|
|
13060
|
-
|
|
13061
|
-
|
|
13062
|
-
|
|
13063
|
-
|
|
13064
|
-
|
|
13065
|
-
|
|
13066
|
-
|
|
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(
|
|
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
|
-
|
|
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:
|
|
14038
|
-
label: "
|
|
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:
|
|
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
|
|
14059
|
-
*
|
|
14060
|
-
*
|
|
14061
|
-
*
|
|
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
|
-
|
|
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
|
|
15395
|
-
*
|
|
15396
|
-
*
|
|
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 =
|
|
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 },
|