arcane-cli 1.5.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/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
- await writeFile2(filePath, JSON.stringify(manifest, null, 2), "utf-8");
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 {
@@ -4353,6 +4362,9 @@ function defaultScopePatch(options, manifest) {
4353
4362
  function migrateLegacyComponents(components) {
4354
4363
  const out = [];
4355
4364
  const seen = /* @__PURE__ */ new Set();
4365
+ const inheritedFiles = [];
4366
+ const inheritedHashes = {};
4367
+ let carrier = null;
4356
4368
  for (const entry of components) {
4357
4369
  const replacements = LEGACY_COMPONENT_MIGRATIONS[entry.name];
4358
4370
  if (!replacements) {
@@ -4361,13 +4373,24 @@ function migrateLegacyComponents(components) {
4361
4373
  out.push(entry);
4362
4374
  continue;
4363
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
+ }
4364
4383
  for (const name of replacements) {
4384
+ carrier ??= name;
4365
4385
  if (seen.has(name)) continue;
4366
4386
  seen.add(name);
4367
4387
  out.push({ name, files: [], installedVersion: entry.installedVersion });
4368
4388
  }
4369
4389
  }
4370
- return out;
4390
+ return {
4391
+ components: out,
4392
+ inherited: { files: inheritedFiles, fileHashes: inheritedHashes, carrier }
4393
+ };
4371
4394
  }
4372
4395
  async function resolveOrphan(targetDir, file, recordedHash, prune) {
4373
4396
  try {
@@ -4392,6 +4415,19 @@ async function resolveOrphan(targetDir, file, recordedHash, prune) {
4392
4415
  console.log(` Pruned orphaned file: ${file}`);
4393
4416
  return "pruned";
4394
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
+ }
4395
4431
  function findReadoptedSpellComponents(components, scope = "repo", spellScope = "repo") {
4396
4432
  if (scope !== "repo" || spellScope !== "repo") return /* @__PURE__ */ new Set();
4397
4433
  return new Set(
@@ -4539,7 +4575,17 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
4539
4575
  const conflictedFiles = [];
4540
4576
  const customizedShims = [];
4541
4577
  const orphanReport = [];
4542
- const componentsToUpdate = migrateLegacyComponents(manifest.components);
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
+ }
4543
4589
  for (const installed of componentsToUpdate) {
4544
4590
  let component;
4545
4591
  try {
@@ -4658,24 +4704,11 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
4658
4704
  }
4659
4705
  for (const file of installed.files) {
4660
4706
  if (updatedFiles.includes(file)) continue;
4661
- const recordedHash = installed.fileHashes?.[file];
4662
- const status = await resolveOrphan(
4663
- targetDir,
4707
+ orphanCandidates.push({
4708
+ owner: installed.name,
4664
4709
  file,
4665
- recordedHash,
4666
- Boolean(options.prune) && !options.dryRun
4667
- );
4668
- orphanReport.push({ file, status });
4669
- if (status === "reported") {
4670
- try {
4671
- validateTargetPath(targetDir, file);
4672
- if (await fileExists(join9(targetDir, file))) {
4673
- updatedFiles.push(file);
4674
- if (recordedHash !== void 0) fileHashes[file] = recordedHash;
4675
- }
4676
- } catch {
4677
- }
4678
- }
4710
+ recordedHash: installed.fileHashes?.[file]
4711
+ });
4679
4712
  }
4680
4713
  updatedComponents.push({
4681
4714
  ...installed,
@@ -4684,6 +4717,26 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
4684
4717
  fileHashes
4685
4718
  });
4686
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
+ }
4687
4740
  const orphansToReport = orphanReport.filter((o) => o.status !== "not-found");
4688
4741
  if (orphansToReport.length > 0) {
4689
4742
  const verb = options.dryRun ? "[dry-run] Found" : "Found";
@@ -5189,8 +5242,6 @@ async function uninstallUserTier(options, storeRoot, manifest) {
5189
5242
  // src/commands/doctor.ts
5190
5243
  import { access as access4, readdir as readdir8, readFile as readFile10, mkdir as mkdir7, copyFile as fsCopyFile } from "fs/promises";
5191
5244
  import { join as join13, dirname as dirname9 } from "path";
5192
- import { execFile as execFile3 } from "child_process";
5193
- import { promisify as promisify2 } from "util";
5194
5245
 
5195
5246
  // src/config/incidents.ts
5196
5247
  var INCIDENT_QUEUE = [
@@ -5331,10 +5382,33 @@ async function scanRepository(repoRoot, rules, extensions, excludePrefixes = [])
5331
5382
  return findings;
5332
5383
  }
5333
5384
 
5334
- // src/modules/platform-policy.ts
5385
+ // src/modules/exec.ts
5335
5386
  import { execFile as execFile2 } from "child_process";
5336
- import { promisify } from "util";
5337
- var execFileAsync = promisify(execFile2);
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
5338
5412
  var SANCTIONED_MERGE_METHODS = ["merge", "rebase"];
5339
5413
  function compareToLadder(effective, declaredButBlocked, interactionNote) {
5340
5414
  const disallowedButPermitted = effective.filter(
@@ -5401,17 +5475,19 @@ function evaluateGitHubMergePolicy(rulesets) {
5401
5475
  }
5402
5476
  async function fetchGitHubRulesets(owner, repo) {
5403
5477
  try {
5404
- const { stdout: listOut } = await execFileAsync("gh", [
5405
- "api",
5406
- `repos/${owner}/${repo}/rulesets`
5407
- ]);
5478
+ const { stdout: listOut } = await execFileWithTimeout(
5479
+ "gh",
5480
+ ["api", `repos/${owner}/${repo}/rulesets`],
5481
+ EXTERNAL_CLI_TIMEOUT_MS
5482
+ );
5408
5483
  const summaries = JSON.parse(listOut);
5409
5484
  const detailed = await Promise.all(
5410
5485
  summaries.map(async (s) => {
5411
- const { stdout } = await execFileAsync("gh", [
5412
- "api",
5413
- `repos/${owner}/${repo}/rulesets/${s.id}`
5414
- ]);
5486
+ const { stdout } = await execFileWithTimeout(
5487
+ "gh",
5488
+ ["api", `repos/${owner}/${repo}/rulesets/${s.id}`],
5489
+ EXTERNAL_CLI_TIMEOUT_MS
5490
+ );
5415
5491
  return JSON.parse(stdout);
5416
5492
  })
5417
5493
  );
@@ -5443,7 +5519,7 @@ function evaluateAdoMergePolicy(policies) {
5443
5519
  async function fetchAdoMergeTypePolicies(org, project, repoName, branch) {
5444
5520
  try {
5445
5521
  const orgUrl = `https://dev.azure.com/${org}`;
5446
- const { stdout: repoOut } = await execFileAsync("az", [
5522
+ const { stdout: repoOut } = await execFileWithTimeout("az", [
5447
5523
  "repos",
5448
5524
  "show",
5449
5525
  "--repository",
@@ -5456,10 +5532,10 @@ async function fetchAdoMergeTypePolicies(org, project, repoName, branch) {
5456
5532
  "id",
5457
5533
  "--output",
5458
5534
  "tsv"
5459
- ]);
5535
+ ], EXTERNAL_CLI_TIMEOUT_MS);
5460
5536
  const repositoryId = repoOut.trim();
5461
5537
  if (!repositoryId) return null;
5462
- const { stdout } = await execFileAsync("az", [
5538
+ const { stdout } = await execFileWithTimeout("az", [
5463
5539
  "repos",
5464
5540
  "policy",
5465
5541
  "list",
@@ -5473,7 +5549,7 @@ async function fetchAdoMergeTypePolicies(org, project, repoName, branch) {
5473
5549
  project,
5474
5550
  "--output",
5475
5551
  "json"
5476
- ]);
5552
+ ], EXTERNAL_CLI_TIMEOUT_MS);
5477
5553
  return JSON.parse(stdout);
5478
5554
  } catch {
5479
5555
  return null;
@@ -5501,7 +5577,6 @@ function parseAdoRemote(url) {
5501
5577
  }
5502
5578
 
5503
5579
  // src/commands/doctor.ts
5504
- var execFileAsync2 = promisify2(execFile3);
5505
5580
  function isRecord3(value) {
5506
5581
  return typeof value === "object" && value !== null && !Array.isArray(value);
5507
5582
  }
@@ -5518,40 +5593,33 @@ async function checkNodeVersion() {
5518
5593
  message: `${raw} \u2014 Node.js 18 or later is required`
5519
5594
  };
5520
5595
  }
5521
- async function checkVSCodeExtension(id, label) {
5596
+ async function checkVSCodeExtension(id, label, homeDir = resolveHomeDir()) {
5522
5597
  const name = `VS Code extension: ${label}`;
5523
- const home = process.env["HOME"] ?? "";
5524
- const extDirs = [
5525
- join13(home, ".vscode", "extensions"),
5526
- 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" }
5527
5601
  ];
5528
- for (const dir of extDirs) {
5602
+ let installBin = null;
5603
+ for (const flavour of flavours) {
5604
+ let entries;
5529
5605
  try {
5530
- const { readdir: readdir11 } = await import("fs/promises");
5531
- const entries = await readdir11(dir);
5532
- const prefix = id.toLowerCase() + "-";
5533
- const found = entries.some(
5534
- (e) => e.toLowerCase() === id.toLowerCase() || e.toLowerCase().startsWith(prefix)
5535
- );
5536
- if (found) {
5537
- return { name, passed: true, message: `${id} is installed` };
5538
- }
5606
+ entries = await readdir8(flavour.dir);
5539
5607
  } catch {
5608
+ continue;
5540
5609
  }
5541
- }
5542
- let installBin = "code";
5543
- for (const bin of ["code", "code-insiders"]) {
5544
- try {
5545
- await execFileAsync2(bin, ["--version"]);
5546
- installBin = bin;
5547
- break;
5548
- } 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` };
5549
5617
  }
5550
5618
  }
5551
5619
  return {
5552
5620
  name,
5553
5621
  passed: false,
5554
- message: `${id} not found. Install via: ${installBin} --install-extension ${id}`,
5622
+ message: `${id} not found. Install via: ${installBin ?? "code"} --install-extension ${id}`,
5555
5623
  blocking: false
5556
5624
  };
5557
5625
  }
@@ -6037,7 +6105,7 @@ async function runDoctor(targetDir, options = {}, assetsDir) {
6037
6105
  }
6038
6106
  const results = await Promise.all([
6039
6107
  checkNodeVersion(),
6040
- checkVSCodeExtension("GitHub.copilot-chat", "GitHub Copilot (Chat)"),
6108
+ checkVSCodeExtension("GitHub.copilot-chat", "GitHub Copilot (Chat)", resolveHomeDir()),
6041
6109
  checkArcaneManifest(targetDir),
6042
6110
  Promise.resolve(checkIncidentReleaseGate()),
6043
6111
  checkPullRebase(targetDir),
@@ -6067,26 +6135,24 @@ async function runDoctor(targetDir, options = {}, assetsDir) {
6067
6135
  console.log(" All checks passed. Your environment is ready.\n");
6068
6136
  } else {
6069
6137
  console.log(" One or more checks failed. Fix the issues above before proceeding.\n");
6070
- const hasMissingSession = sessionResults.some((r) => !r.passed);
6071
- if (options.fix && hasMissingSession && assetsDir) {
6072
- console.log(" \u{1F527} Fixing missing session continuity files...\n");
6073
- const created = await fixSessionContinuity(targetDir, assetsDir);
6074
- if (created.length > 0) {
6075
- for (const file of created) {
6076
- console.log(` \u2713 Created: ${file}`);
6077
- }
6078
- 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(`
6079
6149
  ${created.length} file(s) created. Repo is now close-session-ready.
6080
6150
  `);
6081
- } else {
6082
- console.log(" No files needed creation (all present).\n");
6083
- }
6084
- } else if (hasMissingSession && !options.fix) {
6085
- console.log(" \u{1F4A1} Run `spell doctor --fix` to create missing session files.\n");
6086
- }
6087
- if (!allPassed) {
6088
- process.exitCode = 1;
6151
+ } else {
6152
+ console.log(" No files needed creation (all present).\n");
6089
6153
  }
6154
+ } else if (hasMissingSession && !options.fix) {
6155
+ console.log(" \u{1F4A1} Run `spell doctor --fix` to create missing session files.\n");
6090
6156
  }
6091
6157
  }
6092
6158