pi-lens 2.2.9 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +198 -0
- package/README.md +709 -519
- package/clients/__tests__/file-time.test.js +216 -0
- package/clients/__tests__/file-time.test.ts +276 -0
- package/clients/__tests__/format-service.test.js +245 -0
- package/clients/__tests__/format-service.test.ts +339 -0
- package/clients/__tests__/formatters.test.js +271 -0
- package/clients/__tests__/formatters.test.ts +401 -0
- package/clients/amain-types.js +164 -0
- package/clients/amain-types.ts +165 -0
- package/clients/architect-client.js +56 -12
- package/clients/architect-client.ts +81 -16
- package/clients/ast-grep-client.js +2 -2
- package/clients/ast-grep-client.ts +14 -39
- package/clients/ast-grep-parser.ts +1 -1
- package/clients/ast-grep-rule-manager.js +8 -0
- package/clients/ast-grep-rule-manager.ts +10 -1
- package/clients/ast-grep-types.js +9 -0
- package/clients/ast-grep-types.ts +106 -0
- package/clients/auto-loop.js +10 -0
- package/clients/auto-loop.ts +14 -1
- package/clients/biome-client.js +81 -19
- package/clients/biome-client.ts +103 -22
- package/clients/bus/bus.js +191 -0
- package/clients/bus/bus.ts +251 -0
- package/clients/bus/events.js +214 -0
- package/clients/bus/events.ts +279 -0
- package/clients/bus/index.js +8 -0
- package/clients/bus/index.ts +9 -0
- package/clients/bus/integration.js +158 -0
- package/clients/bus/integration.ts +214 -0
- package/clients/complexity-client.js +13 -7
- package/clients/complexity-client.ts +13 -7
- package/clients/config-validator.js +465 -0
- package/clients/config-validator.ts +558 -0
- package/clients/dependency-checker.js +4 -10
- package/clients/dependency-checker.ts +4 -10
- package/clients/dispatch/__tests__/autofix-integration.test.js +245 -0
- package/clients/dispatch/__tests__/autofix-integration.test.ts +300 -0
- package/clients/dispatch/__tests__/runner-registration.test.js +236 -0
- package/clients/dispatch/__tests__/runner-registration.test.ts +282 -0
- package/clients/dispatch/bus-dispatcher.js +177 -0
- package/clients/dispatch/bus-dispatcher.ts +251 -0
- package/clients/dispatch/dispatcher.edge.test.js +82 -0
- package/clients/dispatch/dispatcher.edge.test.ts +100 -0
- package/clients/dispatch/dispatcher.format.test.js +46 -0
- package/clients/dispatch/dispatcher.format.test.ts +58 -0
- package/clients/dispatch/dispatcher.inline.test.js +74 -0
- package/clients/dispatch/dispatcher.inline.test.ts +93 -0
- package/clients/dispatch/dispatcher.js +19 -53
- package/clients/dispatch/dispatcher.ts +20 -67
- package/clients/dispatch/plan.js +9 -4
- package/clients/dispatch/plan.ts +9 -4
- package/clients/dispatch/runners/architect.js +21 -7
- package/clients/dispatch/runners/architect.test.js +138 -0
- package/clients/dispatch/runners/architect.test.ts +162 -0
- package/clients/dispatch/runners/architect.ts +22 -7
- package/clients/dispatch/runners/ast-grep-napi.js +462 -0
- package/clients/dispatch/runners/ast-grep-napi.test.js +111 -0
- package/clients/dispatch/runners/ast-grep-napi.test.ts +133 -0
- package/clients/dispatch/runners/ast-grep-napi.ts +506 -0
- package/clients/dispatch/runners/ast-grep.js +62 -19
- package/clients/dispatch/runners/ast-grep.ts +70 -18
- package/clients/dispatch/runners/biome.js +29 -53
- package/clients/dispatch/runners/biome.ts +29 -63
- package/clients/dispatch/runners/config-validation.js +67 -0
- package/clients/dispatch/runners/config-validation.ts +82 -0
- package/clients/dispatch/runners/go-vet.js +4 -28
- package/clients/dispatch/runners/go-vet.ts +4 -32
- package/clients/dispatch/runners/index.js +30 -10
- package/clients/dispatch/runners/index.ts +30 -10
- package/clients/dispatch/runners/oxlint.js +141 -0
- package/clients/dispatch/runners/oxlint.test.js +230 -0
- package/clients/dispatch/runners/oxlint.test.ts +303 -0
- package/clients/dispatch/runners/oxlint.ts +175 -0
- package/clients/dispatch/runners/pyright.js +40 -70
- package/clients/dispatch/runners/pyright.test.js +16 -2
- package/clients/dispatch/runners/pyright.test.ts +14 -2
- package/clients/dispatch/runners/pyright.ts +48 -91
- package/clients/dispatch/runners/python-slop.js +97 -0
- package/clients/dispatch/runners/python-slop.test.js +203 -0
- package/clients/dispatch/runners/python-slop.test.ts +298 -0
- package/clients/dispatch/runners/python-slop.ts +124 -0
- package/clients/dispatch/runners/ruff.js +18 -71
- package/clients/dispatch/runners/ruff.ts +19 -79
- package/clients/dispatch/runners/rust-clippy.js +28 -32
- package/clients/dispatch/runners/rust-clippy.ts +29 -31
- package/clients/dispatch/runners/scan_codebase.test.js +89 -0
- package/clients/dispatch/runners/scan_codebase.test.ts +105 -0
- package/clients/dispatch/runners/shellcheck.js +147 -0
- package/clients/dispatch/runners/shellcheck.test.js +98 -0
- package/clients/dispatch/runners/shellcheck.test.ts +129 -0
- package/clients/dispatch/runners/shellcheck.ts +188 -0
- package/clients/dispatch/runners/similarity.js +230 -0
- package/clients/dispatch/runners/similarity.ts +339 -0
- package/clients/dispatch/runners/spellcheck.js +106 -0
- package/clients/dispatch/runners/spellcheck.test.js +158 -0
- package/clients/dispatch/runners/spellcheck.test.ts +214 -0
- package/clients/dispatch/runners/spellcheck.ts +136 -0
- package/clients/dispatch/runners/tree-sitter.js +107 -0
- package/clients/dispatch/runners/tree-sitter.ts +135 -0
- package/clients/dispatch/runners/ts-lsp.js +104 -33
- package/clients/dispatch/runners/ts-lsp.ts +120 -38
- package/clients/dispatch/runners/ts-slop.js +113 -0
- package/clients/dispatch/runners/ts-slop.test.js +180 -0
- package/clients/dispatch/runners/ts-slop.test.ts +230 -0
- package/clients/dispatch/runners/ts-slop.ts +142 -0
- package/clients/dispatch/runners/utils/diagnostic-parsers.js +134 -0
- package/clients/dispatch/runners/utils/diagnostic-parsers.ts +186 -0
- package/clients/dispatch/runners/utils/runner-helpers.js +115 -0
- package/clients/dispatch/runners/utils/runner-helpers.ts +167 -0
- package/clients/dispatch/runners/utils.js +2 -4
- package/clients/dispatch/runners/utils.ts +2 -4
- package/clients/dispatch/types.ts +1 -1
- package/clients/dispatch/utils/format-utils.js +49 -0
- package/clients/dispatch/utils/format-utils.ts +60 -0
- package/clients/dogfood.test.js +201 -0
- package/clients/dogfood.test.ts +269 -0
- package/clients/file-time.js +152 -0
- package/clients/file-time.ts +208 -0
- package/clients/file-utils.js +40 -0
- package/clients/file-utils.ts +44 -0
- package/clients/fix-scanners.js +10 -20
- package/clients/fix-scanners.ts +10 -22
- package/clients/format-service.js +172 -0
- package/clients/format-service.ts +254 -0
- package/clients/formatters.js +435 -0
- package/clients/formatters.ts +508 -0
- package/clients/go-client.js +5 -14
- package/clients/go-client.ts +5 -13
- package/clients/installer/index.js +356 -0
- package/clients/installer/index.ts +426 -0
- package/clients/jscpd-client.js +11 -9
- package/clients/jscpd-client.ts +12 -8
- package/clients/knip-client.js +3 -7
- package/clients/knip-client.ts +3 -6
- package/clients/lsp/__tests__/client.test.js +325 -0
- package/clients/lsp/__tests__/client.test.ts +434 -0
- package/clients/lsp/__tests__/config.test.js +166 -0
- package/clients/lsp/__tests__/config.test.ts +209 -0
- package/clients/lsp/__tests__/error-recovery.test.js +213 -0
- package/clients/lsp/__tests__/error-recovery.test.ts +279 -0
- package/clients/lsp/__tests__/integration.test.js +127 -0
- package/clients/lsp/__tests__/integration.test.ts +160 -0
- package/clients/lsp/__tests__/launch.test.js +260 -0
- package/clients/lsp/__tests__/launch.test.ts +329 -0
- package/clients/lsp/__tests__/server.test.js +259 -0
- package/clients/lsp/__tests__/server.test.ts +332 -0
- package/clients/lsp/__tests__/service.test.js +417 -0
- package/clients/lsp/__tests__/service.test.ts +499 -0
- package/clients/lsp/client.js +235 -0
- package/clients/lsp/client.ts +328 -0
- package/clients/lsp/config.js +115 -0
- package/clients/lsp/config.ts +149 -0
- package/clients/lsp/index.js +222 -0
- package/clients/lsp/index.ts +280 -0
- package/clients/lsp/installer/index.js +391 -0
- package/clients/lsp/interactive-install.js +210 -0
- package/clients/lsp/interactive-install.ts +251 -0
- package/clients/lsp/language.js +170 -0
- package/clients/lsp/language.ts +216 -0
- package/clients/lsp/launch.js +174 -0
- package/clients/lsp/launch.ts +240 -0
- package/clients/lsp/lsp/launch.js +116 -0
- package/clients/lsp/lsp/server.js +532 -0
- package/clients/lsp/lsp-index.js +10 -0
- package/clients/lsp/lsp-index.ts +11 -0
- package/clients/lsp/path-utils.js +48 -0
- package/clients/lsp/path-utils.ts +52 -0
- package/clients/lsp/server.js +615 -0
- package/clients/lsp/server.ts +800 -0
- package/clients/lsp/test-py-spawn/requirements.txt +1 -0
- package/clients/lsp/test-py-spawn/test.py +3 -0
- package/clients/lsp/test-py-svc/requirements.txt +1 -0
- package/clients/lsp/test-py-svc/test.py +3 -0
- package/clients/lsp/test-python-project/requirements.txt +1 -0
- package/clients/lsp/test-python-project/test.py +5 -0
- package/clients/metrics-history.js +2 -2
- package/clients/metrics-history.ts +2 -2
- package/clients/production-readiness.js +522 -0
- package/clients/production-readiness.ts +556 -0
- package/clients/project-index.js +255 -0
- package/clients/project-index.ts +383 -0
- package/clients/project-metadata.js +531 -0
- package/clients/project-metadata.ts +624 -0
- package/clients/ruff-client.js +56 -16
- package/clients/ruff-client.ts +72 -15
- package/clients/runner-tracker.js +152 -0
- package/clients/runner-tracker.ts +213 -0
- package/clients/rust-client.js +4 -11
- package/clients/rust-client.ts +5 -11
- package/clients/safe-spawn.js +96 -0
- package/clients/safe-spawn.ts +128 -0
- package/clients/scan-architectural-debt.js +3 -6
- package/clients/scan-architectural-debt.ts +3 -6
- package/clients/scan-utils.js +5 -20
- package/clients/scan-utils.ts +5 -29
- package/clients/secrets-scanner.js +3 -17
- package/clients/secrets-scanner.ts +4 -20
- package/clients/services/__tests__/effect-integration.test.js +86 -0
- package/clients/services/__tests__/effect-integration.test.ts +111 -0
- package/clients/services/effect-integration.js +194 -0
- package/clients/services/effect-integration.ts +268 -0
- package/clients/services/index.js +7 -0
- package/clients/services/index.ts +8 -0
- package/clients/services/runner-service.js +105 -0
- package/clients/services/runner-service.ts +179 -0
- package/clients/sg-runner.js +87 -13
- package/clients/sg-runner.ts +97 -13
- package/clients/state-matrix.js +160 -0
- package/clients/state-matrix.ts +202 -0
- package/clients/subprocess-client.js +10 -9
- package/clients/subprocess-client.ts +10 -8
- package/clients/test-runner-client.js +3 -7
- package/clients/test-runner-client.ts +3 -6
- package/clients/tool-availability.js +4 -10
- package/clients/tool-availability.ts +4 -9
- package/clients/tree-sitter-client.js +564 -0
- package/clients/tree-sitter-client.ts +797 -0
- package/clients/tree-sitter-query-loader.js +355 -0
- package/clients/tree-sitter-query-loader.ts +425 -0
- package/clients/type-coverage-client.js +3 -7
- package/clients/type-coverage-client.ts +3 -6
- package/clients/typescript-client.codefix.test.js +157 -0
- package/clients/typescript-client.codefix.test.ts +186 -0
- package/clients/typescript-client.js +43 -0
- package/clients/typescript-client.ts +98 -0
- package/commands/booboo.js +799 -219
- package/commands/booboo.ts +1004 -225
- package/commands/clients/ast-grep-client.js +250 -0
- package/commands/clients/ast-grep-parser.js +86 -0
- package/commands/clients/ast-grep-rule-manager.js +91 -0
- package/commands/clients/ast-grep-types.js +9 -0
- package/commands/clients/biome-client.js +380 -0
- package/commands/clients/complexity-client.js +667 -0
- package/commands/clients/file-kinds.js +177 -0
- package/commands/clients/file-utils.js +40 -0
- package/commands/clients/jscpd-client.js +169 -0
- package/commands/clients/knip-client.js +211 -0
- package/commands/clients/ruff-client.js +297 -0
- package/commands/clients/safe-spawn.js +88 -0
- package/commands/clients/scan-utils.js +83 -0
- package/commands/clients/sg-runner.js +190 -0
- package/commands/clients/types.js +11 -0
- package/commands/clients/typescript-client.js +505 -0
- package/commands/fix-from-booboo.js +398 -0
- package/commands/fix-from-booboo.ts +485 -0
- package/commands/fix-simplified.js +618 -0
- package/commands/fix-simplified.ts +768 -0
- package/commands/rate.js +10 -14
- package/commands/rate.ts +9 -16
- package/default-architect.yaml +59 -15
- package/index.ts +342 -429
- package/package.json +16 -3
- package/rules/ast-grep-rules/rules/empty-catch.yml +38 -13
- package/rules/ast-grep-rules/rules/no-array-constructor.yml +1 -0
- package/rules/ast-grep-rules/rules/no-debugger.yml +2 -0
- package/rules/python-slop-rules/.sgconfig.yml +4 -0
- package/rules/python-slop-rules/rules/slop-rules.yml +647 -0
- package/rules/tree-sitter-queries/python/bare-except.yml +54 -0
- package/rules/tree-sitter-queries/python/eval-exec.yml +50 -0
- package/rules/tree-sitter-queries/python/is-vs-equals.yml +60 -0
- package/rules/tree-sitter-queries/python/mutable-default-arg.yml +57 -0
- package/rules/tree-sitter-queries/python/unreachable-except.yml +60 -0
- package/rules/tree-sitter-queries/python/wildcard-import.yml +46 -0
- package/rules/tree-sitter-queries/tsx/dangerously-set-inner-html.yml +63 -0
- package/rules/tree-sitter-queries/typescript/await-in-loop.yml +56 -0
- package/rules/tree-sitter-queries/typescript/console-statement.yml +47 -0
- package/rules/tree-sitter-queries/typescript/debugger.yml +47 -0
- package/rules/tree-sitter-queries/typescript/deep-nesting.yml +117 -0
- package/rules/tree-sitter-queries/typescript/deep-promise-chain.yml +73 -0
- package/rules/tree-sitter-queries/typescript/empty-catch.yml +64 -0
- package/rules/tree-sitter-queries/typescript/eval.yml +48 -0
- package/rules/tree-sitter-queries/typescript/hardcoded-secrets.yml +78 -0
- package/rules/tree-sitter-queries/typescript/long-parameter-list.yml +62 -0
- package/rules/tree-sitter-queries/typescript/mixed-async-styles.yml +49 -0
- package/rules/tree-sitter-queries/typescript/nested-ternary.yml +45 -0
- package/rules/ts-slop-rules/.sgconfig.yml +4 -0
- package/rules/ts-slop-rules/rules/in-correct-optional-input-type.yml +10 -0
- package/rules/ts-slop-rules/rules/jwt-no-verify.yml +13 -0
- package/rules/ts-slop-rules/rules/no-architecture-violation.yml +10 -0
- package/rules/ts-slop-rules/rules/no-case-declarations.yml +10 -0
- package/rules/ts-slop-rules/rules/no-dangerously-set-inner-html.yml +10 -0
- package/rules/ts-slop-rules/rules/no-debugger.yml +10 -0
- package/rules/ts-slop-rules/rules/no-dupe-args.yml +10 -0
- package/rules/ts-slop-rules/rules/no-dupe-class-members.yml +10 -0
- package/rules/ts-slop-rules/rules/no-dupe-keys.yml +10 -0
- package/rules/ts-slop-rules/rules/no-eval.yml +13 -0
- package/rules/ts-slop-rules/rules/no-hardcoded-secrets.yml +12 -0
- package/rules/ts-slop-rules/rules/no-implied-eval.yml +12 -0
- package/rules/ts-slop-rules/rules/no-inner-html.yml +13 -0
- package/rules/ts-slop-rules/rules/no-javascript-url.yml +10 -0
- package/rules/ts-slop-rules/rules/no-mutable-default.yml +10 -0
- package/rules/ts-slop-rules/rules/no-nested-links.yml +12 -0
- package/rules/ts-slop-rules/rules/no-new-symbol.yml +10 -0
- package/rules/ts-slop-rules/rules/no-new-wrappers.yml +13 -0
- package/rules/ts-slop-rules/rules/no-open-redirect.yml +16 -0
- package/rules/ts-slop-rules/rules/slop-rules.yml +455 -0
- package/rules/ts-slop-rules/rules/weak-rsa-key.yml +12 -0
- package/skills/ast-grep/SKILL.md +182 -0
- package/clients/dispatch/runners/secrets.js +0 -109
- package/commands/fix.js +0 -244
- package/commands/fix.ts +0 -373
- package/rules/ast-grep-rules/rules/no-lonely-if.yml +0 -13
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for shellcheck runner
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as fs from "node:fs";
|
|
6
|
+
import { createRequire } from "node:module";
|
|
7
|
+
import * as path from "node:path";
|
|
8
|
+
import { describe, expect, it } from "vitest";
|
|
9
|
+
import type { DispatchContext } from "../types.js";
|
|
10
|
+
|
|
11
|
+
function createMockContext(filePath: string): DispatchContext {
|
|
12
|
+
return {
|
|
13
|
+
filePath,
|
|
14
|
+
cwd: process.cwd(),
|
|
15
|
+
kind: "shell" as any,
|
|
16
|
+
autofix: false,
|
|
17
|
+
deltaMode: false,
|
|
18
|
+
baselines: { get: () => [], add: () => {}, save: () => {} } as any,
|
|
19
|
+
pi: {} as any,
|
|
20
|
+
hasTool: async () => false,
|
|
21
|
+
log: () => {},
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Helper for safe file cleanup
|
|
26
|
+
function safeUnlink(filePath: string): void {
|
|
27
|
+
try {
|
|
28
|
+
if (fs.existsSync(filePath)) {
|
|
29
|
+
fs.unlinkSync(filePath);
|
|
30
|
+
}
|
|
31
|
+
} catch {
|
|
32
|
+
// Ignore cleanup errors on Windows
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
describe("shellcheck runner", () => {
|
|
37
|
+
const require = createRequire(import.meta.url);
|
|
38
|
+
|
|
39
|
+
it("should have correct runner definition", async () => {
|
|
40
|
+
const shellcheckModule = await import("./shellcheck.js");
|
|
41
|
+
const runner = shellcheckModule.default;
|
|
42
|
+
|
|
43
|
+
expect(runner.id).toBe("shellcheck");
|
|
44
|
+
expect(runner.appliesTo).toEqual(["shell"]);
|
|
45
|
+
expect(runner.priority).toBe(20);
|
|
46
|
+
expect(runner.enabledByDefault).toBe(true);
|
|
47
|
+
expect(runner.skipTestFiles).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("should detect shellcheck availability", () => {
|
|
51
|
+
const { spawnSync } =
|
|
52
|
+
require("node:child_process") as typeof import("node:child_process");
|
|
53
|
+
const result = spawnSync("shellcheck", ["--version"], {
|
|
54
|
+
encoding: "utf-8",
|
|
55
|
+
timeout: 10000,
|
|
56
|
+
shell: true,
|
|
57
|
+
});
|
|
58
|
+
expect(
|
|
59
|
+
result.error || result.status !== 0 ? "not available" : "available",
|
|
60
|
+
).toBeTruthy();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("should detect undefined variable", async () => {
|
|
64
|
+
const tmpFile = path.join(
|
|
65
|
+
process.env.TEMP || "/tmp",
|
|
66
|
+
`shellcheck_test_${Date.now()}.sh`,
|
|
67
|
+
);
|
|
68
|
+
fs.writeFileSync(
|
|
69
|
+
tmpFile,
|
|
70
|
+
["#!/bin/bash", "# Test script with issues", 'echo "\$UNDEFINED_VAR"', ""].join("\n"),
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
const shellcheckModule = await import("./shellcheck.js");
|
|
75
|
+
const runner = shellcheckModule.default;
|
|
76
|
+
const result = await runner.run(createMockContext(tmpFile));
|
|
77
|
+
|
|
78
|
+
if (result.status !== "skipped") {
|
|
79
|
+
expect(result.diagnostics.length).toBeGreaterThanOrEqual(1);
|
|
80
|
+
expect(
|
|
81
|
+
result.diagnostics.some(
|
|
82
|
+
(d) =>
|
|
83
|
+
d.tool === "shellcheck" &&
|
|
84
|
+
(d.message.includes("undefined") ||
|
|
85
|
+
d.message.includes("SC2154")),
|
|
86
|
+
),
|
|
87
|
+
).toBe(true);
|
|
88
|
+
}
|
|
89
|
+
} finally {
|
|
90
|
+
safeUnlink(tmpFile);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("should pass clean shell scripts", async () => {
|
|
95
|
+
const tmpFile = path.join(
|
|
96
|
+
process.env.TEMP || "/tmp",
|
|
97
|
+
`shellcheck_ok_${Date.now()}.sh`,
|
|
98
|
+
);
|
|
99
|
+
fs.writeFileSync(
|
|
100
|
+
tmpFile,
|
|
101
|
+
[
|
|
102
|
+
"#!/bin/bash",
|
|
103
|
+
"# Clean shell script",
|
|
104
|
+
"set -euo pipefail",
|
|
105
|
+
"",
|
|
106
|
+
"main() {",
|
|
107
|
+
' local name="\${1:-world}"',
|
|
108
|
+
' echo "Hello, \${name}!"',
|
|
109
|
+
"}",
|
|
110
|
+
"",
|
|
111
|
+
'main "\$@"',
|
|
112
|
+
"",
|
|
113
|
+
].join("\n"),
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
const shellcheckModule = await import("./shellcheck.js");
|
|
118
|
+
const runner = shellcheckModule.default;
|
|
119
|
+
const result = await runner.run(createMockContext(tmpFile));
|
|
120
|
+
|
|
121
|
+
if (result.status !== "skipped") {
|
|
122
|
+
expect(result.diagnostics.length).toBe(0);
|
|
123
|
+
expect(result.status).toBe("succeeded");
|
|
124
|
+
}
|
|
125
|
+
} finally {
|
|
126
|
+
safeUnlink(tmpFile);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
});
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shellcheck runner for dispatch system
|
|
3
|
+
*
|
|
4
|
+
* Industry-standard linter for shell scripts (bash, sh, zsh).
|
|
5
|
+
* Detects syntax errors, undefined variables, quoting issues, and best practices.
|
|
6
|
+
*
|
|
7
|
+
* Why shellcheck?
|
|
8
|
+
* - Industry standard (used in CI/CD everywhere)
|
|
9
|
+
* - Comprehensive checks (syntax, variables, quotes, best practices)
|
|
10
|
+
* - JSON output for easy parsing
|
|
11
|
+
* - Available on all platforms (apt, brew, cargo, etc.)
|
|
12
|
+
*
|
|
13
|
+
* Alternative considered: bash-language-server
|
|
14
|
+
* - LSP approach like OpenCode uses
|
|
15
|
+
* - Richer features but heavier
|
|
16
|
+
* - shellcheck is simpler and faster for basic linting
|
|
17
|
+
*
|
|
18
|
+
* Install: apt install shellcheck, brew install shellcheck, or cargo install shellcheck
|
|
19
|
+
*
|
|
20
|
+
* Config: .shellcheckrc (optional, zero-config works)
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { safeSpawn } from "../../safe-spawn.js";
|
|
24
|
+
import {
|
|
25
|
+
createAvailabilityChecker,
|
|
26
|
+
createConfigFinder,
|
|
27
|
+
} from "./utils/runner-helpers.js";
|
|
28
|
+
import type {
|
|
29
|
+
Diagnostic,
|
|
30
|
+
DispatchContext,
|
|
31
|
+
RunnerDefinition,
|
|
32
|
+
RunnerResult,
|
|
33
|
+
} from "../types.js";
|
|
34
|
+
|
|
35
|
+
const shellcheck = createAvailabilityChecker("shellcheck", ".exe");
|
|
36
|
+
const findShellcheckConfig = createConfigFinder(".shellcheckrc");
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Parse shellcheck JSON output
|
|
40
|
+
*
|
|
41
|
+
* Format: Array of check objects
|
|
42
|
+
* [{
|
|
43
|
+
* "file": "script.sh",
|
|
44
|
+
* "line": 10,
|
|
45
|
+
* "endLine": 10,
|
|
46
|
+
* "column": 5,
|
|
47
|
+
* "endColumn": 10,
|
|
48
|
+
* "level": "warning",
|
|
49
|
+
* "code": 2154,
|
|
50
|
+
* "message": "var is referenced but not assigned.",
|
|
51
|
+
* "fix": null
|
|
52
|
+
* }]
|
|
53
|
+
*
|
|
54
|
+
* Levels: "error", "warning", "info", "style"
|
|
55
|
+
*/
|
|
56
|
+
function parseShellcheckOutput(raw: string, filePath: string): Diagnostic[] {
|
|
57
|
+
const diagnostics: Diagnostic[] = [];
|
|
58
|
+
|
|
59
|
+
if (!raw.trim()) {
|
|
60
|
+
return diagnostics;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const parsed = JSON.parse(raw) as Array<{
|
|
65
|
+
file?: string;
|
|
66
|
+
line?: number;
|
|
67
|
+
endLine?: number;
|
|
68
|
+
column?: number;
|
|
69
|
+
endColumn?: number;
|
|
70
|
+
level?: string;
|
|
71
|
+
code?: number;
|
|
72
|
+
message?: string;
|
|
73
|
+
fix?: unknown;
|
|
74
|
+
}>;
|
|
75
|
+
|
|
76
|
+
if (!Array.isArray(parsed)) {
|
|
77
|
+
return diagnostics;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
for (const item of parsed) {
|
|
81
|
+
if (!item.message || !item.line) continue;
|
|
82
|
+
|
|
83
|
+
// Map shellcheck levels to our severity
|
|
84
|
+
const severityMap: Record<string, "error" | "warning" | "info"> = {
|
|
85
|
+
error: "error",
|
|
86
|
+
warning: "warning",
|
|
87
|
+
info: "info",
|
|
88
|
+
style: "info",
|
|
89
|
+
};
|
|
90
|
+
const severity = severityMap[item.level || "warning"] || "warning";
|
|
91
|
+
|
|
92
|
+
const ruleCode = item.code ? `SC${item.code}` : "unknown";
|
|
93
|
+
|
|
94
|
+
diagnostics.push({
|
|
95
|
+
id: `shellcheck-${item.line}-${ruleCode}`,
|
|
96
|
+
message: `[${ruleCode}] ${item.message}`,
|
|
97
|
+
filePath,
|
|
98
|
+
line: item.line,
|
|
99
|
+
column: item.column || 1,
|
|
100
|
+
severity,
|
|
101
|
+
semantic: severity === "error" ? "blocking" : "warning",
|
|
102
|
+
tool: "shellcheck",
|
|
103
|
+
rule: ruleCode,
|
|
104
|
+
fixable: !!item.fix,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
} catch {
|
|
108
|
+
// JSON parse failed, return empty
|
|
109
|
+
return diagnostics;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return diagnostics;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const shellcheckRunner: RunnerDefinition = {
|
|
116
|
+
id: "shellcheck",
|
|
117
|
+
appliesTo: ["shell"],
|
|
118
|
+
priority: 20,
|
|
119
|
+
enabledByDefault: true,
|
|
120
|
+
skipTestFiles: false, // Shell scripts in test directories should still be checked
|
|
121
|
+
|
|
122
|
+
async run(ctx: DispatchContext): Promise<RunnerResult> {
|
|
123
|
+
// Skip if shellcheck is not installed
|
|
124
|
+
if (!shellcheck.isAvailable(ctx.cwd || process.cwd())) {
|
|
125
|
+
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Check if user explicitly disabled shellcheck
|
|
129
|
+
if (ctx.pi.getFlag("no-shellcheck")) {
|
|
130
|
+
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Determine shell dialect from file extension
|
|
134
|
+
const shellDialect = ctx.filePath.endsWith(".zsh")
|
|
135
|
+
? "bash"
|
|
136
|
+
: ctx.filePath.endsWith(".fish")
|
|
137
|
+
? "bash"
|
|
138
|
+
: ctx.filePath.endsWith(".sh")
|
|
139
|
+
? "bash"
|
|
140
|
+
: "bash"; // Default to bash for generic shell files
|
|
141
|
+
|
|
142
|
+
// Build args
|
|
143
|
+
// --format json: JSON output
|
|
144
|
+
// --shell: Specify shell dialect (bash, sh, zsh, ksh, busybox)
|
|
145
|
+
// --severity: Minimum severity (we'll filter ourselves)
|
|
146
|
+
const args: string[] = [
|
|
147
|
+
"--format",
|
|
148
|
+
"json",
|
|
149
|
+
"--shell",
|
|
150
|
+
shellDialect,
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
// Check for config file
|
|
154
|
+
const configPath = findShellcheckConfig(ctx.cwd);
|
|
155
|
+
if (!configPath) {
|
|
156
|
+
// No config file, use default settings
|
|
157
|
+
// Exclude "style" and "info" by default to reduce noise
|
|
158
|
+
args.push("--severity", "warning");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
args.push(ctx.filePath);
|
|
162
|
+
|
|
163
|
+
const result = safeSpawn(shellcheck.getCommand()!, args, {
|
|
164
|
+
timeout: 15000,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// shellcheck exits with code 1 if issues found, 0 if clean
|
|
168
|
+
if (result.status === 0 && !result.stdout?.trim()) {
|
|
169
|
+
return { status: "succeeded", diagnostics: [], semantic: "none" };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Parse diagnostics
|
|
173
|
+
const raw = result.stdout + result.stderr;
|
|
174
|
+
const diagnostics = parseShellcheckOutput(raw, ctx.filePath);
|
|
175
|
+
|
|
176
|
+
if (diagnostics.length === 0) {
|
|
177
|
+
return { status: "succeeded", diagnostics: [], semantic: "none" };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
status: "failed",
|
|
182
|
+
diagnostics,
|
|
183
|
+
semantic: "warning",
|
|
184
|
+
};
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
export default shellcheckRunner;
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Similarity Runner: Detect semantic code reuse opportunities
|
|
3
|
+
*
|
|
4
|
+
* Uses Amain's 57×72 state matrix algorithm to find similar functions.
|
|
5
|
+
* Integrated into dispatch flow as a warning (non-blocking) suggestion.
|
|
6
|
+
*/
|
|
7
|
+
import * as fs from "node:fs/promises";
|
|
8
|
+
import * as path from "node:path";
|
|
9
|
+
import * as ts from "typescript";
|
|
10
|
+
import { EXCLUDED_DIRS } from "../../file-utils.js";
|
|
11
|
+
import { buildProjectIndex, findSimilarFunctions, loadIndex, } from "../../project-index.js";
|
|
12
|
+
import { buildStateMatrix, countTransitions } from "../../state-matrix.js";
|
|
13
|
+
// ============================================================================
|
|
14
|
+
// Configuration
|
|
15
|
+
// ============================================================================
|
|
16
|
+
const CONFIG = {
|
|
17
|
+
SIMILARITY_THRESHOLD: 0.75, // 75% minimum similarity
|
|
18
|
+
MIN_TRANSITIONS: 20, // Skip functions with <20 AST transitions
|
|
19
|
+
MAX_SUGGESTIONS: 3, // Max 3 suggestions per file
|
|
20
|
+
USAGE_THRESHOLD: 2, // Only suggest utilities with 2+ uses (placeholder)
|
|
21
|
+
};
|
|
22
|
+
// ============================================================================
|
|
23
|
+
// Runner Implementation
|
|
24
|
+
// ============================================================================
|
|
25
|
+
const similarityRunner = {
|
|
26
|
+
id: "similarity",
|
|
27
|
+
appliesTo: ["jsts"], // TypeScript/JavaScript only for MVP
|
|
28
|
+
priority: 35, // After ts-lsp, before ast-grep
|
|
29
|
+
enabledByDefault: true,
|
|
30
|
+
async run(ctx) {
|
|
31
|
+
const { filePath } = ctx;
|
|
32
|
+
// Only check TypeScript files
|
|
33
|
+
if (!filePath.match(/\.tsx?$/)) {
|
|
34
|
+
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
35
|
+
}
|
|
36
|
+
// Load file content
|
|
37
|
+
const content = await fs.readFile(filePath, "utf-8").catch(() => null);
|
|
38
|
+
if (!content) {
|
|
39
|
+
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
40
|
+
}
|
|
41
|
+
// Find project root and load index
|
|
42
|
+
const projectRoot = await findProjectRoot(filePath);
|
|
43
|
+
if (!projectRoot) {
|
|
44
|
+
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
45
|
+
}
|
|
46
|
+
const index = await loadOrBuildIndex(projectRoot);
|
|
47
|
+
if (!index || index.entries.size === 0) {
|
|
48
|
+
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
49
|
+
}
|
|
50
|
+
// Parse the file
|
|
51
|
+
const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
52
|
+
// Extract functions and check for similarities
|
|
53
|
+
const newFunctions = extractFunctions(sourceFile, content);
|
|
54
|
+
const diagnostics = [];
|
|
55
|
+
for (const func of newFunctions) {
|
|
56
|
+
// Guardrail: Skip tiny functions
|
|
57
|
+
if (func.transitionCount < CONFIG.MIN_TRANSITIONS) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
// Find similar functions in index
|
|
61
|
+
const matches = findSimilarFunctions(func.matrix, index, CONFIG.SIMILARITY_THRESHOLD, CONFIG.MAX_SUGGESTIONS);
|
|
62
|
+
// Create diagnostic for each match
|
|
63
|
+
for (const match of matches) {
|
|
64
|
+
// Skip if it's the same function (self-match by path/name)
|
|
65
|
+
if (match.targetId ===
|
|
66
|
+
`${path.relative(projectRoot, filePath)}:${func.name}`) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
diagnostics.push({
|
|
70
|
+
id: `similarity-${func.name}-${match.targetId}`,
|
|
71
|
+
tool: "similarity",
|
|
72
|
+
filePath,
|
|
73
|
+
line: func.line,
|
|
74
|
+
column: func.column,
|
|
75
|
+
message: buildSuggestionMessage(func, match),
|
|
76
|
+
severity: "warning", // 🟡 Not blocking
|
|
77
|
+
semantic: "warning",
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// Return limited number of suggestions
|
|
82
|
+
const limitedResults = diagnostics.slice(0, CONFIG.MAX_SUGGESTIONS);
|
|
83
|
+
if (limitedResults.length === 0) {
|
|
84
|
+
return { status: "succeeded", diagnostics: [], semantic: "none" };
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
status: "succeeded",
|
|
88
|
+
diagnostics: limitedResults,
|
|
89
|
+
semantic: "warning",
|
|
90
|
+
};
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
function extractFunctions(sourceFile, _fullContent) {
|
|
94
|
+
const functions = [];
|
|
95
|
+
function visit(node) {
|
|
96
|
+
// Function declarations
|
|
97
|
+
if (ts.isFunctionDeclaration(node) && node.name) {
|
|
98
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
|
|
99
|
+
const funcCode = getNodeText(node, sourceFile);
|
|
100
|
+
const matrix = buildStateMatrix(funcCode);
|
|
101
|
+
const transitionCount = countTransitions(matrix);
|
|
102
|
+
functions.push({
|
|
103
|
+
name: node.name.text,
|
|
104
|
+
line: line + 1, // 1-indexed
|
|
105
|
+
column: character + 1, // 1-indexed
|
|
106
|
+
matrix,
|
|
107
|
+
transitionCount,
|
|
108
|
+
signature: getSignature(node),
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
// Arrow functions assigned to const
|
|
112
|
+
if (ts.isVariableStatement(node)) {
|
|
113
|
+
extractArrowFunctions(node, functions, sourceFile);
|
|
114
|
+
}
|
|
115
|
+
ts.forEachChild(node, visit);
|
|
116
|
+
}
|
|
117
|
+
visit(sourceFile);
|
|
118
|
+
return functions;
|
|
119
|
+
}
|
|
120
|
+
function extractArrowFunctions(node, functions, sourceFile) {
|
|
121
|
+
for (const decl of node.declarationList.declarations) {
|
|
122
|
+
if (!ts.isIdentifier(decl.name) || !decl.initializer) {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
const func = decl.initializer;
|
|
126
|
+
if (!ts.isArrowFunction(func) && !ts.isFunctionExpression(func)) {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
|
|
130
|
+
const funcCode = getNodeText(func, sourceFile);
|
|
131
|
+
const matrix = buildStateMatrix(funcCode);
|
|
132
|
+
const transitionCount = countTransitions(matrix);
|
|
133
|
+
functions.push({
|
|
134
|
+
name: decl.name.text,
|
|
135
|
+
line: line + 1,
|
|
136
|
+
column: character + 1,
|
|
137
|
+
matrix,
|
|
138
|
+
transitionCount,
|
|
139
|
+
signature: getArrowSignature(func),
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function getNodeText(node, sourceFile) {
|
|
144
|
+
return sourceFile.text.substring(node.getStart(sourceFile), node.getEnd());
|
|
145
|
+
}
|
|
146
|
+
function getSignature(node) {
|
|
147
|
+
const params = node.parameters
|
|
148
|
+
.map((p) => (ts.isIdentifier(p.name) ? p.name.text : "param"))
|
|
149
|
+
.join(", ");
|
|
150
|
+
return `(${params})`;
|
|
151
|
+
}
|
|
152
|
+
function getArrowSignature(node) {
|
|
153
|
+
const params = node.parameters
|
|
154
|
+
.map((p) => (ts.isIdentifier(p.name) ? p.name.text : "param"))
|
|
155
|
+
.join(", ");
|
|
156
|
+
return `(${params})`;
|
|
157
|
+
}
|
|
158
|
+
// ============================================================================
|
|
159
|
+
// Message Building
|
|
160
|
+
// ============================================================================
|
|
161
|
+
function buildSuggestionMessage(func, match) {
|
|
162
|
+
const similarityPct = Math.round(match.similarity * 100);
|
|
163
|
+
const parts = match.targetId.split(":");
|
|
164
|
+
const file = parts[0];
|
|
165
|
+
const name = parts[1] || match.targetName;
|
|
166
|
+
const location = `${file}:1`; // TODO: get actual line
|
|
167
|
+
return `Function '${func.name}' has ${similarityPct}% similarity to existing utility '${name}()' in ${location}. Consider reusing the existing utility.`;
|
|
168
|
+
}
|
|
169
|
+
// ============================================================================
|
|
170
|
+
// Index Management
|
|
171
|
+
// ============================================================================
|
|
172
|
+
const indexCache = new Map();
|
|
173
|
+
async function findProjectRoot(filePath) {
|
|
174
|
+
let dir = path.dirname(filePath);
|
|
175
|
+
while (dir !== path.dirname(dir)) {
|
|
176
|
+
try {
|
|
177
|
+
await fs.access(path.join(dir, "package.json"));
|
|
178
|
+
return dir;
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
dir = path.dirname(dir);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
async function loadOrBuildIndex(projectRoot) {
|
|
187
|
+
// Check cache
|
|
188
|
+
const cached = indexCache.get(projectRoot);
|
|
189
|
+
if (cached) {
|
|
190
|
+
return cached;
|
|
191
|
+
}
|
|
192
|
+
// Try to load existing index
|
|
193
|
+
const existing = await loadIndex(projectRoot);
|
|
194
|
+
if (existing) {
|
|
195
|
+
indexCache.set(projectRoot, existing);
|
|
196
|
+
return existing;
|
|
197
|
+
}
|
|
198
|
+
// Build new index
|
|
199
|
+
const { glob } = await import("glob");
|
|
200
|
+
// Build ignore patterns from centralized EXCLUDED_DIRS
|
|
201
|
+
const ignorePatterns = [
|
|
202
|
+
...EXCLUDED_DIRS.map((d) => `**/${d}/**`),
|
|
203
|
+
"**/*.test.ts",
|
|
204
|
+
"**/*.spec.ts",
|
|
205
|
+
"**/*.poc.test.ts",
|
|
206
|
+
];
|
|
207
|
+
const files = await glob("**/*.ts", {
|
|
208
|
+
cwd: projectRoot,
|
|
209
|
+
ignore: ignorePatterns,
|
|
210
|
+
});
|
|
211
|
+
if (files.length === 0) {
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
const absoluteFiles = files.map((f) => path.join(projectRoot, f));
|
|
215
|
+
const index = await buildProjectIndex(projectRoot, absoluteFiles);
|
|
216
|
+
indexCache.set(projectRoot, index);
|
|
217
|
+
return index;
|
|
218
|
+
}
|
|
219
|
+
// ============================================================================
|
|
220
|
+
// Testing Helper
|
|
221
|
+
// ============================================================================
|
|
222
|
+
export async function buildIndexForTesting(projectRoot) {
|
|
223
|
+
const index = await loadOrBuildIndex(projectRoot);
|
|
224
|
+
if (!index) {
|
|
225
|
+
throw new Error("Failed to build index");
|
|
226
|
+
}
|
|
227
|
+
return index;
|
|
228
|
+
}
|
|
229
|
+
export { CONFIG };
|
|
230
|
+
export default similarityRunner;
|