arcane-cli 1.9.2 → 1.10.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/index.js CHANGED
@@ -121,6 +121,15 @@ async function copyDirectory(srcDir, targetDir, relativeDir, opts = {}) {
121
121
  }
122
122
  return copiedFiles;
123
123
  }
124
+ async function listDirectoryFiles(srcDir, relativeDir) {
125
+ const files = [];
126
+ for (const entry of await readdir(srcDir, { withFileTypes: true })) {
127
+ const rel = `${relativeDir}/${entry.name}`;
128
+ if (entry.isDirectory()) files.push(...await listDirectoryFiles(path.join(srcDir, entry.name), rel));
129
+ else files.push(rel);
130
+ }
131
+ return files;
132
+ }
124
133
 
125
134
  // src/modules/manifest.ts
126
135
  import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
@@ -1470,7 +1479,7 @@ async function printWelcome(version, cwd) {
1470
1479
  }
1471
1480
  printFooter(version);
1472
1481
  console.log();
1473
- const label = (s) => chalk.hex(STEEL_DIM)(s.padEnd(11));
1482
+ const label2 = (s) => chalk.hex(STEEL_DIM)(s.padEnd(11));
1474
1483
  const dot = chalk.hex(FAINT)("\xB7");
1475
1484
  let repoLine;
