instrlint 0.1.1 → 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/README.md CHANGED
@@ -149,7 +149,7 @@ npx instrlint install --claude-code --project
149
149
  npx instrlint install --codex
150
150
  ```
151
151
 
152
- Then in your editor:
152
+ Then **restart Claude Code** to activate the command. Then in your editor:
153
153
 
154
154
  ```
155
155
  /instrlint
@@ -157,6 +157,8 @@ Then in your editor:
157
157
  /instrlint ci --fail-on warning
158
158
  ```
159
159
 
160
+ > **Note:** Claude Code only loads custom commands at startup. `/reload-plugins` does not pick up newly installed commands.
161
+
160
162
  ## Score and grade
161
163
 
162
164
  | Grade | Score | Meaning |
package/README.zh-TW.md CHANGED
@@ -155,7 +155,7 @@ npx instrlint install --claude-code --project
155
155
  npx instrlint install --codex
156
156
  ```
157
157
 
158
- 安裝後在編輯器中使用:
158
+ 安裝後**重新啟動 Claude Code** 以啟用指令,然後在編輯器中使用:
159
159
 
160
160
  ```
161
161
  /instrlint
@@ -163,6 +163,8 @@ npx instrlint install --codex
163
163
  /instrlint ci --fail-on warning
164
164
  ```
165
165
 
166
+ > **注意:** Claude Code 只在啟動時載入 custom commands。`/reload-plugins` 無法載入新安裝的指令。
167
+
166
168
  ## 分數與等級
167
169
 
