arcane-cli 0.24.1 → 0.25.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/dist/index.js CHANGED
@@ -6,7 +6,7 @@ const require = __createRequire(import.meta.url);
6
6
  import { Command } from "commander";
7
7
  import { createRequire as createRequire2 } from "module";
8
8
  import { fileURLToPath } from "url";
9
- import { dirname as dirname4, join as join12 } from "path";
9
+ import { dirname as dirname4, join as join14 } from "path";
10
10
 
11
11
  // src/commands/init.ts
12
12
  import { select as select2, confirm, input as input2 } from "@inquirer/prompts";
@@ -3858,8 +3858,8 @@ async function checkVSCodeExtension(id, label) {
3858
3858
  ];
3859
3859
  for (const dir of extDirs) {
3860
3860
  try {
3861
- const { readdir: readdir5 } = await import("fs/promises");
3862
- const entries = await readdir5(dir);
3861
+ const { readdir: readdir7 } = await import("fs/promises");
3862
+ const entries = await readdir7(dir);
3863
3863
  const prefix = id.toLowerCase() + "-";
3864
3864
  const found = entries.some(
3865
3865
  (e) => e.toLowerCase() === id.toLowerCase() || e.toLowerCase().startsWith(prefix)
@@ -4194,6 +4194,215 @@ async function runDoctor(targetDir, options = {}, assetsDir) {
4194
4194
  }
4195
4195
  }
4196
4196
 
4197
+ // src/modules/ward.ts
4198
+ import { readdir as readdir6, readFile as readFile8 } from "fs/promises";
4199
+ import { join as join13, relative as relative2 } from "path";
4200
+
4201
+ // src/modules/denylist-scan.ts
4202
+ import { readFile as readFile7, readdir as readdir5 } from "fs/promises";
4203
+ import { join as join12, relative } from "path";
4204
+ function escapeRegExp(value) {
4205
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4206
+ }
4207
+ function createDenylistRules(tokens, labelPrefix) {
4208
+ return tokens.map((token, index) => ({
4209
+ label: `${labelPrefix}-${index + 1}`,
4210
+ pattern: new RegExp(
4211
+ `(?:^|[^A-Za-z0-9])${escapeRegExp(token)}(?=$|[^A-Za-z0-9])`,
4212
+ "i"
4213
+ )
4214
+ }));
4215
+ }
4216
+ var SKIP_DIRECTORIES = /* @__PURE__ */ new Set([
4217
+ "node_modules",
4218
+ ".git",
4219
+ "dist",
4220
+ "coverage",
4221
+ ".vitest"
4222
+ ]);
4223
+ var DEFAULT_SCANNABLE_EXTENSIONS = [".md", ".ts", ".js", ".json", ".yml", ".yaml"];
4224
+ async function collectScannableFiles(root, repoRoot = root, extensions = DEFAULT_SCANNABLE_EXTENSIONS) {
4225
+ const collected = [];
4226
+ let entries;
4227
+ try {
4228
+ entries = await readdir5(root, { withFileTypes: true });
4229
+ } catch {
4230
+ return collected;
4231
+ }
4232
+ for (const entry of entries) {
4233
+ const absolutePath = join12(root, entry.name);
4234
+ if (entry.isDirectory()) {
4235
+ if (SKIP_DIRECTORIES.has(entry.name)) continue;
4236
+ collected.push(...await collectScannableFiles(absolutePath, repoRoot, extensions));
4237
+ } else if (extensions.some((ext) => entry.name.endsWith(ext))) {
4238
+ const displayPath = relative(repoRoot, absolutePath).replace(/\\/g, "/");
4239
+ collected.push([absolutePath, displayPath]);
4240
+ }
4241
+ }
4242
+ return collected;
4243
+ }
4244
+
4245
+ // src/modules/ward.ts
4246
+ async function scanFileAllRules(absolutePath, displayPath, rules) {
4247
+ const findings = [];
4248
+ let content;
4249
+ try {
4250
+ content = await readFile8(absolutePath, "utf8");
4251
+ } catch {
4252
+ return findings;
4253
+ }
4254
+ const lines = content.split("\n");
4255
+ for (let index = 0; index < lines.length; index++) {
4256
+ const searchable = lines[index].replace(/\{[A-Z][A-Z0-9_]*\}/g, "");
4257
+ for (const rule of rules) {
4258
+ if (rule.pattern.test(searchable)) {
4259
+ findings.push({ file: displayPath, line: index + 1, rule: rule.label });
4260
+ }
4261
+ }
4262
+ }
4263
+ return findings;
4264
+ }
4265
+ var VENDOR_IDENTIFIER_PROTECTION_LIST = [
4266
+ "claude",
4267
+ "gpt-4",
4268
+ "gpt-3.5",
4269
+ "gemini",
4270
+ "llama",
4271
+ "mistral",
4272
+ "copilot",
4273
+ "anthropic",
4274
+ "openai"
4275
+ ];
4276
+ var GREP_PROOF_EXTENSIONS = [".gif", ".png", ".jpg", ".jpeg", ".mp4", ".mov", ".mp3", ".wav"];
4277
+ var WARD_TEXT_EXTENSIONS = [
4278
+ ".md",
4279
+ ".ts",
4280
+ ".tsx",
4281
+ ".js",
4282
+ ".jsx",
4283
+ ".json",
4284
+ ".yml",
4285
+ ".yaml",
4286
+ ".txt",
4287
+ ".mdx"
4288
+ ];
4289
+ function applyExclusions(findings, fileLines, exclusions) {
4290
+ if (exclusions.length === 0) return findings;
4291
+ return findings.filter((finding) => {
4292
+ const applicable = exclusions.filter((e) => e.rule === finding.rule);
4293
+ if (applicable.length === 0) return true;
4294
+ const lines = fileLines.get(finding.file);
4295
+ const lineText = lines?.[finding.line - 1] ?? "";
4296
+ return !applicable.some((e) => lineText.includes(e.context));
4297
+ });
4298
+ }
4299
+ async function scanFilenames(root, rules, repoRoot = root) {
4300
+ const findings = [];
4301
+ let entries;
4302
+ try {
4303
+ entries = await readdir6(root, { withFileTypes: true });
4304
+ } catch {
4305
+ return findings;
4306
+ }
4307
+ for (const entry of entries) {
4308
+ const absolutePath = join13(root, entry.name);
4309
+ if (entry.isDirectory()) {
4310
+ if (SKIP_DIRECTORIES.has(entry.name)) continue;
4311
+ findings.push(...await scanFilenames(absolutePath, rules, repoRoot));
4312
+ continue;
4313
+ }
4314
+ const displayPath = relative2(repoRoot, absolutePath).replace(/\\/g, "/");
4315
+ for (const rule of rules) {
4316
+ if (rule.pattern.test(entry.name)) {
4317
+ findings.push({ file: displayPath, line: 0, rule: rule.label });
4318
+ break;
4319
+ }
4320
+ }
4321
+ }
4322
+ return findings;
4323
+ }
4324
+ async function flagGrepProofMedia(root, repoRoot = root) {
4325
+ const files = await collectScannableFiles(root, repoRoot, GREP_PROOF_EXTENSIONS);
4326
+ return files.map(([, displayPath]) => ({
4327
+ file: displayPath,
4328
+ reason: "binary media format -- text extraction is unreliable; review manually for embedded identifiers"
4329
+ }));
4330
+ }
4331
+ async function runWard(repoRoot, options = {}) {
4332
+ const userTerms = options.terms ?? [];
4333
+ const exclusions = options.exclusions ?? [];
4334
+ const extensions = [...WARD_TEXT_EXTENSIONS, ...options.binaryExtensions ?? []];
4335
+ const userRules = createDenylistRules(userTerms, "ward-term");
4336
+ const vendorRules = createDenylistRules([...VENDOR_IDENTIFIER_PROTECTION_LIST], "ward-vendor");
4337
+ const allRules = [...userRules, ...vendorRules];
4338
+ const contentFiles = await collectScannableFiles(repoRoot, repoRoot, extensions);
4339
+ const fileLines = /* @__PURE__ */ new Map();
4340
+ const contentFindings = [];
4341
+ for (const [absolutePath, displayPath] of contentFiles) {
4342
+ const findings2 = await scanFileAllRules(absolutePath, displayPath, allRules);
4343
+ if (findings2.length > 0) {
4344
+ try {
4345
+ fileLines.set(displayPath, (await readFile8(absolutePath, "utf8")).split("\n"));
4346
+ } catch {
4347
+ }
4348
+ }
4349
+ contentFindings.push(...findings2);
4350
+ }
4351
+ const filenameFindings = await scanFilenames(repoRoot, allRules);
4352
+ const combined = [...contentFindings, ...filenameFindings];
4353
+ const excluded = applyExclusions(combined, fileLines, exclusions);
4354
+ const vendorLabels = new Set(vendorRules.map((r) => r.label));
4355
+ const findings = excluded.map((f) => ({
4356
+ ...f,
4357
+ category: vendorLabels.has(f.rule) ? "protected-vendor-identifier" : "leak"
4358
+ }));
4359
+ const mediaFlags = await flagGrepProofMedia(repoRoot);
4360
+ return { findings, mediaFlags };
4361
+ }
4362
+
4363
+ // src/commands/ward.ts
4364
+ function parseTerms(raw) {
4365
+ if (!raw) return [];
4366
+ return raw.split(",").map((t) => t.trim()).filter(Boolean);
4367
+ }
4368
+ async function runWardCli(targetDir, options = {}, exclusions = []) {
4369
+ const terms = parseTerms(options.terms);
4370
+ const report = await runWard(targetDir, { terms, exclusions });
4371
+ console.log("\nspell ward \u2014 scanning for leaked identifiers\n");
4372
+ const leaks = report.findings.filter((f) => f.category === "leak");
4373
+ const protectedHits = report.findings.filter((f) => f.category === "protected-vendor-identifier");
4374
+ if (leaks.length === 0 && protectedHits.length === 0) {
4375
+ console.log(" \u2713 No denylisted terms found.");
4376
+ }
4377
+ for (const finding of leaks) {
4378
+ const where = finding.line === 0 ? `${finding.file} (filename)` : `${finding.file}:${finding.line}`;
4379
+ console.log(` \u2717 [leak] ${where} \u2014 rule: ${finding.rule}`);
4380
+ }
4381
+ for (const finding of protectedHits) {
4382
+ const where = finding.line === 0 ? `${finding.file} (filename)` : `${finding.file}:${finding.line}`;
4383
+ console.log(
4384
+ ` \u26A0 [protected-vendor-identifier] ${where} \u2014 rule: ${finding.rule} \u2014 verify before any automated rename touches this line`
4385
+ );
4386
+ }
4387
+ if (report.mediaFlags.length > 0) {
4388
+ console.log("\n Grep-proof media flagged for manual review:");
4389
+ for (const flag of report.mediaFlags) {
4390
+ console.log(` ? ${flag.file} \u2014 ${flag.reason}`);
4391
+ }
4392
+ }
4393
+ console.log();
4394
+ if (options.gate && leaks.length > 0) {
4395
+ console.log(` ${leaks.length} leak(s) found. Failing (--gate mode).
4396
+ `);
4397
+ process.exitCode = 1;
4398
+ return;
4399
+ }
4400
+ if (leaks.length > 0) {
4401
+ console.log(` ${leaks.length} leak(s) found. Run with --gate to fail CI on this.
4402
+ `);
4403
+ }
4404
+ }
4405
+
4197
4406
  // src/commands/unblock-push.ts
4198
4407
  import { basename as basename2, resolve as resolve2 } from "path";
4199
4408
  import { input as input4 } from "@inquirer/prompts";
@@ -4296,7 +4505,7 @@ Name did not match ("${typed.trim()}" \u2260 "${repoName}"). Nothing changed.`);
4296
4505
  import { realpathSync } from "fs";
4297
4506
  var require3 = createRequire2(import.meta.url);
4298
4507
  var pkg = require3("../package.json");
4299
- var ASSETS_DIR = process.env["ARCANE_ASSETS_DIR"] ?? join12(dirname4(fileURLToPath(import.meta.url)), "assets");
4508
+ var ASSETS_DIR = process.env["ARCANE_ASSETS_DIR"] ?? join14(dirname4(fileURLToPath(import.meta.url)), "assets");
4300
4509
  var program = new Command();
4301
4510
  program.name("spell").description("Arcane framework CLI \u2014 scaffold and manage governance files").version(pkg.version).action(async () => {
4302
4511
  if (program.args.length > 0) {
@@ -4362,6 +4571,9 @@ program.command("unblock-push").description("Lift this repository's push block (
4362
4571
  program.command("doctor").description("Check your environment for Arcane prerequisites").option("--fix", "Automatically create missing session continuity files").action(async (opts) => {
4363
4572
  await runDoctor(process.cwd(), { fix: opts.fix }, ASSETS_DIR);
4364
4573
  });
4574
+ program.command("ward").description("Scan the repo for leaked third-party identifiers and trademarks (T18)").option("--terms <terms>", "Comma-separated denylist terms, in addition to the built-in vendor-identifier list").option("--gate", "Exit non-zero if any leak is found (for CI)").action(async (opts) => {
4575
+ await runWardCli(process.cwd(), opts);
4576
+ });
4365
4577
  var agentsCommand = program.command("agents").description("Manage the portable agent system (init, sync, list)");
4366
4578
  agentsCommand.command("init").description(
4367
4579
  "Interactive agent roster setup \u2014 choose profile, naming strategy, and install definitions"