arcane-cli 1.4.0 → 1.5.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 +10 -0
- package/dist/assets/.arcane/governance/portable-bootstrap.md +1 -1
- package/dist/index.js +209 -87
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -263,6 +263,16 @@ spell doctor # fails loudly if the user tier it now depends on is missi
|
|
|
263
263
|
|
|
264
264
|
This is what removes the duplication: with the repository's own copies gone, one workspace holding ten Arcane projects shows one set of `/spell-*` entries, not ten. It is also what makes the answer predictable — while both tiers own a command name, which copy a client runs is not something you can read off the docs.
|
|
265
265
|
|
|
266
|
+
**Tired of answering that question in every new repository?** Tell the machine once:
|
|
267
|
+
|
|
268
|
+
```bash
|
|
269
|
+
spell update --user --default-scope user # or repo, to go back
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
That pre-selects the answer `spell init` offers in **new** repositories on this machine. It rewrites nothing that already exists, and the question is still asked, so a repository you want to keep self-contained is one keystroke away. An absent `spell_scope` means `repo` permanently, on every machine ([ARC-048](DECISIONS.md#arc-048--an-absent-spell_scope-means-repo-permanently-new-repositories-may-be-pre-selected)) — a preference decides what you are *offered*, never what an existing repository *means*.
|
|
273
|
+
|
|
274
|
+
If a repository that opted in ever lands on a machine with no tier, `spell update` there now says so, names the store path, and gives you both ways out.
|
|
275
|
+
|
|
266
276
|
**Shared, team and cloud repositories should stay `repo`.** The field is committed and inherited by every clone, so opting in tells *everyone* who clones that repository to get their spells from a machine-wide install they may not have. Reverse it by setting the field back and running `spell update`, which reinstalls every spell file.
|
|
267
277
|
|
|
268
278
|
---
|
|
@@ -52,7 +52,7 @@ Paste this entire file into any AI client (Claude web, ChatGPT, etc.) to give it
|
|
|
52
52
|
## Where Documents Live
|
|
53
53
|
|
|
54
54
|
- **Framework-managed standards:** `.arcane/governance/`. Spells reference this single installed layer; **do not create a duplicate root `governance/` tree. Enforcement: explicitly advisory prose (ARC-023) — no check scans a repo for a duplicate root-level `governance/` directory.**
|
|
55
|
-
- **Framework-managed spells:** `.arcane/spells/<id>.md` is each spell's one authored source (ARC-045). The files each AI client discovers — `.github/prompts/<id>.prompt.md` for Copilot, `.claude/commands/<id>.md` for Claude Code, `.agents/skills/<id>/SKILL.md` for Codex — are thin generated shims that point back at it; edit the canonical file, never a shim. **Enforcement: executable check (ARC-023) — `npm run check:self-host-parity` re-renders every shim from its canonical file and fails on any difference; `spell update` keeps a hand-edited shim untouched and names it rather than merging into it.** The same canonical spells can also be installed once per machine — `spell init --user` puts them at `~/.arcane/spells/<id>.md` with one Codex/Copilot skill under `~/.agents/skills/` and one Claude Code command under `~/.claude/commands/` per spell (ARC-045 decision 3); governance never moves up a tier. The agent roster follows the same shape (ARC-047): `spell agents init|sync --user` keeps `agents.yaml` and the role definitions at `~/.arcane` and renders one `~/.claude/agents/<name>.md` per agent -- the one home location read by both VS Code and Claude Code, so a persona shows up once in each rather than twice in one (ARC-047 decisions 3 and 11). Agent files are never installed by `spell init`/`spell update` at either tier -- they are rendered from the roster, whose naming strategy picks each file's name, which is why an upgrade never touches them. A repository may then opt out of carrying its own copies entirely with `spell_scope: "user"` in `.arcane.json` (ARC-045 decision 4) — `spell update --prune` removes what it no longer manages, keeping anything you edited, and `spell doctor` fails loudly if the tier it now depends on is missing.
|
|
55
|
+
- **Framework-managed spells:** `.arcane/spells/<id>.md` is each spell's one authored source (ARC-045). The files each AI client discovers — `.github/prompts/<id>.prompt.md` for Copilot, `.claude/commands/<id>.md` for Claude Code, `.agents/skills/<id>/SKILL.md` for Codex — are thin generated shims that point back at it; edit the canonical file, never a shim. **Enforcement: executable check (ARC-023) — `npm run check:self-host-parity` re-renders every shim from its canonical file and fails on any difference; `spell update` keeps a hand-edited shim untouched and names it rather than merging into it.** The same canonical spells can also be installed once per machine — `spell init --user` puts them at `~/.arcane/spells/<id>.md` with one Codex/Copilot skill under `~/.agents/skills/` and one Claude Code command under `~/.claude/commands/` per spell (ARC-045 decision 3); governance never moves up a tier. The agent roster follows the same shape (ARC-047): `spell agents init|sync --user` keeps `agents.yaml` and the role definitions at `~/.arcane` and renders one `~/.claude/agents/<name>.md` per agent -- the one home location read by both VS Code and Claude Code, so a persona shows up once in each rather than twice in one (ARC-047 decisions 3 and 11). Agent files are never installed by `spell init`/`spell update` at either tier -- they are rendered from the roster, whose naming strategy picks each file's name, which is why an upgrade never touches them. An absent `spell_scope` means `repo` permanently (ARC-048); a machine may set `default_spell_scope` in its own store to pre-select the answer `spell init` offers in NEW repositories, which never rewrites an existing one and never suppresses the question. A repository may then opt out of carrying its own copies entirely with `spell_scope: "user"` in `.arcane.json` (ARC-045 decision 4) — `spell update --prune` removes what it no longer manages, keeping anything you edited, and `spell doctor` fails loudly if the tier it now depends on is missing.
|
|
56
56
|
- **Project-owned orientation and continuity:** `README.md`, `project.md`, `TODO.md`, `DECISIONS.md`, `ai-context/`, and `journal/`. Arcane creates missing files once and preserves existing content.
|
|
57
57
|
- **Project/domain documents:** use explicit descriptive paths such as `docs/`, `security/`, `infrastructure/`, or a configured business root. These add project context alongside framework standards.
|
|
58
58
|
- **Overrides survive `arcane update`:** editing a managed `.arcane/governance/` standard no longer risks silent loss. Additive project documents were always safe; a hand-edit to a shipped standard now is too. **Enforcement: executable check (ARC-038, shipped BC-31)** — `spell update` compares each file's on-disk content against its recorded hash at install time: unchanged, it refreshes normally; edited, it fetches the exact previously-installed published version as a three-way merge base against your edit and the new vendor content, auto-merging non-overlapping changes and leaving standard conflict markers only on genuine collisions. If the prior published version can't be fetched, your edit is left untouched with a warning rather than replaced.
|
package/dist/index.js
CHANGED
|
@@ -262,7 +262,16 @@ async function resolveSecretsScanExcludePrefixes(targetDir) {
|
|
|
262
262
|
}
|
|
263
263
|
async function writeManifest(targetDir, manifest) {
|
|
264
264
|
const filePath = manifestPath(targetDir);
|
|
265
|
-
|
|
265
|
+
let existing = "";
|
|
266
|
+
try {
|
|
267
|
+
existing = await readFile2(filePath, "utf-8");
|
|
268
|
+
} catch {
|
|
269
|
+
existing = "";
|
|
270
|
+
}
|
|
271
|
+
const eol = existing.includes("\r\n") ? "\r\n" : "\n";
|
|
272
|
+
const trailingNewline = /\r?\n$/.test(existing) ? eol : "";
|
|
273
|
+
const body = JSON.stringify(manifest, null, 2).replaceAll("\n", eol);
|
|
274
|
+
await writeFile2(filePath, body + trailingNewline, "utf-8");
|
|
266
275
|
}
|
|
267
276
|
function addComponent(manifest, component) {
|
|
268
277
|
return {
|
|
@@ -1025,6 +1034,9 @@ function componentForSpellScope(component, spellScope) {
|
|
|
1025
1034
|
function effectiveSpellScope(manifest) {
|
|
1026
1035
|
return manifest.spell_scope ?? "repo";
|
|
1027
1036
|
}
|
|
1037
|
+
function preferredSpellScope(manifest) {
|
|
1038
|
+
return manifest?.default_spell_scope === "user" ? "user" : "repo";
|
|
1039
|
+
}
|
|
1028
1040
|
function spellIdsInStore(components) {
|
|
1029
1041
|
const ids = /* @__PURE__ */ new Set();
|
|
1030
1042
|
for (const component of components) {
|
|
@@ -3469,15 +3481,18 @@ async function runInit(options, targetDir, assetsDir, packageVersion) {
|
|
|
3469
3481
|
let spell_scope;
|
|
3470
3482
|
if (!options.profile && !options.dryRun) {
|
|
3471
3483
|
let storeVersion;
|
|
3484
|
+
let preferred = "repo";
|
|
3472
3485
|
try {
|
|
3473
|
-
|
|
3486
|
+
const store = await readManifest(userTierRoot());
|
|
3487
|
+
storeVersion = store.version;
|
|
3488
|
+
preferred = preferredSpellScope(store);
|
|
3474
3489
|
} catch {
|
|
3475
3490
|
}
|
|
3476
3491
|
if (storeVersion !== void 0) {
|
|
3477
3492
|
console.log();
|
|
3478
3493
|
const useTier = await confirm({
|
|
3479
3494
|
message: `This machine has an Arcane user tier (v${storeVersion}). Use it for this repository's spells instead of installing copies here?`,
|
|
3480
|
-
default:
|
|
3495
|
+
default: preferred === "user"
|
|
3481
3496
|
});
|
|
3482
3497
|
spell_scope = useTier ? "user" : "repo";
|
|
3483
3498
|
}
|
|
@@ -4319,9 +4334,37 @@ async function fetchPublishedFile(version, relativeAssetPath) {
|
|
|
4319
4334
|
}
|
|
4320
4335
|
|
|
4321
4336
|
// src/commands/update.ts
|
|
4337
|
+
async function missingTierWarning(scope, spellScope, homeDir) {
|
|
4338
|
+
if (scope !== "repo" || spellScope !== "user") return [];
|
|
4339
|
+
const storeRoot = homeDir === void 0 ? userTierRoot() : userTierRoot(homeDir);
|
|
4340
|
+
try {
|
|
4341
|
+
await readManifest(storeRoot);
|
|
4342
|
+
return [];
|
|
4343
|
+
} catch {
|
|
4344
|
+
return [
|
|
4345
|
+
"",
|
|
4346
|
+
` ! This repository takes its spells from the user tier, and ${storeRoot} is not installed.`,
|
|
4347
|
+
" No client can reach a spell here: the routing block in CLAUDE.md and AGENTS.md still",
|
|
4348
|
+
" names spells that resolve to nothing, so an agent improvises the workflow instead of",
|
|
4349
|
+
" failing. Fix it with one of:",
|
|
4350
|
+
"",
|
|
4351
|
+
" spell init --user # install the tier on this machine",
|
|
4352
|
+
' # or set "spell_scope": "repo" in .arcane.json and re-run `spell update`',
|
|
4353
|
+
""
|
|
4354
|
+
];
|
|
4355
|
+
}
|
|
4356
|
+
}
|
|
4357
|
+
function defaultScopePatch(options, manifest) {
|
|
4358
|
+
if (options.defaultScope === void 0) return void 0;
|
|
4359
|
+
if (manifest.default_spell_scope === options.defaultScope) return void 0;
|
|
4360
|
+
return { default_spell_scope: options.defaultScope };
|
|
4361
|
+
}
|
|
4322
4362
|
function migrateLegacyComponents(components) {
|
|
4323
4363
|
const out = [];
|
|
4324
4364
|
const seen = /* @__PURE__ */ new Set();
|
|
4365
|
+
const inheritedFiles = [];
|
|
4366
|
+
const inheritedHashes = {};
|
|
4367
|
+
let carrier = null;
|
|
4325
4368
|
for (const entry of components) {
|
|
4326
4369
|
const replacements = LEGACY_COMPONENT_MIGRATIONS[entry.name];
|
|
4327
4370
|
if (!replacements) {
|
|
@@ -4330,13 +4373,24 @@ function migrateLegacyComponents(components) {
|
|
|
4330
4373
|
out.push(entry);
|
|
4331
4374
|
continue;
|
|
4332
4375
|
}
|
|
4376
|
+
for (const file of entry.files) {
|
|
4377
|
+
if (!inheritedFiles.includes(file)) inheritedFiles.push(file);
|
|
4378
|
+
const hash = entry.fileHashes?.[file];
|
|
4379
|
+
if (hash !== void 0 && inheritedHashes[file] === void 0) {
|
|
4380
|
+
inheritedHashes[file] = hash;
|
|
4381
|
+
}
|
|
4382
|
+
}
|
|
4333
4383
|
for (const name of replacements) {
|
|
4384
|
+
carrier ??= name;
|
|
4334
4385
|
if (seen.has(name)) continue;
|
|
4335
4386
|
seen.add(name);
|
|
4336
4387
|
out.push({ name, files: [], installedVersion: entry.installedVersion });
|
|
4337
4388
|
}
|
|
4338
4389
|
}
|
|
4339
|
-
return
|
|
4390
|
+
return {
|
|
4391
|
+
components: out,
|
|
4392
|
+
inherited: { files: inheritedFiles, fileHashes: inheritedHashes, carrier }
|
|
4393
|
+
};
|
|
4340
4394
|
}
|
|
4341
4395
|
async function resolveOrphan(targetDir, file, recordedHash, prune) {
|
|
4342
4396
|
try {
|
|
@@ -4361,6 +4415,19 @@ async function resolveOrphan(targetDir, file, recordedHash, prune) {
|
|
|
4361
4415
|
console.log(` Pruned orphaned file: ${file}`);
|
|
4362
4416
|
return "pruned";
|
|
4363
4417
|
}
|
|
4418
|
+
async function retainReportedOrphan(targetDir, file, recordedHash, into) {
|
|
4419
|
+
try {
|
|
4420
|
+
validateTargetPath(targetDir, file);
|
|
4421
|
+
} catch {
|
|
4422
|
+
return;
|
|
4423
|
+
}
|
|
4424
|
+
if (!await fileExists(join9(targetDir, file))) return;
|
|
4425
|
+
if (!into.files.includes(file)) into.files.push(file);
|
|
4426
|
+
if (recordedHash !== void 0) {
|
|
4427
|
+
into.fileHashes ??= {};
|
|
4428
|
+
into.fileHashes[file] = recordedHash;
|
|
4429
|
+
}
|
|
4430
|
+
}
|
|
4364
4431
|
function findReadoptedSpellComponents(components, scope = "repo", spellScope = "repo") {
|
|
4365
4432
|
if (scope !== "repo" || spellScope !== "repo") return /* @__PURE__ */ new Set();
|
|
4366
4433
|
return new Set(
|
|
@@ -4432,6 +4499,7 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
|
|
|
4432
4499
|
const spellScope = effectiveSpellScope(manifest);
|
|
4433
4500
|
const readopted = findReadoptedSpellComponents(manifest.components, scope, spellScope);
|
|
4434
4501
|
const homeDir = dirname5(targetDir);
|
|
4502
|
+
for (const line of await missingTierWarning(scope, spellScope)) console.log(line);
|
|
4435
4503
|
if (scope === "repo") {
|
|
4436
4504
|
console.warn(
|
|
4437
4505
|
`WARNING: Arcane v${packageVersion} update safety notice: commit your work before updating.`
|
|
@@ -4479,8 +4547,9 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
|
|
|
4479
4547
|
fallbackDir: assetsDir
|
|
4480
4548
|
});
|
|
4481
4549
|
for (const line of describeFanoutOutcomes(fanout.outcomes, options.dryRun)) console.log(` ${line}`);
|
|
4482
|
-
|
|
4483
|
-
|
|
4550
|
+
const scopePatch = defaultScopePatch(options, manifest);
|
|
4551
|
+
if (!options.dryRun && (JSON.stringify(fanout.record) !== JSON.stringify(manifest.fanout ?? {}) || scopePatch !== void 0)) {
|
|
4552
|
+
await writeManifest(targetDir, { ...manifest, ...scopePatch, fanout: fanout.record });
|
|
4484
4553
|
}
|
|
4485
4554
|
}
|
|
4486
4555
|
return;
|
|
@@ -4506,7 +4575,17 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
|
|
|
4506
4575
|
const conflictedFiles = [];
|
|
4507
4576
|
const customizedShims = [];
|
|
4508
4577
|
const orphanReport = [];
|
|
4509
|
-
const
|
|
4578
|
+
const orphanCandidates = [];
|
|
4579
|
+
const { components: componentsToUpdate, inherited } = migrateLegacyComponents(manifest.components);
|
|
4580
|
+
if (inherited.carrier !== null) {
|
|
4581
|
+
for (const file of inherited.files) {
|
|
4582
|
+
orphanCandidates.push({
|
|
4583
|
+
owner: inherited.carrier,
|
|
4584
|
+
file,
|
|
4585
|
+
recordedHash: inherited.fileHashes[file]
|
|
4586
|
+
});
|
|
4587
|
+
}
|
|
4588
|
+
}
|
|
4510
4589
|
for (const installed of componentsToUpdate) {
|
|
4511
4590
|
let component;
|
|
4512
4591
|
try {
|
|
@@ -4625,24 +4704,11 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
|
|
|
4625
4704
|
}
|
|
4626
4705
|
for (const file of installed.files) {
|
|
4627
4706
|
if (updatedFiles.includes(file)) continue;
|
|
4628
|
-
|
|
4629
|
-
|
|
4630
|
-
targetDir,
|
|
4707
|
+
orphanCandidates.push({
|
|
4708
|
+
owner: installed.name,
|
|
4631
4709
|
file,
|
|
4632
|
-
recordedHash
|
|
4633
|
-
|
|
4634
|
-
);
|
|
4635
|
-
orphanReport.push({ file, status });
|
|
4636
|
-
if (status === "reported") {
|
|
4637
|
-
try {
|
|
4638
|
-
validateTargetPath(targetDir, file);
|
|
4639
|
-
if (await fileExists(join9(targetDir, file))) {
|
|
4640
|
-
updatedFiles.push(file);
|
|
4641
|
-
if (recordedHash !== void 0) fileHashes[file] = recordedHash;
|
|
4642
|
-
}
|
|
4643
|
-
} catch {
|
|
4644
|
-
}
|
|
4645
|
-
}
|
|
4710
|
+
recordedHash: installed.fileHashes?.[file]
|
|
4711
|
+
});
|
|
4646
4712
|
}
|
|
4647
4713
|
updatedComponents.push({
|
|
4648
4714
|
...installed,
|
|
@@ -4651,6 +4717,26 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
|
|
|
4651
4717
|
fileHashes
|
|
4652
4718
|
});
|
|
4653
4719
|
}
|
|
4720
|
+
const claimed = new Set(updatedComponents.flatMap((c) => c.files));
|
|
4721
|
+
const resolvedOrphans = /* @__PURE__ */ new Set();
|
|
4722
|
+
for (const { owner, file, recordedHash } of orphanCandidates) {
|
|
4723
|
+
if (claimed.has(file) || resolvedOrphans.has(file)) continue;
|
|
4724
|
+
resolvedOrphans.add(file);
|
|
4725
|
+
const status = await resolveOrphan(
|
|
4726
|
+
targetDir,
|
|
4727
|
+
file,
|
|
4728
|
+
recordedHash,
|
|
4729
|
+
Boolean(options.prune) && !options.dryRun
|
|
4730
|
+
);
|
|
4731
|
+
orphanReport.push({ file, status });
|
|
4732
|
+
if (status !== "reported") continue;
|
|
4733
|
+
let into = updatedComponents.find((c) => c.name === owner);
|
|
4734
|
+
if (!into) {
|
|
4735
|
+
into = { name: owner, files: [], installedVersion: packageVersion, fileHashes: {} };
|
|
4736
|
+
updatedComponents.push(into);
|
|
4737
|
+
}
|
|
4738
|
+
await retainReportedOrphan(targetDir, file, recordedHash, into);
|
|
4739
|
+
}
|
|
4654
4740
|
const orphansToReport = orphanReport.filter((o) => o.status !== "not-found");
|
|
4655
4741
|
if (orphansToReport.length > 0) {
|
|
4656
4742
|
const verb = options.dryRun ? "[dry-run] Found" : "Found";
|
|
@@ -4726,7 +4812,8 @@ ${verb} ${orphansToReport.length} orphaned file(s) (tracked before this update,
|
|
|
4726
4812
|
version: packageVersion,
|
|
4727
4813
|
components: updatedComponents,
|
|
4728
4814
|
...retrofitPatch,
|
|
4729
|
-
...fanoutRecord !== void 0 ? { fanout: fanoutRecord } : {}
|
|
4815
|
+
...fanoutRecord !== void 0 ? { fanout: fanoutRecord } : {},
|
|
4816
|
+
...defaultScopePatch(options, manifest)
|
|
4730
4817
|
};
|
|
4731
4818
|
await writeManifest(targetDir, updated);
|
|
4732
4819
|
console.log(
|
|
@@ -5155,8 +5242,6 @@ async function uninstallUserTier(options, storeRoot, manifest) {
|
|
|
5155
5242
|
// src/commands/doctor.ts
|
|
5156
5243
|
import { access as access4, readdir as readdir8, readFile as readFile10, mkdir as mkdir7, copyFile as fsCopyFile } from "fs/promises";
|
|
5157
5244
|
import { join as join13, dirname as dirname9 } from "path";
|
|
5158
|
-
import { execFile as execFile3 } from "child_process";
|
|
5159
|
-
import { promisify as promisify2 } from "util";
|
|
5160
5245
|
|
|
5161
5246
|
// src/config/incidents.ts
|
|
5162
5247
|
var INCIDENT_QUEUE = [
|
|
@@ -5297,10 +5382,33 @@ async function scanRepository(repoRoot, rules, extensions, excludePrefixes = [])
|
|
|
5297
5382
|
return findings;
|
|
5298
5383
|
}
|
|
5299
5384
|
|
|
5300
|
-
// src/modules/
|
|
5385
|
+
// src/modules/exec.ts
|
|
5301
5386
|
import { execFile as execFile2 } from "child_process";
|
|
5302
|
-
|
|
5303
|
-
|
|
5387
|
+
var EXTERNAL_CLI_TIMEOUT_MS = 1e4;
|
|
5388
|
+
function execFileWithTimeout(file, args, timeoutMs) {
|
|
5389
|
+
return new Promise((resolve5, reject) => {
|
|
5390
|
+
const child = execFile2(
|
|
5391
|
+
file,
|
|
5392
|
+
args,
|
|
5393
|
+
{
|
|
5394
|
+
timeout: timeoutMs,
|
|
5395
|
+
killSignal: "SIGTERM",
|
|
5396
|
+
windowsHide: true,
|
|
5397
|
+
encoding: "utf8"
|
|
5398
|
+
},
|
|
5399
|
+
(error, stdout, stderr) => {
|
|
5400
|
+
if (error) {
|
|
5401
|
+
reject(error);
|
|
5402
|
+
return;
|
|
5403
|
+
}
|
|
5404
|
+
resolve5({ stdout, stderr });
|
|
5405
|
+
}
|
|
5406
|
+
);
|
|
5407
|
+
child.stdin?.end();
|
|
5408
|
+
});
|
|
5409
|
+
}
|
|
5410
|
+
|
|
5411
|
+
// src/modules/platform-policy.ts
|
|
5304
5412
|
var SANCTIONED_MERGE_METHODS = ["merge", "rebase"];
|
|
5305
5413
|
function compareToLadder(effective, declaredButBlocked, interactionNote) {
|
|
5306
5414
|
const disallowedButPermitted = effective.filter(
|
|
@@ -5367,17 +5475,19 @@ function evaluateGitHubMergePolicy(rulesets) {
|
|
|
5367
5475
|
}
|
|
5368
5476
|
async function fetchGitHubRulesets(owner, repo) {
|
|
5369
5477
|
try {
|
|
5370
|
-
const { stdout: listOut } = await
|
|
5371
|
-
"
|
|
5372
|
-
`repos/${owner}/${repo}/rulesets`
|
|
5373
|
-
|
|
5478
|
+
const { stdout: listOut } = await execFileWithTimeout(
|
|
5479
|
+
"gh",
|
|
5480
|
+
["api", `repos/${owner}/${repo}/rulesets`],
|
|
5481
|
+
EXTERNAL_CLI_TIMEOUT_MS
|
|
5482
|
+
);
|
|
5374
5483
|
const summaries = JSON.parse(listOut);
|
|
5375
5484
|
const detailed = await Promise.all(
|
|
5376
5485
|
summaries.map(async (s) => {
|
|
5377
|
-
const { stdout } = await
|
|
5378
|
-
"
|
|
5379
|
-
`repos/${owner}/${repo}/rulesets/${s.id}`
|
|
5380
|
-
|
|
5486
|
+
const { stdout } = await execFileWithTimeout(
|
|
5487
|
+
"gh",
|
|
5488
|
+
["api", `repos/${owner}/${repo}/rulesets/${s.id}`],
|
|
5489
|
+
EXTERNAL_CLI_TIMEOUT_MS
|
|
5490
|
+
);
|
|
5381
5491
|
return JSON.parse(stdout);
|
|
5382
5492
|
})
|
|
5383
5493
|
);
|
|
@@ -5409,7 +5519,7 @@ function evaluateAdoMergePolicy(policies) {
|
|
|
5409
5519
|
async function fetchAdoMergeTypePolicies(org, project, repoName, branch) {
|
|
5410
5520
|
try {
|
|
5411
5521
|
const orgUrl = `https://dev.azure.com/${org}`;
|
|
5412
|
-
const { stdout: repoOut } = await
|
|
5522
|
+
const { stdout: repoOut } = await execFileWithTimeout("az", [
|
|
5413
5523
|
"repos",
|
|
5414
5524
|
"show",
|
|
5415
5525
|
"--repository",
|
|
@@ -5422,10 +5532,10 @@ async function fetchAdoMergeTypePolicies(org, project, repoName, branch) {
|
|
|
5422
5532
|
"id",
|
|
5423
5533
|
"--output",
|
|
5424
5534
|
"tsv"
|
|
5425
|
-
]);
|
|
5535
|
+
], EXTERNAL_CLI_TIMEOUT_MS);
|
|
5426
5536
|
const repositoryId = repoOut.trim();
|
|
5427
5537
|
if (!repositoryId) return null;
|
|
5428
|
-
const { stdout } = await
|
|
5538
|
+
const { stdout } = await execFileWithTimeout("az", [
|
|
5429
5539
|
"repos",
|
|
5430
5540
|
"policy",
|
|
5431
5541
|
"list",
|
|
@@ -5439,7 +5549,7 @@ async function fetchAdoMergeTypePolicies(org, project, repoName, branch) {
|
|
|
5439
5549
|
project,
|
|
5440
5550
|
"--output",
|
|
5441
5551
|
"json"
|
|
5442
|
-
]);
|
|
5552
|
+
], EXTERNAL_CLI_TIMEOUT_MS);
|
|
5443
5553
|
return JSON.parse(stdout);
|
|
5444
5554
|
} catch {
|
|
5445
5555
|
return null;
|
|
@@ -5467,7 +5577,6 @@ function parseAdoRemote(url) {
|
|
|
5467
5577
|
}
|
|
5468
5578
|
|
|
5469
5579
|
// src/commands/doctor.ts
|
|
5470
|
-
var execFileAsync2 = promisify2(execFile3);
|
|
5471
5580
|
function isRecord3(value) {
|
|
5472
5581
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5473
5582
|
}
|
|
@@ -5484,40 +5593,33 @@ async function checkNodeVersion() {
|
|
|
5484
5593
|
message: `${raw} \u2014 Node.js 18 or later is required`
|
|
5485
5594
|
};
|
|
5486
5595
|
}
|
|
5487
|
-
async function checkVSCodeExtension(id, label) {
|
|
5596
|
+
async function checkVSCodeExtension(id, label, homeDir = resolveHomeDir()) {
|
|
5488
5597
|
const name = `VS Code extension: ${label}`;
|
|
5489
|
-
const
|
|
5490
|
-
|
|
5491
|
-
join13(
|
|
5492
|
-
join13(home, ".vscode-insiders", "extensions")
|
|
5598
|
+
const flavours = [
|
|
5599
|
+
{ dir: join13(homeDir, ".vscode", "extensions"), bin: "code" },
|
|
5600
|
+
{ dir: join13(homeDir, ".vscode-insiders", "extensions"), bin: "code-insiders" }
|
|
5493
5601
|
];
|
|
5494
|
-
|
|
5602
|
+
let installBin = null;
|
|
5603
|
+
for (const flavour of flavours) {
|
|
5604
|
+
let entries;
|
|
5495
5605
|
try {
|
|
5496
|
-
|
|
5497
|
-
const entries = await readdir11(dir);
|
|
5498
|
-
const prefix = id.toLowerCase() + "-";
|
|
5499
|
-
const found = entries.some(
|
|
5500
|
-
(e) => e.toLowerCase() === id.toLowerCase() || e.toLowerCase().startsWith(prefix)
|
|
5501
|
-
);
|
|
5502
|
-
if (found) {
|
|
5503
|
-
return { name, passed: true, message: `${id} is installed` };
|
|
5504
|
-
}
|
|
5606
|
+
entries = await readdir8(flavour.dir);
|
|
5505
5607
|
} catch {
|
|
5608
|
+
continue;
|
|
5506
5609
|
}
|
|
5507
|
-
|
|
5508
|
-
|
|
5509
|
-
|
|
5510
|
-
|
|
5511
|
-
|
|
5512
|
-
|
|
5513
|
-
|
|
5514
|
-
} catch {
|
|
5610
|
+
installBin ??= flavour.bin;
|
|
5611
|
+
const prefix = id.toLowerCase() + "-";
|
|
5612
|
+
const found = entries.some(
|
|
5613
|
+
(e) => e.toLowerCase() === id.toLowerCase() || e.toLowerCase().startsWith(prefix)
|
|
5614
|
+
);
|
|
5615
|
+
if (found) {
|
|
5616
|
+
return { name, passed: true, message: `${id} is installed` };
|
|
5515
5617
|
}
|
|
5516
5618
|
}
|
|
5517
5619
|
return {
|
|
5518
5620
|
name,
|
|
5519
5621
|
passed: false,
|
|
5520
|
-
message: `${id} not found. Install via: ${installBin} --install-extension ${id}`,
|
|
5622
|
+
message: `${id} not found. Install via: ${installBin ?? "code"} --install-extension ${id}`,
|
|
5521
5623
|
blocking: false
|
|
5522
5624
|
};
|
|
5523
5625
|
}
|
|
@@ -6003,7 +6105,7 @@ async function runDoctor(targetDir, options = {}, assetsDir) {
|
|
|
6003
6105
|
}
|
|
6004
6106
|
const results = await Promise.all([
|
|
6005
6107
|
checkNodeVersion(),
|
|
6006
|
-
checkVSCodeExtension("GitHub.copilot-chat", "GitHub Copilot (Chat)"),
|
|
6108
|
+
checkVSCodeExtension("GitHub.copilot-chat", "GitHub Copilot (Chat)", resolveHomeDir()),
|
|
6007
6109
|
checkArcaneManifest(targetDir),
|
|
6008
6110
|
Promise.resolve(checkIncidentReleaseGate()),
|
|
6009
6111
|
checkPullRebase(targetDir),
|
|
@@ -6033,26 +6135,24 @@ async function runDoctor(targetDir, options = {}, assetsDir) {
|
|
|
6033
6135
|
console.log(" All checks passed. Your environment is ready.\n");
|
|
6034
6136
|
} else {
|
|
6035
6137
|
console.log(" One or more checks failed. Fix the issues above before proceeding.\n");
|
|
6036
|
-
|
|
6037
|
-
|
|
6038
|
-
|
|
6039
|
-
|
|
6040
|
-
|
|
6041
|
-
|
|
6042
|
-
|
|
6043
|
-
|
|
6044
|
-
console.log(`
|
|
6138
|
+
process.exitCode = 1;
|
|
6139
|
+
}
|
|
6140
|
+
const hasMissingSession = sessionResults.some((r) => !r.passed);
|
|
6141
|
+
if (options.fix && hasMissingSession && assetsDir) {
|
|
6142
|
+
console.log(" \u{1F527} Fixing missing session continuity files...\n");
|
|
6143
|
+
const created = await fixSessionContinuity(targetDir, assetsDir);
|
|
6144
|
+
if (created.length > 0) {
|
|
6145
|
+
for (const file of created) {
|
|
6146
|
+
console.log(` \u2713 Created: ${file}`);
|
|
6147
|
+
}
|
|
6148
|
+
console.log(`
|
|
6045
6149
|
${created.length} file(s) created. Repo is now close-session-ready.
|
|
6046
6150
|
`);
|
|
6047
|
-
|
|
6048
|
-
|
|
6049
|
-
}
|
|
6050
|
-
} else if (hasMissingSession && !options.fix) {
|
|
6051
|
-
console.log(" \u{1F4A1} Run `spell doctor --fix` to create missing session files.\n");
|
|
6052
|
-
}
|
|
6053
|
-
if (!allPassed) {
|
|
6054
|
-
process.exitCode = 1;
|
|
6151
|
+
} else {
|
|
6152
|
+
console.log(" No files needed creation (all present).\n");
|
|
6055
6153
|
}
|
|
6154
|
+
} else if (hasMissingSession && !options.fix) {
|
|
6155
|
+
console.log(" \u{1F4A1} Run `spell doctor --fix` to create missing session files.\n");
|
|
6056
6156
|
}
|
|
6057
6157
|
}
|
|
6058
6158
|
|
|
@@ -7106,9 +7206,31 @@ program.command("add <component>").description("Add a component to an existing A
|
|
|
7106
7206
|
program.command("update").description("Update installed components to the current package version").option("--dry-run", "Preview what would be updated without making changes").option(
|
|
7107
7207
|
"--prune",
|
|
7108
7208
|
"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"
|
|
7109
|
-
).option("--user", USER_FLAG_DESCRIPTION).
|
|
7209
|
+
).option("--user", USER_FLAG_DESCRIPTION).option(
|
|
7210
|
+
"--default-scope <scope>",
|
|
7211
|
+
"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"
|
|
7212
|
+
).action(async (opts) => {
|
|
7213
|
+
if (opts.defaultScope !== void 0) {
|
|
7214
|
+
if (!opts.user) {
|
|
7215
|
+
console.error(
|
|
7216
|
+
"\n \u2717 --default-scope applies to the user tier only. It records a preference in\n ~/.arcane, not in a repository. Re-run it as:\n\n spell update --user --default-scope " + opts.defaultScope + "\n"
|
|
7217
|
+
);
|
|
7218
|
+
process.exit(1);
|
|
7219
|
+
}
|
|
7220
|
+
if (opts.defaultScope !== "repo" && opts.defaultScope !== "user") {
|
|
7221
|
+
console.error(
|
|
7222
|
+
"\n \u2717 --default-scope takes repo or user, not: " + opts.defaultScope + "\n"
|
|
7223
|
+
);
|
|
7224
|
+
process.exit(1);
|
|
7225
|
+
}
|
|
7226
|
+
}
|
|
7110
7227
|
await runUpdate(
|
|
7111
|
-
{
|
|
7228
|
+
{
|
|
7229
|
+
dryRun: opts.dryRun,
|
|
7230
|
+
prune: opts.prune,
|
|
7231
|
+
user: opts.user,
|
|
7232
|
+
...opts.defaultScope === void 0 ? {} : { defaultScope: opts.defaultScope }
|
|
7233
|
+
},
|
|
7112
7234
|
targetDirFor(opts),
|
|
7113
7235
|
ASSETS_DIR,
|
|
7114
7236
|
pkg.version
|