pi-lens 2.1.0 → 2.2.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.
Files changed (34) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +70 -1
  3. package/clients/ast-grep-client.js +12 -12
  4. package/clients/ast-grep-client.ts +21 -11
  5. package/clients/dispatch/dispatcher.js +2 -2
  6. package/clients/dispatch/dispatcher.ts +2 -2
  7. package/clients/dispatch/runners/index.js +3 -1
  8. package/clients/dispatch/runners/index.ts +3 -1
  9. package/clients/dispatch/runners/pyright.js +68 -0
  10. package/clients/dispatch/runners/pyright.test.js +84 -0
  11. package/clients/dispatch/runners/pyright.test.ts +109 -0
  12. package/clients/dispatch/runners/pyright.ts +102 -0
  13. package/clients/dispatch/runners/secrets.js +109 -0
  14. package/clients/secrets-scanner.js +113 -0
  15. package/clients/secrets-scanner.test.js +100 -0
  16. package/clients/secrets-scanner.test.ts +113 -0
  17. package/clients/secrets-scanner.ts +134 -0
  18. package/clients/sg-runner.js +15 -2
  19. package/clients/sg-runner.ts +25 -2
  20. package/commands/fix.js +48 -50
  21. package/commands/fix.ts +71 -61
  22. package/commands/rate.js +285 -0
  23. package/commands/rate.test.js +119 -0
  24. package/commands/rate.test.ts +131 -0
  25. package/commands/rate.ts +348 -0
  26. package/commands/refactor.js +33 -9
  27. package/commands/refactor.ts +44 -11
  28. package/default-architect.yaml +7 -0
  29. package/index.ts +58 -10
  30. package/package.json +1 -1
  31. package/rules/ast-grep-rules/rules/no-default-export.yml +19 -0
  32. package/rules/ast-grep-rules/rules/no-hardcoded-secrets.yml +9 -6
  33. package/rules/ast-grep-rules/rules/no-process-env.yml +12 -12
  34. package/rules/ast-grep-rules/rules/no-relative-imports.yml +21 -0
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Secrets scanner runner
3
+ *
4
+ * Scans file content for potential secret patterns before write.
5
+ * Works on all file types via regex matching.
6
+ *
7
+ * Patterns detected:
8
+ * - Stripe/OpenAI keys (sk-*)
9
+ * - GitHub tokens (ghp_*, gho_*, github_pat_*)
10
+ * - AWS keys (AKIA*)
11
+ * - Slack tokens (xoxp-*, xoxb-*)
12
+ * - Private keys (BEGIN PRIVATE KEY)
13
+ * - Generic API key patterns
14
+ */
15
+ const SECRET_PATTERNS = [
16
+ {
17
+ pattern: /sk-[a-zA-Z0-9]{20,}/g,
18
+ name: "stripe-openai-key",
19
+ message: "Possible Stripe or OpenAI API key (sk-*)",
20
+ },
21
+ {
22
+ pattern: /ghp_[a-zA-Z0-9]{36}/g,
23
+ name: "github-personal-token",
24
+ message: "GitHub personal access token (ghp_*)",
25
+ },
26
+ {
27
+ pattern: /gho_[a-zA-Z0-9]{36}/g,
28
+ name: "github-oauth-token",
29
+ message: "GitHub OAuth token (gho_*)",
30
+ },
31
+ {
32
+ pattern: /github_pat_[a-zA-Z_]{82}/g,
33
+ name: "github-fine-grained-pat",
34
+ message: "GitHub fine-grained PAT (github_pat_*)",
35
+ },
36
+ {
37
+ pattern: /AKIA[0-9A-Z]{16}/g,
38
+ name: "aws-access-key",
39
+ message: "AWS access key ID (AKIA*)",
40
+ },
41
+ {
42
+ pattern: /xox[bp]-[a-zA-Z0-9]{10,}/g,
43
+ name: "slack-token",
44
+ message: "Slack token (xoxb-*/xoxp-*)",
45
+ },
46
+ {
47
+ pattern: /-----BEGIN\s+(RSA\s+)?PRIVATE KEY-----/g,
48
+ name: "private-key",
49
+ message: "Private key material detected",
50
+ },
51
+ {
52
+ pattern: /password\s*[:=]\s*["'][^"']{8,}["']/gi,
53
+ name: "hardcoded-password",
54
+ message: "Possible hardcoded password",
55
+ },
56
+ {
57
+ pattern: /(?:secret|api_?key|token)\s*[:=]\s*["'][a-zA-Z0-9]{20,}["']/gi,
58
+ name: "generic-api-secret",
59
+ message: "Possible hardcoded secret, API key, or token",
60
+ },
61
+ ];
62
+ function scanContent(content) {
63
+ const findings = [];
64
+ const lines = content.split("\n");
65
+ for (let i = 0; i < lines.length; i++) {
66
+ const line = lines[i];
67
+ for (const secret of SECRET_PATTERNS) {
68
+ secret.pattern.lastIndex = 0; // Reset regex state
69
+ const match = secret.pattern.exec(line);
70
+ if (match) {
71
+ findings.push({
72
+ line: i + 1,
73
+ pattern: secret,
74
+ match: match[0].slice(0, 20) + "...",
75
+ });
76
+ }
77
+ }
78
+ }
79
+ return findings;
80
+ }
81
+ async function run(ctx) {
82
+ const diagnostics = [];
83
+ // Get the content being written - check both full content and the diff
84
+ const content = ctx.content;
85
+ if (!content)
86
+ return { diagnostics };
87
+ const findings = scanContent(content);
88
+ for (const finding of findings) {
89
+ diagnostics.push({
90
+ id: `secrets-${finding.pattern.name}-${finding.line}`,
91
+ message: `${finding.pattern.message} at line ${finding.line}. Remove before committing.`,
92
+ filePath: ctx.filePath,
93
+ line: finding.line,
94
+ severity: "error",
95
+ semantic: "blocking", // Always blocking - secrets should never be committed
96
+ tool: "secrets",
97
+ rule: finding.pattern.name,
98
+ fixable: false,
99
+ });
100
+ }
101
+ return { diagnostics };
102
+ }
103
+ const runner = {
104
+ id: "secrets",
105
+ appliesTo: ["*"], // All file types
106
+ priority: 0, // Run first - block before other checks
107
+ run,
108
+ };
109
+ export default runner;
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Content-level secrets scanner
3
+ *
4
+ * Scans file content for potential secret patterns before write.
5
+ * Works on all file types via regex matching.
6
+ *
7
+ * Detected patterns:
8
+ * - Stripe/OpenAI keys (sk-*)
9
+ * - GitHub tokens (ghp_*, gho_*, github_pat_*)
10
+ * - AWS keys (AKIA*)
11
+ * - Slack tokens (xoxp-*, xoxb-*)
12
+ * - Private keys (BEGIN PRIVATE KEY)
13
+ * - Generic API key/password patterns
14
+ */
15
+ // Patterns ordered by specificity - first match wins per line
16
+ const SECRET_PATTERNS = [
17
+ // High-confidence: specific key prefixes
18
+ {
19
+ pattern: /sk-[a-zA-Z0-9-]{20,}/g,
20
+ name: "stripe-openai-key",
21
+ message: "Possible Stripe or OpenAI API key (sk-*)",
22
+ },
23
+ {
24
+ pattern: /ghp_[a-zA-Z0-9]{36}/g,
25
+ name: "github-personal-token",
26
+ message: "GitHub personal access token (ghp_*)",
27
+ },
28
+ {
29
+ pattern: /gho_[a-zA-Z0-9]{36}/g,
30
+ name: "github-oauth-token",
31
+ message: "GitHub OAuth token (gho_*)",
32
+ },
33
+ {
34
+ pattern: /github_pat_[a-zA-Z_]{82}/g,
35
+ name: "github-fine-grained-pat",
36
+ message: "GitHub fine-grained PAT (github_pat_*)",
37
+ },
38
+ {
39
+ pattern: /AKIA[0-9A-Z]{16}/g,
40
+ name: "aws-access-key",
41
+ message: "AWS access key ID (AKIA*)",
42
+ },
43
+ {
44
+ pattern: /xox[bp]-[a-zA-Z0-9]{10,}/g,
45
+ name: "slack-token",
46
+ message: "Slack token (xoxb-*/xoxp-*)",
47
+ },
48
+ {
49
+ pattern: /-----BEGIN\s+(RSA\s+)?PRIVATE KEY-----/g,
50
+ name: "private-key",
51
+ message: "Private key material detected",
52
+ },
53
+ // Medium-confidence: quoted credentials
54
+ {
55
+ pattern: /password\s*[:=]\s*["'][^"']{4,}["']/gi,
56
+ name: "hardcoded-password",
57
+ message: "Possible hardcoded password",
58
+ },
59
+ {
60
+ pattern: /(?:secret|api_?key|token|access_?key)\s*[:=]\s*["'][a-zA-Z0-9_\-/.]{8,}["']/gi,
61
+ name: "hardcoded-secret",
62
+ message: "Possible hardcoded secret or API key",
63
+ },
64
+ // .env format: KEY=VALUE (no quotes)
65
+ {
66
+ pattern: /^(?:API_?KEY|SECRET|TOKEN|PASSWORD|AWS_?ACCESS_?KEY)\s*=\s*\S{8,}/gim,
67
+ name: "env-file-secret",
68
+ message: "Possible secret in .env format",
69
+ },
70
+ ];
71
+ /**
72
+ * Scan content for potential secrets
73
+ * Returns findings with line numbers
74
+ */
75
+ export function scanForSecrets(content) {
76
+ const findings = [];
77
+ const lines = content.split("\n");
78
+ for (let i = 0; i < lines.length; i++) {
79
+ const line = lines[i];
80
+ let matched = false;
81
+ for (const pattern of SECRET_PATTERNS) {
82
+ // Reset lastIndex before each test (important for global regex)
83
+ const regex = new RegExp(pattern.pattern.source, pattern.pattern.flags);
84
+ if (regex.test(line)) {
85
+ findings.push({
86
+ line: i + 1,
87
+ message: pattern.message,
88
+ });
89
+ matched = true;
90
+ break; // One finding per line
91
+ }
92
+ }
93
+ }
94
+ return findings;
95
+ }
96
+ /**
97
+ * Format secrets findings for terminal output
98
+ */
99
+ export function formatSecrets(findings, filePath) {
100
+ if (findings.length === 0)
101
+ return "";
102
+ const lines = [
103
+ `🔴 STOP — ${findings.length} potential secret(s) in ${filePath}:`,
104
+ ];
105
+ for (const f of findings.slice(0, 5)) {
106
+ lines.push(` L${f.line}: ${f.message}`);
107
+ }
108
+ if (findings.length > 5) {
109
+ lines.push(` ... and ${findings.length - 5} more`);
110
+ }
111
+ lines.push(" → Remove before continuing. Use env vars instead.");
112
+ return lines.join("\n");
113
+ }
@@ -0,0 +1,100 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { formatSecrets, scanForSecrets } from "./secrets-scanner.js";
3
+ describe("scanForSecrets", () => {
4
+ it("should detect Stripe/OpenAI keys (sk-*)", () => {
5
+ const content = `const apiKey = "sk-live-1234567890abcdefghij";`;
6
+ const findings = scanForSecrets(content);
7
+ expect(findings.length).toBe(1);
8
+ expect(findings[0].message).toContain("Stripe or OpenAI");
9
+ });
10
+ it("should detect GitHub personal tokens (ghp_*)", () => {
11
+ const content = `token = "ghp_1234567890abcdefghijklmnopqrstuvwxyz";`;
12
+ const findings = scanForSecrets(content);
13
+ expect(findings.length).toBe(1);
14
+ expect(findings[0].message).toContain("GitHub personal");
15
+ });
16
+ it("should detect AWS access keys (AKIA*)", () => {
17
+ const content = `const AWS_KEY = "AKIAIOSFODNN7EXAMPLE";`;
18
+ const findings = scanForSecrets(content);
19
+ expect(findings.length).toBe(1);
20
+ expect(findings[0].message).toContain("AWS access key");
21
+ });
22
+ it("should detect private key material", () => {
23
+ const content = `-----BEGIN RSA PRIVATE KEY-----
24
+ MIIEpAIBAAKCAQEA...`;
25
+ const findings = scanForSecrets(content);
26
+ expect(findings.length).toBe(1);
27
+ expect(findings[0].message).toContain("Private key");
28
+ });
29
+ it("should detect hardcoded passwords", () => {
30
+ const content = `const config = { password: "hunter2" };`;
31
+ const findings = scanForSecrets(content);
32
+ expect(findings.length).toBe(1);
33
+ expect(findings[0].message).toContain("password");
34
+ });
35
+ it("should detect secrets in .env format", () => {
36
+ const content = `API_KEY=sk-live-1234567890abcdefghij
37
+ DATABASE_URL=postgres://localhost`;
38
+ const findings = scanForSecrets(content);
39
+ expect(findings.length).toBe(1);
40
+ // sk-* pattern catches this first (more specific)
41
+ expect(findings[0].message).toContain("Stripe or OpenAI");
42
+ });
43
+ it("should NOT flag safe content", () => {
44
+ const content = `
45
+ const name = "test";
46
+ const url = "https://example.com";
47
+ const port = 3000;
48
+ const message = "Hello world";
49
+ `;
50
+ const findings = scanForSecrets(content);
51
+ expect(findings.length).toBe(0);
52
+ });
53
+ it("should NOT flag env var references", () => {
54
+ const content = `const key = process.env.API_KEY;`;
55
+ const findings = scanForSecrets(content);
56
+ expect(findings.length).toBe(0);
57
+ });
58
+ it("should detect multiple secrets", () => {
59
+ const content = `
60
+ const sk = "sk-live-1234567890abcdefghij";
61
+ const gh = "ghp_1234567890abcdefghijklmnopqrstuvwxyz";
62
+ `;
63
+ const findings = scanForSecrets(content);
64
+ expect(findings.length).toBe(2);
65
+ });
66
+ it("should report correct line numbers", () => {
67
+ const content = `line 1
68
+ line 2
69
+ const secret = "sk-live-1234567890abcdefghij";
70
+ line 4`;
71
+ const findings = scanForSecrets(content);
72
+ expect(findings.length).toBe(1);
73
+ expect(findings[0].line).toBe(3);
74
+ });
75
+ });
76
+ describe("formatSecrets", () => {
77
+ it("should format findings for terminal output", () => {
78
+ const findings = [
79
+ { line: 5, message: "Possible Stripe or OpenAI API key (sk-*)" },
80
+ ];
81
+ const output = formatSecrets(findings, "src/config.ts");
82
+ expect(output).toContain("STOP");
83
+ expect(output).toContain("1 potential secret(s)");
84
+ expect(output).toContain("L5");
85
+ expect(output).toContain("src/config.ts");
86
+ });
87
+ it("should return empty string for no findings", () => {
88
+ const output = formatSecrets([], "src/config.ts");
89
+ expect(output).toBe("");
90
+ });
91
+ it("should truncate at 5 findings", () => {
92
+ const findings = Array.from({ length: 10 }, (_, i) => ({
93
+ line: i + 1,
94
+ message: "Test secret",
95
+ }));
96
+ const output = formatSecrets(findings, "src/config.ts");
97
+ expect(output).toContain("10 potential secret(s)");
98
+ expect(output).toContain("... and 5 more");
99
+ });
100
+ });
@@ -0,0 +1,113 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { formatSecrets, scanForSecrets } from "./secrets-scanner.js";
3
+
4
+ describe("scanForSecrets", () => {
5
+ it("should detect Stripe/OpenAI keys (sk-*)", () => {
6
+ const content = `const apiKey = "sk-live-1234567890abcdefghij";`;
7
+ const findings = scanForSecrets(content);
8
+ expect(findings.length).toBe(1);
9
+ expect(findings[0].message).toContain("Stripe or OpenAI");
10
+ });
11
+
12
+ it("should detect GitHub personal tokens (ghp_*)", () => {
13
+ const content = `token = "ghp_1234567890abcdefghijklmnopqrstuvwxyz";`;
14
+ const findings = scanForSecrets(content);
15
+ expect(findings.length).toBe(1);
16
+ expect(findings[0].message).toContain("GitHub personal");
17
+ });
18
+
19
+ it("should detect AWS access keys (AKIA*)", () => {
20
+ const content = `const AWS_KEY = "AKIAIOSFODNN7EXAMPLE";`;
21
+ const findings = scanForSecrets(content);
22
+ expect(findings.length).toBe(1);
23
+ expect(findings[0].message).toContain("AWS access key");
24
+ });
25
+
26
+ it("should detect private key material", () => {
27
+ const content = `-----BEGIN RSA PRIVATE KEY-----
28
+ MIIEpAIBAAKCAQEA...`;
29
+ const findings = scanForSecrets(content);
30
+ expect(findings.length).toBe(1);
31
+ expect(findings[0].message).toContain("Private key");
32
+ });
33
+
34
+ it("should detect hardcoded passwords", () => {
35
+ const content = `const config = { password: "hunter2" };`;
36
+ const findings = scanForSecrets(content);
37
+ expect(findings.length).toBe(1);
38
+ expect(findings[0].message).toContain("password");
39
+ });
40
+
41
+ it("should detect secrets in .env format", () => {
42
+ const content = `API_KEY=sk-live-1234567890abcdefghij
43
+ DATABASE_URL=postgres://localhost`;
44
+ const findings = scanForSecrets(content);
45
+ expect(findings.length).toBe(1);
46
+ // sk-* pattern catches this first (more specific)
47
+ expect(findings[0].message).toContain("Stripe or OpenAI");
48
+ });
49
+
50
+ it("should NOT flag safe content", () => {
51
+ const content = `
52
+ const name = "test";
53
+ const url = "https://example.com";
54
+ const port = 3000;
55
+ const message = "Hello world";
56
+ `;
57
+ const findings = scanForSecrets(content);
58
+ expect(findings.length).toBe(0);
59
+ });
60
+
61
+ it("should NOT flag env var references", () => {
62
+ const content = `const key = process.env.API_KEY;`;
63
+ const findings = scanForSecrets(content);
64
+ expect(findings.length).toBe(0);
65
+ });
66
+
67
+ it("should detect multiple secrets", () => {
68
+ const content = `
69
+ const sk = "sk-live-1234567890abcdefghij";
70
+ const gh = "ghp_1234567890abcdefghijklmnopqrstuvwxyz";
71
+ `;
72
+ const findings = scanForSecrets(content);
73
+ expect(findings.length).toBe(2);
74
+ });
75
+
76
+ it("should report correct line numbers", () => {
77
+ const content = `line 1
78
+ line 2
79
+ const secret = "sk-live-1234567890abcdefghij";
80
+ line 4`;
81
+ const findings = scanForSecrets(content);
82
+ expect(findings.length).toBe(1);
83
+ expect(findings[0].line).toBe(3);
84
+ });
85
+ });
86
+
87
+ describe("formatSecrets", () => {
88
+ it("should format findings for terminal output", () => {
89
+ const findings = [
90
+ { line: 5, message: "Possible Stripe or OpenAI API key (sk-*)" },
91
+ ];
92
+ const output = formatSecrets(findings, "src/config.ts");
93
+ expect(output).toContain("STOP");
94
+ expect(output).toContain("1 potential secret(s)");
95
+ expect(output).toContain("L5");
96
+ expect(output).toContain("src/config.ts");
97
+ });
98
+
99
+ it("should return empty string for no findings", () => {
100
+ const output = formatSecrets([], "src/config.ts");
101
+ expect(output).toBe("");
102
+ });
103
+
104
+ it("should truncate at 5 findings", () => {
105
+ const findings = Array.from({ length: 10 }, (_, i) => ({
106
+ line: i + 1,
107
+ message: "Test secret",
108
+ }));
109
+ const output = formatSecrets(findings, "src/config.ts");
110
+ expect(output).toContain("10 potential secret(s)");
111
+ expect(output).toContain("... and 5 more");
112
+ });
113
+ });
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Content-level secrets scanner
3
+ *
4
+ * Scans file content for potential secret patterns before write.
5
+ * Works on all file types via regex matching.
6
+ *
7
+ * Detected patterns:
8
+ * - Stripe/OpenAI keys (sk-*)
9
+ * - GitHub tokens (ghp_*, gho_*, github_pat_*)
10
+ * - AWS keys (AKIA*)
11
+ * - Slack tokens (xoxp-*, xoxb-*)
12
+ * - Private keys (BEGIN PRIVATE KEY)
13
+ * - Generic API key/password patterns
14
+ */
15
+
16
+ interface SecretPattern {
17
+ pattern: RegExp;
18
+ name: string;
19
+ message: string;
20
+ }
21
+
22
+ // Patterns ordered by specificity - first match wins per line
23
+ const SECRET_PATTERNS: SecretPattern[] = [
24
+ // High-confidence: specific key prefixes
25
+ {
26
+ pattern: /sk-[a-zA-Z0-9-]{20,}/g,
27
+ name: "stripe-openai-key",
28
+ message: "Possible Stripe or OpenAI API key (sk-*)",
29
+ },
30
+ {
31
+ pattern: /ghp_[a-zA-Z0-9]{36}/g,
32
+ name: "github-personal-token",
33
+ message: "GitHub personal access token (ghp_*)",
34
+ },
35
+ {
36
+ pattern: /gho_[a-zA-Z0-9]{36}/g,
37
+ name: "github-oauth-token",
38
+ message: "GitHub OAuth token (gho_*)",
39
+ },
40
+ {
41
+ pattern: /github_pat_[a-zA-Z_]{82}/g,
42
+ name: "github-fine-grained-pat",
43
+ message: "GitHub fine-grained PAT (github_pat_*)",
44
+ },
45
+ {
46
+ pattern: /AKIA[0-9A-Z]{16}/g,
47
+ name: "aws-access-key",
48
+ message: "AWS access key ID (AKIA*)",
49
+ },
50
+ {
51
+ pattern: /xox[bp]-[a-zA-Z0-9]{10,}/g,
52
+ name: "slack-token",
53
+ message: "Slack token (xoxb-*/xoxp-*)",
54
+ },
55
+ {
56
+ pattern: /-----BEGIN\s+(RSA\s+)?PRIVATE KEY-----/g,
57
+ name: "private-key",
58
+ message: "Private key material detected",
59
+ },
60
+ // Medium-confidence: quoted credentials
61
+ {
62
+ pattern: /password\s*[:=]\s*["'][^"']{4,}["']/gi,
63
+ name: "hardcoded-password",
64
+ message: "Possible hardcoded password",
65
+ },
66
+ {
67
+ pattern:
68
+ /(?:secret|api_?key|token|access_?key)\s*[:=]\s*["'][a-zA-Z0-9_\-/.]{8,}["']/gi,
69
+ name: "hardcoded-secret",
70
+ message: "Possible hardcoded secret or API key",
71
+ },
72
+ // .env format: KEY=VALUE (no quotes)
73
+ {
74
+ pattern:
75
+ /^(?:API_?KEY|SECRET|TOKEN|PASSWORD|AWS_?ACCESS_?KEY)\s*=\s*\S{8,}/gim,
76
+ name: "env-file-secret",
77
+ message: "Possible secret in .env format",
78
+ },
79
+ ];
80
+
81
+ export interface SecretFinding {
82
+ line: number;
83
+ message: string;
84
+ }
85
+
86
+ /**
87
+ * Scan content for potential secrets
88
+ * Returns findings with line numbers
89
+ */
90
+ export function scanForSecrets(content: string): SecretFinding[] {
91
+ const findings: SecretFinding[] = [];
92
+ const lines = content.split("\n");
93
+
94
+ for (let i = 0; i < lines.length; i++) {
95
+ const line = lines[i];
96
+ let matched = false;
97
+ for (const pattern of SECRET_PATTERNS) {
98
+ // Reset lastIndex before each test (important for global regex)
99
+ const regex = new RegExp(pattern.pattern.source, pattern.pattern.flags);
100
+ if (regex.test(line)) {
101
+ findings.push({
102
+ line: i + 1,
103
+ message: pattern.message,
104
+ });
105
+ matched = true;
106
+ break; // One finding per line
107
+ }
108
+ }
109
+ }
110
+
111
+ return findings;
112
+ }
113
+
114
+ /**
115
+ * Format secrets findings for terminal output
116
+ */
117
+ export function formatSecrets(
118
+ findings: SecretFinding[],
119
+ filePath: string,
120
+ ): string {
121
+ if (findings.length === 0) return "";
122
+
123
+ const lines = [
124
+ `🔴 STOP — ${findings.length} potential secret(s) in ${filePath}:`,
125
+ ];
126
+ for (const f of findings.slice(0, 5)) {
127
+ lines.push(` L${f.line}: ${f.message}`);
128
+ }
129
+ if (findings.length > 5) {
130
+ lines.push(` ... and ${findings.length - 5} more`);
131
+ }
132
+ lines.push(" → Remove before continuing. Use env vars instead.");
133
+ return lines.join("\n");
134
+ }
@@ -126,9 +126,15 @@ export class SgRunner {
126
126
  /**
127
127
  * Format matches for display
128
128
  */
129
- formatMatches(matches, isDryRun = false, maxItems = 50) {
130
- if (matches.length === 0)
129
+ formatMatches(matches, isDryRun = false, maxItems = 50, showModeIndicator = false) {
130
+ if (matches.length === 0) {
131
+ if (showModeIndicator) {
132
+ return isDryRun
133
+ ? "[DRY-RUN] No matches found."
134
+ : "[APPLIED] No changes made (no matches found).";
135
+ }
131
136
  return "No matches found";
137
+ }
132
138
  const shown = matches.slice(0, maxItems);
133
139
  const lines = shown.map((m) => {
134
140
  const loc = `${m.file}:${m.range.start.line + 1}:${m.range.start.column + 1}`;
@@ -140,6 +146,13 @@ export class SgRunner {
140
146
  if (matches.length > maxItems) {
141
147
  lines.unshift(`Found ${matches.length} matches (showing first ${maxItems}):`);
142
148
  }
149
+ if (showModeIndicator) {
150
+ const prefix = isDryRun ? "[DRY-RUN]" : "[APPLIED]";
151
+ const suffix = isDryRun
152
+ ? "\n\n(Dry run — use apply=true to apply changes)"
153
+ : "";
154
+ return `${prefix} ${matches.length} replacement(s):\n\n${lines.join("\n")}${suffix}`;
155
+ }
143
156
  return lines.join("\n");
144
157
  }
145
158
  }
@@ -162,8 +162,21 @@ export class SgRunner {
162
162
  /**
163
163
  * Format matches for display
164
164
  */
165
- formatMatches(matches: SgMatch[], isDryRun = false, maxItems = 50): string {
166
- if (matches.length === 0) return "No matches found";
165
+ formatMatches(
166
+ matches: SgMatch[],
167
+ isDryRun = false,
168
+ maxItems = 50,
169
+ showModeIndicator = false,
170
+ ): string {
171
+ if (matches.length === 0) {
172
+ if (showModeIndicator) {
173
+ return isDryRun
174
+ ? "[DRY-RUN] No matches found."
175
+ : "[APPLIED] No changes made (no matches found).";
176
+ }
177
+ return "No matches found";
178
+ }
179
+
167
180
  const shown = matches.slice(0, maxItems);
168
181
  const lines = shown.map((m) => {
169
182
  const loc = `${m.file}:${m.range.start.line + 1}:${m.range.start.column + 1}`;
@@ -172,11 +185,21 @@ export class SgRunner {
172
185
  ? `${loc}\n - ${text}\n + ${m.replacement}`
173
186
  : `${loc}: ${text}`;
174
187
  });
188
+
175
189
  if (matches.length > maxItems) {
176
190
  lines.unshift(
177
191
  `Found ${matches.length} matches (showing first ${maxItems}):`,
178
192
  );
179
193
  }
194
+
195
+ if (showModeIndicator) {
196
+ const prefix = isDryRun ? "[DRY-RUN]" : "[APPLIED]";
197
+ const suffix = isDryRun
198
+ ? "\n\n(Dry run — use apply=true to apply changes)"
199
+ : "";
200
+ return `${prefix} ${matches.length} replacement(s):\n\n${lines.join("\n")}${suffix}`;
201
+ }
202
+
180
203
  return lines.join("\n");
181
204
  }
182
205
  }