arcane-cli 1.4.0 → 1.5.0

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 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
@@ -1025,6 +1025,9 @@ function componentForSpellScope(component, spellScope) {
1025
1025
  function effectiveSpellScope(manifest) {
1026
1026
  return manifest.spell_scope ?? "repo";
1027
1027
  }
1028
+ function preferredSpellScope(manifest) {
1029
+ return manifest?.default_spell_scope === "user" ? "user" : "repo";
1030
+ }
1028
1031
  function spellIdsInStore(components) {
1029
1032
  const ids = /* @__PURE__ */ new Set();
1030
1033
  for (const component of components) {
@@ -3469,15 +3472,18 @@ async function runInit(options, targetDir, assetsDir, packageVersion) {
3469
3472
  let spell_scope;
3470
3473
  if (!options.profile && !options.dryRun) {
3471
3474
  let storeVersion;
3475
+ let preferred = "repo";
3472
3476
  try {
3473
- storeVersion = (await readManifest(userTierRoot())).version;
3477
+ const store = await readManifest(userTierRoot());
3478
+ storeVersion = store.version;
3479
+ preferred = preferredSpellScope(store);
3474
3480
  } catch {
3475
3481
  }
3476
3482
  if (storeVersion !== void 0) {
3477
3483
  console.log();
3478
3484
  const useTier = await confirm({
3479
3485
  message: `This machine has an Arcane user tier (v${storeVersion}). Use it for this repository's spells instead of installing copies here?`,
3480
- default: false
3486
+ default: preferred === "user"
3481
3487
  });
3482
3488
  spell_scope = useTier ? "user" : "repo";
3483
3489
  }
@@ -4319,6 +4325,31 @@ async function fetchPublishedFile(version, relativeAssetPath) {
4319
4325
  }
4320
4326
 
4321
4327
  // src/commands/update.ts
4328
+ async function missingTierWarning(scope, spellScope, homeDir) {
4329
+ if (scope !== "repo" || spellScope !== "user") return [];
4330
+ const storeRoot = homeDir === void 0 ? userTierRoot() : userTierRoot(homeDir);
4331
+ try {
4332
+ await readManifest(storeRoot);
4333
+ return [];
4334
+ } catch {
4335
+ return [
4336
+ "",
4337
+ ` ! This repository takes its spells from the user tier, and ${storeRoot} is not installed.`,
4338
+ " No client can reach a spell here: the routing block in CLAUDE.md and AGENTS.md still",
4339
+ " names spells that resolve to nothing, so an agent improvises the workflow instead of",
4340
+ " failing. Fix it with one of:",
4341
+ "",
4342
+ " spell init --user # install the tier on this machine",
4343
+ ' # or set "spell_scope": "repo" in .arcane.json and re-run `spell update`',
4344
+ ""
4345
+ ];
4346
+ }
4347
+ }
4348
+ function defaultScopePatch(options, manifest) {
4349
+ if (options.defaultScope === void 0) return void 0;
4350
+ if (manifest.default_spell_scope === options.defaultScope) return void 0;
4351
+ return { default_spell_scope: options.defaultScope };
4352
+ }
4322
4353
  function migrateLegacyComponents(components) {
4323
4354
  const out = [];
4324
4355
  const seen = /* @__PURE__ */ new Set();
@@ -4432,6 +4463,7 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
4432
4463
  const spellScope = effectiveSpellScope(manifest);
4433
4464
  const readopted = findReadoptedSpellComponents(manifest.components, scope, spellScope);
4434
4465
  const homeDir = dirname5(targetDir);
4466
+ for (const line of await missingTierWarning(scope, spellScope)) console.log(line);
4435
4467
  if (scope === "repo") {
4436
4468
  console.warn(
4437
4469
  `WARNING: Arcane v${packageVersion} update safety notice: commit your work before updating.`
@@ -4479,8 +4511,9 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
4479
4511
  fallbackDir: assetsDir
4480
4512
  });
4481
4513
  for (const line of describeFanoutOutcomes(fanout.outcomes, options.dryRun)) console.log(` ${line}`);
4482
- if (!options.dryRun && JSON.stringify(fanout.record) !== JSON.stringify(manifest.fanout ?? {})) {
4483
- await writeManifest(targetDir, { ...manifest, fanout: fanout.record });
4514
+ const scopePatch = defaultScopePatch(options, manifest);
4515
+ if (!options.dryRun && (JSON.stringify(fanout.record) !== JSON.stringify(manifest.fanout ?? {}) || scopePatch !== void 0)) {
4516
+ await writeManifest(targetDir, { ...manifest, ...scopePatch, fanout: fanout.record });
4484
4517
  }
4485
4518
  }
4486
4519
  return;
@@ -4726,7 +4759,8 @@ ${verb} ${orphansToReport.length} orphaned file(s) (tracked before this update,
4726
4759
  version: packageVersion,
4727
4760
  components: updatedComponents,
4728
4761
  ...retrofitPatch,
4729
- ...fanoutRecord !== void 0 ? { fanout: fanoutRecord } : {}
4762
+ ...fanoutRecord !== void 0 ? { fanout: fanoutRecord } : {},
4763
+ ...defaultScopePatch(options, manifest)
4730
4764
  };
4731
4765
  await writeManifest(targetDir, updated);
4732
4766
  console.log(
@@ -7106,9 +7140,31 @@ program.command("add <component>").description("Add a component to an existing A
7106
7140
  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
7141
  "--prune",
7108
7142
  "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).action(async (opts) => {
7143
+ ).option("--user", USER_FLAG_DESCRIPTION).option(
7144
+ "--default-scope <scope>",
7145
+ "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"
7146
+ ).action(async (opts) => {
7147
+ if (opts.defaultScope !== void 0) {
7148
+ if (!opts.user) {
7149
+ console.error(
7150
+ "\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"
7151
+ );
7152
+ process.exit(1);
7153
+ }
7154
+ if (opts.defaultScope !== "repo" && opts.defaultScope !== "user") {
7155
+ console.error(
7156
+ "\n \u2717 --default-scope takes repo or user, not: " + opts.defaultScope + "\n"
7157
+ );
7158
+ process.exit(1);
7159
+ }
7160
+ }
7110
7161
  await runUpdate(
7111
- { dryRun: opts.dryRun, prune: opts.prune, user: opts.user },
7162
+ {
7163
+ dryRun: opts.dryRun,
7164
+ prune: opts.prune,
7165
+ user: opts.user,
7166
+ ...opts.defaultScope === void 0 ? {} : { defaultScope: opts.defaultScope }
7167
+ },
7112
7168
  targetDirFor(opts),
7113
7169
  ASSETS_DIR,
7114
7170
  pkg.version