supipowers 1.2.6 → 1.5.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 +118 -56
- package/bin/install.ts +48 -128
- package/package.json +11 -3
- package/skills/code-review/SKILL.md +137 -40
- package/skills/context-mode/SKILL.md +67 -56
- package/skills/creating-supi-agents/SKILL.md +204 -0
- package/skills/debugging/SKILL.md +86 -40
- package/skills/fix-pr/SKILL.md +96 -65
- package/skills/planning/SKILL.md +103 -46
- package/skills/qa-strategy/SKILL.md +68 -46
- package/skills/receiving-code-review/SKILL.md +60 -53
- package/skills/release/SKILL.md +111 -39
- package/skills/tdd/SKILL.md +118 -67
- package/skills/verification/SKILL.md +71 -37
- package/src/bootstrap.ts +27 -5
- package/src/commands/agents.ts +249 -0
- package/src/commands/ai-review.ts +1113 -0
- package/src/commands/config.ts +224 -95
- package/src/commands/doctor.ts +19 -13
- package/src/commands/fix-pr.ts +8 -11
- package/src/commands/generate.ts +200 -0
- package/src/commands/model-picker.ts +5 -15
- package/src/commands/model.ts +4 -5
- package/src/commands/optimize-context.ts +202 -0
- package/src/commands/plan.ts +148 -92
- package/src/commands/qa.ts +14 -23
- package/src/commands/release.ts +504 -275
- package/src/commands/review.ts +643 -86
- package/src/commands/status.ts +44 -17
- package/src/commands/supi.ts +69 -41
- package/src/commands/update.ts +57 -2
- package/src/config/defaults.ts +6 -39
- package/src/config/loader.ts +388 -40
- package/src/config/model-resolver.ts +26 -22
- package/src/config/schema.ts +113 -48
- package/src/context/analyzer.ts +61 -2
- package/src/context/optimizer.ts +199 -0
- package/src/context-mode/compressor.ts +14 -11
- package/src/context-mode/detector.ts +16 -54
- package/src/context-mode/event-extractor.ts +45 -16
- package/src/context-mode/event-store.ts +225 -16
- package/src/context-mode/hooks.ts +195 -22
- package/src/context-mode/knowledge/chunker.ts +235 -0
- package/src/context-mode/knowledge/store.ts +187 -0
- package/src/context-mode/routing.ts +12 -23
- package/src/context-mode/sandbox/executor.ts +183 -0
- package/src/context-mode/sandbox/runners.ts +40 -0
- package/src/context-mode/snapshot-builder.ts +243 -7
- package/src/context-mode/tools.ts +440 -0
- package/src/context-mode/web/fetcher.ts +117 -0
- package/src/context-mode/web/html-to-md.ts +293 -0
- package/src/debug/logger.ts +107 -0
- package/src/deps/registry.ts +0 -20
- package/src/docs/drift.ts +454 -0
- package/src/fix-pr/fetch-comments.ts +66 -0
- package/src/git/commit-msg.ts +2 -1
- package/src/git/commit.ts +123 -141
- package/src/git/conventions.ts +2 -2
- package/src/git/status.ts +4 -1
- package/src/lsp/bridge.ts +138 -12
- package/src/planning/approval-flow.ts +125 -19
- package/src/planning/plan-writer-prompt.ts +4 -11
- package/src/planning/planning-ask-tool.ts +81 -0
- package/src/planning/prompt-builder.ts +9 -169
- package/src/planning/system-prompt.ts +290 -0
- package/src/platform/omp.ts +50 -4
- package/src/platform/progress.ts +182 -0
- package/src/platform/test-utils.ts +4 -1
- package/src/platform/tui-colors.ts +30 -0
- package/src/platform/types.ts +1 -0
- package/src/qa/detect-app-type.ts +102 -0
- package/src/qa/discover-routes.ts +353 -0
- package/src/quality/ai-session.ts +96 -0
- package/src/quality/ai-setup.ts +86 -0
- package/src/quality/gates/ai-review.ts +129 -0
- package/src/quality/gates/build.ts +8 -0
- package/src/quality/gates/command.ts +150 -0
- package/src/quality/gates/format.ts +28 -0
- package/src/quality/gates/lint.ts +22 -0
- package/src/quality/gates/lsp-diagnostics.ts +84 -0
- package/src/quality/gates/test-suite.ts +8 -0
- package/src/quality/gates/typecheck.ts +22 -0
- package/src/quality/registry.ts +25 -0
- package/src/quality/review-gates.ts +33 -0
- package/src/quality/runner.ts +268 -0
- package/src/quality/schemas.ts +48 -0
- package/src/quality/setup.ts +227 -0
- package/src/release/changelog.ts +7 -3
- package/src/release/channels/custom.ts +43 -0
- package/src/release/channels/gitea.ts +35 -0
- package/src/release/channels/github.ts +35 -0
- package/src/release/channels/gitlab.ts +35 -0
- package/src/release/channels/registry.ts +52 -0
- package/src/release/channels/types.ts +27 -0
- package/src/release/detector.ts +10 -63
- package/src/release/executor.ts +61 -51
- package/src/release/prompt.ts +38 -38
- package/src/release/version.ts +129 -10
- package/src/review/agent-loader.ts +331 -0
- package/src/review/consolidator.ts +180 -0
- package/src/review/default-agents/correctness.md +72 -0
- package/src/review/default-agents/maintainability.md +64 -0
- package/src/review/default-agents/security.md +67 -0
- package/src/review/fixer.ts +219 -0
- package/src/review/multi-agent-runner.ts +135 -0
- package/src/review/output.ts +147 -0
- package/src/review/prompts/agent-review-wrapper.md +36 -0
- package/src/review/prompts/fix-findings.md +32 -0
- package/src/review/prompts/fix-output-schema.md +18 -0
- package/src/review/prompts/invalid-output-retry.md +22 -0
- package/src/review/prompts/output-instructions.md +14 -0
- package/src/review/prompts/review-output-schema.md +38 -0
- package/src/review/prompts/single-review.md +53 -0
- package/src/review/prompts/validation-review.md +30 -0
- package/src/review/runner.ts +128 -0
- package/src/review/scope.ts +353 -0
- package/src/review/template.ts +15 -0
- package/src/review/types.ts +296 -0
- package/src/review/validator.ts +160 -0
- package/src/storage/plans.ts +5 -3
- package/src/storage/reports.ts +50 -7
- package/src/storage/review-sessions.ts +117 -0
- package/src/text.ts +19 -0
- package/src/types.ts +336 -26
- package/src/utils/paths.ts +39 -0
- package/src/visual/companion.ts +5 -3
- package/src/visual/start-server.ts +101 -0
- package/src/visual/stop-server.ts +39 -0
- package/bin/ctx-mode-wrapper.mjs +0 -66
- package/src/config/profiles.ts +0 -64
- package/src/context-mode/installer.ts +0 -38
- package/src/quality/ai-review-gate.ts +0 -43
- package/src/quality/gate-runner.ts +0 -67
- package/src/quality/lsp-gate.ts +0 -24
- package/src/quality/test-gate.ts +0 -39
- package/src/visual/scripts/start-server.sh +0 -98
- package/src/visual/scripts/stop-server.sh +0 -21
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
|
|
4
|
+
export interface AppDetection {
|
|
5
|
+
type: string;
|
|
6
|
+
devCommand: string;
|
|
7
|
+
port: number;
|
|
8
|
+
baseUrl: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function fileExists(cwd: string, ...segments: string[]): boolean {
|
|
12
|
+
return fs.existsSync(path.join(cwd, ...segments));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function dirExists(cwd: string, ...segments: string[]): boolean {
|
|
16
|
+
try {
|
|
17
|
+
return fs.statSync(path.join(cwd, ...segments)).isDirectory();
|
|
18
|
+
} catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function readJson(filePath: string): Record<string, unknown> | null {
|
|
24
|
+
try {
|
|
25
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
26
|
+
} catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function detectAppType(cwd: string): AppDetection {
|
|
32
|
+
let type = "generic";
|
|
33
|
+
let devCommand = "npm run dev";
|
|
34
|
+
let port = 3000;
|
|
35
|
+
|
|
36
|
+
const hasNextConfig =
|
|
37
|
+
fileExists(cwd, "next.config.js") ||
|
|
38
|
+
fileExists(cwd, "next.config.mjs") ||
|
|
39
|
+
fileExists(cwd, "next.config.ts");
|
|
40
|
+
const hasNextAppDir = dirExists(cwd, "app") || dirExists(cwd, "src", "app");
|
|
41
|
+
const hasNextPagesDir = dirExists(cwd, "pages") || dirExists(cwd, "src", "pages");
|
|
42
|
+
|
|
43
|
+
if (hasNextConfig || hasNextAppDir || hasNextPagesDir) {
|
|
44
|
+
// Default nextjs-app; only switch to pages if pages dir exists and app dir does not
|
|
45
|
+
if (hasNextAppDir) {
|
|
46
|
+
type = "nextjs-app";
|
|
47
|
+
} else if (hasNextPagesDir) {
|
|
48
|
+
type = "nextjs-pages";
|
|
49
|
+
} else {
|
|
50
|
+
type = "nextjs-app";
|
|
51
|
+
}
|
|
52
|
+
port = 3000;
|
|
53
|
+
} else if (
|
|
54
|
+
fileExists(cwd, "vite.config.ts") ||
|
|
55
|
+
fileExists(cwd, "vite.config.js") ||
|
|
56
|
+
fileExists(cwd, "vite.config.mjs")
|
|
57
|
+
) {
|
|
58
|
+
type = "vite";
|
|
59
|
+
port = 5173;
|
|
60
|
+
} else if (fileExists(cwd, "angular.json")) {
|
|
61
|
+
type = "generic";
|
|
62
|
+
devCommand = "npm start";
|
|
63
|
+
port = 4200;
|
|
64
|
+
} else if (fileExists(cwd, "package.json")) {
|
|
65
|
+
try {
|
|
66
|
+
const raw = fs.readFileSync(path.join(cwd, "package.json"), "utf8");
|
|
67
|
+
if (raw.includes('"express"')) {
|
|
68
|
+
type = "express";
|
|
69
|
+
port = 3000;
|
|
70
|
+
}
|
|
71
|
+
} catch {
|
|
72
|
+
// proceed with defaults
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Override devCommand from package.json scripts
|
|
77
|
+
const pkgPath = path.join(cwd, "package.json");
|
|
78
|
+
const pkg = readJson(pkgPath);
|
|
79
|
+
if (pkg) {
|
|
80
|
+
const scripts = pkg.scripts as Record<string, string> | undefined;
|
|
81
|
+
if (scripts?.dev) {
|
|
82
|
+
devCommand = "npm run dev";
|
|
83
|
+
} else if (scripts?.start) {
|
|
84
|
+
devCommand = "npm start";
|
|
85
|
+
} else if (scripts?.serve) {
|
|
86
|
+
devCommand = "npm run serve";
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Extract port from dev script text
|
|
90
|
+
const scriptText = scripts?.dev ?? scripts?.start ?? "";
|
|
91
|
+
if (scriptText) {
|
|
92
|
+
const portMatch = scriptText.match(/(?:--port|PORT=)\s*(\d+)/);
|
|
93
|
+
if (portMatch) {
|
|
94
|
+
port = parseInt(portMatch[1], 10);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const baseUrl = `http://localhost:${port}`;
|
|
100
|
+
|
|
101
|
+
return { type, devCommand, port, baseUrl };
|
|
102
|
+
}
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
|
|
4
|
+
export interface DiscoveredRoute {
|
|
5
|
+
path: string;
|
|
6
|
+
file: string;
|
|
7
|
+
type: "page" | "api" | "auth";
|
|
8
|
+
hasForm: boolean;
|
|
9
|
+
methods?: string[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const CODE_EXTENSIONS = [".tsx", ".jsx", ".ts", ".js"];
|
|
13
|
+
|
|
14
|
+
const FORM_PATTERNS =
|
|
15
|
+
/<form|onSubmit|handleSubmit|useForm|formik|react-hook-form/;
|
|
16
|
+
|
|
17
|
+
const HTTP_METHODS_RE = /\b(GET|POST|PUT|PATCH|DELETE)\b/g;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Recursively walk `dir`, returning relative file paths whose extension
|
|
21
|
+
* is in `extensions`. All returned paths use forward slashes.
|
|
22
|
+
*/
|
|
23
|
+
function walkDir(dir: string, extensions: string[]): string[] {
|
|
24
|
+
if (!fs.existsSync(dir)) return [];
|
|
25
|
+
|
|
26
|
+
const results: string[] = [];
|
|
27
|
+
const extSet = new Set(extensions);
|
|
28
|
+
|
|
29
|
+
function recurse(current: string): void {
|
|
30
|
+
let entries: fs.Dirent[];
|
|
31
|
+
try {
|
|
32
|
+
entries = fs.readdirSync(current, { withFileTypes: true });
|
|
33
|
+
} catch {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
for (const entry of entries) {
|
|
38
|
+
const full = path.join(current, entry.name);
|
|
39
|
+
if (entry.isDirectory()) {
|
|
40
|
+
recurse(full);
|
|
41
|
+
} else if (entry.isFile() && extSet.has(path.extname(entry.name))) {
|
|
42
|
+
results.push(path.relative(dir, full).replace(/\\/g, "/"));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
recurse(dir);
|
|
48
|
+
return results;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Try to read file content; return empty string on failure. */
|
|
52
|
+
function safeRead(filePath: string): string {
|
|
53
|
+
try {
|
|
54
|
+
return fs.readFileSync(filePath, "utf-8");
|
|
55
|
+
} catch {
|
|
56
|
+
return "";
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function hasForm(content: string): boolean {
|
|
61
|
+
return FORM_PATTERNS.test(content);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function extractHttpMethods(content: string): string[] {
|
|
65
|
+
const matches = content.match(HTTP_METHODS_RE);
|
|
66
|
+
if (!matches || matches.length === 0) return ["GET"];
|
|
67
|
+
return [...new Set(matches)];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ────────────────────────────────────────────────
|
|
71
|
+
// Framework scanners
|
|
72
|
+
// ────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
function scanNextjsApp(cwd: string): DiscoveredRoute[] {
|
|
75
|
+
const routes: DiscoveredRoute[] = [];
|
|
76
|
+
|
|
77
|
+
for (const prefix of ["app", "src/app"]) {
|
|
78
|
+
const dir = path.join(cwd, prefix);
|
|
79
|
+
if (!fs.existsSync(dir)) continue;
|
|
80
|
+
|
|
81
|
+
const files = walkDir(dir, CODE_EXTENSIONS);
|
|
82
|
+
|
|
83
|
+
for (const rel of files) {
|
|
84
|
+
const basename = path.basename(rel);
|
|
85
|
+
const fullPath = path.join(dir, rel).replace(/\\/g, "/");
|
|
86
|
+
|
|
87
|
+
// page files → page routes
|
|
88
|
+
if (/^page\.(tsx|jsx|ts|js)$/.test(basename)) {
|
|
89
|
+
const routePath = rel.replace(/(^|\/)page\.\w+$/, "");
|
|
90
|
+
let route = routePath ? "/" + routePath : "/";
|
|
91
|
+
// strip route groups: /(groupname)
|
|
92
|
+
route = route.replace(/\/\([^)]*\)/g, "");
|
|
93
|
+
if (route === "" || route === "/") {
|
|
94
|
+
route = "/";
|
|
95
|
+
}
|
|
96
|
+
// normalize double slashes
|
|
97
|
+
route = route.replace(/\/+/g, "/");
|
|
98
|
+
|
|
99
|
+
const content = safeRead(path.join(dir, rel));
|
|
100
|
+
routes.push({
|
|
101
|
+
path: route,
|
|
102
|
+
file: (prefix + "/" + rel).replace(/\\/g, "/"),
|
|
103
|
+
type: "page",
|
|
104
|
+
hasForm: hasForm(content),
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// route files → API routes
|
|
109
|
+
if (/^route\.(tsx|ts|js)$/.test(basename)) {
|
|
110
|
+
const route = "/" + rel.replace(/\/route\.\w+$/, "");
|
|
111
|
+
const content = safeRead(path.join(dir, rel));
|
|
112
|
+
const methods = extractHttpMethods(content);
|
|
113
|
+
|
|
114
|
+
routes.push({
|
|
115
|
+
path: route.replace(/\/+/g, "/"),
|
|
116
|
+
file: (prefix + "/" + rel).replace(/\\/g, "/"),
|
|
117
|
+
type: "api",
|
|
118
|
+
hasForm: false,
|
|
119
|
+
methods,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return routes;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function scanNextjsPages(cwd: string): DiscoveredRoute[] {
|
|
129
|
+
const routes: DiscoveredRoute[] = [];
|
|
130
|
+
const SKIP_BASENAMES = /^_(app|document|error)\./;
|
|
131
|
+
|
|
132
|
+
for (const prefix of ["pages", "src/pages"]) {
|
|
133
|
+
const dir = path.join(cwd, prefix);
|
|
134
|
+
if (!fs.existsSync(dir)) continue;
|
|
135
|
+
|
|
136
|
+
const files = walkDir(dir, CODE_EXTENSIONS);
|
|
137
|
+
|
|
138
|
+
for (const rel of files) {
|
|
139
|
+
const basename = path.basename(rel);
|
|
140
|
+
if (SKIP_BASENAMES.test(basename)) continue;
|
|
141
|
+
|
|
142
|
+
const filePath = (prefix + "/" + rel).replace(/\\/g, "/");
|
|
143
|
+
const fullDisk = path.join(dir, rel);
|
|
144
|
+
|
|
145
|
+
// Strip extension, then replace /index with /
|
|
146
|
+
const routePath = rel
|
|
147
|
+
.replace(/\.(tsx|jsx|ts|js)$/, "")
|
|
148
|
+
.replace(/(^|\/)index$/, "");
|
|
149
|
+
let route = routePath ? "/" + routePath : "/";
|
|
150
|
+
if (route === "/" || route === "") route = "/";
|
|
151
|
+
route = route.replace(/\/+/g, "/");
|
|
152
|
+
|
|
153
|
+
if (rel.includes("api/")) {
|
|
154
|
+
const content = safeRead(fullDisk);
|
|
155
|
+
const methods = extractHttpMethods(content);
|
|
156
|
+
routes.push({
|
|
157
|
+
path: route,
|
|
158
|
+
file: filePath,
|
|
159
|
+
type: "api",
|
|
160
|
+
hasForm: false,
|
|
161
|
+
methods,
|
|
162
|
+
});
|
|
163
|
+
} else {
|
|
164
|
+
const content = safeRead(fullDisk);
|
|
165
|
+
routes.push({
|
|
166
|
+
path: route,
|
|
167
|
+
file: filePath,
|
|
168
|
+
type: "page",
|
|
169
|
+
hasForm: hasForm(content),
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return routes;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function scanReactRouter(cwd: string): DiscoveredRoute[] {
|
|
179
|
+
const routes: DiscoveredRoute[] = [];
|
|
180
|
+
const srcDir = path.join(cwd, "src");
|
|
181
|
+
if (!fs.existsSync(srcDir)) return routes;
|
|
182
|
+
|
|
183
|
+
const files = walkDir(srcDir, CODE_EXTENSIONS);
|
|
184
|
+
|
|
185
|
+
for (const rel of files) {
|
|
186
|
+
const fullDisk = path.join(srcDir, rel);
|
|
187
|
+
const content = safeRead(fullDisk);
|
|
188
|
+
if (!content) continue;
|
|
189
|
+
|
|
190
|
+
if (!/<Route|createBrowserRouter|useRoutes/.test(content)) continue;
|
|
191
|
+
|
|
192
|
+
const filePath = ("src/" + rel).replace(/\\/g, "/");
|
|
193
|
+
|
|
194
|
+
// Extract path="..." and path: '...' patterns
|
|
195
|
+
const pathMatches = content.matchAll(/path[=:]\s*["']([^"']+)["']/g);
|
|
196
|
+
for (const m of pathMatches) {
|
|
197
|
+
const routePath = m[1];
|
|
198
|
+
if (!routePath) continue;
|
|
199
|
+
routes.push({
|
|
200
|
+
path: routePath,
|
|
201
|
+
file: filePath,
|
|
202
|
+
type: "page",
|
|
203
|
+
hasForm: hasForm(content),
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return routes;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function scanExpress(cwd: string): DiscoveredRoute[] {
|
|
212
|
+
const routes: DiscoveredRoute[] = [];
|
|
213
|
+
const EXPRESS_RE =
|
|
214
|
+
/\.(get|post|put|patch|delete)\s*\(\s*["'](\/?[^"']*)["']/gi;
|
|
215
|
+
|
|
216
|
+
for (const dirName of ["src", "routes", "server"]) {
|
|
217
|
+
const dir = path.join(cwd, dirName);
|
|
218
|
+
if (!fs.existsSync(dir)) continue;
|
|
219
|
+
|
|
220
|
+
const files = walkDir(dir, [".ts", ".js"]);
|
|
221
|
+
|
|
222
|
+
for (const rel of files) {
|
|
223
|
+
const fullDisk = path.join(dir, rel);
|
|
224
|
+
const content = safeRead(fullDisk);
|
|
225
|
+
if (!content) continue;
|
|
226
|
+
|
|
227
|
+
const matches = content.matchAll(EXPRESS_RE);
|
|
228
|
+
for (const m of matches) {
|
|
229
|
+
let method = m[1].toUpperCase();
|
|
230
|
+
const routePath = m[2];
|
|
231
|
+
if (!routePath || !routePath.startsWith("/")) continue;
|
|
232
|
+
|
|
233
|
+
routes.push({
|
|
234
|
+
path: routePath,
|
|
235
|
+
file: (dirName + "/" + rel).replace(/\\/g, "/"),
|
|
236
|
+
type: "api",
|
|
237
|
+
hasForm: false,
|
|
238
|
+
methods: [method],
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return routes;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function scanGeneric(cwd: string): DiscoveredRoute[] {
|
|
248
|
+
const routes: DiscoveredRoute[] = [];
|
|
249
|
+
const srcDir = path.join(cwd, "src");
|
|
250
|
+
if (!fs.existsSync(srcDir)) return routes;
|
|
251
|
+
|
|
252
|
+
const files = walkDir(srcDir, CODE_EXTENSIONS);
|
|
253
|
+
|
|
254
|
+
for (const rel of files) {
|
|
255
|
+
const fullDisk = path.join(srcDir, rel);
|
|
256
|
+
const content = safeRead(fullDisk);
|
|
257
|
+
if (!content) continue;
|
|
258
|
+
|
|
259
|
+
if (!/<Route|createBrowserRouter|useRoutes/.test(content)) continue;
|
|
260
|
+
|
|
261
|
+
const filePath = ("src/" + rel).replace(/\\/g, "/");
|
|
262
|
+
|
|
263
|
+
const pathMatches = content.matchAll(/path[=:]\s*["']([^"']+)["']/g);
|
|
264
|
+
for (const m of pathMatches) {
|
|
265
|
+
const routePath = m[1];
|
|
266
|
+
if (!routePath) continue;
|
|
267
|
+
routes.push({
|
|
268
|
+
path: routePath,
|
|
269
|
+
file: filePath,
|
|
270
|
+
type: "page",
|
|
271
|
+
hasForm: hasForm(content),
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return routes;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ────────────────────────────────────────────────
|
|
280
|
+
// Auth scan (runs for all framework types)
|
|
281
|
+
// ────────────────────────────────────────────────
|
|
282
|
+
|
|
283
|
+
const AUTH_FILENAME_RE = /(auth|login|signup|register)/i;
|
|
284
|
+
const AUTH_EXCLUDE_RE = /(node_modules|\.next|\.test\.|\.spec\.)/;
|
|
285
|
+
|
|
286
|
+
function scanAuth(cwd: string): DiscoveredRoute[] {
|
|
287
|
+
const routes: DiscoveredRoute[] = [];
|
|
288
|
+
const srcDir = path.join(cwd, "src");
|
|
289
|
+
if (!fs.existsSync(srcDir)) return routes;
|
|
290
|
+
|
|
291
|
+
const files = walkDir(srcDir, CODE_EXTENSIONS);
|
|
292
|
+
|
|
293
|
+
for (const rel of files) {
|
|
294
|
+
const fullRel = "src/" + rel;
|
|
295
|
+
|
|
296
|
+
// Exclude node_modules, .next, test/spec files
|
|
297
|
+
if (AUTH_EXCLUDE_RE.test(fullRel)) continue;
|
|
298
|
+
|
|
299
|
+
const basename = path.basename(rel);
|
|
300
|
+
if (!AUTH_FILENAME_RE.test(basename)) continue;
|
|
301
|
+
|
|
302
|
+
const fullDisk = path.join(srcDir, rel);
|
|
303
|
+
const content = safeRead(fullDisk);
|
|
304
|
+
|
|
305
|
+
// Route hint from filename without extension
|
|
306
|
+
const nameWithoutExt = basename.replace(/\.(tsx|jsx|ts|js)$/, "");
|
|
307
|
+
|
|
308
|
+
routes.push({
|
|
309
|
+
path: "/" + nameWithoutExt,
|
|
310
|
+
file: fullRel.replace(/\\/g, "/"),
|
|
311
|
+
type: "auth",
|
|
312
|
+
hasForm: hasForm(content),
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return routes;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// ────────────────────────────────────────────────
|
|
320
|
+
// Main entry point
|
|
321
|
+
// ────────────────────────────────────────────────
|
|
322
|
+
|
|
323
|
+
export function discoverRoutes(
|
|
324
|
+
cwd: string,
|
|
325
|
+
appType: string,
|
|
326
|
+
): DiscoveredRoute[] {
|
|
327
|
+
let routes: DiscoveredRoute[];
|
|
328
|
+
|
|
329
|
+
switch (appType) {
|
|
330
|
+
case "nextjs-app":
|
|
331
|
+
routes = scanNextjsApp(cwd);
|
|
332
|
+
break;
|
|
333
|
+
case "nextjs-pages":
|
|
334
|
+
routes = scanNextjsPages(cwd);
|
|
335
|
+
break;
|
|
336
|
+
case "react-router":
|
|
337
|
+
routes = scanReactRouter(cwd);
|
|
338
|
+
break;
|
|
339
|
+
case "express":
|
|
340
|
+
routes = scanExpress(cwd);
|
|
341
|
+
break;
|
|
342
|
+
case "vite":
|
|
343
|
+
case "generic":
|
|
344
|
+
default:
|
|
345
|
+
routes = scanGeneric(cwd);
|
|
346
|
+
break;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Auth scan always runs
|
|
350
|
+
routes.push(...scanAuth(cwd));
|
|
351
|
+
|
|
352
|
+
return routes;
|
|
353
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { GateExecutionContext } from "../types.js";
|
|
2
|
+
|
|
3
|
+
export interface StructuredAgentRunOptions {
|
|
4
|
+
cwd: string;
|
|
5
|
+
prompt: string;
|
|
6
|
+
model?: string;
|
|
7
|
+
thinkingLevel?: string | null;
|
|
8
|
+
timeoutMs?: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type StructuredAgentRunResult =
|
|
12
|
+
| { status: "ok"; finalText: string }
|
|
13
|
+
| { status: "error"; finalText: null; error: string };
|
|
14
|
+
|
|
15
|
+
function extractTextFromContent(content: unknown): string {
|
|
16
|
+
if (typeof content === "string") {
|
|
17
|
+
return content.trim();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (Array.isArray(content)) {
|
|
21
|
+
return content
|
|
22
|
+
.map((part) => {
|
|
23
|
+
if (typeof part === "string") {
|
|
24
|
+
return part;
|
|
25
|
+
}
|
|
26
|
+
if (part && typeof part === "object" && "text" in part && typeof part.text === "string") {
|
|
27
|
+
return part.text;
|
|
28
|
+
}
|
|
29
|
+
return "";
|
|
30
|
+
})
|
|
31
|
+
.join("")
|
|
32
|
+
.trim();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (content && typeof content === "object" && "text" in content && typeof content.text === "string") {
|
|
36
|
+
return content.text.trim();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return "";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function extractFinalAssistantText(messages: unknown[]): string | null {
|
|
43
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
44
|
+
const message = messages[index];
|
|
45
|
+
if (!message || typeof message !== "object") {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const role = "role" in message ? message.role : undefined;
|
|
50
|
+
if (role !== undefined && role !== "assistant") {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const content = "content" in message ? message.content : message;
|
|
55
|
+
const text = extractTextFromContent(content);
|
|
56
|
+
if (text.length > 0) {
|
|
57
|
+
return text;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function runStructuredAgentSession(
|
|
65
|
+
createAgentSession: GateExecutionContext["createAgentSession"],
|
|
66
|
+
options: StructuredAgentRunOptions,
|
|
67
|
+
): Promise<StructuredAgentRunResult> {
|
|
68
|
+
const session = await createAgentSession({
|
|
69
|
+
cwd: options.cwd,
|
|
70
|
+
model: options.model,
|
|
71
|
+
thinkingLevel: options.thinkingLevel ?? null,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
await session.prompt(options.prompt, { expandPromptTemplates: false });
|
|
76
|
+
const finalText = extractFinalAssistantText(session.state.messages);
|
|
77
|
+
|
|
78
|
+
if (!finalText) {
|
|
79
|
+
return {
|
|
80
|
+
status: "error",
|
|
81
|
+
finalText: null,
|
|
82
|
+
error: "No final assistant message was returned.",
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return { status: "ok", finalText };
|
|
87
|
+
} catch (error) {
|
|
88
|
+
return {
|
|
89
|
+
status: "error",
|
|
90
|
+
finalText: null,
|
|
91
|
+
error: error instanceof Error ? error.message : "Agent session failed.",
|
|
92
|
+
};
|
|
93
|
+
} finally {
|
|
94
|
+
await session.dispose();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { loadModelConfig } from "../config/model-config.js";
|
|
2
|
+
import { modelRegistry } from "../config/model-registry-instance.js";
|
|
3
|
+
import { createModelBridge, resolveModelForAction } from "../config/model-resolver.js";
|
|
4
|
+
import { validateQualityGates } from "../config/schema.js";
|
|
5
|
+
import { stripMarkdownCodeFence } from "../text.js";
|
|
6
|
+
import type { Platform } from "../platform/types.js";
|
|
7
|
+
import type { ProjectFacts, QualityGatesConfig, SetupProposal } from "../types.js";
|
|
8
|
+
import { runStructuredAgentSession } from "./ai-session.js";
|
|
9
|
+
|
|
10
|
+
modelRegistry.register({
|
|
11
|
+
id: "quality-gate-setup",
|
|
12
|
+
category: "command",
|
|
13
|
+
label: "Quality gate setup",
|
|
14
|
+
harnessRoleHint: "slow",
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
function buildAiSetupPrompt(projectFacts: ProjectFacts, proposal: SetupProposal): string {
|
|
18
|
+
return [
|
|
19
|
+
"You are configuring Supipowers review quality gates for a repository.",
|
|
20
|
+
"Return JSON only as a QualityGatesConfig object.",
|
|
21
|
+
"",
|
|
22
|
+
"Available gates and shapes:",
|
|
23
|
+
'- "lsp-diagnostics": {"enabled": true|false}',
|
|
24
|
+
'- "lint": {"enabled": true, "command": "..."} or {"enabled": false}',
|
|
25
|
+
'- "typecheck": {"enabled": true, "command": "..."} or {"enabled": false}',
|
|
26
|
+
'- "format": {"enabled": true, "command": "..."} or {"enabled": false}',
|
|
27
|
+
'- "test-suite": {"enabled": true, "command": "..."} or {"enabled": false}',
|
|
28
|
+
'- "build": {"enabled": true, "command": "..."} or {"enabled": false}',
|
|
29
|
+
"",
|
|
30
|
+
"Rules:",
|
|
31
|
+
"- Prefer commands that already exist in package.json scripts.",
|
|
32
|
+
"- Never invent a mutating format command such as --write or lint --fix.",
|
|
33
|
+
"- Prefer checks that verify correctness during review: lsp, lint, typecheck, format check, tests, and build.",
|
|
34
|
+
"- Keep deterministic suggestions unless you have repository evidence to improve them.",
|
|
35
|
+
"- Omit gates you do not recommend instead of disabling them unless the baseline already contains them.",
|
|
36
|
+
"- Do not include explanations outside the JSON.",
|
|
37
|
+
"",
|
|
38
|
+
"Project facts:",
|
|
39
|
+
JSON.stringify(projectFacts, null, 2),
|
|
40
|
+
"",
|
|
41
|
+
"Deterministic baseline proposal:",
|
|
42
|
+
JSON.stringify(proposal.gates, null, 2),
|
|
43
|
+
].join("\n");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function parseAiSetupSuggestion(raw: string): QualityGatesConfig {
|
|
47
|
+
const parsed = JSON.parse(stripMarkdownCodeFence(raw)) as QualityGatesConfig;
|
|
48
|
+
const validation = validateQualityGates(parsed);
|
|
49
|
+
if (!validation.valid) {
|
|
50
|
+
throw new Error(validation.errors.join("\n"));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return parsed;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function suggestQualityGatesWithAi(input: {
|
|
57
|
+
platform: Platform;
|
|
58
|
+
cwd: string;
|
|
59
|
+
projectFacts: ProjectFacts;
|
|
60
|
+
proposal: SetupProposal;
|
|
61
|
+
}): Promise<QualityGatesConfig> {
|
|
62
|
+
const modelConfig = loadModelConfig(input.platform.paths, input.cwd);
|
|
63
|
+
const resolvedModel = resolveModelForAction(
|
|
64
|
+
"quality-gate-setup",
|
|
65
|
+
modelRegistry,
|
|
66
|
+
modelConfig,
|
|
67
|
+
createModelBridge(input.platform),
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const result = await runStructuredAgentSession(
|
|
71
|
+
input.platform.createAgentSession.bind(input.platform),
|
|
72
|
+
{
|
|
73
|
+
cwd: input.cwd,
|
|
74
|
+
prompt: buildAiSetupPrompt(input.projectFacts, input.proposal),
|
|
75
|
+
model: resolvedModel.model,
|
|
76
|
+
thinkingLevel: resolvedModel.thinkingLevel,
|
|
77
|
+
timeoutMs: 120_000,
|
|
78
|
+
},
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
if (result.status !== "ok") {
|
|
82
|
+
throw new Error(result.error);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return parseAiSetupSuggestion(result.finalText);
|
|
86
|
+
}
|