@staff0rd/assist 0.96.0 → 0.97.0

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