1476
1485
  try {
@@ -1481,11 +1490,11 @@ async function printWelcome(version, cwd) {
1481
1490
  } catch {
1482
1491
  repoLine = `${chalk.hex(STEEL_DIM)("\u25CB")} not initialized ${chalk.hex(FAINT)("\u2014")} run ${chalk.hex(STEEL).inverse(" spell init ")} ${chalk.hex(STEEL_DIM)("to scaffold")}`;
1483
1492
  }
1484
- console.log(` ${label("This repo")}${repoLine}`);
1493
+ console.log(` ${label2("This repo")}${repoLine}`);
1485
1494
  const phases = ["plan", "architect", "implement", "test", "review", "ship"].join(
1486
1495
  chalk.hex(STEEL_DEEP)(" \u2192 ")
1487
1496
  );
1488
- console.log(` ${label("Spell Loop")}${chalk.hex("#dbe3f7")(phases)}`);
1497
+ console.log(` ${label2("Spell Loop")}${chalk.hex("#dbe3f7")(phases)}`);
1489
1498
  const flat = ["plan", "architect", "implement", "test", "review", "ship"].join(" \u2192 ");
1490
1499
  const loopStart = 13 + flat.indexOf("implement");
1491
1500
  const loopEnd = 13 + flat.indexOf("review") + 5;
@@ -2469,7 +2478,7 @@ async function runAgentsInit(options, targetDir, assetsDir) {
2469
2478
  const scope = options.scope ?? "repo";
2470
2479
  if (scope === "user") await requireUserStore(targetDir);
2471
2480
  const baseDir = agentsBaseDir(targetDir, scope);
2472
- const label = scope === "user" ? "~/.arcane" : ".arcane";
2481
+ const label2 = scope === "user" ? "~/.arcane" : ".arcane";
2473
2482
  if (!options.force && await rosterExists(targetDir, scope)) {
2474
2483
  console.log(
2475
2484
  'Agent roster already exists at .arcane/agents.yaml. Run "spell agents sync" to regenerate outputs, or use --force to reinitialize.'
@@ -2569,7 +2578,7 @@ All 12 roles across ${allCategories.join(", ")}` : "\nPick individual roles from
2569
2578
  await mkdir4(arcaneDir, { recursive: true });
2570
2579
  await writeFile6(join4(baseDir, "agents.yaml"), stringify(roster), "utf8");
2571
2580
  } else {
2572
- console.log(` [dry-run] Would write: ${label}/agents.yaml`);
2581
+ console.log(` [dry-run] Would write: ${label2}/agents.yaml`);
2573
2582
  }
2574
2583
  for (const roleId of selectedRoleIds) {
2575
2584
  const src = join4(assetsDir, "agents", `${roleId}.yaml`);
@@ -2581,7 +2590,7 @@ All 12 roles across ${allCategories.join(", ")}` : "\nPick individual roles from
2581
2590
  console.warn(` ! Could not copy definition for "${roleId}" \u2014 skipping`);
2582
2591
  }
2583
2592
  } else {
2584
- console.log(` [dry-run] Would copy: ${label}/agents/${roleId}.yaml`);
2593
+ console.log(` [dry-run] Would copy: ${label2}/agents/${roleId}.yaml`);
2585
2594
  }
2586
2595
  }
2587
2596
  if (options.dryRun) {
@@ -2608,7 +2617,7 @@ All 12 roles across ${allCategories.join(", ")}` : "\nPick individual roles from
2608
2617
  }
2609
2618
  console.log();
2610
2619
  console.log(
2611
- ` \u{1F4A1} Customize: edit ${label}/agents.yaml \u2192 spell agents sync${scope === "user" ? " --user" : ""}`
2620
+ ` \u{1F4A1} Customize: edit ${label2}/agents.yaml \u2192 spell agents sync${scope === "user" ? " --user" : ""}`
2612
2621
  );
2613
2622
  console.log();
2614
2623
  if (hasUnresolvedRoles) {
@@ -4407,30 +4416,116 @@ async function runInitUser(options, storeRoot, assetsDir, packageVersion) {
4407
4416
  console.log();
4408
4417
  }
4409
4418
 
4410
- // src/commands/add.ts
4419
+ // src/modules/component-install.ts
4420
+ import { readFile as readFile7, stat, writeFile as writeFile9 } from "fs/promises";
4411
4421
  import { join as join8 } from "path";
4412
- async function runAdd(name, options, targetDir, assetsDir, packageVersion) {
4413
- let manifest;
4422
+ async function planComponentInstall(component, targetDir, assetsDir, opts = {}) {
4423
+ const files = [];
4424
+ const stateOf = async (file, src) => {
4425
+ const dest = join8(targetDir, file);
4426
+ if (!await fileExists(dest)) return { file, src, state: "missing" };
4427
+ if ((await stat(dest)).isDirectory()) return { file, src, state: "directory" };
4428
+ const same = await hashFile(dest) === await hashFile(src);
4429
+ return { file, src, state: same ? "identical" : "differs" };
4430
+ };
4431
+ for (const file of component.files) {
4432
+ files.push(await stateOf(file, join8(assetsDir, component.sourceOverrides?.[file] ?? file)));
4433
+ }
4434
+ for (const dir of component.directories ?? []) {
4435
+ const srcDir = join8(assetsDir, dir);
4436
+ for (const file of await listDirectoryFiles(srcDir, dir)) {
4437
+ files.push(await stateOf(file, join8(assetsDir, file)));
4438
+ }
4439
+ }
4440
+ const of = (...states) => files.filter((f) => states.includes(f.state));
4441
+ if (opts.force) {
4442
+ return { component, files, toCopy: of("missing", "identical", "differs"), kept: [], conflicts: of("directory") };
4443
+ }
4444
+ if (component.skipExisting) {
4445
+ return { component, files, toCopy: of("missing"), kept: of("identical", "differs", "directory"), conflicts: [] };
4446
+ }
4447
+ return { component, files, toCopy: of("missing", "identical"), kept: [], conflicts: of("differs", "directory") };
4448
+ }
4449
+ async function executeInstallPlan(plan, targetDir) {
4450
+ const files = [];
4451
+ const fileHashes = {};
4452
+ const written = [];
4414
4453
  try {
4415
- manifest = await readManifest(targetDir);
4416
- } catch (err) {
4417
- if (err instanceof ManifestNotFoundError) {
4418
- console.error(
4419
- 'Not initialized. Run "spell init" first before adding components.'
4420
- );
4421
- process.exit(1);
4422
- return;
4454
+ for (const { file, src, state } of plan.toCopy) {
4455
+ const previous = state === "missing" ? null : await readFile7(join8(targetDir, file));
4456
+ fileHashes[file] = await copyFile(src, targetDir, file, { force: true });
4457
+ written.push({ file, previous });
4458
+ files.push(file);
4423
4459
  }
4460
+ } catch (err) {
4461
+ await rollback(written, targetDir);
4424
4462
  throw err;
4425
4463
  }
4464
+ return { files, fileHashes };
4465
+ }
4466
+ async function rollback(written, targetDir) {
4467
+ for (const { file, previous } of [...written].reverse()) {
4468
+ try {
4469
+ if (previous === null) {
4470
+ await removeWithin(targetDir, file);
4471
+ await removeEmptyAncestors(targetDir, file);
4472
+ } else {
4473
+ await writeFile9(join8(targetDir, file), previous);
4474
+ }
4475
+ } catch {
4476
+ }
4477
+ }
4478
+ }
4479
+
4480
+ // src/commands/add.ts
4481
+ function describeExisting(f) {
4482
+ if (f.state === "directory") return "is a directory";
4483
+ return f.state === "identical" ? "identical to the packaged file" : "differs from the packaged file";
4484
+ }
4485
+ function directoryConflict(plan) {
4486
+ return plan.conflicts.find((f) => f.state === "directory");
4487
+ }
4488
+ function printDryRun2(name, plan, options) {
4489
+ for (const f of plan.files) {
4490
+ if (f.state === "missing") {
4491
+ console.log(` [dry-run] Would copy: ${f.file}`);
4492
+ } else if (f.state === "directory" && !(plan.component.skipExisting && !options.force)) {
4493
+ console.log(` [dry-run] Would refuse: ${f.file} (exists, is a directory) \u2014 remove it first`);
4494
+ } else if (options.force) {
4495
+ console.log(` [dry-run] Would overwrite: ${f.file} (exists, ${describeExisting(f)})`);
4496
+ } else if (plan.component.skipExisting) {
4497
+ console.log(` [dry-run] Would keep: ${f.file} (exists, yours, ${describeExisting(f)})`);
4498
+ } else if (f.state === "identical") {
4499
+ console.log(` [dry-run] Would adopt: ${f.file} (exists, identical to the packaged file)`);
4500
+ } else {
4501
+ console.log(` [dry-run] Would refuse: ${f.file} (exists, ${describeExisting(f)}) \u2014 needs --force`);
4502
+ }
4503
+ }
4504
+ const dir = directoryConflict(plan);
4505
+ if (dir) {
4506
+ console.log(`
4507
+ [dry-run] Would refuse component "${name}": a directory is at "${dir.file}", which --force does not replace. Remove it first.`);
4508
+ return;
4509
+ }
4510
+ if (plan.conflicts.length > 0) {
4511
+ console.log(
4512
+ `
4513
+ [dry-run] Would refuse component "${name}": ${plan.conflicts.length} existing file${plan.conflicts.length === 1 ? "" : "s"}. Re-run with --force to overwrite.`
4514
+ );
4515
+ return;
4516
+ }
4517
+ console.log(`
4518
+ [dry-run] Would add component "${name}" \u2014 ${plan.toCopy.length} files`);
4519
+ }
4520
+ async function addOne(name, options, targetDir, assetsDir, packageVersion) {
4521
+ const manifest = await readManifest(targetDir);
4426
4522
  let component;
4427
4523
  try {
4428
4524
  component = getComponent(name);
4429
4525
  } catch (err) {
4430
4526
  if (err instanceof ComponentNotFoundError) {
4431
4527
  console.error(err.message);
4432
- process.exit(1);
4433
- return;
4528
+ return "failed";
4434
4529
  }
4435
4530
  throw err;
4436
4531
  }
@@ -4438,58 +4533,81 @@ async function runAdd(name, options, targetDir, assetsDir, packageVersion) {
4438
4533
  console.log(
4439
4534
  `Component "${name}" is already installed. Use "spell update" to update it.`
4440
4535
  );
4441
- return;
4536
+ return "skipped";
4442
4537
  }
4443
- let fileCount = 0;
4444
- const installedFiles = [];
4445
- const fileHashes = {};
4446
- for (const file of component.files) {
4447
- const srcPath = join8(assetsDir, component.sourceOverrides?.[file] ?? file);
4448
- if (options.dryRun) {
4449
- console.log(` [dry-run] Would copy: ${file}`);
4450
- } else {
4451
- fileHashes[file] = await copyFile(srcPath, targetDir, file, { force: options.force });
4452
- }
4453
- installedFiles.push(file);
4454
- fileCount++;
4538
+ const plan = await planComponentInstall(component, targetDir, assetsDir, { force: options.force });
4539
+ if (options.dryRun) {
4540
+ printDryRun2(name, plan, options);
4541
+ return plan.conflicts.length > 0 ? "failed" : "added";
4455
4542
  }
4456
- for (const dir of component.directories ?? []) {
4457
- const srcDirPath = join8(assetsDir, dir);
4458
- if (options.dryRun) {
4459
- console.log(` [dry-run] Would copy directory: ${dir}/`);
4460
- } else {
4461
- const copied = await copyDirectory(srcDirPath, targetDir, dir, { force: options.force });
4462
- for (const { path: copiedPath, hash } of copied) {
4463
- installedFiles.push(copiedPath);
4464
- fileHashes[copiedPath] = hash;
4465
- }
4466
- fileCount += copied.length;
4467
- }
4543
+ const dir = directoryConflict(plan);
4544
+ if (dir) {
4545
+ console.error(
4546
+ `Cannot add "${name}": "${dir.file}" is a directory, which --force does not replace. Nothing was written. Remove it first.`
4547
+ );
4548
+ return "failed";
4468
4549
  }
4469
- if (options.dryRun) {
4470
- console.log(
4471
- `
4472
- [dry-run] Would add component "${name}" \u2014 ${fileCount} files`
4550
+ if (plan.conflicts.length > 0) {
4551
+ const others = plan.conflicts.length - 1;
4552
+ console.error(
4553
+ `Cannot add "${name}": "${plan.conflicts[0].file}" already exists${others > 0 ? ` (and ${others} more)` : ""}. Nothing was written. Use --force to overwrite.`
4473
4554
  );
4474
- return;
4555
+ return "failed";
4475
4556
  }
4557
+ const { files, fileHashes } = await executeInstallPlan(plan, targetDir);
4558
+ for (const f of plan.kept) console.log(` Kept existing (yours): ${f.file}`);
4476
4559
  const newComponent = {
4477
4560
  name,
4478
- files: installedFiles,
4561
+ files,
4479
4562
  installedVersion: packageVersion,
4480
4563
  fileHashes
4481
4564
  };
4482
- const updated = addComponent(manifest, newComponent);
4483
- await writeManifest(targetDir, updated);
4565
+ await writeManifest(targetDir, addComponent(manifest, newComponent));
4484
4566
  console.log(
4485
4567
  `
4486
- \u2713 Added component "${name}" \u2014 ${fileCount} files copied`
4568
+ \u2713 Added component "${name}" \u2014 ${files.length} files copied${plan.kept.length > 0 ? `, ${plan.kept.length} kept` : ""}`
4487
4569
  );
4570
+ return "added";
4571
+ }
4572
+ async function runAdd(names, options, targetDir, assetsDir, packageVersion) {
4573
+ try {
4574
+ await readManifest(targetDir);
4575
+ } catch (err) {
4576
+ if (err instanceof ManifestNotFoundError) {
4577
+ console.error(
4578
+ 'Not initialized. Run "spell init" first before adding components.'
4579
+ );
4580
+ process.exit(1);
4581
+ return;
4582
+ }
4583
+ throw err;
4584
+ }
4585
+ const added = [];
4586
+ const refused = [];
4587
+ for (const [index, name] of names.entries()) {
4588
+ const outcome = await addOne(name, options, targetDir, assetsDir, packageVersion);
4589
+ if (outcome === "added") added.push(name);
4590
+ if (outcome !== "failed") continue;
4591
+ refused.push(name);
4592
+ if (options.dryRun) continue;
4593
+ if (names.length > 1) {
4594
+ const notAttempted = names.slice(index + 1);
4595
+ console.error(
4596
+ `Added: ${added.length > 0 ? added.join(", ") : "none"}.${notAttempted.length > 0 ? ` Not attempted: ${notAttempted.join(", ")}.` : ""}`
4597
+ );
4598
+ }
4599
+ process.exit(1);
4600
+ return;
4601
+ }
4602
+ if (refused.length > 0) {
4603
+ console.error(`[dry-run] Would refuse: ${refused.join(", ")}.`);
4604
+ process.exit(1);
4605
+ }
4488
4606
  }
