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