@staff0rd/assist 0.96.0 → 0.97.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ import { Command } from "commander";
6
6
  // package.json
7
7
  var package_default = {
8
8
  name: "@staff0rd/assist",
9
- version: "0.96.0",
9
+ version: "0.97.1",
10
10
  type: "module",
11
11
  main: "dist/index.js",
12
12
  bin: {
@@ -44,6 +44,7 @@ var package_default = {
44
44
  minimatch: "^10.1.1",
45
45
  "node-notifier": "^10.0.1",
46
46
  semver: "^7.7.3",
47
+ "shell-quote": "^1.8.3",
47
48
  typescript: "^5.9.3",
48
49
  yaml: "^2.8.2",
49
50
  zod: "^4.3.6"
@@ -59,6 +60,7 @@ var package_default = {
59
60
  "@types/react": "^19.2.14",
60
61
  "@types/react-dom": "^19.2.3",
61
62
  "@types/semver": "^7.7.1",
63
+ "@types/shell-quote": "^1.7.5",
62
64
  esbuild: "^0.27.3",
63
65
  jotai: "^2.18.0",
64
66
  jscpd: "^4.0.5",
@@ -1625,14 +1627,14 @@ function flushIfFailed(exitCode, chunks) {
1625
1627
 
1626
1628
  // src/commands/verify/run/runAllEntries.ts
1627
1629
  function runEntry(entry, onComplete) {
1628
- return new Promise((resolve5) => {
1630
+ return new Promise((resolve6) => {
1629
1631
  const child = spawnCommand(entry.fullCommand, entry.cwd, entry.env);
1630
1632
  const chunks = collectOutput(child);
1631
1633
  child.on("close", (code) => {
1632
1634
  const exitCode = code ?? 1;
1633
1635
  flushIfFailed(exitCode, chunks);
1634
1636
  onComplete?.(exitCode);
1635
- resolve5({ script: entry.name, code: exitCode });
1637
+ resolve6({ script: entry.name, code: exitCode });
1636
1638
  });
1637
1639
  });
1638
1640
  }
@@ -2375,12 +2377,12 @@ function respondJson(res, status2, data) {
2375
2377
  res.end(JSON.stringify(data));
2376
2378
  }
2377
2379
  function readBody(req) {
2378
- return new Promise((resolve5, reject) => {
2380
+ return new Promise((resolve6, reject) => {
2379
2381
  let body = "";
2380
2382
  req.on("data", (chunk) => {
2381
2383
  body += chunk.toString();
2382
2384
  });
2383
- req.on("end", () => resolve5(body));
2385
+ req.on("end", () => resolve6(body));
2384
2386
  req.on("error", reject);
2385
2387
  });
2386
2388
  }
@@ -2517,6 +2519,9 @@ function registerBacklog(program2) {
2517
2519
  backlogCommand.command("web").description("Start a web view of the backlog").option("-p, --port <number>", "Port to listen on", "3000").action(web);
2518
2520
  }
2519
2521
 
2522
+ // src/shared/isApprovedRead.ts
2523
+ import { resolve as resolve3 } from "path";
2524
+
2520
2525
  // src/shared/tokenize.ts
2521
2526
  function tokenize(command) {
2522
2527
  const tokens = [];
@@ -2596,7 +2601,6 @@ function extractGraphqlQuery(args) {
2596
2601
  }
2597
2602
 
2598
2603
  // src/shared/loadCliReads.ts
2599
- import { execFileSync } from "child_process";
2600
2604
  import { existsSync as existsSync16, readFileSync as readFileSync13, writeFileSync as writeFileSync12 } from "fs";
2601
2605
  import { dirname as dirname12, resolve as resolve2 } from "path";
2602
2606
  import { fileURLToPath as fileURLToPath4 } from "url";
@@ -2605,31 +2609,155 @@ var __dirname5 = dirname12(__filename2);
2605
2609
  function getCliReadsPath() {
2606
2610
  return resolve2(__dirname5, "..", "assist.cli-reads");
2607
2611
  }
2608
- function loadCliReads() {
2612
+ var cachedLines;
2613
+ function getCliReadsLines() {
2614
+ if (cachedLines) return cachedLines;
2609
2615
  const path31 = getCliReadsPath();
2610
- if (!existsSync16(path31)) return [];
2611
- return readFileSync13(path31, "utf-8").split("\n").filter((line) => line.trim() !== "");
2616
+ if (!existsSync16(path31)) {
2617
+ cachedLines = [];
2618
+ return cachedLines;
2619
+ }
2620
+ cachedLines = readFileSync13(path31, "utf-8").split("\n").filter((line) => line.trim() !== "");
2621
+ return cachedLines;
2622
+ }
2623
+ function loadCliReads() {
2624
+ return getCliReadsLines();
2612
2625
  }
2613
2626
  function saveCliReads(commands) {
2614
2627
  writeFileSync12(getCliReadsPath(), `${commands.join("\n")}
2615
2628
  `);
2629
+ cachedLines = void 0;
2616
2630
  }
2617
2631
  function findCliRead(command) {
2618
- const filePath = getCliReadsPath();
2619
- if (!existsSync16(filePath)) return void 0;
2620
2632
  const words = command.split(/\s+/);
2621
2633
  if (words.length < 2) return void 0;
2622
2634
  const prefix2 = `${words[0]} ${words[1]}`;
2623
- let candidates;
2635
+ const candidates = getCliReadsLines().filter(
2636
+ (line) => line === prefix2 || line.startsWith(`${prefix2} `)
2637
+ );
2638
+ return candidates.sort((a, b) => b.length - a.length).find((rc) => command === rc || command.startsWith(`${rc} `));
2639
+ }
2640
+
2641
+ // src/shared/matchesBashAllow.ts
2642
+ import { existsSync as existsSync17, readFileSync as readFileSync14 } from "fs";
2643
+ import { homedir as homedir3 } from "os";
2644
+ import { join as join11 } from "path";
2645
+ var cached;
2646
+ function loadBashAllowPrefixes() {
2647
+ if (cached) return cached;
2648
+ cached = parsePrefixes(collectAllowEntries());
2649
+ return cached;
2650
+ }
2651
+ function matchesBashAllow(command) {
2652
+ const prefixes = loadBashAllowPrefixes();
2653
+ return prefixes.find(
2654
+ (pfx) => command === pfx || command.startsWith(`${pfx} `)
2655
+ );
2656
+ }
2657
+ function collectAllowEntries() {
2658
+ const paths = [
2659
+ join11(homedir3(), ".claude", "settings.json"),
2660
+ join11(process.cwd(), ".claude", "settings.json"),
2661
+ join11(process.cwd(), ".claude", "settings.local.json")
2662
+ ];
2663
+ const entries = [];
2664
+ for (const p of paths) {
2665
+ entries.push(...readAllowArray(p));
2666
+ }
2667
+ return entries;
2668
+ }
2669
+ function readAllowArray(filePath) {
2670
+ if (!existsSync17(filePath)) return [];
2624
2671
  try {
2625
- const output = execFileSync("grep", ["-E", `^${prefix2}( |$)`, filePath], {
2626
- encoding: "utf-8"
2627
- });
2628
- candidates = output.split("\n").filter((l) => l !== "");
2672
+ const data = JSON.parse(readFileSync14(filePath, "utf-8"));
2673
+ const allow = data?.permissions?.allow;
2674
+ return Array.isArray(allow) ? allow.filter((e) => typeof e === "string") : [];
2675
+ } catch {
2676
+ return [];
2677
+ }
2678
+ }
2679
+ function parsePrefixes(entries) {
2680
+ const re = /^Bash\((.+?)(?::.*)\)$/;
2681
+ const prefixes = [];
2682
+ for (const entry of entries) {
2683
+ const m = entry.match(re);
2684
+ if (m) prefixes.push(m[1]);
2685
+ }
2686
+ return prefixes;
2687
+ }
2688
+
2689
+ // src/shared/isApprovedRead.ts
2690
+ function isApprovedRead(command) {
2691
+ if (isCdToCwd(command)) return "cd to current directory";
2692
+ const matched = findCliRead(command);
2693
+ if (matched) return `Read-only CLI command: ${matched}`;
2694
+ if (isGhApiRead(command)) return "Read-only gh api command";
2695
+ const allowMatch = matchesBashAllow(command);
2696
+ if (allowMatch) return `Allowed by settings: ${allowMatch}`;
2697
+ return void 0;
2698
+ }
2699
+ function isCdToCwd(command) {
2700
+ const parts = command.split(/\s+/);
2701
+ if (parts[0] !== "cd" || parts.length > 2) return false;
2702
+ if (parts.length === 1) return false;
2703
+ const resolved = resolve3(normalizeMsysPath(parts[1]));
2704
+ return resolved === resolve3(process.cwd());
2705
+ }
2706
+ function normalizeMsysPath(p) {
2707
+ const m = p.match(/^\/([a-zA-Z])(\/.*)/);
2708
+ return m ? `${m[1].toUpperCase()}:${m[2]}` : p;
2709
+ }
2710
+
2711
+ // src/shared/splitCompound.ts
2712
+ import { parse } from "shell-quote";
2713
+ var SEPARATOR_OPS = /* @__PURE__ */ new Set(["|", "&&", "||", ";"]);
2714
+ var UNSAFE_OPS = /* @__PURE__ */ new Set(["(", ")", ">", ">>", "<", "<&", "|&", ">&"]);
2715
+ var FD_REDIRECT_RE = /\d+>&\d+/g;
2716
+ function splitCompound(command) {
2717
+ const tokens = tokenizeCommand(command);
2718
+ if (!tokens) return void 0;
2719
+ const groups = groupByOperator(tokens);
2720
+ if (!groups) return void 0;
2721
+ const result = groups.map((parts) => stripEnvPrefix(parts).join(" ")).filter((cmd) => cmd !== "");
2722
+ return result.length > 0 ? result : void 0;
2723
+ }
2724
+ function tokenizeCommand(command) {
2725
+ const trimmed = command.trim().replace(FD_REDIRECT_RE, "");
2726
+ if (!trimmed) return void 0;
2727
+ try {
2728
+ const tokens = parse(trimmed);
2729
+ const hasBacktick = tokens.some(
2730
+ (t) => typeof t === "string" && /`.+`/.test(t)
2731
+ );
2732
+ return hasBacktick ? void 0 : tokens;
2629
2733
  } catch {
2630
2734
  return void 0;
2631
2735
  }
2632
- return candidates.sort((a, b) => b.length - a.length).find((rc) => command === rc || command.startsWith(`${rc} `));
2736
+ }
2737
+ function getOp(token) {
2738
+ return typeof token === "object" && token !== null && "op" in token ? token.op : void 0;
2739
+ }
2740
+ function groupByOperator(tokens) {
2741
+ const groups = [[]];
2742
+ for (const token of tokens) {
2743
+ const op = getOp(token);
2744
+ if (op === void 0) {
2745
+ if (typeof token !== "string") return void 0;
2746
+ groups[groups.length - 1].push(token);
2747
+ } else if (SEPARATOR_OPS.has(op)) {
2748
+ groups.push([]);
2749
+ } else if (UNSAFE_OPS.has(op)) {
2750
+ return void 0;
2751
+ } else {
2752
+ return void 0;
2753
+ }
2754
+ }
2755
+ return groups;
2756
+ }
2757
+ function stripEnvPrefix(parts) {
2758
+ let i = 0;
2759
+ while (i < parts.length && /^[A-Za-z_]\w*=/.test(parts[i])) i++;
2760
+ return i > 0 ? parts.slice(i) : parts;
2633
2761
  }
2634
2762
 
2635
2763
  // src/commands/cliHook/index.ts
@@ -2645,1291 +2773,1309 @@ async function cliHook() {
2645
2773
  return;
2646
2774
  }
2647
2775
  const command = data.tool_input.command.trim();
2648
- const matched = findCliRead(command);
2649
- if (matched) {
2650
- console.log(
2651
- JSON.stringify({
2652
- hookSpecificOutput: {
2653
- hookEventName: "PreToolUse",
2654
- permissionDecision: "allow",
2655
- permissionDecisionReason: `Read-only CLI command: ${matched}`
2656
- }
2657
- })
2658
- );
2776
+ const parts = splitCompound(command);
2777
+ if (!parts) return;
2778
+ const reasons = [];
2779
+ for (const part of parts) {
2780
+ const reason = isApprovedRead(part);
2781
+ if (!reason) return;
2782
+ reasons.push(reason);
2783
+ }
2784
+ console.log(
2785
+ JSON.stringify({
2786
+ hookSpecificOutput: {
2787
+ hookEventName: "PreToolUse",
2788
+ permissionDecision: "allow",
2789
+ permissionDecisionReason: reasons.join("; ")
2790
+ }
2791
+ })
2792
+ );
2793
+ }
2794
+
2795
+ // src/commands/cliHook/cliHookCheck.ts
2796
+ function cliHookCheck(command) {
2797
+ const trimmed = command.trim();
2798
+ const parts = splitCompound(trimmed);
2799
+ if (!parts) {
2800
+ console.log("not approved (unable to parse)");
2801
+ process.exitCode = 1;
2659
2802
  return;
2660
2803
  }
2661
- if (isGhApiRead(command)) {
2662
- console.log(
2663
- JSON.stringify({
2664
- hookSpecificOutput: {
2665
- hookEventName: "PreToolUse",
2666
- permissionDecision: "allow",
2667
- permissionDecisionReason: "Read-only gh api command"
2668
- }
2669
- })
2670
- );
2804
+ const reasons = [];
2805
+ for (const part of parts) {
2806
+ const reason = isApprovedRead(part);
2807
+ if (!reason) {
2808
+ console.log(`not approved (unrecognised: ${part})`);
2809
+ process.exitCode = 1;
2810
+ return;
2811
+ }
2812
+ reasons.push(` ${part} -> ${reason}`);
2671
2813
  }
2814
+ console.log(`approved
2815
+ ${reasons.join("\n")}`);
2672
2816
  }
2673
2817
 
2674
- // src/commands/registerCliHook.ts
2675
- function registerCliHook(program2) {
2676
- program2.command("cli-hook").description("PreToolUse hook for auto-approving read-only CLI commands").action(() => {
2677
- cliHook();
2678
- });
2679
- }
2818
+ // src/commands/permitCliReads/index.ts
2819
+ import { existsSync as existsSync18, mkdirSync as mkdirSync4, readFileSync as readFileSync15, writeFileSync as writeFileSync13 } from "fs";
2820
+ import { homedir as homedir4 } from "os";
2821
+ import { join as join12 } from "path";
2680
2822
 
2681
- // src/commands/complexity/analyze.ts
2682
- import chalk35 from "chalk";
2823
+ // src/shared/getInstallDir.ts
2824
+ import { execSync as execSync13 } from "child_process";
2825
+ import { dirname as dirname13, resolve as resolve4 } from "path";
2826
+ import { fileURLToPath as fileURLToPath5 } from "url";
2827
+ var __filename3 = fileURLToPath5(import.meta.url);
2828
+ var __dirname6 = dirname13(__filename3);
2829
+ function getInstallDir() {
2830
+ return resolve4(__dirname6, "..");
2831
+ }
2832
+ function isGitRepo(dir) {
2833
+ try {
2834
+ const result = execSync13("git rev-parse --show-toplevel", {
2835
+ cwd: dir,
2836
+ stdio: "pipe"
2837
+ }).toString().trim();
2838
+ return resolve4(result) === resolve4(dir);
2839
+ } catch {
2840
+ return false;
2841
+ }
2842
+ }
2683
2843
 
2684
- // src/commands/complexity/cyclomatic.ts
2685
- import chalk31 from "chalk";
2844
+ // src/commands/permitCliReads/assertCliExists.ts
2845
+ import { execSync as execSync14 } from "child_process";
2846
+ function assertCliExists(cli) {
2847
+ const binary = cli.split(/\s+/)[0];
2848
+ const opts = {
2849
+ encoding: "utf-8",
2850
+ stdio: ["ignore", "pipe", "pipe"]
2851
+ };
2852
+ try {
2853
+ execSync14(`command -v ${binary}`, opts);
2854
+ } catch {
2855
+ try {
2856
+ execSync14(`where ${binary}`, opts);
2857
+ } catch {
2858
+ console.error(`CLI "${cli}" not found in PATH`);
2859
+ process.exit(1);
2860
+ }
2861
+ }
2862
+ }
2686
2863
 
2687
- // src/commands/complexity/shared/index.ts
2688
- import fs11 from "fs";
2689
- import path16 from "path";
2864
+ // src/commands/permitCliReads/colorize.ts
2690
2865
  import chalk30 from "chalk";
2691
- import ts5 from "typescript";
2866
+ function colorize(plainOutput) {
2867
+ return plainOutput.split("\n").map((line) => {
2868
+ if (line.startsWith(" R ")) return chalk30.green(line);
2869
+ if (line.startsWith(" W ")) return chalk30.red(line);
2870
+ return line;
2871
+ }).join("\n");
2872
+ }
2692
2873
 
2693
- // src/commands/complexity/findSourceFiles.ts
2694
- import fs10 from "fs";
2695
- import path15 from "path";
2696
- import { minimatch as minimatch3 } from "minimatch";
2697
- function applyIgnoreGlobs(files) {
2698
- const { complexity } = loadConfig();
2699
- return files.filter(
2700
- (f) => !complexity.ignore.some((glob) => minimatch3(f, glob))
2701
- );
2874
+ // src/lib/isClaudeCode.ts
2875
+ function isClaudeCode() {
2876
+ return process.env.CLAUDECODE !== void 0;
2702
2877
  }
2703
- function walk(dir, results) {
2704
- if (!fs10.existsSync(dir)) {
2705
- return;
2706
- }
2707
- const extensions = [".ts", ".tsx"];
2708
- const entries = fs10.readdirSync(dir, { withFileTypes: true });
2709
- for (const entry of entries) {
2710
- const fullPath = path15.join(dir, entry.name);
2711
- if (entry.isDirectory()) {
2712
- if (entry.name !== "node_modules" && entry.name !== ".git") {
2713
- walk(fullPath, results);
2714
- }
2715
- } else if (entry.isFile() && extensions.some((ext) => entry.name.endsWith(ext))) {
2716
- results.push(fullPath);
2878
+
2879
+ // src/commands/permitCliReads/mapAsync.ts
2880
+ async function mapAsync(items, concurrency, fn) {
2881
+ const results = new Array(items.length);
2882
+ let next2 = 0;
2883
+ async function worker() {
2884
+ while (next2 < items.length) {
2885
+ const idx = next2++;
2886
+ results[idx] = await fn(items[idx]);
2717
2887
  }
2718
2888
  }
2889
+ const workers = Array.from(
2890
+ { length: Math.min(concurrency, items.length) },
2891
+ () => worker()
2892
+ );
2893
+ await Promise.all(workers);
2894
+ return results;
2719
2895
  }
2720
- function findSourceFiles2(pattern2, baseDir = ".") {
2721
- const results = [];
2722
- if (pattern2.includes("*")) {
2723
- walk(baseDir, results);
2724
- return applyIgnoreGlobs(results.filter((f) => minimatch3(f, pattern2)));
2896
+
2897
+ // src/commands/permitCliReads/parseCommands.ts
2898
+ var COMMAND_SECTION_RE = /^((?:core |general |available |additional |other |management |targeted |alias |github actions )?(?:commands|subgroups)):?$/i;
2899
+ function isSkippable(name) {
2900
+ return name.startsWith("-") || name.startsWith("<") || name.startsWith("[");
2901
+ }
2902
+ function parseCommandLine(trimmed) {
2903
+ const azMatch = trimmed.match(/^(\S+)\s+(?:\[.*?]\s+)?:\s*(.+)/);
2904
+ if (azMatch && !isSkippable(azMatch[1])) {
2905
+ return { name: azMatch[1], description: azMatch[2].trim() };
2725
2906
  }
2726
- if (fs10.existsSync(pattern2) && fs10.statSync(pattern2).isFile()) {
2727
- return [pattern2];
2907
+ const colonMatch = trimmed.match(/^(\S+?):\s{2,}(.+)/);
2908
+ if (colonMatch && !isSkippable(colonMatch[1])) {
2909
+ return { name: colonMatch[1], description: colonMatch[2].trim() };
2728
2910
  }
2729
- if (fs10.existsSync(pattern2) && fs10.statSync(pattern2).isDirectory()) {
2730
- walk(pattern2, results);
2731
- return applyIgnoreGlobs(results);
2911
+ const spaceMatch = trimmed.match(/^(\S+)(?:,\s*\S+)?\s{2,}(.+)/);
2912
+ if (spaceMatch && !isSkippable(spaceMatch[1])) {
2913
+ return { name: spaceMatch[1], description: spaceMatch[2].trim() };
2732
2914
  }
2733
- walk(baseDir, results);
2734
- return applyIgnoreGlobs(results.filter((f) => minimatch3(f, pattern2)));
2735
- }
2736
-
2737
- // src/commands/complexity/shared/getNodeName.ts
2738
- import ts from "typescript";
2739
- var FUNCTION_TYPE_CHECKS = [
2740
- ts.isFunctionDeclaration,
2741
- ts.isFunctionExpression,
2742
- ts.isArrowFunction,
2743
- ts.isMethodDeclaration,
2744
- ts.isGetAccessor,
2745
- ts.isSetAccessor,
2746
- ts.isConstructorDeclaration
2747
- ];
2748
- function getIdentifierText(name) {
2749
- if (ts.isIdentifier(name) || ts.isStringLiteral(name)) return name.text;
2750
- return "<computed>";
2751
- }
2752
- function getArrowFunctionName(node) {
2753
- const { parent } = node;
2754
- if (ts.isVariableDeclaration(parent) && ts.isIdentifier(parent.name))
2755
- return parent.name.text;
2756
- if (ts.isPropertyAssignment(parent) && ts.isIdentifier(parent.name))
2757
- return parent.name.text;
2758
- return "<arrow>";
2915
+ if (/^\S+$/.test(trimmed) && !isSkippable(trimmed)) {
2916
+ return { name: trimmed, description: "" };
2917
+ }
2918
+ return void 0;
2759
2919
  }
2760
- function getNodeName(node) {
2761
- if (ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node))
2762
- return node.name?.text ?? "<anonymous>";
2763
- if (ts.isMethodDeclaration(node) || ts.isMethodSignature(node))
2764
- return getIdentifierText(node.name);
2765
- if (ts.isArrowFunction(node)) return getArrowFunctionName(node);
2766
- if (ts.isGetAccessor(node) || ts.isSetAccessor(node)) {
2767
- const prefix2 = ts.isGetAccessor(node) ? "get " : "set ";
2768
- return `${prefix2}${getIdentifierText(node.name)}`;
2920
+ function parseCommands(helpText) {
2921
+ const commands = [];
2922
+ let inCommandSection = false;
2923
+ for (const line of helpText.split("\n")) {
2924
+ const trimmed = line.trim();
2925
+ if (COMMAND_SECTION_RE.test(trimmed)) {
2926
+ inCommandSection = true;
2927
+ continue;
2928
+ }
2929
+ if (inCommandSection && trimmed && !line.startsWith(" ") && !line.startsWith(" ")) {
2930
+ inCommandSection = false;
2931
+ continue;
2932
+ }
2933
+ if (!inCommandSection || !trimmed) continue;
2934
+ if (trimmed.startsWith("-") || trimmed.startsWith("=")) continue;
2935
+ const parsed = parseCommandLine(trimmed);
2936
+ if (parsed) commands.push(parsed);
2769
2937
  }
2770
- if (ts.isConstructorDeclaration(node)) return "constructor";
2771
- return "<unknown>";
2938
+ return commands;
2772
2939
  }
2773
- function hasFunctionBody(node) {
2774
- if (!FUNCTION_TYPE_CHECKS.some((check2) => check2(node))) return false;
2775
- return node.body !== void 0;
2940
+ var COMMAND_SECTION_MULTILINE_RE = new RegExp(
2941
+ COMMAND_SECTION_RE.source,
2942
+ "im"
2943
+ );
2944
+ function hasSubcommands(helpText) {
2945
+ return COMMAND_SECTION_MULTILINE_RE.test(helpText);
2776
2946
  }
2777
2947
 
2778
- // src/commands/complexity/shared/calculateCyclomaticComplexity.ts
2779
- import ts2 from "typescript";
2780
- var complexityKinds = /* @__PURE__ */ new Set([
2781
- ts2.SyntaxKind.IfStatement,
2782
- ts2.SyntaxKind.ForStatement,
2783
- ts2.SyntaxKind.ForInStatement,
2784
- ts2.SyntaxKind.ForOfStatement,
2785
- ts2.SyntaxKind.WhileStatement,
2786
- ts2.SyntaxKind.DoStatement,
2787
- ts2.SyntaxKind.CaseClause,
2788
- ts2.SyntaxKind.CatchClause,
2789
- ts2.SyntaxKind.ConditionalExpression
2790
- ]);
2791
- var logicalOperators = /* @__PURE__ */ new Set([
2792
- ts2.SyntaxKind.AmpersandAmpersandToken,
2793
- ts2.SyntaxKind.BarBarToken,
2794
- ts2.SyntaxKind.QuestionQuestionToken
2795
- ]);
2796
- function calculateCyclomaticComplexity(node) {
2797
- let complexity = 1;
2798
- function visit(n) {
2799
- if (complexityKinds.has(n.kind)) {
2800
- complexity++;
2801
- } else if (ts2.isBinaryExpression(n) && logicalOperators.has(n.operatorToken.kind)) {
2802
- complexity++;
2803
- }
2804
- ts2.forEachChild(n, visit);
2805
- }
2806
- ts2.forEachChild(node, visit);
2807
- return complexity;
2808
- }
2809
-
2810
- // src/commands/complexity/shared/calculateHalstead/index.ts
2811
- import ts4 from "typescript";
2812
-
2813
- // src/commands/complexity/shared/calculateHalstead/operatorChecks.ts
2814
- import ts3 from "typescript";
2815
- var operatorChecks = [
2816
- (n) => ts3.isBinaryExpression(n) ? n.operatorToken.getText() : void 0,
2817
- (n) => ts3.isPrefixUnaryExpression(n) || ts3.isPostfixUnaryExpression(n) ? ts3.tokenToString(n.operator) ?? "" : void 0,
2818
- (n) => ts3.isCallExpression(n) ? "()" : void 0,
2819
- (n) => ts3.isPropertyAccessExpression(n) ? "." : void 0,
2820
- (n) => ts3.isElementAccessExpression(n) ? "[]" : void 0,
2821
- (n) => ts3.isConditionalExpression(n) ? "?:" : void 0,
2822
- (n) => ts3.isReturnStatement(n) ? "return" : void 0,
2823
- (n) => ts3.isIfStatement(n) ? "if" : void 0,
2824
- (n) => ts3.isForStatement(n) || ts3.isForInStatement(n) || ts3.isForOfStatement(n) ? "for" : void 0,
2825
- (n) => ts3.isWhileStatement(n) ? "while" : void 0,
2826
- (n) => ts3.isDoStatement(n) ? "do" : void 0,
2827
- (n) => ts3.isSwitchStatement(n) ? "switch" : void 0,
2828
- (n) => ts3.isCaseClause(n) ? "case" : void 0,
2829
- (n) => ts3.isDefaultClause(n) ? "default" : void 0,
2830
- (n) => ts3.isBreakStatement(n) ? "break" : void 0,
2831
- (n) => ts3.isContinueStatement(n) ? "continue" : void 0,
2832
- (n) => ts3.isThrowStatement(n) ? "throw" : void 0,
2833
- (n) => ts3.isTryStatement(n) ? "try" : void 0,
2834
- (n) => ts3.isCatchClause(n) ? "catch" : void 0,
2835
- (n) => ts3.isNewExpression(n) ? "new" : void 0,
2836
- (n) => ts3.isTypeOfExpression(n) ? "typeof" : void 0,
2837
- (n) => ts3.isAwaitExpression(n) ? "await" : void 0
2838
- ];
2839
-
2840
- // src/commands/complexity/shared/calculateHalstead/index.ts
2841
- function classifyNode(n, operators, operands) {
2842
- if (ts4.isIdentifier(n) || ts4.isNumericLiteral(n) || ts4.isStringLiteral(n)) {
2843
- operands.set(n.text, (operands.get(n.text) ?? 0) + 1);
2844
- return;
2845
- }
2846
- for (const check2 of operatorChecks) {
2847
- const op = check2(n);
2848
- if (op !== void 0) {
2849
- operators.set(op, (operators.get(op) ?? 0) + 1);
2850
- return;
2851
- }
2852
- }
2853
- }
2854
- function computeHalsteadMetrics(operators, operands) {
2855
- const n1 = operators.size;
2856
- const n2 = operands.size;
2857
- const N1 = Array.from(operators.values()).reduce((a, b) => a + b, 0);
2858
- const N2 = Array.from(operands.values()).reduce((a, b) => a + b, 0);
2859
- const vocabulary = n1 + n2;
2860
- const length = N1 + N2;
2861
- const volume = length > 0 && vocabulary > 0 ? length * Math.log2(vocabulary) : 0;
2862
- const difficulty = n2 > 0 ? n1 / 2 * (N2 / n2) : 0;
2863
- const effort = volume * difficulty;
2864
- return {
2865
- length,
2866
- vocabulary,
2867
- volume,
2868
- difficulty,
2869
- effort,
2870
- time: effort / 18,
2871
- bugsDelivered: volume / 3e3
2872
- };
2873
- }
2874
- function calculateHalstead(node) {
2875
- const operators = /* @__PURE__ */ new Map();
2876
- const operands = /* @__PURE__ */ new Map();
2877
- function visit(n) {
2878
- classifyNode(n, operators, operands);
2879
- ts4.forEachChild(n, visit);
2880
- }
2881
- ts4.forEachChild(node, visit);
2882
- return computeHalsteadMetrics(operators, operands);
2883
- }
2884
-
2885
- // src/commands/complexity/shared/countSloc.ts
2886
- function countSloc(content) {
2887
- let inMultiLineComment = false;
2888
- let count = 0;
2889
- for (const line of content.split("\n")) {
2890
- const trimmed = line.trim();
2891
- if (inMultiLineComment) {
2892
- if (trimmed.includes("*/")) {
2893
- inMultiLineComment = false;
2894
- const afterComment = trimmed.substring(trimmed.indexOf("*/") + 2);
2895
- if (afterComment.trim().length > 0) {
2896
- count++;
2897
- }
2898
- }
2899
- continue;
2900
- }
2901
- if (trimmed.startsWith("//")) {
2902
- continue;
2903
- }
2904
- if (trimmed.startsWith("/*")) {
2905
- if (trimmed.includes("*/")) {
2906
- const afterComment = trimmed.substring(trimmed.indexOf("*/") + 2);
2907
- if (afterComment.trim().length > 0) {
2908
- count++;
2909
- }
2910
- } else {
2911
- inMultiLineComment = true;
2948
+ // src/commands/permitCliReads/runHelp.ts
2949
+ import { exec as exec2 } from "child_process";
2950
+ function runHelp(args) {
2951
+ return new Promise((resolve6) => {
2952
+ exec2(
2953
+ `${args.join(" ")} --help`,
2954
+ { encoding: "utf-8", timeout: 3e4 },
2955
+ (_err, stdout, stderr) => {
2956
+ resolve6(stdout || stderr || "");
2912
2957
  }
2913
- continue;
2914
- }
2915
- if (trimmed.length > 0) {
2916
- count++;
2917
- }
2918
- }
2919
- return count;
2958
+ );
2959
+ });
2920
2960
  }
2921
2961
 
2922
- // src/commands/complexity/shared/index.ts
2923
- function createSourceFromFile(filePath) {
2924
- const content = fs11.readFileSync(filePath, "utf-8");
2925
- return ts5.createSourceFile(
2926
- path16.basename(filePath),
2927
- content,
2928
- ts5.ScriptTarget.Latest,
2929
- true,
2930
- filePath.endsWith(".tsx") ? ts5.ScriptKind.TSX : ts5.ScriptKind.TS
2931
- );
2932
- }
2933
- function withSourceFiles(pattern2, callback) {
2934
- const files = findSourceFiles2(pattern2);
2935
- if (files.length === 0) {
2936
- console.log(chalk30.yellow("No files found matching pattern"));
2937
- return void 0;
2938
- }
2939
- return callback(files);
2962
+ // src/commands/permitCliReads/discoverAll.ts
2963
+ var SAFETY_DEPTH = 10;
2964
+ var CONCURRENCY = 8;
2965
+ var interactive = !isClaudeCode();
2966
+ function showProgress(p, label2) {
2967
+ if (!interactive) return;
2968
+ const pct = Math.round(p.done / p.total * 100);
2969
+ process.stderr.write(`\r\x1B[K[${pct}%] Scanning ${label2}...`);
2940
2970
  }
2941
- function forEachFunction(files, callback) {
2942
- for (const file of files) {
2943
- const sourceFile = createSourceFromFile(file);
2944
- const visit = (node) => {
2945
- if (hasFunctionBody(node)) {
2946
- callback(file, getNodeName(node), node);
2947
- }
2948
- ts5.forEachChild(node, visit);
2949
- };
2950
- visit(sourceFile);
2971
+ async function resolveCommand(cli, path31, description, depth, p) {
2972
+ showProgress(p, path31.join(" "));
2973
+ const subHelp = await runHelp([cli, ...path31]);
2974
+ if (!subHelp || !hasSubcommands(subHelp)) {
2975
+ return [{ path: path31, description }];
2951
2976
  }
2977
+ const children = await discoverAt(cli, path31, depth + 1, p);
2978
+ return children.length > 0 ? children : [{ path: path31, description }];
2952
2979
  }
2953
-
2954
- // src/commands/complexity/cyclomatic.ts
2955
- async function cyclomatic(pattern2 = "**/*.ts", options2 = {}) {
2956
- withSourceFiles(pattern2, (files) => {
2957
- const results = [];
2958
- let hasViolation = false;
2959
- forEachFunction(files, (file, name, node) => {
2960
- const complexity = calculateCyclomaticComplexity(node);
2961
- results.push({ file, name, complexity });
2962
- if (options2.threshold !== void 0 && complexity > options2.threshold) {
2963
- hasViolation = true;
2964
- }
2965
- });
2966
- results.sort((a, b) => b.complexity - a.complexity);
2967
- for (const { file, name, complexity } of results) {
2968
- const exceedsThreshold = options2.threshold !== void 0 && complexity > options2.threshold;
2969
- const color = exceedsThreshold ? chalk31.red : chalk31.white;
2970
- console.log(`${color(`${file}:${name}`)} \u2192 ${chalk31.cyan(complexity)}`);
2971
- }
2972
- console.log(
2973
- chalk31.dim(
2974
- `
2975
- Analyzed ${results.length} functions across ${files.length} files`
2976
- )
2977
- );
2978
- if (hasViolation) {
2979
- process.exit(1);
2980
- }
2981
- });
2980
+ async function discoverAt(cli, parentPath, depth, p) {
2981
+ if (depth > SAFETY_DEPTH) return [];
2982
+ const helpText = await runHelp([cli, ...parentPath]);
2983
+ if (!helpText) return [];
2984
+ const cmds = parseCommands(helpText);
2985
+ const results = await mapAsync(
2986
+ cmds,
2987
+ CONCURRENCY,
2988
+ (cmd) => resolveCommand(cli, [...parentPath, cmd.name], cmd.description, depth, p)
2989
+ );
2990
+ return results.flat();
2982
2991
  }
2983
-
2984
- // src/commands/complexity/halstead.ts
2985
- import chalk32 from "chalk";
2986
- async function halstead(pattern2 = "**/*.ts", options2 = {}) {
2987
- withSourceFiles(pattern2, (files) => {
2988
- const results = [];
2989
- let hasViolation = false;
2990
- forEachFunction(files, (file, name, node) => {
2991
- const metrics = calculateHalstead(node);
2992
- results.push({ file, name, metrics });
2993
- if (options2.threshold !== void 0 && metrics.volume > options2.threshold) {
2994
- hasViolation = true;
2995
- }
2996
- });
2997
- results.sort((a, b) => b.metrics.effort - a.metrics.effort);
2998
- for (const { file, name, metrics } of results) {
2999
- const exceedsThreshold = options2.threshold !== void 0 && metrics.volume > options2.threshold;
3000
- const color = exceedsThreshold ? chalk32.red : chalk32.white;
3001
- console.log(
3002
- `${color(`${file}:${name}`)} \u2192 volume: ${chalk32.cyan(metrics.volume.toFixed(1))}, difficulty: ${chalk32.yellow(metrics.difficulty.toFixed(1))}, effort: ${chalk32.magenta(metrics.effort.toFixed(1))}`
3003
- );
3004
- }
3005
- console.log(
3006
- chalk32.dim(
3007
- `
3008
- Analyzed ${results.length} functions across ${files.length} files`
3009
- )
2992
+ async function discoverAll(cli) {
2993
+ const topLevel = parseCommands(await runHelp([cli]));
2994
+ const p = { done: 0, total: topLevel.length };
2995
+ const results = await mapAsync(topLevel, CONCURRENCY, async (cmd) => {
2996
+ showProgress(p, cmd.name);
2997
+ const resolved = await resolveCommand(
2998
+ cli,
2999
+ [cmd.name],
3000
+ cmd.description,
3001
+ 1,
3002
+ p
3010
3003
  );
3011
- if (hasViolation) {
3012
- process.exit(1);
3013
- }
3004
+ p.done++;
3005
+ showProgress(p, cmd.name);
3006
+ return resolved;
3014
3007
  });
3008
+ if (interactive) process.stderr.write("\r\x1B[K");
3009
+ return results.flat();
3015
3010
  }
3016
3011
 
3017
- // src/commands/complexity/maintainability/index.ts
3018
- import fs12 from "fs";
3019
-
3020
- // src/commands/complexity/maintainability/displayMaintainabilityResults.ts
3021
- import chalk33 from "chalk";
3022
- function displayMaintainabilityResults(results, threshold) {
3023
- const filtered = threshold !== void 0 ? results.filter((r) => r.minMaintainability < threshold) : results;
3024
- if (threshold !== void 0 && filtered.length === 0) {
3025
- console.log(chalk33.green("All files pass maintainability threshold"));
3026
- } else {
3027
- for (const { file, avgMaintainability, minMaintainability } of filtered) {
3028
- const color = threshold !== void 0 ? chalk33.red : chalk33.white;
3029
- console.log(
3030
- `${color(file)} \u2192 avg: ${chalk33.cyan(avgMaintainability.toFixed(1))}, min: ${chalk33.yellow(minMaintainability.toFixed(1))}`
3031
- );
3032
- }
3033
- }
3034
- console.log(chalk33.dim(`
3035
- Analyzed ${results.length} files`));
3036
- if (filtered.length > 0 && threshold !== void 0) {
3037
- console.error(
3038
- chalk33.red(
3039
- `
3040
- Fail: ${filtered.length} file(s) below threshold ${threshold}. Maintainability index (0\u2013100) is derived from Halstead volume, cyclomatic complexity, and lines of code.
3041
-
3042
- \u26A0\uFE0F ${chalk33.bold("Diagnose and fix one file at a time")} \u2014 do not investigate or fix multiple files in parallel. Run 'assist complexity <file>' to see all metrics. For larger files, start by extracting responsibilities into smaller files.`
3043
- )
3044
- );
3045
- process.exit(1);
3012
+ // src/commands/permitCliReads/classifyVerb.ts
3013
+ var READ_VERBS = /* @__PURE__ */ new Set([
3014
+ "list",
3015
+ "show",
3016
+ "view",
3017
+ "export",
3018
+ "get",
3019
+ "diff",
3020
+ "status",
3021
+ "search",
3022
+ "checks",
3023
+ "describe",
3024
+ "inspect",
3025
+ "logs",
3026
+ "cat",
3027
+ "top",
3028
+ "explain",
3029
+ "exists",
3030
+ "browse",
3031
+ "watch"
3032
+ ]);
3033
+ var WRITE_VERBS = /* @__PURE__ */ new Set([
3034
+ "create",
3035
+ "delete",
3036
+ "import",
3037
+ "set",
3038
+ "update",
3039
+ "merge",
3040
+ "close",
3041
+ "reopen",
3042
+ "edit",
3043
+ "apply",
3044
+ "patch",
3045
+ "drain",
3046
+ "cordon",
3047
+ "taint",
3048
+ "push",
3049
+ "deploy",
3050
+ "add",
3051
+ "remove",
3052
+ "assign",
3053
+ "unassign",
3054
+ "lock",
3055
+ "unlock",
3056
+ "start",
3057
+ "stop",
3058
+ "restart",
3059
+ "enable",
3060
+ "disable",
3061
+ "revoke",
3062
+ "rotate"
3063
+ ]);
3064
+ function classifyVerb(verbOrPath) {
3065
+ const segments = Array.isArray(verbOrPath) ? verbOrPath : [verbOrPath];
3066
+ let hasRead = false;
3067
+ for (const s of segments) {
3068
+ if (WRITE_VERBS.has(s)) return "w";
3069
+ if (READ_VERBS.has(s)) hasRead = true;
3046
3070
  }
3071
+ return hasRead ? "r" : "?";
3047
3072
  }
3048
3073
 
3049
- // src/commands/complexity/maintainability/index.ts
3050
- function calculateMaintainabilityIndex(halsteadVolume, cyclomaticComplexity, sloc2) {
3051
- if (halsteadVolume === 0 || sloc2 === 0) {
3052
- return 100;
3053
- }
3054
- const mi = 171 - 5.2 * Math.log(halsteadVolume) - 0.23 * cyclomaticComplexity - 16.2 * Math.log(sloc2);
3055
- return Math.max(0, Math.min(100, mi));
3056
- }
3057
- function collectFileMetrics(files) {
3058
- const fileMetrics = /* @__PURE__ */ new Map();
3059
- for (const file of files) {
3060
- const content = fs12.readFileSync(file, "utf-8");
3061
- fileMetrics.set(file, { sloc: countSloc(content), functions: [] });
3062
- }
3063
- forEachFunction(files, (file, _name, node) => {
3064
- const metrics = fileMetrics.get(file);
3065
- if (metrics) {
3066
- const complexity = calculateCyclomaticComplexity(node);
3067
- const halstead2 = calculateHalstead(node);
3068
- const mi = calculateMaintainabilityIndex(
3069
- halstead2.volume,
3070
- complexity,
3071
- metrics.sloc
3072
- );
3073
- metrics.functions.push(mi);
3074
- }
3075
- });
3076
- return fileMetrics;
3074
+ // src/commands/permitCliReads/formatHuman.ts
3075
+ function prefix(kind) {
3076
+ if (kind === "r") return " R ";
3077
+ if (kind === "w") return " W ";
3078
+ return " ? ";
3077
3079
  }
3078
- function aggregateResults(fileMetrics) {
3079
- const results = [];
3080
- for (const [file, metrics] of fileMetrics) {
3081
- if (metrics.functions.length === 0) continue;
3082
- const avgMaintainability = metrics.functions.reduce((a, b) => a + b, 0) / metrics.functions.length;
3083
- const minMaintainability = Math.min(...metrics.functions);
3084
- results.push({ file, avgMaintainability, minMaintainability });
3080
+ function formatHuman(cli, commands) {
3081
+ const sorted = [...commands].sort(
3082
+ (a, b) => a.path.join(" ").localeCompare(b.path.join(" "))
3083
+ );
3084
+ const lines = [`Discovered ${commands.length} commands for "${cli}":
3085
+ `];
3086
+ for (const cmd of sorted) {
3087
+ const full = `${cli} ${cmd.path.join(" ")}`;
3088
+ const text = cmd.description ? `${full} \u2014 ${cmd.description}` : full;
3089
+ lines.push(`${prefix(classifyVerb(cmd.path))}${text}`);
3085
3090
  }
3086
- results.sort((a, b) => a.minMaintainability - b.minMaintainability);
3087
- return results;
3088
- }
3089
- async function maintainability(pattern2 = "**/*.ts", options2 = {}) {
3090
- withSourceFiles(pattern2, (files) => {
3091
- const fileMetrics = collectFileMetrics(files);
3092
- const results = aggregateResults(fileMetrics);
3093
- displayMaintainabilityResults(results, options2.threshold);
3094
- });
3091
+ return lines.join("\n");
3095
3092
  }
3096
3093
 
3097
- // src/commands/complexity/sloc.ts
3098
- import fs13 from "fs";
3099
- import chalk34 from "chalk";
3100
- async function sloc(pattern2 = "**/*.ts", options2 = {}) {
3101
- withSourceFiles(pattern2, (files) => {
3102
- const results = [];
3103
- let hasViolation = false;
3104
- for (const file of files) {
3105
- const content = fs13.readFileSync(file, "utf-8");
3106
- const lines = countSloc(content);
3107
- results.push({ file, lines });
3108
- if (options2.threshold !== void 0 && lines > options2.threshold) {
3109
- hasViolation = true;
3110
- }
3111
- }
3112
- results.sort((a, b) => b.lines - a.lines);
3113
- for (const { file, lines } of results) {
3114
- const exceedsThreshold = options2.threshold !== void 0 && lines > options2.threshold;
3115
- const color = exceedsThreshold ? chalk34.red : chalk34.white;
3116
- console.log(`${color(file)} \u2192 ${chalk34.cyan(lines)} lines`);
3117
- }
3118
- const total = results.reduce((sum, r) => sum + r.lines, 0);
3119
- console.log(
3120
- chalk34.dim(`
3121
- Total: ${total} lines across ${files.length} files`)
3122
- );
3123
- if (hasViolation) {
3124
- process.exit(1);
3125
- }
3126
- });
3094
+ // src/commands/permitCliReads/parseCached.ts
3095
+ function parseCached(cli, cached2) {
3096
+ const prefix2 = `${cli} `;
3097
+ const commands = [];
3098
+ for (const line of cached2.split("\n")) {
3099
+ const trimmed = line.replace(/^ [RW?] {2}/, "").trim();
3100
+ if (!trimmed.startsWith(prefix2)) continue;
3101
+ const rest = trimmed.slice(prefix2.length);
3102
+ const dashIdx = rest.indexOf(" \u2014 ");
3103
+ const pathStr = dashIdx >= 0 ? rest.slice(0, dashIdx) : rest;
3104
+ const description = dashIdx >= 0 ? rest.slice(dashIdx + 3) : "";
3105
+ commands.push({ path: pathStr.split(" "), description });
3106
+ }
3107
+ return commands;
3127
3108
  }
3128
3109
 
3129
- // src/commands/complexity/analyze.ts
3130
- async function analyze(pattern2) {
3131
- const searchPattern = pattern2.includes("*") || pattern2.includes("/") ? pattern2 : `**/${pattern2}`;
3132
- const files = findSourceFiles2(searchPattern);
3133
- if (files.length === 0) {
3134
- console.log(chalk35.yellow("No files found matching pattern"));
3135
- return;
3136
- }
3137
- if (files.length === 1) {
3138
- const file = files[0];
3139
- console.log(chalk35.bold.underline("SLOC"));
3140
- await sloc(file);
3141
- console.log();
3142
- console.log(chalk35.bold.underline("Cyclomatic Complexity"));
3143
- await cyclomatic(file);
3144
- console.log();
3145
- console.log(chalk35.bold.underline("Halstead Metrics"));
3146
- await halstead(file);
3147
- console.log();
3148
- console.log(chalk35.bold.underline("Maintainability Index"));
3149
- await maintainability(file);
3110
+ // src/commands/permitCliReads/updateSettings.ts
3111
+ function updateSettings(cli, commands) {
3112
+ const existing = loadCliReads();
3113
+ const readEntries = commands.filter((cmd) => classifyVerb(cmd.path) === "r").map((cmd) => `${cli} ${cmd.path.join(" ")}`);
3114
+ const merged = [.../* @__PURE__ */ new Set([...existing, ...readEntries])].sort();
3115
+ if (merged.length === existing.length && merged.every((e, i) => e === existing[i]))
3150
3116
  return;
3151
- }
3152
- await maintainability(searchPattern);
3117
+ saveCliReads(merged);
3153
3118
  }
3154
3119
 
3155
- // src/commands/registerComplexity.ts
3156
- function registerComplexity(program2) {
3157
- const complexityCommand = program2.command("complexity").description("Analyze TypeScript code complexity metrics").argument("[pattern]").action((pattern2) => {
3158
- if (!pattern2) {
3159
- complexityCommand.help();
3160
- return;
3161
- }
3162
- return analyze(pattern2);
3163
- });
3164
- complexityCommand.command("cyclomatic [pattern]").description("Calculate cyclomatic complexity per function").option("--threshold <number>", "Max complexity threshold", Number.parseInt).action(cyclomatic);
3165
- complexityCommand.command("halstead [pattern]").description("Calculate Halstead metrics per function").option("--threshold <number>", "Max volume threshold", Number.parseInt).action(halstead);
3166
- complexityCommand.command("maintainability [pattern]").description("Calculate maintainability index per file").option(
3167
- "--threshold <number>",
3168
- "Min maintainability threshold",
3169
- Number.parseInt
3170
- ).action(maintainability);
3171
- complexityCommand.command("sloc [pattern]").description("Count source lines of code per file").option("--threshold <number>", "Max lines threshold", Number.parseInt).action(sloc);
3120
+ // src/commands/permitCliReads/index.ts
3121
+ function logPath(cli) {
3122
+ const safeName = cli.replace(/\s+/g, "-");
3123
+ return join12(homedir4(), ".assist", `cli-discover-${safeName}.log`);
3172
3124
  }
3173
-
3174
- // src/commands/deploy/redirect.ts
3175
- import { existsSync as existsSync17, readFileSync as readFileSync14, writeFileSync as writeFileSync13 } from "fs";
3176
- import chalk36 from "chalk";
3177
- var TRAILING_SLASH_SCRIPT = ` <script>
3178
- if (!window.location.pathname.endsWith('/')) {
3179
- window.location.href = \`\${window.location.pathname}/\${window.location.search}\${window.location.hash}\`;
3180
- }
3181
- </script>`;
3182
- function redirect() {
3183
- const indexPath = "index.html";
3184
- if (!existsSync17(indexPath)) {
3185
- console.log(chalk36.yellow("No index.html found"));
3186
- return;
3125
+ function readCache(cli) {
3126
+ const path31 = logPath(cli);
3127
+ if (!existsSync18(path31)) return void 0;
3128
+ return readFileSync15(path31, "utf-8");
3129
+ }
3130
+ function writeCache(cli, output) {
3131
+ const dir = join12(homedir4(), ".assist");
3132
+ mkdirSync4(dir, { recursive: true });
3133
+ writeFileSync13(logPath(cli), output);
3134
+ }
3135
+ async function permitCliReads(cli, options2 = { noCache: false }) {
3136
+ if (!cli) {
3137
+ console.error("Usage: assist cli-hook add <cli>");
3138
+ process.exit(1);
3187
3139
  }
3188
- const content = readFileSync14(indexPath, "utf-8");
3189
- if (content.includes("window.location.pathname.endsWith('/')")) {
3190
- console.log(chalk36.dim("Trailing slash script already present"));
3191
- return;
3140
+ const installDir = getInstallDir();
3141
+ if (!isGitRepo(installDir)) {
3142
+ console.error(
3143
+ "cli-hook add must be run from the assist git repo, not a global install."
3144
+ );
3145
+ process.exit(1);
3192
3146
  }
3193
- const headCloseIndex = content.indexOf("</head>");
3194
- if (headCloseIndex === -1) {
3195
- console.log(chalk36.red("Could not find </head> tag in index.html"));
3196
- return;
3147
+ if (!options2.noCache) {
3148
+ const cached2 = readCache(cli);
3149
+ if (cached2) {
3150
+ console.log(colorize(cached2));
3151
+ updateSettings(cli, parseCached(cli, cached2));
3152
+ return;
3153
+ }
3197
3154
  }
3198
- const newContent = content.slice(0, headCloseIndex) + TRAILING_SLASH_SCRIPT + "\n " + content.slice(headCloseIndex);
3199
- writeFileSync13(indexPath, newContent);
3200
- console.log(chalk36.green("Added trailing slash redirect to index.html"));
3155
+ assertCliExists(cli);
3156
+ const commands = await discoverAll(cli);
3157
+ const output = formatHuman(cli, commands);
3158
+ console.log(colorize(output));
3159
+ writeCache(cli, output);
3160
+ updateSettings(cli, commands);
3201
3161
  }
3202
3162
 
3203
- // src/commands/registerDeploy.ts
3204
- function registerDeploy(program2) {
3205
- const deployCommand = program2.command("deploy").description("Netlify deployment utilities");
3206
- deployCommand.command("init").description("Initialize Netlify project and configure deployment").action(init5);
3207
- deployCommand.command("redirect").description("Add trailing slash redirect script to index.html").action(redirect);
3163
+ // src/commands/registerCliHook.ts
3164
+ function registerCliHook(program2) {
3165
+ const cmd = program2.command("cli-hook").description("PreToolUse hook for auto-approving read-only CLI commands").action(() => {
3166
+ cliHook();
3167
+ });
3168
+ cmd.command("check <command>").description("Check whether a command would be auto-approved").action((command) => {
3169
+ cliHookCheck(command);
3170
+ });
3171
+ cmd.command("add").description("Discover a CLI's commands and auto-permit read-only ones").argument(
3172
+ "<cli...>",
3173
+ "CLI binary and optional subcommand (e.g. gh, az, acli jira)"
3174
+ ).option("--no-cache", "Force fresh discovery, ignoring cached results").action((cli, options2) => {
3175
+ permitCliReads(cli.join(" "), { noCache: !options2.cache });
3176
+ });
3208
3177
  }
3209
3178
 
3210
- // src/commands/devlog/list/index.ts
3211
- import { execSync as execSync14 } from "child_process";
3212
- import { basename as basename3 } from "path";
3179
+ // src/commands/complexity/analyze.ts
3180
+ import chalk36 from "chalk";
3213
3181
 
3214
- // src/commands/devlog/shared.ts
3215
- import { execSync as execSync13 } from "child_process";
3216
- import chalk37 from "chalk";
3182
+ // src/commands/complexity/cyclomatic.ts
3183
+ import chalk32 from "chalk";
3217
3184
 
3218
- // src/commands/devlog/loadDevlogEntries.ts
3219
- import { readdirSync, readFileSync as readFileSync15 } from "fs";
3220
- import { homedir as homedir3 } from "os";
3221
- import { join as join11 } from "path";
3222
- var DEVLOG_DIR = join11(homedir3(), "git/blog/src/content/devlog");
3223
- function loadDevlogEntries(repoName) {
3224
- const entries = /* @__PURE__ */ new Map();
3225
- try {
3226
- const files = readdirSync(DEVLOG_DIR).filter((f) => f.endsWith(".md"));
3227
- for (const file of files) {
3228
- const content = readFileSync15(join11(DEVLOG_DIR, file), "utf-8");
3229
- const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
3230
- if (frontmatterMatch) {
3231
- const frontmatter = frontmatterMatch[1];
3232
- const dateMatch = frontmatter.match(/date:\s*"?(\d{4}-\d{2}-\d{2})"?/);
3233
- const versionMatch = frontmatter.match(/version:\s*(.+)/);
3234
- const titleMatch = frontmatter.match(/title:\s*(.+)/);
3235
- const tagsMatch = frontmatter.match(/tags:\s*\[([^\]]*)\]/);
3236
- if (dateMatch && versionMatch && titleMatch && tagsMatch) {
3237
- const tags = tagsMatch[1].split(",").map((t) => t.trim());
3238
- const firstTag = tags[0];
3239
- if (firstTag !== repoName) {
3240
- continue;
3241
- }
3242
- const date = dateMatch[1];
3243
- const version2 = versionMatch[1].trim();
3244
- const title = titleMatch[1].trim();
3245
- const existing = entries.get(date) || [];
3246
- existing.push({ version: version2, title, filename: file });
3247
- entries.set(date, existing);
3248
- }
3185
+ // src/commands/complexity/shared/index.ts
3186
+ import fs11 from "fs";
3187
+ import path16 from "path";
3188
+ import chalk31 from "chalk";
3189
+ import ts5 from "typescript";
3190
+
3191
+ // src/commands/complexity/findSourceFiles.ts
3192
+ import fs10 from "fs";
3193
+ import path15 from "path";
3194
+ import { minimatch as minimatch3 } from "minimatch";
3195
+ function applyIgnoreGlobs(files) {
3196
+ const { complexity } = loadConfig();
3197
+ return files.filter(
3198
+ (f) => !complexity.ignore.some((glob) => minimatch3(f, glob))
3199
+ );
3200
+ }
3201
+ function walk(dir, results) {
3202
+ if (!fs10.existsSync(dir)) {
3203
+ return;
3204
+ }
3205
+ const extensions = [".ts", ".tsx"];
3206
+ const entries = fs10.readdirSync(dir, { withFileTypes: true });
3207
+ for (const entry of entries) {
3208
+ const fullPath = path15.join(dir, entry.name);
3209
+ if (entry.isDirectory()) {
3210
+ if (entry.name !== "node_modules" && entry.name !== ".git") {
3211
+ walk(fullPath, results);
3249
3212
  }
3213
+ } else if (entry.isFile() && extensions.some((ext) => entry.name.endsWith(ext))) {
3214
+ results.push(fullPath);
3250
3215
  }
3251
- } catch {
3252
3216
  }
3253
- return entries;
3254
3217
  }
3255
-
3256
- // src/commands/devlog/shared.ts
3257
- function getCommitFiles(hash) {
3258
- try {
3259
- const output = execSync13(`git show --name-only --format="" ${hash}`, {
3260
- encoding: "utf-8"
3261
- });
3262
- return output.trim().split("\n").filter(Boolean);
3263
- } catch {
3264
- return [];
3218
+ function findSourceFiles2(pattern2, baseDir = ".") {
3219
+ const results = [];
3220
+ if (pattern2.includes("*")) {
3221
+ walk(baseDir, results);
3222
+ return applyIgnoreGlobs(results.filter((f) => minimatch3(f, pattern2)));
3223
+ }
3224
+ if (fs10.existsSync(pattern2) && fs10.statSync(pattern2).isFile()) {
3225
+ return [pattern2];
3226
+ }
3227
+ if (fs10.existsSync(pattern2) && fs10.statSync(pattern2).isDirectory()) {
3228
+ walk(pattern2, results);
3229
+ return applyIgnoreGlobs(results);
3265
3230
  }
3231
+ walk(baseDir, results);
3232
+ return applyIgnoreGlobs(results.filter((f) => minimatch3(f, pattern2)));
3266
3233
  }
3267
- function shouldIgnoreCommit(files, ignorePaths) {
3268
- if (ignorePaths.length === 0 || files.length === 0) {
3269
- return false;
3234
+
3235
+ // src/commands/complexity/shared/getNodeName.ts
3236
+ import ts from "typescript";
3237
+ var FUNCTION_TYPE_CHECKS = [
3238
+ ts.isFunctionDeclaration,
3239
+ ts.isFunctionExpression,
3240
+ ts.isArrowFunction,
3241
+ ts.isMethodDeclaration,
3242
+ ts.isGetAccessor,
3243
+ ts.isSetAccessor,
3244
+ ts.isConstructorDeclaration
3245
+ ];
3246
+ function getIdentifierText(name) {
3247
+ if (ts.isIdentifier(name) || ts.isStringLiteral(name)) return name.text;
3248
+ return "<computed>";
3249
+ }
3250
+ function getArrowFunctionName(node) {
3251
+ const { parent } = node;
3252
+ if (ts.isVariableDeclaration(parent) && ts.isIdentifier(parent.name))
3253
+ return parent.name.text;
3254
+ if (ts.isPropertyAssignment(parent) && ts.isIdentifier(parent.name))
3255
+ return parent.name.text;
3256
+ return "<arrow>";
3257
+ }
3258
+ function getNodeName(node) {
3259
+ if (ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node))
3260
+ return node.name?.text ?? "<anonymous>";
3261
+ if (ts.isMethodDeclaration(node) || ts.isMethodSignature(node))
3262
+ return getIdentifierText(node.name);
3263
+ if (ts.isArrowFunction(node)) return getArrowFunctionName(node);
3264
+ if (ts.isGetAccessor(node) || ts.isSetAccessor(node)) {
3265
+ const prefix2 = ts.isGetAccessor(node) ? "get " : "set ";
3266
+ return `${prefix2}${getIdentifierText(node.name)}`;
3270
3267
  }
3271
- return files.every(
3272
- (file) => ignorePaths.some((ignorePath) => file.startsWith(ignorePath))
3273
- );
3268
+ if (ts.isConstructorDeclaration(node)) return "constructor";
3269
+ return "<unknown>";
3274
3270
  }
3275
- function printCommitsWithFiles(commits, ignore2, verbose) {
3276
- for (const commit2 of commits) {
3277
- console.log(` ${chalk37.yellow(commit2.hash)} ${commit2.message}`);
3278
- if (verbose) {
3279
- const visibleFiles = commit2.files.filter(
3280
- (file) => !ignore2.some((p) => file.startsWith(p))
3281
- );
3282
- for (const file of visibleFiles) {
3283
- console.log(` ${chalk37.dim(file)}`);
3284
- }
3271
+ function hasFunctionBody(node) {
3272
+ if (!FUNCTION_TYPE_CHECKS.some((check2) => check2(node))) return false;
3273
+ return node.body !== void 0;
3274
+ }
3275
+
3276
+ // src/commands/complexity/shared/calculateCyclomaticComplexity.ts
3277
+ import ts2 from "typescript";
3278
+ var complexityKinds = /* @__PURE__ */ new Set([
3279
+ ts2.SyntaxKind.IfStatement,
3280
+ ts2.SyntaxKind.ForStatement,
3281
+ ts2.SyntaxKind.ForInStatement,
3282
+ ts2.SyntaxKind.ForOfStatement,
3283
+ ts2.SyntaxKind.WhileStatement,
3284
+ ts2.SyntaxKind.DoStatement,
3285
+ ts2.SyntaxKind.CaseClause,
3286
+ ts2.SyntaxKind.CatchClause,
3287
+ ts2.SyntaxKind.ConditionalExpression
3288
+ ]);
3289
+ var logicalOperators = /* @__PURE__ */ new Set([
3290
+ ts2.SyntaxKind.AmpersandAmpersandToken,
3291
+ ts2.SyntaxKind.BarBarToken,
3292
+ ts2.SyntaxKind.QuestionQuestionToken
3293
+ ]);
3294
+ function calculateCyclomaticComplexity(node) {
3295
+ let complexity = 1;
3296
+ function visit(n) {
3297
+ if (complexityKinds.has(n.kind)) {
3298
+ complexity++;
3299
+ } else if (ts2.isBinaryExpression(n) && logicalOperators.has(n.operatorToken.kind)) {
3300
+ complexity++;
3285
3301
  }
3302
+ ts2.forEachChild(n, visit);
3286
3303
  }
3304
+ ts2.forEachChild(node, visit);
3305
+ return complexity;
3287
3306
  }
3288
- function parseGitLogCommits(output, ignore2, afterDate) {
3289
- const lines = output.trim().split("\n");
3290
- const commitsByDate = /* @__PURE__ */ new Map();
3291
- for (const line of lines) {
3292
- const [date, hash, ...messageParts] = line.split("|");
3293
- const message = messageParts.join("|");
3294
- if (afterDate && date <= afterDate) {
3295
- continue;
3296
- }
3297
- const files = getCommitFiles(hash);
3298
- if (!shouldIgnoreCommit(files, ignore2)) {
3299
- const existing = commitsByDate.get(date) || [];
3300
- existing.push({ date, hash, message, files });
3301
- commitsByDate.set(date, existing);
3307
+
3308
+ // src/commands/complexity/shared/calculateHalstead/index.ts
3309
+ import ts4 from "typescript";
3310
+
3311
+ // src/commands/complexity/shared/calculateHalstead/operatorChecks.ts
3312
+ import ts3 from "typescript";
3313
+ var operatorChecks = [
3314
+ (n) => ts3.isBinaryExpression(n) ? n.operatorToken.getText() : void 0,
3315
+ (n) => ts3.isPrefixUnaryExpression(n) || ts3.isPostfixUnaryExpression(n) ? ts3.tokenToString(n.operator) ?? "" : void 0,
3316
+ (n) => ts3.isCallExpression(n) ? "()" : void 0,
3317
+ (n) => ts3.isPropertyAccessExpression(n) ? "." : void 0,
3318
+ (n) => ts3.isElementAccessExpression(n) ? "[]" : void 0,
3319
+ (n) => ts3.isConditionalExpression(n) ? "?:" : void 0,
3320
+ (n) => ts3.isReturnStatement(n) ? "return" : void 0,
3321
+ (n) => ts3.isIfStatement(n) ? "if" : void 0,
3322
+ (n) => ts3.isForStatement(n) || ts3.isForInStatement(n) || ts3.isForOfStatement(n) ? "for" : void 0,
3323
+ (n) => ts3.isWhileStatement(n) ? "while" : void 0,
3324
+ (n) => ts3.isDoStatement(n) ? "do" : void 0,
3325
+ (n) => ts3.isSwitchStatement(n) ? "switch" : void 0,
3326
+ (n) => ts3.isCaseClause(n) ? "case" : void 0,
3327
+ (n) => ts3.isDefaultClause(n) ? "default" : void 0,
3328
+ (n) => ts3.isBreakStatement(n) ? "break" : void 0,
3329
+ (n) => ts3.isContinueStatement(n) ? "continue" : void 0,
3330
+ (n) => ts3.isThrowStatement(n) ? "throw" : void 0,
3331
+ (n) => ts3.isTryStatement(n) ? "try" : void 0,
3332
+ (n) => ts3.isCatchClause(n) ? "catch" : void 0,
3333
+ (n) => ts3.isNewExpression(n) ? "new" : void 0,
3334
+ (n) => ts3.isTypeOfExpression(n) ? "typeof" : void 0,
3335
+ (n) => ts3.isAwaitExpression(n) ? "await" : void 0
3336
+ ];
3337
+
3338
+ // src/commands/complexity/shared/calculateHalstead/index.ts
3339
+ function classifyNode(n, operators, operands) {
3340
+ if (ts4.isIdentifier(n) || ts4.isNumericLiteral(n) || ts4.isStringLiteral(n)) {
3341
+ operands.set(n.text, (operands.get(n.text) ?? 0) + 1);
3342
+ return;
3343
+ }
3344
+ for (const check2 of operatorChecks) {
3345
+ const op = check2(n);
3346
+ if (op !== void 0) {
3347
+ operators.set(op, (operators.get(op) ?? 0) + 1);
3348
+ return;
3302
3349
  }
3303
3350
  }
3304
- return commitsByDate;
3305
3351
  }
3306
-
3307
- // src/commands/devlog/list/printDateHeader.ts
3308
- import chalk38 from "chalk";
3309
- function printDateHeader(date, isSkipped, entries) {
3310
- if (isSkipped) {
3311
- console.log(`${chalk38.bold.blue(date)} ${chalk38.dim("skipped")}`);
3312
- } else if (entries && entries.length > 0) {
3313
- const entryInfo = entries.map((e) => `${chalk38.green(e.version)} ${e.title}`).join(" | ");
3314
- console.log(`${chalk38.bold.blue(date)} ${entryInfo}`);
3315
- } else {
3316
- console.log(`${chalk38.bold.blue(date)} ${chalk38.red("\u26A0 devlog missing")}`);
3352
+ function computeHalsteadMetrics(operators, operands) {
3353
+ const n1 = operators.size;
3354
+ const n2 = operands.size;
3355
+ const N1 = Array.from(operators.values()).reduce((a, b) => a + b, 0);
3356
+ const N2 = Array.from(operands.values()).reduce((a, b) => a + b, 0);
3357
+ const vocabulary = n1 + n2;
3358
+ const length = N1 + N2;
3359
+ const volume = length > 0 && vocabulary > 0 ? length * Math.log2(vocabulary) : 0;
3360
+ const difficulty = n2 > 0 ? n1 / 2 * (N2 / n2) : 0;
3361
+ const effort = volume * difficulty;
3362
+ return {
3363
+ length,
3364
+ vocabulary,
3365
+ volume,
3366
+ difficulty,
3367
+ effort,
3368
+ time: effort / 18,
3369
+ bugsDelivered: volume / 3e3
3370
+ };
3371
+ }
3372
+ function calculateHalstead(node) {
3373
+ const operators = /* @__PURE__ */ new Map();
3374
+ const operands = /* @__PURE__ */ new Map();
3375
+ function visit(n) {
3376
+ classifyNode(n, operators, operands);
3377
+ ts4.forEachChild(n, visit);
3317
3378
  }
3379
+ ts4.forEachChild(node, visit);
3380
+ return computeHalsteadMetrics(operators, operands);
3318
3381
  }
3319
3382
 
3320
- // src/commands/devlog/list/index.ts
3321
- function list3(options2) {
3322
- const config = loadConfig();
3323
- const days = options2.days ?? 30;
3324
- const ignore2 = options2.ignore ?? config.devlog?.ignore ?? [];
3325
- const skipDays = new Set(config.devlog?.skip?.days ?? []);
3326
- const repoName = basename3(process.cwd());
3327
- const devlogEntries = loadDevlogEntries(repoName);
3328
- const reverseFlag = options2.reverse ? "--reverse " : "";
3329
- const limitFlag = options2.reverse ? "" : "-n 500 ";
3330
- const output = execSync14(
3331
- `git log ${reverseFlag}${limitFlag}--pretty=format:'%ad|%h|%s' --date=short`,
3332
- { encoding: "utf-8" }
3333
- );
3334
- const commitsByDate = parseGitLogCommits(output, ignore2);
3335
- let dateCount = 0;
3336
- let isFirst = true;
3337
- for (const [date, dateCommits] of commitsByDate) {
3338
- if (options2.since) {
3339
- if (date < options2.since) {
3340
- break;
3383
+ // src/commands/complexity/shared/countSloc.ts
3384
+ function countSloc(content) {
3385
+ let inMultiLineComment = false;
3386
+ let count = 0;
3387
+ for (const line of content.split("\n")) {
3388
+ const trimmed = line.trim();
3389
+ if (inMultiLineComment) {
3390
+ if (trimmed.includes("*/")) {
3391
+ inMultiLineComment = false;
3392
+ const afterComment = trimmed.substring(trimmed.indexOf("*/") + 2);
3393
+ if (afterComment.trim().length > 0) {
3394
+ count++;
3395
+ }
3341
3396
  }
3342
- } else if (dateCount >= days) {
3343
- break;
3397
+ continue;
3344
3398
  }
3345
- dateCount++;
3346
- if (!isFirst) {
3347
- console.log();
3399
+ if (trimmed.startsWith("//")) {
3400
+ continue;
3401
+ }
3402
+ if (trimmed.startsWith("/*")) {
3403
+ if (trimmed.includes("*/")) {
3404
+ const afterComment = trimmed.substring(trimmed.indexOf("*/") + 2);
3405
+ if (afterComment.trim().length > 0) {
3406
+ count++;
3407
+ }
3408
+ } else {
3409
+ inMultiLineComment = true;
3410
+ }
3411
+ continue;
3412
+ }
3413
+ if (trimmed.length > 0) {
3414
+ count++;
3348
3415
  }
3349
- isFirst = false;
3350
- printDateHeader(date, skipDays.has(date), devlogEntries.get(date));
3351
- printCommitsWithFiles(dateCommits, ignore2, options2.verbose ?? false);
3352
3416
  }
3417
+ return count;
3353
3418
  }
3354
3419
 
3355
- // src/commands/devlog/getLastVersionInfo.ts
3356
- import { execSync as execSync15 } from "child_process";
3357
- import semver from "semver";
3358
- function getVersionAtCommit(hash) {
3359
- try {
3360
- const content = execSync15(`git show ${hash}:package.json`, {
3361
- encoding: "utf-8"
3362
- });
3363
- const pkg = JSON.parse(content);
3364
- return pkg.version ?? null;
3365
- } catch {
3366
- return null;
3367
- }
3368
- }
3369
- function stripToMinor(version2) {
3370
- const parsed = semver.parse(semver.coerce(version2));
3371
- return parsed ? `v${parsed.major}.${parsed.minor}` : `v${version2}`;
3420
+ // src/commands/complexity/shared/index.ts
3421
+ function createSourceFromFile(filePath) {
3422
+ const content = fs11.readFileSync(filePath, "utf-8");
3423
+ return ts5.createSourceFile(
3424
+ path16.basename(filePath),
3425
+ content,
3426
+ ts5.ScriptTarget.Latest,
3427
+ true,
3428
+ filePath.endsWith(".tsx") ? ts5.ScriptKind.TSX : ts5.ScriptKind.TS
3429
+ );
3372
3430
  }
3373
- function getLastVersionInfoFromGit() {
3374
- try {
3375
- const output = execSync15(
3376
- "git log -1 --pretty=format:'%ad|%h' --date=short",
3377
- {
3378
- encoding: "utf-8"
3379
- }
3380
- ).trim();
3381
- const [date, hash] = output.split("|");
3382
- if (!date || !hash) return null;
3383
- const version2 = getVersionAtCommit(hash);
3384
- if (!version2) return null;
3385
- return { date, version: stripToMinor(version2) };
3386
- } catch {
3387
- return null;
3431
+ function withSourceFiles(pattern2, callback) {
3432
+ const files = findSourceFiles2(pattern2);
3433
+ if (files.length === 0) {
3434
+ console.log(chalk31.yellow("No files found matching pattern"));
3435
+ return void 0;
3388
3436
  }
3437
+ return callback(files);
3389
3438
  }
3390
- function findLastDate(entries) {
3391
- const dates = Array.from(entries.keys()).sort().reverse();
3392
- return dates[0] ?? null;
3393
- }
3394
- function getLastVersionInfo(repoName, config) {
3395
- const entries = loadDevlogEntries(repoName);
3396
- const lastDate = findLastDate(entries);
3397
- if (!lastDate) return null;
3398
- if (config?.commit?.conventional) {
3399
- const gitInfo = getLastVersionInfoFromGit();
3400
- if (gitInfo) return { date: lastDate, version: gitInfo.version };
3439
+ function forEachFunction(files, callback) {
3440
+ for (const file of files) {
3441
+ const sourceFile = createSourceFromFile(file);
3442
+ const visit = (node) => {
3443
+ if (hasFunctionBody(node)) {
3444
+ callback(file, getNodeName(node), node);
3445
+ }
3446
+ ts5.forEachChild(node, visit);
3447
+ };
3448
+ visit(sourceFile);
3401
3449
  }
3402
- const lastVersion = entries.get(lastDate)?.[0]?.version;
3403
- return lastVersion ? { date: lastDate, version: lastVersion } : null;
3404
- }
3405
- function cleanVersion(version2) {
3406
- return semver.clean(version2) ?? semver.coerce(version2)?.version ?? null;
3407
- }
3408
- function bumpVersion(version2, type) {
3409
- const cleaned = cleanVersion(version2);
3410
- if (!cleaned) return version2;
3411
- const bumped = semver.inc(cleaned, type);
3412
- if (!bumped) return version2;
3413
- if (type === "minor") return stripToMinor(bumped);
3414
- return `v${bumped}`;
3415
3450
  }
3416
3451
 
3417
- // src/commands/devlog/next/displayNextEntry/index.ts
3418
- import { execSync as execSync16 } from "child_process";
3419
- import chalk40 from "chalk";
3452
+ // src/commands/complexity/cyclomatic.ts
3453
+ async function cyclomatic(pattern2 = "**/*.ts", options2 = {}) {
3454
+ withSourceFiles(pattern2, (files) => {
3455
+ const results = [];
3456
+ let hasViolation = false;
3457
+ forEachFunction(files, (file, name, node) => {
3458
+ const complexity = calculateCyclomaticComplexity(node);
3459
+ results.push({ file, name, complexity });
3460
+ if (options2.threshold !== void 0 && complexity > options2.threshold) {
3461
+ hasViolation = true;
3462
+ }
3463
+ });
3464
+ results.sort((a, b) => b.complexity - a.complexity);
3465
+ for (const { file, name, complexity } of results) {
3466
+ const exceedsThreshold = options2.threshold !== void 0 && complexity > options2.threshold;
3467
+ const color = exceedsThreshold ? chalk32.red : chalk32.white;
3468
+ console.log(`${color(`${file}:${name}`)} \u2192 ${chalk32.cyan(complexity)}`);
3469
+ }
3470
+ console.log(
3471
+ chalk32.dim(
3472
+ `
3473
+ Analyzed ${results.length} functions across ${files.length} files`
3474
+ )
3475
+ );
3476
+ if (hasViolation) {
3477
+ process.exit(1);
3478
+ }
3479
+ });
3480
+ }
3420
3481
 
3421
- // src/commands/devlog/next/displayNextEntry/displayVersion.ts
3422
- import chalk39 from "chalk";
3423
- function displayVersion(conventional, firstHash, patchVersion, minorVersion) {
3424
- if (conventional && firstHash) {
3425
- const version2 = getVersionAtCommit(firstHash);
3426
- if (version2) {
3427
- console.log(`${chalk39.bold("version:")} ${stripToMinor(version2)}`);
3428
- } else {
3429
- console.log(`${chalk39.bold("version:")} ${chalk39.red("unknown")}`);
3482
+ // src/commands/complexity/halstead.ts
3483
+ import chalk33 from "chalk";
3484
+ async function halstead(pattern2 = "**/*.ts", options2 = {}) {
3485
+ withSourceFiles(pattern2, (files) => {
3486
+ const results = [];
3487
+ let hasViolation = false;
3488
+ forEachFunction(files, (file, name, node) => {
3489
+ const metrics = calculateHalstead(node);
3490
+ results.push({ file, name, metrics });
3491
+ if (options2.threshold !== void 0 && metrics.volume > options2.threshold) {
3492
+ hasViolation = true;
3493
+ }
3494
+ });
3495
+ results.sort((a, b) => b.metrics.effort - a.metrics.effort);
3496
+ for (const { file, name, metrics } of results) {
3497
+ const exceedsThreshold = options2.threshold !== void 0 && metrics.volume > options2.threshold;
3498
+ const color = exceedsThreshold ? chalk33.red : chalk33.white;
3499
+ console.log(
3500
+ `${color(`${file}:${name}`)} \u2192 volume: ${chalk33.cyan(metrics.volume.toFixed(1))}, difficulty: ${chalk33.yellow(metrics.difficulty.toFixed(1))}, effort: ${chalk33.magenta(metrics.effort.toFixed(1))}`
3501
+ );
3430
3502
  }
3431
- } else if (patchVersion && minorVersion) {
3432
3503
  console.log(
3433
- `${chalk39.bold("version:")} ${patchVersion} (patch) or ${minorVersion} (minor)`
3504
+ chalk33.dim(
3505
+ `
3506
+ Analyzed ${results.length} functions across ${files.length} files`
3507
+ )
3434
3508
  );
3509
+ if (hasViolation) {
3510
+ process.exit(1);
3511
+ }
3512
+ });
3513
+ }
3514
+
3515
+ // src/commands/complexity/maintainability/index.ts
3516
+ import fs12 from "fs";
3517
+
3518
+ // src/commands/complexity/maintainability/displayMaintainabilityResults.ts
3519
+ import chalk34 from "chalk";
3520
+ function displayMaintainabilityResults(results, threshold) {
3521
+ const filtered = threshold !== void 0 ? results.filter((r) => r.minMaintainability < threshold) : results;
3522
+ if (threshold !== void 0 && filtered.length === 0) {
3523
+ console.log(chalk34.green("All files pass maintainability threshold"));
3435
3524
  } else {
3436
- console.log(`${chalk39.bold("version:")} v0.1 (initial)`);
3525
+ for (const { file, avgMaintainability, minMaintainability } of filtered) {
3526
+ const color = threshold !== void 0 ? chalk34.red : chalk34.white;
3527
+ console.log(
3528
+ `${color(file)} \u2192 avg: ${chalk34.cyan(avgMaintainability.toFixed(1))}, min: ${chalk34.yellow(minMaintainability.toFixed(1))}`
3529
+ );
3530
+ }
3437
3531
  }
3438
- }
3532
+ console.log(chalk34.dim(`
3533
+ Analyzed ${results.length} files`));
3534
+ if (filtered.length > 0 && threshold !== void 0) {
3535
+ console.error(
3536
+ chalk34.red(
3537
+ `
3538
+ Fail: ${filtered.length} file(s) below threshold ${threshold}. Maintainability index (0\u2013100) is derived from Halstead volume, cyclomatic complexity, and lines of code.
3439
3539
 
3440
- // src/commands/devlog/next/displayNextEntry/index.ts
3441
- function computeVersions(lastInfo) {
3442
- if (!lastInfo) return { patch: null, minor: null };
3443
- return {
3444
- patch: bumpVersion(lastInfo.version, "patch"),
3445
- minor: bumpVersion(lastInfo.version, "minor")
3446
- };
3447
- }
3448
- function findTargetDate(commitsByDate, skipDays) {
3449
- return Array.from(commitsByDate.keys()).filter((d) => !skipDays.has(d)).sort()[0];
3450
- }
3451
- function fetchCommitsByDate(ignore2, lastDate) {
3452
- const output = execSync16(
3453
- "git log --pretty=format:'%ad|%h|%s' --date=short -n 500",
3454
- { encoding: "utf-8" }
3455
- );
3456
- return parseGitLogCommits(output, ignore2, lastDate);
3457
- }
3458
- function printVersionInfo(config, lastInfo, firstHash) {
3459
- const versions = computeVersions(lastInfo);
3460
- displayVersion(
3461
- !!config.commit?.conventional,
3462
- firstHash,
3463
- versions.patch,
3464
- versions.minor
3465
- );
3466
- }
3467
- function resolveIgnoreList(options2, config) {
3468
- return options2.ignore ?? config.devlog?.ignore ?? [];
3469
- }
3470
- function resolveSkipDays(config) {
3471
- return new Set(config.devlog?.skip?.days ?? []);
3472
- }
3473
- function getLastDate(lastInfo) {
3474
- return lastInfo?.date ?? null;
3475
- }
3476
- function getCommitsForDate(commitsByDate, date) {
3477
- return commitsByDate.get(date) ?? [];
3478
- }
3479
- function noCommitsMessage(hasLastInfo) {
3480
- return hasLastInfo ? "No commits after last versioned entry" : "No commits found";
3481
- }
3482
- function logName(repoName) {
3483
- console.log(`${chalk40.bold("name:")} ${repoName}`);
3484
- }
3485
- function displayNextEntry(ctx, targetDate, commits) {
3486
- logName(ctx.repoName);
3487
- printVersionInfo(ctx.config, ctx.lastInfo, commits[0]?.hash);
3488
- console.log(chalk40.bold.blue(targetDate));
3489
- printCommitsWithFiles(commits, ctx.ignore, ctx.verbose);
3490
- }
3491
- function logNoCommits(lastInfo) {
3492
- console.log(chalk40.dim(noCommitsMessage(!!lastInfo)));
3540
+ \u26A0\uFE0F ${chalk34.bold("Diagnose and fix one file at a time")} \u2014 do not investigate or fix multiple files in parallel. Run 'assist complexity <file>' to see all metrics. For larger files, start by extracting responsibilities into smaller files.`
3541
+ )
3542
+ );
3543
+ process.exit(1);
3544
+ }
3493
3545
  }
3494
3546
 
3495
- // src/commands/devlog/next/index.ts
3496
- function resolveContextData(config, options2) {
3497
- const repoName = getRepoName();
3498
- const lastInfo = getLastVersionInfo(repoName, config);
3499
- return { repoName, lastInfo, ignore: resolveIgnoreList(options2, config) };
3500
- }
3501
- function buildContext(options2) {
3502
- const config = loadConfig();
3503
- const data = resolveContextData(config, options2);
3504
- return { config, ...data, verbose: options2.verbose ?? false };
3547
+ // src/commands/complexity/maintainability/index.ts
3548
+ function calculateMaintainabilityIndex(halsteadVolume, cyclomaticComplexity, sloc2) {
3549
+ if (halsteadVolume === 0 || sloc2 === 0) {
3550
+ return 100;
3551
+ }
3552
+ const mi = 171 - 5.2 * Math.log(halsteadVolume) - 0.23 * cyclomaticComplexity - 16.2 * Math.log(sloc2);
3553
+ return Math.max(0, Math.min(100, mi));
3505
3554
  }
3506
- function fetchNextCommits(ctx) {
3507
- const commitsByDate = fetchCommitsByDate(
3508
- ctx.ignore,
3509
- getLastDate(ctx.lastInfo)
3510
- );
3511
- const targetDate = findTargetDate(commitsByDate, resolveSkipDays(ctx.config));
3512
- return targetDate ? { targetDate, commits: getCommitsForDate(commitsByDate, targetDate) } : null;
3555
+ function collectFileMetrics(files) {
3556
+ const fileMetrics = /* @__PURE__ */ new Map();
3557
+ for (const file of files) {
3558
+ const content = fs12.readFileSync(file, "utf-8");
3559
+ fileMetrics.set(file, { sloc: countSloc(content), functions: [] });
3560
+ }
3561
+ forEachFunction(files, (file, _name, node) => {
3562
+ const metrics = fileMetrics.get(file);
3563
+ if (metrics) {
3564
+ const complexity = calculateCyclomaticComplexity(node);
3565
+ const halstead2 = calculateHalstead(node);
3566
+ const mi = calculateMaintainabilityIndex(
3567
+ halstead2.volume,
3568
+ complexity,
3569
+ metrics.sloc
3570
+ );
3571
+ metrics.functions.push(mi);
3572
+ }
3573
+ });
3574
+ return fileMetrics;
3513
3575
  }
3514
- function showResult(ctx, found) {
3515
- if (!found) {
3516
- logNoCommits(ctx.lastInfo);
3517
- return;
3576
+ function aggregateResults(fileMetrics) {
3577
+ const results = [];
3578
+ for (const [file, metrics] of fileMetrics) {
3579
+ if (metrics.functions.length === 0) continue;
3580
+ const avgMaintainability = metrics.functions.reduce((a, b) => a + b, 0) / metrics.functions.length;
3581
+ const minMaintainability = Math.min(...metrics.functions);
3582
+ results.push({ file, avgMaintainability, minMaintainability });
3518
3583
  }
3519
- displayNextEntry(ctx, found.targetDate, found.commits);
3584
+ results.sort((a, b) => a.minMaintainability - b.minMaintainability);
3585
+ return results;
3520
3586
  }
3521
- function next(options2) {
3522
- const ctx = buildContext(options2);
3523
- showResult(ctx, fetchNextCommits(ctx));
3587
+ async function maintainability(pattern2 = "**/*.ts", options2 = {}) {
3588
+ withSourceFiles(pattern2, (files) => {
3589
+ const fileMetrics = collectFileMetrics(files);
3590
+ const results = aggregateResults(fileMetrics);
3591
+ displayMaintainabilityResults(results, options2.threshold);
3592
+ });
3524
3593
  }
3525
3594
 
3526
- // src/commands/devlog/skip.ts
3527
- import chalk41 from "chalk";
3528
- function skip(date) {
3529
- if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) {
3530
- console.log(chalk41.red("Invalid date format. Use YYYY-MM-DD"));
3531
- process.exit(1);
3595
+ // src/commands/complexity/sloc.ts
3596
+ import fs13 from "fs";
3597
+ import chalk35 from "chalk";
3598
+ async function sloc(pattern2 = "**/*.ts", options2 = {}) {
3599
+ withSourceFiles(pattern2, (files) => {
3600
+ const results = [];
3601
+ let hasViolation = false;
3602
+ for (const file of files) {
3603
+ const content = fs13.readFileSync(file, "utf-8");
3604
+ const lines = countSloc(content);
3605
+ results.push({ file, lines });
3606
+ if (options2.threshold !== void 0 && lines > options2.threshold) {
3607
+ hasViolation = true;
3608
+ }
3609
+ }
3610
+ results.sort((a, b) => b.lines - a.lines);
3611
+ for (const { file, lines } of results) {
3612
+ const exceedsThreshold = options2.threshold !== void 0 && lines > options2.threshold;
3613
+ const color = exceedsThreshold ? chalk35.red : chalk35.white;
3614
+ console.log(`${color(file)} \u2192 ${chalk35.cyan(lines)} lines`);
3615
+ }
3616
+ const total = results.reduce((sum, r) => sum + r.lines, 0);
3617
+ console.log(
3618
+ chalk35.dim(`
3619
+ Total: ${total} lines across ${files.length} files`)
3620
+ );
3621
+ if (hasViolation) {
3622
+ process.exit(1);
3623
+ }
3624
+ });
3625
+ }
3626
+
3627
+ // src/commands/complexity/analyze.ts
3628
+ async function analyze(pattern2) {
3629
+ const searchPattern = pattern2.includes("*") || pattern2.includes("/") ? pattern2 : `**/${pattern2}`;
3630
+ const files = findSourceFiles2(searchPattern);
3631
+ if (files.length === 0) {
3632
+ console.log(chalk36.yellow("No files found matching pattern"));
3633
+ return;
3532
3634
  }
3533
- const config = loadProjectConfig();
3534
- const devlog = config.devlog ?? {};
3535
- const skip2 = devlog.skip ?? {};
3536
- const skipDays = skip2.days ?? [];
3537
- if (skipDays.includes(date)) {
3538
- console.log(chalk41.yellow(`${date} is already in skip list`));
3635
+ if (files.length === 1) {
3636
+ const file = files[0];
3637
+ console.log(chalk36.bold.underline("SLOC"));
3638
+ await sloc(file);
3639
+ console.log();
3640
+ console.log(chalk36.bold.underline("Cyclomatic Complexity"));
3641
+ await cyclomatic(file);
3642
+ console.log();
3643
+ console.log(chalk36.bold.underline("Halstead Metrics"));
3644
+ await halstead(file);
3645
+ console.log();
3646
+ console.log(chalk36.bold.underline("Maintainability Index"));
3647
+ await maintainability(file);
3539
3648
  return;
3540
3649
  }
3541
- skipDays.push(date);
3542
- skipDays.sort();
3543
- skip2.days = skipDays;
3544
- devlog.skip = skip2;
3545
- config.devlog = devlog;
3546
- saveConfig(config);
3547
- console.log(chalk41.green(`Added ${date} to skip list`));
3548
- }
3549
-
3550
- // src/commands/devlog/version.ts
3551
- import chalk42 from "chalk";
3552
- function version() {
3553
- const config = loadConfig();
3554
- const name = getRepoName();
3555
- const lastInfo = getLastVersionInfo(name, config);
3556
- const lastVersion = lastInfo?.version ?? null;
3557
- const nextVersion = lastVersion ? bumpVersion(lastVersion, "patch") : null;
3558
- console.log(`${chalk42.bold("name:")} ${name}`);
3559
- console.log(`${chalk42.bold("last:")} ${lastVersion ?? chalk42.dim("none")}`);
3560
- console.log(`${chalk42.bold("next:")} ${nextVersion ?? chalk42.dim("none")}`);
3650
+ await maintainability(searchPattern);
3561
3651
  }
3562
3652
 
3563
- // src/commands/registerDevlog.ts
3564
- function registerDevlog(program2) {
3565
- const devlogCommand = program2.command("devlog").description("Development log utilities");
3566
- devlogCommand.command("list").description("Group git commits by date").option(
3567
- "--days <number>",
3568
- "Number of days to show (default: 30)",
3653
+ // src/commands/registerComplexity.ts
3654
+ function registerComplexity(program2) {
3655
+ const complexityCommand = program2.command("complexity").description("Analyze TypeScript code complexity metrics").argument("[pattern]").action((pattern2) => {
3656
+ if (!pattern2) {
3657
+ complexityCommand.help();
3658
+ return;
3659
+ }
3660
+ return analyze(pattern2);
3661
+ });
3662
+ complexityCommand.command("cyclomatic [pattern]").description("Calculate cyclomatic complexity per function").option("--threshold <number>", "Max complexity threshold", Number.parseInt).action(cyclomatic);
3663
+ complexityCommand.command("halstead [pattern]").description("Calculate Halstead metrics per function").option("--threshold <number>", "Max volume threshold", Number.parseInt).action(halstead);
3664
+ complexityCommand.command("maintainability [pattern]").description("Calculate maintainability index per file").option(
3665
+ "--threshold <number>",
3666
+ "Min maintainability threshold",
3569
3667
  Number.parseInt
3570
- ).option("--since <date>", "Only show commits since this date (YYYY-MM-DD)").option("-r, --reverse", "Show earliest commits first").option("-v, --verbose", "Show file names for each commit").action(list3);
3571
- devlogCommand.command("version").description("Show current repo name and version info").action(version);
3572
- devlogCommand.command("next").description("Show commits for the day after the last versioned entry").option("-v, --verbose", "Show file names for each commit").action(next);
3573
- devlogCommand.command("skip <date>").description("Add a date (YYYY-MM-DD) to the skip list").action(skip);
3668
+ ).action(maintainability);
3669
+ complexityCommand.command("sloc [pattern]").description("Count source lines of code per file").option("--threshold <number>", "Max lines threshold", Number.parseInt).action(sloc);
3574
3670
  }
3575
3671
 
3576
- // src/commands/permitCliReads/index.ts
3577
- import { existsSync as existsSync18, mkdirSync as mkdirSync4, readFileSync as readFileSync16, writeFileSync as writeFileSync14 } from "fs";
3578
- import { homedir as homedir4 } from "os";
3579
- import { join as join12 } from "path";
3580
-
3581
- // src/shared/getInstallDir.ts
3582
- import { execSync as execSync17 } from "child_process";
3583
- import { dirname as dirname13, resolve as resolve3 } from "path";
3584
- import { fileURLToPath as fileURLToPath5 } from "url";
3585
- var __filename3 = fileURLToPath5(import.meta.url);
3586
- var __dirname6 = dirname13(__filename3);
3587
- function getInstallDir() {
3588
- return resolve3(__dirname6, "..");
3589
- }
3590
- function isGitRepo(dir) {
3591
- try {
3592
- const result = execSync17("git rev-parse --show-toplevel", {
3593
- cwd: dir,
3594
- stdio: "pipe"
3595
- }).toString().trim();
3596
- return resolve3(result) === resolve3(dir);
3597
- } catch {
3598
- return false;
3672
+ // src/commands/deploy/redirect.ts
3673
+ import { existsSync as existsSync19, readFileSync as readFileSync16, writeFileSync as writeFileSync14 } from "fs";
3674
+ import chalk37 from "chalk";
3675
+ var TRAILING_SLASH_SCRIPT = ` <script>
3676
+ if (!window.location.pathname.endsWith('/')) {
3677
+ window.location.href = \`\${window.location.pathname}/\${window.location.search}\${window.location.hash}\`;
3678
+ }
3679
+ </script>`;
3680
+ function redirect() {
3681
+ const indexPath = "index.html";
3682
+ if (!existsSync19(indexPath)) {
3683
+ console.log(chalk37.yellow("No index.html found"));
3684
+ return;
3599
3685
  }
3600
- }
3601
-
3602
- // src/commands/permitCliReads/assertCliExists.ts
3603
- import { execSync as execSync18 } from "child_process";
3604
- function assertCliExists(cli) {
3605
- const binary = cli.split(/\s+/)[0];
3606
- const opts = {
3607
- encoding: "utf-8",
3608
- stdio: ["ignore", "pipe", "pipe"]
3609
- };
3610
- try {
3611
- execSync18(`command -v ${binary}`, opts);
3612
- } catch {
3613
- try {
3614
- execSync18(`where ${binary}`, opts);
3615
- } catch {
3616
- console.error(`CLI "${cli}" not found in PATH`);
3617
- process.exit(1);
3618
- }
3686
+ const content = readFileSync16(indexPath, "utf-8");
3687
+ if (content.includes("window.location.pathname.endsWith('/')")) {
3688
+ console.log(chalk37.dim("Trailing slash script already present"));
3689
+ return;
3619
3690
  }
3691
+ const headCloseIndex = content.indexOf("</head>");
3692
+ if (headCloseIndex === -1) {
3693
+ console.log(chalk37.red("Could not find </head> tag in index.html"));
3694
+ return;
3695
+ }
3696
+ const newContent = content.slice(0, headCloseIndex) + TRAILING_SLASH_SCRIPT + "\n " + content.slice(headCloseIndex);
3697
+ writeFileSync14(indexPath, newContent);
3698
+ console.log(chalk37.green("Added trailing slash redirect to index.html"));
3620
3699
  }
3621
3700
 
3622
- // src/commands/permitCliReads/colorize.ts
3623
- import chalk43 from "chalk";
3624
- function colorize(plainOutput) {
3625
- return plainOutput.split("\n").map((line) => {
3626
- if (line.startsWith(" R ")) return chalk43.green(line);
3627
- if (line.startsWith(" W ")) return chalk43.red(line);
3628
- return line;
3629
- }).join("\n");
3701
+ // src/commands/registerDeploy.ts
3702
+ function registerDeploy(program2) {
3703
+ const deployCommand = program2.command("deploy").description("Netlify deployment utilities");
3704
+ deployCommand.command("init").description("Initialize Netlify project and configure deployment").action(init5);
3705
+ deployCommand.command("redirect").description("Add trailing slash redirect script to index.html").action(redirect);
3630
3706
  }
3631
3707
 
3632
- // src/lib/isClaudeCode.ts
3633
- function isClaudeCode() {
3634
- return process.env.CLAUDECODE !== void 0;
3635
- }
3708
+ // src/commands/devlog/list/index.ts
3709
+ import { execSync as execSync16 } from "child_process";
3710
+ import { basename as basename3 } from "path";
3636
3711
 
3637
- // src/commands/permitCliReads/mapAsync.ts
3638
- async function mapAsync(items, concurrency, fn) {
3639
- const results = new Array(items.length);
3640
- let next2 = 0;
3641
- async function worker() {
3642
- while (next2 < items.length) {
3643
- const idx = next2++;
3644
- results[idx] = await fn(items[idx]);
3712
+ // src/commands/devlog/shared.ts
3713
+ import { execSync as execSync15 } from "child_process";
3714
+ import chalk38 from "chalk";
3715
+
3716
+ // src/commands/devlog/loadDevlogEntries.ts
3717
+ import { readdirSync, readFileSync as readFileSync17 } from "fs";
3718
+ import { homedir as homedir5 } from "os";
3719
+ import { join as join13 } from "path";
3720
+ var DEVLOG_DIR = join13(homedir5(), "git/blog/src/content/devlog");
3721
+ function loadDevlogEntries(repoName) {
3722
+ const entries = /* @__PURE__ */ new Map();
3723
+ try {
3724
+ const files = readdirSync(DEVLOG_DIR).filter((f) => f.endsWith(".md"));
3725
+ for (const file of files) {
3726
+ const content = readFileSync17(join13(DEVLOG_DIR, file), "utf-8");
3727
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
3728
+ if (frontmatterMatch) {
3729
+ const frontmatter = frontmatterMatch[1];
3730
+ const dateMatch = frontmatter.match(/date:\s*"?(\d{4}-\d{2}-\d{2})"?/);
3731
+ const versionMatch = frontmatter.match(/version:\s*(.+)/);
3732
+ const titleMatch = frontmatter.match(/title:\s*(.+)/);
3733
+ const tagsMatch = frontmatter.match(/tags:\s*\[([^\]]*)\]/);
3734
+ if (dateMatch && versionMatch && titleMatch && tagsMatch) {
3735
+ const tags = tagsMatch[1].split(",").map((t) => t.trim());
3736
+ const firstTag = tags[0];
3737
+ if (firstTag !== repoName) {
3738
+ continue;
3739
+ }
3740
+ const date = dateMatch[1];
3741
+ const version2 = versionMatch[1].trim();
3742
+ const title = titleMatch[1].trim();
3743
+ const existing = entries.get(date) || [];
3744
+ existing.push({ version: version2, title, filename: file });
3745
+ entries.set(date, existing);
3746
+ }
3747
+ }
3645
3748
  }
3749
+ } catch {
3646
3750
  }
3647
- const workers = Array.from(
3648
- { length: Math.min(concurrency, items.length) },
3649
- () => worker()
3650
- );
3651
- await Promise.all(workers);
3652
- return results;
3751
+ return entries;
3653
3752
  }
3654
3753
 
3655
- // src/commands/permitCliReads/parseCommands.ts
3656
- var COMMAND_SECTION_RE = /^((?:core |general |available |additional |other |management |targeted |alias |github actions )?(?:commands|subgroups)):?$/i;
3657
- function isSkippable(name) {
3658
- return name.startsWith("-") || name.startsWith("<") || name.startsWith("[");
3659
- }
3660
- function parseCommandLine(trimmed) {
3661
- const azMatch = trimmed.match(/^(\S+)\s+(?:\[.*?]\s+)?:\s*(.+)/);
3662
- if (azMatch && !isSkippable(azMatch[1])) {
3663
- return { name: azMatch[1], description: azMatch[2].trim() };
3664
- }
3665
- const colonMatch = trimmed.match(/^(\S+?):\s{2,}(.+)/);
3666
- if (colonMatch && !isSkippable(colonMatch[1])) {
3667
- return { name: colonMatch[1], description: colonMatch[2].trim() };
3754
+ // src/commands/devlog/shared.ts
3755
+ function getCommitFiles(hash) {
3756
+ try {
3757
+ const output = execSync15(`git show --name-only --format="" ${hash}`, {
3758
+ encoding: "utf-8"
3759
+ });
3760
+ return output.trim().split("\n").filter(Boolean);
3761
+ } catch {
3762
+ return [];
3668
3763
  }
3669
- const spaceMatch = trimmed.match(/^(\S+)(?:,\s*\S+)?\s{2,}(.+)/);
3670
- if (spaceMatch && !isSkippable(spaceMatch[1])) {
3671
- return { name: spaceMatch[1], description: spaceMatch[2].trim() };
3764
+ }
3765
+ function shouldIgnoreCommit(files, ignorePaths) {
3766
+ if (ignorePaths.length === 0 || files.length === 0) {
3767
+ return false;
3672
3768
  }
3673
- if (/^\S+$/.test(trimmed) && !isSkippable(trimmed)) {
3674
- return { name: trimmed, description: "" };
3769
+ return files.every(
3770
+ (file) => ignorePaths.some((ignorePath) => file.startsWith(ignorePath))
3771
+ );
3772
+ }
3773
+ function printCommitsWithFiles(commits, ignore2, verbose) {
3774
+ for (const commit2 of commits) {
3775
+ console.log(` ${chalk38.yellow(commit2.hash)} ${commit2.message}`);
3776
+ if (verbose) {
3777
+ const visibleFiles = commit2.files.filter(
3778
+ (file) => !ignore2.some((p) => file.startsWith(p))
3779
+ );
3780
+ for (const file of visibleFiles) {
3781
+ console.log(` ${chalk38.dim(file)}`);
3782
+ }
3783
+ }
3675
3784
  }
3676
- return void 0;
3677
3785
  }
3678
- function parseCommands(helpText) {
3679
- const commands = [];
3680
- let inCommandSection = false;
3681
- for (const line of helpText.split("\n")) {
3682
- const trimmed = line.trim();
3683
- if (COMMAND_SECTION_RE.test(trimmed)) {
3684
- inCommandSection = true;
3786
+ function parseGitLogCommits(output, ignore2, afterDate) {
3787
+ const lines = output.trim().split("\n");
3788
+ const commitsByDate = /* @__PURE__ */ new Map();
3789
+ for (const line of lines) {
3790
+ const [date, hash, ...messageParts] = line.split("|");
3791
+ const message = messageParts.join("|");
3792
+ if (afterDate && date <= afterDate) {
3685
3793
  continue;
3686
3794
  }
3687
- if (inCommandSection && trimmed && !line.startsWith(" ") && !line.startsWith(" ")) {
3688
- inCommandSection = false;
3689
- continue;
3795
+ const files = getCommitFiles(hash);
3796
+ if (!shouldIgnoreCommit(files, ignore2)) {
3797
+ const existing = commitsByDate.get(date) || [];
3798
+ existing.push({ date, hash, message, files });
3799
+ commitsByDate.set(date, existing);
3690
3800
  }
3691
- if (!inCommandSection || !trimmed) continue;
3692
- if (trimmed.startsWith("-") || trimmed.startsWith("=")) continue;
3693
- const parsed = parseCommandLine(trimmed);
3694
- if (parsed) commands.push(parsed);
3695
3801
  }
3696
- return commands;
3802
+ return commitsByDate;
3697
3803
  }
3698
- var COMMAND_SECTION_MULTILINE_RE = new RegExp(
3699
- COMMAND_SECTION_RE.source,
3700
- "im"
3701
- );
3702
- function hasSubcommands(helpText) {
3703
- return COMMAND_SECTION_MULTILINE_RE.test(helpText);
3804
+
3805
+ // src/commands/devlog/list/printDateHeader.ts
3806
+ import chalk39 from "chalk";
3807
+ function printDateHeader(date, isSkipped, entries) {
3808
+ if (isSkipped) {
3809
+ console.log(`${chalk39.bold.blue(date)} ${chalk39.dim("skipped")}`);
3810
+ } else if (entries && entries.length > 0) {
3811
+ const entryInfo = entries.map((e) => `${chalk39.green(e.version)} ${e.title}`).join(" | ");
3812
+ console.log(`${chalk39.bold.blue(date)} ${entryInfo}`);
3813
+ } else {
3814
+ console.log(`${chalk39.bold.blue(date)} ${chalk39.red("\u26A0 devlog missing")}`);
3815
+ }
3704
3816
  }
3705
3817
 
3706
- // src/commands/permitCliReads/runHelp.ts
3707
- import { exec as exec2 } from "child_process";
3708
- function runHelp(args) {
3709
- return new Promise((resolve5) => {
3710
- exec2(
3711
- `${args.join(" ")} --help`,
3712
- { encoding: "utf-8", timeout: 3e4 },
3713
- (_err, stdout, stderr) => {
3714
- resolve5(stdout || stderr || "");
3818
+ // src/commands/devlog/list/index.ts
3819
+ function list3(options2) {
3820
+ const config = loadConfig();
3821
+ const days = options2.days ?? 30;
3822
+ const ignore2 = options2.ignore ?? config.devlog?.ignore ?? [];
3823
+ const skipDays = new Set(config.devlog?.skip?.days ?? []);
3824
+ const repoName = basename3(process.cwd());
3825
+ const devlogEntries = loadDevlogEntries(repoName);
3826
+ const reverseFlag = options2.reverse ? "--reverse " : "";
3827
+ const limitFlag = options2.reverse ? "" : "-n 500 ";
3828
+ const output = execSync16(
3829
+ `git log ${reverseFlag}${limitFlag}--pretty=format:'%ad|%h|%s' --date=short`,
3830
+ { encoding: "utf-8" }
3831
+ );
3832
+ const commitsByDate = parseGitLogCommits(output, ignore2);
3833
+ let dateCount = 0;
3834
+ let isFirst = true;
3835
+ for (const [date, dateCommits] of commitsByDate) {
3836
+ if (options2.since) {
3837
+ if (date < options2.since) {
3838
+ break;
3715
3839
  }
3716
- );
3717
- });
3840
+ } else if (dateCount >= days) {
3841
+ break;
3842
+ }
3843
+ dateCount++;
3844
+ if (!isFirst) {
3845
+ console.log();
3846
+ }
3847
+ isFirst = false;
3848
+ printDateHeader(date, skipDays.has(date), devlogEntries.get(date));
3849
+ printCommitsWithFiles(dateCommits, ignore2, options2.verbose ?? false);
3850
+ }
3718
3851
  }
3719
3852
 
3720
- // src/commands/permitCliReads/discoverAll.ts
3721
- var SAFETY_DEPTH = 10;
3722
- var CONCURRENCY = 8;
3723
- var interactive = !isClaudeCode();
3724
- function showProgress(p, label2) {
3725
- if (!interactive) return;
3726
- const pct = Math.round(p.done / p.total * 100);
3727
- process.stderr.write(`\r\x1B[K[${pct}%] Scanning ${label2}...`);
3728
- }
3729
- async function resolveCommand(cli, path31, description, depth, p) {
3730
- showProgress(p, path31.join(" "));
3731
- const subHelp = await runHelp([cli, ...path31]);
3732
- if (!subHelp || !hasSubcommands(subHelp)) {
3733
- return [{ path: path31, description }];
3853
+ // src/commands/devlog/getLastVersionInfo.ts
3854
+ import { execSync as execSync17 } from "child_process";
3855
+ import semver from "semver";
3856
+ function getVersionAtCommit(hash) {
3857
+ try {
3858
+ const content = execSync17(`git show ${hash}:package.json`, {
3859
+ encoding: "utf-8"
3860
+ });
3861
+ const pkg = JSON.parse(content);
3862
+ return pkg.version ?? null;
3863
+ } catch {
3864
+ return null;
3734
3865
  }
3735
- const children = await discoverAt(cli, path31, depth + 1, p);
3736
- return children.length > 0 ? children : [{ path: path31, description }];
3737
- }
3738
- async function discoverAt(cli, parentPath, depth, p) {
3739
- if (depth > SAFETY_DEPTH) return [];
3740
- const helpText = await runHelp([cli, ...parentPath]);
3741
- if (!helpText) return [];
3742
- const cmds = parseCommands(helpText);
3743
- const results = await mapAsync(
3744
- cmds,
3745
- CONCURRENCY,
3746
- (cmd) => resolveCommand(cli, [...parentPath, cmd.name], cmd.description, depth, p)
3747
- );
3748
- return results.flat();
3749
- }
3750
- async function discoverAll(cli) {
3751
- const topLevel = parseCommands(await runHelp([cli]));
3752
- const p = { done: 0, total: topLevel.length };
3753
- const results = await mapAsync(topLevel, CONCURRENCY, async (cmd) => {
3754
- showProgress(p, cmd.name);
3755
- const resolved = await resolveCommand(
3756
- cli,
3757
- [cmd.name],
3758
- cmd.description,
3759
- 1,
3760
- p
3761
- );
3762
- p.done++;
3763
- showProgress(p, cmd.name);
3764
- return resolved;
3765
- });
3766
- if (interactive) process.stderr.write("\r\x1B[K");
3767
- return results.flat();
3768
3866
  }
3769
-
3770
- // src/commands/permitCliReads/classifyVerb.ts
3771
- var READ_VERBS = /* @__PURE__ */ new Set([
3772
- "list",
3773
- "show",
3774
- "view",
3775
- "export",
3776
- "get",
3777
- "diff",
3778
- "status",
3779
- "search",
3780
- "checks",
3781
- "describe",
3782
- "inspect",
3783
- "logs",
3784
- "cat",
3785
- "top",
3786
- "explain",
3787
- "exists",
3788
- "browse",
3789
- "watch"
3790
- ]);
3791
- var WRITE_VERBS = /* @__PURE__ */ new Set([
3792
- "create",
3793
- "delete",
3794
- "import",
3795
- "set",
3796
- "update",
3797
- "merge",
3798
- "close",
3799
- "reopen",
3800
- "edit",
3801
- "apply",
3802
- "patch",
3803
- "drain",
3804
- "cordon",
3805
- "taint",
3806
- "push",
3807
- "deploy",
3808
- "add",
3809
- "remove",
3810
- "assign",
3811
- "unassign",
3812
- "lock",
3813
- "unlock",
3814
- "start",
3815
- "stop",
3816
- "restart",
3817
- "enable",
3818
- "disable",
3819
- "revoke",
3820
- "rotate"
3821
- ]);
3822
- function classifyVerb(verb) {
3823
- if (READ_VERBS.has(verb)) return "r";
3824
- if (WRITE_VERBS.has(verb)) return "w";
3825
- return "?";
3867
+ function stripToMinor(version2) {
3868
+ const parsed = semver.parse(semver.coerce(version2));
3869
+ return parsed ? `v${parsed.major}.${parsed.minor}` : `v${version2}`;
3826
3870
  }
3827
-
3828
- // src/commands/permitCliReads/formatHuman.ts
3829
- function prefix(kind) {
3830
- if (kind === "r") return " R ";
3831
- if (kind === "w") return " W ";
3832
- return " ? ";
3871
+ function getLastVersionInfoFromGit() {
3872
+ try {
3873
+ const output = execSync17(
3874
+ "git log -1 --pretty=format:'%ad|%h' --date=short",
3875
+ {
3876
+ encoding: "utf-8"
3877
+ }
3878
+ ).trim();
3879
+ const [date, hash] = output.split("|");
3880
+ if (!date || !hash) return null;
3881
+ const version2 = getVersionAtCommit(hash);
3882
+ if (!version2) return null;
3883
+ return { date, version: stripToMinor(version2) };
3884
+ } catch {
3885
+ return null;
3886
+ }
3833
3887
  }
3834
- function formatHuman(cli, commands) {
3835
- const sorted = [...commands].sort(
3836
- (a, b) => a.path.join(" ").localeCompare(b.path.join(" "))
3837
- );
3838
- const lines = [`Discovered ${commands.length} commands for "${cli}":
3839
- `];
3840
- for (const cmd of sorted) {
3841
- const verb = cmd.path[cmd.path.length - 1];
3842
- const full = `${cli} ${cmd.path.join(" ")}`;
3843
- const text = cmd.description ? `${full} \u2014 ${cmd.description}` : full;
3844
- lines.push(`${prefix(classifyVerb(verb))}${text}`);
3888
+ function findLastDate(entries) {
3889
+ const dates = Array.from(entries.keys()).sort().reverse();
3890
+ return dates[0] ?? null;
3891
+ }
3892
+ function getLastVersionInfo(repoName, config) {
3893
+ const entries = loadDevlogEntries(repoName);
3894
+ const lastDate = findLastDate(entries);
3895
+ if (!lastDate) return null;
3896
+ if (config?.commit?.conventional) {
3897
+ const gitInfo = getLastVersionInfoFromGit();
3898
+ if (gitInfo) return { date: lastDate, version: gitInfo.version };
3845
3899
  }
3846
- return lines.join("\n");
3900
+ const lastVersion = entries.get(lastDate)?.[0]?.version;
3901
+ return lastVersion ? { date: lastDate, version: lastVersion } : null;
3902
+ }
3903
+ function cleanVersion(version2) {
3904
+ return semver.clean(version2) ?? semver.coerce(version2)?.version ?? null;
3905
+ }
3906
+ function bumpVersion(version2, type) {
3907
+ const cleaned = cleanVersion(version2);
3908
+ if (!cleaned) return version2;
3909
+ const bumped = semver.inc(cleaned, type);
3910
+ if (!bumped) return version2;
3911
+ if (type === "minor") return stripToMinor(bumped);
3912
+ return `v${bumped}`;
3847
3913
  }
3848
3914
 
3849
- // src/commands/permitCliReads/parseCached.ts
3850
- function parseCached(cli, cached) {
3851
- const prefix2 = `${cli} `;
3852
- const commands = [];
3853
- for (const line of cached.split("\n")) {
3854
- const trimmed = line.replace(/^ [RW?] {2}/, "").trim();
3855
- if (!trimmed.startsWith(prefix2)) continue;
3856
- const rest = trimmed.slice(prefix2.length);
3857
- const dashIdx = rest.indexOf(" \u2014 ");
3858
- const pathStr = dashIdx >= 0 ? rest.slice(0, dashIdx) : rest;
3859
- const description = dashIdx >= 0 ? rest.slice(dashIdx + 3) : "";
3860
- commands.push({ path: pathStr.split(" "), description });
3915
+ // src/commands/devlog/next/displayNextEntry/index.ts
3916
+ import { execSync as execSync18 } from "child_process";
3917
+ import chalk41 from "chalk";
3918
+
3919
+ // src/commands/devlog/next/displayNextEntry/displayVersion.ts
3920
+ import chalk40 from "chalk";
3921
+ function displayVersion(conventional, firstHash, patchVersion, minorVersion) {
3922
+ if (conventional && firstHash) {
3923
+ const version2 = getVersionAtCommit(firstHash);
3924
+ if (version2) {
3925
+ console.log(`${chalk40.bold("version:")} ${stripToMinor(version2)}`);
3926
+ } else {
3927
+ console.log(`${chalk40.bold("version:")} ${chalk40.red("unknown")}`);
3928
+ }
3929
+ } else if (patchVersion && minorVersion) {
3930
+ console.log(
3931
+ `${chalk40.bold("version:")} ${patchVersion} (patch) or ${minorVersion} (minor)`
3932
+ );
3933
+ } else {
3934
+ console.log(`${chalk40.bold("version:")} v0.1 (initial)`);
3861
3935
  }
3862
- return commands;
3863
3936
  }
3864
3937
 
3865
- // src/commands/permitCliReads/updateSettings.ts
3866
- function updateSettings(cli, commands) {
3867
- const existing = loadCliReads();
3868
- const readEntries = commands.filter((cmd) => classifyVerb(cmd.path[cmd.path.length - 1]) === "r").map((cmd) => `${cli} ${cmd.path.join(" ")}`);
3869
- const merged = [.../* @__PURE__ */ new Set([...existing, ...readEntries])].sort();
3870
- if (merged.length === existing.length && merged.every((e, i) => e === existing[i]))
3871
- return;
3872
- saveCliReads(merged);
3938
+ // src/commands/devlog/next/displayNextEntry/index.ts
3939
+ function computeVersions(lastInfo) {
3940
+ if (!lastInfo) return { patch: null, minor: null };
3941
+ return {
3942
+ patch: bumpVersion(lastInfo.version, "patch"),
3943
+ minor: bumpVersion(lastInfo.version, "minor")
3944
+ };
3945
+ }
3946
+ function findTargetDate(commitsByDate, skipDays) {
3947
+ return Array.from(commitsByDate.keys()).filter((d) => !skipDays.has(d)).sort()[0];
3948
+ }
3949
+ function fetchCommitsByDate(ignore2, lastDate) {
3950
+ const output = execSync18(
3951
+ "git log --pretty=format:'%ad|%h|%s' --date=short -n 500",
3952
+ { encoding: "utf-8" }
3953
+ );
3954
+ return parseGitLogCommits(output, ignore2, lastDate);
3955
+ }
3956
+ function printVersionInfo(config, lastInfo, firstHash) {
3957
+ const versions = computeVersions(lastInfo);
3958
+ displayVersion(
3959
+ !!config.commit?.conventional,
3960
+ firstHash,
3961
+ versions.patch,
3962
+ versions.minor
3963
+ );
3964
+ }
3965
+ function resolveIgnoreList(options2, config) {
3966
+ return options2.ignore ?? config.devlog?.ignore ?? [];
3967
+ }
3968
+ function resolveSkipDays(config) {
3969
+ return new Set(config.devlog?.skip?.days ?? []);
3970
+ }
3971
+ function getLastDate(lastInfo) {
3972
+ return lastInfo?.date ?? null;
3973
+ }
3974
+ function getCommitsForDate(commitsByDate, date) {
3975
+ return commitsByDate.get(date) ?? [];
3976
+ }
3977
+ function noCommitsMessage(hasLastInfo) {
3978
+ return hasLastInfo ? "No commits after last versioned entry" : "No commits found";
3979
+ }
3980
+ function logName(repoName) {
3981
+ console.log(`${chalk41.bold("name:")} ${repoName}`);
3982
+ }
3983
+ function displayNextEntry(ctx, targetDate, commits) {
3984
+ logName(ctx.repoName);
3985
+ printVersionInfo(ctx.config, ctx.lastInfo, commits[0]?.hash);
3986
+ console.log(chalk41.bold.blue(targetDate));
3987
+ printCommitsWithFiles(commits, ctx.ignore, ctx.verbose);
3988
+ }
3989
+ function logNoCommits(lastInfo) {
3990
+ console.log(chalk41.dim(noCommitsMessage(!!lastInfo)));
3873
3991
  }
3874
3992
 
3875
- // src/commands/permitCliReads/index.ts
3876
- function logPath(cli) {
3877
- const safeName = cli.replace(/\s+/g, "-");
3878
- return join12(homedir4(), ".assist", `cli-discover-${safeName}.log`);
3993
+ // src/commands/devlog/next/index.ts
3994
+ function resolveContextData(config, options2) {
3995
+ const repoName = getRepoName();
3996
+ const lastInfo = getLastVersionInfo(repoName, config);
3997
+ return { repoName, lastInfo, ignore: resolveIgnoreList(options2, config) };
3879
3998
  }
3880
- function readCache(cli) {
3881
- const path31 = logPath(cli);
3882
- if (!existsSync18(path31)) return void 0;
3883
- return readFileSync16(path31, "utf-8");
3999
+ function buildContext(options2) {
4000
+ const config = loadConfig();
4001
+ const data = resolveContextData(config, options2);
4002
+ return { config, ...data, verbose: options2.verbose ?? false };
3884
4003
  }
3885
- function writeCache(cli, output) {
3886
- const dir = join12(homedir4(), ".assist");
3887
- mkdirSync4(dir, { recursive: true });
3888
- writeFileSync14(logPath(cli), output);
4004
+ function fetchNextCommits(ctx) {
4005
+ const commitsByDate = fetchCommitsByDate(
4006
+ ctx.ignore,
4007
+ getLastDate(ctx.lastInfo)
4008
+ );
4009
+ const targetDate = findTargetDate(commitsByDate, resolveSkipDays(ctx.config));
4010
+ return targetDate ? { targetDate, commits: getCommitsForDate(commitsByDate, targetDate) } : null;
3889
4011
  }
3890
- async function permitCliReads(cli, options2 = { noCache: false }) {
3891
- if (!cli) {
3892
- console.error("Usage: assist permit-cli-reads <cli>");
3893
- process.exit(1);
4012
+ function showResult(ctx, found) {
4013
+ if (!found) {
4014
+ logNoCommits(ctx.lastInfo);
4015
+ return;
3894
4016
  }
3895
- const installDir = getInstallDir();
3896
- if (!isGitRepo(installDir)) {
3897
- console.error(
3898
- "permit-cli-reads must be run from the assist git repo, not a global install."
3899
- );
4017
+ displayNextEntry(ctx, found.targetDate, found.commits);
4018
+ }
4019
+ function next(options2) {
4020
+ const ctx = buildContext(options2);
4021
+ showResult(ctx, fetchNextCommits(ctx));
4022
+ }
4023
+
4024
+ // src/commands/devlog/skip.ts
4025
+ import chalk42 from "chalk";
4026
+ function skip(date) {
4027
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) {
4028
+ console.log(chalk42.red("Invalid date format. Use YYYY-MM-DD"));
3900
4029
  process.exit(1);
3901
4030
  }
3902
- if (!options2.noCache) {
3903
- const cached = readCache(cli);
3904
- if (cached) {
3905
- console.log(colorize(cached));
3906
- updateSettings(cli, parseCached(cli, cached));
3907
- return;
3908
- }
4031
+ const config = loadProjectConfig();
4032
+ const devlog = config.devlog ?? {};
4033
+ const skip2 = devlog.skip ?? {};
4034
+ const skipDays = skip2.days ?? [];
4035
+ if (skipDays.includes(date)) {
4036
+ console.log(chalk42.yellow(`${date} is already in skip list`));
4037
+ return;
3909
4038
  }
3910
- assertCliExists(cli);
3911
- const commands = await discoverAll(cli);
3912
- const output = formatHuman(cli, commands);
3913
- console.log(colorize(output));
3914
- writeCache(cli, output);
3915
- updateSettings(cli, commands);
4039
+ skipDays.push(date);
4040
+ skipDays.sort();
4041
+ skip2.days = skipDays;
4042
+ devlog.skip = skip2;
4043
+ config.devlog = devlog;
4044
+ saveConfig(config);
4045
+ console.log(chalk42.green(`Added ${date} to skip list`));
3916
4046
  }
3917
4047
 
3918
- // src/commands/registerPermitCliReads.ts
3919
- function registerPermitCliReads(program2) {
3920
- program2.command("permit-cli-reads").description("Discover a CLI's commands and auto-permit read-only ones").argument(
3921
- "<cli...>",
3922
- "CLI binary and optional subcommand (e.g. gh, az, acli jira)"
3923
- ).option("--no-cache", "Force fresh discovery, ignoring cached results").action((cli, options2) => {
3924
- permitCliReads(cli.join(" "), { noCache: !options2.cache });
3925
- });
4048
+ // src/commands/devlog/version.ts
4049
+ import chalk43 from "chalk";
4050
+ function version() {
4051
+ const config = loadConfig();
4052
+ const name = getRepoName();
4053
+ const lastInfo = getLastVersionInfo(name, config);
4054
+ const lastVersion = lastInfo?.version ?? null;
4055
+ const nextVersion = lastVersion ? bumpVersion(lastVersion, "patch") : null;
4056
+ console.log(`${chalk43.bold("name:")} ${name}`);
4057
+ console.log(`${chalk43.bold("last:")} ${lastVersion ?? chalk43.dim("none")}`);
4058
+ console.log(`${chalk43.bold("next:")} ${nextVersion ?? chalk43.dim("none")}`);
4059
+ }
4060
+
4061
+ // src/commands/registerDevlog.ts
4062
+ function registerDevlog(program2) {
4063
+ const devlogCommand = program2.command("devlog").description("Development log utilities");
4064
+ devlogCommand.command("list").description("Group git commits by date").option(
4065
+ "--days <number>",
4066
+ "Number of days to show (default: 30)",
4067
+ Number.parseInt
4068
+ ).option("--since <date>", "Only show commits since this date (YYYY-MM-DD)").option("-r, --reverse", "Show earliest commits first").option("-v, --verbose", "Show file names for each commit").action(list3);
4069
+ devlogCommand.command("version").description("Show current repo name and version info").action(version);
4070
+ devlogCommand.command("next").description("Show commits for the day after the last versioned entry").option("-v, --verbose", "Show file names for each commit").action(next);
4071
+ devlogCommand.command("skip <date>").description("Add a date (YYYY-MM-DD) to the skip list").action(skip);
3926
4072
  }
3927
4073
 
3928
4074
  // src/commands/prs/comment.ts
3929
4075
  import { spawnSync as spawnSync2 } from "child_process";
3930
4076
  import { unlinkSync as unlinkSync3, writeFileSync as writeFileSync15 } from "fs";
3931
4077
  import { tmpdir as tmpdir2 } from "os";
3932
- import { join as join13 } from "path";
4078
+ import { join as join14 } from "path";
3933
4079
 
3934
4080
  // src/commands/prs/shared.ts
3935
4081
  import { execSync as execSync19 } from "child_process";
@@ -4001,7 +4147,7 @@ function comment(path31, line, body) {
4001
4147
  validateLine(line);
4002
4148
  try {
4003
4149
  const prId = getCurrentPrNodeId();
4004
- const queryFile = join13(tmpdir2(), `gh-query-${Date.now()}.graphql`);
4150
+ const queryFile = join14(tmpdir2(), `gh-query-${Date.now()}.graphql`);
4005
4151
  writeFileSync15(queryFile, MUTATION);
4006
4152
  try {
4007
4153
  const result = spawnSync2(
@@ -4046,26 +4192,26 @@ import { execSync as execSync21 } from "child_process";
4046
4192
  import { execSync as execSync20 } from "child_process";
4047
4193
  import { unlinkSync as unlinkSync5, writeFileSync as writeFileSync16 } from "fs";
4048
4194
  import { tmpdir as tmpdir3 } from "os";
4049
- import { join as join15 } from "path";
4195
+ import { join as join16 } from "path";
4050
4196
 
4051
4197
  // src/commands/prs/loadCommentsCache.ts
4052
- import { existsSync as existsSync19, readFileSync as readFileSync17, unlinkSync as unlinkSync4 } from "fs";
4053
- import { join as join14 } from "path";
4054
- import { parse } from "yaml";
4198
+ import { existsSync as existsSync20, readFileSync as readFileSync18, unlinkSync as unlinkSync4 } from "fs";
4199
+ import { join as join15 } from "path";
4200
+ import { parse as parse2 } from "yaml";
4055
4201
  function getCachePath(prNumber) {
4056
- return join14(process.cwd(), ".assist", `pr-${prNumber}-comments.yaml`);
4202
+ return join15(process.cwd(), ".assist", `pr-${prNumber}-comments.yaml`);
4057
4203
  }
4058
4204
  function loadCommentsCache(prNumber) {
4059
4205
  const cachePath = getCachePath(prNumber);
4060
- if (!existsSync19(cachePath)) {
4206
+ if (!existsSync20(cachePath)) {
4061
4207
  return null;
4062
4208
  }
4063
- const content = readFileSync17(cachePath, "utf-8");
4064
- return parse(content);
4209
+ const content = readFileSync18(cachePath, "utf-8");
4210
+ return parse2(content);
4065
4211
  }
4066
4212
  function deleteCommentsCache(prNumber) {
4067
4213
  const cachePath = getCachePath(prNumber);
4068
- if (existsSync19(cachePath)) {
4214
+ if (existsSync20(cachePath)) {
4069
4215
  unlinkSync4(cachePath);
4070
4216
  console.log("No more unresolved line comments. Cache dropped.");
4071
4217
  }
@@ -4080,7 +4226,7 @@ function replyToComment(org, repo, prNumber, commentId, message) {
4080
4226
  }
4081
4227
  function resolveThread(threadId) {
4082
4228
  const mutation = `mutation($threadId: ID!) { resolveReviewThread(input: {threadId: $threadId}) { thread { isResolved } } }`;
4083
- const queryFile = join15(tmpdir3(), `gh-mutation-${Date.now()}.graphql`);
4229
+ const queryFile = join16(tmpdir3(), `gh-mutation-${Date.now()}.graphql`);
4084
4230
  writeFileSync16(queryFile, mutation);
4085
4231
  try {
4086
4232
  execSync20(
@@ -4161,18 +4307,18 @@ function fixed(commentId, sha) {
4161
4307
  }
4162
4308
 
4163
4309
  // src/commands/prs/listComments/index.ts
4164
- import { existsSync as existsSync20, mkdirSync as mkdirSync5, writeFileSync as writeFileSync18 } from "fs";
4165
- import { join as join17 } from "path";
4310
+ import { existsSync as existsSync21, mkdirSync as mkdirSync5, writeFileSync as writeFileSync18 } from "fs";
4311
+ import { join as join18 } from "path";
4166
4312
  import { stringify } from "yaml";
4167
4313
 
4168
4314
  // src/commands/prs/fetchThreadIds.ts
4169
4315
  import { execSync as execSync22 } from "child_process";
4170
4316
  import { unlinkSync as unlinkSync6, writeFileSync as writeFileSync17 } from "fs";
4171
4317
  import { tmpdir as tmpdir4 } from "os";
4172
- import { join as join16 } from "path";
4318
+ import { join as join17 } from "path";
4173
4319
  var THREAD_QUERY = `query($owner: String!, $repo: String!, $prNumber: Int!) { repository(owner: $owner, name: $repo) { pullRequest(number: $prNumber) { reviewThreads(first: 100) { nodes { id isResolved comments(first: 100) { nodes { databaseId } } } } } } }`;
4174
4320
  function fetchThreadIds(org, repo, prNumber) {
4175
- const queryFile = join16(tmpdir4(), `gh-query-${Date.now()}.graphql`);
4321
+ const queryFile = join17(tmpdir4(), `gh-query-${Date.now()}.graphql`);
4176
4322
  writeFileSync17(queryFile, THREAD_QUERY);
4177
4323
  try {
4178
4324
  const result = execSync22(
@@ -4277,8 +4423,8 @@ function printComments(comments) {
4277
4423
  }
4278
4424
  }
4279
4425
  function writeCommentsCache(prNumber, comments) {
4280
- const assistDir = join17(process.cwd(), ".assist");
4281
- if (!existsSync20(assistDir)) {
4426
+ const assistDir = join18(process.cwd(), ".assist");
4427
+ if (!existsSync21(assistDir)) {
4282
4428
  mkdirSync5(assistDir, { recursive: true });
4283
4429
  }
4284
4430
  const cacheData = {
@@ -4286,7 +4432,7 @@ function writeCommentsCache(prNumber, comments) {
4286
4432
  fetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
4287
4433
  comments
4288
4434
  };
4289
- const cachePath = join17(assistDir, `pr-${prNumber}-comments.yaml`);
4435
+ const cachePath = join18(assistDir, `pr-${prNumber}-comments.yaml`);
4290
4436
  writeFileSync18(cachePath, stringify(cacheData));
4291
4437
  }
4292
4438
  function handleKnownErrors(error) {
@@ -4665,7 +4811,7 @@ function getViolations(pattern2, options2 = {}, maxLines = DEFAULT_MAX_LINES) {
4665
4811
 
4666
4812
  // src/commands/refactor/check/index.ts
4667
4813
  function runScript(script, cwd) {
4668
- return new Promise((resolve5) => {
4814
+ return new Promise((resolve6) => {
4669
4815
  const child = spawn3("npm", ["run", script], {
4670
4816
  stdio: "pipe",
4671
4817
  shell: true,
@@ -4679,7 +4825,7 @@ function runScript(script, cwd) {
4679
4825
  output += data.toString();
4680
4826
  });
4681
4827
  child.on("close", (code) => {
4682
- resolve5({ script, code: code ?? 1, output });
4828
+ resolve6({ script, code: code ?? 1, output });
4683
4829
  });
4684
4830
  });
4685
4831
  }
@@ -5251,8 +5397,8 @@ function registerRefactor(program2) {
5251
5397
  }
5252
5398
 
5253
5399
  // src/commands/transcript/shared.ts
5254
- import { existsSync as existsSync21, readdirSync as readdirSync2, statSync } from "fs";
5255
- import { basename as basename4, join as join18, relative } from "path";
5400
+ import { existsSync as existsSync22, readdirSync as readdirSync2, statSync } from "fs";
5401
+ import { basename as basename4, join as join19, relative } from "path";
5256
5402
  import * as readline2 from "readline";
5257
5403
  var DATE_PREFIX_REGEX = /^\d{4}-\d{2}-\d{2}/;
5258
5404
  function getDatePrefix(daysOffset = 0) {
@@ -5267,10 +5413,10 @@ function isValidDatePrefix(filename) {
5267
5413
  return DATE_PREFIX_REGEX.test(filename);
5268
5414
  }
5269
5415
  function collectFiles(dir, extension) {
5270
- if (!existsSync21(dir)) return [];
5416
+ if (!existsSync22(dir)) return [];
5271
5417
  const results = [];
5272
5418
  for (const entry of readdirSync2(dir)) {
5273
- const fullPath = join18(dir, entry);
5419
+ const fullPath = join19(dir, entry);
5274
5420
  if (statSync(fullPath).isDirectory()) {
5275
5421
  results.push(...collectFiles(fullPath, extension));
5276
5422
  } else if (entry.endsWith(extension)) {
@@ -5302,9 +5448,9 @@ function createReadlineInterface() {
5302
5448
  });
5303
5449
  }
5304
5450
  function askQuestion(rl, question) {
5305
- return new Promise((resolve5) => {
5451
+ return new Promise((resolve6) => {
5306
5452
  rl.question(question, (answer) => {
5307
- resolve5(answer.trim());
5453
+ resolve6(answer.trim());
5308
5454
  });
5309
5455
  });
5310
5456
  }
@@ -5364,14 +5510,14 @@ async function configure() {
5364
5510
  }
5365
5511
 
5366
5512
  // src/commands/transcript/format/index.ts
5367
- import { existsSync as existsSync23 } from "fs";
5513
+ import { existsSync as existsSync24 } from "fs";
5368
5514
 
5369
5515
  // src/commands/transcript/format/fixInvalidDatePrefixes/index.ts
5370
- import { dirname as dirname15, join as join20 } from "path";
5516
+ import { dirname as dirname15, join as join21 } from "path";
5371
5517
 
5372
5518
  // src/commands/transcript/format/fixInvalidDatePrefixes/promptForDateFix.ts
5373
5519
  import { renameSync } from "fs";
5374
- import { join as join19 } from "path";
5520
+ import { join as join20 } from "path";
5375
5521
  async function resolveDate(rl, choice) {
5376
5522
  if (choice === "1") return getDatePrefix(0);
5377
5523
  if (choice === "2") return getDatePrefix(-1);
@@ -5386,7 +5532,7 @@ async function resolveDate(rl, choice) {
5386
5532
  }
5387
5533
  function renameWithPrefix(vttDir, vttFile, prefix2) {
5388
5534
  const newFilename = `${prefix2}.${vttFile}`;
5389
- renameSync(join19(vttDir, vttFile), join19(vttDir, newFilename));
5535
+ renameSync(join20(vttDir, vttFile), join20(vttDir, newFilename));
5390
5536
  console.log(`Renamed to: ${newFilename}`);
5391
5537
  return newFilename;
5392
5538
  }
@@ -5420,12 +5566,12 @@ async function fixInvalidDatePrefixes(vttFiles) {
5420
5566
  const vttFileDir = dirname15(vttFile.absolutePath);
5421
5567
  const newFilename = await promptForDateFix(vttFile.filename, vttFileDir);
5422
5568
  if (newFilename) {
5423
- const newRelativePath = join20(
5569
+ const newRelativePath = join21(
5424
5570
  dirname15(vttFile.relativePath),
5425
5571
  newFilename
5426
5572
  );
5427
5573
  vttFiles[i] = {
5428
- absolutePath: join20(vttFileDir, newFilename),
5574
+ absolutePath: join21(vttFileDir, newFilename),
5429
5575
  relativePath: newRelativePath,
5430
5576
  filename: newFilename
5431
5577
  };
@@ -5438,8 +5584,8 @@ async function fixInvalidDatePrefixes(vttFiles) {
5438
5584
  }
5439
5585
 
5440
5586
  // src/commands/transcript/format/processVttFile/index.ts
5441
- import { existsSync as existsSync22, mkdirSync as mkdirSync6, readFileSync as readFileSync18, writeFileSync as writeFileSync19 } from "fs";
5442
- import { basename as basename5, dirname as dirname16, join as join21 } from "path";
5587
+ import { existsSync as existsSync23, mkdirSync as mkdirSync6, readFileSync as readFileSync19, writeFileSync as writeFileSync19 } from "fs";
5588
+ import { basename as basename5, dirname as dirname16, join as join22 } from "path";
5443
5589
 
5444
5590
  // src/commands/transcript/cleanText.ts
5445
5591
  function cleanText(text) {
@@ -5649,21 +5795,21 @@ function toMdFilename(vttFilename) {
5649
5795
  return `${basename5(vttFilename, ".vtt").replace(/\s*Transcription\s*/g, " ").trim()}.md`;
5650
5796
  }
5651
5797
  function resolveOutputDir(relativeDir, transcriptsDir) {
5652
- return relativeDir === "." ? transcriptsDir : join21(transcriptsDir, relativeDir);
5798
+ return relativeDir === "." ? transcriptsDir : join22(transcriptsDir, relativeDir);
5653
5799
  }
5654
5800
  function buildOutputPaths(vttFile, transcriptsDir) {
5655
5801
  const mdFile = toMdFilename(vttFile.filename);
5656
5802
  const relativeDir = dirname16(vttFile.relativePath);
5657
5803
  const outputDir = resolveOutputDir(relativeDir, transcriptsDir);
5658
- const outputPath = join21(outputDir, mdFile);
5804
+ const outputPath = join22(outputDir, mdFile);
5659
5805
  return { outputDir, outputPath, mdFile, relativeDir };
5660
5806
  }
5661
5807
  function logSkipped(relativeDir, mdFile) {
5662
- console.log(`Skipping (already exists): ${join21(relativeDir, mdFile)}`);
5808
+ console.log(`Skipping (already exists): ${join22(relativeDir, mdFile)}`);
5663
5809
  return "skipped";
5664
5810
  }
5665
5811
  function ensureDirectory(dir, label2) {
5666
- if (!existsSync22(dir)) {
5812
+ if (!existsSync23(dir)) {
5667
5813
  mkdirSync6(dir, { recursive: true });
5668
5814
  console.log(`Created ${label2}: ${dir}`);
5669
5815
  }
@@ -5686,7 +5832,7 @@ function logReduction(cueCount, messageCount) {
5686
5832
  }
5687
5833
  function readAndParseCues(inputPath) {
5688
5834
  console.log(`Reading: ${inputPath}`);
5689
- return processCues(readFileSync18(inputPath, "utf-8"));
5835
+ return processCues(readFileSync19(inputPath, "utf-8"));
5690
5836
  }
5691
5837
  function writeFormatted(outputPath, content) {
5692
5838
  writeFileSync19(outputPath, content, "utf-8");
@@ -5699,7 +5845,7 @@ function convertVttToMarkdown(inputPath, outputPath) {
5699
5845
  logReduction(cues.length, chatMessages.length);
5700
5846
  }
5701
5847
  function tryProcessVtt(vttFile, paths) {
5702
- if (existsSync22(paths.outputPath))
5848
+ if (existsSync23(paths.outputPath))
5703
5849
  return logSkipped(paths.relativeDir, paths.mdFile);
5704
5850
  convertVttToMarkdown(vttFile.absolutePath, paths.outputPath);
5705
5851
  return "processed";
@@ -5725,7 +5871,7 @@ function processAllFiles(vttFiles, transcriptsDir) {
5725
5871
  logSummary(counts);
5726
5872
  }
5727
5873
  function requireVttDir(vttDir) {
5728
- if (!existsSync23(vttDir)) {
5874
+ if (!existsSync24(vttDir)) {
5729
5875
  console.error(`VTT directory not found: ${vttDir}`);
5730
5876
  process.exit(1);
5731
5877
  }
@@ -5757,18 +5903,18 @@ async function format() {
5757
5903
  }
5758
5904
 
5759
5905
  // src/commands/transcript/summarise/index.ts
5760
- import { existsSync as existsSync25 } from "fs";
5761
- import { basename as basename6, dirname as dirname18, join as join23, relative as relative2 } from "path";
5906
+ import { existsSync as existsSync26 } from "fs";
5907
+ import { basename as basename6, dirname as dirname18, join as join24, relative as relative2 } from "path";
5762
5908
 
5763
5909
  // src/commands/transcript/summarise/processStagedFile/index.ts
5764
5910
  import {
5765
- existsSync as existsSync24,
5911
+ existsSync as existsSync25,
5766
5912
  mkdirSync as mkdirSync7,
5767
- readFileSync as readFileSync19,
5913
+ readFileSync as readFileSync20,
5768
5914
  renameSync as renameSync2,
5769
5915
  rmSync
5770
5916
  } from "fs";
5771
- import { dirname as dirname17, join as join22 } from "path";
5917
+ import { dirname as dirname17, join as join23 } from "path";
5772
5918
 
5773
5919
  // src/commands/transcript/summarise/processStagedFile/validateStagedContent.ts
5774
5920
  import chalk51 from "chalk";
@@ -5797,9 +5943,9 @@ function validateStagedContent(filename, content) {
5797
5943
  }
5798
5944
 
5799
5945
  // src/commands/transcript/summarise/processStagedFile/index.ts
5800
- var STAGING_DIR = join22(process.cwd(), ".assist", "transcript");
5946
+ var STAGING_DIR = join23(process.cwd(), ".assist", "transcript");
5801
5947
  function processStagedFile() {
5802
- if (!existsSync24(STAGING_DIR)) {
5948
+ if (!existsSync25(STAGING_DIR)) {
5803
5949
  return false;
5804
5950
  }
5805
5951
  const stagedFiles = findMdFilesRecursive(STAGING_DIR);
@@ -5808,7 +5954,7 @@ function processStagedFile() {
5808
5954
  }
5809
5955
  const { transcriptsDir, summaryDir } = getTranscriptConfig();
5810
5956
  const stagedFile = stagedFiles[0];
5811
- const content = readFileSync19(stagedFile.absolutePath, "utf-8");
5957
+ const content = readFileSync20(stagedFile.absolutePath, "utf-8");
5812
5958
  validateStagedContent(stagedFile.filename, content);
5813
5959
  const stagedBaseName = getTranscriptBaseName(stagedFile.filename);
5814
5960
  const transcriptFiles = findMdFilesRecursive(transcriptsDir);
@@ -5821,9 +5967,9 @@ function processStagedFile() {
5821
5967
  );
5822
5968
  process.exit(1);
5823
5969
  }
5824
- const destPath = join22(summaryDir, matchingTranscript.relativePath);
5970
+ const destPath = join23(summaryDir, matchingTranscript.relativePath);
5825
5971
  const destDir = dirname17(destPath);
5826
- if (!existsSync24(destDir)) {
5972
+ if (!existsSync25(destDir)) {
5827
5973
  mkdirSync7(destDir, { recursive: true });
5828
5974
  }
5829
5975
  renameSync2(stagedFile.absolutePath, destPath);
@@ -5837,7 +5983,7 @@ function processStagedFile() {
5837
5983
  // src/commands/transcript/summarise/index.ts
5838
5984
  function buildRelativeKey(relativePath, baseName) {
5839
5985
  const relDir = dirname18(relativePath);
5840
- return relDir === "." ? baseName : join23(relDir, baseName);
5986
+ return relDir === "." ? baseName : join24(relDir, baseName);
5841
5987
  }
5842
5988
  function buildSummaryIndex(summaryDir) {
5843
5989
  const summaryFiles = findMdFilesRecursive(summaryDir);
@@ -5850,7 +5996,7 @@ function buildSummaryIndex(summaryDir) {
5850
5996
  function summarise() {
5851
5997
  processStagedFile();
5852
5998
  const { transcriptsDir, summaryDir } = getTranscriptConfig();
5853
- if (!existsSync25(transcriptsDir)) {
5999
+ if (!existsSync26(transcriptsDir)) {
5854
6000
  console.log("No transcripts directory found.");
5855
6001
  return;
5856
6002
  }
@@ -5871,8 +6017,8 @@ function summarise() {
5871
6017
  }
5872
6018
  const next2 = missing[0];
5873
6019
  const outputFilename = `${getTranscriptBaseName(next2.filename)}.md`;
5874
- const outputPath = join23(STAGING_DIR, outputFilename);
5875
- const summaryFileDir = join23(summaryDir, dirname18(next2.relativePath));
6020
+ const outputPath = join24(STAGING_DIR, outputFilename);
6021
+ const summaryFileDir = join24(summaryDir, dirname18(next2.relativePath));
5876
6022
  const relativeTranscriptPath = encodeURI(
5877
6023
  relative2(summaryFileDir, next2.absolutePath).replace(/\\/g, "/")
5878
6024
  );
@@ -5918,50 +6064,50 @@ function registerVerify(program2) {
5918
6064
 
5919
6065
  // src/commands/voice/devices.ts
5920
6066
  import { spawnSync as spawnSync3 } from "child_process";
5921
- import { join as join25 } from "path";
6067
+ import { join as join26 } from "path";
5922
6068
 
5923
6069
  // src/commands/voice/shared.ts
5924
- import { homedir as homedir5 } from "os";
5925
- import { dirname as dirname19, join as join24 } from "path";
6070
+ import { homedir as homedir6 } from "os";
6071
+ import { dirname as dirname19, join as join25 } from "path";
5926
6072
  import { fileURLToPath as fileURLToPath6 } from "url";
5927
6073
  var __dirname7 = dirname19(fileURLToPath6(import.meta.url));
5928
- var VOICE_DIR = join24(homedir5(), ".assist", "voice");
6074
+ var VOICE_DIR = join25(homedir6(), ".assist", "voice");
5929
6075
  var voicePaths = {
5930
6076
  dir: VOICE_DIR,
5931
- pid: join24(VOICE_DIR, "voice.pid"),
5932
- log: join24(VOICE_DIR, "voice.log"),
5933
- venv: join24(VOICE_DIR, ".venv"),
5934
- lock: join24(VOICE_DIR, "voice.lock")
6077
+ pid: join25(VOICE_DIR, "voice.pid"),
6078
+ log: join25(VOICE_DIR, "voice.log"),
6079
+ venv: join25(VOICE_DIR, ".venv"),
6080
+ lock: join25(VOICE_DIR, "voice.lock")
5935
6081
  };
5936
6082
  function getPythonDir() {
5937
- return join24(__dirname7, "commands", "voice", "python");
6083
+ return join25(__dirname7, "commands", "voice", "python");
5938
6084
  }
5939
6085
  function getVenvPython() {
5940
- return process.platform === "win32" ? join24(voicePaths.venv, "Scripts", "python.exe") : join24(voicePaths.venv, "bin", "python");
6086
+ return process.platform === "win32" ? join25(voicePaths.venv, "Scripts", "python.exe") : join25(voicePaths.venv, "bin", "python");
5941
6087
  }
5942
6088
  function getLockDir() {
5943
6089
  const config = loadConfig();
5944
6090
  return config.voice?.lockDir ?? VOICE_DIR;
5945
6091
  }
5946
6092
  function getLockFile() {
5947
- return join24(getLockDir(), "voice.lock");
6093
+ return join25(getLockDir(), "voice.lock");
5948
6094
  }
5949
6095
 
5950
6096
  // src/commands/voice/devices.ts
5951
6097
  function devices() {
5952
- const script = join25(getPythonDir(), "list_devices.py");
6098
+ const script = join26(getPythonDir(), "list_devices.py");
5953
6099
  spawnSync3(getVenvPython(), [script], { stdio: "inherit" });
5954
6100
  }
5955
6101
 
5956
6102
  // src/commands/voice/logs.ts
5957
- import { existsSync as existsSync26, readFileSync as readFileSync20 } from "fs";
6103
+ import { existsSync as existsSync27, readFileSync as readFileSync21 } from "fs";
5958
6104
  function logs(options2) {
5959
- if (!existsSync26(voicePaths.log)) {
6105
+ if (!existsSync27(voicePaths.log)) {
5960
6106
  console.log("No voice log file found");
5961
6107
  return;
5962
6108
  }
5963
6109
  const count = Number.parseInt(options2.lines ?? "150", 10);
5964
- const content = readFileSync20(voicePaths.log, "utf-8").trim();
6110
+ const content = readFileSync21(voicePaths.log, "utf-8").trim();
5965
6111
  if (!content) {
5966
6112
  console.log("Voice log is empty");
5967
6113
  return;
@@ -5984,12 +6130,12 @@ function logs(options2) {
5984
6130
  // src/commands/voice/setup.ts
5985
6131
  import { spawnSync as spawnSync4 } from "child_process";
5986
6132
  import { mkdirSync as mkdirSync9 } from "fs";
5987
- import { join as join27 } from "path";
6133
+ import { join as join28 } from "path";
5988
6134
 
5989
6135
  // src/commands/voice/checkLockFile.ts
5990
6136
  import { execSync as execSync27 } from "child_process";
5991
- import { existsSync as existsSync27, mkdirSync as mkdirSync8, readFileSync as readFileSync21, writeFileSync as writeFileSync20 } from "fs";
5992
- import { join as join26 } from "path";
6137
+ import { existsSync as existsSync28, mkdirSync as mkdirSync8, readFileSync as readFileSync22, writeFileSync as writeFileSync20 } from "fs";
6138
+ import { join as join27 } from "path";
5993
6139
  function isProcessAlive(pid) {
5994
6140
  try {
5995
6141
  process.kill(pid, 0);
@@ -6000,9 +6146,9 @@ function isProcessAlive(pid) {
6000
6146
  }
6001
6147
  function checkLockFile() {
6002
6148
  const lockFile = getLockFile();
6003
- if (!existsSync27(lockFile)) return;
6149
+ if (!existsSync28(lockFile)) return;
6004
6150
  try {
6005
- const lock = JSON.parse(readFileSync21(lockFile, "utf-8"));
6151
+ const lock = JSON.parse(readFileSync22(lockFile, "utf-8"));
6006
6152
  if (lock.pid && isProcessAlive(lock.pid)) {
6007
6153
  console.error(
6008
6154
  `Voice daemon already running (PID ${lock.pid}, env: ${lock.env}). Stop it first with: assist voice stop`
@@ -6013,7 +6159,7 @@ function checkLockFile() {
6013
6159
  }
6014
6160
  }
6015
6161
  function bootstrapVenv() {
6016
- if (existsSync27(getVenvPython())) return;
6162
+ if (existsSync28(getVenvPython())) return;
6017
6163
  console.log("Setting up Python environment...");
6018
6164
  const pythonDir = getPythonDir();
6019
6165
  execSync27(
@@ -6026,7 +6172,7 @@ function bootstrapVenv() {
6026
6172
  }
6027
6173
  function writeLockFile(pid) {
6028
6174
  const lockFile = getLockFile();
6029
- mkdirSync8(join26(lockFile, ".."), { recursive: true });
6175
+ mkdirSync8(join27(lockFile, ".."), { recursive: true });
6030
6176
  writeFileSync20(
6031
6177
  lockFile,
6032
6178
  JSON.stringify({
@@ -6042,7 +6188,7 @@ function setup() {
6042
6188
  mkdirSync9(voicePaths.dir, { recursive: true });
6043
6189
  bootstrapVenv();
6044
6190
  console.log("\nDownloading models...\n");
6045
- const script = join27(getPythonDir(), "setup_models.py");
6191
+ const script = join28(getPythonDir(), "setup_models.py");
6046
6192
  const result = spawnSync4(getVenvPython(), [script], {
6047
6193
  stdio: "inherit",
6048
6194
  env: { ...process.env, VOICE_LOG_FILE: voicePaths.log }
@@ -6056,7 +6202,7 @@ function setup() {
6056
6202
  // src/commands/voice/start.ts
6057
6203
  import { spawn as spawn4 } from "child_process";
6058
6204
  import { mkdirSync as mkdirSync10, writeFileSync as writeFileSync21 } from "fs";
6059
- import { join as join28 } from "path";
6205
+ import { join as join29 } from "path";
6060
6206
 
6061
6207
  // src/commands/voice/buildDaemonEnv.ts
6062
6208
  function buildDaemonEnv(options2) {
@@ -6094,7 +6240,7 @@ function start2(options2) {
6094
6240
  bootstrapVenv();
6095
6241
  const debug = options2.debug || options2.foreground || process.platform === "win32";
6096
6242
  const env = buildDaemonEnv({ debug });
6097
- const script = join28(getPythonDir(), "voice_daemon.py");
6243
+ const script = join29(getPythonDir(), "voice_daemon.py");
6098
6244
  const python = getVenvPython();
6099
6245
  if (options2.foreground) {
6100
6246
  spawnForeground(python, script, env);
@@ -6104,7 +6250,7 @@ function start2(options2) {
6104
6250
  }
6105
6251
 
6106
6252
  // src/commands/voice/status.ts
6107
- import { existsSync as existsSync28, readFileSync as readFileSync22 } from "fs";
6253
+ import { existsSync as existsSync29, readFileSync as readFileSync23 } from "fs";
6108
6254
  function isProcessAlive2(pid) {
6109
6255
  try {
6110
6256
  process.kill(pid, 0);
@@ -6114,16 +6260,16 @@ function isProcessAlive2(pid) {
6114
6260
  }
6115
6261
  }
6116
6262
  function readRecentLogs(count) {
6117
- if (!existsSync28(voicePaths.log)) return [];
6118
- const lines = readFileSync22(voicePaths.log, "utf-8").trim().split("\n");
6263
+ if (!existsSync29(voicePaths.log)) return [];
6264
+ const lines = readFileSync23(voicePaths.log, "utf-8").trim().split("\n");
6119
6265
  return lines.slice(-count);
6120
6266
  }
6121
6267
  function status() {
6122
- if (!existsSync28(voicePaths.pid)) {
6268
+ if (!existsSync29(voicePaths.pid)) {
6123
6269
  console.log("Voice daemon: not running (no PID file)");
6124
6270
  return;
6125
6271
  }
6126
- const pid = Number.parseInt(readFileSync22(voicePaths.pid, "utf-8").trim(), 10);
6272
+ const pid = Number.parseInt(readFileSync23(voicePaths.pid, "utf-8").trim(), 10);
6127
6273
  const alive = isProcessAlive2(pid);
6128
6274
  console.log(`Voice daemon: ${alive ? "running" : "dead"} (PID ${pid})`);
6129
6275
  const recent = readRecentLogs(5);
@@ -6142,13 +6288,13 @@ function status() {
6142
6288
  }
6143
6289
 
6144
6290
  // src/commands/voice/stop.ts
6145
- import { existsSync as existsSync29, readFileSync as readFileSync23, unlinkSync as unlinkSync7 } from "fs";
6291
+ import { existsSync as existsSync30, readFileSync as readFileSync24, unlinkSync as unlinkSync7 } from "fs";
6146
6292
  function stop() {
6147
- if (!existsSync29(voicePaths.pid)) {
6293
+ if (!existsSync30(voicePaths.pid)) {
6148
6294
  console.log("Voice daemon is not running (no PID file)");
6149
6295
  return;
6150
6296
  }
6151
- const pid = Number.parseInt(readFileSync23(voicePaths.pid, "utf-8").trim(), 10);
6297
+ const pid = Number.parseInt(readFileSync24(voicePaths.pid, "utf-8").trim(), 10);
6152
6298
  try {
6153
6299
  process.kill(pid, "SIGTERM");
6154
6300
  console.log(`Sent SIGTERM to voice daemon (PID ${pid})`);
@@ -6161,7 +6307,7 @@ function stop() {
6161
6307
  }
6162
6308
  try {
6163
6309
  const lockFile = getLockFile();
6164
- if (existsSync29(lockFile)) unlinkSync7(lockFile);
6310
+ if (existsSync30(lockFile)) unlinkSync7(lockFile);
6165
6311
  } catch {
6166
6312
  }
6167
6313
  console.log("Voice daemon stopped");
@@ -6245,7 +6391,7 @@ function extractCode(url, expectedState) {
6245
6391
  return code;
6246
6392
  }
6247
6393
  function waitForCallback(port, expectedState) {
6248
- return new Promise((resolve5, reject) => {
6394
+ return new Promise((resolve6, reject) => {
6249
6395
  const timeout = setTimeout(() => {
6250
6396
  server.close();
6251
6397
  reject(new Error("Authorization timed out after 120 seconds"));
@@ -6262,7 +6408,7 @@ function waitForCallback(port, expectedState) {
6262
6408
  const code = extractCode(url, expectedState);
6263
6409
  respondHtml(res, 200, "Authorization successful!");
6264
6410
  server.close();
6265
- resolve5(code);
6411
+ resolve6(code);
6266
6412
  } catch (err) {
6267
6413
  respondHtml(res, 400, err.message);
6268
6414
  server.close();
@@ -6392,7 +6538,7 @@ import { spawn as spawn5 } from "child_process";
6392
6538
 
6393
6539
  // src/commands/run/add.ts
6394
6540
  import { mkdirSync as mkdirSync11, writeFileSync as writeFileSync22 } from "fs";
6395
- import { join as join29 } from "path";
6541
+ import { join as join30 } from "path";
6396
6542
  function findAddIndex() {
6397
6543
  const addIndex = process.argv.indexOf("add");
6398
6544
  if (addIndex === -1 || addIndex + 2 >= process.argv.length) return -1;
@@ -6446,7 +6592,7 @@ function saveNewRunConfig(name, command, args) {
6446
6592
  saveConfig(config);
6447
6593
  }
6448
6594
  function createCommandFile(name) {
6449
- const dir = join29(".claude", "commands");
6595
+ const dir = join30(".claude", "commands");
6450
6596
  mkdirSync11(dir, { recursive: true });
6451
6597
  const content = `---
6452
6598
  description: Run ${name}
@@ -6454,7 +6600,7 @@ description: Run ${name}
6454
6600
 
6455
6601
  Run \`assist run ${name} $ARGUMENTS 2>&1\`.
6456
6602
  `;
6457
- const filePath = join29(dir, `${name}.md`);
6603
+ const filePath = join30(dir, `${name}.md`);
6458
6604
  writeFileSync22(filePath, content);
6459
6605
  console.log(`Created command file: ${filePath}`);
6460
6606
  }
@@ -6706,7 +6852,6 @@ program.command("notify").description(
6706
6852
  "Show notification from Claude Code hook (reads JSON from stdin)"
6707
6853
  ).action(notify);
6708
6854
  program.command("update").description("Update assist to the latest version and sync commands").action(update);
6709
- registerPermitCliReads(program);
6710
6855
  registerCliHook(program);
6711
6856
  registerPrs(program);
6712
6857
  registerRoam(program);