4489
4607
 
4490
4608
  // src/commands/update.ts
4491
4609
  import { dirname as dirname5, join as join10 } from "path";
4492
- import { readFile as readFile7, writeFile as writeFile9 } from "fs/promises";
4610
+ import { readFile as readFile8, writeFile as writeFile10 } from "fs/promises";
4493
4611
 
4494
4612
  // src/modules/merge3.ts
4495
4613
  var CONFLICT_START = "<<<<<<< yours (edited)";
@@ -4693,27 +4811,111 @@ function formatRegistryChanges(changes) {
4693
4811
  }
4694
4812
  if (newlyAvailable.length > 0) {
4695
4813
  const profile = changes.profile ? ` "${changes.profile}"` : "";
4696
- lines.push(` Newly available for your${profile} profile, not installed \u2014 nothing was added:`);
4814
+ lines.push(
4815
+ changes.installedByUpdate ? ` Newly available for your${profile} profile, not installed by this run:` : ` Newly available for your${profile} profile, not installed \u2014 nothing was added:`
4816
+ );
4697
4817
  for (const component of newlyAvailable) {
4698
4818
  lines.push(` ${component.name} \u2014 ${component.description}`);
4699
4819
  lines.push(` spell add ${component.name}`);
4700
4820
  if (component.alreadyPresent.length > 0) {
4701
4821
  lines.push(
4702
- ` (already here and yours: ${component.alreadyPresent.join(", ")} \u2014 \`spell add\` stops at an existing file rather than overwrite it)`
4822
+ ` (already here and yours: ${component.alreadyPresent.join(", ")} \u2014 \`spell add\` keeps it rather than overwrite it)`
4703
4823
  );
4704
4824
  }
4705
4825
  }
4826
+ if (!changes.addNewUsed) lines.push(" To install them in one step: `spell update --add-new`");
4706
4827
  }
4707
4828
  if (missingRequires.length > 0) {
4708
4829
  lines.push(" Missing prerequisites \u2014 installed components cite these, and they are not installed:");
4709
- for (const { name, requiredBy } of missingRequires) {
4830
+ for (const { name, requiredBy, leftAloneReason } of missingRequires) {
4710
4831
  lines.push(` ${name} (required by ${requiredBy.join(", ")})`);
4711
4832
  lines.push(` spell add ${name}`);
4833
+ if (leftAloneReason) lines.push(` (not installed by update: ${leftAloneReason})`);
4712
4834
  }
4713
4835
  }
4714
4836
  return lines;
4715
4837
  }
4716
4838
 
