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.cjs CHANGED
@@ -32,7 +32,8 @@ var import_commander = require("commander");
32
32
 
33
33
  // src/commands/run-command.ts
34
34
  var import_child_process = require("child_process");
35
- var import_path8 = require("path");
35
+ var import_readline = require("readline");
36
+ var import_path10 = require("path");
36
37
  var import_chalk4 = __toESM(require("chalk"), 1);
37
38
 
38
39
  // src/core/scanner.ts
@@ -814,8 +815,12 @@ function analyzeBudget(instructions) {
814
815
  autoFixable: false
815
816
  });
816
817
  }
817
- const { tokens: rulesTokens, method: rulesMethod } = sumTokens(instructions.rules);
818
- const { tokens: skillsTokens, method: skillsMethod } = sumTokens(instructions.skills);
818
+ const { tokens: rulesTokens, method: rulesMethod } = sumTokens(
819
+ instructions.rules
820
+ );
821
+ const { tokens: skillsTokens, method: skillsMethod } = sumTokens(
822
+ instructions.skills
823
+ );
819
824
  const { tokens: subFilesTokens, method: subFilesMethod } = sumTokens(
820
825
  instructions.subFiles
821
826
  );
@@ -853,11 +858,18 @@ function analyzeBudget(instructions) {
853
858
  autoFixable: false
854
859
  });
855
860
  }
