@tryaura/aura-cli 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import { A as MAX_MUTABLE_FILE_BYTES, B as AuraManifestError, C as renderRedactedWriteDiff, D as renderRemoveDiff, E as renderMoveDiff, F as parseAuraManifest, I as errorCode, L as errorMessage$1, M as assertAuraManifestWritable, N as createAuraManifestWriteOperation, O as renderSymlinkDiff, P as createEmptyAuraManifest, R as isRecord$4, S as hashManagedSnippet, T as renderConflict, U as pluralize, V as SHARED_INSTRUCTIONS_TEMPLATE, _ as managedSnippetContentProblems, a as planSharedInstructionLink, b as canonicalizeManagedSnippet, d as planDesiredMcpConvergence, g as reconcileParsedManagedBlock, h as createAppMcpConvergence, j as MAX_RETAINED_PLAN_BYTES, k as FILE_MODES, l as rememberMcpSecretPlanner, m as rememberMcpConvergence, n as planSkillDeployment, s as createAppMcpSecretPlanner, v as readManagedBlock, w as renderArchiveDiff, x as hashCanonicalManagedSnippet, z as resolveAuraManifestPath } from "./skill-deployment-plan-C4GTrVSy.js";
1
+ import { A as renderRemoveDiff, B as errorMessage$1, C as canonicalizeManagedSnippet, D as renderArchiveDiff, E as renderRedactedWriteDiff, F as assertAuraManifestWritable, H as resolveAuraManifestPath, I as createAuraManifestWriteOperation, K as pluralize, L as createEmptyAuraManifest, M as FILE_MODES, N as MAX_MUTABLE_FILE_BYTES, O as renderConflict, P as MAX_RETAINED_PLAN_BYTES, R as parseAuraManifest, S as readManagedBlock, T as hashManagedSnippet, U as AuraManifestError, V as isRecord$4, W as SHARED_INSTRUCTIONS_TEMPLATE, _ as planSharedSkillTreeUpdate, a as rememberMcpSecretPlanner, d as createAppMcpConvergence, g as managedContentRevisionStatus, j as renderSymlinkDiff, k as renderMoveDiff, m as sharedSkillsRoot, p as planSkillDeployment, 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-BigeLxDX.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";
@@ -823,7 +823,7 @@ function resolveAllowedRoots(model, managedHomeRoots) {
823
823
  path: resolve(model.projectRoot ?? model.cwd)
824
824
  });
825
825
  const sharedDirectory = dirname(resolve(model.sharedInstructions.path));