168
170
  | 等級 | 分數 | 說明 |
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
@@ -1592,9 +1593,13 @@ var en_default = {
1592
1593
  "ci.writtenTo": "\u2192 written to {{file}}",
1593
1594
  "initCi.created": "\u2713 Created {{path}}",
1594
1595
  "initCi.alreadyExists": "{{path}} already exists. Use --force to overwrite.",
1595
- "install.installed": "\u2713 Installed to {{path}}",
1596
+ "install.installed": "\u2713 Installed to {{path}}\n \u2192 Restart Claude Code to activate /instrlint",
1596
1597
  "install.alreadyExists": "{{path}} already exists. Use --force to overwrite.",
1597
1598
  "install.unknownTarget": "Specify --claude-code or --codex",
1599
+ "install.outdatedTitle": "instrlint skill is outdated",
1600
+ "install.outdatedVersions": "installed: {{installed}} \u2192 current: {{current}}",
1601
+ "install.updateCmd": "npx instrlint install {{flag}} --force",
1602
+ "install.updatePrompt": "Update skill now?",
1598
1603
  "fix.manualActions": "MANUAL ACTIONS NEEDED",
1599
1604
  "fix.hookCreate": "Add to .claude/settings.json:",
1600
1605
  "fix.hookWarning": "\u26A0 Hook executes shell commands \u2014 review carefully before adding",
@@ -1693,9 +1698,13 @@ var zh_TW_default = {
1693
1698
  "ci.writtenTo": "\u2192 \u5DF2\u5BEB\u5165 {{file}}",
1694
1699
  "initCi.created": "\u2713 \u5DF2\u5EFA\u7ACB {{path}}",
1695
1700
  "initCi.alreadyExists": "{{path}} \u5DF2\u5B58\u5728\u3002\u4F7F\u7528 --force \u8986\u84CB\u3002",
1696
- "install.installed": "\u2713 \u5DF2\u5B89\u88DD\u81F3 {{path}}",
1701
+ "install.installed": "\u2713 \u5DF2\u5B89\u88DD\u81F3 {{path}}\n \u2192 \u91CD\u65B0\u555F\u52D5 Claude Code \u5F8C\u5373\u53EF\u4F7F\u7528 /instrlint",
1697
1702
  "install.alreadyExists": "{{path}} \u5DF2\u5B58\u5728\u3002\u4F7F\u7528 --force \u8986\u84CB\u3002",
1698
1703
  "install.unknownTarget": "\u8ACB\u6307\u5B9A --claude-code \u6216 --codex",
1704
+ "install.outdatedTitle": "instrlint skill \u7248\u672C\u904E\u820A",
1705
+ "install.outdatedVersions": "\u5DF2\u5B89\u88DD\uFF1A{{installed}} \u2192 \u6700\u65B0\uFF1A{{current}}",
1706
+ "install.updateCmd": "npx instrlint install {{flag}} --force",
1707
+ "install.updatePrompt": "\u662F\u5426\u7ACB\u5373\u66F4\u65B0\uFF1F",
1699
1708
  "fix.manualActions": "\u9700\u8981\u624B\u52D5\u64CD\u4F5C",
1700
1709
  "fix.hookCreate": "\u52A0\u5165 .claude/settings.json\uFF1A",
1701
1710
  "fix.hookWarning": "\u26A0 Hook \u6703\u57F7\u884C shell command\uFF0C\u8ACB\u4ED4\u7D30\u78BA\u8A8D\u5F8C\u518D\u52A0\u5165",
@@ -2290,6 +2299,136 @@ function markdownStructureSuggestions(suggestions, projectRoot) {
2290
2299
  return lines;
2291
2300
  }
2292
2301
 
2302
+ // src/utils/skill-version.ts
2303
+ var import_fs10 = require("fs");
2304
+ var import_path8 = require("path");
2305
+ var import_os = require("os");
2306
+ var CURRENT_VERSION = "0.1.3";
2307
+ var VERSION_RE = /^instrlint-version:\s*(.+)$/m;
2308
+ function extractVersion(content) {
2309
+ const m = VERSION_RE.exec(content);
2310
+ return m ? m[1].trim() : null;
2311
+ }
2312
+ function readInstalledVersion(path) {
2313
+ if (!(0, import_fs10.existsSync)(path)) return null;
2314
+ try {
2315
+ return extractVersion((0, import_fs10.readFileSync)(path, "utf8"));
2316
+ } catch {
2317
+ return null;
2318
+ }
2319
+ }
2320
+ function checkSkillUpdate(projectRoot) {
2321
+ const candidates = [
2322
+ {
2323
+ path: (0, import_path8.join)(projectRoot, ".claude", "commands", "instrlint.md"),
2324
+ isProject: true
2325
+ },
2326
+ {
2327
+ path: (0, import_path8.join)((0, import_os.homedir)(), ".claude", "commands", "instrlint.md"),
2328
+ isProject: false
2329
+ }
2330
+ ];
2331
+ for (const { path, isProject } of candidates) {
2332
+ const installed = readInstalledVersion(path);
2333
+ if (installed !== null && installed !== CURRENT_VERSION) {
2334
+ return {
2335
+ installedVersion: installed,
2336
+ currentVersion: CURRENT_VERSION,
2337
+ installPath: path,
2338
+ isProject
2339
+ };
2340
+ }
2341
+ }
2342
+ return null;
2343
+ }
2344
+ function injectVersion(content, version) {
2345
+ return content.replace(/^(---\n[\s\S]*?)(---)$/m, `$1instrlint-version: ${version}
2346
+ $2`);
2347
+ }
2348
+
2349
+ // src/commands/install-command.ts
2350
+ var import_fs11 = require("fs");
2351
+ var import_path9 = require("path");
2352
+ var import_os2 = require("os");
2353
+ var import_url = require("url");
2354
+ function resolveSkillFile(target) {
2355
+ const thisFile = (0, import_url.fileURLToPath)(importMetaUrl);
2356
+ const subDir = target === "claude-code" ? "claude-code" : "codex";
2357
+ for (const levels of [2, 3]) {
2358
+ const parts = Array(levels).fill("..");
2359
+ const candidate = (0, import_path9.join)(thisFile, ...parts, "skills", subDir, "SKILL.md");
2360
+ if ((0, import_fs11.existsSync)(candidate)) return candidate;
2361
+ }
2362
+ return (0, import_path9.join)(thisFile, "..", "..", "skills", subDir, "SKILL.md");
2363
+ }
2364
+ function readSkillContent(target) {
2365
+ const skillPath = resolveSkillFile(target);
2366
+ try {
2367
+ const raw = (0, import_fs11.readFileSync)(skillPath, "utf8");
2368
+ return injectVersion(raw, CURRENT_VERSION);
2369
+ } catch {
2370
+ throw new Error(
2371
+ `Could not read skill file at ${skillPath}. Make sure the package is properly installed.`
2372
+ );
2373
+ }
2374
+ }
2375
+ function installClaudeCode(content, projectRoot, isProject, force, output) {
2376
+ const targetDir = isProject ? (0, import_path9.join)(projectRoot, ".claude", "commands") : (0, import_path9.join)((0, import_os2.homedir)(), ".claude", "commands");
2377
+ const targetPath = (0, import_path9.join)(targetDir, "instrlint.md");
2378
+ if ((0, import_fs11.existsSync)(targetPath) && !force) {
2379
+ output.error(t("install.alreadyExists", { path: targetPath }));
2380
+ return { exitCode: 1, errorMessage: "file already exists" };
2381
+ }
2382
+ (0, import_fs11.mkdirSync)(targetDir, { recursive: true });
2383
+ (0, import_fs11.writeFileSync)(targetPath, content, "utf8");
2384
+ output.log(t("install.installed", { path: targetPath }));
2385
+ return { exitCode: 0 };
2386
+ }
2387
+ function installCodex(content, projectRoot, force, output) {
2388
+ const targetDir = (0, import_path9.join)(projectRoot, ".agents", "skills", "instrlint");
2389
+ const targetPath = (0, import_path9.join)(targetDir, "SKILL.md");
2390
+ if ((0, import_fs11.existsSync)(targetPath) && !force) {
2391
+ output.error(t("install.alreadyExists", { path: targetPath }));
2392
+ return { exitCode: 1, errorMessage: "file already exists" };
2393
+ }
2394
+ (0, import_fs11.mkdirSync)(targetDir, { recursive: true });
2395
+ (0, import_fs11.writeFileSync)(targetPath, content, "utf8");
2396
+ output.log(t("install.installed", { path: targetPath }));
2397
+ return { exitCode: 0 };
2398
+ }
2399
+ function runInstall(opts, output = console) {
2400
+ const projectRoot = opts.projectRoot ?? process.cwd();
2401
+ const force = opts.force ?? false;
2402
+ if (opts.claudeCode) {
2403
+ let content;
2404
+ try {
2405
+ content = readSkillContent("claude-code");
2406
+ } catch (err) {
2407
+ output.error(String(err));
2408
+ return { exitCode: 1, errorMessage: String(err) };
2409
+ }
2410
+ return installClaudeCode(
2411
+ content,
2412
+ projectRoot,
2413
+ opts.project ?? false,
2414
+ force,
2415
+ output
2416
+ );
2417
+ }
2418
+ if (opts.codex) {
2419
+ let content;
2420
+ try {
2421
+ content = readSkillContent("codex");
2422
+ } catch (err) {
2423
+ output.error(String(err));
2424
+ return { exitCode: 1, errorMessage: String(err) };
2425
+ }
2426
+ return installCodex(content, projectRoot, force, output);
2427
+ }
2428
+ output.error(t("install.unknownTarget"));
2429
+ return { exitCode: 1, errorMessage: "no target specified" };
2430
+ }
2431
+
2293
2432
  // src/commands/run-command.ts
2294
2433
  function isGitClean(cwd) {
2295
2434
  try {
@@ -2334,7 +2473,7 @@ async function runAll(opts, output = console) {
2334
2473
  const { score, grade } = calculateScore(allFindings, summary);
2335
2474
  const actionPlan = buildActionPlan(allFindings);
2336
2475
  const report = {
2337
- project: (0, import_path8.basename)(projectRoot),
2476
+ project: (0, import_path10.basename)(projectRoot),
2338
2477
  tool: instructions.tool,
2339
2478
  score,
2340
2479
  grade,
@@ -2379,6 +2518,7 @@ async function runAll(opts, output = console) {
2379
2518
  printStructureSuggestions(suggestions, projectRoot, output);
2380
2519
  return { exitCode: 0 };
2381
2520
  }
2521
+ const skillUpdate = checkSkillUpdate(projectRoot);
2382
2522
  if (opts.format === "json") {
2383
2523
  output.log(reportJson(report));
2384
2524
  return { exitCode: 0 };
@@ -2386,12 +2526,52 @@ async function runAll(opts, output = console) {
2386
2526
  if (opts.format === "markdown") {
2387
2527
  const mdSuggestions = buildStructureSuggestions(allFindings);
2388
2528
  const mdExtra = markdownStructureSuggestions(mdSuggestions, projectRoot);
2389
- output.log(reportMarkdown(report, mdExtra));
2529
+ const updateSection = skillUpdate ? [
2530
+ "",
2531
+ `> \u26A0\uFE0F **${t("install.outdatedTitle")}** (${t("install.outdatedVersions", { installed: skillUpdate.installedVersion, current: skillUpdate.currentVersion })})`,
2532
+ `> \`${t("install.updateCmd", { flag: skillUpdate.isProject ? "--claude-code --project" : "--claude-code" })}\``
2533
+ ] : [];
2534
+ output.log(reportMarkdown(report, [...mdExtra, ...updateSection]));
2390
2535
  return { exitCode: 0 };
2391
2536
  }
2392
2537
  printCombinedTerminal(report, output);
2538
+ if (skillUpdate && process.stdout.isTTY) {
2539
+ output.log("");
2540
+ output.log(
2541
+ ` ${import_chalk4.default.yellow("\u26A0")} ${import_chalk4.default.bold(t("install.outdatedTitle"))} (${t("install.outdatedVersions", { installed: skillUpdate.installedVersion, current: skillUpdate.currentVersion })})`
2542
+ );
2543
+ const confirmed = await promptYesNo(
2544
+ ` ${t("install.updatePrompt")}`,
2545
+ output
2546
+ );
2547
+ if (confirmed) {
2548
+ runInstall(
2549
+ {
2550
+ claudeCode: true,
2551
+ project: skillUpdate.isProject,
2552
+ force: true,
2553
+ projectRoot
2554
+ },
2555
+ output
2556
+ );
2557
+ }
2558
+ output.log("");
2559
+ }
2393
2560
  return { exitCode: 0 };
2394
2561
  }
2562
+ function promptYesNo(question, output) {
2563
+ return new Promise((resolve) => {
2564
+ const rl = (0, import_readline.createInterface)({
2565
+ input: process.stdin,
2566
+ output: process.stdout
2567
+ });
2568
+ rl.question(`${question} ${import_chalk4.default.gray("[Y/n]")} `, (answer) => {
2569
+ rl.close();
2570
+ const trimmed = answer.trim().toLowerCase();
2571
+ resolve(trimmed === "" || trimmed === "y");
2572
+ });
2573
+ });
2574
+ }
2395
2575
 
2396
2576
  // src/commands/deadrules-command.ts
2397
2577
  var import_chalk5 = __toESM(require("chalk"), 1);
@@ -2552,8 +2732,8 @@ async function runStructure(opts, output = console) {
2552
2732
  }
2553
2733
 
2554
2734
  // src/commands/ci-command.ts
2555
- var import_fs10 = require("fs");
2556
- var import_path9 = require("path");
2735
+ var import_fs12 = require("fs");
2736
+ var import_path11 = require("path");
2557
2737
 
2558
2738
  // src/reporters/sarif.ts
2559
2739
  function severityToLevel(severity) {
@@ -2659,7 +2839,7 @@ async function runCi(opts, output = console) {
2659
2839
  const { score, grade } = calculateScore(allFindings, summary);
2660
2840
  const actionPlan = buildActionPlan(allFindings);
2661
2841
  const report = {
2662
- project: (0, import_path9.basename)(projectRoot),
2842
+ project: (0, import_path11.basename)(projectRoot),
2663
2843
  tool: instructions.tool,
2664
2844
  score,
2665
2845
  grade,
@@ -2680,7 +2860,7 @@ async function runCi(opts, output = console) {
2680
2860
  formatted = reportJson(report);
2681
2861
  }
2682
2862
  if (opts.output != null) {
2683
- (0, import_fs10.writeFileSync)(opts.output, formatted, "utf8");
2863
+ (0, import_fs12.writeFileSync)(opts.output, formatted, "utf8");
2684
2864
  const pass = !shouldFail(allFindings, failOn);
2685
2865
  const statusKey = pass ? "ci.passed" : "ci.failed";
2686
2866
  output.error(
@@ -2694,8 +2874,8 @@ async function runCi(opts, output = console) {
2694
2874
  }
2695
2875
 
2696
2876
  // src/commands/init-ci-command.ts
2697
- var import_fs11 = require("fs");
2698
- var import_path10 = require("path");
2877
+ var import_fs13 = require("fs");
2878
+ var import_path12 = require("path");
2699
2879
  function githubWorkflow() {
2700
2880
  return `name: instrlint
2701
2881
 
@@ -2764,14 +2944,14 @@ instrlint:
2764
2944
  function runInitCi(opts, output = console) {
2765
2945
  const projectRoot = opts.projectRoot ?? process.cwd();
2766
2946
  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) {
2947
+ const workflowDir = (0, import_path12.join)(projectRoot, ".github", "workflows");
2948
+ const workflowPath = (0, import_path12.join)(workflowDir, "instrlint.yml");
2949
+ if ((0, import_fs13.existsSync)(workflowPath) && !opts.force) {
2770
2950
  output.error(t("initCi.alreadyExists", { path: workflowPath }));
2771
2951
  return { exitCode: 1, errorMessage: "file already exists" };
2772
2952
  }
2773
- (0, import_fs11.mkdirSync)(workflowDir, { recursive: true });
2774
- (0, import_fs11.writeFileSync)(workflowPath, githubWorkflow(), "utf8");
2953
+ (0, import_fs13.mkdirSync)(workflowDir, { recursive: true });
2954
+ (0, import_fs13.writeFileSync)(workflowPath, githubWorkflow(), "utf8");
2775
2955
  output.log(t("initCi.created", { path: workflowPath }));
2776
2956
  return { exitCode: 0 };
2777
2957
  }
@@ -2783,88 +2963,6 @@ function runInitCi(opts, output = console) {
2783
2963
  return { exitCode: 1, errorMessage: "no target specified" };
2784
2964
  }
2785
2965
 
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", "skills", "instrlint") : (0, import_path11.join)((0, import_os.homedir)(), ".claude", "skills", "instrlint");
2813
- const targetPath = (0, import_path11.join)(targetDir, "SKILL.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
2966
  // src/cli.ts
2869
2967
  var program = new import_commander.Command();
2870
2968
  program.enablePositionalOptions().name("instrlint").description(
@@ -2882,27 +2980,27 @@ program.command("budget").description("Token budget analysis only").option(
2882
2980
  "--format <type>",
2883
2981
  "output format (terminal|json|markdown)",
2884
2982
  "terminal"
2885
- ).option("--tool <name>", "force tool detection (claude-code|codex|cursor)").action(async function() {
2983
+ ).option("--lang <locale>", "output language (en|zh-TW)").option("--tool <name>", "force tool detection (claude-code|codex|cursor)").action(async function() {
2886
2984
  const opts = this.opts();
2887
- const lang = this.parent?.opts()?.lang;
2985
+ const lang = opts.lang ?? this.parent?.opts()?.lang;
2888
2986
  const result = await runBudget({
2889
2987
  ...opts,
2890
2988
  ...lang !== void 0 && { lang }
2891
2989
  });
2892
2990
  if (result.exitCode !== 0) process.exit(result.exitCode);
2893
2991
  });
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() {
2992
+ 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
2993
  const opts = this.opts();
2896
- const lang = this.parent?.opts()?.lang;
2994
+ const lang = opts.lang ?? this.parent?.opts()?.lang;
2897
2995
  const result = await runDeadRules({
2898
2996
  ...opts,
2899
2997
  ...lang !== void 0 && { lang }
2900
2998
  });
2901
2999
  if (result.exitCode !== 0) process.exit(result.exitCode);
2902
3000
  });
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() {
3001
+ 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
3002
  const opts = this.opts();
2905
- const lang = this.parent?.opts()?.lang;
3003
+ const lang = opts.lang ?? this.parent?.opts()?.lang;
2906
3004
  const result = await runStructure({
2907
3005
  ...opts,
2908
3006
  ...lang !== void 0 && { lang }