856
- const tokenMethod = [rootFileMethod, rulesMethod, skillsMethod, subFilesMethod].every(
857
- (m) => m === "measured"
858
- ) ? "measured" : "estimated";
861
+ const tokenMethod = [
862
+ rootFileMethod,
863
+ rulesMethod,
864
+ skillsMethod,
865
+ subFilesMethod
866
+ ].every((m) => m === "measured") ? "measured" : "estimated";
859
867
  const fileBreakdown = [
860
- { path: instructions.rootFile.path, tokenCount: rootFileTokens, tokenMethod: rootFileMethod },
868
+ {
869
+ path: instructions.rootFile.path,
870
+ tokenCount: rootFileTokens,
871
+ tokenMethod: rootFileMethod
872
+ },
861
873
  ...instructions.rules.map((r) => ({
862
874
  path: r.path,
863
875
  tokenCount: r.tokenCount,
@@ -877,6 +889,7 @@ function analyzeBudget(instructions) {
877
889
  const summary = {
878
890
  systemPromptTokens: SYSTEM_PROMPT_TOKENS,
879
891
  rootFileTokens,
892
+ rootFileLines: rootLines,
880
893
  rootFileMethod,
881
894
  rulesTokens,
882
895
  rulesMethod,
@@ -1453,6 +1466,8 @@ var INFO_DEDUCTION = 1;
1453
1466
  var MAX_CRITICAL_DEDUCTION = 40;
1454
1467
  var MAX_WARNING_DEDUCTION = 30;
1455
1468
  var MAX_INFO_DEDUCTION = 10;
1469
+ var MAX_ROOT_FILE_PENALTY = 30;
1470
+ var MAX_BUDGET_DEDUCTION = 30;
1456
1471
  function gradeFromScore(score) {
1457
1472
  if (score >= 90) return "A";
1458
1473
  if (score >= 80) return "B";
@@ -1464,16 +1479,32 @@ function calculateScore(findings, budget) {
1464
1479
  const criticals = findings.filter((f) => f.severity === "critical").length;
1465
1480
  const warnings = findings.filter((f) => f.severity === "warning").length;
1466
1481
  const infos = findings.filter((f) => f.severity === "info").length;
1467
- const criticalDeduction = Math.min(criticals * CRITICAL_DEDUCTION, MAX_CRITICAL_DEDUCTION);
1468
- const warningDeduction = Math.min(warnings * WARNING_DEDUCTION, MAX_WARNING_DEDUCTION);
1482
+ const criticalDeduction = Math.min(
1483
+ criticals * CRITICAL_DEDUCTION,
1484
+ MAX_CRITICAL_DEDUCTION
1485
+ );
1486
+ const warningDeduction = Math.min(
1487
+ warnings * WARNING_DEDUCTION,
1488
+ MAX_WARNING_DEDUCTION
1489
+ );
1469
1490
  const infoDeduction = Math.min(infos * INFO_DEDUCTION, MAX_INFO_DEDUCTION);
1491
+ const rootLines = budget.rootFileLines;
1492
+ let rootFilePenalty = 0;
1493
+ if (rootLines > 400) {
1494
+ rootFilePenalty = 10 + Math.floor((rootLines - 400) / 100) * 5;
1495
+ } else if (rootLines > 200) {
1496
+ rootFilePenalty = 5 + Math.floor((rootLines - 200) / 100) * 3;
1497
+ }
1498
+ rootFilePenalty = Math.min(rootFilePenalty, MAX_ROOT_FILE_PENALTY);
1470
1499
  const baselinePct = budget.totalBaseline / CONTEXT_WINDOW2;
1471
1500
  let budgetDeduction = 0;
1472
- if (baselinePct > 0.5) budgetDeduction = 15;
1473
- else if (baselinePct > 0.25) budgetDeduction = 5;
1501
+ if (baselinePct > 0.25) {
1502
+ budgetDeduction = 5 + Math.floor((baselinePct - 0.25) * 40);
1503
+ }
1504
+ budgetDeduction = Math.min(budgetDeduction, MAX_BUDGET_DEDUCTION);
1474
1505
  const score = Math.max(
1475
1506
  0,
1476
- 100 - criticalDeduction - warningDeduction - infoDeduction - budgetDeduction
1507
+ 100 - criticalDeduction - warningDeduction - infoDeduction - rootFilePenalty - budgetDeduction
1477
1508
  );
1478
1509
  return { score, grade: gradeFromScore(score) };
1479
1510
  }
@@ -1585,6 +1616,8 @@ var en_default = {
1585
1616
  "markdown.budgetIssues": "Budget Issues",
1586
1617
  "markdown.refactoringOpportunities": "Refactoring Opportunities",
1587
1618
  "markdown.lineRef": "(line {{line}})",
1619
+ "markdown.budgetCategory": "Category",
1620
+ "markdown.budgetTokens": "Tokens",
1588
1621
  "markdown.actionPlan": "## Action Plan",
1589
1622
  "markdown.attribution": "*Generated by [instrlint](https://github.com/jed1978/instrlint)*",
1590
1623
  "ci.passed": "\u2713 instrlint passed score={{score}} grade={{grade}}",
@@ -1592,9 +1625,13 @@ var en_default = {
1592
1625
  "ci.writtenTo": "\u2192 written to {{file}}",
1593
1626
  "initCi.created": "\u2713 Created {{path}}",
1594
1627
  "initCi.alreadyExists": "{{path}} already exists. Use --force to overwrite.",
1595
- "install.installed": "\u2713 Installed to {{path}}",
1628
+ "install.installed": "\u2713 Installed to {{path}}\n \u2192 Restart Claude Code to activate /instrlint",
1596
1629
  "install.alreadyExists": "{{path}} already exists. Use --force to overwrite.",
1597
1630
  "install.unknownTarget": "Specify --claude-code or --codex",
1631
+ "install.outdatedTitle": "instrlint skill is outdated",
1632
+ "install.outdatedVersions": "installed: {{installed}} \u2192 current: {{current}}",
1633
+ "install.updateCmd": "npx instrlint install {{flag}} --force",
1634
+ "install.updatePrompt": "Update skill now?",
1598
1635
  "fix.manualActions": "MANUAL ACTIONS NEEDED",
1599
1636
  "fix.hookCreate": "Add to .claude/settings.json:",
1600
1637
  "fix.hookWarning": "\u26A0 Hook executes shell commands \u2014 review carefully before adding",
@@ -1686,6 +1723,8 @@ var zh_TW_default = {
1686
1723
  "markdown.budgetIssues": "Token \u9810\u7B97\u554F\u984C",
1687
1724
  "markdown.refactoringOpportunities": "\u91CD\u69CB\u5EFA\u8B70",
1688
1725
  "markdown.lineRef": "\uFF08\u7B2C {{line}} \u884C\uFF09",
1726
+ "markdown.budgetCategory": "\u985E\u5225",
1727
+ "markdown.budgetTokens": "Token \u6578",
1689
1728
  "markdown.actionPlan": "## \u884C\u52D5\u8A08\u756B",
1690
1729
  "markdown.attribution": "*\u7531 [instrlint](https://github.com/jed1978/instrlint) \u751F\u6210*",
1691
1730
  "ci.passed": "\u2713 instrlint \u901A\u904E \u5206\u6578={{score}} \u7B49\u7D1A={{grade}}",
@@ -1693,9 +1732,13 @@ var zh_TW_default = {
1693
1732
  "ci.writtenTo": "\u2192 \u5DF2\u5BEB\u5165 {{file}}",
1694
1733
  "initCi.created": "\u2713 \u5DF2\u5EFA\u7ACB {{path}}",
1695
1734
  "initCi.alreadyExists": "{{path}} \u5DF2\u5B58\u5728\u3002\u4F7F\u7528 --force \u8986\u84CB\u3002",
1696
- "install.installed": "\u2713 \u5DF2\u5B89\u88DD\u81F3 {{path}}",
1735
+ "install.installed": "\u2713 \u5DF2\u5B89\u88DD\u81F3 {{path}}\n \u2192 \u91CD\u65B0\u555F\u52D5 Claude Code \u5F8C\u5373\u53EF\u4F7F\u7528 /instrlint",
1697
1736
  "install.alreadyExists": "{{path}} \u5DF2\u5B58\u5728\u3002\u4F7F\u7528 --force \u8986\u84CB\u3002",
1698
1737
  "install.unknownTarget": "\u8ACB\u6307\u5B9A --claude-code \u6216 --codex",
1738
+ "install.outdatedTitle": "instrlint skill \u7248\u672C\u904E\u820A",
1739
+ "install.outdatedVersions": "\u5DF2\u5B89\u88DD\uFF1A{{installed}} \u2192 \u6700\u65B0\uFF1A{{current}}",
1740
+ "install.updateCmd": "npx instrlint install {{flag}} --force",
1741
+ "install.updatePrompt": "\u662F\u5426\u7ACB\u5373\u66F4\u65B0\uFF1F",
1699
1742
  "fix.manualActions": "\u9700\u8981\u624B\u52D5\u64CD\u4F5C",
1700
1743
  "fix.hookCreate": "\u52A0\u5165 .claude/settings.json\uFF1A",
1701
1744
  "fix.hookWarning": "\u26A0 Hook \u6703\u57F7\u884C shell command\uFF0C\u8ACB\u4ED4\u7D30\u78BA\u8A8D\u5F8C\u518D\u52A0\u5165",
@@ -2025,15 +2068,25 @@ function mdSeverityIcon(f) {
2025
2068
  if (f.severity === "warning") return "\u{1F7E1}";
2026
2069
  return "\u2139\uFE0F";
2027
2070
  }
2071
+ function mdBar(fraction, width = 20) {
2072
+ const filled = Math.round(Math.min(1, Math.max(0, fraction)) * width);
2073
+ const empty = width - filled;
2074
+ return "`" + "\u2588".repeat(filled) + "\u2591".repeat(empty) + "`";
2075
+ }
2076
+ function mdFormatTokens(count, method) {
2077
+ const fmt = new Intl.NumberFormat(getLocale());
2078
+ return method === "estimated" ? `~${fmt.format(count)}` : fmt.format(count);
2079
+ }
2028
2080
  function reportMarkdown(report, extraSections = []) {
2029
- const { project, tool, score, grade, findings } = report;
2081
+ const { project, tool, score, grade, findings, budget } = report;
2030
2082
  const criticals = findings.filter((f) => f.severity === "critical").length;
2031
2083
  const warnings = findings.filter((f) => f.severity === "warning").length;
2032
2084
  const infos = findings.filter((f) => f.severity === "info").length;
2085
+ const gradeEmoji = grade === "A" ? "\u{1F7E2}" : grade === "B" ? "\u{1F535}" : grade === "C" ? "\u{1F7E1}" : grade === "D" ? "\u{1F7E0}" : "\u{1F534}";
2033
2086
  const lines = [
2034
2087
  `# ${t("markdown.title", { project })}`,
2035
2088
  "",
2036
- t("markdown.scoreLine", { score: String(score), grade, tool }),
2089
+ `${gradeEmoji} **${score}/100 (${grade})** ${mdBar(score / 100, 25)} \xB7 \`${tool}\``,
2037
2090
  "",
2038
2091
  t("markdown.summary"),
2039
2092
  "",
@@ -2044,6 +2097,58 @@ function reportMarkdown(report, extraSections = []) {
2044
2097
  `| ${t("markdown.info")} | ${infos} |`,
2045
2098
  ""
2046
2099
  ];
2100
+ const window = budget.totalBaseline + budget.availableTokens;
2101
+ const budgetRows = [
2102
+ {
2103
+ labelKey: "label.systemPrompt",
2104
+ tokens: budget.systemPromptTokens,
2105
+ method: "estimated"
2106
+ },
2107
+ {
2108
+ labelKey: "label.rootFile",
2109
+ tokens: budget.rootFileTokens,
2110
+ method: budget.rootFileMethod
2111
+ },
2112
+ {
2113
+ labelKey: "label.ruleFiles",
2114
+ tokens: budget.rulesTokens,
2115
+ method: budget.rulesMethod
2116
+ },
2117
+ {
2118
+ labelKey: "label.skillFiles",
2119
+ tokens: budget.skillsTokens,
2120
+ method: budget.skillsMethod
2121
+ },
2122
+ {
2123
+ labelKey: "label.subDirFiles",
2124
+ tokens: budget.subFilesTokens,
2125
+ method: budget.subFilesMethod
2126
+ },
2127
+ {
2128
+ labelKey: "label.mcpServers",
2129
+ tokens: budget.mcpTokens,
2130
+ method: "estimated"
2131
+ }
2132
+ ].filter((r) => r.tokens > 0);
2133
+ lines.push(`## ${t("label.tokenBudget")}`, "");
2134
+ lines.push(
2135
+ `| ${t("markdown.budgetCategory")} | ${t("markdown.budgetTokens")} | % | |`,
2136
+ "|------|--------|---|---|"
2137
+ );
2138
+ for (const row of budgetRows) {
2139
+ const pctVal = Math.round(row.tokens / window * 100);
2140
+ lines.push(
2141
+ `| ${t(row.labelKey)} | ${mdFormatTokens(row.tokens, row.method)} | ${pctVal}% | ${mdBar(row.tokens / window, 12)} |`
2142
+ );
2143
+ }
2144
+ const baselinePct = Math.round(budget.totalBaseline / window * 100);
2145
+ lines.push(
2146
+ `| **${t("label.baselineTotal")}** | **${mdFormatTokens(budget.totalBaseline, budget.tokenMethod)}** | **${baselinePct}%** | ${mdBar(budget.totalBaseline / window, 12)} |`
2147
+ );
2148
+ lines.push(
2149
+ `| ${t("label.available")} | ${mdFormatTokens(budget.availableTokens, "estimated")} | ${100 - baselinePct}% | |`
2150
+ );
2151
+ lines.push("");
2047
2152
  const categories = [
2048
2153
  {
2049
2154
  labelKey: "markdown.contradictions",
@@ -2290,6 +2395,163 @@ function markdownStructureSuggestions(suggestions, projectRoot) {
2290
2395
  return lines;
2291
2396
  }
2292
2397
 
2398
+ // src/utils/skill-version.ts
2399
+ var import_fs10 = require("fs");
2400
+ var import_path8 = require("path");
2401
+ var import_os = require("os");
2402
+ var import_url = require("url");
2403
+ function readPackageVersion() {
2404
+ const thisFile = (0, import_url.fileURLToPath)(importMetaUrl);
2405
+ for (const levels of [2, 3]) {
2406
+ const pkgPath = (0, import_path8.join)(thisFile, ...Array(levels).fill(".."), "package.json");
2407
+ if ((0, import_fs10.existsSync)(pkgPath)) {
2408
+ const raw = JSON.parse((0, import_fs10.readFileSync)(pkgPath, "utf8"));
2409
+ if (typeof raw === "object" && raw !== null && "version" in raw && typeof raw.version === "string") {
2410
+ return raw.version;
2411
+ }
2412
+ }
2413
+ }
2414
+ return "0.0.0";
2415
+ }
2416
+ var CURRENT_VERSION = readPackageVersion();
2417
+ var VERSION_RE = /^instrlint-version:\s*(.+)$/m;
2418
+ function extractVersion(content) {
2419
+ const m = VERSION_RE.exec(content);
2420
+ return m ? m[1].trim() : null;
2421
+ }
2422
+ function readInstalledVersion(path) {
2423
+ if (!(0, import_fs10.existsSync)(path)) return null;
2424
+ try {
2425
+ return extractVersion((0, import_fs10.readFileSync)(path, "utf8"));
2426
+ } catch {
2427
+ return null;
2428
+ }
2429
+ }
2430
+ function checkSkillUpdate(projectRoot) {
2431
+ const candidates = [
2432
+ {
2433
+ path: (0, import_path8.join)(projectRoot, ".claude", "commands", "instrlint.md"),
2434
+ isProject: true
2435
+ },
2436
+ {
2437
+ path: (0, import_path8.join)((0, import_os.homedir)(), ".claude", "commands", "instrlint.md"),
2438
+ isProject: false
2439
+ }
2440
+ ];
2441
+ for (const { path, isProject } of candidates) {
2442
+ const installed = readInstalledVersion(path);
2443
+ if (installed !== null && installed !== CURRENT_VERSION) {
2444
+ return {
2445
+ installedVersion: installed,
2446
+ currentVersion: CURRENT_VERSION,
2447
+ installPath: path,
2448
+ isProject
2449
+ };
2450
+ }
2451
+ }
2452
+ return null;
2453
+ }
2454
+ function injectVersion(content, version) {
2455
+ if (/^instrlint-version:/m.test(content)) {
2456
+ return content.replace(
2457
+ /^instrlint-version:.*$/m,
2458
+ `instrlint-version: ${version}`
2459
+ );
2460
+ }
2461
+ const updated = content.replace(
2462
+ /^(---\n[\s\S]*?)(---)$/m,
2463
+ `$1instrlint-version: ${version}
2464
+ $2`
2465
+ );
2466
+ if (updated === content) {
2467
+ throw new Error("injectVersion: no YAML frontmatter found in skill file");
2468
+ }
2469
+ return updated;
2470
+ }
2471
+
2472
+ // src/commands/install-command.ts
2473
+ var import_fs11 = require("fs");
2474
+ var import_path9 = require("path");
2475
+ var import_os2 = require("os");
2476
+ var import_url2 = require("url");
2477
+ function resolveSkillFile(target) {
2478
+ const thisFile = (0, import_url2.fileURLToPath)(importMetaUrl);
2479
+ const subDir = target === "claude-code" ? "claude-code" : "codex";
2480
+ for (const levels of [2, 3]) {
2481
+ const parts = Array(levels).fill("..");
2482
+ const candidate = (0, import_path9.join)(thisFile, ...parts, "skills", subDir, "SKILL.md");
2483
+ if ((0, import_fs11.existsSync)(candidate)) return candidate;
2484
+ }
2485
+ return (0, import_path9.join)(thisFile, "..", "..", "skills", subDir, "SKILL.md");
2486
+ }
2487
+ function readSkillContent(target) {
2488
+ const skillPath = resolveSkillFile(target);
2489
+ try {
2490
+ const raw = (0, import_fs11.readFileSync)(skillPath, "utf8");
2491
+ return injectVersion(raw, CURRENT_VERSION);
2492
+ } catch {
2493
+ throw new Error(
2494
+ `Could not read skill file at ${skillPath}. Make sure the package is properly installed.`
2495
+ );
2496
+ }
2497
+ }
2498
+ function installClaudeCode(content, projectRoot, isProject, force, output) {
2499
+ const targetDir = isProject ? (0, import_path9.join)(projectRoot, ".claude", "commands") : (0, import_path9.join)((0, import_os2.homedir)(), ".claude", "commands");
2500
+ const targetPath = (0, import_path9.join)(targetDir, "instrlint.md");
2501
+ if ((0, import_fs11.existsSync)(targetPath) && !force) {
2502
+ output.error(t("install.alreadyExists", { path: targetPath }));
2503
+ return { exitCode: 1, errorMessage: "file already exists" };
2504
+ }
2505
+ (0, import_fs11.mkdirSync)(targetDir, { recursive: true });
2506
+ (0, import_fs11.writeFileSync)(targetPath, content, "utf8");
2507
+ output.log(t("install.installed", { path: targetPath }));
2508
+ return { exitCode: 0 };
2509
+ }
2510
+ function installCodex(content, projectRoot, force, output) {
2511
+ const targetDir = (0, import_path9.join)(projectRoot, ".agents", "skills", "instrlint");
2512
+ const targetPath = (0, import_path9.join)(targetDir, "SKILL.md");
2513
+ if ((0, import_fs11.existsSync)(targetPath) && !force) {
2514
+ output.error(t("install.alreadyExists", { path: targetPath }));
2515
+ return { exitCode: 1, errorMessage: "file already exists" };
2516
+ }
2517
+ (0, import_fs11.mkdirSync)(targetDir, { recursive: true });
2518
+ (0, import_fs11.writeFileSync)(targetPath, content, "utf8");
2519
+ output.log(t("install.installed", { path: targetPath }));
2520
+ return { exitCode: 0 };
2521
+ }
2522
+ function runInstall(opts, output = console) {
2523
+ const projectRoot = opts.projectRoot ?? process.cwd();
2524
+ const force = opts.force ?? false;
2525
+ if (opts.claudeCode) {
2526
+ let content;
2527
+ try {
2528
+ content = readSkillContent("claude-code");
2529
+ } catch (err) {
2530
+ output.error(String(err));
2531
+ return { exitCode: 1, errorMessage: String(err) };
2532
+ }
2533
+ return installClaudeCode(
2534
+ content,
2535
+ projectRoot,
2536
+ opts.project ?? false,
2537
+ force,
2538
+ output
2539
+ );
2540
+ }
2541
+ if (opts.codex) {
2542
+ let content;
2543
+ try {
2544
+ content = readSkillContent("codex");
2545
+ } catch (err) {
2546
+ output.error(String(err));
2547
+ return { exitCode: 1, errorMessage: String(err) };
2548
+ }
2549
+ return installCodex(content, projectRoot, force, output);
2550
+ }
2551
+ output.error(t("install.unknownTarget"));
2552
+ return { exitCode: 1, errorMessage: "no target specified" };
2553
+ }
2554
+
2293
2555
  // src/commands/run-command.ts
2294
2556
  function isGitClean(cwd) {
2295
2557
  try {
@@ -2334,7 +2596,7 @@ async function runAll(opts, output = console) {
2334
2596
  const { score, grade } = calculateScore(allFindings, summary);
2335
2597
  const actionPlan = buildActionPlan(allFindings);
2336
2598
  const report = {
2337
- project: (0, import_path8.basename)(projectRoot),
2599
+ project: (0, import_path10.basename)(projectRoot),
2338
2600
  tool: instructions.tool,
2339
2601
  score,
2340
2602
  grade,
@@ -2379,6 +2641,7 @@ async function runAll(opts, output = console) {
2379
2641
  printStructureSuggestions(suggestions, projectRoot, output);
2380
2642
  return { exitCode: 0 };
2381
2643
  }
2644
+ const skillUpdate = checkSkillUpdate(projectRoot);
2382
2645
  if (opts.format === "json") {
2383
2646
  output.log(reportJson(report));
2384
2647
  return { exitCode: 0 };
@@ -2386,12 +2649,54 @@ async function runAll(opts, output = console) {
2386
2649
  if (opts.format === "markdown") {
2387
2650
  const mdSuggestions = buildStructureSuggestions(allFindings);
2388
2651
  const mdExtra = markdownStructureSuggestions(mdSuggestions, projectRoot);
2389
- output.log(reportMarkdown(report, mdExtra));
2652
+ const updateSection = skillUpdate ? [
2653
+ "",
2654
+ `> \u26A0\uFE0F **${t("install.outdatedTitle")}** (${t("install.outdatedVersions", { installed: skillUpdate.installedVersion, current: skillUpdate.currentVersion })})`,
2655
+ `> \`${t("install.updateCmd", { flag: skillUpdate.isProject ? "--claude-code --project" : "--claude-code" })}\``
2656
+ ] : [];
2657
+ output.log(reportMarkdown(report, [...mdExtra, ...updateSection]));
2390
2658
  return { exitCode: 0 };
2391
2659
  }
2392
2660
  printCombinedTerminal(report, output);
2661
+ if (skillUpdate && process.stdout.isTTY) {
2662
+ output.log("");
2663
+ output.log(
2664
+ ` ${import_chalk4.default.yellow("\u26A0")} ${import_chalk4.default.bold(t("install.outdatedTitle"))} (${t("install.outdatedVersions", { installed: skillUpdate.installedVersion, current: skillUpdate.currentVersion })})`
2665
+ );
2666
+ const confirmed = await promptYesNo(` ${t("install.updatePrompt")}`);
2667
+ if (confirmed) {
2668
+ const installResult = runInstall(
2669
+ {
2670
+ claudeCode: true,
2671
+ project: skillUpdate.isProject,
2672
+ force: true,
2673
+ projectRoot
2674
+ },
2675
+ output
2676
+ );
2677
+ if (installResult.exitCode !== 0) {
2678
+ output.error?.(
2679
+ `Update failed: ${installResult.errorMessage ?? "unknown error"}`
2680
+ );
2681
+ }
2682
+ }
2683
+ output.log("");
2684
+ }
2393
2685
  return { exitCode: 0 };
2394
2686
  }
2687
+ function promptYesNo(question) {
2688
+ return new Promise((resolve) => {
2689
+ const rl = (0, import_readline.createInterface)({
2690
+ input: process.stdin,
2691
+ output: process.stdout
2692
+ });
2693
+ rl.question(`${question} ${import_chalk4.default.gray("[Y/n]")} `, (answer) => {
2694
+ rl.close();
2695
+ const trimmed = answer.trim().toLowerCase();
2696
+ resolve(trimmed === "" || trimmed === "y");
2697
+ });
2698
+ });
2699
+ }
2395
2700
 
2396
2701
  // src/commands/deadrules-command.ts
2397
2702
  var import_chalk5 = __toESM(require("chalk"), 1);
@@ -2552,8 +2857,8 @@ async function runStructure(opts, output = console) {
2552
2857
  }
2553
2858
 
2554
2859
  // src/commands/ci-command.ts
2555
- var import_fs10 = require("fs");
2556
- var import_path9 = require("path");
2860
+ var import_fs12 = require("fs");
2861
+ var import_path11 = require("path");
2557
2862
 
2558
2863
  // src/reporters/sarif.ts
2559
2864
  function severityToLevel(severity) {
@@ -2659,7 +2964,7 @@ async function runCi(opts, output = console) {
2659
2964
  const { score, grade } = calculateScore(allFindings, summary);
2660
2965
  const actionPlan = buildActionPlan(allFindings);
2661
2966
  const report = {
2662
- project: (0, import_path9.basename)(projectRoot),
2967
+ project: (0, import_path11.basename)(projectRoot),
2663
2968
  tool: instructions.tool,
2664
2969
  score,
2665
2970
  grade,
@@ -2680,7 +2985,7 @@ async function runCi(opts, output = console) {
2680
2985
  formatted = reportJson(report);
2681
2986
  }
2682
2987
  if (opts.output != null) {
2683
- (0, import_fs10.writeFileSync)(opts.output, formatted, "utf8");
2988
+ (0, import_fs12.writeFileSync)(opts.output, formatted, "utf8");
2684
2989
  const pass = !shouldFail(allFindings, failOn);
2685
2990
  const statusKey = pass ? "ci.passed" : "ci.failed";
2686
2991
  output.error(
@@ -2694,8 +2999,8 @@ async function runCi(opts, output = console) {
2694
2999
  }
2695
3000
 
2696
3001
  // src/commands/init-ci-command.ts
2697
- var import_fs11 = require("fs");
2698
- var import_path10 = require("path");
3002
+ var import_fs13 = require("fs");
3003
+ var import_path12 = require("path");
2699
3004
  function githubWorkflow() {
2700
3005
  return `name: instrlint
2701
3006
 
@@ -2764,14 +3069,14 @@ instrlint:
2764
3069
  function runInitCi(opts, output = console) {
2765
3070
  const projectRoot = opts.projectRoot ?? process.cwd();
2766
3071
  if (opts.github) {
2767
- const workflowDir = (0, import_path10.join)(projectRoot, ".github", "workflows");
2768
- const workflowPath = (0, import_path10.join)(workflowDir, "instrlint.yml");
2769
- if ((0, import_fs11.existsSync)(workflowPath) && !opts.force) {
3072
+ const workflowDir = (0, import_path12.join)(projectRoot, ".github", "workflows");
3073
+ const workflowPath = (0, import_path12.join)(workflowDir, "instrlint.yml");
3074
+ if ((0, import_fs13.existsSync)(workflowPath) && !opts.force) {
2770
3075
  output.error(t("initCi.alreadyExists", { path: workflowPath }));
2771
3076
  return { exitCode: 1, errorMessage: "file already exists" };
2772
3077
  }
2773
- (0, import_fs11.mkdirSync)(workflowDir, { recursive: true });
2774
- (0, import_fs11.writeFileSync)(workflowPath, githubWorkflow(), "utf8");
3078
+ (0, import_fs13.mkdirSync)(workflowDir, { recursive: true });
3079
+ (0, import_fs13.writeFileSync)(workflowPath, githubWorkflow(), "utf8");
2775
3080
  output.log(t("initCi.created", { path: workflowPath }));
2776
3081
  return { exitCode: 0 };
2777
3082
  }
@@ -2783,93 +3088,11 @@ function runInitCi(opts, output = console) {
2783
3088
  return { exitCode: 1, errorMessage: "no target specified" };
2784
3089
  }
2785
3090
 
2786
- // src/commands/install-command.ts
2787
- var import_fs12 = require("fs");
2788
- var import_path11 = require("path");
2789
- var import_os = require("os");
2790
- var import_url = require("url");
2791
- function resolveSkillFile(target) {
2792
- const thisFile = (0, import_url.fileURLToPath)(importMetaUrl);
2793
- const subDir = target === "claude-code" ? "claude-code" : "codex";
2794
- for (const levels of [2, 3]) {
2795
- const parts = Array(levels).fill("..");
2796
- const candidate = (0, import_path11.join)(thisFile, ...parts, "skills", subDir, "SKILL.md");
2797
- if ((0, import_fs12.existsSync)(candidate)) return candidate;
2798
- }
2799
- return (0, import_path11.join)(thisFile, "..", "..", "skills", subDir, "SKILL.md");
2800
- }
2801
- function readSkillContent(target) {
2802
- const skillPath = resolveSkillFile(target);
2803
- try {
2804
- return (0, import_fs12.readFileSync)(skillPath, "utf8");
2805
- } catch {
2806
- throw new Error(
2807
- `Could not read skill file at ${skillPath}. Make sure the package is properly installed.`
2808
- );
2809
- }
2810
- }
2811
- function installClaudeCode(content, projectRoot, isProject, force, output) {
2812
- const targetDir = isProject ? (0, import_path11.join)(projectRoot, ".claude", "commands") : (0, import_path11.join)((0, import_os.homedir)(), ".claude", "commands");
2813
- const targetPath = (0, import_path11.join)(targetDir, "instrlint.md");
2814
- if ((0, import_fs12.existsSync)(targetPath) && !force) {
2815
- output.error(t("install.alreadyExists", { path: targetPath }));
2816
- return { exitCode: 1, errorMessage: "file already exists" };
2817
- }
2818
- (0, import_fs12.mkdirSync)(targetDir, { recursive: true });
2819
- (0, import_fs12.writeFileSync)(targetPath, content, "utf8");
2820
- output.log(t("install.installed", { path: targetPath }));
2821
- return { exitCode: 0 };
2822
- }
2823
- function installCodex(content, projectRoot, force, output) {
2824
- const targetDir = (0, import_path11.join)(projectRoot, ".agents", "skills", "instrlint");
2825
- const targetPath = (0, import_path11.join)(targetDir, "SKILL.md");
2826
- if ((0, import_fs12.existsSync)(targetPath) && !force) {
2827
- output.error(t("install.alreadyExists", { path: targetPath }));
2828
- return { exitCode: 1, errorMessage: "file already exists" };
2829
- }
2830
- (0, import_fs12.mkdirSync)(targetDir, { recursive: true });
2831
- (0, import_fs12.writeFileSync)(targetPath, content, "utf8");
2832
- output.log(t("install.installed", { path: targetPath }));
2833
- return { exitCode: 0 };
2834
- }
2835
- function runInstall(opts, output = console) {
2836
- const projectRoot = opts.projectRoot ?? process.cwd();
2837
- const force = opts.force ?? false;
2838
- if (opts.claudeCode) {
2839
- let content;
2840
- try {
2841
- content = readSkillContent("claude-code");
2842
- } catch (err) {
2843
- output.error(String(err));
2844
- return { exitCode: 1, errorMessage: String(err) };
2845
- }
2846
- return installClaudeCode(
2847
- content,
2848
- projectRoot,
2849
- opts.project ?? false,
2850
- force,
2851
- output
2852
- );
2853
- }
2854
- if (opts.codex) {
2855
- let content;
2856
- try {
2857
- content = readSkillContent("codex");
2858
- } catch (err) {
2859
- output.error(String(err));
2860
- return { exitCode: 1, errorMessage: String(err) };
2861
- }
2862
- return installCodex(content, projectRoot, force, output);
2863
- }
2864
- output.error(t("install.unknownTarget"));
2865
- return { exitCode: 1, errorMessage: "no target specified" };
2866
- }
2867
-
2868
3091
  // src/cli.ts
2869
3092
  var program = new import_commander.Command();
2870
3093
  program.enablePositionalOptions().name("instrlint").description(
2871
3094
  "Lint and optimize your CLAUDE.md / AGENTS.md \u2014 find dead rules, token waste, and structural issues"
2872
- ).version("0.1.0").option(
3095
+ ).version(CURRENT_VERSION).option(
2873
3096
  "--format <type>",
2874
3097
  "output format (terminal|json|markdown)",
2875
3098
  "terminal"
@@ -2882,27 +3105,27 @@ program.command("budget").description("Token budget analysis only").option(
2882
3105
  "--format <type>",
2883
3106
  "output format (terminal|json|markdown)",
2884
3107
  "terminal"
2885
- ).option("--tool <name>", "force tool detection (claude-code|codex|cursor)").action(async function() {
3108
+ ).option("--lang <locale>", "output language (en|zh-TW)").option("--tool <name>", "force tool detection (claude-code|codex|cursor)").action(async function() {
2886
3109
  const opts = this.opts();
2887
- const lang = this.parent?.opts()?.lang;
3110
+ const lang = opts.lang ?? this.parent?.opts()?.lang;
2888
3111
  const result = await runBudget({
2889
3112
  ...opts,
2890
3113
  ...lang !== void 0 && { lang }
2891
3114
  });
2892
3115
  if (result.exitCode !== 0) process.exit(result.exitCode);
2893
3116
  });
2894
- 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() {
3117
+ 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() {
2895
3118
  const opts = this.opts();
2896
- const lang = this.parent?.opts()?.lang;
3119
+ const lang = opts.lang ?? this.parent?.opts()?.lang;
2897
3120
  const result = await runDeadRules({
2898
3121
  ...opts,
2899
3122
  ...lang !== void 0 && { lang }
2900
3123
  });
2901
3124
  if (result.exitCode !== 0) process.exit(result.exitCode);
2902
3125
  });
2903
- 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() {
3126
+ 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() {
2904
3127
  const opts = this.opts();
2905
- const lang = this.parent?.opts()?.lang;
3128
+ const lang = opts.lang ?? this.parent?.opts()?.lang;
2906
3129
  const result = await runStructure({
2907
3130
  ...opts,
2908
3131
  ...lang !== void 0 && { lang }