instrlint 0.1.2 → 0.1.3

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
 
@@ -1565,9 +1566,13 @@ var en_default = {
1565
1566
  "ci.writtenTo": "\u2192 written to {{file}}",
1566
1567
  "initCi.created": "\u2713 Created {{path}}",
1567
1568
  "initCi.alreadyExists": "{{path}} already exists. Use --force to overwrite.",
1568
- "install.installed": "\u2713 Installed to {{path}}",
1569
+ "install.installed": "\u2713 Installed to {{path}}\n \u2192 Restart Claude Code to activate /instrlint",
1569
1570
  "install.alreadyExists": "{{path}} already exists. Use --force to overwrite.",
1570
1571
  "install.unknownTarget": "Specify --claude-code or --codex",
1572
+ "install.outdatedTitle": "instrlint skill is outdated",
1573
+ "install.outdatedVersions": "installed: {{installed}} \u2192 current: {{current}}",
1574
+ "install.updateCmd": "npx instrlint install {{flag}} --force",
1575
+ "install.updatePrompt": "Update skill now?",
1571
1576
  "fix.manualActions": "MANUAL ACTIONS NEEDED",
1572
1577
  "fix.hookCreate": "Add to .claude/settings.json:",
1573
1578
  "fix.hookWarning": "\u26A0 Hook executes shell commands \u2014 review carefully before adding",
@@ -1666,9 +1671,13 @@ var zh_TW_default = {
1666
1671
  "ci.writtenTo": "\u2192 \u5DF2\u5BEB\u5165 {{file}}",
1667
1672
  "initCi.created": "\u2713 \u5DF2\u5EFA\u7ACB {{path}}",
1668
1673
  "initCi.alreadyExists": "{{path}} \u5DF2\u5B58\u5728\u3002\u4F7F\u7528 --force \u8986\u84CB\u3002",
1669
- "install.installed": "\u2713 \u5DF2\u5B89\u88DD\u81F3 {{path}}",
1674
+ "install.installed": "\u2713 \u5DF2\u5B89\u88DD\u81F3 {{path}}\n \u2192 \u91CD\u65B0\u555F\u52D5 Claude Code \u5F8C\u5373\u53EF\u4F7F\u7528 /instrlint",
1670
1675
  "install.alreadyExists": "{{path}} \u5DF2\u5B58\u5728\u3002\u4F7F\u7528 --force \u8986\u84CB\u3002",
1671
1676
  "install.unknownTarget": "\u8ACB\u6307\u5B9A --claude-code \u6216 --codex",
1677
+ "install.outdatedTitle": "instrlint skill \u7248\u672C\u904E\u820A",
1678
+ "install.outdatedVersions": "\u5DF2\u5B89\u88DD\uFF1A{{installed}} \u2192 \u6700\u65B0\uFF1A{{current}}",
1679
+ "install.updateCmd": "npx instrlint install {{flag}} --force",
1680
+ "install.updatePrompt": "\u662F\u5426\u7ACB\u5373\u66F4\u65B0\uFF1F",
1672
1681
  "fix.manualActions": "\u9700\u8981\u624B\u52D5\u64CD\u4F5C",
1673
1682
  "fix.hookCreate": "\u52A0\u5165 .claude/settings.json\uFF1A",
1674
1683
  "fix.hookWarning": "\u26A0 Hook \u6703\u57F7\u884C shell command\uFF0C\u8ACB\u4ED4\u7D30\u78BA\u8A8D\u5F8C\u518D\u52A0\u5165",
@@ -2263,6 +2272,136 @@ function markdownStructureSuggestions(suggestions, projectRoot) {
2263
2272
  return lines;
2264
2273
  }
2265
2274
 
2275
+ // src/utils/skill-version.ts
2276
+ import { existsSync as existsSync7, readFileSync as readFileSync8 } from "fs";
2277
+ import { join as join7 } from "path";
2278
+ import { homedir } from "os";
2279
+ var CURRENT_VERSION = "0.1.3";
2280
+ var VERSION_RE = /^instrlint-version:\s*(.+)$/m;
2281
+ function extractVersion(content) {
2282
+ const m = VERSION_RE.exec(content);
2283
+ return m ? m[1].trim() : null;
2284
+ }
2285
+ function readInstalledVersion(path) {
2286
+ if (!existsSync7(path)) return null;
2287
+ try {
2288
+ return extractVersion(readFileSync8(path, "utf8"));
2289
+ } catch {
2290
+ return null;
2291
+ }
2292
+ }
2293
+ function checkSkillUpdate(projectRoot) {
2294
+ const candidates = [
2295
+ {
2296
+ path: join7(projectRoot, ".claude", "commands", "instrlint.md"),
2297
+ isProject: true
2298
+ },
2299
+ {
2300
+ path: join7(homedir(), ".claude", "commands", "instrlint.md"),
2301
+ isProject: false
2302
+ }
2303
+ ];
2304
+ for (const { path, isProject } of candidates) {
2305
+ const installed = readInstalledVersion(path);
2306
+ if (installed !== null && installed !== CURRENT_VERSION) {
2307
+ return {
2308
+ installedVersion: installed,
2309
+ currentVersion: CURRENT_VERSION,
2310
+ installPath: path,
2311
+ isProject
2312
+ };
2313
+ }
2314
+ }
2315
+ return null;
2316
+ }
2317
+ function injectVersion(content, version) {
2318
+ return content.replace(/^(---\n[\s\S]*?)(---)$/m, `$1instrlint-version: ${version}
2319
+ $2`);
2320
+ }
2321
+
2322
+ // src/commands/install-command.ts
2323
+ import { existsSync as existsSync8, mkdirSync, readFileSync as readFileSync9, writeFileSync as writeFileSync2 } from "fs";
2324
+ import { join as join8 } from "path";
2325
+ import { homedir as homedir2 } from "os";
2326
+ import { fileURLToPath } from "url";
2327
+ function resolveSkillFile(target) {
2328
+ const thisFile = fileURLToPath(import.meta.url);
2329
+ const subDir = target === "claude-code" ? "claude-code" : "codex";
2330
+ for (const levels of [2, 3]) {
2331
+ const parts = Array(levels).fill("..");
2332
+ const candidate = join8(thisFile, ...parts, "skills", subDir, "SKILL.md");
2333
+ if (existsSync8(candidate)) return candidate;
2334
+ }
2335
+ return join8(thisFile, "..", "..", "skills", subDir, "SKILL.md");
2336
+ }
2337
+ function readSkillContent(target) {
2338
+ const skillPath = resolveSkillFile(target);
2339
+ try {
2340
+ const raw = readFileSync9(skillPath, "utf8");
2341
+ return injectVersion(raw, CURRENT_VERSION);
2342
+ } catch {
2343
+ throw new Error(
2344
+ `Could not read skill file at ${skillPath}. Make sure the package is properly installed.`
2345
+ );
2346
+ }
2347
+ }
2348
+ function installClaudeCode(content, projectRoot, isProject, force, output) {
2349
+ const targetDir = isProject ? join8(projectRoot, ".claude", "commands") : join8(homedir2(), ".claude", "commands");
2350
+ const targetPath = join8(targetDir, "instrlint.md");
2351
+ if (existsSync8(targetPath) && !force) {
2352
+ output.error(t("install.alreadyExists", { path: targetPath }));
2353
+ return { exitCode: 1, errorMessage: "file already exists" };
2354
+ }
2355
+ mkdirSync(targetDir, { recursive: true });
2356
+ writeFileSync2(targetPath, content, "utf8");
2357
+ output.log(t("install.installed", { path: targetPath }));
2358
+ return { exitCode: 0 };
2359
+ }
2360
+ function installCodex(content, projectRoot, force, output) {
2361
+ const targetDir = join8(projectRoot, ".agents", "skills", "instrlint");
2362
+ const targetPath = join8(targetDir, "SKILL.md");
2363
+ if (existsSync8(targetPath) && !force) {
2364
+ output.error(t("install.alreadyExists", { path: targetPath }));
2365
+ return { exitCode: 1, errorMessage: "file already exists" };
2366
+ }
2367
+ mkdirSync(targetDir, { recursive: true });
2368
+ writeFileSync2(targetPath, content, "utf8");
2369
+ output.log(t("install.installed", { path: targetPath }));
2370
+ return { exitCode: 0 };
2371
+ }
2372
+ function runInstall(opts, output = console) {
2373
+ const projectRoot = opts.projectRoot ?? process.cwd();
2374
+ const force = opts.force ?? false;
2375
+ if (opts.claudeCode) {
2376
+ let content;
2377
+ try {
2378
+ content = readSkillContent("claude-code");
2379
+ } catch (err) {
2380
+ output.error(String(err));
2381
+ return { exitCode: 1, errorMessage: String(err) };
2382
+ }
2383
+ return installClaudeCode(
2384
+ content,
2385
+ projectRoot,
2386
+ opts.project ?? false,
2387
+ force,
2388
+ output
2389
+ );
2390
+ }
2391
+ if (opts.codex) {
2392
+ let content;
2393
+ try {
2394
+ content = readSkillContent("codex");
2395
+ } catch (err) {
2396
+ output.error(String(err));
2397
+ return { exitCode: 1, errorMessage: String(err) };
2398
+ }
2399
+ return installCodex(content, projectRoot, force, output);
2400
+ }
2401
+ output.error(t("install.unknownTarget"));
2402
+ return { exitCode: 1, errorMessage: "no target specified" };
2403
+ }
2404
+
2266
2405
  // src/commands/run-command.ts
2267
2406
  function isGitClean(cwd) {
2268
2407
  try {
@@ -2352,6 +2491,7 @@ async function runAll(opts, output = console) {
2352
2491
  printStructureSuggestions(suggestions, projectRoot, output);
2353
2492
  return { exitCode: 0 };
2354
2493
  }
2494
+ const skillUpdate = checkSkillUpdate(projectRoot);
2355
2495
  if (opts.format === "json") {
2356
2496
  output.log(reportJson(report));
2357
2497
  return { exitCode: 0 };
@@ -2359,12 +2499,52 @@ async function runAll(opts, output = console) {
2359
2499
  if (opts.format === "markdown") {
2360
2500
  const mdSuggestions = buildStructureSuggestions(allFindings);
2361
2501
  const mdExtra = markdownStructureSuggestions(mdSuggestions, projectRoot);
2362
- output.log(reportMarkdown(report, mdExtra));
2502
+ const updateSection = skillUpdate ? [
2503
+ "",
2504
+ `> \u26A0\uFE0F **${t("install.outdatedTitle")}** (${t("install.outdatedVersions", { installed: skillUpdate.installedVersion, current: skillUpdate.currentVersion })})`,
2505
+ `> \`${t("install.updateCmd", { flag: skillUpdate.isProject ? "--claude-code --project" : "--claude-code" })}\``
2506
+ ] : [];
2507
+ output.log(reportMarkdown(report, [...mdExtra, ...updateSection]));
2363
2508
  return { exitCode: 0 };
2364
2509
  }
2365
2510
  printCombinedTerminal(report, output);
2511
+ if (skillUpdate && process.stdout.isTTY) {
2512
+ output.log("");
2513
+ output.log(
2514
+ ` ${chalk4.yellow("\u26A0")} ${chalk4.bold(t("install.outdatedTitle"))} (${t("install.outdatedVersions", { installed: skillUpdate.installedVersion, current: skillUpdate.currentVersion })})`
2515
+ );
2516
+ const confirmed = await promptYesNo(
2517
+ ` ${t("install.updatePrompt")}`,
2518
+ output
2519
+ );
2520
+ if (confirmed) {
2521
+ runInstall(
2522
+ {
2523
+ claudeCode: true,
2524
+ project: skillUpdate.isProject,
2525
+ force: true,
2526
+ projectRoot
2527
+ },
2528
+ output
2529
+ );
2530
+ }
2531
+ output.log("");
2532
+ }
2366
2533
  return { exitCode: 0 };
2367
2534
  }
2535
+ function promptYesNo(question, output) {
2536
+ return new Promise((resolve) => {
2537
+ const rl = createInterface({
2538
+ input: process.stdin,
2539
+ output: process.stdout
2540
+ });
2541
+ rl.question(`${question} ${chalk4.gray("[Y/n]")} `, (answer) => {
2542
+ rl.close();
2543
+ const trimmed = answer.trim().toLowerCase();
2544
+ resolve(trimmed === "" || trimmed === "y");
2545
+ });
2546
+ });
2547
+ }
2368
2548
 
2369
2549
  // src/commands/deadrules-command.ts
2370
2550
  import chalk5 from "chalk";
@@ -2525,7 +2705,7 @@ async function runStructure(opts, output = console) {
2525
2705
  }
2526
2706
 
2527
2707
  // src/commands/ci-command.ts
2528
- import { writeFileSync as writeFileSync2 } from "fs";
2708
+ import { writeFileSync as writeFileSync3 } from "fs";
2529
2709
  import { basename as basename3 } from "path";
2530
2710
 
2531
2711
  // src/reporters/sarif.ts
@@ -2653,7 +2833,7 @@ async function runCi(opts, output = console) {
2653
2833
  formatted = reportJson(report);
2654
2834
  }
2655
2835
  if (opts.output != null) {
2656
- writeFileSync2(opts.output, formatted, "utf8");
2836
+ writeFileSync3(opts.output, formatted, "utf8");
2657
2837
  const pass = !shouldFail(allFindings, failOn);
2658
2838
  const statusKey = pass ? "ci.passed" : "ci.failed";
2659
2839
  output.error(
@@ -2667,8 +2847,8 @@ async function runCi(opts, output = console) {
2667
2847
  }
2668
2848
 
2669
2849
  // src/commands/init-ci-command.ts
2670
- import { existsSync as existsSync7, mkdirSync, writeFileSync as writeFileSync3 } from "fs";
2671
- import { join as join7 } from "path";
2850
+ import { existsSync as existsSync9, mkdirSync as mkdirSync2, writeFileSync as writeFileSync4 } from "fs";
2851
+ import { join as join9 } from "path";
2672
2852
  function githubWorkflow() {
2673
2853
  return `name: instrlint
2674
2854
 
@@ -2737,14 +2917,14 @@ instrlint:
2737
2917
  function runInitCi(opts, output = console) {
2738
2918
  const projectRoot = opts.projectRoot ?? process.cwd();
2739
2919
  if (opts.github) {
2740
- const workflowDir = join7(projectRoot, ".github", "workflows");
2741
- const workflowPath = join7(workflowDir, "instrlint.yml");
2742
- if (existsSync7(workflowPath) && !opts.force) {
2920
+ const workflowDir = join9(projectRoot, ".github", "workflows");
2921
+ const workflowPath = join9(workflowDir, "instrlint.yml");
2922
+ if (existsSync9(workflowPath) && !opts.force) {
2743
2923
  output.error(t("initCi.alreadyExists", { path: workflowPath }));
2744
2924
  return { exitCode: 1, errorMessage: "file already exists" };
2745
2925
  }
2746
- mkdirSync(workflowDir, { recursive: true });
2747
- writeFileSync3(workflowPath, githubWorkflow(), "utf8");
2926
+ mkdirSync2(workflowDir, { recursive: true });
2927
+ writeFileSync4(workflowPath, githubWorkflow(), "utf8");
2748
2928
  output.log(t("initCi.created", { path: workflowPath }));
2749
2929
  return { exitCode: 0 };
2750
2930
  }
@@ -2756,88 +2936,6 @@ function runInitCi(opts, output = console) {
2756
2936
  return { exitCode: 1, errorMessage: "no target specified" };
2757
2937
  }
2758
2938
 
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
2939
  // src/cli.ts
2842
2940
  var program = new Command();
2843
2941
  program.enablePositionalOptions().name("instrlint").description(
@@ -2855,27 +2953,27 @@ program.command("budget").description("Token budget analysis only").option(
2855
2953
  "--format <type>",
2856
2954
  "output format (terminal|json|markdown)",
2857
2955
  "terminal"
2858
- ).option("--tool <name>", "force tool detection (claude-code|codex|cursor)").action(async function() {
2956
+ ).option("--lang <locale>", "output language (en|zh-TW)").option("--tool <name>", "force tool detection (claude-code|codex|cursor)").action(async function() {
2859
2957
  const opts = this.opts();
2860
- const lang = this.parent?.opts()?.lang;
2958
+ const lang = opts.lang ?? this.parent?.opts()?.lang;
2861
2959
  const result = await runBudget({
2862
2960
  ...opts,
2863
2961
  ...lang !== void 0 && { lang }
2864
2962
  });
2865
2963
  if (result.exitCode !== 0) process.exit(result.exitCode);
2866
2964
  });
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() {
2965
+ 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
2966
  const opts = this.opts();
2869
- const lang = this.parent?.opts()?.lang;
2967
+ const lang = opts.lang ?? this.parent?.opts()?.lang;
2870
2968
  const result = await runDeadRules({
2871
2969
  ...opts,
2872
2970
  ...lang !== void 0 && { lang }
2873
2971
  });
2874
2972
  if (result.exitCode !== 0) process.exit(result.exitCode);
2875
2973
  });
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() {
2974
+ 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
2975
  const opts = this.opts();
2878
- const lang = this.parent?.opts()?.lang;
2976
+ const lang = opts.lang ?? this.parent?.opts()?.lang;
2879
2977
  const result = await runStructure({
2880
2978
  ...opts,
2881
2979
  ...lang !== void 0 && { lang }