826
- if (isStrictDescendant$1(homeDir, sharedDirectory)) add(roots, {
826
+ if (isStrictDescendant(homeDir, sharedDirectory)) add(roots, {
827
827
  exact: false,
828
828
  path: sharedDirectory
829
829
  });
@@ -838,7 +838,7 @@ function resolveAllowedRoots(model, managedHomeRoots) {
838
838
  */
839
839
  function matchRoot(path, roots, caseInsensitive) {
840
840
  const resolvedPath = resolve(path);
841
- const directoryRoot = roots.filter((root) => !root.exact && isStrictDescendant$1(root.path, resolvedPath)).sort((left, right) => right.path.length - left.path.length)[0];
841
+ const directoryRoot = roots.filter((root) => !root.exact && isStrictDescendant(root.path, resolvedPath)).sort((left, right) => right.path.length - left.path.length)[0];
842
842
  if (directoryRoot !== void 0) return directoryRoot;
843
843
  return roots.find((root) => root.exact && comparablePath(root.path, caseInsensitive) === comparablePath(resolvedPath, caseInsensitive));
844
844
  }
@@ -847,7 +847,7 @@ function describeRoots(roots) {
847
847
  return roots.map((root) => root.exact ? root.path : `${root.path}${sep}`).sort().join(", ");
848
848
  }
849
849
  /** Returns true when `candidate` sits strictly inside `root`. */
850
- function isStrictDescendant$1(root, candidate) {
850
+ function isStrictDescendant(root, candidate) {
851
851
  const difference = relative(root, candidate);
852
852
  return difference.length > 0 && difference !== ".." && !difference.startsWith(`..${sep}`) && !isAbsolute(difference);
853
853
  }
@@ -858,13 +858,13 @@ function deriveManagedHomeRoots(model, homeDir) {
858
858
  function rootsForSpec(spec, homeDir) {
859
859
  if (spec.scope !== "global" || !isAbsolute(spec.path)) return [];
860
860
  const path = resolve(spec.path);
861
- if (!isStrictDescendant$1(homeDir, path)) return [];
861
+ if (!isStrictDescendant(homeDir, path)) return [];
862
862
  if (spec.kind === "skills") return [{
863
863
  exact: false,
864
864
  path
865
865
  }];
866
866
  const directory = dirname(path);
867
- return isStrictDescendant$1(homeDir, directory) ? [{
867
+ return isStrictDescendant(homeDir, directory) ? [{
868
868
  exact: true,
869
869
  path
870
870
  }, {
@@ -879,7 +879,7 @@ function validateManagedHomeRoots(managedHomeRoots, homeDir) {
879
879
  return managedHomeRoots.map((candidate) => {
880
880
  if (!isAbsolute(candidate)) throw new FixPlanError("invalid-options", `managedHomeRoots must contain absolute paths: ${candidate}`, { path: candidate });
881
881
  const path = resolve(candidate);
882
- if (!isStrictDescendant$1(homeDir, path)) throw new FixPlanError("invalid-options", `managedHomeRoots must sit strictly inside ${homeDir}: ${candidate}`, { path: candidate });
882
+ if (!isStrictDescendant(homeDir, path)) throw new FixPlanError("invalid-options", `managedHomeRoots must sit strictly inside ${homeDir}: ${candidate}`, { path: candidate });
883
883
  return {
884
884
  exact: false,
885
885
  path
@@ -3198,6 +3198,35 @@ function directorySourceProblem(source) {
3198
3198
  if (source.kind === "private-directory" && !TOKEN_ENV_PATTERN.test(source.tokenEnv)) return `declares token variable "${source.tokenEnv}"; expected an environment variable name of uppercase letters, digits, and underscores — a name, never a value`;
3199
3199
  }
3200
3200
  //#endregion
3201
+ //#region ../core/src/plugin-validation-version.ts
3202
+ /**
3203
+ * A version's canonical rendering, or `undefined` when it is not semver at all.
3204
+ *
3205
+ * Parsing alone is too weak for an identity field: `semver` also accepts a leading `v` and
3206
+ * surrounding whitespace, so a version declared as "v1.0.0" or "1.0.0\n" would pass and then be
3207
+ * carried around raw. Requiring the declaration to equal its canonical form refuses those while
3208
+ * still accepting "1.2.3-rc.1+build.5" as written — build metadata has to be rebuilt by hand
3209
+ * because `valid()` drops it.
3210
+ */
3211
+ function canonicalSemver(version) {
3212
+ const parsed = parse(version);
3213
+ if (parsed === null) return;
3214
+ return parsed.build.length === 0 ? parsed.version : `${parsed.version}+${parsed.build.join(".")}`;
3215
+ }
3216
+ /**
3217
+ * Why a content revision could never be compared to a recorded one, or `undefined` when it can.
3218
+ *
3219
+ * Managed snippets and skills are held at their recorded revision until a newer one is reviewed,
3220
+ * and "newer" is a semver comparison. A version semver cannot order therefore has no upgrade path
3221
+ * at all: setup would never offer it and MGD-002 would never report it, so the content would sit
3222
+ * frozen with nothing on screen explaining why. Refusing it at registry build is what keeps that
3223
+ * dead end out of a user's manifest, enforcing the SDK's documented "semver version of the content
3224
+ * itself" rather than trusting it.
3225
+ */
3226
+ function contentVersionProblem(version) {
3227
+ return canonicalSemver(version) === version ? void 0 : `declares version "${version}"; expected a canonical semver version such as "1.0.0", because managed content is held at its recorded revision until a newer one is reviewed`;
3228
+ }
3229
+ //#endregion
3201
3230
  //#region ../core/src/skills/path-guards.ts
3202
3231
  /**
3203
3232
  * Pure guards over server-supplied names and paths, run before anything derived from a fetched
@@ -3444,20 +3473,6 @@ function formatApiVersionViolation(candidate) {
3444
3473
  function formatViolations(violations) {
3445
3474
  return [violations.length === 1 ? "Aura cannot build the plugin registry:" : `Aura cannot build the plugin registry (${violations.length} problems):`, ...violations.map((violation) => ` - ${violation}`)].join("\n");
3446
3475
  }
3447
- /**
3448
- * A version's canonical rendering, or `undefined` when it is not semver at all.
3449
- *
3450
- * Parsing alone is too weak for an identity field: `semver` also accepts a leading `v` and
3451
- * surrounding whitespace, so a version declared as "v1.0.0" or "1.0.0\n" would pass and then be
3452
- * carried around raw. Requiring the declaration to equal its canonical form refuses those while
3453
- * still accepting "1.2.3-rc.1+build.5" as written — build metadata has to be rebuilt by hand
3454
- * because `valid()` drops it.
3455
- */
3456
- function canonicalSemver(version) {
3457
- const parsed = parse(version);
3458
- if (parsed === null) return;
3459
- return parsed.build.length === 0 ? parsed.version : `${parsed.version}+${parsed.build.join(".")}`;
3460
- }
3461
3476
  function formatPlugin(plugin) {
3462
3477
  return `Plugin "${plugin.name}" (${plugin.id})`;
3463
3478
  }
@@ -3517,7 +3532,17 @@ function collectCandidate(state, candidate, bareCheckIdPlugins, collected) {
3517
3532
  collectSkillDirectories(state, plugin, collected.skillDirectories);
3518
3533
  collectSkills$1(state, plugin, collected.skills);
3519
3534
  collectNamespaced(state, "skill-source", plugin.skillSources, plugin, collected.skillSources);
3520
- collectNamespaced(state, "snippet", plugin.snippets, plugin, collected.snippets);
3535
+ collectSnippets(state, plugin, collected.snippets);
3536
+ }
3537
+ /** Snippets are managed content, so their revision has to be comparable before one is installed. */
3538
+ function collectSnippets(state, plugin, collected) {
3539
+ const claimed = [];
3540
+ collectNamespaced(state, "snippet", plugin.snippets, plugin, claimed);
3541
+ for (const snippet of claimed) {
3542
+ const problem = contentVersionProblem(snippet.version);
3543
+ if (problem === void 0) collected.push(snippet);
3544
+ else state.violations.push(`Plugin "${plugin.name}" (${plugin.id}) snippet "${snippet.id}" ${problem}.`);
3545
+ }
3521
3546
  }
3522
3547
  /**
3523
3548
  * Directory ids are global, like adapter ids: `directory:agenticskills` names a place, so two
@@ -3544,6 +3569,11 @@ function collectSkills$1(state, plugin, collected) {
3544
3569
  state.violations.push(`Plugin "${plugin.name}" (${plugin.id}) contributes skill-pack ID "${skill.id}"; expected a kebab-case local skill ID of at most 64 characters.`);
3545
3570
  continue;
3546
3571
  }
3572
+ const versionProblem = contentVersionProblem(skill.version);
3573
+ if (versionProblem !== void 0) {
3574
+ state.violations.push(`Plugin "${plugin.name}" (${plugin.id}) skill "${skill.id}" ${versionProblem}.`);
3575
+ continue;
3576
+ }
3547
3577
  if (claimId(state, "skill-pack", `${source.id}/${skill.id}`, plugin)) collected.push({
3548
3578
  skill,
3549
3579
  source
@@ -4198,7 +4228,7 @@ async function discoverAdapterFiles(adapter, options) {
4198
4228
  adapter,
4199
4229
  projectBoundary,
4200
4230
  reader,
4201
- sharedSkillsRoot: join(environment.homeDir, "agents", "skills")
4231
+ sharedSkillsRoot: sharedSkillsRoot(environment.homeDir)
4202
4232
  })));
4203
4233
  for (const read of reads) {
4204
4234
  files.set(read.file.spec.id, read.file);
@@ -5033,7 +5063,7 @@ const SKILLS_DIAGNOSTIC_ID = "core/skills";
5033
5063
  const SKILL_FILE$1 = "SKILL.md";
5034
5064
  /** Canonical shared skill root for one captured environment. */
5035
5065
  function sharedSkillsPath(environment) {
5036
- return join(environment.homeDir, "agents", "skills");
5066
+ return sharedSkillsRoot(environment.homeDir);
5037
5067
  }
5038
5068
  /** Resolves every bundled skill pack without letting one broken pack hide the others. */
5039
5069
  async function resolveBundledSkills(registrations, reader) {
@@ -6723,15 +6753,26 @@ function applyRequiredMcpServers(model, config) {
6723
6753
  };
6724
6754
  const manifest = model.manifest.value;
6725
6755
  const configured = new Set(manifest.mcpServers.map((server) => `${server.scope}\0${server.name}`));
6756
+ const configuredCatalogIds = new Set(manifest.mcpServers.flatMap((server) => server.catalogId === void 0 ? [] : [server.catalogId]));
6726
6757
  const diagnostics = [];
6727
6758
  const required = [];
6759
+ const overridden = [];
6760
+ const overrideIds = new Set(manifest.overrides?.requiredMcpServers ?? []);
6728
6761
  for (const selection of config.requiredMcpServers) {
6762
+ if (overrideIds.has(selection.value)) {
6763
+ overridden.push({
6764
+ catalogId: selection.value,
6765
+ requiredBy: selection.provenance.label
6766
+ });
6767
+ continue;
6768
+ }
6769
+ if (configuredCatalogIds.has(selection.value)) continue;
6729
6770
  const catalog = model.availableMcpServers.find((candidate) => candidate.id === selection.value);
6730
6771
  if (catalog === void 0) {
6731
6772
  diagnostics.push(note(`is not in this run's MCP catalog, so it was skipped`, selection.value, selection));
6732
6773
  continue;
6733
6774
  }
6734
- const name = catalog.id.slice(catalog.id.lastIndexOf("/") + 1);
6775
+ const name = catalog.manifest.serverName;
6735
6776
  if (configured.has(`global\0${name}`)) {
6736
6777
  diagnostics.push(note(`is already configured as "${name}", so your own settings were kept`, catalog.id, selection));
6737
6778
  continue;
@@ -6752,9 +6793,10 @@ function applyRequiredMcpServers(model, config) {
6752
6793
  }
6753
6794
  return {
6754
6795
  diagnostics: Object.freeze(diagnostics),
6755
- model: required.length === 0 ? model : Object.freeze({
6796
+ model: required.length === 0 && overridden.length === 0 ? model : Object.freeze({
6756
6797
  ...model,
6757
- requiredMcpServers: Object.freeze(required)
6798
+ ...overridden.length === 0 ? {} : { overriddenRequiredMcpServers: Object.freeze(overridden) },
6799
+ ...required.length === 0 ? {} : { requiredMcpServers: Object.freeze(required) }
6758
6800
  })
6759
6801
  };
6760
6802
  }
@@ -8583,6 +8625,18 @@ function defaultAnswer(question) {
8583
8625
  function selectedValues(answer) {
8584
8626
  return answer === void 0 || answer.kind !== "options" ? [] : answer.values;
8585
8627
  }
8628
+ /**
8629
+ * Folds one review form's answers back into the decisions carried by a chain's state.
8630
+ *
8631
+ * Review forms are keyed `<prefix><identity>` so a single form can carry one question per item.
8632
+ * Answers already given for items this form did not ask about are kept: a stage re-entered through
8633
+ * ← re-asks a subset, and dropping the rest would silently reset decisions the user already made.
8634
+ */
8635
+ function foldDecisions(previous, answers, prefix, fallback) {
8636
+ const decisions = { ...previous };
8637
+ for (const [id, answer] of Object.entries(answers)) if (id.startsWith(prefix)) decisions[id.slice(prefix.length)] = selectedValues(answer)[0] ?? fallback;
8638
+ return decisions;
8639
+ }
8586
8640
  //#endregion
8587
8641
  //#region src/guided-fix.ts
8588
8642
  async function gatherGuidedFixes(request, wizard, diagnostics) {
@@ -9808,6 +9862,51 @@ function buildAppCatalog(adapters, model, skipped) {
9808
9862
  }));
9809
9863
  }
9810
9864
  //#endregion
9865
+ //#region src/setup/planner-app-steps.ts
9866
+ /**
9867
+ * Summary lines the plan cannot express as file operations: installing an app Aura does not
9868
+ * install itself, and what unchecking a previously-managed app does — the manifest flips to
9869
+ * `managed: false` while the app's own configuration stays in place (ownership cleanup is a later
9870
+ * milestone).
9871
+ */
9872
+ function appManualSteps(context) {
9873
+ const apps = context.selections.apps;
9874
+ if (apps === void 0) return [];
9875
+ return [...installSteps(context, apps.managed), ...stopManagingSteps(context, apps.managed)];
9876
+ }
9877
+ function installSteps(context, managed) {
9878
+ const steps = [];
9879
+ for (const id of managed) {
9880
+ const entry = context.appCatalog.find((candidate) => catalogEntryId(candidate) === id);
9881
+ if (entry?.kind === "undetected") steps.push(entry.installHint === void 0 ? `Install ${entry.displayName} — its adapter provides no install instructions.` : `Install ${entry.displayName}: ${entry.installHint}`);
9882
+ }
9883
+ return steps;
9884
+ }
9885
+ function stopManagingSteps(context, managed) {
9886
+ const previous = context.manifest.status === "ready" ? context.manifest.value.apps : {};
9887
+ const managedIds = new Set(managed);
9888
+ const steps = [];
9889
+ for (const [id, app] of Object.entries(previous)) {
9890
+ const entry = context.appCatalog.find((candidate) => catalogEntryId(candidate) === id);
9891
+ if (entry !== void 0 && app.managed && !managedIds.has(id)) steps.push(`Aura stops managing ${catalogEntryName(entry)}; its existing configuration is left in place.`);
9892
+ }
9893
+ return steps;
9894
+ }
9895
+ //#endregion
9896
+ //#region src/setup/app-ignore.ts
9897
+ /** Applies this setup pass's explicit ignored-app decisions while preserving unknown adapters. */
9898
+ function withIgnoredAppSelections(manifest, previous, apps, appCatalog) {
9899
+ const available = new Set(appCatalog.map(catalogEntryId));
9900
+ const managed = new Set(apps.managed);
9901
+ const selectedIgnored = new Set(apps.ignored ?? []);
9902
+ const ignoredApps = [.../* @__PURE__ */ new Set([...(previous.ignoredApps ?? []).filter((id) => !available.has(id)), ...appCatalog.map(catalogEntryId).filter((id) => !managed.has(id) && selectedIgnored.has(id))])];
9903
+ const { ignoredApps: _previous, ...rest } = manifest;
9904
+ return ignoredApps.length === 0 ? rest : {
9905
+ ...rest,
9906
+ ignoredApps
9907
+ };
9908
+ }
9909
+ //#endregion
9811
9910
  //#region src/setup/instructions.ts
9812
9911
  function instructionTargets(model) {
9813
9912
  return {
@@ -10148,7 +10247,7 @@ function planManifestWrite(context, desired, createManifest, apps) {
10148
10247
  }
10149
10248
  /** Decides whether desired manifest bytes or their required private mode differ from disk. */
10150
10249
  function shouldWriteManifest(context, desired, createManifest, apps) {
10151
- return context.manifest.status === "ready" ? differsFromDisk(context.manifest, desired) : createManifest || answersSomethingToRecord(context.selections, apps);
10250
+ return context.manifest.status === "ready" ? differsFromDisk(context.manifest, desired) : createManifest || context.preset?.explicit === true || answersSomethingToRecord(context.selections, apps);
10152
10251
  }
10153
10252
  function differsFromDisk(state, desired) {
10154
10253
  const mode = state.mode;
@@ -10180,6 +10279,7 @@ function planSetupMcp(context, desiredManifest) {
10180
10279
  const missingDefinitions = new Set(context.mcpCatalog.missingRequiredIds);
10181
10280
  const selectedCatalogIds = new Set(manifest.mcpServers.flatMap((server) => server.catalogId === void 0 ? [] : [server.catalogId]));
10182
10281
  const overriddenRequiredIds = new Set(context.selections.mcp.overriddenRequiredIds ?? []);
10282
+ if (context.preset !== void 0) manifest = withRequiredOverrides(manifest, context.mcpCatalog.requiredIds, overriddenRequiredIds);
10183
10283
  for (const id of context.mcpCatalog.requiredIds) if (!missingDefinitions.has(id) && !selectedCatalogIds.has(id) && !overriddenRequiredIds.has(id)) blockers.push({
10184
10284
  path: presetPath,
10185
10285
  reason: unconfiguredRequirement(context, id)
@@ -10202,6 +10302,19 @@ function planSetupMcp(context, desiredManifest) {
10202
10302
  operations
10203
10303
  };
10204
10304
  }
10305
+ function withRequiredOverrides(manifest, requiredIds, overriddenIds) {
10306
+ const requiredMcpServers = [...requiredIds].filter((id) => overriddenIds.has(id));
10307
+ const { requiredMcpServers: _previousRequired, ...extension } = manifest.overrides ?? {};
10308
+ const overrides = requiredMcpServers.length === 0 && Object.keys(extension).length === 0 ? void 0 : {
10309
+ ...extension,
10310
+ ...requiredMcpServers.length === 0 ? {} : { requiredMcpServers }
10311
+ };
10312
+ const { overrides: _previousOverrides, ...rest } = manifest;
10313
+ return overrides === void 0 ? rest : {
10314
+ ...rest,
10315
+ overrides
10316
+ };
10317
+ }
10205
10318
  /**
10206
10319
  * Says which of the two ways a requirement went unmet happened, because the fix differs.
10207
10320
  *
@@ -10236,6 +10349,24 @@ function credentialSteps(context, manifest) {
10236
10349
  }));
10237
10350
  }
10238
10351
  //#endregion
10352
+ //#region src/setup/preset-policy.ts
10353
+ /**
10354
+ * The preset's own check settings, shown as read-only policy.
10355
+ *
10356
+ * The preset is named once in the group heading rather than on every line: with a preset that
10357
+ * configures a dozen checks, a repeated "(from preset acme)" is noise that hides the settings the
10358
+ * user is actually meant to read.
10359
+ */
10360
+ function presetPolicyNotices(context) {
10361
+ const preset = context.preset;
10362
+ if (preset === void 0 || preset.checkSummary.length === 0) return [];
10363
+ return preset.checkSummary.map((message, index) => ({
10364
+ ...index === 0 ? { heading: `Effective check policy from preset ${preset.name} (not copied into your manifest):` } : {},
10365
+ kind: "policy",
10366
+ message
10367
+ }));
10368
+ }
10369
+ //#endregion
10239
10370
  //#region src/setup/snippet-planner.ts
10240
10371
  /** Reconcile notes the user has to see, and the heading each one belongs under. */
10241
10372
  const NOTICE_KINDS = Object.freeze({
@@ -10260,11 +10391,17 @@ function planSnippets(context, source) {
10260
10391
  });
10261
10392
  const unavailable = selectedEntries.filter((entry) => entry.status === "unavailable");
10262
10393
  const pinned = selection.selected.filter((id) => previousById.get(id)?.pinned === true);
10394
+ const held = heldRevisions(selection.selected, catalog, previousById, new Set(selection.updates));
10395
+ const heldIds = new Set(held.map((entry) => entry.id));
10263
10396
  const pinnedIds = new Set(pinned);
10264
- const preserved = [...unavailable.map((entry) => entry.id), ...pinned];
10397
+ const preserved = [
10398
+ ...unavailable.map((entry) => entry.id),
10399
+ ...pinned,
10400
+ ...heldIds
10401
+ ];
10265
10402
  const parsed = readManagedBlock(source);
10266
10403
  const blockers = snippetBlockers(context.model.sharedInstructions.path, parsed, unavailable, pinned.filter((id) => !unavailable.some((entry) => entry.id === id)));
10267
- const desired = selectedEntries.flatMap((entry) => entry.status === "available" && !pinnedIds.has(entry.id) ? [{
10404
+ const desired = selectedEntries.flatMap((entry) => entry.status === "available" && !pinnedIds.has(entry.id) && !heldIds.has(entry.id) ? [{
10268
10405
  content: entry.content,
10269
10406
  id: entry.id
10270
10407
  }] : []);
@@ -10274,14 +10411,14 @@ function planSnippets(context, source) {
10274
10411
  previousSnippetHashes: new Map(previous.map((entry) => [entry.id, entry.hash]))
10275
10412
  });
10276
10413
  collectReconcileBlockers(context.model.sharedInstructions.path, parsed, reconciled, blockers);
10277
- const manifestSnippets = desiredManifestSnippets(selection.selected, catalog, previousById);
10278
- const notices = reconciled.notes.flatMap((note) => {
10414
+ const manifestSnippets = desiredManifestSnippets(selection.selected, catalog, previousById, heldIds);
10415
+ const notices = [...reconciled.notes.flatMap((note) => {
10279
10416
  const kind = NOTICE_KINDS[note.code];
10280
10417
  return kind === void 0 ? [] : [{
10281
10418
  kind,
10282
10419
  message: note.message
10283
10420
  }];
10284
- });
10421
+ }), ...held.map(heldNotice)];
10285
10422
  return {
10286
10423
  blockers,
10287
10424
  content: reconciled.content,
@@ -10290,6 +10427,39 @@ function planSnippets(context, source) {
10290
10427
  updated: reconciled.status === "updated"
10291
10428
  };
10292
10429
  }
10430
+ /**
10431
+ * The selections whose source moved but whose recorded revision this run keeps.
10432
+ *
10433
+ * Uses the same comparison the review form asks with, so "the step never offered it" and "the plan
10434
+ * never applied it" cannot drift apart: any revision the user was not asked about is held, and any
10435
+ * revision held is one the user was asked about and declined.
10436
+ */
10437
+ function heldRevisions(selectedIds, catalog, previousById, accepted) {
10438
+ return selectedIds.flatMap((id) => {
10439
+ const installed = previousById.get(id);
10440
+ const available = catalog.get(id);
10441
+ if (installed === void 0 || installed.pinned === true || available?.status !== "available" || accepted.has(id)) return [];
10442
+ const status = managedContentRevisionStatus(installed.version, installed.hash, available.version, hashManagedSnippet(available.content));
10443
+ return status === "current" ? [] : [{
10444
+ availableVersion: available.version,
10445
+ id,
10446
+ installedVersion: installed.version,
10447
+ status
10448
+ }];
10449
+ });
10450
+ }
10451
+ /**
10452
+ * Says out loud that a snippet was left at its recorded revision.
10453
+ *
10454
+ * Holding content back is the safe default, but a run that holds silently is indistinguishable
10455
+ * from one that had nothing to do — which is exactly the reading a `--yes` run would get wrong.
10456
+ */
10457
+ function heldNotice(held) {
10458
+ return {
10459
+ kind: "held",
10460
+ message: held.status === "update" ? `${held.id} stays at ${held.installedVersion}; ${held.availableVersion} is available. Run setup interactively to review it.` : `${held.id} stays at ${held.installedVersion}; the installed plugin offers ${held.availableVersion}, which is not newer.`
10461
+ };
10462
+ }
10293
10463
  function snippetBlockers(path, parsed, unavailable, pinned) {
10294
10464
  const blockers = [];
10295
10465
  if (parsed.status === "invalid") blockers.push({
@@ -10317,10 +10487,11 @@ function collectReconcileBlockers(path, parsed, reconciled, blockers) {
10317
10487
  reason: reconciled.problems.map((problem) => problem.message).join(" ")
10318
10488
  });
10319
10489
  }
10320
- function desiredManifestSnippets(selectedIds, catalog, previousById) {
10490
+ function desiredManifestSnippets(selectedIds, catalog, previousById, heldUpdates) {
10321
10491
  return selectedIds.flatMap((id) => {
10322
10492
  const preserved = previousById.get(id);
10323
10493
  if (preserved?.pinned === true) return [preserved];
10494
+ if (heldUpdates.has(id) && preserved !== void 0) return [preserved];
10324
10495
  const entry = catalog.get(id);
10325
10496
  if (entry?.status === "available") return [{
10326
10497
  hash: hashManagedSnippet(entry.content),
@@ -10334,7 +10505,7 @@ function desiredManifestSnippets(selectedIds, catalog, previousById) {
10334
10505
  //#endregion
10335
10506
  //#region src/setup/skill-planner-links.ts
10336
10507
  function sharedRoot(context) {
10337
- return join(context.model.homeDir, "agents", "skills");
10508
+ return sharedSkillsRoot(context.model.homeDir);
10338
10509
  }
10339
10510
  function planLinks(id, context, state) {
10340
10511
  for (const app of managedApps(context)) for (const directory of app.skillDirectories ?? []) {
@@ -10377,10 +10548,6 @@ function removeSkillEntries(entries) {
10377
10548
  function skillIdentity(source, id) {
10378
10549
  return `${source}\0${id}`;
10379
10550
  }
10380
- function isStrictDescendant(root, candidate) {
10381
- const difference = relative(resolve(root), resolve(candidate));
10382
- return difference.length > 0 && difference !== ".." && !difference.startsWith(`..${sep}`) && !difference.startsWith(sep);
10383
- }
10384
10551
  //#endregion
10385
10552
  //#region src/setup/skill-planner-policy.ts
10386
10553
  /**
@@ -10400,56 +10567,28 @@ function disallowedSkillBlockers(selected, policy, sharedRoot) {
10400
10567
  }));
10401
10568
  }
10402
10569
  //#endregion
10403
- //#region src/setup/skill-planner.ts
10404
- function planSkills(context) {
10405
- const previous = context.manifest.status === "ready" ? context.manifest.value.skills : [];
10406
- const selected = context.selections.skills?.selected ?? previous.map(toSelection);
10407
- const duplicate = duplicateLocalId(selected);
10408
- if (duplicate !== void 0) return abandoned(previous, {
10409
- path: sharedRoot(context),
10410
- reason: `Skill "${duplicate}" was selected from more than one source. Choose one source for each installed skill ID.`
10411
- });
10412
- const disallowed = disallowedSkillBlockers(selected, context.skillCatalog.policy, sharedRoot(context));
10413
- if (disallowed.length > 0) return {
10414
- blockers: disallowed,
10415
- manifestSkills: previous,
10416
- manualSteps: [],
10417
- operations: []
10418
- };
10419
- const state = {
10420
- blockers: [],
10421
- manifestSkills: [],
10422
- manualSteps: [],
10423
- operations: []
10424
- };
10425
- const catalog = new Map([...context.model.availableSkills ?? [], ...context.selections.skills?.resolved ?? []].map((skill) => [skillIdentity(skill.source.id, skill.id), skill]));
10426
- const previousByIdentity = new Map(previous.map((skill) => [skillIdentity(skill.source, skill.id), skill]));
10427
- const previousById = new Map(previous.map((skill) => [skill.id, skill]));
10428
- const sharedById = new Map((context.model.sharedSkills ?? []).map((skill) => [skill.id, skill]));
10429
- for (const selection of selected) reconcileSelection(selection, previousByIdentity.get(skillIdentity(selection.source, selection.id)), previousById.get(selection.id), catalog.get(skillIdentity(selection.source, selection.id)), sharedById.get(selection.id), context, state);
10430
- const desiredIds = new Set(selected.map((selection) => selection.id));
10431
- for (const skill of previous) if (!desiredIds.has(skill.id)) reconcileRemoval(skill, sharedById.get(skill.id), context, state);
10432
- return {
10433
- blockers: Object.freeze(state.blockers),
10434
- manifestSkills: Object.freeze(state.manifestSkills),
10435
- manualSteps: Object.freeze(state.manualSteps),
10436
- operations: Object.freeze(state.operations)
10437
- };
10438
- }
10439
- function abandoned(previous, blocker) {
10570
+ //#region src/setup/skill-planner-reconcile.ts
10571
+ /** The manifest row one resolved pack becomes. */
10572
+ function manifestSkill(skill) {
10440
10573
  return {
10441
- blockers: [blocker],
10442
- manifestSkills: previous,
10443
- manualSteps: [],
10444
- operations: []
10574
+ id: skill.id,
10575
+ pinned: false,
10576
+ source: skill.source.id,
10577
+ treeHash: skill.treeHash,
10578
+ version: skill.version
10445
10579
  };
10446
10580
  }
10447
- function reconcileSelection(selection, previousIdentity, previousAtId, available, shared, context, state) {
10581
+ function reconcileSelection(selection, previousIdentity, previousAtId, offered, shared, context, state) {
10448
10582
  const previous = previousIdentity ?? previousAtId;
10449
10583
  if (previous?.pinned === true && previousIdentity !== void 0) {
10450
- preserveOrRestore(previous, available, shared, context, state);
10584
+ preserveOrRestore(previous, offered.available, shared, context, state);
10451
10585
  return;
10452
10586
  }
10587
+ if (offered.held && previousIdentity !== void 0) {
10588
+ keepRecordedRevision(previousIdentity, offered.available, shared, context, state);
10589
+ return;
10590
+ }
10591
+ const available = offered.available;
10453
10592
  if (available === void 0) {
10454
10593
  if (previousIdentity !== void 0) preserveOrRestore(previousIdentity, void 0, shared, context, state);
10455
10594
  else state.blockers.push({
@@ -10476,6 +10615,23 @@ function reconcileSelection(selection, previousIdentity, previousAtId, available
10476
10615
  if (shared?.treeHash !== desired.treeHash) planTreeReplacement(shared, available, context, state.operations);
10477
10616
  planLinks(selection.id, context, state);
10478
10617
  }
10618
+ /**
10619
+ * Keeps a skill at the revision the manifest records after its update went unreviewed.
10620
+ *
10621
+ * A missing shared tree is the one case that cannot simply be left alone: the recorded revision is
10622
+ * gone from disk and the only pack on hand is the newer one the user declined, so the step says
10623
+ * exactly that instead of reporting the skill as unavailable or as pinned.
10624
+ */
10625
+ function keepRecordedRevision(previous, available, shared, context, state) {
10626
+ state.manifestSkills.push(previous);
10627
+ state.notices.push({
10628
+ kind: "held",
10629
+ message: `${previous.id} stays at ${previous.version}; ${available?.version ?? "a new revision"} is available. Run setup interactively to review it.`
10630
+ });
10631
+ if (shared === void 0) state.manualSteps.push(`Skill "${previous.id}" stays at version ${previous.version}, but its files are missing from ${join(sharedRoot(context), previous.id)}. Re-run setup and accept version ${available?.version ?? "the available revision"}, or reinstall the recorded revision from its source.`);
10632
+ else if (shared.treeHash !== previous.treeHash) state.manualSteps.push(`Review the local edits in ${shared.path} before updating skill "${previous.id}"; Aura left its files and provenance unchanged.`);
10633
+ planLinks(previous.id, context, state);
10634
+ }
10479
10635
  function preserveOrRestore(previous, available, shared, context, state) {
10480
10636
  state.manifestSkills.push(previous);
10481
10637
  if (shared === void 0) {
@@ -10495,13 +10651,67 @@ function reconcileRemoval(previous, shared, context, state) {
10495
10651
  }
10496
10652
  function planTreeReplacement(shared, desired, context, operations) {
10497
10653
  const root = join(sharedRoot(context), desired.id);
10498
- const desiredPaths = new Set(desired.files.map((file) => resolve(root, ...file.path.split("/"))));
10499
- if (shared !== void 0) operations.push(...removeSkillEntries(shared.entries.filter((entry) => !desiredPaths.has(resolve(entry.path)) && (entry.kind !== "directory" || ![...desiredPaths].some((path) => isStrictDescendant(entry.path, path))))));
10500
- operations.push(...desired.files.map((file) => ({
10501
- content: file.content,
10502
- path: join(root, ...file.path.split("/")),
10503
- type: "write"
10504
- })));
10654
+ operations.push(...planSharedSkillTreeUpdate(root, shared?.entries ?? [], desired));
10655
+ }
10656
+ //#endregion
10657
+ //#region src/setup/skill-planner.ts
10658
+ function planSkills(context) {
10659
+ const previous = context.manifest.status === "ready" ? context.manifest.value.skills : [];
10660
+ const selected = context.selections.skills?.selected ?? previous.map(toSelection);
10661
+ const duplicate = duplicateLocalId(selected);
10662
+ if (duplicate !== void 0) return abandoned(previous, {
10663
+ path: sharedRoot(context),
10664
+ reason: `Skill "${duplicate}" was selected from more than one source. Choose one source for each installed skill ID.`
10665
+ });
10666
+ const disallowed = disallowedSkillBlockers(selected, context.skillCatalog.policy, sharedRoot(context));
10667
+ if (disallowed.length > 0) return {
10668
+ blockers: disallowed,
10669
+ manifestSkills: previous,
10670
+ manualSteps: [],
10671
+ notices: [],
10672
+ operations: []
10673
+ };
10674
+ const state = {
10675
+ blockers: [],
10676
+ manifestSkills: [],
10677
+ manualSteps: [],
10678
+ notices: [],
10679
+ operations: []
10680
+ };
10681
+ const catalog = new Map([...context.model.availableSkills ?? [], ...context.selections.skills?.resolved ?? []].map((skill) => [skillIdentity(skill.source.id, skill.id), skill]));
10682
+ const previousByIdentity = new Map(previous.map((skill) => [skillIdentity(skill.source, skill.id), skill]));
10683
+ const previousById = new Map(previous.map((skill) => [skill.id, skill]));
10684
+ const acceptedUpdates = new Set(context.selections.skills?.updates ?? []);
10685
+ const sharedById = new Map((context.model.sharedSkills ?? []).map((skill) => [skill.id, skill]));
10686
+ for (const selection of selected) {
10687
+ const identity = skillIdentity(selection.source, selection.id);
10688
+ const previousAtIdentity = previousByIdentity.get(identity);
10689
+ reconcileSelection(selection, previousAtIdentity, previousById.get(selection.id), offeredRevision(previousAtIdentity, catalog.get(identity), acceptedUpdates.has(identity)), sharedById.get(selection.id), context, state);
10690
+ }
10691
+ const desiredIds = new Set(selected.map((selection) => selection.id));
10692
+ for (const skill of previous) if (!desiredIds.has(skill.id)) reconcileRemoval(skill, sharedById.get(skill.id), context, state);
10693
+ return {
10694
+ blockers: Object.freeze(state.blockers),
10695
+ manifestSkills: Object.freeze(state.manifestSkills),
10696
+ manualSteps: Object.freeze(state.manualSteps),
10697
+ notices: Object.freeze(state.notices),
10698
+ operations: Object.freeze(state.operations)
10699
+ };
10700
+ }
10701
+ function offeredRevision(previous, available, accepted) {
10702
+ return {
10703
+ available,
10704
+ held: previous !== void 0 && available !== void 0 && previous.treeHash !== available.treeHash && !accepted
10705
+ };
10706
+ }
10707
+ function abandoned(previous, blocker) {
10708
+ return {
10709
+ blockers: [blocker],
10710
+ manifestSkills: previous,
10711
+ manualSteps: [],
10712
+ notices: [],
10713
+ operations: []
10714
+ };
10505
10715
  }
10506
10716
  function duplicateLocalId(selected) {
10507
10717
  const seen = /* @__PURE__ */ new Set();
@@ -10510,15 +10720,6 @@ function duplicateLocalId(selected) {
10510
10720
  seen.add(skill.id);
10511
10721
  }
10512
10722
  }
10513
- function manifestSkill(skill) {
10514
- return {
10515
- id: skill.id,
10516
- pinned: false,
10517
- source: skill.source.id,
10518
- treeHash: skill.treeHash,
10519
- version: skill.version
10520
- };
10521
- }
10522
10723
  function toSelection(skill) {
10523
10724
  return {
10524
10725
  id: skill.id,
@@ -10546,7 +10747,7 @@ function planSetup(context) {
10546
10747
  blockers.push(...snippetPlan.blockers);
10547
10748
  const skillPlan = planSkills(context);
10548
10749
  blockers.push(...skillPlan.blockers);
10549
- const mcpPlan = planSetupMcp(context, desiredManifest(context.manifest, apps, context.appCatalog, instructionPlan.ownership, context.selections.snippets === void 0 ? void 0 : snippetPlan.manifestSnippets, skillPlan.manifestSkills));
10750
+ const mcpPlan = planSetupMcp(context, desiredManifest(context.manifest, apps, context.appCatalog, instructionPlan.ownership, context.selections.snippets === void 0 ? void 0 : snippetPlan.manifestSnippets, skillPlan.manifestSkills, context));
10550
10751
  blockers.push(...mcpPlan.blockers);
10551
10752
  const manifest = mcpPlan.manifest;
10552
10753
  const baseline = context.selections.baseline;
@@ -10566,7 +10767,11 @@ function planSetup(context) {
10566
10767
  return Object.freeze({
10567
10768
  blockers: Object.freeze(blockers),
10568
10769
  manifest,
10569
- notices: snippetPlan.notices,
10770
+ notices: [
10771
+ ...snippetPlan.notices,
10772
+ ...skillPlan.notices,
10773
+ ...presetPolicyNotices(context)
10774
+ ],
10570
10775
  plan: Object.freeze({
10571
10776
  manualSteps: Object.freeze([
10572
10777
  ...appManualSteps(context),
@@ -10579,24 +10784,28 @@ function planSetup(context) {
10579
10784
  })
10580
10785
  });
10581
10786
  }
10582
- function desiredManifest(state, apps, appCatalog, ownershipUpdates, snippets, skills) {
10787
+ function desiredManifest(state, apps, appCatalog, ownershipUpdates, snippets, skills, context) {
10583
10788
  const base = state.status === "ready" ? state.value : createEmptyAuraManifest();
10584
10789
  const ownership = desiredOwnership(base, ownershipUpdates);
10585
10790
  const withSkills = {
10586
10791
  ...apps === void 0 ? {
10587
10792
  ...base,
10588
10793
  ownership
10589
- } : {
10794
+ } : withIgnoredAppSelections({
10590
10795
  ...base,
10591
10796
  apps: desiredApps(base, apps, appCatalog),
10592
10797
  ownership
10593
- },
10798
+ }, base, apps, appCatalog),
10594
10799
  skills
10595
10800
  };
10596
- return snippets === void 0 ? withSkills : {
10801
+ const withSnippets = snippets === void 0 ? withSkills : {
10597
10802
  ...withSkills,
10598
10803
  snippets
10599
10804
  };
10805
+ return context.preset?.explicit === true ? {
10806
+ ...withSnippets,
10807
+ preset: context.preset.reference
10808
+ } : withSnippets;
10600
10809
  }
10601
10810
  function planSetupSnippets(context, instructionOperations) {
10602
10811
  return planSnippets(context, plannedSharedContent(instructionOperations, context.model.sharedInstructions.path) ?? context.model.sharedInstructions.content ?? "");
@@ -10659,35 +10868,6 @@ function desiredApps(base, apps, appCatalog) {
10659
10868
  for (const id of managed) if (!next.has(id)) next.set(id, { managed: true });
10660
10869
  return Object.fromEntries(next);
10661
10870
  }
10662
- /**
10663
- * Summary lines the plan cannot express as file operations: installing an app Aura does not
10664
- * install itself, and what unchecking a previously-managed app does — the manifest flips to
10665
- * `managed: false` while the app's own configuration stays in place (ownership cleanup is a later
10666
- * milestone).
10667
- */
10668
- function appManualSteps(context) {
10669
- const apps = context.selections.apps;
10670
- if (apps === void 0) return [];
10671
- return [...installSteps(context, apps.managed), ...stopManagingSteps(context, apps.managed)];
10672
- }
10673
- function installSteps(context, managed) {
10674
- const steps = [];
10675
- for (const id of managed) {
10676
- const entry = context.appCatalog.find((candidate) => catalogEntryId(candidate) === id);
10677
- if (entry?.kind === "undetected") steps.push(entry.installHint === void 0 ? `Install ${entry.displayName} — its adapter provides no install instructions.` : `Install ${entry.displayName}: ${entry.installHint}`);
10678
- }
10679
- return steps;
10680
- }
10681
- function stopManagingSteps(context, managed) {
10682
- const previous = context.manifest.status === "ready" ? context.manifest.value.apps : {};
10683
- const managedIds = new Set(managed);
10684
- const steps = [];
10685
- for (const [id, app] of Object.entries(previous)) {
10686
- const entry = context.appCatalog.find((candidate) => catalogEntryId(candidate) === id);
10687
- if (entry !== void 0 && app.managed && !managedIds.has(id)) steps.push(`Aura stops managing ${catalogEntryName(entry)}; its existing configuration is left in place.`);
10688
- }
10689
- return steps;
10690
- }
10691
10871
  //#endregion
10692
10872
  //#region src/setup/summary.ts
10693
10873
  /** The already-converged variant of the summary: nothing to confirm, at most context to show. */
@@ -10711,14 +10891,22 @@ function renderSetupSummary(preview, blockers, notices, withDetail, output) {
10711
10891
  if (!withDetail && preview.changedOperationCount > 0) output.write("\nRe-run with --detail to see the full diff of every change.\n");
10712
10892
  }
10713
10893
  const NOTICE_HEADINGS = Object.freeze({
10894
+ held: "Managed content kept at its recorded revision:",
10714
10895
  overwritten: "Hand edits Aura is replacing:",
10896
+ policy: "Effective check policy:",
10715
10897
  preserved: "Preserved content Aura does not own:"
10716
10898
  });
10717
10899
  function renderNotices(notices, output) {
10718
- for (const kind of ["overwritten", "preserved"]) {
10900
+ for (const kind of [
10901
+ "overwritten",
10902
+ "preserved",
10903
+ "held",
10904
+ "policy"
10905
+ ]) {
10719
10906
  const matching = notices.filter((notice) => notice.kind === kind);
10720
10907
  if (matching.length === 0) continue;
10721
- output.write(`\n${NOTICE_HEADINGS[kind]}\n`);
10908
+ const heading = matching.find((notice) => notice.heading !== void 0)?.heading;
10909
+ output.write(`\n${safe(heading ?? NOTICE_HEADINGS[kind])}\n`);
10722
10910
  for (const notice of matching) output.write(` · ${safe(notice.message)}\n`);
10723
10911
  }
10724
10912
  }
@@ -11080,13 +11268,13 @@ function requiringPreset(preset, config) {
11080
11268
  * rest of the run, so a plugin cannot hand us an arbitrarily large one.
11081
11269
  */
11082
11270
  const MAX_SNIPPET_BYTES = 65536;
11083
- function createSnippetCatalog(snippets, manifest) {
11271
+ function createSnippetCatalog(snippets, manifest, presetIds = []) {
11084
11272
  let resolved = [];
11085
11273
  let pending;
11086
11274
  return {
11087
11275
  entries: () => resolved,
11088
11276
  load: () => {
11089
- pending ??= resolveSnippetCatalog(snippets, manifest).then((entries) => {
11277
+ pending ??= resolveSnippetCatalog(snippets, manifest, presetIds).then((entries) => {
11090
11278
  resolved = entries;
11091
11279
  return entries;
11092
11280
  });
@@ -11095,19 +11283,32 @@ function createSnippetCatalog(snippets, manifest) {
11095
11283
  };
11096
11284
  }
11097
11285
  /** Resolves every registry source once and retains manifest-only selections as unavailable rows. */
11098
- async function resolveSnippetCatalog(snippets, manifest) {
11286
+ async function resolveSnippetCatalog(snippets, manifest, presetIds = []) {
11099
11287
  const registered = new Set(snippets.map((snippet) => snippet.id));
11100
11288
  const resolved = await Promise.all(snippets.map(resolveSnippet));
11101
11289
  const previous = manifest.status === "ready" ? manifest.value.snippets : [];
11102
- return Object.freeze([...resolved, ...previous.filter((snippet) => !registered.has(snippet.id)).map((snippet) => Object.freeze({
11103
- category: "general",
11104
- description: "Previously selected, but no installed plugin currently provides it.",
11105
- id: snippet.id,
11106
- name: snippet.id,
11107
- reason: "The contributing plugin is unavailable.",
11108
- status: "unavailable",
11109
- version: snippet.version
11110
- }))]);
11290
+ const previousIds = new Set(previous.map((snippet) => snippet.id));
11291
+ return Object.freeze([
11292
+ ...resolved,
11293
+ ...previous.filter((snippet) => !registered.has(snippet.id)).map((snippet) => Object.freeze({
11294
+ category: "general",
11295
+ description: "Previously selected, but no installed plugin currently provides it.",
11296
+ id: snippet.id,
11297
+ name: snippet.id,
11298
+ reason: "The contributing plugin is unavailable.",
11299
+ status: "unavailable",
11300
+ version: snippet.version
11301
+ })),
11302
+ ...presetIds.filter((id) => !registered.has(id) && !previousIds.has(id)).map((id) => Object.freeze({
11303
+ category: "general",
11304
+ description: "Selected by the active team preset, but unavailable in this build.",
11305
+ id,
11306
+ name: id,
11307
+ reason: "No installed plugin provides this preset selection.",
11308
+ status: "unavailable",
11309
+ version: "unknown"
11310
+ }))
11311
+ ]);
11111
11312
  }
11112
11313
  async function resolveSnippet(snippet) {
11113
11314
  const common = {
@@ -11160,7 +11361,7 @@ function errorMessage(error) {
11160
11361
  const appsStep = {
11161
11362
  gather: async (context, io) => {
11162
11363
  const catalog = context.appCatalog;
11163
- if (catalog.length === 0) return emptyCatalogOutcome$1(context, io);
11364
+ if (catalog.length === 0) return emptyCatalogOutcome$2(context, io);
11164
11365
  const previouslyManaged = previouslyManagedIds(context);
11165
11366
  const detectedCount = detectedAppCount(catalog);
11166
11367
  if (context.revisited !== true) emitBanner(catalog, detectedCount, io);
@@ -11174,21 +11375,21 @@ const appsStep = {
11174
11375
  prompt: "Which applications should Aura manage?"
11175
11376
  }, io, detectedCount > 0 || previouslyManaged.size > 0);
11176
11377
  if (selected === SETUP_ABORTED || selected === SETUP_BACK) return selected;
11177
- return withApps(context, selected);
11378
+ return withApps(context, selected, catalog);
11178
11379
  },
11179
11380
  compactTitle: "Apps",
11180
11381
  id: "apps",
11181
11382
  title: "Applications"
11182
11383
  };
11183
11384
  /** With no adapters registered there is nothing to ask; ← keeps walking, → passes through. */
11184
- function emptyCatalogOutcome$1(context, io) {
11385
+ function emptyCatalogOutcome$2(context, io) {
11185
11386
  if (context.enteredBackward === true) return SETUP_BACK;
11186
11387
  if (context.revisited !== true) io.note("No agent application adapters are registered.");
11187
11388
  return context.selections;
11188
11389
  }
11189
11390
  /** A re-entered step shows the answer this run already gave it, not its cold-start proposal. */
11190
11391
  function initialSelection(context, catalog, previouslyManaged) {
11191
- return context.selections.apps?.managed ?? catalog.filter((entry) => entry.kind === "detected" || previouslyManaged.has(catalogEntryId(entry))).map(catalogEntryId);
11392
+ return context.selections.apps?.managed ?? catalog.filter((entry) => previouslyManaged.has(catalogEntryId(entry)) || entry.kind === "detected" && !ignoredAppIds(context).has(catalogEntryId(entry))).map(catalogEntryId);
11192
11393
  }
11193
11394
  function detectedAppCount(catalog) {
11194
11395
  return catalog.filter((entry) => entry.kind === "detected").length;
@@ -11210,12 +11411,27 @@ async function askForSelection(question, io, mustConfirmEmpty) {
11210
11411
  if (confirmation === "aborted") return SETUP_ABORTED;
11211
11412
  }
11212
11413
  }
11213
- function withApps(context, managed) {
11414
+ function withApps(context, managed, catalog) {
11415
+ const selected = new Set(managed);
11416
+ const manifestApps = context.manifest.status === "ready" ? new Set(Object.keys(context.manifest.value.apps)) : /* @__PURE__ */ new Set();
11417
+ const preserved = ignoredAppIds(context);
11418
+ const ignored = catalog.flatMap((entry) => {
11419
+ const id = catalogEntryId(entry);
11420
+ if (selected.has(id)) return [];
11421
+ if (entry.kind === "detected" && !manifestApps.has(id)) return [id];
11422
+ return preserved.has(id) ? [id] : [];
11423
+ });
11214
11424
  return {
11215
11425
  ...context.selections,
11216
- apps: { managed }
11426
+ apps: ignored.length === 0 ? { managed } : {
11427
+ ignored,
11428
+ managed
11429
+ }
11217
11430
  };
11218
11431
  }
11432
+ function ignoredAppIds(context) {
11433
+ return new Set(context.manifest.status === "ready" ? context.manifest.value.ignoredApps ?? [] : []);
11434
+ }
11219
11435
  function previouslyManagedIds(context) {
11220
11436
  if (context.manifest.status !== "ready") return /* @__PURE__ */ new Set();
11221
11437
  return new Set(Object.entries(context.manifest.value.apps).filter(([, app]) => app.managed).map(([id]) => id));
@@ -11965,7 +12181,6 @@ function appDisabledReason(entry, managed, supportedApps) {
11965
12181
  //#endregion
11966
12182
  //#region src/setup/steps/mcp.ts
11967
12183
  const ADD_CUSTOM = "mcp:add-custom";
11968
- /** Selects catalog and custom MCP servers, then captures their target name, scope, and apps. */
11969
12184
  const mcpStep = {
11970
12185
  addKind: "mcp",
11971
12186
  compactTitle: "MCP",
@@ -11992,8 +12207,8 @@ async function gatherMcp(context, io) {
11992
12207
  mcp: { servers: [] }
11993
12208
  };
11994
12209
  }
11995
- let selectedKeys = initialKeys(entries, context.interactive);
11996
- const overriddenRequiredIds = new Set(context.selections.mcp?.overriddenRequiredIds ?? []);
12210
+ const overriddenRequiredIds = new Set(context.selections.mcp?.overriddenRequiredIds ?? (context.manifest.status === "ready" ? context.manifest.value.overrides?.requiredMcpServers ?? [] : []));
12211
+ let selectedKeys = initialKeys(entries, context.interactive, overriddenRequiredIds);
11997
12212
  for (;;) {
11998
12213
  const picked = await pickServers(context, entries, selectedKeys, overriddenRequiredIds, io);
11999
12214
  if (picked === SETUP_ABORTED || picked === SETUP_BACK) return picked;
@@ -12063,8 +12278,8 @@ function matchesEntry(server, entry) {
12063
12278
  * sight is exactly the first-configuration the skills step refuses for the same reason. Left
12064
12279
  * unchecked the requirement becomes a blocker naming the interactive run, never a silent skip.
12065
12280
  */
12066
- function initialKeys(entries, interactive) {
12067
- return new Set(entries.filter((entry) => entry.selectedServer !== void 0 || entry.required && interactive).map((entry) => entry.key));
12281
+ function initialKeys(entries, interactive, overriddenRequiredIds) {
12282
+ return new Set(entries.filter((entry) => entry.selectedServer !== void 0 || entry.required && interactive && (entry.catalog === void 0 || !overriddenRequiredIds.has(entry.catalog.id))).map((entry) => entry.key));
12068
12283
  }
12069
12284
  async function pickServers(context, entries, initial, overriddenRequiredIds, io) {
12070
12285
  const question = {
@@ -12086,7 +12301,7 @@ async function pickServers(context, entries, initial, overriddenRequiredIds, io)
12086
12301
  const selected = new Set(selectedValues(result["mcp-servers"]));
12087
12302
  const missingRequired = reconcileRequiredOverrides(entries, selected, overriddenRequiredIds);
12088
12303
  if (missingRequired.length === 0 || !context.interactive) return selected;
12089
- const confirmation = await io.confirm(`Override the team preset for this run and omit ${missingRequired.map(safe).join(", ")}?`);
12304
+ const confirmation = await io.confirm(`Override the team preset on this machine and omit ${missingRequired.map(safe).join(", ")}?`);
12090
12305
  if (confirmation === "accepted") {
12091
12306
  for (const id of missingRequired) overriddenRequiredIds.add(id);
12092
12307
  return selected;
@@ -12098,14 +12313,14 @@ function reconcileRequiredOverrides(entries, selected, overriddenRequiredIds) {
12098
12313
  const requiredIds = requiredCatalogIds(entries);
12099
12314
  const missing = requiredIds.filter((id) => !entries.some((entry) => entry.catalog?.id === id && selected.has(entry.key)));
12100
12315
  for (const id of requiredIds) if (!missing.includes(id)) overriddenRequiredIds.delete(id);
12101
- return missing;
12316
+ return missing.filter((id) => !overriddenRequiredIds.has(id));
12102
12317
  }
12103
12318
  function requiredCatalogIds(entries) {
12104
12319
  return [...new Set(entries.flatMap((entry) => entry.required && entry.catalog !== void 0 ? [entry.catalog.id] : []))];
12105
12320
  }
12106
12321
  function serverOption(entry) {
12107
12322
  const configured = entry.selectedServer !== void 0 || entry.existing !== void 0;
12108
- const tags = [...entry.required ? ["preset required"] : [], ...configured ? [entry.catalog === void 0 ? "custom" : "configured"] : []];
12323
+ const tags = [...entry.required ? ["from preset", "required"] : [], ...configured ? [entry.catalog === void 0 ? "custom" : "configured"] : []];
12109
12324
  const suffix = tags.length === 0 ? "" : ` (${tags.join(", ")})`;
12110
12325
  return {
12111
12326
  description: entry.sourceName === void 0 ? "Custom server" : `Plugin: ${entry.sourceName}`,
@@ -12229,6 +12444,7 @@ function pickerOptions(inputs) {
12229
12444
  const unavailableById = new Map(inputs.listing.unavailableSources.map((source) => [source.id, source]));
12230
12445
  const policy = inputs.catalog.policy;
12231
12446
  const unsupported = !hasSkillsHome(inputs.managedApps);
12447
+ const preset = new Set(inputs.presetSkills.map((skill) => skillIdentity(skill.source, skill.id)));
12232
12448
  const entryRows = sortForDisplay(inputs.listing.entries, (entry) => [
12233
12449
  entry.sourceName,
12234
12450
  entry.name,
@@ -12240,7 +12456,7 @@ function pickerOptions(inputs) {
12240
12456
  disabledNote: "no selected app supports skills"
12241
12457
  } : {},
12242
12458
  group: entry.sourceName,
12243
- label: entry.name,
12459
+ label: `${entry.name}${preset.has(entry.identity) ? " (from preset)" : ""}`,
12244
12460
  ...entry.preview === void 0 ? {} : { preview: entry.preview },
12245
12461
  value: entry.identity
12246
12462
  }));
@@ -12272,17 +12488,92 @@ function pickerOptions(inputs) {
12272
12488
  label: source.name,
12273
12489
  value: `source:${source.id}`
12274
12490
  }));
12491
+ const represented = /* @__PURE__ */ new Set([...inputs.listing.entries.map((entry) => entry.identity), ...inputs.manifestSkills.map((skill) => skillIdentity(skill.source, skill.id))]);
12492
+ const presetRows = inputs.presetSkills.filter((skill) => !represented.has(skillIdentity(skill.source, skill.id))).map((skill) => ({
12493
+ description: "Selected by the active team preset, but unavailable in this run.",
12494
+ disabled: true,
12495
+ disabledNote: "preset selection unavailable",
12496
+ group: skill.source,
12497
+ label: `${skill.id} (from preset)`,
12498
+ value: skillIdentity(skill.source, skill.id)
12499
+ }));
12275
12500
  return [
12276
12501
  ...entryRows,
12277
12502
  ...manifestRows,
12503
+ ...presetRows,
12278
12504
  ...sourceRows
12279
12505
  ];
12280
12506
  }
12281
12507
  //#endregion
12508
+ //#region src/setup/steps/skill-review.ts
12509
+ const REVIEW_PREFIX$1 = "review:";
12510
+ /**
12511
+ * Whether the source revision differs from the recorded one at all.
12512
+ *
12513
+ * A diverged revision — a rollback, or a pair this build cannot order — is reviewable for the same
12514
+ * reason an upgrade is: the planner holds the recorded revision either way, so a revision that is
12515
+ * never offered here is one the user can never resolve, and it would sit held with nothing on
12516
+ * screen naming it.
12517
+ */
12518
+ function movedFrom(previous, available) {
12519
+ return managedContentRevisionStatus(previous.version, previous.treeHash, available.version, available.treeHash) !== "current";
12520
+ }
12521
+ /** No review for an unchanged manifest skill or a failed fetch; both preserve the prior entry. */
12522
+ function reviewQuestion$1(identity, decisions, resolution, recorded, entry, selection) {
12523
+ const pack = resolution.resolved.get(identity);
12524
+ const previous = recorded.get(identity);
12525
+ if (previous !== void 0 && (pack === void 0 || !movedFrom(previous, pack))) return [];
12526
+ const localId = entry?.id ?? selection?.id ?? identity;
12527
+ const change = revisionChange(previous, pack);
12528
+ return [{
12529
+ id: `${REVIEW_PREFIX$1}${identity}`,
12530
+ initial: [decisions[identity] ?? "skip"],
12531
+ kind: "select",
12532
+ label: `Review ${localId}`,
12533
+ options: [skipOption(change), installOption(localId, change, pack, entry, resolution.problems.get(identity))],
12534
+ prompt: reviewPrompt$1(localId, change, pack, entry)
12535
+ }];
12536
+ }
12537
+ const CHANGE_VERBS = Object.freeze({
12538
+ install: "Install",
12539
+ switch: "Switch to",
12540
+ update: "Update to"
12541
+ });
12542
+ function revisionChange(previous, pack) {
12543
+ if (previous === void 0) return "install";
12544
+ if (pack === void 0) return "update";
12545
+ return managedContentRevisionStatus(previous.version, previous.treeHash, pack.version, pack.treeHash) === "update" ? "update" : "switch";
12546
+ }
12547
+ function skipOption(change) {
12548
+ return {
12549
+ description: change === "install" ? "Leave this skill out of the plan." : "Keep the installed version and its manifest entry.",
12550
+ label: change === "install" ? "Skip — do not install" : "Skip — keep the installed version",
12551
+ value: "skip"
12552
+ };
12553
+ }
12554
+ function reviewPrompt$1(localId, change, pack, entry) {
12555
+ const sourceName = entry?.sourceName ?? "its directory";
12556
+ if (pack === void 0) return `"${localId}" could not be fetched from ${sourceName}.`;
12557
+ return `${CHANGE_VERBS[change]} "${pack.name}" ${pack.version} from ${sourceName}? Press p on the install row to read its full SKILL.md before deciding.`;
12558
+ }
12559
+ function installOption(localId, change, pack, entry, problem) {
12560
+ if (pack === void 0) return {
12561
+ description: problem ?? "This skill could not be fetched.",
12562
+ disabled: true,
12563
+ label: `Install ${localId}`,
12564
+ value: "install"
12565
+ };
12566
+ return {
12567
+ description: entry?.sourceUrl ?? entry?.sourceName ?? "",
12568
+ label: `${CHANGE_VERBS[change]} ${localId} ${pack.version}`,
12569
+ preview: pack.files.find((file) => file.path === "SKILL.md")?.content,
12570
+ value: "install"
12571
+ };
12572
+ }
12573
+ //#endregion
12282
12574
  //#region src/setup/steps/skill-stages.ts
12283
- const REVIEW_PREFIX = "review:";
12284
12575
  function skillStages(inputs) {
12285
- return [pickerStage(inputs), reviewStage(inputs)];
12576
+ return [pickerStage$1(inputs), reviewStage$1(inputs)];
12286
12577
  }
12287
12578
  /** A remote identity's install needs a fetch, and therefore the review boundary. */
12288
12579
  function isRemoteIdentity(identity) {
@@ -12302,15 +12593,19 @@ function selectionsByIdentity(inputs) {
12302
12593
  source: skill.source
12303
12594
  });
12304
12595
  }
12596
+ for (const skill of inputs.presetSkills) {
12597
+ const identity = skillIdentity(skill.source, skill.id);
12598
+ if (!selections.has(identity)) selections.set(identity, skill);
12599
+ }
12305
12600
  return selections;
12306
12601
  }
12307
12602
  function manifestByIdentity(manifestSkills) {
12308
12603
  return new Map(manifestSkills.map((skill) => [skillIdentity(skill.source, skill.id), skill]));
12309
12604
  }
12310
- function pickerStage(inputs) {
12605
+ function pickerStage$1(inputs) {
12311
12606
  const supportsSkills = hasSkillsHome(inputs.managedApps);
12312
12607
  const selectable = new Set(supportsSkills ? inputs.listing.entries.map((entry) => entry.identity) : []);
12313
- const previous = new Set(inputs.manifestSkills.map((skill) => skillIdentity(skill.source, skill.id)));
12608
+ const previous = new Set([...inputs.manifestSkills, ...inputs.presetSkills].map((skill) => skillIdentity(skill.source, skill.id)));
12314
12609
  return {
12315
12610
  apply: (state, answers) => ({
12316
12611
  ...state,
@@ -12331,75 +12626,43 @@ function pickerStage(inputs) {
12331
12626
  }
12332
12627
  };
12333
12628
  }
12334
- function reviewStage(inputs) {
12629
+ function reviewStage$1(inputs) {
12335
12630
  const recorded = manifestByIdentity(inputs.manifestSkills);
12336
12631
  const offered = selectionsByIdentity(inputs);
12337
12632
  const entries = new Map(inputs.listing.entries.map((entry) => [entry.identity, entry]));
12633
+ const bundled = bundledByIdentity(inputs.availableSkills);
12634
+ const reviewable = (identity) => needsReview(identity, recorded, bundled);
12338
12635
  return {
12339
- apply: (state, answers) => {
12340
- const decisions = { ...state.decisions };
12341
- for (const [id, answer] of Object.entries(answers)) if (id.startsWith(REVIEW_PREFIX)) decisions[id.slice(7)] = selectedValues(answer)[0] ?? "skip";
12342
- return {
12343
- ...state,
12344
- decisions
12345
- };
12346
- },
12636
+ apply: (state, answers) => ({
12637
+ ...state,
12638
+ decisions: foldDecisions(state.decisions, answers, REVIEW_PREFIX$1, "skip")
12639
+ }),
12347
12640
  compactLabel: "Review",
12348
- isApplicable: (state) => state.selected.some(isRemoteIdentity),
12641
+ isApplicable: (state) => state.selected.some(reviewable),
12349
12642
  label: "Review",
12350
12643
  questions: async (state) => {
12351
12644
  const remote = state.selected.filter(isRemoteIdentity);
12352
- if (remote.length === 0) return;
12353
12645
  const resolution = await inputs.catalog.resolve(remote.flatMap((identity) => {
12354
12646
  const selection = offered.get(identity);
12355
12647
  return selection === void 0 ? [] : [selection];
12356
12648
  }), inputs.approvedPrivateSourceIds);
12357
- const questions = remote.flatMap((identity) => reviewQuestion(identity, state, resolution, recorded, entries.get(identity), offered.get(identity)));
12649
+ const resolutionWithBundled = {
12650
+ problems: resolution.problems,
12651
+ resolved: new Map([...resolution.resolved, ...bundled])
12652
+ };
12653
+ const questions = state.selected.filter(reviewable).flatMap((identity) => reviewQuestion$1(identity, state.decisions, resolutionWithBundled, recorded, entries.get(identity), offered.get(identity)));
12358
12654
  return questions.length === 0 ? void 0 : questions;
12359
12655
  }
12360
12656
  };
12361
12657
  }
12362
- /** No review for an unchanged manifest skill or a failed fetch; both preserve the prior entry. */
12363
- function reviewQuestion(identity, state, resolution, recorded, entry, selection) {
12364
- const pack = resolution.resolved.get(identity);
12365
- const previous = recorded.get(identity);
12366
- if (previous !== void 0 && (pack === void 0 || pack.treeHash === previous.treeHash)) return [];
12367
- const localId = entry?.id ?? selection?.id ?? identity;
12368
- const update = previous !== void 0;
12369
- return [{
12370
- id: `${REVIEW_PREFIX}${identity}`,
12371
- initial: [state.decisions[identity] ?? "skip"],
12372
- kind: "select",
12373
- label: `Review ${localId}`,
12374
- options: [skipOption(update), installOption(localId, update, pack, entry, resolution.problems.get(identity))],
12375
- prompt: reviewPrompt(localId, update, pack, entry)
12376
- }];
12658
+ function bundledByIdentity(packs) {
12659
+ return new Map(packs.map((pack) => [skillIdentity(pack.source.id, pack.id), pack]));
12377
12660
  }
12378
- function skipOption(update) {
12379
- return {
12380
- description: update ? "Keep the installed version and its manifest entry." : "Leave this skill out of the plan.",
12381
- label: update ? "Skip — keep the installed version" : "Skip — do not install",
12382
- value: "skip"
12383
- };
12384
- }
12385
- function reviewPrompt(localId, update, pack, entry) {
12386
- const sourceName = entry?.sourceName ?? "its directory";
12387
- if (pack === void 0) return `"${localId}" could not be fetched from ${sourceName}.`;
12388
- return `${update ? "Update" : "Install"} "${pack.name}" ${pack.version} from ${sourceName}? Press p on the install row to read its full SKILL.md before deciding.`;
12389
- }
12390
- function installOption(localId, update, pack, entry, problem) {
12391
- if (pack === void 0) return {
12392
- description: problem ?? "This skill could not be fetched.",
12393
- disabled: true,
12394
- label: `Install ${localId}`,
12395
- value: "install"
12396
- };
12397
- return {
12398
- description: entry?.sourceUrl ?? entry?.sourceName ?? "",
12399
- label: `${update ? "Update to" : "Install"} ${localId} ${pack.version}`,
12400
- preview: pack.files.find((file) => file.path === "SKILL.md")?.content,
12401
- value: "install"
12402
- };
12661
+ function needsReview(identity, recorded, bundled) {
12662
+ if (isRemoteIdentity(identity)) return true;
12663
+ const previous = recorded.get(identity);
12664
+ const available = bundled.get(identity);
12665
+ return previous !== void 0 && available !== void 0 && movedFrom(previous, available);
12403
12666
  }
12404
12667
  //#endregion
12405
12668
  //#region src/setup/steps/skill-finalize.ts
@@ -12490,13 +12753,15 @@ async function gatherApprovedSkills(context, io, approval) {
12490
12753
  const listing = await context.skillCatalog.load(approvedPrivateSourceIds);
12491
12754
  const manifestSkills = recordedSkills(context);
12492
12755
  emitNotes(context, io, listing, manifestSkills);
12493
- if (isEmptyCatalog(listing, manifestSkills)) return emptyCatalogOutcome(context);
12756
+ if (isEmptyCatalog(listing, manifestSkills)) return emptyCatalogOutcome$1(context);
12494
12757
  const inputs = {
12495
12758
  approvedPrivateSourceIds,
12759
+ availableSkills: context.model.availableSkills ?? [],
12496
12760
  catalog: context.skillCatalog,
12497
12761
  listing,
12498
12762
  managedApps: managedSkillApps(context),
12499
- manifestSkills
12763
+ manifestSkills,
12764
+ presetSkills: context.preset?.skills ?? []
12500
12765
  };
12501
12766
  const opening = openingSelection(context, inputs, manifestSkills);
12502
12767
  noteHeldBack(inputs, io, opening.heldBack);
@@ -12505,7 +12770,7 @@ async function gatherApprovedSkills(context, io, approval) {
12505
12770
  flow: context.flow
12506
12771
  });
12507
12772
  if (result === SETUP_ABORTED || result === SETUP_BACK) return result;
12508
- return completedSelections(context, approval, await finalizeSkills(result, inputs, manifestSkills), manifestSkills);
12773
+ return completedSelections$1(context, approval, await finalizeSkills(result, inputs, manifestSkills), manifestSkills, result.decisions);
12509
12774
  }
12510
12775
  /**
12511
12776
  * Says out loud that a changed Apps answer cleared picks the user had already made.
@@ -12522,13 +12787,15 @@ function noteHeldBack(inputs, io, heldBack) {
12522
12787
  function recordedSkills(context) {
12523
12788
  return context.manifest.status === "ready" ? context.manifest.value.skills : [];
12524
12789
  }
12525
- function emptyCatalogOutcome(context) {
12790
+ function emptyCatalogOutcome$1(context) {
12526
12791
  return context.enteredBackward === true ? SETUP_BACK : { ...context.selections };
12527
12792
  }
12528
12793
  function chainEntry(context) {
12529
12794
  return context.enteredBackward === true ? "end" : "start";
12530
12795
  }
12531
- function completedSelections(context, approval, skills, manifestSkills) {
12796
+ function completedSelections$1(context, approval, skills, manifestSkills, decisions) {
12797
+ const recorded = new Set(manifestSkills.map((skill) => skillIdentity(skill.source, skill.id)));
12798
+ const updates = Object.entries(decisions).filter(([identity, decision]) => decision === "install" && recorded.has(identity)).map(([identity]) => identity);
12532
12799
  if (skills.selected.length === 0 && manifestSkills.length === 0) {
12533
12800
  const { skills: _staleSkills, ...selections } = context.selections;
12534
12801
  return approval.length === 0 ? selections : {
@@ -12539,10 +12806,14 @@ function completedSelections(context, approval, skills, manifestSkills) {
12539
12806
  }
12540
12807
  };
12541
12808
  }
12809
+ const selection = updates.length === 0 ? skills : {
12810
+ ...skills,
12811
+ updates
12812
+ };
12542
12813
  return {
12543
12814
  ...context.selections,
12544
- skills: approval.length === 0 ? skills : {
12545
- ...skills,
12815
+ skills: approval.length === 0 ? selection : {
12816
+ ...selection,
12546
12817
  approvedPrivateSourceIds: approval
12547
12818
  }
12548
12819
  };
@@ -12582,47 +12853,152 @@ function isEmptyCatalog(listing, manifestSkills) {
12582
12853
  /** The chain's opening state, plus whatever the current Apps answer forces it to leave out. */
12583
12854
  function openingSelection(context, inputs, manifestSkills) {
12584
12855
  const recorded = new Set(manifestSkills.map((skill) => skillIdentity(skill.source, skill.id)));
12585
- const opening = initialSkillSelection(context.selections.skills?.selected.map((skill) => skillIdentity(skill.source, skill.id)) ?? [...recorded], recorded, inputs.managedApps);
12856
+ const opening = initialSkillSelection(context.selections.skills?.selected.map((skill) => skillIdentity(skill.source, skill.id)) ?? (context.manifest.status === "ready" ? [...recorded] : inputs.presetSkills.map((skill) => skillIdentity(skill.source, skill.id))), recorded, inputs.managedApps);
12586
12857
  return {
12587
12858
  heldBack: opening.heldBack,
12588
12859
  state: {
12589
- decisions: {},
12860
+ decisions: Object.fromEntries((context.selections.skills?.updates ?? []).map((identity) => [identity, "install"])),
12590
12861
  selected: opening.selected
12591
12862
  }
12592
12863
  };
12593
12864
  }
12594
12865
  //#endregion
12866
+ //#region src/setup/steps/snippet-stages.ts
12867
+ const REVIEW_PREFIX = "snippet-update:";
12868
+ function snippetStages(inputs) {
12869
+ return [pickerStage(inputs), reviewStage(inputs)];
12870
+ }
12871
+ function snippetOptions(inputs) {
12872
+ const { catalog, preset, previous } = inputs;
12873
+ return sortForDisplay(catalog, (entry) => [
12874
+ entry.category,
12875
+ entry.name,
12876
+ entry.id
12877
+ ]).map((entry) => entry.status === "available" ? {
12878
+ description: entry.description,
12879
+ group: entry.category,
12880
+ label: `${entry.name}${preset.has(entry.id) ? " (from preset)" : ""}`,
12881
+ preview: entry.content,
12882
+ value: entry.id
12883
+ } : {
12884
+ description: previous.has(entry.id) ? `${entry.reason} Clear it to remove the snippet.` : `${entry.description} ${entry.reason}`,
12885
+ disabled: true,
12886
+ group: entry.category,
12887
+ label: `${previous.has(entry.id) ? `${entry.name} (preserved)` : entry.name}${preset.has(entry.id) ? " (from preset)" : ""}`,
12888
+ value: entry.id
12889
+ });
12890
+ }
12891
+ /** The ids whose review answer accepted the available revision. */
12892
+ function acceptedUpdates(state) {
12893
+ return state.selected.filter((id) => state.decisions[id] === "update");
12894
+ }
12895
+ function pickerStage(inputs) {
12896
+ const options = snippetOptions(inputs);
12897
+ const selectable = new Set(options.filter((option) => option.disabled !== true).map((option) => option.value));
12898
+ return {
12899
+ apply: (state, answers) => ({
12900
+ ...state,
12901
+ selected: selectedValues(answers["snippets"]).filter((id) => selectable.has(id) || inputs.retained.has(id))
12902
+ }),
12903
+ label: "Snippets",
12904
+ questions: (state) => options.length === 0 ? void 0 : [{
12905
+ id: "snippets",
12906
+ initial: state.selected,
12907
+ kind: "multiselect",
12908
+ label: "Snippets",
12909
+ options,
12910
+ prompt: "Which snippets should Aura add to the shared instructions?"
12911
+ }]
12912
+ };
12913
+ }
12914
+ function reviewStage(inputs) {
12915
+ const installed = installedById(inputs.context);
12916
+ const available = new Map(inputs.catalog.map((entry) => [entry.id, entry]));
12917
+ return {
12918
+ apply: (state, answers) => ({
12919
+ ...state,
12920
+ decisions: foldDecisions(state.decisions, answers, REVIEW_PREFIX, "keep")
12921
+ }),
12922
+ compactLabel: "Review",
12923
+ label: "Review",
12924
+ questions: (state) => {
12925
+ const questions = state.selected.flatMap((id) => reviewQuestion(id, state, inputs, installed.get(id), available.get(id)));
12926
+ return questions.length === 0 ? void 0 : questions;
12927
+ }
12928
+ };
12929
+ }
12930
+ /**
12931
+ * The one form asking about a snippet whose source revision no longer matches the recorded one.
12932
+ *
12933
+ * A diverged revision — a rollback, or one this build cannot order — is offered too, because the
12934
+ * alternative is holding the snippet at its recorded revision forever with nothing on screen. Both
12935
+ * default to keeping what is installed, which is what makes a non-interactive run safe.
12936
+ */
12937
+ function reviewQuestion(id, state, inputs, installed, available) {
12938
+ if (installed === void 0 || available?.status !== "available") return [];
12939
+ const status = managedContentRevisionStatus(installed.version, installed.hash, available.version, hashManagedSnippet(available.content));
12940
+ if (status === "current") return [];
12941
+ const preview = updatePreview(inputs.context, installed, available.content);
12942
+ return [{
12943
+ id: `${REVIEW_PREFIX}${id}`,
12944
+ initial: [state.decisions[id] ?? "keep"],
12945
+ kind: "select",
12946
+ label: `Review ${id}`,
12947
+ options: [{
12948
+ description: `Keep installed version ${installed.version}.`,
12949
+ label: "Keep installed version",
12950
+ value: "keep"
12951
+ }, {
12952
+ ...preview === void 0 ? {
12953
+ disabled: true,
12954
+ disabledNote: "resolve MGD-001 first"
12955
+ } : { preview },
12956
+ description: `Review and install version ${available.version}.`,
12957
+ label: `${status === "update" ? "Update to" : "Switch to"} ${available.version}`,
12958
+ value: "update"
12959
+ }],
12960
+ prompt: reviewPrompt(id, status, installed.version, available.version)
12961
+ }];
12962
+ }
12963
+ function reviewPrompt(id, status, installedVersion, availableVersion) {
12964
+ return status === "update" ? `A newer source revision is available for ${id}.` : `The installed plugin offers ${id} at version ${availableVersion}, which is not newer than the recorded version ${installedVersion}. Aura keeps the recorded revision unless you switch to it here.`;
12965
+ }
12966
+ function installedById(context) {
12967
+ return new Map(context.manifest.status === "ready" ? context.manifest.value.snippets.map((snippet) => [snippet.id, snippet]) : []);
12968
+ }
12969
+ /** The diff shown on the update row, or nothing when local edits make it unsafe to offer. */
12970
+ function updatePreview(context, installed, availableContent) {
12971
+ const document = context.model.sharedInstructions;
12972
+ if (document.content === void 0) return;
12973
+ const parsed = readManagedBlock(document.content);
12974
+ if (parsed.status !== "present") return;
12975
+ const snippet = parsed.block.snippets.find((candidate) => candidate.id === installed.id);
12976
+ if (snippet === void 0 || !snippet.hashMatches || snippet.computedHash !== installed.hash) return;
12977
+ return diffManagedSnippet(document.path, snippet.content, availableContent);
12978
+ }
12979
+ //#endregion
12595
12980
  //#region src/setup/steps/snippets.ts
12981
+ /**
12982
+ * The snippets step: a picker over every available snippet, then a review form per pending update.
12983
+ *
12984
+ * The two forms run as one chain so ← walks back from the review into the picker rather than out
12985
+ * of the step: leaving would discard both the boxes just ticked and every review already answered.
12986
+ * Every review defaults to keeping the installed version, which is what holds managed content at
12987
+ * its recorded revision under `--yes`.
12988
+ */
12596
12989
  const snippetsStep = {
12597
12990
  addKind: "snippet",
12598
12991
  gather: async (context, io) => {
12599
12992
  const catalog = await context.snippetCatalog.load();
12600
- const previous = previousSnippetIds(context);
12601
- const options = snippetOptions(catalog, previous);
12993
+ const inputs = stageInputs(context, catalog);
12602
12994
  if (context.revisited !== true) emitUnavailableNotes(catalog, io.note);
12603
- if (options.length === 0) {
12604
- if (context.enteredBackward === true) return SETUP_BACK;
12605
- if (context.revisited !== true) io.note("No snippets are available from the installed plugins.");
12606
- return {
12607
- ...context.selections,
12608
- snippets: { selected: [] }
12609
- };
12610
- }
12611
- const result = await io.ask([{
12612
- id: "snippets",
12613
- initial: context.selections.snippets?.selected ?? options.filter((option) => previous.has(option.value)).map((option) => option.value),
12614
- kind: "multiselect",
12615
- label: "Snippets",
12616
- options,
12617
- prompt: "Which snippets should Aura add to the shared instructions?"
12618
- }]);
12619
- if (result === "aborted") return SETUP_ABORTED;
12620
- if (result === "back") return SETUP_BACK;
12621
- const selected = selectedSnippetIds(options, previous, selectedValues(result["snippets"]));
12622
- return {
12623
- ...context.selections,
12624
- snippets: { selected }
12625
- };
12995
+ if (snippetOptions(inputs).length === 0) return emptyCatalogOutcome(context, io);
12996
+ const result = await runFormChain(snippetStages(inputs), openingState(context, inputs), io, {
12997
+ entry: context.enteredBackward === true ? "end" : "start",
12998
+ flow: context.flow
12999
+ });
13000
+ if (result === SETUP_ABORTED || result === SETUP_BACK) return result;
13001
+ return completedSelections(context, result);
12626
13002
  },
12627
13003
  id: "snippets",
12628
13004
  prerequisites: [{
@@ -12632,42 +13008,49 @@ const snippetsStep = {
12632
13008
  }],
12633
13009
  title: "Snippets"
12634
13010
  };
13011
+ function stageInputs(context, catalog) {
13012
+ const previous = previousSnippetIds(context);
13013
+ const preset = new Set(context.preset?.snippets ?? []);
13014
+ return {
13015
+ catalog,
13016
+ context,
13017
+ preset,
13018
+ previous,
13019
+ retained: context.manifest.status === "ready" ? previous : preset
13020
+ };
13021
+ }
13022
+ /** The chain's opening state: this run's answers if it already has some, else the retained set. */
13023
+ function openingState(context, inputs) {
13024
+ const selected = context.selections.snippets?.selected ?? snippetOptions(inputs).filter((option) => inputs.retained.has(option.value)).map((option) => option.value);
13025
+ return {
13026
+ decisions: Object.fromEntries((context.selections.snippets?.updates ?? []).map((id) => [id, "update"])),
13027
+ selected
13028
+ };
13029
+ }
13030
+ function completedSelections(context, state) {
13031
+ const updates = acceptedUpdates(state);
13032
+ return {
13033
+ ...context.selections,
13034
+ snippets: updates.length === 0 ? { selected: state.selected } : {
13035
+ selected: state.selected,
13036
+ updates
13037
+ }
13038
+ };
13039
+ }
13040
+ function emptyCatalogOutcome(context, io) {
13041
+ if (context.enteredBackward === true) return SETUP_BACK;
13042
+ if (context.revisited !== true) io.note("No snippets are available from the installed plugins.");
13043
+ return {
13044
+ ...context.selections,
13045
+ snippets: { selected: [] }
13046
+ };
13047
+ }
12635
13048
  function previousSnippetIds(context) {
12636
13049
  return new Set(context.manifest.status === "ready" ? context.manifest.value.snippets.map((snippet) => snippet.id) : []);
12637
13050
  }
12638
13051
  function emitUnavailableNotes(catalog, note) {
12639
13052
  for (const entry of catalog) if (entry.status === "unavailable") note(`Snippet ${entry.id} is unavailable: ${entry.reason}`);
12640
13053
  }
12641
- /**
12642
- * Keeps what the user answered, in the order they were offered.
12643
- *
12644
- * An unavailable row cannot be checked, but one carried over from a previous run stays eligible so
12645
- * clearing it is how a user drops a snippet whose plugin is gone. Re-adding it here instead would
12646
- * make that selection permanent.
12647
- */
12648
- function selectedSnippetIds(options, previous, answered) {
12649
- const selectable = new Set(options.filter((option) => option.disabled !== true).map((option) => option.value));
12650
- return answered.filter((id) => selectable.has(id) || previous.has(id));
12651
- }
12652
- function snippetOptions(catalog, previous) {
12653
- return sortForDisplay(catalog, (entry) => [
12654
- entry.category,
12655
- entry.name,
12656
- entry.id
12657
- ]).map((entry) => entry.status === "available" ? {
12658
- description: entry.description,
12659
- group: entry.category,
12660
- label: entry.name,
12661
- preview: entry.content,
12662
- value: entry.id
12663
- } : {
12664
- description: previous.has(entry.id) ? `${entry.reason} Clear it to remove the snippet.` : `${entry.description} ${entry.reason}`,
12665
- disabled: true,
12666
- group: entry.category,
12667
- label: previous.has(entry.id) ? `${entry.name} (preserved)` : entry.name,
12668
- value: entry.id
12669
- });
12670
- }
12671
13054
  //#endregion
12672
13055
  //#region src/setup/steps/index.ts
12673
13056
  /**
@@ -12747,6 +13130,7 @@ async function runSetup(request) {
12747
13130
  presetOrigin: configured.presetOrigin,
12748
13131
  registry: request.registry
12749
13132
  });
13133
+ const preset = setupPresetContext(request, configured.config, configured.preset);
12750
13134
  const stepContext = {
12751
13135
  appCatalog: buildAppCatalog(request.registry.adapters, model, scan.skipped),
12752
13136
  ...initialFindings === void 0 ? {} : { findings: initialFindings },
@@ -12756,7 +13140,8 @@ async function runSetup(request) {
12756
13140
  mcpCatalog: catalogs.mcpCatalog,
12757
13141
  model: effectiveModel,
12758
13142
  skillCatalog: catalogs.skillCatalog,
12759
- snippetCatalog: createSnippetCatalog(request.registry.snippets, model.manifest)
13143
+ snippetCatalog: createSnippetCatalog(request.registry.snippets, model.manifest, preset?.snippets ?? []),
13144
+ ...preset === void 0 ? {} : { preset }
12760
13145
  };
12761
13146
  let start = {
12762
13147
  index: 0,
@@ -12791,6 +13176,32 @@ async function runSetup(request) {
12791
13176
  manifest: ready.manifest
12792
13177
  });
12793
13178
  }
13179
+ function setupPresetContext(request, config, preset) {
13180
+ const selected = config.preset;
13181
+ if (selected === void 0 || preset === void 0) return;
13182
+ return Object.freeze({
13183
+ checkSummary: presetCheckSummary(config),
13184
+ explicit: request.cliReference !== void 0,
13185
+ name: selected.name,
13186
+ reference: request.cliReference ?? selected.reference,
13187
+ skills: Object.freeze([...preset.skills ?? []]),
13188
+ snippets: Object.freeze([...preset.snippets ?? []])
13189
+ });
13190
+ }
13191
+ function presetCheckSummary(config) {
13192
+ const lines = [];
13193
+ for (const [id, check] of Object.entries(config.checks)) {
13194
+ if (check.enabled.provenance.layer === "preset") lines.push(`${id}: ${check.enabled.value ? "enabled" : "disabled"}`);
13195
+ if (check.severity.provenance.layer === "preset") lines.push(`${id}: severity ${check.severity.value}`);
13196
+ if (check.thresholds.provenance.layer === "preset") lines.push(`${id}: ${formatThresholds(check.thresholds.value)}`);
13197
+ }
13198
+ return Object.freeze(lines);
13199
+ }
13200
+ /** Thresholds read as settings, not as the JSON they happen to be stored in. */
13201
+ function formatThresholds(thresholds) {
13202
+ const pairs = Object.entries(thresholds).map(([name, value]) => `${name}=${typeof value === "object" ? JSON.stringify(value) : String(value)}`);
13203
+ return pairs.length === 0 ? "no thresholds" : pairs.join(", ");
13204
+ }
12794
13205
  //#endregion
12795
13206
  //#region src/setup/wizard-scripted.ts
12796
13207
  /**