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/README.md +12 -0
- package/dist/index.js +378 -103
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
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(` ${
|
|
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(` ${
|
|
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
|
|
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: ${
|
|
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: ${
|
|
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 ${
|
|
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/
|
|
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
|
|
4413
|
-
|
|
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
|
-
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4444
|
-
|
|
4445
|
-
|
|
4446
|
-
|
|
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
|
-
|
|
4457
|
-
|
|
4458
|
-
|
|
4459
|
-
|
|
4460
|
-
|
|
4461
|
-
|
|
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 (
|
|
4470
|
-
|
|
4471
|
-
|
|
4472
|
-
|
|
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
|
|
4561
|
+
files,
|
|
4479
4562
|
installedVersion: packageVersion,
|
|
4480
4563
|
fileHashes
|
|
4481
4564
|
};
|
|
4482
|
-
|
|
4483
|
-
await writeManifest(targetDir, updated);
|
|
4565
|
+
await writeManifest(targetDir, addComponent(manifest, newComponent));
|
|
4484
4566
|
console.log(
|
|
4485
4567
|
`
|
|
4486
|
-
\u2713 Added component "${name}" \u2014 ${
|
|
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
|
|
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(
|
|
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\`
|
|
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)
|
|
5030
|
-
|
|
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
|
-
|
|
5165
|
-
|
|
5403
|
+
readFile8(join10(targetDir, file), "utf-8"),
|
|
5404
|
+
readFile8(srcPath, "utf-8")
|
|
5166
5405
|
]);
|
|
5167
5406
|
const result = merge3(oldVendorContent, currentContent, newContent);
|
|
5168
|
-
await
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
6280
|
-
const name = `VS Code extension: ${
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
6886
|
-
console.log(` ${icon} [${
|
|
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
|
|
7192
|
+
import { readFile as readFile14 } from "fs/promises";
|
|
6922
7193
|
|
|
6923
7194
|
// src/modules/ward.ts
|
|
6924
|
-
import { readdir as readdir10, readFile as
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 <
|
|
8088
|
-
async (
|
|
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
|
-
|
|
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
|
},
|