instrlint 0.1.2 → 0.1.5

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/cli.js CHANGED
@@ -5,6 +5,7 @@ import { Command } from "commander";
5
5
 
6
6
  // src/commands/run-command.ts
7
7
  import { execSync } from "child_process";
8
+ import { createInterface } from "readline";
8
9
  import { basename as basename2 } from "path";
9
10
  import chalk4 from "chalk";
10
11
 
@@ -787,8 +788,12 @@ function analyzeBudget(instructions) {
787
788
  autoFixable: false
788
789
  });
789
790
  }
790
- const { tokens: rulesTokens, method: rulesMethod } = sumTokens(instructions.rules);
791
- const { tokens: skillsTokens, method: skillsMethod } = sumTokens(instructions.skills);
791
+ const { tokens: rulesTokens, method: rulesMethod } = sumTokens(
792
+ instructions.rules
793
+ );
794
+ const { tokens: skillsTokens, method: skillsMethod } = sumTokens(
795
+ instructions.skills
796
+ );
792
797
  const { tokens: subFilesTokens, method: subFilesMethod } = sumTokens(
793
798
  instructions.subFiles
794
799
  );
@@ -826,11 +831,18 @@ function analyzeBudget(instructions) {
826
831
  autoFixable: false
827
832
  });
828
833
  }
