arcane-cli 1.11.5 → 1.12.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
@@ -6,11 +6,11 @@ const require = __createRequire(import.meta.url);
6
6
  import { Command } from "commander";
7
7
  import { createRequire as createRequire3 } from "module";
8
8
  import { fileURLToPath } from "url";
9
- import { dirname as dirname12, join as join21 } from "path";
9
+ import { dirname as dirname13, join as join23 } from "path";
10
10
 
11
11
  // src/commands/init.ts
12
12
  import { select as select3, confirm as confirm2, input as input3 } from "@inquirer/prompts";
13
- import { dirname as dirname4, join as join7 } from "path";
13
+ import { dirname as dirname5, join as join8 } from "path";
14
14
  import chalk2 from "chalk";
15
15
 
16
16
  // src/modules/copier.ts
@@ -1417,7 +1417,7 @@ var WORDMARK = [
1417
1417
  var INDENT = " ";
1418
1418
  var LOCKUP_WIDTH = INDENT.length + Math.max(...WORDMARK.map((l) => l.length));
1419
1419
  function sleep(ms) {
1420
- return new Promise((resolve5) => setTimeout(resolve5, ms));
1420
+ return new Promise((resolve6) => setTimeout(resolve6, ms));
1421
1421
  }
1422
1422
  function isAnimatable() {
1423
1423
  return !!process.stdout.isTTY && process.env["TERM"] !== "dumb" && process.env["CI"] !== "true" && process.env["ARCANE_DISABLE_ANIMATION"] !== "true" && (process.stdout.columns ?? 80) >= LOCKUP_WIDTH;
@@ -2849,7 +2849,7 @@ function buildGitEnv() {
2849
2849
  function runGit(cwd, args, options = {}) {
2850
2850
  const commandClass = options.commandClass ?? classifyGitCommand(args);
2851
2851
  const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUTS_MS[commandClass] ?? DEFAULT_TIMEOUTS_MS.write;
2852
- return new Promise((resolve5, reject) => {
2852
+ return new Promise((resolve6, reject) => {
2853
2853
  const child = execFile(
2854
2854
  "git",
2855
2855
  args,
@@ -2883,7 +2883,7 @@ function runGit(cwd, args, options = {}) {
2883
2883
  reject(error);
2884
2884
  return;
2885
2885
  }
2886
- resolve5({ stdout, stderr });
2886
+ resolve6({ stdout, stderr });
2887
2887
  }
2888
2888
  );
2889
2889
  child.stdin?.end();
@@ -3336,7 +3336,27 @@ async function offerRegistryScaffold(targetDir, businessRoot) {
3336
3336
  // src/modules/push-safety.ts
3337
3337
  import { mkdir as mkdir5, readFile as readFile7, writeFile as writeFile8, chmod, access as access3, rm as rm4, readdir as readdir6 } from "fs/promises";
3338
3338
  import { constants } from "fs";
3339
- import { join as join6, resolve as resolve2, dirname as dirname3, basename } from "path";
3339
+ import { join as join7, resolve as resolve3, dirname as dirname4, basename as basename2 } from "path";
3340
+
3341
+ // src/modules/real-path.ts
3342
+ import { realpathSync } from "fs";
3343
+ import { basename, dirname as dirname3, join as join6, resolve as resolve2 } from "path";
3344
+ function resolveRealPath(path4) {
3345
+ const missing = [];
3346
+ let existing = path4;
3347
+ for (; ; ) {
3348
+ try {
3349
+ return join6(realpathSync.native(existing), ...missing);
3350
+ } catch {
3351
+ const parent = dirname3(existing);
3352
+ if (parent === existing) return resolve2(path4);
3353
+ missing.unshift(basename(existing));
3354
+ existing = parent;
3355
+ }
3356
+ }
3357
+ }
3358
+
3359
+ // src/modules/push-safety.ts
3340
3360
  var DISABLED_PUSH_URL = "arcane-push-blocked-run-spell-unblock-push://this-repository";
3341
3361
  var NO_PUSH_URL_MARKER = (remote) => `remote.${remote}.arcaneHadNoPushUrl`;
3342
3362
  var ARCANE_HOOKS_DIR = ".arcane/hooks";
@@ -3365,10 +3385,12 @@ function isArcaneHooksPath(root, value) {
3365
3385
  const windows = process.platform === "win32";
3366
3386
  const normalize = (p) => {
3367
3387
  const separated = windows ? p.replace(/\\/g, "/") : p;
3368
- const absolute = resolve2(root, separated).replace(/\\/g, "/").replace(/\/+$/, "");
3388
+ const resolved = resolve3(root, separated);
3389
+ const absolute = (windows ? resolved.replace(/\\/g, "/") : resolved).replace(/\/+$/, "");
3369
3390
  return windows ? absolute.toLowerCase() : absolute;
3370
3391
  };
3371
- return normalize(value) === normalize(ARCANE_HOOKS_DIR);
3392
+ if (normalize(value) === normalize(ARCANE_HOOKS_DIR)) return true;
3393
+ return normalize(resolveRealPath(resolve3(root, value))) === normalize(join7(resolveRealPath(root), ARCANE_HOOKS_DIR));
3372
3394
  }
3373
3395
  var PRE_PUSH_HOOK_BODY = `#!/bin/sh
3374
3396
  # Installed by Arcane because this repository's .arcane.json sets
@@ -3413,8 +3435,8 @@ async function repositoryRoot(cwd) {
3413
3435
  ]);
3414
3436
  const commonDir = stdout.trim();
3415
3437
  if (commonDir !== "") {
3416
- const parent = dirname3(commonDir);
3417
- if (basename(commonDir) === ".git" && parent !== "") return parent;
3438
+ const parent = dirname4(commonDir);
3439
+ if (basename2(commonDir) === ".git" && parent !== "") return parent;
3418
3440
  }
3419
3441
  } catch {
3420
3442
  }
@@ -3427,10 +3449,11 @@ async function repositoryRoot(cwd) {
3427
3449
  return cwd;
3428
3450
  }
3429
3451
  async function hookFilePath(cwd, hookName = "pre-push") {
3430
- return join6(await repositoryRoot(cwd), ARCANE_HOOKS_DIR, hookName);
3452
+ return join7(await repositoryRoot(cwd), ARCANE_HOOKS_DIR, hookName);
3431
3453
  }
3432
3454
  async function hooksPathValue(cwd) {
3433
- return join6(await repositoryRoot(cwd), ARCANE_HOOKS_DIR).replace(/\\/g, "/");
3455
+ const value = join7(await repositoryRoot(cwd), ARCANE_HOOKS_DIR);
3456
+ return process.platform === "win32" ? value.replace(/\\/g, "/") : value;
3434
3457
  }
3435
3458
  async function installHook(cwd, hookName, body) {
3436
3459
  const root = await repositoryRoot(cwd);
@@ -3449,11 +3472,11 @@ async function installHook(cwd, hookName, body) {
3449
3472
  return { status: "refused-default-hooks", hooks: displaced };
3450
3473
  }
3451
3474
  }
3452
- const hookPath = join6(root, ARCANE_HOOKS_DIR, hookName);
3475
+ const hookPath = join7(root, ARCANE_HOOKS_DIR, hookName);
3453
3476
  if (existing !== void 0 && await hookFileMatches(cwd, hookName, body)) {
3454
3477
  return { status: "already-ours" };
3455
3478
  }
3456
- await mkdir5(join6(root, ARCANE_HOOKS_DIR), { recursive: true });
3479
+ await mkdir5(join7(root, ARCANE_HOOKS_DIR), { recursive: true });
3457
3480
  await writeFile8(hookPath, body, "utf-8");
3458
3481
  await chmod(hookPath, 493);
3459
3482
  await runGit(cwd, ["config", "--local", "core.hooksPath", await hooksPathValue(cwd)]);
@@ -3498,7 +3521,7 @@ async function defaultHooks(cwd) {
3498
3521
  let hooksDir;
3499
3522
  try {
3500
3523
  const { stdout } = await runGit(cwd, ["rev-parse", "--git-common-dir"]);
3501
- hooksDir = join6(resolve2(cwd, stdout.trim()), "hooks");
3524
+ hooksDir = join7(resolve3(cwd, stdout.trim()), "hooks");
3502
3525
  } catch {
3503
3526
  return [];
3504
3527
  }
@@ -3534,7 +3557,7 @@ async function removeHook(cwd, hookName) {
3534
3557
  await rm4(await hookFilePath(cwd, hookName), { force: true });
3535
3558
  } catch {
3536
3559
  }
3537
- const hooksDir = join6(root, ARCANE_HOOKS_DIR);
3560
+ const hooksDir = join7(root, ARCANE_HOOKS_DIR);
3538
3561
  let remaining = [];
3539
3562
  try {
3540
3563
  remaining = await readdir6(hooksDir);
@@ -3937,7 +3960,7 @@ async function runInit(options, targetDir, assetsDir, packageVersion) {
3937
3960
  }
3938
3961
  let flags = {};
3939
3962
  try {
3940
- flags = resolveManifestFlags(options.manifestFlags, {}, join7(targetDir, ".arcane.json"));
3963
+ flags = resolveManifestFlags(options.manifestFlags, {}, join8(targetDir, ".arcane.json"));
3941
3964
  } catch (err) {
3942
3965
  if (!(err instanceof ManifestFlagError)) throw err;
3943
3966
  console.error(`
@@ -4072,7 +4095,7 @@ async function runInit(options, targetDir, assetsDir, packageVersion) {
4072
4095
  const installedFiles = [];
4073
4096
  const fileHashes = {};
4074
4097
  for (const file of component.files) {
4075
- const srcPath = join7(assetsDir, component.sourceOverrides?.[file] ?? file);
4098
+ const srcPath = join8(assetsDir, component.sourceOverrides?.[file] ?? file);
4076
4099
  if (options.dryRun) {
4077
4100
  printDryRun(`Would copy: ${file}`);
4078
4101
  } else {
@@ -4089,7 +4112,7 @@ async function runInit(options, targetDir, assetsDir, packageVersion) {
4089
4112
  fileCount++;
4090
4113
  }
4091
4114
  for (const dir of component.directories ?? []) {
4092
- const srcDirPath = join7(assetsDir, dir);
4115
+ const srcDirPath = join8(assetsDir, dir);
4093
4116
  if (options.dryRun) {
4094
4117
  printDryRun(`Would copy directory: ${dir}/`);
4095
4118
  } else {
@@ -4118,7 +4141,7 @@ async function runInit(options, targetDir, assetsDir, packageVersion) {
4118
4141
  if (options.dryRun) {
4119
4142
  if (hasAgents) {
4120
4143
  const agentFiles = await import("fs").then(
4121
- (fsModule) => fsModule.promises.readdir(join7(assetsDir, "agents"))
4144
+ (fsModule) => fsModule.promises.readdir(join8(assetsDir, "agents"))
4122
4145
  );
4123
4146
  const yamlFiles = agentFiles.filter((f) => f.endsWith(".yaml")).sort();
4124
4147
  printDryRun("Would run agent setup (interactive):");
@@ -4357,7 +4380,7 @@ async function runInitUser(options, storeRoot, assetsDir, packageVersion) {
4357
4380
  console.log(chalk2.hex("#a855f7")(`
4358
4381
  \u2726 Arcane v${packageVersion}
4359
4382
  `));
4360
- const homeDir = dirname4(storeRoot);
4383
+ const homeDir = dirname5(storeRoot);
4361
4384
  const components = USER_TIER_COMPONENTS.map(
4362
4385
  (name) => componentForScope(getComponent(name), "user")
4363
4386
  );
@@ -4375,7 +4398,7 @@ async function runInitUser(options, storeRoot, assetsDir, packageVersion) {
4375
4398
  for (const id of spellIds) {
4376
4399
  for (const client2 of ["codex", "claude"]) {
4377
4400
  const relativePath = USER_FANOUT_PATHS[client2](id);
4378
- if (await fileExists(join7(homeDir, relativePath))) {
4401
+ if (await fileExists(join8(homeDir, relativePath))) {
4379
4402
  printDryRun(`Would leave alone (exists, not written by Arcane): ~/${relativePath}`);
4380
4403
  } else {
4381
4404
  printDryRun(`Would write: ~/${relativePath}`);
@@ -4393,7 +4416,7 @@ async function runInitUser(options, storeRoot, assetsDir, packageVersion) {
4393
4416
  const installedFiles = [];
4394
4417
  const fileHashes = {};
4395
4418
  for (const file of component.files) {
4396
- const srcPath = join7(assetsDir, component.sourceOverrides?.[file] ?? file);
4419
+ const srcPath = join8(assetsDir, component.sourceOverrides?.[file] ?? file);
4397
4420
  fileHashes[file] = await copyFile(srcPath, storeRoot, file, { force: options.force });
4398
4421
  installedFiles.push(file);
4399
4422
  fileCount++;
@@ -4440,23 +4463,23 @@ async function runInitUser(options, storeRoot, assetsDir, packageVersion) {
4440
4463
 
4441
4464
  // src/modules/component-install.ts
4442
4465
  import { readFile as readFile8, stat, writeFile as writeFile9 } from "fs/promises";
4443
- import { join as join8 } from "path";
4466
+ import { join as join9 } from "path";
4444
4467
  async function planComponentInstall(component, targetDir, assetsDir, opts = {}) {
4445
4468
  const files = [];
4446
4469
  const stateOf = async (file, src) => {
4447
- const dest = join8(targetDir, file);
4470
+ const dest = join9(targetDir, file);
4448
4471
  if (!await fileExists(dest)) return { file, src, state: "missing" };
4449
4472
  if ((await stat(dest)).isDirectory()) return { file, src, state: "directory" };
4450
4473
  const same = await hashFile(dest) === await hashFile(src);
4451
4474
  return { file, src, state: same ? "identical" : "differs" };
4452
4475
  };
4453
4476
  for (const file of component.files) {
4454
- files.push(await stateOf(file, join8(assetsDir, component.sourceOverrides?.[file] ?? file)));
4477
+ files.push(await stateOf(file, join9(assetsDir, component.sourceOverrides?.[file] ?? file)));
4455
4478
  }
4456
4479
  for (const dir of component.directories ?? []) {
4457
- const srcDir = join8(assetsDir, dir);
4480
+ const srcDir = join9(assetsDir, dir);
4458
4481
  for (const file of await listDirectoryFiles(srcDir, dir)) {
4459
- files.push(await stateOf(file, join8(assetsDir, file)));
4482
+ files.push(await stateOf(file, join9(assetsDir, file)));
4460
4483
  }
4461
4484
  }
4462
4485
  const of = (...states) => files.filter((f) => states.includes(f.state));
@@ -4474,7 +4497,7 @@ async function executeInstallPlan(plan, targetDir) {
4474
4497
  const written = [];
4475
4498
  try {
4476
4499
  for (const { file, src, state } of plan.toCopy) {
4477
- const previous = state === "missing" ? null : await readFile8(join8(targetDir, file));
4500
+ const previous = state === "missing" ? null : await readFile8(join9(targetDir, file));
4478
4501
  fileHashes[file] = await copyFile(src, targetDir, file, { force: true });
4479
4502
  written.push({ file, previous });
4480
4503
  files.push(file);
@@ -4492,7 +4515,7 @@ async function rollback(written, targetDir) {
4492
4515
  await removeWithin(targetDir, file);
4493
4516
  await removeEmptyAncestors(targetDir, file);
4494
4517
  } else {
4495
- await writeFile9(join8(targetDir, file), previous);
4518
+ await writeFile9(join9(targetDir, file), previous);
4496
4519
  }
4497
4520
  } catch {
4498
4521
  }
@@ -4628,7 +4651,7 @@ async function runAdd(names, options, targetDir, assetsDir, packageVersion) {
4628
4651
  }
4629
4652
 
4630
4653
  // src/commands/update.ts
4631
- import { dirname as dirname5, join as join10 } from "path";
4654
+ import { dirname as dirname6, join as join11 } from "path";
4632
4655
  import { readFile as readFile9, writeFile as writeFile10 } from "fs/promises";
4633
4656
 
4634
4657
  // src/modules/merge3.ts
@@ -4760,7 +4783,7 @@ function merge3(base, ours, theirs) {
4760
4783
  }
4761
4784
 
4762
4785
  // src/modules/registry-changes.ts
4763
- import { join as join9 } from "path";
4786
+ import { join as join10 } from "path";
4764
4787
  function findMissingRequires(installed, profile) {
4765
4788
  const names = new Set(installed.flatMap((c) => LEGACY_COMPONENT_MIGRATIONS[c.name] ?? [c.name]));
4766
4789
  const promised = listProfiles().find((p) => p.id === profile)?.components;
@@ -4806,7 +4829,7 @@ async function findNewlyAvailable(targetDir, profile, installed, spellScope) {
4806
4829
  const alreadyPresent = [];
4807
4830
  if (component.skipExisting) {
4808
4831
  for (const file of component.files) {
4809
- if (await fileExists(join9(targetDir, file))) alreadyPresent.push(file);
4832
+ if (await fileExists(join10(targetDir, file))) alreadyPresent.push(file);
4810
4833
  }
4811
4834
  if (component.files.length > 0 && alreadyPresent.length === component.files.length) continue;
4812
4835
  }
@@ -5065,7 +5088,7 @@ async function resolveOrphan(targetDir, file, recordedHash, prune) {
5065
5088
  console.log(` ! Refused (path escapes the target directory): ${file}`);
5066
5089
  return "reported";
5067
5090
  }
5068
- const filePath = join10(targetDir, file);
5091
+ const filePath = join11(targetDir, file);
5069
5092
  if (!await fileExists(filePath)) return "not-found";
5070
5093
  if (!prune) return "reported";
5071
5094
  if (recordedHash === void 0) {
@@ -5087,7 +5110,7 @@ async function isReleasableUserOwnedFile(targetDir, file, recordedHash) {
5087
5110
  } catch {
5088
5111
  return false;
5089
5112
  }
5090
- const filePath = join10(targetDir, file);
5113
+ const filePath = join11(targetDir, file);
5091
5114
  if (!await fileExists(filePath)) return false;
5092
5115
  if (recordedHash === void 0) return true;
5093
5116
  return !await fileMatchesHash(filePath, recordedHash);
@@ -5098,7 +5121,7 @@ async function retainReportedOrphan(targetDir, file, recordedHash, into) {
5098
5121
  } catch {
5099
5122
  return;
5100
5123
  }
5101
- if (!await fileExists(join10(targetDir, file))) return;
5124
+ if (!await fileExists(join11(targetDir, file))) return;
5102
5125
  if (!into.files.includes(file)) into.files.push(file);
5103
5126
  if (recordedHash !== void 0) {
5104
5127
  into.fileHashes ??= {};
@@ -5127,7 +5150,7 @@ async function findMissingTrackedFiles(targetDir, components, scope = "repo", sp
5127
5150
  const candidates = readopted.has(installed.name) ? component.files : installed.files;
5128
5151
  for (const file of candidates) {
5129
5152
  if (!component.files.includes(file)) continue;
5130
- if (!await fileExists(join10(targetDir, file))) missing.push(file);
5153
+ if (!await fileExists(join11(targetDir, file))) missing.push(file);
5131
5154
  }
5132
5155
  }
5133
5156
  return missing;
@@ -5169,7 +5192,7 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
5169
5192
  const movedFilesPresent = /* @__PURE__ */ new Set();
5170
5193
  for (const move of COMPONENT_FILE_MOVES) {
5171
5194
  for (const file of move.files) {
5172
- if (await fileExists(join10(targetDir, file))) movedFilesPresent.add(file);
5195
+ if (await fileExists(join11(targetDir, file))) movedFilesPresent.add(file);
5173
5196
  }
5174
5197
  }
5175
5198
  manifest = {
@@ -5200,7 +5223,7 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
5200
5223
  return;
5201
5224
  }
5202
5225
  try {
5203
- flagPatch = resolveManifestFlags(options.manifestFlags, manifest, join10(targetDir, ".arcane.json"));
5226
+ flagPatch = resolveManifestFlags(options.manifestFlags, manifest, join11(targetDir, ".arcane.json"));
5204
5227
  } catch (err) {
5205
5228
  if (!(err instanceof ManifestFlagError)) throw err;
5206
5229
  console.error(`
@@ -5213,7 +5236,7 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
5213
5236
  const answered = { ...manifest, ...flagPatch };
5214
5237
  const spellScope = effectiveSpellScope(manifest);
5215
5238
  const readopted = findReadoptedSpellComponents(manifest.components, scope, spellScope);
5216
- const homeDir = dirname5(targetDir);
5239
+ const homeDir = dirname6(targetDir);
5217
5240
  for (const line of await missingTierWarning(scope, spellScope)) console.log(line);
5218
5241
  if (scope === "repo") {
5219
5242
  console.warn(
@@ -5408,8 +5431,8 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
5408
5431
  const updatedFiles = [];
5409
5432
  const fileHashes = {};
5410
5433
  for (const file of component.files) {
5411
- const srcPath = join10(assetsDir, component.sourceOverrides?.[file] ?? file);
5412
- const targetExists = await fileExists(join10(targetDir, file));
5434
+ const srcPath = join11(assetsDir, component.sourceOverrides?.[file] ?? file);
5435
+ const targetExists = await fileExists(join11(targetDir, file));
5413
5436
  const preserveExisting = Boolean(component.skipExisting) && targetExists;
5414
5437
  const recordedHash = installed.fileHashes?.[file];
5415
5438
  if (component.initOnly && !targetExists) {
@@ -5429,7 +5452,7 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
5429
5452
  continue;
5430
5453
  }
5431
5454
  }
5432
- const editedByOperator = recordedHash !== void 0 && targetExists && !preserveExisting && !await fileMatchesHash(join10(targetDir, file), recordedHash);
5455
+ const editedByOperator = recordedHash !== void 0 && targetExists && !preserveExisting && !await fileMatchesHash(join11(targetDir, file), recordedHash);
5433
5456
  if (preserveExisting) {
5434
5457
  console.log(` ${options.dryRun ? "[dry-run] Would preserve" : "Preserved"}: ${file}`);
5435
5458
  } else if (editedByOperator && isClientShimPath(file)) {
@@ -5456,11 +5479,11 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
5456
5479
  handled = true;
5457
5480
  } else {
5458
5481
  const [currentContent, newContent] = await Promise.all([
5459
- readFile9(join10(targetDir, file), "utf-8"),
5482
+ readFile9(join11(targetDir, file), "utf-8"),
5460
5483
  readFile9(srcPath, "utf-8")
5461
5484
  ]);
5462
5485
  const result = merge3(oldVendorContent, currentContent, newContent);
5463
- await writeFile10(join10(targetDir, file), result.content, "utf-8");
5486
+ await writeFile10(join11(targetDir, file), result.content, "utf-8");
5464
5487
  fileHashes[file] = await hashFile(srcPath);
5465
5488
  if (result.hasConflict) {
5466
5489
  conflictedFiles.push(file);
@@ -5482,7 +5505,7 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
5482
5505
  }
5483
5506
  }
5484
5507
  for (const dir of component.directories ?? []) {
5485
- const srcDirPath = join10(assetsDir, dir);
5508
+ const srcDirPath = join11(assetsDir, dir);
5486
5509
  if (options.dryRun) {
5487
5510
  console.log(` [dry-run] Would update directory: ${dir}/`);
5488
5511
  } else {
@@ -5642,11 +5665,11 @@ ${verb} ${orphansToReport.length} orphaned file(s) (tracked before this update,
5642
5665
  }
5643
5666
 
5644
5667
  // src/commands/status.ts
5645
- import { dirname as dirname7 } from "path";
5668
+ import { dirname as dirname8 } from "path";
5646
5669
 
5647
5670
  // src/modules/version-check.ts
5648
5671
  import { readFile as readFile10, writeFile as writeFile11, mkdir as mkdir6 } from "fs/promises";
5649
- import { dirname as dirname6, join as join11 } from "path";
5672
+ import { dirname as dirname7, join as join12 } from "path";
5650
5673
  import { homedir as homedir3 } from "os";
5651
5674
  import { createRequire } from "module";
5652
5675
  var require2 = createRequire(import.meta.url);
@@ -5660,7 +5683,7 @@ function getFeedUrl() {
5660
5683
  }
5661
5684
  return url;
5662
5685
  }
5663
- var DEFAULT_CACHE_PATH = join11(homedir3(), ".arcane", "version-cache.json");
5686
+ var DEFAULT_CACHE_PATH = join12(homedir3(), ".arcane", "version-cache.json");
5664
5687
  var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
5665
5688
  async function readCache(cachePath) {
5666
5689
  try {
@@ -5675,7 +5698,7 @@ async function readCache(cachePath) {
5675
5698
  }
5676
5699
  }
5677
5700
  async function writeCache(cachePath, latest) {
5678
- await mkdir6(dirname6(cachePath), { recursive: true });
5701
+ await mkdir6(dirname7(cachePath), { recursive: true });
5679
5702
  const entry = { latest, checkedAt: (/* @__PURE__ */ new Date()).toISOString() };
5680
5703
  await writeFile11(cachePath, JSON.stringify(entry, null, 2), "utf-8");
5681
5704
  }
@@ -5816,7 +5839,7 @@ async function runStatus(targetDir, packageVersion, options = {}) {
5816
5839
  console.log("");
5817
5840
  if (scope === "user") {
5818
5841
  console.log(` Scope: user \u2014 ${targetDir}`);
5819
- const health = await inspectUserTierFanout(dirname7(targetDir), manifest.fanout);
5842
+ const health = await inspectUserTierFanout(dirname8(targetDir), manifest.fanout);
5820
5843
  console.log(
5821
5844
  ` Client files: ${health.total} (${health.byClient.codex} ${FANOUT_CLIENT_LABELS.codex}, ${health.byClient.claude} ${FANOUT_CLIENT_LABELS.claude})`
5822
5845
  );
@@ -5879,7 +5902,7 @@ async function runStatus(targetDir, packageVersion, options = {}) {
5879
5902
 
5880
5903
  // src/commands/uninstall.ts
5881
5904
  import { rm as rm5 } from "fs/promises";
5882
- import { dirname as dirname8, join as join12 } from "path";
5905
+ import { dirname as dirname9, join as join13 } from "path";
5883
5906
  import { confirm as confirm3 } from "@inquirer/prompts";
5884
5907
  async function runUninstall(options, targetDir) {
5885
5908
  let manifest;
@@ -5962,16 +5985,16 @@ Run \`spell unblock-push\` first (interactive), then uninstall.`
5962
5985
  }
5963
5986
  }
5964
5987
  }
5965
- await rm5(join12(targetDir, ".arcane.json"), { force: true });
5988
+ await rm5(join13(targetDir, ".arcane.json"), { force: true });
5966
5989
  const agentDirs = [
5967
5990
  ".github/agents",
5968
5991
  ".arcane/agents",
5969
5992
  ".arcane/generated"
5970
5993
  ];
5971
5994
  for (const dir of agentDirs) {
5972
- await rm5(join12(targetDir, dir), { recursive: true, force: true });
5995
+ await rm5(join13(targetDir, dir), { recursive: true, force: true });
5973
5996
  }
5974
- await rm5(join12(targetDir, ".arcane/agents.yaml"), { force: true });
5997
+ await rm5(join13(targetDir, ".arcane/agents.yaml"), { force: true });
5975
5998
  const markerFiles = [
5976
5999
  "CLAUDE.md",
5977
6000
  "AGENTS.md",
@@ -5988,7 +6011,7 @@ Run \`spell unblock-push\` first (interactive), then uninstall.`
5988
6011
  \u2713 Uninstalled \u2014 ${removed} files removed.`);
5989
6012
  }
5990
6013
  async function uninstallUserTier(options, storeRoot, manifest) {
5991
- const homeDir = dirname8(storeRoot);
6014
+ const homeDir = dirname9(storeRoot);
5992
6015
  const storeFiles = manifest.components.flatMap((c) => c.files);
5993
6016
  const recordedCount = Object.keys(manifest.fanout ?? {}).length;
5994
6017
  if (options.dryRun) {
@@ -6033,10 +6056,10 @@ async function uninstallUserTier(options, storeRoot, manifest) {
6033
6056
  console.log(` ! Skipped ${file}: ${err instanceof Error ? err.message : String(err)}`);
6034
6057
  }
6035
6058
  }
6036
- await rm5(join12(storeRoot, ".arcane.json"), { force: true });
6037
- await rm5(join12(storeRoot, "agents"), { recursive: true, force: true });
6038
- await rm5(join12(storeRoot, "agents.yaml"), { force: true });
6039
- await removeDirectoryIfEmpty(join12(storeRoot, USER_TIER_SPELLS_DIR));
6059
+ await rm5(join13(storeRoot, ".arcane.json"), { force: true });
6060
+ await rm5(join13(storeRoot, "agents"), { recursive: true, force: true });
6061
+ await rm5(join13(storeRoot, "agents.yaml"), { force: true });
6062
+ await removeDirectoryIfEmpty(join13(storeRoot, USER_TIER_SPELLS_DIR));
6040
6063
  await removeDirectoryIfEmpty(storeRoot);
6041
6064
  for (const line of describeFanoutOutcomes(fanout.outcomes)) console.log(` ${line}`);
6042
6065
  const pruned = fanout.outcomes.filter((o) => o.status === "pruned").length;
@@ -6049,7 +6072,7 @@ async function uninstallUserTier(options, storeRoot, manifest) {
6049
6072
 
6050
6073
  // src/commands/doctor.ts
6051
6074
  import { access as access4, readdir as readdir9, readFile as readFile13, mkdir as mkdir7, copyFile as fsCopyFile } from "fs/promises";
6052
- import { join as join15, dirname as dirname9 } from "path";
6075
+ import { join as join16, dirname as dirname10 } from "path";
6053
6076
 
6054
6077
  // src/config/incidents.ts
6055
6078
  var INCIDENT_QUEUE = [
@@ -6118,7 +6141,7 @@ function evaluateIncidentGate(records) {
6118
6141
 
6119
6142
  // src/modules/denylist-scan.ts
6120
6143
  import { readFile as readFile11, readdir as readdir7 } from "fs/promises";
6121
- import { join as join13, relative } from "path";
6144
+ import { join as join14, relative } from "path";
6122
6145
  function escapeRegExp(value) {
6123
6146
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
6124
6147
  }
@@ -6169,7 +6192,7 @@ async function collectScannableFiles(root, repoRoot = root, extensions = DEFAULT
6169
6192
  return collected;
6170
6193
  }
6171
6194
  for (const entry of entries) {
6172
- const absolutePath = join13(root, entry.name);
6195
+ const absolutePath = join14(root, entry.name);
6173
6196
  if (entry.isDirectory()) {
6174
6197
  if (SKIP_DIRECTORIES.has(entry.name)) continue;
6175
6198
  collected.push(...await collectScannableFiles(absolutePath, repoRoot, extensions));
@@ -6196,7 +6219,7 @@ import { existsSync, readFileSync } from "fs";
6196
6219
  import { win32 } from "path";
6197
6220
  var EXTERNAL_CLI_TIMEOUT_MS = 1e4;
6198
6221
  function execFileWithTimeout(file, args, timeoutMs, options = {}) {
6199
- return new Promise((resolve5, reject) => {
6222
+ return new Promise((resolve6, reject) => {
6200
6223
  const child = execFile2(
6201
6224
  file,
6202
6225
  args,
@@ -6212,7 +6235,7 @@ function execFileWithTimeout(file, args, timeoutMs, options = {}) {
6212
6235
  reject(error);
6213
6236
  return;
6214
6237
  }
6215
- resolve5({ stdout, stderr });
6238
+ resolve6({ stdout, stderr });
6216
6239
  }
6217
6240
  );
6218
6241
  child.stdin?.end();
@@ -6430,7 +6453,7 @@ function parseAdoRemote(url) {
6430
6453
 
6431
6454
  // src/modules/placeholders.ts
6432
6455
  import { readdir as readdir8, readFile as readFile12 } from "fs/promises";
6433
- import { join as join14 } from "path";
6456
+ import { join as join15 } from "path";
6434
6457
  var PLACEHOLDER_STANDARD_FILE = "spell-authoring-standards.md";
6435
6458
  var RUNTIME_PLACEHOLDERS_START = "<!-- runtime-placeholders:start -->";
6436
6459
  var RUNTIME_PLACEHOLDERS_END = "<!-- runtime-placeholders:end -->";
@@ -6525,7 +6548,7 @@ function parseRuntimePlaceholders(standardContent) {
6525
6548
  async function scanGovernancePlaceholders(governanceDir) {
6526
6549
  let standard;
6527
6550
  try {
6528
- standard = await readFile12(join14(governanceDir, PLACEHOLDER_STANDARD_FILE), "utf8");
6551
+ standard = await readFile12(join15(governanceDir, PLACEHOLDER_STANDARD_FILE), "utf8");
6529
6552
  } catch (err) {
6530
6553
  if (err.code === "ENOENT") {
6531
6554
  return { status: "skipped", reason: `${PLACEHOLDER_STANDARD_FILE} is not installed` };
@@ -6544,7 +6567,7 @@ async function scanGovernancePlaceholders(governanceDir) {
6544
6567
  const templateDocs = [];
6545
6568
  const findings = [];
6546
6569
  for (const file of files) {
6547
- const content = await readFile12(join14(governanceDir, file), "utf8");
6570
+ const content = await readFile12(join15(governanceDir, file), "utf8");
6548
6571
  const status = parseFrontmatterStatus(content);
6549
6572
  if (status === "template") templateDocs.push(file);
6550
6573
  if (status !== "active") continue;
@@ -6575,8 +6598,8 @@ async function checkNodeVersion() {
6575
6598
  async function checkVSCodeExtension(id, label2, homeDir = resolveHomeDir()) {
6576
6599
  const name = `VS Code extension: ${label2}`;
6577
6600
  const flavours = [
6578
- { dir: join15(homeDir, ".vscode", "extensions"), bin: "code" },
6579
- { dir: join15(homeDir, ".vscode-insiders", "extensions"), bin: "code-insiders" }
6601
+ { dir: join16(homeDir, ".vscode", "extensions"), bin: "code" },
6602
+ { dir: join16(homeDir, ".vscode-insiders", "extensions"), bin: "code-insiders" }
6580
6603
  ];
6581
6604
  let installBin = null;
6582
6605
  for (const flavour of flavours) {
@@ -6604,11 +6627,11 @@ async function checkVSCodeExtension(id, label2, homeDir = resolveHomeDir()) {
6604
6627
  }
6605
6628
  async function checkArcaneManifest(targetDir) {
6606
6629
  const name = "Arcane manifest (.arcane.json)";
6607
- const manifestPath2 = join15(targetDir, ".arcane.json");
6630
+ const manifestPath2 = join16(targetDir, ".arcane.json");
6608
6631
  try {
6609
6632
  await access4(manifestPath2);
6610
6633
  } catch {
6611
- const selfHostedManifestPath = join15(targetDir, "src", "assets", ".arcane.json");
6634
+ const selfHostedManifestPath = join16(targetDir, "src", "assets", ".arcane.json");
6612
6635
  try {
6613
6636
  const raw = await readFile13(selfHostedManifestPath, "utf8");
6614
6637
  const manifest = JSON.parse(raw);
@@ -6714,7 +6737,7 @@ async function checkSessionContinuity(targetDir) {
6714
6737
  const results = [];
6715
6738
  for (const file of SESSION_CONTINUITY_FILES) {
6716
6739
  const name = `Session file: ${file}`;
6717
- const filePath = join15(targetDir, file);
6740
+ const filePath = join16(targetDir, file);
6718
6741
  try {
6719
6742
  await access4(filePath);
6720
6743
  results.push({ name, passed: true, message: "present" });
@@ -6732,12 +6755,12 @@ async function checkSessionContinuity(targetDir) {
6732
6755
  async function fixSessionContinuity(targetDir, assetsDir) {
6733
6756
  const created = [];
6734
6757
  for (const file of SESSION_CONTINUITY_FILES) {
6735
- const destPath = join15(targetDir, file);
6758
+ const destPath = join16(targetDir, file);
6736
6759
  try {
6737
6760
  await access4(destPath);
6738
6761
  } catch {
6739
- const srcPath = join15(assetsDir, file);
6740
- await mkdir7(dirname9(destPath), { recursive: true });
6762
+ const srcPath = join16(assetsDir, file);
6763
+ await mkdir7(dirname10(destPath), { recursive: true });
6741
6764
  await fsCopyFile(srcPath, destPath);
6742
6765
  created.push(file);
6743
6766
  }
@@ -6864,7 +6887,7 @@ async function checkGovernancePlaceholders(targetDir) {
6864
6887
  const name = "Governance placeholders (ARC-051)";
6865
6888
  let scan;
6866
6889
  try {
6867
- scan = await scanGovernancePlaceholders(join15(targetDir, ".arcane", "governance"));
6890
+ scan = await scanGovernancePlaceholders(join16(targetDir, ".arcane", "governance"));
6868
6891
  } catch (err) {
6869
6892
  return {
6870
6893
  name,
@@ -6951,7 +6974,7 @@ async function checkNewlyAvailableComponents(targetDir) {
6951
6974
  }
6952
6975
  async function checkMcpConfig(targetDir) {
6953
6976
  const name = "MCP config (per-server timeout, I12)";
6954
- const filePath = join15(targetDir, ".mcp.json");
6977
+ const filePath = join16(targetDir, ".mcp.json");
6955
6978
  let raw;
6956
6979
  try {
6957
6980
  raw = await readFile13(filePath, "utf8");
@@ -7034,7 +7057,7 @@ function isDelegationsFile(value) {
7034
7057
  }
7035
7058
  async function checkDelegations(targetDir) {
7036
7059
  const name = "Standing delegations (T13)";
7037
- const filePath = join15(targetDir, ".arcane", "delegations.json");
7060
+ const filePath = join16(targetDir, ".arcane", "delegations.json");
7038
7061
  let raw;
7039
7062
  try {
7040
7063
  raw = await readFile13(filePath, "utf8");
@@ -7147,7 +7170,7 @@ async function checkSpellScope(targetDir, homeDir = resolveHomeDir()) {
7147
7170
  let probed = false;
7148
7171
  const probe = "spell-status.md";
7149
7172
  try {
7150
- const entries = await readdir9(join15(storeRoot, USER_TIER_SPELLS_DIR));
7173
+ const entries = await readdir9(join16(storeRoot, USER_TIER_SPELLS_DIR));
7151
7174
  const spells = entries.filter((e) => e.startsWith("spell-") && e.endsWith(".md"));
7152
7175
  spellCount = spells.length;
7153
7176
  probed = spells.includes(probe);
@@ -7261,7 +7284,7 @@ import { readFile as readFile15 } from "fs/promises";
7261
7284
 
7262
7285
  // src/modules/ward.ts
7263
7286
  import { readdir as readdir10, readFile as readFile14 } from "fs/promises";
7264
- import { join as join16, relative as relative2 } from "path";
7287
+ import { join as join17, relative as relative2 } from "path";
7265
7288
  async function scanFileAllRules(absolutePath, displayPath, rules) {
7266
7289
  const findings = [];
7267
7290
  let content;
@@ -7324,7 +7347,7 @@ async function scanFilenames(root, rules, repoRoot = root) {
7324
7347
  return findings;
7325
7348
  }
7326
7349
  for (const entry of entries) {
7327
- const absolutePath = join16(root, entry.name);
7350
+ const absolutePath = join17(root, entry.name);
7328
7351
  if (entry.isDirectory()) {
7329
7352
  if (SKIP_DIRECTORIES.has(entry.name)) continue;
7330
7353
  findings.push(...await scanFilenames(absolutePath, rules, repoRoot));
@@ -7439,7 +7462,7 @@ async function runWardCli(targetDir, options = {}, exclusions = []) {
7439
7462
  }
7440
7463
 
7441
7464
  // src/commands/unblock-push.ts
7442
- import { basename as basename2, resolve as resolve3 } from "path";
7465
+ import { basename as basename3, resolve as resolve4 } from "path";
7443
7466
  import { input as input4 } from "@inquirer/prompts";
7444
7467
  async function runUnblockPush(targetDir) {
7445
7468
  let manifest;
@@ -7471,7 +7494,7 @@ async function runUnblockPush(targetDir) {
7471
7494
  process.exit(1);
7472
7495
  return;
7473
7496
  }
7474
- const repoName = basename2(resolve3(targetDir));
7497
+ const repoName = basename3(resolve4(targetDir));
7475
7498
  if (repoName === "") {
7476
7499
  console.error(
7477
7500
  "Could not determine a repository name to confirm against (is this repository at a filesystem root?). Refusing to unblock."
@@ -7756,6 +7779,70 @@ function resolveIdentityEnvironment(env = process.env) {
7756
7779
  import { createServer } from "http";
7757
7780
  import { execFile as execFile3 } from "child_process";
7758
7781
  import * as client from "openid-client";
7782
+
7783
+ // src/modules/result-pages.ts
7784
+ import { createHash as createHash3 } from "crypto";
7785
+ import { readFile as readFile16 } from "fs/promises";
7786
+ import { join as join18 } from "path";
7787
+ var RESULT_KINDS = ["success", "denied", "failure"];
7788
+ function resultPagePath(kind) {
7789
+ return join18("sign-in", `sign-in-callback-${kind}.html`);
7790
+ }
7791
+ function styleHashes(html) {
7792
+ return [...html.matchAll(/<style\b[^>]*>([\s\S]*?)<\/style\s*>/gi)].map((match) => {
7793
+ const text = match[1].replace(/\r\n?/g, "\n");
7794
+ return `'sha256-${createHash3("sha256").update(text, "utf8").digest("base64")}'`;
7795
+ });
7796
+ }
7797
+ function resultPageHeaders(html) {
7798
+ const hashes = [...new Set(styleHashes(html))];
7799
+ return {
7800
+ "cache-control": "no-store",
7801
+ "content-security-policy": [
7802
+ "default-src 'none'",
7803
+ `style-src ${hashes.length > 0 ? hashes.join(" ") : "'none'"}`,
7804
+ "img-src data:",
7805
+ "font-src data:",
7806
+ "base-uri 'none'",
7807
+ "form-action 'none'",
7808
+ "frame-ancestors 'none'"
7809
+ ].join("; "),
7810
+ "content-type": "text/html; charset=utf-8",
7811
+ "referrer-policy": "no-referrer",
7812
+ "x-content-type-options": "nosniff"
7813
+ };
7814
+ }
7815
+ function resultPage(html) {
7816
+ return { html, headers: resultPageHeaders(html) };
7817
+ }
7818
+ function plainPage(heading, next) {
7819
+ return resultPage(
7820
+ [
7821
+ '<!doctype html><html lang="en"><meta charset="utf-8">',
7822
+ '<meta name="viewport" content="width=device-width, initial-scale=1">',
7823
+ `<title>${heading} \u2014 Arcane</title>`,
7824
+ "<style>:root{color-scheme:light dark}body{font-family:system-ui,sans-serif;margin:3rem;line-height:1.5}</style>",
7825
+ `<body><h1>${heading}</h1><p>${next}</p></body></html>`
7826
+ ].join("")
7827
+ );
7828
+ }
7829
+ var PLAIN_RESULT_PAGES = {
7830
+ success: plainPage("Signed in to Arcane", "Return to your terminal. Your browser can be closed now."),
7831
+ denied: plainPage("Sign-in was not completed", "Return to your terminal to try again. No new session was stored."),
7832
+ failure: plainPage("Could not finish sign-in", "Return to your terminal for details. No new session was stored.")
7833
+ };
7834
+ async function loadResultPages(assetsDir) {
7835
+ try {
7836
+ const [success, denied, failure] = await Promise.all(
7837
+ RESULT_KINDS.map(async (kind) => resultPage(await readFile16(join18(assetsDir, resultPagePath(kind)), "utf8")))
7838
+ );
7839
+ return { success, denied, failure };
7840
+ } catch {
7841
+ return null;
7842
+ }
7843
+ }
7844
+
7845
+ // src/modules/oidc.ts
7759
7846
  var IdentityUnreachableError = class extends Error {
7760
7847
  constructor(host, cause) {
7761
7848
  super(`Could not reach the identity service at ${host}.`);
@@ -7769,6 +7856,12 @@ var LoginTimeoutError = class extends Error {
7769
7856
  this.name = "LoginTimeoutError";
7770
7857
  }
7771
7858
  };
7859
+ var LoginBrowserUnavailableError = class extends Error {
7860
+ constructor() {
7861
+ super("No browser could be opened. Run `spell login --device-code` and finish signing in on another device.");
7862
+ this.name = "LoginBrowserUnavailableError";
7863
+ }
7864
+ };
7772
7865
  var LoginDeniedError = class extends Error {
7773
7866
  constructor(code) {
7774
7867
  super(`The identity service refused the sign-in (${code}).`);
@@ -7811,25 +7904,17 @@ function toTokenResult(tokens, previousRefreshToken) {
7811
7904
  expiresAt: new Date(now.getTime() + seconds * 1e3)
7812
7905
  };
7813
7906
  }
7814
- var CLOSE_PAGE = [
7815
- '<!doctype html><meta charset="utf-8"><title>Arcane</title>',
7816
- '<body style="font-family:system-ui;margin:3rem"><h1>Signed in to Arcane</h1>',
7817
- "<p>You can close this window and go back to the terminal.</p></body>"
7818
- ].join("");
7819
- var FAILED_PAGE = [
7820
- '<!doctype html><meta charset="utf-8"><title>Arcane</title>',
7821
- '<body style="font-family:system-ui;margin:3rem"><h1>Sign-in not completed</h1>',
7822
- "<p>Go back to the terminal for what to do next.</p></body>"
7823
- ].join("");
7907
+ var resultPath = (kind) => `/result/${kind}`;
7908
+ var RESULT_PATHS = new Set(RESULT_KINDS.map(resultPath));
7824
7909
  async function browserLogin(config, options) {
7825
7910
  const codeVerifier = client.randomPKCECodeVerifier();
7826
7911
  const codeChallenge = await client.calculatePKCECodeChallenge(codeVerifier);
7827
7912
  const state = client.randomState();
7828
7913
  const nonce = client.randomNonce();
7829
7914
  const server = createServer();
7830
- await new Promise((resolve5, reject) => {
7915
+ await new Promise((resolve6, reject) => {
7831
7916
  server.once("error", reject);
7832
- server.listen(0, "127.0.0.1", () => resolve5());
7917
+ server.listen(0, "127.0.0.1", () => resolve6());
7833
7918
  });
7834
7919
  const address = server.address();
7835
7920
  if (address === null || typeof address === "string") {
@@ -7845,24 +7930,66 @@ async function browserLogin(config, options) {
7845
7930
  state,
7846
7931
  nonce
7847
7932
  });
7933
+ const pages = options.pages ?? PLAIN_RESULT_PAGES;
7848
7934
  const timeoutMs = options.timeoutMs ?? 5 * 60 * 1e3;
7849
7935
  let timer;
7936
+ let callbackConsumed = false;
7937
+ let browserResult;
7850
7938
  const close = () => {
7851
7939
  if (timer) clearTimeout(timer);
7852
7940
  server.closeAllConnections?.();
7853
7941
  server.close();
7854
7942
  };
7855
- const tokens = new Promise((resolve5, reject) => {
7943
+ const tokens = new Promise((resolve6, reject) => {
7856
7944
  server.on("request", (req, res) => {
7857
7945
  const url = new URL(req.url ?? "/", redirectUri);
7946
+ if (RESULT_PATHS.has(url.pathname)) {
7947
+ if (!browserResult || url.pathname !== resultPath(browserResult.kind) || url.search !== "") {
7948
+ res.writeHead(404).end();
7949
+ return;
7950
+ }
7951
+ const served = browserResult;
7952
+ const page = pages[served.kind];
7953
+ const settle = () => {
7954
+ if (served.kind === "success") resolve6(served.result);
7955
+ else reject(served.error);
7956
+ };
7957
+ res.once("finish", settle).once("close", settle);
7958
+ res.writeHead(200, page.headers).end(page.html);
7959
+ return;
7960
+ }
7858
7961
  if (url.pathname !== "/" || !url.searchParams.has("code") && !url.searchParams.has("error")) {
7859
7962
  res.writeHead(404).end();
7860
7963
  return;
7861
7964
  }
7965
+ if (callbackConsumed) {
7966
+ res.writeHead(409, { "cache-control": "no-store" }).end();
7967
+ return;
7968
+ }
7969
+ if (url.searchParams.get("state") !== state) {
7970
+ res.writeHead(400, pages.failure.headers).end(pages.failure.html);
7971
+ return;
7972
+ }
7973
+ callbackConsumed = true;
7974
+ const redirectToResult = (result) => {
7975
+ browserResult = result;
7976
+ res.writeHead(303, {
7977
+ "cache-control": "no-store",
7978
+ location: resultPath(result.kind),
7979
+ "referrer-policy": "no-referrer"
7980
+ }).end();
7981
+ if (result.kind === "success") {
7982
+ if (timer) clearTimeout(timer);
7983
+ timer = setTimeout(() => resolve6(result.result), Math.min(timeoutMs, 5e3));
7984
+ }
7985
+ };
7862
7986
  const errorCode = url.searchParams.get("error");
7863
7987
  if (errorCode) {
7864
- res.writeHead(200, { "content-type": "text/html; charset=utf-8" }).end(FAILED_PAGE);
7865
- reject(new LoginDeniedError(errorCode.replace(/[^a-z_]/gi, "")));
7988
+ const code = errorCode.replace(/[^a-z_]/gi, "");
7989
+ redirectToResult({
7990
+ kind: code === "access_denied" ? "denied" : "failure",
7991
+ error: new LoginDeniedError(code)
7992
+ });
7866
7993
  return;
7867
7994
  }
7868
7995
  client.authorizationCodeGrant(config, url, {
@@ -7873,11 +8000,9 @@ async function browserLogin(config, options) {
7873
8000
  }).then(async (response) => {
7874
8001
  const result = toTokenResult(response);
7875
8002
  await options.onValidated?.(result);
7876
- res.writeHead(200, { "content-type": "text/html; charset=utf-8" }).end(CLOSE_PAGE);
7877
- resolve5(result);
8003
+ redirectToResult({ kind: "success", result });
7878
8004
  }).catch((error) => {
7879
- res.writeHead(200, { "content-type": "text/html; charset=utf-8" }).end(FAILED_PAGE);
7880
- reject(error);
8005
+ redirectToResult({ kind: "failure", error });
7881
8006
  });
7882
8007
  });
7883
8008
  timer = setTimeout(() => reject(new LoginTimeoutError()), timeoutMs);
@@ -7886,7 +8011,10 @@ async function browserLogin(config, options) {
7886
8011
  });
7887
8012
  try {
7888
8013
  const opened = await options.openUrl(authorizationUrl.href);
7889
- if (!opened) options.onCannotOpen?.(authorizationUrl.href);
8014
+ if (!opened) {
8015
+ options.onCannotOpen?.();
8016
+ throw new LoginBrowserUnavailableError();
8017
+ }
7890
8018
  return await tokens;
7891
8019
  } finally {
7892
8020
  close();
@@ -7904,12 +8032,12 @@ async function refreshSession(config, refreshToken, scopes) {
7904
8032
  }
7905
8033
  function openInDefaultBrowser(url) {
7906
8034
  const [command, args] = process.platform === "win32" ? ["rundll32", ["url.dll,FileProtocolHandler", url]] : process.platform === "darwin" ? ["open", [url]] : ["xdg-open", [url]];
7907
- return new Promise((resolve5) => {
8035
+ return new Promise((resolve6) => {
7908
8036
  try {
7909
- const child = execFile3(command, args, { windowsHide: true }, (error) => resolve5(!error));
7910
- child.on("error", () => resolve5(false));
8037
+ const child = execFile3(command, args, { windowsHide: true }, (error) => resolve6(!error));
8038
+ child.on("error", () => resolve6(false));
7911
8039
  } catch {
7912
- resolve5(false);
8040
+ resolve6(false);
7913
8041
  }
7914
8042
  });
7915
8043
  }
@@ -7918,7 +8046,7 @@ function openInDefaultBrowser(url) {
7918
8046
  import { execFileSync } from "child_process";
7919
8047
  import { mkdirSync, readFileSync as readFileSync2, renameSync, rmSync, writeFileSync, existsSync as existsSync2 } from "fs";
7920
8048
  import { userInfo } from "os";
7921
- import { dirname as dirname10, join as join17 } from "path";
8049
+ import { dirname as dirname11, join as join19 } from "path";
7922
8050
 
7923
8051
  // src/modules/keychain-backend.ts
7924
8052
  import { createRequire as createRequire2 } from "module";
@@ -8007,7 +8135,7 @@ var FileSecretBackend = class {
8007
8135
  rmSync(this.filePath, { force: true });
8008
8136
  return;
8009
8137
  }
8010
- mkdirSync(dirname10(this.filePath), { recursive: true });
8138
+ mkdirSync(dirname11(this.filePath), { recursive: true });
8011
8139
  const tmp = `${this.filePath}.${process.pid}.tmp`;
8012
8140
  writeFileSync(tmp, JSON.stringify(entries), { encoding: "utf8", mode: 384 });
8013
8141
  try {
@@ -8052,7 +8180,7 @@ function restrictToCurrentUser(filePath) {
8052
8180
  });
8053
8181
  }
8054
8182
  function insecureSessionFilePath(homeDir) {
8055
- return join17(userTierRoot(homeDir), INSECURE_SESSION_FILE_NAME);
8183
+ return join19(userTierRoot(homeDir), INSECURE_SESSION_FILE_NAME);
8056
8184
  }
8057
8185
  function createSessionStorage(options = {}) {
8058
8186
  const keychain = new ChunkedSecretStore(options.keychainBackend ?? createKeychainBackend());
@@ -8118,7 +8246,7 @@ function createSessionStorage(options = {}) {
8118
8246
  }
8119
8247
 
8120
8248
  // src/commands/login.ts
8121
- function defaultLoginDeps() {
8249
+ function defaultLoginDeps(assetsDir) {
8122
8250
  return {
8123
8251
  storage: createSessionStorage(),
8124
8252
  environment: () => resolveIdentityEnvironment(),
@@ -8126,10 +8254,12 @@ function defaultLoginDeps() {
8126
8254
  browserLogin,
8127
8255
  deviceLogin,
8128
8256
  openUrl: openInDefaultBrowser,
8129
- isInteractive: () => Boolean(process.stdin.isTTY && process.stdout.isTTY)
8257
+ isInteractive: () => Boolean(process.stdin.isTTY && process.stdout.isTTY),
8258
+ resultPages: () => loadResultPages(assetsDir)
8130
8259
  };
8131
8260
  }
8132
8261
  var INSECURE_STORAGE_WARNING = "Insecure storage: the session is kept in a file protected only by your account's file permissions, not in the OS keychain. Run `spell logout` to remove it.";
8262
+ var MISSING_RESULT_PAGES_WARNING = "This arcane-cli install is missing its sign-in result pages (assets/sign-in/), so the browser will show a plain page. Reinstall the package to restore them.";
8133
8263
  function safeErrorText(error) {
8134
8264
  const message = error instanceof Error ? error.message : String(error);
8135
8265
  return message.split("\n")[0].replace(/([?&](?:code|state|id_token|access_token|refresh_token)=)[^&\s]+/gi, "$1[redacted]");
@@ -8149,7 +8279,7 @@ function describeAccount(record) {
8149
8279
  const who = record.email ? `${record.email} (${record.sub})` : record.sub;
8150
8280
  return record.environment === "development" ? `${who} [development]` : who;
8151
8281
  }
8152
- async function runLogin(options, deps = defaultLoginDeps()) {
8282
+ async function runLogin(options, deps) {
8153
8283
  let environment;
8154
8284
  try {
8155
8285
  environment = deps.environment();
@@ -8195,14 +8325,16 @@ async function runLogin(options, deps = defaultLoginDeps()) {
8195
8325
  });
8196
8326
  saveValidatedSession(result);
8197
8327
  } else {
8328
+ const pages = await deps.resultPages();
8329
+ if (!pages) printWarning(MISSING_RESULT_PAGES_WARNING);
8198
8330
  printInfo("Opening your browser to sign in to Arcane. Come back here when it says you are signed in.");
8199
8331
  await deps.browserLogin(config, {
8200
8332
  scopes: SIGN_IN_SCOPES,
8201
8333
  openUrl: deps.openUrl,
8334
+ pages: pages ?? void 0,
8202
8335
  onValidated: saveValidatedSession,
8203
- onCannotOpen: (url) => {
8204
- printWarning("No browser could be opened. Open this address yourself, or run `spell login --device-code`:");
8205
- printInfo(url);
8336
+ onCannotOpen: () => {
8337
+ printWarning("No browser could be opened. Run `spell login --device-code` to sign in on another device.");
8206
8338
  }
8207
8339
  });
8208
8340
  if (!record) throw new Error("The browser sign-in ended without storing a session.");
@@ -8316,15 +8448,15 @@ async function runWhoami(options, deps = defaultWhoamiDeps()) {
8316
8448
 
8317
8449
  // src/commands/report.ts
8318
8450
  import { access as access5 } from "fs/promises";
8319
- import { join as join20 } from "path";
8451
+ import { join as join22 } from "path";
8320
8452
 
8321
8453
  // src/modules/show-report/generate.ts
8322
- import { mkdir as mkdir8, readdir as readdir11, readFile as readFile17, writeFile as writeFile12 } from "fs/promises";
8323
- import { basename as basename3, dirname as dirname11, isAbsolute, join as join19, relative as relative3, resolve as resolve4 } from "path";
8454
+ import { mkdir as mkdir8, readdir as readdir11, readFile as readFile18, writeFile as writeFile12 } from "fs/promises";
8455
+ import { basename as basename4, dirname as dirname12, isAbsolute, join as join21, relative as relative3, resolve as resolve5 } from "path";
8324
8456
 
8325
8457
  // src/modules/show-report/model.ts
8326
- import { readFile as readFile16 } from "fs/promises";
8327
- import { join as join18 } from "path";
8458
+ import { readFile as readFile17 } from "fs/promises";
8459
+ import { join as join20 } from "path";
8328
8460
 
8329
8461
  // src/modules/show-report/plan-parser.ts
8330
8462
  function parseFrontmatter(content) {
@@ -8597,7 +8729,7 @@ async function getCast(cwd, fromRef, toRef, generatedOutputs = []) {
8597
8729
  // src/modules/show-report/model.ts
8598
8730
  async function readOptional(path4) {
8599
8731
  try {
8600
- return await readFile16(path4, "utf8");
8732
+ return await readFile17(path4, "utf8");
8601
8733
  } catch {
8602
8734
  return null;
8603
8735
  }
@@ -8693,13 +8825,13 @@ function buildCorrections(ledgerRows, programName) {
8693
8825
  };
8694
8826
  }
8695
8827
  async function buildShowReportModel(options) {
8696
- const planAbsPath = join18(options.rootDir, options.planRelPath);
8697
- const queueAbsPath = join18(options.rootDir, options.queueRelPath);
8828
+ const planAbsPath = join20(options.rootDir, options.planRelPath);
8829
+ const queueAbsPath = join20(options.rootDir, options.queueRelPath);
8698
8830
  const ledgerRelPath = "docs/verification-ledger.md";
8699
- const ledgerAbsPath = join18(options.rootDir, ledgerRelPath);
8700
- const decisionsAbsPath = join18(options.rootDir, "DECISIONS.md");
8831
+ const ledgerAbsPath = join20(options.rootDir, ledgerRelPath);
8832
+ const decisionsAbsPath = join20(options.rootDir, "DECISIONS.md");
8701
8833
  const [planContent, queueContent, ledgerContent, decisionsContent] = await Promise.all([
8702
- readFile16(planAbsPath, "utf8"),
8834
+ readFile17(planAbsPath, "utf8"),
8703
8835
  readOptional(queueAbsPath),
8704
8836
  readOptional(ledgerAbsPath),
8705
8837
  readOptional(decisionsAbsPath)
@@ -8895,7 +9027,7 @@ function toPosix(path4) {
8895
9027
  async function programFromPlan(rootDir, slug, planRelPath) {
8896
9028
  let planContent;
8897
9029
  try {
8898
- planContent = await readFile17(join19(rootDir, planRelPath), "utf8");
9030
+ planContent = await readFile18(join21(rootDir, planRelPath), "utf8");
8899
9031
  } catch {
8900
9032
  return { skipped: { slug, reason: `no PLAN.md at ${planRelPath}` } };
8901
9033
  }
@@ -8907,13 +9039,13 @@ async function programFromPlan(rootDir, slug, planRelPath) {
8907
9039
  program: {
8908
9040
  slug,
8909
9041
  planRelPath,
8910
- queueRelPath: toPosix(join19(dirname11(planRelPath), "OPERATOR-QUEUE.md")),
9042
+ queueRelPath: toPosix(join21(dirname12(planRelPath), "OPERATOR-QUEUE.md")),
8911
9043
  ledgerProgramName: programNameFromTitle(frontmatter.title ?? slug)
8912
9044
  }
8913
9045
  };
8914
9046
  }
8915
9047
  async function discoverPrograms(rootDir) {
8916
- const plansDir = join19(rootDir, "docs", "plans");
9048
+ const plansDir = join21(rootDir, "docs", "plans");
8917
9049
  let entries;
8918
9050
  try {
8919
9051
  entries = (await readdir11(plansDir, { withFileTypes: true })).filter((e) => e.isDirectory());
@@ -8931,23 +9063,23 @@ async function discoverPrograms(rootDir) {
8931
9063
  return { programs, skipped };
8932
9064
  }
8933
9065
  async function programFromPlanPath(rootDir, planPath) {
8934
- const planAbs = isAbsolute(planPath) ? planPath : resolve4(rootDir, planPath);
9066
+ const planAbs = isAbsolute(planPath) ? planPath : resolve5(rootDir, planPath);
8935
9067
  const planRelPath = toPosix(relative3(rootDir, planAbs));
8936
- return programFromPlan(rootDir, basename3(dirname11(planAbs)), planRelPath);
9068
+ return programFromPlan(rootDir, basename4(dirname12(planAbs)), planRelPath);
8937
9069
  }
8938
9070
  function normalizeLineEndings2(content) {
8939
9071
  return content.replace(/\r\n?/g, "\n");
8940
9072
  }
8941
9073
  async function readOptionalFile(path4) {
8942
9074
  try {
8943
- return await readFile17(path4, "utf8");
9075
+ return await readFile18(path4, "utf8");
8944
9076
  } catch (error) {
8945
9077
  if (error.code === "ENOENT") return null;
8946
9078
  throw error;
8947
9079
  }
8948
9080
  }
8949
9081
  async function generateReport(rootDir, program2, templateHtml, theme = "auto") {
8950
- const planContent = await readFile17(join19(rootDir, program2.planRelPath), "utf8");
9082
+ const planContent = await readFile18(join21(rootDir, program2.planRelPath), "utf8");
8951
9083
  const frontmatter = parseFrontmatter(planContent);
8952
9084
  const model = await buildShowReportModel({
8953
9085
  rootDir,
@@ -8967,7 +9099,7 @@ async function generateReport(rootDir, program2, templateHtml, theme = "auto") {
8967
9099
  };
8968
9100
  }
8969
9101
  async function runReportGeneration(options) {
8970
- const templateHtml = await readFile17(options.templatePath, "utf8");
9102
+ const templateHtml = await readFile18(options.templatePath, "utf8");
8971
9103
  const theme = options.theme ?? "auto";
8972
9104
  const drifted = [];
8973
9105
  const repaired = [];
@@ -8978,19 +9110,19 @@ async function runReportGeneration(options) {
8978
9110
  if (!generated.themeApplied) themeSkipped.push(program2.slug);
8979
9111
  const missing = JSON.parse(generated.json).sections.flatMap((section) => section.rows).filter((row) => row.descriptionState === "unwritten").map((row) => row.id);
8980
9112
  if (missing.length > 0) unwritten.push({ slug: program2.slug, ids: missing });
8981
- const outAbs = options.outDir ? isAbsolute(options.outDir) ? options.outDir : resolve4(options.rootDir, options.outDir) : resolve4(options.rootDir, dirname11(program2.planRelPath));
9113
+ const outAbs = options.outDir ? isAbsolute(options.outDir) ? options.outDir : resolve5(options.rootDir, options.outDir) : resolve5(options.rootDir, dirname12(program2.planRelPath));
8982
9114
  const outRel = toPosix(relative3(options.rootDir, outAbs));
8983
9115
  const outputs = [
8984
9116
  [`${outRel}/show-report.json`, generated.json],
8985
9117
  [`${outRel}/show-report.html`, generated.html]
8986
9118
  ];
8987
9119
  for (const [relPath, expected] of outputs) {
8988
- const actual = await readOptionalFile(join19(options.rootDir, relPath));
9120
+ const actual = await readOptionalFile(join21(options.rootDir, relPath));
8989
9121
  if (actual === null || normalizeLineEndings2(actual) !== normalizeLineEndings2(expected)) {
8990
9122
  drifted.push(`${relPath} does not match a regeneration from its sources.`);
8991
9123
  if (options.mode === "write") {
8992
9124
  await mkdir8(outAbs, { recursive: true });
8993
- await writeFile12(join19(options.rootDir, relPath), expected, "utf8");
9125
+ await writeFile12(join21(options.rootDir, relPath), expected, "utf8");
8994
9126
  repaired.push(relPath);
8995
9127
  }
8996
9128
  }
@@ -9001,7 +9133,7 @@ async function runReportGeneration(options) {
9001
9133
 
9002
9134
  // src/commands/report.ts
9003
9135
  async function runReport(targetDir, options = {}, assetsDir) {
9004
- const templatePath = join20(assetsDir, "report", "show-report.template.html");
9136
+ const templatePath = join22(assetsDir, "report", "show-report.template.html");
9005
9137
  try {
9006
9138
  await access5(templatePath);
9007
9139
  } catch {
@@ -9075,10 +9207,10 @@ async function runReport(targetDir, options = {}, assetsDir) {
9075
9207
  }
9076
9208
 
9077
9209
  // src/index.ts
9078
- import { realpathSync } from "fs";
9210
+ import { realpathSync as realpathSync2 } from "fs";
9079
9211
  var require3 = createRequire3(import.meta.url);
9080
9212
  var pkg = require3("../package.json");
9081
- var ASSETS_DIR = process.env["ARCANE_ASSETS_DIR"] ?? join21(dirname12(fileURLToPath(import.meta.url)), "assets");
9213
+ var ASSETS_DIR = process.env["ARCANE_ASSETS_DIR"] ?? join23(dirname13(fileURLToPath(import.meta.url)), "assets");
9082
9214
  var program = new Command();
9083
9215
  var USER_FLAG_DESCRIPTION = "Operate on the per-user tier at ~/.arcane \u2014 spells shared by every repository on this machine, fanned out to ~/.agents/skills (Codex, Copilot) and ~/.claude/commands (Claude Code)";
9084
9216
  var USER_TIER_AGENTS_DESCRIPTION = "Operate on the per-user tier at ~/.arcane \u2014 one agent roster shared by every repository on this machine, rendered to ~/.copilot/agents (VS Code agent modes)";
@@ -9205,7 +9337,7 @@ program.command("login").description("Sign in to your Arcane account (browser by
9205
9337
  "--insecure-storage",
9206
9338
  "Keep the session in a file under ~/.arcane instead of the OS keychain; protected only by your account's file permissions"
9207
9339
  ).action(async (opts) => {
9208
- await runLogin({ deviceCode: opts.deviceCode, insecureStorage: opts.insecureStorage });
9340
+ await runLogin({ deviceCode: opts.deviceCode, insecureStorage: opts.insecureStorage }, defaultLoginDeps(ASSETS_DIR));
9209
9341
  });
9210
9342
  program.command("logout").description("Sign out on this machine: remove the stored session").action(() => {
9211
9343
  runLogout();
@@ -9288,7 +9420,7 @@ agentsCommand.command("list").description("Show the current agent roster with as
9288
9420
  const scope = opts.user ? "user" : "repo";
9289
9421
  await runAgentsList(agentsTargetDir(scope, process.cwd()), scope);
9290
9422
  });
9291
- if (realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)) {
9423
+ if (realpathSync2(process.argv[1]) === fileURLToPath(import.meta.url)) {
9292
9424
  await program.parseAsync(process.argv);
9293
9425
  }
9294
9426
  export {