4839
+ // src/modules/update-installs.ts
4840
+ import { checkbox as checkbox2 } from "@inquirer/prompts";
4841
+ function offerableNewComponents(newlyAvailable, missingRequires) {
4842
+ return newlyAvailable.filter(
4843
+ (c) => !getComponent(c.name).initOnly && !missingRequires.some((m) => m.name === c.name)
4844
+ );
4845
+ }
4846
+ async function selectNewComponents(candidates, mode) {
4847
+ if (candidates.length === 0) return [];
4848
+ if (mode.addNew) return candidates;
4849
+ if (!mode.interactive || mode.dryRun) return [];
4850
+ const chosen = await checkbox2({
4851
+ message: "Newly available components. Space to select, Enter to confirm (none selected installs nothing):",
4852
+ choices: candidates.map((c) => ({ name: `${c.name} \u2014 ${c.description}`, value: c.name, checked: false }))
4853
+ });
4854
+ return candidates.filter((c) => chosen.includes(c.name));
4855
+ }
4856
+ async function planUpdateInstalls(args) {
4857
+ const items = [];
4858
+ const leftAlone = [];
4859
+ for (const { name, requiredBy } of args.missingRequires) {
4860
+ const component = componentForSpellScope(getComponent(name), args.spellScope);
4861
+ if (component.initOnly) {
4862
+ leftAlone.push({ name, kind: "prerequisite", reason: "it is an initOnly component: adding it mid-life changes how Git treats existing files, so it is your call" });
4863
+ continue;
4864
+ }
4865
+ if (component.files.length === 0) continue;
4866
+ const plan = await planComponentInstall(component, args.targetDir, args.assetsDir, {});
4867
+ if (plan.conflicts.length > 0) {
4868
+ leftAlone.push({ name, kind: "prerequisite", reason: conflictReason(plan) });
4869
+ continue;
4870
+ }
4871
+ items.push({ plan, kind: "prerequisite", requiredBy });
4872
+ }
4873
+ for (const candidate of args.selectedNew) {
4874
+ const component = componentForSpellScope(getComponent(candidate.name), args.spellScope);
4875
+ if (component.initOnly || component.files.length === 0) continue;
4876
+ const plan = await planComponentInstall(component, args.targetDir, args.assetsDir, {});
4877
+ if (plan.conflicts.length > 0) {
4878
+ leftAlone.push({ name: candidate.name, kind: "new", reason: conflictReason(plan) });
4879
+ continue;
4880
+ }
4881
+ items.push({ plan, kind: "new", requiredBy: [] });
4882
+ }
4883
+ return { items, leftAlone };
4884
+ }
4885
+ function conflictReason(plan) {
4886
+ const first = plan.conflicts[0];
4887
+ if (first.state === "directory") return `${first.file} is a directory, which update does not replace`;
4888
+ const more = plan.conflicts.length > 1 ? ` (and ${plan.conflicts.length - 1} more)` : "";
4889
+ return `${first.file} already exists${more}, and update does not overwrite`;
4890
+ }
4891
+ function label(item) {
4892
+ return item.kind === "prerequisite" ? `prerequisite of ${item.requiredBy.join(", ")}` : "newly available";
4893
+ }
4894
+ function describeInstallPlan(plan) {
4895
+ const lines = [];
4896
+ for (const item of plan.items) {
4897
+ lines.push(` [dry-run] Would install: ${item.plan.component.name} (${label(item)}) \u2014 ${item.plan.toCopy.length} files`);
4898
+ for (const f of item.plan.kept) lines.push(` [dry-run] Would keep: ${f.file} (exists, yours)`);
4899
+ }
4900
+ return lines;
4901
+ }
4902
+ async function executeUpdateInstalls(plan, targetDir, packageVersion) {
4903
+ const installed = [];
4904
+ const lines = [];
4905
+ for (const item of plan.items) {
4906
+ const { files, fileHashes } = await executeInstallPlan(item.plan, targetDir);
4907
+ installed.push({
4908
+ name: item.plan.component.name,
4909
+ files,
4910
+ installedVersion: packageVersion,
4911
+ fileHashes
4912
+ });
4913
+ lines.push(` Installed: ${item.plan.component.name} (${label(item)}) \u2014 ${files.length} files`);
4914
+ for (const f of item.plan.kept) lines.push(` Kept existing (yours): ${f.file}`);
4915
+ }
4916
+ return { installed, lines };
4917
+ }
4918
+
4717
4919
  // src/modules/npm-registry.ts
4718
4920
  var NPM_PACKAGE_NAME = "arcane-cli";
4719
4921
  async function fetchPublishedFile(version, relativeAssetPath) {
@@ -4947,6 +5149,13 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
4947
5149
  return;
4948
5150
  }
4949
5151
  const scope = resolvedScope.scope;
5152
+ if (options.addNew && scope !== "repo") {
5153
+ console.error(
5154
+ "\n \u2717 --add-new offers the components a repository's profile gained; the user tier has no profile. Run it without --user, inside a repository.\n"
5155
+ );
5156
+ process.exit(1);
5157
+ return;
5158
+ }
4950
5159
  let flagPatch = {};
4951
5160
  if (hasManifestFlags(options.manifestFlags)) {
4952
5161
  if (scope !== "repo") {
@@ -5015,19 +5224,49 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
5015
5224
  missingRequires: findMissingRequires(manifest.components, manifest.profile)
5016
5225
  } : {}
5017
5226
  };
5227
+ let installPlan = { items: [], leftAlone: [] };
5228
+ if (scope === "repo" && manifest.components.length > 0) {
5229
+ const missingRequires = registryChanges.missingRequires ?? [];
5230
+ const selectedNew = await selectNewComponents(
5231
+ offerableNewComponents(registryChanges.newlyAvailable ?? [], missingRequires),
5232
+ { addNew: Boolean(options.addNew), interactive: Boolean(process.stdin.isTTY), dryRun: Boolean(options.dryRun) }
5233
+ );
5234
+ installPlan = await planUpdateInstalls({ targetDir, assetsDir, spellScope, missingRequires, selectedNew });
5235
+ const installing = new Set(installPlan.items.map((i) => i.plan.component.name));
5236
+ registryChanges.installedByUpdate = installing.size > 0;
5237
+ registryChanges.addNewUsed = Boolean(options.addNew);
5238
+ registryChanges.newlyAvailable = (registryChanges.newlyAvailable ?? []).filter((c) => !installing.has(c.name));
5239
+ registryChanges.missingRequires = missingRequires.filter((m) => !installing.has(m.name)).map((m) => ({ ...m, leftAloneReason: installPlan.leftAlone.find((l) => l.name === m.name)?.reason }));
5240
+ }
5018
5241
  const printRegistryChanges = () => {
5019
5242
  for (const line of formatRegistryChanges(registryChanges)) console.log(line);
5020
5243
  };