829
- const tokenMethod = [rootFileMethod, rulesMethod, skillsMethod, subFilesMethod].every(
830
- (m) => m === "measured"
831
- ) ? "measured" : "estimated";
834
+ const tokenMethod = [
835
+ rootFileMethod,
836
+ rulesMethod,
837
+ skillsMethod,
838
+ subFilesMethod
839
+ ].every((m) => m === "measured") ? "measured" : "estimated";
832
840
  const fileBreakdown = [
833
- { path: instructions.rootFile.path, tokenCount: rootFileTokens, tokenMethod: rootFileMethod },
841
+ {
842
+ path: instructions.rootFile.path,
843
+ tokenCount: rootFileTokens,
844
+ tokenMethod: rootFileMethod
845
+ },
834
846
  ...instructions.rules.map((r) => ({
835
847
  path: r.path,
836
848
  tokenCount: r.tokenCount,
@@ -850,6 +862,7 @@ function analyzeBudget(instructions) {
850
862
  const summary = {
851
863
  systemPromptTokens: SYSTEM_PROMPT_TOKENS,
852
864
  rootFileTokens,
865
+ rootFileLines: rootLines,
853
866
  rootFileMethod,
854
867
  rulesTokens,
855
868
  rulesMethod,
@@ -1426,6 +1439,8 @@ var INFO_DEDUCTION = 1;
1426
1439
  var MAX_CRITICAL_DEDUCTION = 40;
1427
1440
  var MAX_WARNING_DEDUCTION = 30;
1428
1441
  var MAX_INFO_DEDUCTION = 10;
1442
+ var MAX_ROOT_FILE_PENALTY = 30;
1443
+ var MAX_BUDGET_DEDUCTION = 30;
1429
1444
  function gradeFromScore(score) {
1430
1445
  if (score >= 90) return "A";
1431
1446
  if (score >= 80) return "B";
@@ -1437,16 +1452,32 @@ function calculateScore(findings, budget) {
1437
1452
  const criticals = findings.filter((f) => f.severity === "critical").length;
1438
1453
  const warnings = findings.filter((f) => f.severity === "warning").length;
1439
1454
  const infos = findings.filter((f) => f.severity === "info").length;
1440
- const criticalDeduction = Math.min(criticals * CRITICAL_DEDUCTION, MAX_CRITICAL_DEDUCTION);
1441
- const warningDeduction = Math.min(warnings * WARNING_DEDUCTION, MAX_WARNING_DEDUCTION);
1455
+ const criticalDeduction = Math.min(
1456
+ criticals * CRITICAL_DEDUCTION,
1457
+ MAX_CRITICAL_DEDUCTION
1458
+ );
1459
+ const warningDeduction = Math.min(
1460
+ warnings * WARNING_DEDUCTION,
1461
+ MAX_WARNING_DEDUCTION
1462
+ );
1442
1463
  const infoDeduction = Math.min(infos * INFO_DEDUCTION, MAX_INFO_DEDUCTION);
1464
+ const rootLines = budget.rootFileLines;
1465
+ let rootFilePenalty = 0;
1466
+ if (rootLines > 400) {
1467
+ rootFilePenalty = 10 + Math.floor((rootLines - 400) / 100) * 5;
1468
+ } else if (rootLines > 200) {
1469
+ rootFilePenalty = 5 + Math.floor((rootLines - 200) / 100) * 3;
1470
+ }
1471
+ rootFilePenalty = Math.min(rootFilePenalty, MAX_ROOT_FILE_PENALTY);
1443
1472
  const baselinePct = budget.totalBaseline / CONTEXT_WINDOW2;
1444
1473
  let budgetDeduction = 0;
1445
- if (baselinePct > 0.5) budgetDeduction = 15;
1446
- else if (baselinePct > 0.25) budgetDeduction = 5;
1474
+ if (baselinePct > 0.25) {
1475
+ budgetDeduction = 5 + Math.floor((baselinePct - 0.25) * 40);
1476
+ }
1477
+ budgetDeduction = Math.min(budgetDeduction, MAX_BUDGET_DEDUCTION);
1447
1478
  const score = Math.max(
1448
1479
  0,
1449
- 100 - criticalDeduction - warningDeduction - infoDeduction - budgetDeduction
1480
+ 100 - criticalDeduction - warningDeduction - infoDeduction - rootFilePenalty - budgetDeduction
1450
1481
  );
1451
1482
  return { score, grade: gradeFromScore(score) };
1452
1483
  }
@@ -1558,6 +1589,8 @@ var en_default = {
1558
1589
  "markdown.budgetIssues": "Budget Issues",
1559
1590
  "markdown.refactoringOpportunities": "Refactoring Opportunities",
1560
1591
  "markdown.lineRef": "(line {{line}})",
1592
+ "markdown.budgetCategory": "Category",
1593
+ "markdown.budgetTokens": "Tokens",
1561
1594
  "markdown.actionPlan": "## Action Plan",
1562
1595
  "markdown.attribution": "*Generated by [instrlint](https://github.com/jed1978/instrlint)*",
1563
1596
  "ci.passed": "\u2713 instrlint passed score={{score}} grade={{grade}}",
@@ -1565,9 +1598,13 @@ var en_default = {
1565
1598
  "ci.writtenTo": "\u2192 written to {{file}}",
1566
1599
  "initCi.created": "\u2713 Created {{path}}",
1567
1600
  "initCi.alreadyExists": "{{path}} already exists. Use --force to overwrite.",
1568
- "install.installed": "\u2713 Installed to {{path}}",
1601
+ "install.installed": "\u2713 Installed to {{path}}\n \u2192 Restart Claude Code to activate /instrlint",
1569
1602
  "install.alreadyExists": "{{path}} already exists. Use --force to overwrite.",
1570
1603
  "install.unknownTarget": "Specify --claude-code or --codex",
1604
+ "install.outdatedTitle": "instrlint skill is outdated",
1605
+ "install.outdatedVersions": "installed: {{installed}} \u2192 current: {{current}}",
1606
+ "install.updateCmd": "npx instrlint install {{flag}} --force",
1607
+ "install.updatePrompt": "Update skill now?",
1571
1608
  "fix.manualActions": "MANUAL ACTIONS NEEDED",
1572
1609
  "fix.hookCreate": "Add to .claude/settings.json:",
1573
1610
  "fix.hookWarning": "\u26A0 Hook executes shell commands \u2014 review carefully before adding",
@@ -1659,6 +1696,8 @@ var zh_TW_default = {
1659
1696
  "markdown.budgetIssues": "Token \u9810\u7B97\u554F\u984C",
1660
1697
  "markdown.refactoringOpportunities": "\u91CD\u69CB\u5EFA\u8B70",
1661
1698
  "markdown.lineRef": "\uFF08\u7B2C {{line}} \u884C\uFF09",
1699
+ "markdown.budgetCategory": "\u985E\u5225",
1700
+ "markdown.budgetTokens": "Token \u6578",
1662
1701
  "markdown.actionPlan": "## \u884C\u52D5\u8A08\u756B",
1663
1702
  "markdown.attribution": "*\u7531 [instrlint](https://github.com/jed1978/instrlint) \u751F\u6210*",
1664
1703
  "ci.passed": "\u2713 instrlint \u901A\u904E \u5206\u6578={{score}} \u7B49\u7D1A={{grade}}",
@@ -1666,9 +1705,13 @@ var zh_TW_default = {
1666
1705
  "ci.writtenTo": "\u2192 \u5DF2\u5BEB\u5165 {{file}}",
1667
1706
  "initCi.created": "\u2713 \u5DF2\u5EFA\u7ACB {{path}}",
1668
1707
  "initCi.alreadyExists": "{{path}} \u5DF2\u5B58\u5728\u3002\u4F7F\u7528 --force \u8986\u84CB\u3002",
1669
- "install.installed": "\u2713 \u5DF2\u5B89\u88DD\u81F3 {{path}}",
1708
+ "install.installed": "\u2713 \u5DF2\u5B89\u88DD\u81F3 {{path}}\n \u2192 \u91CD\u65B0\u555F\u52D5 Claude Code \u5F8C\u5373\u53EF\u4F7F\u7528 /instrlint",
1670
1709
  "install.alreadyExists": "{{path}} \u5DF2\u5B58\u5728\u3002\u4F7F\u7528 --force \u8986\u84CB\u3002",
1671
1710
  "install.unknownTarget": "\u8ACB\u6307\u5B9A --claude-code \u6216 --codex",
1711
+ "install.outdatedTitle": "instrlint skill \u7248\u672C\u904E\u820A",
1712
+ "install.outdatedVersions": "\u5DF2\u5B89\u88DD\uFF1A{{installed}} \u2192 \u6700\u65B0\uFF1A{{current}}",
1713
+ "install.updateCmd": "npx instrlint install {{flag}} --force",
1714
+ "install.updatePrompt": "\u662F\u5426\u7ACB\u5373\u66F4\u65B0\uFF1F",
1672
1715
  "fix.manualActions": "\u9700\u8981\u624B\u52D5\u64CD\u4F5C",
1673
1716
  "fix.hookCreate": "\u52A0\u5165 .claude/settings.json\uFF1A",
1674
1717
  "fix.hookWarning": "\u26A0 Hook \u6703\u57F7\u884C shell command\uFF0C\u8ACB\u4ED4\u7D30\u78BA\u8A8D\u5F8C\u518D\u52A0\u5165",
@@ -1998,15 +2041,25 @@ function mdSeverityIcon(f) {
1998
2041
  if (f.severity === "warning") return "\u{1F7E1}";
1999
2042
  return "\u2139\uFE0F";
2000
2043
  }
2044
+ function mdBar(fraction, width = 20) {
2045
+ const filled = Math.round(Math.min(1, Math.max(0, fraction)) * width);
2046
+ const empty = width - filled;
2047
+ return "`" + "\u2588".repeat(filled) + "\u2591".repeat(empty) + "`";
2048
+ }
2049
+ function mdFormatTokens(count, method) {
2050
+ const fmt = new Intl.NumberFormat(getLocale());
2051
+ return method === "estimated" ? `~${fmt.format(count)}` : fmt.format(count);
2052
+ }
2001
2053
  function reportMarkdown(report, extraSections = []) {
2002
- const { project, tool, score, grade, findings } = report;
2054
+ const { project, tool, score, grade, findings, budget } = report;
2003
2055
  const criticals = findings.filter((f) => f.severity === "critical").length;
2004
2056
  const warnings = findings.filter((f) => f.severity === "warning").length;
2005
2057
  const infos = findings.filter((f) => f.severity === "info").length;
2058
+ const gradeEmoji = grade === "A" ? "\u{1F7E2}" : grade === "B" ? "\u{1F535}" : grade === "C" ? "\u{1F7E1}" : grade === "D" ? "\u{1F7E0}" : "\u{1F534}";
2006
2059
  const lines = [
2007
2060
  `# ${t("markdown.title", { project })}`,
2008
2061
  "",
2009
- t("markdown.scoreLine", { score: String(score), grade, tool }),
2062
+ `${gradeEmoji} **${score}/100 (${grade})** ${mdBar(score / 100, 25)} \xB7 \`${tool}\``,
2010
2063
  "",
2011
2064
  t("markdown.summary"),
2012
2065
  "",
@@ -2017,6 +2070,58 @@ function reportMarkdown(report, extraSections = []) {
2017
2070
  `| ${t("markdown.info")} | ${infos} |`,
2018
2071
  ""
2019
2072
  ];
2073
+ const window = budget.totalBaseline + budget.availableTokens;
2074
+ const budgetRows = [
2075
+ {
2076
+ labelKey: "label.systemPrompt",
2077
+ tokens: budget.systemPromptTokens,
2078
+ method: "estimated"
2079
+ },
2080
+ {
2081
+ labelKey: "label.rootFile",
2082
+ tokens: budget.rootFileTokens,
2083
+ method: budget.rootFileMethod
2084
+ },
2085
+ {
2086
+ labelKey: "label.ruleFiles",
2087
+ tokens: budget.rulesTokens,
2088
+ method: budget.rulesMethod
2089
+ },
2090
+ {
2091
+ labelKey: "label.skillFiles",
2092
+ tokens: budget.skillsTokens,
2093
+ method: budget.skillsMethod
2094
+ },
2095
+ {
2096
+ labelKey: "label.subDirFiles",
2097
+ tokens: budget.subFilesTokens,
2098
+ method: budget.subFilesMethod
2099
+ },
2100
+ {
2101
+ labelKey: "label.mcpServers",
2102
+ tokens: budget.mcpTokens,
2103
+ method: "estimated"
2104
+ }
2105
+ ].filter((r) => r.tokens > 0);
2106
+ lines.push(`## ${t("label.tokenBudget")}`, "");
2107
+ lines.push(
2108
+ `| ${t("markdown.budgetCategory")} | ${t("markdown.budgetTokens")} | % | |`,
2109
+ "|------|--------|---|---|"
2110
+ );
2111
+ for (const row of budgetRows) {
2112
+ const pctVal = Math.round(row.tokens / window * 100);
2113
+ lines.push(
2114
+ `| ${t(row.labelKey)} | ${mdFormatTokens(row.tokens, row.method)} | ${pctVal}% | ${mdBar(row.tokens / window, 12)} |`
2115
+ );
2116
+ }
2117
+ const baselinePct = Math.round(budget.totalBaseline / window * 100);
2118
+ lines.push(
2119
+ `| **${t("label.baselineTotal")}** | **${mdFormatTokens(budget.totalBaseline, budget.tokenMethod)}** | **${baselinePct}%** | ${mdBar(budget.totalBaseline / window, 12)} |`
2120
+ );
2121
+ lines.push(
2122
+ `| ${t("label.available")} | ${mdFormatTokens(budget.availableTokens, "estimated")} | ${100 - baselinePct}% | |`
2123
+ );
2124
+ lines.push("");
2020
2125
  const categories = [
2021
2126
  {
2022
2127
  labelKey: "markdown.contradictions",
@@ -2263,6 +2368,163 @@ function markdownStructureSuggestions(suggestions, projectRoot) {
2263
2368
  return lines;
2264
2369
  }
2265
2370
 
2371
+ // src/utils/skill-version.ts
2372
+ import { existsSync as existsSync7, readFileSync as readFileSync8 } from "fs";
2373
+ import { join as join7 } from "path";
2374
+ import { homedir } from "os";
2375
+ import { fileURLToPath } from "url";
2376
+ function readPackageVersion() {
2377
+ const thisFile = fileURLToPath(import.meta.url);
2378
+ for (const levels of [2, 3]) {
2379
+ const pkgPath = join7(thisFile, ...Array(levels).fill(".."), "package.json");
2380
+ if (existsSync7(pkgPath)) {
2381
+ const raw = JSON.parse(readFileSync8(pkgPath, "utf8"));
2382
+ if (typeof raw === "object" && raw !== null && "version" in raw && typeof raw.version === "string") {
2383
+ return raw.version;
2384
+ }
2385
+ }
2386
+ }
2387
+ return "0.0.0";
2388
+ }
2389
+ var CURRENT_VERSION = readPackageVersion();
2390
+ var VERSION_RE = /^instrlint-version:\s*(.+)$/m;
2391
+ function extractVersion(content) {
2392
+ const m = VERSION_RE.exec(content);
2393
+ return m ? m[1].trim() : null;
2394
+ }
2395
+ function readInstalledVersion(path) {
2396
+ if (!existsSync7(path)) return null;
2397
+ try {
2398
+ return extractVersion(readFileSync8(path, "utf8"));
2399
+ } catch {
2400
+ return null;
2401
+ }
2402
+ }
2403
+ function checkSkillUpdate(projectRoot) {
2404
+ const candidates = [
2405
+ {
2406
+ path: join7(projectRoot, ".claude", "commands", "instrlint.md"),
2407
+ isProject: true
2408
+ },
2409
+ {
2410
+ path: join7(homedir(), ".claude", "commands", "instrlint.md"),
2411
+ isProject: false
2412
+ }
2413
+ ];
2414
+ for (const { path, isProject } of candidates) {
2415
+ const installed = readInstalledVersion(path);
2416
+ if (installed !== null && installed !== CURRENT_VERSION) {
2417
+ return {
2418
+ installedVersion: installed,
2419
+ currentVersion: CURRENT_VERSION,
2420
+ installPath: path,
2421
+ isProject
2422
+ };
2423
+ }
2424
+ }
2425
+ return null;
2426
+ }
2427
+ function injectVersion(content, version) {
2428
+ if (/^instrlint-version:/m.test(content)) {
2429
+ return content.replace(
2430
+ /^instrlint-version:.*$/m,
2431
+ `instrlint-version: ${version}`
2432
+ );
2433
+ }
2434
+ const updated = content.replace(
2435
+ /^(---\n[\s\S]*?)(---)$/m,
2436
+ `$1instrlint-version: ${version}
2437
+ $2`
2438
+ );
2439
+ if (updated === content) {
2440
+ throw new Error("injectVersion: no YAML frontmatter found in skill file");
2441
+ }
2442
+ return updated;
2443
+ }
2444
+
2445
+ // src/commands/install-command.ts
2446
+ import { existsSync as existsSync8, mkdirSync, readFileSync as readFileSync9, writeFileSync as writeFileSync2 } from "fs";
2447
+ import { join as join8 } from "path";
2448
+ import { homedir as homedir2 } from "os";
2449
+ import { fileURLToPath as fileURLToPath2 } from "url";
2450
+ function resolveSkillFile(target) {
2451
+ const thisFile = fileURLToPath2(import.meta.url);
2452
+ const subDir = target === "claude-code" ? "claude-code" : "codex";
2453
+ for (const levels of [2, 3]) {
2454
+ const parts = Array(levels).fill("..");
2455
+ const candidate = join8(thisFile, ...parts, "skills", subDir, "SKILL.md");
2456
+ if (existsSync8(candidate)) return candidate;
2457
+ }
2458
+ return join8(thisFile, "..", "..", "skills", subDir, "SKILL.md");
2459
+ }
2460
+ function readSkillContent(target) {
2461
+ const skillPath = resolveSkillFile(target);
2462
+ try {
2463
+ const raw = readFileSync9(skillPath, "utf8");
2464
+ return injectVersion(raw, CURRENT_VERSION);
2465
+ } catch {
2466
+ throw new Error(
2467
+ `Could not read skill file at ${skillPath}. Make sure the package is properly installed.`
2468
+ );
2469
+ }
2470
+ }
2471
+ function installClaudeCode(content, projectRoot, isProject, force, output) {
2472
+ const targetDir = isProject ? join8(projectRoot, ".claude", "commands") : join8(homedir2(), ".claude", "commands");
2473
+ const targetPath = join8(targetDir, "instrlint.md");
2474
+ if (existsSync8(targetPath) && !force) {
2475
+ output.error(t("install.alreadyExists", { path: targetPath }));
2476
+ return { exitCode: 1, errorMessage: "file already exists" };
2477
+ }
2478
+ mkdirSync(targetDir, { recursive: true });
2479
+ writeFileSync2(targetPath, content, "utf8");
2480
+ output.log(t("install.installed", { path: targetPath }));
2481
+ return { exitCode: 0 };
2482
+ }
2483
+ function installCodex(content, projectRoot, force, output) {
2484
+ const targetDir = join8(projectRoot, ".agents", "skills", "instrlint");
2485
+ const targetPath = join8(targetDir, "SKILL.md");
2486
+ if (existsSync8(targetPath) && !force) {
2487
+ output.error(t("install.alreadyExists", { path: targetPath }));
2488
+ return { exitCode: 1, errorMessage: "file already exists" };
2489
+ }
2490
+ mkdirSync(targetDir, { recursive: true });
2491
+ writeFileSync2(targetPath, content, "utf8");
2492
+ output.log(t("install.installed", { path: targetPath }));
2493
+ return { exitCode: 0 };
2494
+ }
2495
+ function runInstall(opts, output = console) {
2496
+ const projectRoot = opts.projectRoot ?? process.cwd();
2497
+ const force = opts.force ?? false;
2498
+ if (opts.claudeCode) {
2499
+ let content;
2500
+ try {
2501
+ content = readSkillContent("claude-code");
2502
+ } catch (err) {
2503
+ output.error(String(err));
2504
+ return { exitCode: 1, errorMessage: String(err) };
2505
+ }
2506
+ return installClaudeCode(
2507
+ content,
2508
+ projectRoot,
2509
+ opts.project ?? false,
2510
+ force,
2511
+ output
2512
+ );
2513
+ }
2514
+ if (opts.codex) {
2515
+ let content;
2516
+ try {
2517
+ content = readSkillContent("codex");
2518
+ } catch (err) {
2519
+ output.error(String(err));
2520
+ return { exitCode: 1, errorMessage: String(err) };
2521
+ }
2522
+ return installCodex(content, projectRoot, force, output);
2523
+ }
2524
+ output.error(t("install.unknownTarget"));
2525
+ return { exitCode: 1, errorMessage: "no target specified" };
2526
+ }
2527
+
2266
2528
  // src/commands/run-command.ts
2267
2529
  function isGitClean(cwd) {
2268
2530
  try {
@@ -2352,6 +2614,7 @@ async function runAll(opts, output = console) {
2352
2614
  printStructureSuggestions(suggestions, projectRoot, output);
2353
2615
  return { exitCode: 0 };
2354
2616
  }
2617
+ const skillUpdate = checkSkillUpdate(projectRoot);
2355
2618
  if (opts.format === "json") {
2356
2619
  output.log(reportJson(report));
2357
2620
  return { exitCode: 0 };
@@ -2359,12 +2622,54 @@ async function runAll(opts, output = console) {
2359
2622
  if (opts.format === "markdown") {
2360
2623
  const mdSuggestions = buildStructureSuggestions(allFindings);
2361
2624
  const mdExtra = markdownStructureSuggestions(mdSuggestions, projectRoot);
2362
- output.log(reportMarkdown(report, mdExtra));
2625
+ const updateSection = skillUpdate ? [
2626
+ "",
2627
+ `> \u26A0\uFE0F **${t("install.outdatedTitle")}** (${t("install.outdatedVersions", { installed: skillUpdate.installedVersion, current: skillUpdate.currentVersion })})`,
2628
+ `> \`${t("install.updateCmd", { flag: skillUpdate.isProject ? "--claude-code --project" : "--claude-code" })}\``
2629
+ ] : [];
2630
+ output.log(reportMarkdown(report, [...mdExtra, ...updateSection]));
2363
2631
  return { exitCode: 0 };
2364
2632
  }
2365
2633
  printCombinedTerminal(report, output);
2634
+ if (skillUpdate && process.stdout.isTTY) {
2635
+ output.log("");
2636
+ output.log(
2637
+ ` ${chalk4.yellow("\u26A0")} ${chalk4.bold(t("install.outdatedTitle"))} (${t("install.outdatedVersions", { installed: skillUpdate.installedVersion, current: skillUpdate.currentVersion })})`
2638
+ );
2639
+ const confirmed = await promptYesNo(` ${t("install.updatePrompt")}`);
2640
+ if (confirmed) {
2641
+ const installResult = runInstall(
2642
+ {
2643
+ claudeCode: true,
2644
+ project: skillUpdate.isProject,
2645
+ force: true,
2646
+ projectRoot
2647
+ },
2648
+ output
2649
+ );
2650
+ if (installResult.exitCode !== 0) {
2651
+ output.error?.(
2652
+ `Update failed: ${installResult.errorMessage ?? "unknown error"}`
2653
+ );
2654
+ }
2655
+ }
2656
+ output.log("");
2657
+ }
2366
2658
  return { exitCode: 0 };
2367
2659
  }
2660
+ function promptYesNo(question) {
2661
+ return new Promise((resolve) => {
2662
+ const rl = createInterface({
2663
+ input: process.stdin,
2664
+ output: process.stdout
2665
+ });
2666
+ rl.question(`${question} ${chalk4.gray("[Y/n]")} `, (answer) => {
2667
+ rl.close();
2668
+ const trimmed = answer.trim().toLowerCase();
2669
+ resolve(trimmed === "" || trimmed === "y");
2670
+ });
2671
+ });
2672
+ }
2368
2673
 
2369
2674
  // src/commands/deadrules-command.ts
2370
2675
  import chalk5 from "chalk";
@@ -2525,7 +2830,7 @@ async function runStructure(opts, output = console) {
2525
2830
  }
2526
2831
 
2527
2832
  // src/commands/ci-command.ts
2528
- import { writeFileSync as writeFileSync2 } from "fs";
2833
+ import { writeFileSync as writeFileSync3 } from "fs";
2529
2834
  import { basename as basename3 } from "path";
2530
2835
 
2531
2836
  // src/reporters/sarif.ts
@@ -2653,7 +2958,7 @@ async function runCi(opts, output = console) {
2653
2958
  formatted = reportJson(report);
2654
2959
  }
2655
2960
  if (opts.output != null) {
2656
- writeFileSync2(opts.output, formatted, "utf8");
2961
+ writeFileSync3(opts.output, formatted, "utf8");
2657
2962
  const pass = !shouldFail(allFindings, failOn);
2658
2963
  const statusKey = pass ? "ci.passed" : "ci.failed";
2659
2964
  output.error(
@@ -2667,8 +2972,8 @@ async function runCi(opts, output = console) {
2667
2972
  }
2668
2973
 
2669
2974
  // src/commands/init-ci-command.ts
2670
- import { existsSync as existsSync7, mkdirSync, writeFileSync as writeFileSync3 } from "fs";
2671
- import { join as join7 } from "path";
2975
+ import { existsSync as existsSync9, mkdirSync as mkdirSync2, writeFileSync as writeFileSync4 } from "fs";
2976
+ import { join as join9 } from "path";
2672
2977
  function githubWorkflow() {
2673
2978
  return `name: instrlint
2674
2979
 
@@ -2737,14 +3042,14 @@ instrlint:
2737
3042
  function runInitCi(opts, output = console) {
2738
3043
  const projectRoot = opts.projectRoot ?? process.cwd();
2739
3044
  if (opts.github) {
2740
- const workflowDir = join7(projectRoot, ".github", "workflows");
2741
- const workflowPath = join7(workflowDir, "instrlint.yml");
2742
- if (existsSync7(workflowPath) && !opts.force) {
3045
+ const workflowDir = join9(projectRoot, ".github", "workflows");
3046
+ const workflowPath = join9(workflowDir, "instrlint.yml");
3047
+ if (existsSync9(workflowPath) && !opts.force) {
2743
3048
  output.error(t("initCi.alreadyExists", { path: workflowPath }));
2744
3049
  return { exitCode: 1, errorMessage: "file already exists" };
2745
3050
  }
2746
- mkdirSync(workflowDir, { recursive: true });
2747
- writeFileSync3(workflowPath, githubWorkflow(), "utf8");
3051
+ mkdirSync2(workflowDir, { recursive: true });
3052
+ writeFileSync4(workflowPath, githubWorkflow(), "utf8");
2748
3053
  output.log(t("initCi.created", { path: workflowPath }));
2749
3054
  return { exitCode: 0 };
2750
3055
  }
@@ -2756,93 +3061,11 @@ function runInitCi(opts, output = console) {
2756
3061
  return { exitCode: 1, errorMessage: "no target specified" };
2757
3062
  }
2758
3063
 
2759
- // src/commands/install-command.ts
2760
- import { existsSync as existsSync8, mkdirSync as mkdirSync2, readFileSync as readFileSync8, writeFileSync as writeFileSync4 } from "fs";
2761
- import { join as join8 } from "path";
2762
- import { homedir } from "os";
2763
- import { fileURLToPath } from "url";
2764
- function resolveSkillFile(target) {
2765
- const thisFile = fileURLToPath(import.meta.url);
2766
- const subDir = target === "claude-code" ? "claude-code" : "codex";
2767
- for (const levels of [2, 3]) {
2768
- const parts = Array(levels).fill("..");
2769
- const candidate = join8(thisFile, ...parts, "skills", subDir, "SKILL.md");
2770
- if (existsSync8(candidate)) return candidate;
2771
- }
2772
- return join8(thisFile, "..", "..", "skills", subDir, "SKILL.md");
2773
- }
2774
- function readSkillContent(target) {
2775
- const skillPath = resolveSkillFile(target);
2776
- try {
2777
- return readFileSync8(skillPath, "utf8");
2778
- } catch {
2779
- throw new Error(
2780
- `Could not read skill file at ${skillPath}. Make sure the package is properly installed.`
2781
- );
2782
- }
2783
- }
2784
- function installClaudeCode(content, projectRoot, isProject, force, output) {
2785
- const targetDir = isProject ? join8(projectRoot, ".claude", "commands") : join8(homedir(), ".claude", "commands");
2786
- const targetPath = join8(targetDir, "instrlint.md");
2787
- if (existsSync8(targetPath) && !force) {
2788
- output.error(t("install.alreadyExists", { path: targetPath }));
2789
- return { exitCode: 1, errorMessage: "file already exists" };
2790
- }
2791
- mkdirSync2(targetDir, { recursive: true });
2792
- writeFileSync4(targetPath, content, "utf8");
2793
- output.log(t("install.installed", { path: targetPath }));
2794
- return { exitCode: 0 };
2795
- }
2796
- function installCodex(content, projectRoot, force, output) {
2797
- const targetDir = join8(projectRoot, ".agents", "skills", "instrlint");
2798
- const targetPath = join8(targetDir, "SKILL.md");
2799
- if (existsSync8(targetPath) && !force) {
2800
- output.error(t("install.alreadyExists", { path: targetPath }));
2801
- return { exitCode: 1, errorMessage: "file already exists" };
2802
- }
2803
- mkdirSync2(targetDir, { recursive: true });
2804
- writeFileSync4(targetPath, content, "utf8");
2805
- output.log(t("install.installed", { path: targetPath }));
2806
- return { exitCode: 0 };
2807
- }
2808
- function runInstall(opts, output = console) {
2809
- const projectRoot = opts.projectRoot ?? process.cwd();
2810
- const force = opts.force ?? false;
2811
- if (opts.claudeCode) {
2812
- let content;
2813
- try {
2814
- content = readSkillContent("claude-code");
2815
- } catch (err) {
2816
- output.error(String(err));
2817
- return { exitCode: 1, errorMessage: String(err) };
2818
- }
2819
- return installClaudeCode(
2820
- content,
2821
- projectRoot,
2822
- opts.project ?? false,
2823
- force,
2824
- output
2825
- );
2826
- }
2827
- if (opts.codex) {
2828
- let content;
2829
- try {
2830
- content = readSkillContent("codex");
2831
- } catch (err) {
2832
- output.error(String(err));
2833
- return { exitCode: 1, errorMessage: String(err) };
2834
- }
2835
- return installCodex(content, projectRoot, force, output);
2836
- }
2837
- output.error(t("install.unknownTarget"));
2838
- return { exitCode: 1, errorMessage: "no target specified" };
2839
- }
2840
-
2841
3064
  // src/cli.ts
2842
3065
  var program = new Command();
2843
3066
  program.enablePositionalOptions().name("instrlint").description(
2844
3067
  "Lint and optimize your CLAUDE.md / AGENTS.md \u2014 find dead rules, token waste, and structural issues"
2845
- ).version("0.1.0").option(
3068
+ ).version(CURRENT_VERSION).option(
2846
3069
  "--format <type>",
2847
3070
  "output format (terminal|json|markdown)",
2848
3071
  "terminal"
@@ -2855,27 +3078,27 @@ program.command("budget").description("Token budget analysis only").option(
2855
3078
  "--format <type>",
2856
3079
  "output format (terminal|json|markdown)",
2857
3080
  "terminal"
2858
- ).option("--tool <name>", "force tool detection (claude-code|codex|cursor)").action(async function() {
3081
+ ).option("--lang <locale>", "output language (en|zh-TW)").option("--tool <name>", "force tool detection (claude-code|codex|cursor)").action(async function() {
2859
3082
  const opts = this.opts();
2860
- const lang = this.parent?.opts()?.lang;
3083
+ const lang = opts.lang ?? this.parent?.opts()?.lang;
2861
3084
  const result = await runBudget({
2862
3085
  ...opts,
2863
3086
  ...lang !== void 0 && { lang }
2864
3087
  });
2865
3088
  if (result.exitCode !== 0) process.exit(result.exitCode);
2866
3089
  });
2867
- program.command("deadrules").description("Dead rule detection only").option("--format <type>", "output format (terminal|json)", "terminal").option("--tool <name>", "force tool detection (claude-code|codex|cursor)").action(async function() {
3090
+ program.command("deadrules").description("Dead rule detection only").option("--format <type>", "output format (terminal|json)", "terminal").option("--lang <locale>", "output language (en|zh-TW)").option("--tool <name>", "force tool detection (claude-code|codex|cursor)").action(async function() {
2868
3091
  const opts = this.opts();
2869
- const lang = this.parent?.opts()?.lang;
3092
+ const lang = opts.lang ?? this.parent?.opts()?.lang;
2870
3093
  const result = await runDeadRules({
2871
3094
  ...opts,
2872
3095
  ...lang !== void 0 && { lang }
2873
3096
  });
2874
3097
  if (result.exitCode !== 0) process.exit(result.exitCode);
2875
3098
  });
2876
- program.command("structure").description("Structural analysis only").option("--format <type>", "output format (terminal|json)", "terminal").option("--tool <name>", "force tool detection (claude-code|codex|cursor)").action(async function() {
3099
+ program.command("structure").description("Structural analysis only").option("--format <type>", "output format (terminal|json)", "terminal").option("--lang <locale>", "output language (en|zh-TW)").option("--tool <name>", "force tool detection (claude-code|codex|cursor)").action(async function() {
2877
3100
  const opts = this.opts();
2878
- const lang = this.parent?.opts()?.lang;
3101
+ const lang = opts.lang ?? this.parent?.opts()?.lang;
2879
3102
  const result = await runStructure({
2880
3103
  ...opts,
2881
3104
  ...lang !== void 0 && { lang }