@mrclrchtr/supi-code-intelligence 0.1.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/README.md +212 -0
- package/node_modules/@mrclrchtr/supi-core/README.md +90 -0
- package/node_modules/@mrclrchtr/supi-core/package.json +30 -0
- package/node_modules/@mrclrchtr/supi-core/src/config-settings.ts +76 -0
- package/node_modules/@mrclrchtr/supi-core/src/config.ts +186 -0
- package/node_modules/@mrclrchtr/supi-core/src/context-messages.ts +119 -0
- package/node_modules/@mrclrchtr/supi-core/src/context-provider-registry.ts +36 -0
- package/node_modules/@mrclrchtr/supi-core/src/context-tag.ts +31 -0
- package/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +255 -0
- package/node_modules/@mrclrchtr/supi-core/src/index.ts +83 -0
- package/node_modules/@mrclrchtr/supi-core/src/project-roots.ts +170 -0
- package/node_modules/@mrclrchtr/supi-core/src/registry-utils.ts +54 -0
- package/node_modules/@mrclrchtr/supi-core/src/session-utils.ts +29 -0
- package/node_modules/@mrclrchtr/supi-core/src/settings-command.ts +15 -0
- package/node_modules/@mrclrchtr/supi-core/src/settings-registry.ts +41 -0
- package/node_modules/@mrclrchtr/supi-core/src/settings-ui.ts +226 -0
- package/node_modules/@mrclrchtr/supi-core/src/terminal.ts +60 -0
- package/node_modules/@mrclrchtr/supi-lsp/README.md +112 -0
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/README.md +90 -0
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/package.json +30 -0
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/config-settings.ts +76 -0
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/config.ts +186 -0
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/context-messages.ts +119 -0
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/context-provider-registry.ts +36 -0
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/context-tag.ts +31 -0
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +255 -0
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/index.ts +83 -0
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/project-roots.ts +170 -0
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/registry-utils.ts +54 -0
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/session-utils.ts +29 -0
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/settings-command.ts +15 -0
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/settings-registry.ts +41 -0
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/settings-ui.ts +226 -0
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/terminal.ts +60 -0
- package/node_modules/@mrclrchtr/supi-lsp/package.json +45 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/capabilities.ts +62 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-refresh.ts +229 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client.ts +545 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/client/transport.ts +192 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/config.ts +143 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/defaults.json +82 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/diagnostic-augmentation.ts +82 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/diagnostic-display.ts +68 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/diagnostic-summary.ts +73 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/diagnostics.ts +98 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/stale-diagnostics.ts +47 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/suppression-diagnostics.ts +58 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/format.ts +359 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/guidance.ts +163 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/index.ts +17 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/lsp-state.ts +82 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/lsp.ts +470 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-client-state.ts +34 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-diagnostics.ts +139 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-helpers.ts +39 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-project-info.ts +46 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-types.ts +39 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-workspace-recovery.ts +83 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-workspace-symbol.ts +18 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager.ts +550 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/overrides.ts +173 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/pattern-matcher.ts +197 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/renderer.ts +120 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/scanner.ts +153 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/search-fallback.ts +98 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/service-registry.ts +153 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/settings-registration.ts +292 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/summary.ts +153 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/tool-actions.ts +430 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/tree-persist.ts +48 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/tsconfig-scope.ts +156 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/types.ts +409 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/ui.ts +358 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/utils.ts +122 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/workspace-sentinels.ts +114 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/README.md +97 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/package.json +67 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/resources/.gitkeep +0 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/bash/tree-sitter-bash.wasm +0 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/bash/tree-sitter-bash.wasm.json +7 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/c/tree-sitter-c.wasm +0 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/c/tree-sitter-c.wasm.json +7 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/cpp/tree-sitter-cpp.wasm +0 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/cpp/tree-sitter-cpp.wasm.json +7 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/go/tree-sitter-go.wasm +0 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/go/tree-sitter-go.wasm.json +7 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/html/tree-sitter-html.wasm +0 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/html/tree-sitter-html.wasm.json +7 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/java/tree-sitter-java.wasm +0 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/java/tree-sitter-java.wasm.json +7 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/javascript/tree-sitter-javascript.wasm +0 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/javascript/tree-sitter-javascript.wasm.json +7 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/kotlin/tree-sitter-kotlin.wasm +0 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/kotlin/tree-sitter-kotlin.wasm.json +12 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/python/tree-sitter-python.wasm +0 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/python/tree-sitter-python.wasm.json +7 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/r/tree-sitter-r.wasm +0 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/r/tree-sitter-r.wasm.json +7 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/ruby/tree-sitter-ruby.wasm +0 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/ruby/tree-sitter-ruby.wasm.json +7 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/rust/tree-sitter-rust.wasm +0 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/rust/tree-sitter-rust.wasm.json +7 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/sql/tree-sitter-sql.wasm +0 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/sql/tree-sitter-sql.wasm.json +19 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/tsx/tree-sitter-tsx.wasm +0 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/tsx/tree-sitter-tsx.wasm.json +7 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/typescript/tree-sitter-typescript.wasm +0 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/typescript/tree-sitter-typescript.wasm.json +7 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/scripts/generate-kotlin-wasm.mjs +126 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/scripts/generate-sql-wasm.mjs +144 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/scripts/vendor-wasm.mjs +151 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/callees.ts +343 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/coordinates.ts +108 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/exports.ts +315 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/formatting.ts +104 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/imports.ts +42 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/index.ts +16 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/language.ts +116 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/node-at.ts +96 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/outline.ts +287 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/runtime.ts +237 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/session.ts +112 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/structure.ts +7 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/syntax-node.ts +13 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/tree-sitter.ts +306 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/types.ts +146 -0
- package/package.json +47 -0
- package/src/actions/affected-action.ts +310 -0
- package/src/actions/brief-action.ts +242 -0
- package/src/actions/callees-action.ts +134 -0
- package/src/actions/callers-action.ts +215 -0
- package/src/actions/implementations-action.ts +190 -0
- package/src/actions/index-action.ts +187 -0
- package/src/actions/pattern-action.ts +232 -0
- package/src/architecture.ts +367 -0
- package/src/brief-focused.ts +383 -0
- package/src/brief.ts +228 -0
- package/src/code-intelligence.ts +122 -0
- package/src/git-context.ts +65 -0
- package/src/guidance.ts +39 -0
- package/src/index.ts +28 -0
- package/src/resolve-target.ts +104 -0
- package/src/search-helpers.ts +283 -0
- package/src/target-resolution.ts +368 -0
- package/src/tool-actions.ts +109 -0
- package/src/types.ts +57 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
// Pattern action — bounded, scope-aware text search.
|
|
2
|
+
|
|
3
|
+
import type { RgMatch } from "../search-helpers.ts";
|
|
4
|
+
import {
|
|
5
|
+
escapeRegex,
|
|
6
|
+
groupByFile,
|
|
7
|
+
normalizePath,
|
|
8
|
+
runRipgrep,
|
|
9
|
+
runRipgrepDetailed,
|
|
10
|
+
} from "../search-helpers.ts";
|
|
11
|
+
import type { ActionParams } from "../tool-actions.ts";
|
|
12
|
+
import type { CodeIntelResult, SearchDetails } from "../types.ts";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Execute the bounded text-search action.
|
|
16
|
+
*
|
|
17
|
+
* Public `pattern` input is treated as a literal string by default. Callers can
|
|
18
|
+
* opt into raw ripgrep regex semantics with `regex: true`; malformed regex
|
|
19
|
+
* patterns are surfaced as explicit user-facing errors instead of being
|
|
20
|
+
* collapsed into a misleading no-match response.
|
|
21
|
+
*/
|
|
22
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: function has multiple distinct paths (validation, regex vs literal, summary vs detailed, zero vs results) that are clearer when explicit than when split; 18 is acceptable for the use case
|
|
23
|
+
export async function executePatternAction(
|
|
24
|
+
params: ActionParams,
|
|
25
|
+
cwd: string,
|
|
26
|
+
): Promise<CodeIntelResult> {
|
|
27
|
+
if (!params.pattern) {
|
|
28
|
+
return {
|
|
29
|
+
content: "**Error:** `pattern` action requires a `pattern` parameter.",
|
|
30
|
+
details: undefined,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const maxResults = params.maxResults ?? 8;
|
|
35
|
+
const contextLines = params.contextLines ?? 1;
|
|
36
|
+
const scopePath = params.path ? normalizePath(params.path, cwd) : cwd;
|
|
37
|
+
const relScope = params.path ?? ".";
|
|
38
|
+
|
|
39
|
+
const matches = params.regex
|
|
40
|
+
? getRegexMatches({
|
|
41
|
+
pattern: params.pattern,
|
|
42
|
+
scopePath,
|
|
43
|
+
cwd,
|
|
44
|
+
maxResults,
|
|
45
|
+
contextLines,
|
|
46
|
+
summary: params.summary,
|
|
47
|
+
})
|
|
48
|
+
: runRipgrep(escapeRegex(params.pattern), scopePath, cwd, {
|
|
49
|
+
maxMatches: params.summary ? undefined : maxResults * 3,
|
|
50
|
+
contextLines,
|
|
51
|
+
filterLowSignal: true,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
if (typeof matches === "string") {
|
|
55
|
+
const errorDetails: SearchDetails = {
|
|
56
|
+
confidence: "unavailable",
|
|
57
|
+
scope: params.path ?? null,
|
|
58
|
+
candidateCount: 0,
|
|
59
|
+
omittedCount: 0,
|
|
60
|
+
nextQueries: ["Fix the regex pattern and retry"],
|
|
61
|
+
};
|
|
62
|
+
return { content: matches, details: { type: "search", data: errorDetails } };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (matches.length === 0) {
|
|
66
|
+
const emptyDetails: SearchDetails = {
|
|
67
|
+
confidence: "heuristic",
|
|
68
|
+
scope: params.path ?? null,
|
|
69
|
+
candidateCount: 0,
|
|
70
|
+
omittedCount: 0,
|
|
71
|
+
nextQueries: params.regex
|
|
72
|
+
? ["Set `regex: false` for literal matching"]
|
|
73
|
+
: ["Set `regex: true` for regex matching"],
|
|
74
|
+
};
|
|
75
|
+
return {
|
|
76
|
+
content: `No matches found for \`${params.pattern}\` in \`${relScope}\`.`,
|
|
77
|
+
details: { type: "search", data: emptyDetails },
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let content: string;
|
|
82
|
+
if (params.summary) {
|
|
83
|
+
content = formatPatternSummary(params.pattern, relScope, matches, maxResults);
|
|
84
|
+
} else {
|
|
85
|
+
content = formatPatternResults(params.pattern, relScope, matches, maxResults);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const details: SearchDetails = {
|
|
89
|
+
confidence: "heuristic",
|
|
90
|
+
scope: params.path ?? null,
|
|
91
|
+
candidateCount: matches.length,
|
|
92
|
+
omittedCount: 0,
|
|
93
|
+
nextQueries: params.regex
|
|
94
|
+
? ["Set `regex: false` for literal matching"]
|
|
95
|
+
: ["Set `regex: true` for regex matching"],
|
|
96
|
+
};
|
|
97
|
+
return { content, details: { type: "search" as const, data: details } };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function getRegexMatches(options: {
|
|
101
|
+
pattern: string;
|
|
102
|
+
scopePath: string;
|
|
103
|
+
cwd: string;
|
|
104
|
+
maxResults: number;
|
|
105
|
+
contextLines: number;
|
|
106
|
+
summary?: boolean;
|
|
107
|
+
}): RgMatch[] | string {
|
|
108
|
+
const result = runRipgrepDetailed(options.pattern, options.scopePath, options.cwd, {
|
|
109
|
+
maxMatches: options.summary ? undefined : options.maxResults * 3,
|
|
110
|
+
contextLines: options.contextLines,
|
|
111
|
+
filterLowSignal: true,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
if (result.error) {
|
|
115
|
+
return formatRegexError(options.pattern, result.error);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return result.matches;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function formatRegexError(pattern: string, error: string): string {
|
|
122
|
+
const lines = error
|
|
123
|
+
.split("\n")
|
|
124
|
+
.map((line) => line.trim())
|
|
125
|
+
.filter(Boolean);
|
|
126
|
+
const detail =
|
|
127
|
+
[...lines]
|
|
128
|
+
.reverse()
|
|
129
|
+
.find((line) => line.startsWith("error:"))
|
|
130
|
+
?.replace(/^error:\s*/, "") ??
|
|
131
|
+
lines.at(-1) ??
|
|
132
|
+
"ripgrep rejected the regex.";
|
|
133
|
+
return `**Error:** Invalid regex pattern \`${pattern}\`: ${detail}`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function formatPatternResults(
|
|
137
|
+
pattern: string,
|
|
138
|
+
relScope: string,
|
|
139
|
+
matches: RgMatch[],
|
|
140
|
+
maxResults: number,
|
|
141
|
+
): string {
|
|
142
|
+
const lines: string[] = [];
|
|
143
|
+
lines.push(`# Pattern: \`${pattern}\``);
|
|
144
|
+
lines.push("");
|
|
145
|
+
lines.push(`**${matches.length} match${matches.length > 1 ? "es" : ""}** in \`${relScope}\``);
|
|
146
|
+
lines.push("");
|
|
147
|
+
|
|
148
|
+
const byFile = groupByFile(matches);
|
|
149
|
+
let shown = 0;
|
|
150
|
+
for (const [file, fileMatches] of byFile) {
|
|
151
|
+
if (shown >= maxResults) break;
|
|
152
|
+
lines.push(`### ${file}`);
|
|
153
|
+
const renderedLines = new Set<number>();
|
|
154
|
+
const matchLines = new Set(fileMatches.map((match) => match.line));
|
|
155
|
+
for (const m of fileMatches.slice(0, 5)) {
|
|
156
|
+
renderMatchWithContext(lines, m, renderedLines, matchLines);
|
|
157
|
+
}
|
|
158
|
+
if (fileMatches.length > 5) {
|
|
159
|
+
lines.push(`- _+${fileMatches.length - 5} more in this file_`);
|
|
160
|
+
}
|
|
161
|
+
lines.push("");
|
|
162
|
+
shown++;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (byFile.size > maxResults) {
|
|
166
|
+
lines.push(
|
|
167
|
+
`_+${byFile.size - maxResults} more files omitted. Narrow with \`path\` or increase \`maxResults\`._`,
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return lines.join("\n");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function formatPatternSummary(
|
|
175
|
+
pattern: string,
|
|
176
|
+
relScope: string,
|
|
177
|
+
matches: RgMatch[],
|
|
178
|
+
maxResults: number,
|
|
179
|
+
): string {
|
|
180
|
+
const byFile = groupByFile(matches);
|
|
181
|
+
const byDir = new Map<string, number>();
|
|
182
|
+
for (const [file] of byFile) {
|
|
183
|
+
const dir = file.includes("/") ? file.split("/").slice(0, -1).join("/") : ".";
|
|
184
|
+
byDir.set(dir, (byDir.get(dir) ?? 0) + 1);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const lines: string[] = [];
|
|
188
|
+
lines.push(`# Pattern Summary: \`${pattern}\``);
|
|
189
|
+
lines.push("");
|
|
190
|
+
lines.push(
|
|
191
|
+
`**${matches.length} match${matches.length > 1 ? "es" : ""}** across **${byFile.size} file${byFile.size !== 1 ? "s" : ""}** in \`${relScope}\``,
|
|
192
|
+
);
|
|
193
|
+
lines.push("");
|
|
194
|
+
|
|
195
|
+
const sortedDirs = [...byDir.entries()].sort((a, b) => b[1] - a[1]).slice(0, maxResults);
|
|
196
|
+
for (const [dir, count] of sortedDirs) {
|
|
197
|
+
lines.push(`- \`${dir}/\` — ${count} file${count !== 1 ? "s" : ""}`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (byDir.size > maxResults) {
|
|
201
|
+
lines.push(`- _+${byDir.size - maxResults} more directories_`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
lines.push("");
|
|
205
|
+
return lines.join("\n");
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function renderMatchWithContext(
|
|
209
|
+
lines: string[],
|
|
210
|
+
m: RgMatch,
|
|
211
|
+
renderedLines: Set<number>,
|
|
212
|
+
matchLines: Set<number>,
|
|
213
|
+
): void {
|
|
214
|
+
if (m.context) {
|
|
215
|
+
for (const c of m.context.filter((cx) => cx.line < m.line)) {
|
|
216
|
+
if (renderedLines.has(c.line) || matchLines.has(c.line)) continue;
|
|
217
|
+
lines.push(` L${c.line}: ${c.text.slice(0, 120)}`);
|
|
218
|
+
renderedLines.add(c.line);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (!renderedLines.has(m.line)) {
|
|
222
|
+
lines.push(`- L${m.line}: \`${m.text.slice(0, 120)}\``);
|
|
223
|
+
renderedLines.add(m.line);
|
|
224
|
+
}
|
|
225
|
+
if (m.context) {
|
|
226
|
+
for (const c of m.context.filter((cx) => cx.line > m.line)) {
|
|
227
|
+
if (renderedLines.has(c.line) || matchLines.has(c.line)) continue;
|
|
228
|
+
lines.push(` L${c.line}: ${c.text.slice(0, 120)}`);
|
|
229
|
+
renderedLines.add(c.line);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
// Shared architecture model — scans project metadata to build a structural model
|
|
2
|
+
// that powers both auto-injected overviews and on-demand briefs.
|
|
3
|
+
|
|
4
|
+
import * as fs from "node:fs";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
import { findProjectRoot, isWithinOrEqual, walkProject } from "@mrclrchtr/supi-core";
|
|
7
|
+
|
|
8
|
+
// ── Types ─────────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
export interface ArchitectureModel {
|
|
11
|
+
/** Absolute root directory of the project */
|
|
12
|
+
root: string;
|
|
13
|
+
/** Project name from root manifest */
|
|
14
|
+
name: string | null;
|
|
15
|
+
/** Project description from root manifest */
|
|
16
|
+
description: string | null;
|
|
17
|
+
/** Detected modules/packages */
|
|
18
|
+
modules: ModuleInfo[];
|
|
19
|
+
/** Internal dependency edges between modules */
|
|
20
|
+
edges: DependencyEdge[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ModuleInfo {
|
|
24
|
+
/** Package/module name */
|
|
25
|
+
name: string;
|
|
26
|
+
/** Short description from manifest */
|
|
27
|
+
description: string | null;
|
|
28
|
+
/** Absolute path to the module root */
|
|
29
|
+
root: string;
|
|
30
|
+
/** Relative path from project root */
|
|
31
|
+
relativePath: string;
|
|
32
|
+
/** Main entrypoint(s) from manifest */
|
|
33
|
+
entrypoints: string[];
|
|
34
|
+
/** Whether this is a leaf module (no internal dependents) */
|
|
35
|
+
isLeaf: boolean;
|
|
36
|
+
/** Internal dependency names */
|
|
37
|
+
internalDeps: string[];
|
|
38
|
+
/** External dependency names */
|
|
39
|
+
externalDeps: string[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface DependencyEdge {
|
|
43
|
+
/** Source module name */
|
|
44
|
+
from: string;
|
|
45
|
+
/** Target module name */
|
|
46
|
+
to: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ── Model building ────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
const PROJECT_MARKERS = [
|
|
52
|
+
"package.json",
|
|
53
|
+
"pnpm-workspace.yaml",
|
|
54
|
+
"deno.json",
|
|
55
|
+
"deno.jsonc",
|
|
56
|
+
"Cargo.toml",
|
|
57
|
+
"go.mod",
|
|
58
|
+
"pyproject.toml",
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Build an architecture model from project metadata.
|
|
63
|
+
* Starts from cheap manifest scanning — no deep AST or LSP analysis.
|
|
64
|
+
*/
|
|
65
|
+
export async function buildArchitectureModel(cwd: string): Promise<ArchitectureModel | null> {
|
|
66
|
+
const root = findProjectRoot(cwd, PROJECT_MARKERS, cwd);
|
|
67
|
+
|
|
68
|
+
// Read root manifest
|
|
69
|
+
const rootManifest = readPackageJson(root);
|
|
70
|
+
if (!rootManifest) {
|
|
71
|
+
// Try to detect at least some source structure
|
|
72
|
+
return buildMinimalModel(root);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const projectName = rootManifest.name ?? null;
|
|
76
|
+
const projectDescription = rootManifest.description ?? null;
|
|
77
|
+
|
|
78
|
+
// Detect workspace packages
|
|
79
|
+
const workspaceModules = await detectWorkspaceModules(root, rootManifest);
|
|
80
|
+
|
|
81
|
+
if (workspaceModules.length === 0) {
|
|
82
|
+
// Single-package project
|
|
83
|
+
return buildSinglePackageModel(root, rootManifest);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Build dependency edges between workspace modules
|
|
87
|
+
const moduleNames = new Set(workspaceModules.map((m) => m.name));
|
|
88
|
+
const edges: DependencyEdge[] = [];
|
|
89
|
+
|
|
90
|
+
for (const mod of workspaceModules) {
|
|
91
|
+
for (const dep of mod.internalDeps) {
|
|
92
|
+
if (moduleNames.has(dep)) {
|
|
93
|
+
edges.push({ from: mod.name, to: dep });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Mark leaf modules (no other module depends on them internally)
|
|
99
|
+
const dependedOn = new Set(edges.map((e) => e.to));
|
|
100
|
+
for (const mod of workspaceModules) {
|
|
101
|
+
mod.isLeaf = !dependedOn.has(mod.name);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
root,
|
|
106
|
+
name: projectName,
|
|
107
|
+
description: projectDescription,
|
|
108
|
+
modules: workspaceModules,
|
|
109
|
+
edges,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ── Workspace detection ───────────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
interface PackageJson {
|
|
116
|
+
name?: string;
|
|
117
|
+
description?: string;
|
|
118
|
+
main?: string;
|
|
119
|
+
module?: string;
|
|
120
|
+
exports?: unknown;
|
|
121
|
+
dependencies?: Record<string, string>;
|
|
122
|
+
devDependencies?: Record<string, string>;
|
|
123
|
+
peerDependencies?: Record<string, string>;
|
|
124
|
+
workspaces?: string[] | { packages?: string[] };
|
|
125
|
+
pi?: { extensions?: string[] };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function readPackageJson(dir: string): PackageJson | null {
|
|
129
|
+
try {
|
|
130
|
+
const raw = fs.readFileSync(path.join(dir, "package.json"), "utf-8");
|
|
131
|
+
return JSON.parse(raw);
|
|
132
|
+
} catch {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: workspace package detection with manifest parsing
|
|
138
|
+
async function detectWorkspaceModules(
|
|
139
|
+
root: string,
|
|
140
|
+
rootManifest: PackageJson,
|
|
141
|
+
): Promise<ModuleInfo[]> {
|
|
142
|
+
// Check for pnpm-workspace.yaml
|
|
143
|
+
let workspaceGlobs: string[] = [];
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
const pnpmWs = fs.readFileSync(path.join(root, "pnpm-workspace.yaml"), "utf-8");
|
|
147
|
+
// Simple YAML parsing for packages array
|
|
148
|
+
const packagesMatch = pnpmWs.match(/packages:\s*\n((?:\s*-\s*.+\n?)*)/);
|
|
149
|
+
if (packagesMatch) {
|
|
150
|
+
workspaceGlobs = packagesMatch[1]
|
|
151
|
+
.split("\n")
|
|
152
|
+
.map((line) => line.replace(/^\s*-\s*/, "").replace(/['"\s]/g, ""))
|
|
153
|
+
.filter(Boolean);
|
|
154
|
+
}
|
|
155
|
+
} catch {
|
|
156
|
+
// Try npm/yarn workspaces from package.json
|
|
157
|
+
const ws = rootManifest.workspaces;
|
|
158
|
+
if (Array.isArray(ws)) {
|
|
159
|
+
workspaceGlobs = ws;
|
|
160
|
+
} else if (ws && Array.isArray(ws.packages)) {
|
|
161
|
+
workspaceGlobs = ws.packages;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (workspaceGlobs.length === 0) return [];
|
|
166
|
+
|
|
167
|
+
// Resolve workspace globs to actual package directories
|
|
168
|
+
const modules: ModuleInfo[] = [];
|
|
169
|
+
const resolvedDirs = resolveWorkspaceGlobs(root, workspaceGlobs);
|
|
170
|
+
|
|
171
|
+
for (const dir of resolvedDirs) {
|
|
172
|
+
const manifest = readPackageJson(dir);
|
|
173
|
+
if (!manifest?.name) continue;
|
|
174
|
+
|
|
175
|
+
const relativePath = path.relative(root, dir);
|
|
176
|
+
const allDeps = {
|
|
177
|
+
...manifest.dependencies,
|
|
178
|
+
...manifest.peerDependencies,
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
// Separate internal vs external deps
|
|
182
|
+
const internalDeps: string[] = [];
|
|
183
|
+
const externalDeps: string[] = [];
|
|
184
|
+
for (const depName of Object.keys(allDeps)) {
|
|
185
|
+
// Internal if it matches a workspace package pattern
|
|
186
|
+
const depVersion = allDeps[depName];
|
|
187
|
+
if (depVersion?.startsWith("workspace:")) {
|
|
188
|
+
internalDeps.push(depName);
|
|
189
|
+
} else {
|
|
190
|
+
externalDeps.push(depName);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Detect entrypoints
|
|
195
|
+
const entrypoints: string[] = [];
|
|
196
|
+
if (manifest.pi?.extensions) {
|
|
197
|
+
entrypoints.push(...manifest.pi.extensions);
|
|
198
|
+
} else if (manifest.main) {
|
|
199
|
+
entrypoints.push(manifest.main);
|
|
200
|
+
} else if (manifest.module) {
|
|
201
|
+
entrypoints.push(manifest.module);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
modules.push({
|
|
205
|
+
name: manifest.name,
|
|
206
|
+
description: manifest.description ?? null,
|
|
207
|
+
root: dir,
|
|
208
|
+
relativePath,
|
|
209
|
+
entrypoints,
|
|
210
|
+
isLeaf: false, // Will be computed after edges
|
|
211
|
+
internalDeps,
|
|
212
|
+
externalDeps,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return modules;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function resolveWorkspaceGlobs(root: string, globs: string[]): string[] {
|
|
220
|
+
const dirs: string[] = [];
|
|
221
|
+
|
|
222
|
+
for (const glob of globs) {
|
|
223
|
+
if (glob.includes("*")) {
|
|
224
|
+
// Expand glob: "packages/*" -> list all dirs under packages/
|
|
225
|
+
const prefix = glob.replace(/\/?\*.*$/, "");
|
|
226
|
+
const baseDir = path.join(root, prefix);
|
|
227
|
+
const recursive = glob.includes("**");
|
|
228
|
+
try {
|
|
229
|
+
collectPackageDirs(baseDir, dirs, recursive ? 5 : 0);
|
|
230
|
+
} catch {
|
|
231
|
+
// Glob base directory doesn't exist
|
|
232
|
+
}
|
|
233
|
+
} else {
|
|
234
|
+
// Exact path
|
|
235
|
+
const fullPath = path.join(root, glob);
|
|
236
|
+
if (fs.existsSync(path.join(fullPath, "package.json"))) {
|
|
237
|
+
dirs.push(fullPath);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return dirs;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Collect directories containing a package.json.
|
|
247
|
+
* When depth > 0, recurse into subdirectories (for `**` globs).
|
|
248
|
+
*/
|
|
249
|
+
function collectPackageDirs(baseDir: string, dirs: string[], depth: number): void {
|
|
250
|
+
let entries: fs.Dirent[];
|
|
251
|
+
try {
|
|
252
|
+
entries = fs.readdirSync(baseDir, { withFileTypes: true });
|
|
253
|
+
} catch {
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
for (const entry of entries) {
|
|
257
|
+
if (!entry.isDirectory()) continue;
|
|
258
|
+
if (entry.name.startsWith(".")) continue;
|
|
259
|
+
if (entry.name === "node_modules") continue;
|
|
260
|
+
const fullPath = path.join(baseDir, entry.name);
|
|
261
|
+
if (fs.existsSync(path.join(fullPath, "package.json"))) {
|
|
262
|
+
dirs.push(fullPath);
|
|
263
|
+
} else if (depth > 0) {
|
|
264
|
+
collectPackageDirs(fullPath, dirs, depth - 1);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ── Fallback models ───────────────────────────────────────────────────
|
|
270
|
+
|
|
271
|
+
function buildSinglePackageModel(root: string, manifest: PackageJson): ArchitectureModel {
|
|
272
|
+
const entrypoints: string[] = [];
|
|
273
|
+
if (manifest.pi?.extensions) {
|
|
274
|
+
entrypoints.push(...manifest.pi.extensions);
|
|
275
|
+
} else if (manifest.main) {
|
|
276
|
+
entrypoints.push(manifest.main);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const mod: ModuleInfo = {
|
|
280
|
+
name: manifest.name ?? path.basename(root),
|
|
281
|
+
description: manifest.description ?? null,
|
|
282
|
+
root,
|
|
283
|
+
relativePath: ".",
|
|
284
|
+
entrypoints,
|
|
285
|
+
isLeaf: true,
|
|
286
|
+
internalDeps: [],
|
|
287
|
+
externalDeps: Object.keys({
|
|
288
|
+
...manifest.dependencies,
|
|
289
|
+
...manifest.peerDependencies,
|
|
290
|
+
}),
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
return {
|
|
294
|
+
root,
|
|
295
|
+
name: manifest.name ?? null,
|
|
296
|
+
description: manifest.description ?? null,
|
|
297
|
+
modules: [mod],
|
|
298
|
+
edges: [],
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function buildMinimalModel(root: string): ArchitectureModel | null {
|
|
303
|
+
// Check if there are any recognizable source files
|
|
304
|
+
let hasSource = false;
|
|
305
|
+
walkProject(root, 2, (_dir, entries) => {
|
|
306
|
+
for (const name of entries) {
|
|
307
|
+
if (
|
|
308
|
+
name.endsWith(".ts") ||
|
|
309
|
+
name.endsWith(".js") ||
|
|
310
|
+
name.endsWith(".tsx") ||
|
|
311
|
+
name.endsWith(".jsx") ||
|
|
312
|
+
name.endsWith(".py") ||
|
|
313
|
+
name.endsWith(".rs") ||
|
|
314
|
+
name.endsWith(".go")
|
|
315
|
+
) {
|
|
316
|
+
hasSource = true;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
if (!hasSource) return null;
|
|
322
|
+
|
|
323
|
+
return {
|
|
324
|
+
root,
|
|
325
|
+
name: path.basename(root),
|
|
326
|
+
description: null,
|
|
327
|
+
modules: [],
|
|
328
|
+
edges: [],
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// ── Query helpers ─────────────────────────────────────────────────────
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Find the module containing a given file path.
|
|
336
|
+
* Returns the most specific (deepest) matching module.
|
|
337
|
+
*/
|
|
338
|
+
export function findModuleForPath(model: ArchitectureModel, filePath: string): ModuleInfo | null {
|
|
339
|
+
const resolved = path.resolve(filePath);
|
|
340
|
+
let best: ModuleInfo | null = null;
|
|
341
|
+
|
|
342
|
+
for (const mod of model.modules) {
|
|
343
|
+
if (isWithinOrEqual(mod.root, resolved)) {
|
|
344
|
+
if (!best || mod.root.length > best.root.length) {
|
|
345
|
+
best = mod;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return best;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Get modules that depend on a given module (reverse dependencies / dependents).
|
|
355
|
+
*/
|
|
356
|
+
export function getDependents(model: ArchitectureModel, moduleName: string): ModuleInfo[] {
|
|
357
|
+
return model.modules.filter((m) => m.internalDeps.includes(moduleName));
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Get the internal dependencies of a module as ModuleInfo objects.
|
|
362
|
+
*/
|
|
363
|
+
export function getDependencies(model: ArchitectureModel, moduleName: string): ModuleInfo[] {
|
|
364
|
+
const mod = model.modules.find((m) => m.name === moduleName);
|
|
365
|
+
if (!mod) return [];
|
|
366
|
+
return model.modules.filter((m) => mod.internalDeps.includes(m.name));
|
|
367
|
+
}
|