5244
+ const applyInstalls = async () => {
5245
+ const declined = installPlan.leftAlone.filter((l) => l.kind === "new");
5246
+ if (installPlan.items.length === 0 && declined.length === 0) return [];
5247
+ console.log(options.dryRun ? "\n[dry-run] Components this update would install (ARC-052):" : "\nComponents installed by this update (ARC-052):");
5248
+ for (const l of declined) console.log(` ! Not installed: ${l.name} \u2014 ${l.reason}`);
5249
+ if (options.dryRun) {
5250
+ for (const line of describeInstallPlan(installPlan)) console.log(line);
5251
+ return [];
5252
+ }
5253
+ const { installed, lines } = await executeUpdateInstalls(installPlan, targetDir, packageVersion);
5254
+ for (const line of lines) console.log(line);
5255
+ return installed;
5256
+ };
5021
5257
  let sameVersionRestore = false;
5022
5258
  if (manifest.version === packageVersion && manifest.components.length > 0) {
5023
5259
  const missing = await findMissingTrackedFiles(targetDir, manifest.components, scope, spellScope, readopted);
5024
5260
  const unmanaged = findUnmanagedTrackedFiles(manifest.components, scope, spellScope);
5025
5261
  if (missing.length === 0 && unmanaged.length === 0) {
5026
- console.log("Already up to date.");
5262
+ console.log(installPlan.items.length === 0 ? "Already up to date." : "Installed components are at the current version.");
5263
+ const newlyInstalled = await applyInstalls();
5027
5264
  printRegistryChanges();
5028
- if (scope === "repo" && Object.keys(flagPatch).length > 0) {
5029
- if (!options.dryRun) await writeManifest(targetDir, answered);
5030
- reportFlagPatch(flagPatch, manifest, answered, options.dryRun);
5265
+ if (scope === "repo" && (Object.keys(flagPatch).length > 0 || newlyInstalled.length > 0)) {
5266
+ if (!options.dryRun) {
5267
+ await writeManifest(targetDir, { ...answered, components: [...answered.components, ...newlyInstalled] });
5268
+ }
5269
+ if (Object.keys(flagPatch).length > 0) reportFlagPatch(flagPatch, manifest, answered, options.dryRun);
5031
5270
  }
5032
5271
  if (scope === "user") {
5033
5272
  const fanout = await syncUserTierFanout({
@@ -5161,11 +5400,11 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
5161
5400
  handled = true;
5162
5401
  } else {
5163
5402
  const [currentContent, newContent] = await Promise.all([
5164
- readFile7(join10(targetDir, file), "utf-8"),
5165
- readFile7(srcPath, "utf-8")
5403
+ readFile8(join10(targetDir, file), "utf-8"),
5404
+ readFile8(srcPath, "utf-8")
5166
5405
  ]);
5167
5406
  const result = merge3(oldVendorContent, currentContent, newContent);
5168
- await writeFile9(join10(targetDir, file), result.content, "utf-8");
5407
+ await writeFile10(join10(targetDir, file), result.content, "utf-8");
5169
5408
  fileHashes[file] = await hashFile(srcPath);
5170
5409
  if (result.hasConflict) {
5171
5410
  conflictedFiles.push(file);
@@ -5214,6 +5453,7 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
5214
5453
  fileHashes
5215
5454
  });
5216
5455
  }
5456
+ updatedComponents.push(...await applyInstalls());
5217
5457
  const claimed = new Set(updatedComponents.flatMap((c) => c.files));
5218
5458
  const resolvedOrphans = /* @__PURE__ */ new Set();
5219
5459
  for (const { owner, file, recordedHash } of orphanCandidates) {
@@ -5349,7 +5589,7 @@ ${verb} ${orphansToReport.length} orphaned file(s) (tracked before this update,
5349
5589
  import { dirname as dirname7 } from "path";
5350
5590
 
5351
5591
  // src/modules/version-check.ts
5352
- import { readFile as readFile8, writeFile as writeFile10, mkdir as mkdir6 } from "fs/promises";
5592
+ import { readFile as readFile9, writeFile as writeFile11, mkdir as mkdir6 } from "fs/promises";
5353
5593
  import { dirname as dirname6, join as join11 } from "path";
5354
5594
  import { homedir as homedir3 } from "os";
5355
5595
  import { createRequire } from "module";
@@ -5368,7 +5608,7 @@ var DEFAULT_CACHE_PATH = join11(homedir3(), ".arcane", "version-cache.json");
5368
5608
  var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
5369
5609
  async function readCache(cachePath) {
5370
5610
  try {
5371
- const content = await readFile8(cachePath, "utf-8");
5611
+ const content = await readFile9(cachePath, "utf-8");
5372
5612
  const parsed = JSON.parse(content);
5373
5613
  if (typeof parsed.latest !== "string" || typeof parsed.checkedAt !== "string") {
5374
5614
  return null;
@@ -5381,7 +5621,7 @@ async function readCache(cachePath) {
5381
5621
  async function writeCache(cachePath, latest) {
5382
5622
  await mkdir6(dirname6(cachePath), { recursive: true });
5383
5623
  const entry = { latest, checkedAt: (/* @__PURE__ */ new Date()).toISOString() };
5384
- await writeFile10(cachePath, JSON.stringify(entry, null, 2), "utf-8");
5624
+ await writeFile11(cachePath, JSON.stringify(entry, null, 2), "utf-8");
5385
5625
  }
5386
5626
  async function getLatestVersion(feedUrl) {
5387
5627
  const normalizedBase = feedUrl.endsWith("/") ? feedUrl : feedUrl + "/";
@@ -5510,11 +5750,11 @@ async function runStatus(targetDir, packageVersion, options = {}) {
5510
5750
  ` ${"-".repeat(nameWidth)} ${"-".repeat(filesWidth)} ${"-".repeat(versionWidth)}`
5511
5751
  );
5512
5752
  for (const component of manifest.components) {
5513
- const label = component.name;
5753
+ const label2 = component.name;
5514
5754
  const fileCount = component.files.length.toString();
5515
5755
  const displayVersion = component.installedVersion;
5516
5756
  console.log(
5517
- ` ${pad(label, nameWidth)} ${rpad(fileCount, filesWidth)} ${displayVersion}`
5757
+ ` ${pad(label2, nameWidth)} ${rpad(fileCount, filesWidth)} ${displayVersion}`
5518
5758
  );
5519
5759
  }
5520
5760
  console.log("");
@@ -5752,7 +5992,7 @@ async function uninstallUserTier(options, storeRoot, manifest) {
5752
5992
  }
5753
5993
 
5754
5994
  // src/commands/doctor.ts
5755
- import { access as access4, readdir as readdir9, readFile as readFile11, mkdir as mkdir7, copyFile as fsCopyFile } from "fs/promises";
5995
+ import { access as access4, readdir as readdir9, readFile as readFile12, mkdir as mkdir7, copyFile as fsCopyFile } from "fs/promises";
5756
5996
  import { join as join15, dirname as dirname9 } from "path";
5757
5997
 
5758
5998
  // src/config/incidents.ts
@@ -5821,7 +6061,7 @@ function evaluateIncidentGate(records) {
5821
6061
  }
5822
6062
 
5823
6063
  // src/modules/denylist-scan.ts
5824
- import { readFile as readFile9, readdir as readdir7 } from "fs/promises";
6064
+ import { readFile as readFile10, readdir as readdir7 } from "fs/promises";
5825
6065
  import { join as join13, relative } from "path";
5826
6066
  function escapeRegExp(value) {
5827
6067
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -5839,7 +6079,7 @@ async function scanFile(absolutePath, displayPath, rules) {
5839
6079
  const findings = [];
5840
6080
  let content;
5841
6081
  try {
5842
- content = await readFile9(absolutePath, "utf8");
6082
+ content = await readFile10(absolutePath, "utf8");
5843
6083
  } catch {
5844
6084
  return findings;
5845
6085
  }
@@ -6133,7 +6373,7 @@ function parseAdoRemote(url) {
6133
6373
  }
6134
6374
 
6135
6375
  // src/modules/placeholders.ts
6136
- import { readdir as readdir8, readFile as readFile10 } from "fs/promises";
6376
+ import { readdir as readdir8, readFile as readFile11 } from "fs/promises";
6137
6377
  import { join as join14 } from "path";
6138
6378
  var PLACEHOLDER_STANDARD_FILE = "spell-authoring-standards.md";
6139
6379
  var RUNTIME_PLACEHOLDERS_START = "<!-- runtime-placeholders:start -->";
@@ -6229,7 +6469,7 @@ function parseRuntimePlaceholders(standardContent) {
6229
6469
  async function scanGovernancePlaceholders(governanceDir) {
6230
6470
  let standard;
6231
6471
  try {
6232
- standard = await readFile10(join14(governanceDir, PLACEHOLDER_STANDARD_FILE), "utf8");
6472
+ standard = await readFile11(join14(governanceDir, PLACEHOLDER_STANDARD_FILE), "utf8");
6233
6473
  } catch (err) {
6234
6474
  if (err.code === "ENOENT") {
6235
6475
  return { status: "skipped", reason: `${PLACEHOLDER_STANDARD_FILE} is not installed` };
@@ -6248,7 +6488,7 @@ async function scanGovernancePlaceholders(governanceDir) {
6248
6488
  const templateDocs = [];
6249
6489
  const findings = [];
6250
6490
  for (const file of files) {
6251
- const content = await readFile10(join14(governanceDir, file), "utf8");
6491
+ const content = await readFile11(join14(governanceDir, file), "utf8");
6252
6492
  const status = parseFrontmatterStatus(content);
6253
6493
  if (status === "template") templateDocs.push(file);
6254
6494
  if (status !== "active") continue;
@@ -6276,8 +6516,8 @@ async function checkNodeVersion() {
6276
6516
  message: `${raw} \u2014 Node.js 18 or later is required`
6277
6517
  };
6278
6518
  }
6279
- async function checkVSCodeExtension(id, label, homeDir = resolveHomeDir()) {
6280
- const name = `VS Code extension: ${label}`;
6519
+ async function checkVSCodeExtension(id, label2, homeDir = resolveHomeDir()) {
6520
+ const name = `VS Code extension: ${label2}`;
6281
6521
  const flavours = [
6282
6522
  { dir: join15(homeDir, ".vscode", "extensions"), bin: "code" },
6283
6523
  { dir: join15(homeDir, ".vscode-insiders", "extensions"), bin: "code-insiders" }
@@ -6314,7 +6554,7 @@ async function checkArcaneManifest(targetDir) {
6314
6554
  } catch {
6315
6555
  const selfHostedManifestPath = join15(targetDir, "src", "assets", ".arcane.json");
6316
6556
  try {
6317
- const raw = await readFile11(selfHostedManifestPath, "utf8");
6557
+ const raw = await readFile12(selfHostedManifestPath, "utf8");
6318
6558
  const manifest = JSON.parse(raw);
6319
6559
  if (isRecord3(manifest) && manifest["selfHosted"] === true && manifest["tracking_mode"] === "internal") {
6320
6560
  return {
@@ -6333,7 +6573,7 @@ async function checkArcaneManifest(targetDir) {
6333
6573
  };
6334
6574
  }
6335
6575
  try {
6336
- const raw = await readFile11(manifestPath2, "utf8");
6576
+ const raw = await readFile12(manifestPath2, "utf8");
6337
6577
  const manifest = JSON.parse(raw);
6338
6578
  if (!isRecord3(manifest) || typeof manifest["version"] !== "string" || !manifest["version"] || !Array.isArray(manifest["components"])) {
6339
6579
  return {
@@ -6619,15 +6859,45 @@ async function checkComponentRequires(targetDir) {
6619
6859
  name,
6620
6860
  passed: false,
6621
6861
  blocking: false,
6622
- message: missing.map(({ name: required, requiredBy }) => `${requiredBy.join(", ")} cites ${required}, which is not installed \u2014 \`spell add ${required}\``).join("; ")
6862
+ message: missing.map(({ name: required, requiredBy }) => `${requiredBy.join(", ")} cites ${required}, which is not installed \u2014 ${getComponent(required).initOnly ? "" : "`spell update` installs it, or "}\`spell add ${required}\``).join("; ")
6623
6863
  };
6624
6864
  }
6865
+ async function checkNewlyAvailableComponents(targetDir) {
6866
+ const name = "Newly available components";
6867
+ let manifest;
6868
+ try {
6869
+ manifest = await readManifest(targetDir);
6870
+ } catch {
6871
+ return { name, passed: true, blocking: false, skipped: true, message: "skipped \u2014 no readable .arcane.json" };
6872
+ }
6873
+ if (manifest.scope === "user" || !Array.isArray(manifest.components)) {
6874
+ return { name, passed: true, blocking: false, skipped: true, message: "skipped \u2014 no repository install to compare" };
6875
+ }
6876
+ const installed = manifest.components.flatMap(
6877
+ (c) => (LEGACY_COMPONENT_MIGRATIONS[c.name] ?? [c.name]).map((n) => ({ ...c, name: n }))
6878
+ );
6879
+ const prerequisites = findMissingRequires(manifest.components, manifest.profile).map((m) => m.name);
6880
+ const fresh = (await findNewlyAvailable(targetDir, manifest.profile, installed, effectiveSpellScope(manifest))).filter((c) => !prerequisites.includes(c.name));
6881
+ if (fresh.length === 0) {
6882
+ return { name, passed: true, blocking: false, message: "the profile has nothing this install lacks" };
6883
+ }
6884
+ const offerable = fresh.filter((c) => !getComponent(c.name).initOnly).map((c) => c.name);
6885
+ const byHand = fresh.filter((c) => getComponent(c.name).initOnly).map((c) => c.name);
6886
+ const parts = [];
6887
+ if (offerable.length > 0) {
6888
+ parts.push(`${offerable.join(", ")} \u2014 \`spell update --add-new\` installs ${offerable.length === 1 ? "it" : "them"}`);
6889
+ }
6890
+ if (byHand.length > 0) {
6891
+ parts.push(`${byHand.join(", ")} \u2014 initOnly, so only ${byHand.map((n) => `\`spell add ${n}\``).join(", ")}`);
6892
+ }
6893
+ return { name, passed: false, blocking: false, message: `newly available for your profile, not installed: ${parts.join("; ")}` };
6894
+ }
6625
6895
  async function checkMcpConfig(targetDir) {
6626
6896
  const name = "MCP config (per-server timeout, I12)";
6627
6897
  const filePath = join15(targetDir, ".mcp.json");
6628
6898
  let raw;
6629
6899
  try {
6630
- raw = await readFile11(filePath, "utf8");
6900
+ raw = await readFile12(filePath, "utf8");
6631
6901
  } catch {
6632
6902
  return { name, passed: true, blocking: false, message: "no .mcp.json \u2014 nothing to check" };
6633
6903
  }
@@ -6699,7 +6969,7 @@ async function checkDelegations(targetDir) {
6699
6969
  const filePath = join15(targetDir, ".arcane", "delegations.json");
6700
6970
  let raw;
6701
6971
  try {
6702
- raw = await readFile11(filePath, "utf8");
6972
+ raw = await readFile12(filePath, "utf8");
6703
6973
  } catch {
6704
6974
  return { name, passed: true, blocking: false, message: "no delegations recorded" };
6705
6975
  }
@@ -6874,7 +7144,8 @@ async function runDoctor(targetDir, options = {}, assetsDir) {
6874
7144
  checkUserTier(resolveHomeDir(), options.packageVersion),
6875
7145
  checkSpellScope(targetDir),
6876
7146
  checkGovernancePlaceholders(targetDir),
6877
- checkComponentRequires(targetDir)
7147
+ checkComponentRequires(targetDir),
7148
+ checkNewlyAvailableComponents(targetDir)
6878
7149
  ]);
6879
7150
  const sessionResults = await checkSessionContinuity(targetDir);
6880
7151
  results.push(...sessionResults);
@@ -6882,8 +7153,8 @@ async function runDoctor(targetDir, options = {}, assetsDir) {
6882
7153
  for (const result of results) {
6883
7154
  const skipped = result.passed && result.skipped === true;
6884
7155
  const icon = skipped ? "\u2013" : result.passed ? "\u2713" : result.blocking === false ? "\u26A0" : "\u2717";
6885
- const label = skipped ? "skip" : result.passed ? "pass" : result.blocking === false ? "warn" : "FAIL";
6886
- console.log(` ${icon} [${label}] ${result.name}`);
7156
+ const label2 = skipped ? "skip" : result.passed ? "pass" : result.blocking === false ? "warn" : "FAIL";
7157
+ console.log(` ${icon} [${label2}] ${result.name}`);
6887
7158
  if (!result.passed || skipped) {
6888
7159
  console.log(` ${result.message}`);
6889
7160
  }
@@ -6918,16 +7189,16 @@ async function runDoctor(targetDir, options = {}, assetsDir) {
6918
7189
  }
6919
7190
 
6920
7191
  // src/commands/ward.ts
6921
- import { readFile as readFile13 } from "fs/promises";
7192
+ import { readFile as readFile14 } from "fs/promises";
6922
7193
 
6923
7194
  // src/modules/ward.ts
6924
- import { readdir as readdir10, readFile as readFile12 } from "fs/promises";
7195
+ import { readdir as readdir10, readFile as readFile13 } from "fs/promises";
6925
7196
  import { join as join16, relative as relative2 } from "path";
6926
7197
  async function scanFileAllRules(absolutePath, displayPath, rules) {
6927
7198
  const findings = [];
6928
7199
  let content;
6929
7200
  try {
6930
- content = await readFile12(absolutePath, "utf8");
7201
+ content = await readFile13(absolutePath, "utf8");
6931
7202
  } catch {
6932
7203
  return findings;
6933
7204
  }
@@ -7022,7 +7293,7 @@ async function runWard(repoRoot, options = {}) {
7022
7293
  const findings2 = await scanFileAllRules(absolutePath, displayPath, allRules);
7023
7294
  if (findings2.length > 0) {
7024
7295
  try {
7025
- fileLines.set(displayPath, (await readFile12(absolutePath, "utf8")).split("\n"));
7296
+ fileLines.set(displayPath, (await readFile13(absolutePath, "utf8")).split("\n"));
7026
7297
  } catch {
7027
7298
  }
7028
7299
  }
@@ -7050,7 +7321,7 @@ function parseTermsFileContent(raw) {
7050
7321
  }
7051
7322
  async function readTermsFile(filePath) {
7052
7323
  try {
7053
- return parseTermsFileContent(await readFile13(filePath, "utf8"));
7324
+ return parseTermsFileContent(await readFile14(filePath, "utf8"));
7054
7325
  } catch (error) {
7055
7326
  console.warn(
7056
7327
  `\u26A0 arcane: could not read --terms-file "${filePath}": ${error.message}`
@@ -7265,11 +7536,11 @@ import { access as access5 } from "fs/promises";
7265
7536
  import { join as join19 } from "path";
7266
7537
 
7267
7538
  // src/modules/show-report/generate.ts
7268
- import { mkdir as mkdir8, readdir as readdir11, readFile as readFile15, writeFile as writeFile11 } from "fs/promises";
7539
+ import { mkdir as mkdir8, readdir as readdir11, readFile as readFile16, writeFile as writeFile12 } from "fs/promises";
7269
7540
  import { basename as basename3, dirname as dirname10, isAbsolute, join as join18, relative as relative3, resolve as resolve4 } from "path";
7270
7541
 
7271
7542
  // src/modules/show-report/model.ts
7272
- import { readFile as readFile14 } from "fs/promises";
7543
+ import { readFile as readFile15 } from "fs/promises";
7273
7544
  import { join as join17 } from "path";
7274
7545
 
7275
7546
  // src/modules/show-report/plan-parser.ts
@@ -7543,7 +7814,7 @@ async function getCast(cwd, fromRef, toRef, generatedOutputs = []) {
7543
7814
  // src/modules/show-report/model.ts
7544
7815
  async function readOptional(path4) {
7545
7816
  try {
7546
- return await readFile14(path4, "utf8");
7817
+ return await readFile15(path4, "utf8");
7547
7818
  } catch {
7548
7819
  return null;
7549
7820
  }
@@ -7645,7 +7916,7 @@ async function buildShowReportModel(options) {
7645
7916
  const ledgerAbsPath = join17(options.rootDir, ledgerRelPath);
7646
7917
  const decisionsAbsPath = join17(options.rootDir, "DECISIONS.md");
7647
7918
  const [planContent, queueContent, ledgerContent, decisionsContent] = await Promise.all([
7648
- readFile14(planAbsPath, "utf8"),
7919
+ readFile15(planAbsPath, "utf8"),
7649
7920
  readOptional(queueAbsPath),
7650
7921
  readOptional(ledgerAbsPath),
7651
7922
  readOptional(decisionsAbsPath)
@@ -7841,7 +8112,7 @@ function toPosix(path4) {
7841
8112
  async function programFromPlan(rootDir, slug, planRelPath) {
7842
8113
  let planContent;
7843
8114
  try {
7844
- planContent = await readFile15(join18(rootDir, planRelPath), "utf8");
8115
+ planContent = await readFile16(join18(rootDir, planRelPath), "utf8");
7845
8116
  } catch {
7846
8117
  return { skipped: { slug, reason: `no PLAN.md at ${planRelPath}` } };
7847
8118
  }
@@ -7886,14 +8157,14 @@ function normalizeLineEndings2(content) {
7886
8157
  }
7887
8158
  async function readOptionalFile(path4) {
7888
8159
  try {
7889
- return await readFile15(path4, "utf8");
8160
+ return await readFile16(path4, "utf8");
7890
8161
  } catch (error) {
7891
8162
  if (error.code === "ENOENT") return null;
7892
8163
  throw error;
7893
8164
  }
7894
8165
  }
7895
8166
  async function generateReport(rootDir, program2, templateHtml, theme = "auto") {
7896
- const planContent = await readFile15(join18(rootDir, program2.planRelPath), "utf8");
8167
+ const planContent = await readFile16(join18(rootDir, program2.planRelPath), "utf8");
7897
8168
  const frontmatter = parseFrontmatter(planContent);
7898
8169
  const model = await buildShowReportModel({
7899
8170
  rootDir,
@@ -7913,7 +8184,7 @@ async function generateReport(rootDir, program2, templateHtml, theme = "auto") {
7913
8184
  };
7914
8185
  }
7915
8186
  async function runReportGeneration(options) {
7916
- const templateHtml = await readFile15(options.templatePath, "utf8");
8187
+ const templateHtml = await readFile16(options.templatePath, "utf8");
7917
8188
  const theme = options.theme ?? "auto";
7918
8189
  const drifted = [];
7919
8190
  const repaired = [];
@@ -7936,7 +8207,7 @@ async function runReportGeneration(options) {
7936
8207
  drifted.push(`${relPath} does not match a regeneration from its sources.`);
7937
8208
  if (options.mode === "write") {
7938
8209
  await mkdir8(outAbs, { recursive: true });
7939
- await writeFile11(join18(options.rootDir, relPath), expected, "utf8");
8210
+ await writeFile12(join18(options.rootDir, relPath), expected, "utf8");
7940
8211
  repaired.push(relPath);
7941
8212
  }
7942
8213
  }
@@ -8084,10 +8355,10 @@ withManifestFlags(
8084
8355
  );
8085
8356
  }
8086
8357
  );
8087
- program.command("add <component>").description("Add a component to an existing Arcane installation").option("--force", "Overwrite existing files without error").option("--dry-run", "Preview what would be added without making changes").action(
8088
- async (component, opts) => {
8358
+ program.command("add <components...>").description("Add one or more components to an existing Arcane installation, in order; stops at the first failure").option("--force", "Overwrite existing files without error").option("--dry-run", "Preview what would be added without making changes").action(
8359
+ async (components, opts) => {
8089
8360
  await runAdd(
8090
- component,
8361
+ components,
8091
8362
  { force: opts.force, dryRun: opts.dryRun },
8092
8363
  process.cwd(),
8093
8364
  ASSETS_DIR,
@@ -8100,6 +8371,9 @@ withManifestFlags(
8100
8371
  "--prune",
8101
8372
  "Also delete orphaned managed files (tracked before this update, no longer part of any current component). Hash-checked: an edited file is reported, never silently deleted"
8102
8373
  ).option("--user", USER_FLAG_DESCRIPTION).option(
8374
+ "--add-new",
8375
+ "Also install the components your profile gained since your install (never an initOnly component, never over an existing file). Without it a terminal asks and a script installs nothing"
8376
+ ).option(
8103
8377
  "--default-scope <scope>",
8104
8378
  "With --user: remember repo | user as the answer `spell init` pre-selects for NEW repositories on this machine. Never rewrites an existing repository, and the question is still asked"
8105
8379
  )
@@ -8123,6 +8397,7 @@ withManifestFlags(
8123
8397
  dryRun: opts.dryRun,
8124
8398
  prune: opts.prune,
8125
8399
  user: opts.user,
8400
+ addNew: opts.addNew,
8126
8401
  manifestFlags: manifestFlagsFrom(opts),
8127
8402
  ...opts.defaultScope === void 0 ? {} : { defaultScope: opts.defaultScope }
8128
8403
  },