@dadado/agent-kit-cli 5.15.1 → 5.17.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/dashboard/dashboard-data.mjs +69 -65
- package/dashboard/lib/guards.d.mts +5 -0
- package/dashboard/lib/guards.mjs +31 -0
- package/dashboard/lib/identity-probe.mjs +56 -0
- package/dashboard/lib/pipeline-cache.mjs +58 -0
- package/dashboard/lib/processes.mjs +71 -0
- package/dashboard/serve.mjs +43 -1
- package/dashboard/start-broadcast.mjs +2 -12
- package/dashboard/start.mjs +2 -13
- package/dist/add-HTYIFKMN.js +150 -0
- package/dist/chunk-2IPXQFJE.js +253 -0
- package/dist/chunk-2RDRMGD3.js +82 -0
- package/dist/chunk-2YU2H7I6.js +42 -0
- package/dist/chunk-4IWG5CVE.js +214 -0
- package/dist/chunk-5VCOWTTP.js +6 -0
- package/dist/chunk-77V75A7O.js +39 -0
- package/dist/chunk-7HDQEK6T.js +26 -0
- package/dist/chunk-AW74INUW.js +1456 -0
- package/dist/chunk-B73KQ5KX.js +972 -0
- package/dist/chunk-CBCW5R5J.js +484 -0
- package/dist/chunk-DE3LQFSJ.js +18 -0
- package/dist/chunk-DQA434Y4.js +106 -0
- package/dist/chunk-F4VMYR7U.js +318 -0
- package/dist/chunk-F6C3GFKV.js +57 -0
- package/dist/chunk-HFUH3HVS.js +282 -0
- package/dist/chunk-HS7LVVLO.js +74 -0
- package/dist/chunk-IHROBTNM.js +135 -0
- package/dist/chunk-KD2MYBB6.js +213 -0
- package/dist/chunk-MAPOQEA2.js +43 -0
- package/dist/chunk-N3HTJVBR.js +341 -0
- package/dist/chunk-NQ7IQ5XT.js +26 -0
- package/dist/chunk-OUQZC744.js +1315 -0
- package/dist/chunk-PCCQUWE5.js +817 -0
- package/dist/chunk-PNFF6OU3.js +273 -0
- package/dist/chunk-PNG7LO2F.js +53 -0
- package/dist/chunk-QTON2GQX.js +203 -0
- package/dist/chunk-S6GMFDKV.js +50 -0
- package/dist/chunk-SMGFN3Z4.js +503 -0
- package/dist/chunk-STGFBAYT.js +641 -0
- package/dist/chunk-TFZ2JUXH.js +1081 -0
- package/dist/chunk-VY7MTPJ2.js +205 -0
- package/dist/chunk-W24CMQ32.js +525 -0
- package/dist/chunk-WW3YZKHW.js +165 -0
- package/dist/chunk-ZB43YMHW.js +20 -0
- package/dist/chunk-ZCTSUQRR.js +874 -0
- package/dist/chunk-ZXKZBTXE.js +17 -0
- package/dist/contribute-C6AW2P6J.js +367 -0
- package/dist/cursor-awareness-KTL2RGKD.js +77 -0
- package/dist/dashboard-YSNUGBFY.js +21 -0
- package/dist/dashboard-broadcast-M4CMW7RX.js +119 -0
- package/dist/diff-VHNR62SI.js +74 -0
- package/dist/doctor-PET2HRRJ.js +236 -0
- package/dist/dogfood-KKFVRWT6.js +366 -0
- package/dist/guard-EHXF76ID.js +530 -0
- package/dist/handoff-3UI6VKVG.js +302 -0
- package/dist/hook-DYIMKJYL.js +1074 -0
- package/dist/index.js +62 -14068
- package/dist/init-Q4LBM6KT.js +127 -0
- package/dist/install-RHOS3OYZ.js +34 -0
- package/dist/mission-control-LJACEVVN.js +89 -0
- package/dist/monitors-QV2IS3MP.js +176 -0
- package/dist/plan-index-JMC7Z4DW.js +44 -0
- package/dist/public-inbound-radar-WP3XF7GS.js +68 -0
- package/dist/remote-issues-SZ4HDQL5.js +44 -0
- package/dist/run-I3BFGULT.js +243 -0
- package/dist/run-plan-FO7CL4M3.js +118 -0
- package/dist/scan-NI2SWJ2E.js +33 -0
- package/dist/setup-global-M42GEODI.js +340 -0
- package/dist/status-XPVQVQMX.js +150 -0
- package/dist/update-FJ2EBZVO.js +283 -0
- package/dist/validate-V2CW7J4B.js +201 -0
- package/package.json +3 -3
|
@@ -0,0 +1,874 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CI_PLATFORM_FILES
|
|
3
|
+
} from "./chunk-F6C3GFKV.js";
|
|
4
|
+
import {
|
|
5
|
+
fileExists,
|
|
6
|
+
listDirectory,
|
|
7
|
+
readJson
|
|
8
|
+
} from "./chunk-77V75A7O.js";
|
|
9
|
+
|
|
10
|
+
// src/scanner/scan.ts
|
|
11
|
+
import path7 from "path";
|
|
12
|
+
|
|
13
|
+
// src/scanner/detect-git.ts
|
|
14
|
+
import { execFile } from "child_process";
|
|
15
|
+
import path from "path";
|
|
16
|
+
import { promisify } from "util";
|
|
17
|
+
|
|
18
|
+
// src/scanner/paths.ts
|
|
19
|
+
var REPOSITORY_PROFILE_REL = ".cursor/agent-kit.config.json";
|
|
20
|
+
|
|
21
|
+
// src/scanner/detect-git.ts
|
|
22
|
+
var exec = promisify(execFile);
|
|
23
|
+
function remoteHostname(remoteUrl) {
|
|
24
|
+
const scpMatch = remoteUrl.match(/^[^@]+@([^:]+):/);
|
|
25
|
+
if (scpMatch?.[1]) return scpMatch[1].toLowerCase();
|
|
26
|
+
try {
|
|
27
|
+
return new URL(remoteUrl).hostname.toLowerCase();
|
|
28
|
+
} catch {
|
|
29
|
+
return void 0;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function sanitizeRemoteUrl(remoteUrl) {
|
|
33
|
+
try {
|
|
34
|
+
const parsed = new URL(remoteUrl);
|
|
35
|
+
parsed.username = "";
|
|
36
|
+
parsed.password = "";
|
|
37
|
+
return parsed.toString();
|
|
38
|
+
} catch {
|
|
39
|
+
return remoteUrl;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async function detectProvider(rootDir, remoteUrl) {
|
|
43
|
+
const configuration = await readJson(
|
|
44
|
+
path.join(rootDir, REPOSITORY_PROFILE_REL)
|
|
45
|
+
);
|
|
46
|
+
const configuredProvider = configuration?.git?.provider;
|
|
47
|
+
if (configuredProvider) {
|
|
48
|
+
const configuredKind = configuration.git?.providerKind ?? (configuredProvider === "github" ? "github" : configuredProvider === "gitlab" ? remoteUrl && remoteHostname(remoteUrl) === "gitlab.com" ? "gitlab-saas" : "gitlab-self-hosted" : configuredProvider === "other" ? "custom" : "known-other");
|
|
49
|
+
return {
|
|
50
|
+
provider: configuredProvider,
|
|
51
|
+
providerKind: configuredKind,
|
|
52
|
+
confidence: "high",
|
|
53
|
+
evidence: [{ source: "configuration", value: `${REPOSITORY_PROFILE_REL}#git.provider` }]
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
if (!remoteUrl) {
|
|
57
|
+
return { providerKind: "unknown", confidence: "low", evidence: [] };
|
|
58
|
+
}
|
|
59
|
+
const hostname = remoteHostname(remoteUrl);
|
|
60
|
+
const remoteEvidence = [{ source: "git", value: `remote:${remoteUrl}` }];
|
|
61
|
+
if (hostname === "github.com") {
|
|
62
|
+
return {
|
|
63
|
+
provider: "github",
|
|
64
|
+
providerKind: "github",
|
|
65
|
+
confidence: "high",
|
|
66
|
+
evidence: remoteEvidence
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
if (hostname === "gitlab.com") {
|
|
70
|
+
return {
|
|
71
|
+
provider: "gitlab",
|
|
72
|
+
providerKind: "gitlab-saas",
|
|
73
|
+
confidence: "high",
|
|
74
|
+
evidence: remoteEvidence
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
if (hostname === "bitbucket.org") {
|
|
78
|
+
return {
|
|
79
|
+
provider: "bitbucket",
|
|
80
|
+
providerKind: "known-other",
|
|
81
|
+
confidence: "high",
|
|
82
|
+
evidence: remoteEvidence
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
if (hostname === "dev.azure.com" || hostname?.endsWith(".visualstudio.com")) {
|
|
86
|
+
return {
|
|
87
|
+
provider: "azure-devops",
|
|
88
|
+
providerKind: "known-other",
|
|
89
|
+
confidence: "high",
|
|
90
|
+
evidence: remoteEvidence
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
if (hostname === "codeberg.org") {
|
|
94
|
+
return {
|
|
95
|
+
provider: "gitea",
|
|
96
|
+
providerKind: "known-other",
|
|
97
|
+
confidence: "high",
|
|
98
|
+
evidence: remoteEvidence
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
if (await fileExists(path.join(rootDir, ".gitlab-ci.yml"))) {
|
|
102
|
+
return {
|
|
103
|
+
provider: "gitlab",
|
|
104
|
+
providerKind: "gitlab-self-hosted",
|
|
105
|
+
confidence: "medium",
|
|
106
|
+
evidence: [...remoteEvidence, { source: "file", value: ".gitlab-ci.yml" }]
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
provider: "other",
|
|
111
|
+
providerKind: hostname ? "custom" : "unknown",
|
|
112
|
+
confidence: "low",
|
|
113
|
+
evidence: remoteEvidence
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function inferWorkflow(currentBranch, hasLocalStaging, hasRemoteStaging) {
|
|
117
|
+
if (hasLocalStaging || hasRemoteStaging) return "homolog-prod";
|
|
118
|
+
if (!currentBranch) return "unknown";
|
|
119
|
+
if (currentBranch === "main" || currentBranch === "master") return "feature-pr";
|
|
120
|
+
if (currentBranch.includes("develop") || currentBranch.includes("release")) return "gitflow";
|
|
121
|
+
if (currentBranch.includes("staging") || currentBranch.includes("homolog")) return "homolog-prod";
|
|
122
|
+
return "feature-pr";
|
|
123
|
+
}
|
|
124
|
+
var GIT_MAX_BUFFER = 64 * 1024 * 1024;
|
|
125
|
+
async function runGit(args, rootDir) {
|
|
126
|
+
try {
|
|
127
|
+
const { stdout } = await exec("git", args, { cwd: rootDir, maxBuffer: GIT_MAX_BUFFER });
|
|
128
|
+
return stdout.trim();
|
|
129
|
+
} catch {
|
|
130
|
+
return void 0;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async function listTrackedFiles(rootDir) {
|
|
134
|
+
return (await runGit(["ls-files"], rootDir))?.split("\n").filter(Boolean);
|
|
135
|
+
}
|
|
136
|
+
async function detectGitDirty(rootDir) {
|
|
137
|
+
return Boolean(await runGit(["status", "--porcelain"], rootDir));
|
|
138
|
+
}
|
|
139
|
+
async function detectGit(rootDir) {
|
|
140
|
+
const isGit = await runGit(["rev-parse", "--is-inside-work-tree"], rootDir) === "true";
|
|
141
|
+
if (!isGit) {
|
|
142
|
+
return {
|
|
143
|
+
providerKind: "unknown",
|
|
144
|
+
providerConfidence: "low",
|
|
145
|
+
providerEvidence: [],
|
|
146
|
+
remotes: [],
|
|
147
|
+
mode: "none",
|
|
148
|
+
workflow: "unknown",
|
|
149
|
+
isDirty: false,
|
|
150
|
+
hasLocalStaging: false,
|
|
151
|
+
hasRemoteStaging: false
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
const remoteNames = (await runGit(["remote"], rootDir))?.split("\n").filter(Boolean) ?? [];
|
|
155
|
+
const remotes = (await Promise.all(
|
|
156
|
+
remoteNames.map(async (name) => {
|
|
157
|
+
const url = await runGit(["remote", "get-url", name], rootDir);
|
|
158
|
+
return url ? { name, url: sanitizeRemoteUrl(url) } : void 0;
|
|
159
|
+
})
|
|
160
|
+
)).filter((remote) => remote !== void 0);
|
|
161
|
+
const primary = remotes.find((remote) => remote.name === "origin") ?? remotes[0];
|
|
162
|
+
const remoteUrl = primary?.url;
|
|
163
|
+
const [currentBranch, remoteHead, localBranchOutput, remoteBranchOutput, status, provider] = await Promise.all([
|
|
164
|
+
runGit(["branch", "--show-current"], rootDir),
|
|
165
|
+
primary ? runGit(["symbolic-ref", "--short", `refs/remotes/${primary.name}/HEAD`], rootDir) : Promise.resolve(void 0),
|
|
166
|
+
runGit(["for-each-ref", "--format=%(refname:short)", "refs/heads"], rootDir),
|
|
167
|
+
runGit(["for-each-ref", "--format=%(refname:short)", "refs/remotes"], rootDir),
|
|
168
|
+
runGit(["status", "--porcelain"], rootDir),
|
|
169
|
+
detectProvider(rootDir, remoteUrl)
|
|
170
|
+
]);
|
|
171
|
+
const localBranches = localBranchOutput?.split("\n").filter(Boolean);
|
|
172
|
+
const remoteBranches = remoteBranchOutput?.split("\n").filter(Boolean);
|
|
173
|
+
const defaultBranch = remoteHead?.split("/").slice(1).join("/") || (localBranches?.includes("main") ? "main" : localBranches?.includes("master") ? "master" : void 0);
|
|
174
|
+
const hasLocalStaging = localBranches?.includes("staging") ?? false;
|
|
175
|
+
const hasRemoteStaging = remoteBranches?.some((branch) => remoteNames.some((r) => branch === `${r}/staging`)) ?? false;
|
|
176
|
+
return {
|
|
177
|
+
provider: provider.provider,
|
|
178
|
+
providerKind: provider.providerKind,
|
|
179
|
+
providerConfidence: provider.confidence,
|
|
180
|
+
providerEvidence: provider.evidence,
|
|
181
|
+
remoteUrl,
|
|
182
|
+
remoteName: primary?.name,
|
|
183
|
+
remotes,
|
|
184
|
+
mode: remotes.length > 0 ? "remote-hosted" : "local-only",
|
|
185
|
+
currentBranch,
|
|
186
|
+
defaultBranch,
|
|
187
|
+
isDirty: Boolean(status),
|
|
188
|
+
hasLocalStaging,
|
|
189
|
+
hasRemoteStaging,
|
|
190
|
+
workflow: inferWorkflow(currentBranch, hasLocalStaging, hasRemoteStaging)
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// src/scanner/detect-ide.ts
|
|
195
|
+
import path2 from "path";
|
|
196
|
+
async function detectIde(rootDir) {
|
|
197
|
+
const hasCursor = await fileExists(path2.join(rootDir, ".cursor"));
|
|
198
|
+
const hasVSCode = await fileExists(path2.join(rootDir, ".vscode"));
|
|
199
|
+
const hasWindsurf = await fileExists(path2.join(rootDir, ".windsurfrules"));
|
|
200
|
+
if (hasCursor) return { ide: "cursor", plan: "cursor-pro" };
|
|
201
|
+
if (hasVSCode) return { ide: "vscode", plan: "vscode-pro" };
|
|
202
|
+
if (hasWindsurf) return { ide: "windsurf", plan: "windsurf" };
|
|
203
|
+
return { ide: "unknown", plan: "default" };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// src/scanner/detect-infra.ts
|
|
207
|
+
import { readFile, readdir } from "fs/promises";
|
|
208
|
+
import path3 from "path";
|
|
209
|
+
function extractRunCommands(yaml) {
|
|
210
|
+
const lines = yaml.split(/\r?\n/);
|
|
211
|
+
const commands = [];
|
|
212
|
+
for (let i = 0; i < lines.length; i++) {
|
|
213
|
+
const line = lines[i] ?? "";
|
|
214
|
+
const match = /^(\s*)(?:-\s+)?run:\s*(.*)$/.exec(line);
|
|
215
|
+
if (!match) continue;
|
|
216
|
+
const [, indent = "", inline = ""] = match;
|
|
217
|
+
const trimmedInline = inline.trim();
|
|
218
|
+
if (trimmedInline && !/^[|>][+-]?$/.test(trimmedInline)) {
|
|
219
|
+
commands.push(trimmedInline.replace(/^['"]|['"]$/g, ""));
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
const blockIndent = indent.length;
|
|
223
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
224
|
+
const blockLine = lines[j] ?? "";
|
|
225
|
+
if (!blockLine.trim()) continue;
|
|
226
|
+
const lineIndent = blockLine.length - blockLine.trimStart().length;
|
|
227
|
+
if (lineIndent <= blockIndent) break;
|
|
228
|
+
commands.push(blockLine.trim());
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return Array.from(new Set(commands));
|
|
232
|
+
}
|
|
233
|
+
async function detectCiRunCommands(rootDir) {
|
|
234
|
+
const workflowsDir = path3.join(rootDir, ".github", "workflows");
|
|
235
|
+
if (!await fileExists(workflowsDir)) return [];
|
|
236
|
+
let entries;
|
|
237
|
+
try {
|
|
238
|
+
entries = await readdir(workflowsDir);
|
|
239
|
+
} catch {
|
|
240
|
+
return [];
|
|
241
|
+
}
|
|
242
|
+
const workflowFiles = entries.filter((name) => /\.ya?ml$/.test(name));
|
|
243
|
+
const perFile = await Promise.all(
|
|
244
|
+
workflowFiles.map(async (name) => {
|
|
245
|
+
try {
|
|
246
|
+
return extractRunCommands(await readFile(path3.join(workflowsDir, name), "utf8"));
|
|
247
|
+
} catch {
|
|
248
|
+
return [];
|
|
249
|
+
}
|
|
250
|
+
})
|
|
251
|
+
);
|
|
252
|
+
return Array.from(new Set(perFile.flat()));
|
|
253
|
+
}
|
|
254
|
+
async function detectInfra(rootDir) {
|
|
255
|
+
const docker = await fileExists(path3.join(rootDir, "Dockerfile")) || await fileExists(path3.join(rootDir, "docker-compose.yml")) || await fileExists(path3.join(rootDir, "docker-compose.yaml"));
|
|
256
|
+
const kubernetes = await fileExists(path3.join(rootDir, "k8s")) || await fileExists(path3.join(rootDir, "kubernetes"));
|
|
257
|
+
let ci = "none";
|
|
258
|
+
const ciFiles = [];
|
|
259
|
+
for (const [platform, filePath] of Object.entries(CI_PLATFORM_FILES)) {
|
|
260
|
+
if (await fileExists(path3.join(rootDir, filePath))) {
|
|
261
|
+
if (ci === "none") ci = platform;
|
|
262
|
+
ciFiles.push(filePath);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
const infrastructureCandidates = [
|
|
266
|
+
"Dockerfile",
|
|
267
|
+
"docker-compose.yml",
|
|
268
|
+
"docker-compose.yaml",
|
|
269
|
+
"k8s",
|
|
270
|
+
"kubernetes",
|
|
271
|
+
"terraform",
|
|
272
|
+
"infra"
|
|
273
|
+
];
|
|
274
|
+
const deploymentCandidates = [
|
|
275
|
+
"vercel.json",
|
|
276
|
+
"netlify.toml",
|
|
277
|
+
"fly.toml",
|
|
278
|
+
"render.yaml",
|
|
279
|
+
"Procfile",
|
|
280
|
+
"deploy",
|
|
281
|
+
"scripts/deploy.sh"
|
|
282
|
+
];
|
|
283
|
+
const infrastructureFiles = (await Promise.all(
|
|
284
|
+
infrastructureCandidates.map(
|
|
285
|
+
async (file) => await fileExists(path3.join(rootDir, file)) ? file : void 0
|
|
286
|
+
)
|
|
287
|
+
)).filter((file) => file !== void 0);
|
|
288
|
+
const deploymentFiles = (await Promise.all(
|
|
289
|
+
deploymentCandidates.map(
|
|
290
|
+
async (file) => await fileExists(path3.join(rootDir, file)) ? file : void 0
|
|
291
|
+
)
|
|
292
|
+
)).filter((file) => file !== void 0);
|
|
293
|
+
const ciRunCommands = ci === "github-actions" ? await detectCiRunCommands(rootDir) : [];
|
|
294
|
+
return { docker, kubernetes, ci, ciFiles, infrastructureFiles, deploymentFiles, ciRunCommands };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// src/scanner/detect-repository.ts
|
|
298
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
299
|
+
import path4 from "path";
|
|
300
|
+
var CONTEXT_PATHS = [
|
|
301
|
+
["README.md", "README"],
|
|
302
|
+
["README", "README"],
|
|
303
|
+
["AGENTS.md", "agent guidance"],
|
|
304
|
+
["CLAUDE.md", "agent guidance"],
|
|
305
|
+
[".cursor/rules", "Cursor rules"],
|
|
306
|
+
["docs/architecture.md", "architecture"],
|
|
307
|
+
["docs/adr", "architecture decisions"],
|
|
308
|
+
["docs/runbooks", "runbooks"],
|
|
309
|
+
["runbooks", "runbooks"],
|
|
310
|
+
["prompts", "prompts"],
|
|
311
|
+
["schemas", "schemas"],
|
|
312
|
+
["sql", "SQL"],
|
|
313
|
+
["knowledge", "knowledge base"]
|
|
314
|
+
];
|
|
315
|
+
async function existingEvidence(rootDir, candidates) {
|
|
316
|
+
const evidence = await Promise.all(
|
|
317
|
+
candidates.map(
|
|
318
|
+
async ([relativePath, label]) => await fileExists(path4.join(rootDir, relativePath)) ? { source: "file", value: `${relativePath}:${label}` } : void 0
|
|
319
|
+
)
|
|
320
|
+
);
|
|
321
|
+
return evidence.flatMap((item) => item ? [item] : []);
|
|
322
|
+
}
|
|
323
|
+
async function detectContext(rootDir) {
|
|
324
|
+
const sources = await existingEvidence(rootDir, CONTEXT_PATHS);
|
|
325
|
+
const paths = sources.map((item) => item.value.split(":")[0]);
|
|
326
|
+
return {
|
|
327
|
+
sources,
|
|
328
|
+
hasReadme: paths.some((item) => item === "README" || item === "README.md"),
|
|
329
|
+
hasArchitecture: sources.some((item) => item.value.includes("architecture")),
|
|
330
|
+
hasRunbooks: sources.some((item) => item.value.includes("runbooks")),
|
|
331
|
+
hasAgentGuidance: sources.some(
|
|
332
|
+
(item) => item.value.includes("agent guidance") || item.value.includes("Cursor rules")
|
|
333
|
+
)
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
var REPOSITORY_PURPOSE_VALUES = [
|
|
337
|
+
"application",
|
|
338
|
+
"library",
|
|
339
|
+
"monorepo",
|
|
340
|
+
"documentation",
|
|
341
|
+
"knowledge",
|
|
342
|
+
"operations",
|
|
343
|
+
"automation",
|
|
344
|
+
"mixed",
|
|
345
|
+
"unknown"
|
|
346
|
+
];
|
|
347
|
+
function isRepositoryPurpose(value) {
|
|
348
|
+
return typeof value === "string" && REPOSITORY_PURPOSE_VALUES.includes(value);
|
|
349
|
+
}
|
|
350
|
+
async function readConfirmedPurpose(rootDir) {
|
|
351
|
+
const configuration = await readJson(
|
|
352
|
+
path4.join(rootDir, REPOSITORY_PROFILE_REL)
|
|
353
|
+
);
|
|
354
|
+
const configuredPurpose = configuration?.purpose;
|
|
355
|
+
if (!configuredPurpose || !isRepositoryPurpose(configuredPurpose.value) || configuredPurpose.value === "unknown") {
|
|
356
|
+
return void 0;
|
|
357
|
+
}
|
|
358
|
+
const categories = Array.isArray(configuredPurpose.categories) ? configuredPurpose.categories.filter(isRepositoryPurpose) : [];
|
|
359
|
+
const evidence = Array.isArray(configuredPurpose.evidence) ? configuredPurpose.evidence : [];
|
|
360
|
+
return {
|
|
361
|
+
value: configuredPurpose.value,
|
|
362
|
+
categories: categories.length > 0 ? categories : [configuredPurpose.value],
|
|
363
|
+
confidence: "high",
|
|
364
|
+
evidence: evidence.length > 0 ? evidence : [{ source: "configuration", value: `${REPOSITORY_PROFILE_REL}#purpose.value` }]
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
async function detectPurpose(rootDir, stack) {
|
|
368
|
+
const confirmed = await readConfirmedPurpose(rootDir);
|
|
369
|
+
if (confirmed) return confirmed;
|
|
370
|
+
const entries = await listDirectory(rootDir);
|
|
371
|
+
const lowerEntries = entries.map((entry) => entry.toLowerCase());
|
|
372
|
+
const packageJson = await readJson(path4.join(rootDir, "package.json"));
|
|
373
|
+
const categories = [];
|
|
374
|
+
const evidence = [];
|
|
375
|
+
const add = (category, value2) => {
|
|
376
|
+
if (!categories.includes(category)) categories.push(category);
|
|
377
|
+
evidence.push({ source: "file", value: value2 });
|
|
378
|
+
};
|
|
379
|
+
if (stack.workspaces) add("monorepo", "workspace configuration");
|
|
380
|
+
else if (stack.language === "node" && packageJson?.private !== true && (packageJson?.exports !== void 0 || packageJson?.main || packageJson?.types)) {
|
|
381
|
+
add("library", "package export configuration");
|
|
382
|
+
} else if (stack.hasProjectFiles) add("application", "application package marker");
|
|
383
|
+
if (lowerEntries.some((entry) => ["docs", "documentation", "mkdocs.yml"].includes(entry))) {
|
|
384
|
+
add("documentation", "documentation structure");
|
|
385
|
+
}
|
|
386
|
+
if (lowerEntries.some(
|
|
387
|
+
(entry) => ["knowledge", "content", "wiki", "playbooks", "handbook"].includes(entry)
|
|
388
|
+
)) {
|
|
389
|
+
add("knowledge", "knowledge structure");
|
|
390
|
+
}
|
|
391
|
+
if (lowerEntries.some(
|
|
392
|
+
(entry) => ["runbooks", "infra", "terraform", "ansible", "k8s", "kubernetes"].includes(entry)
|
|
393
|
+
)) {
|
|
394
|
+
add("operations", "operations structure");
|
|
395
|
+
}
|
|
396
|
+
if (lowerEntries.some(
|
|
397
|
+
(entry) => ["n8n", "workflows", "automations", "prompts", "sql", "schemas"].includes(entry)
|
|
398
|
+
) || lowerEntries.some((entry) => entry.endsWith(".sql"))) {
|
|
399
|
+
add("automation", "automation or data artifact");
|
|
400
|
+
}
|
|
401
|
+
const meaningfulCategories = categories.filter((category) => category !== "unknown");
|
|
402
|
+
const value = meaningfulCategories.length === 0 ? "unknown" : meaningfulCategories.length === 1 ? meaningfulCategories[0] ?? "unknown" : "mixed";
|
|
403
|
+
return {
|
|
404
|
+
value,
|
|
405
|
+
categories: meaningfulCategories.length > 0 ? meaningfulCategories : ["unknown"],
|
|
406
|
+
confidence: evidence.length > 0 ? "high" : "low",
|
|
407
|
+
evidence
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
async function detectAgentKit(rootDir) {
|
|
411
|
+
const manifestRelativePath = ".cursor/agent-kit.json";
|
|
412
|
+
const manifestPath = path4.join(rootDir, manifestRelativePath);
|
|
413
|
+
const installed = await fileExists(manifestPath);
|
|
414
|
+
const manifest = installed ? await readJson(manifestPath) : null;
|
|
415
|
+
return {
|
|
416
|
+
installed,
|
|
417
|
+
manifestPath: installed ? manifestRelativePath : void 0,
|
|
418
|
+
version: manifest?.version,
|
|
419
|
+
hasPlans: await fileExists(path4.join(rootDir, ".cursor/plans")),
|
|
420
|
+
hasHandoff: await fileExists(path4.join(rootDir, ".cursor/HANDOFF.md")),
|
|
421
|
+
hasMemory: await fileExists(path4.join(rootDir, ".cursor/memory"))
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
var REQUIRED_SECRET_PATTERNS = [
|
|
425
|
+
".env",
|
|
426
|
+
".env.*",
|
|
427
|
+
"*.key",
|
|
428
|
+
"*.pem",
|
|
429
|
+
"*.p12",
|
|
430
|
+
"*.pfx",
|
|
431
|
+
"*credentials*.json",
|
|
432
|
+
"*service-account*.json"
|
|
433
|
+
];
|
|
434
|
+
var KIT_OWNED_IGNORE_PATTERNS = [
|
|
435
|
+
".cursor/HANDOFF.md",
|
|
436
|
+
".cursor/dogfood/",
|
|
437
|
+
".cursor/context/readiness.json",
|
|
438
|
+
".cursor/context/flight-log.json",
|
|
439
|
+
".cursor/context/mission-timing.json"
|
|
440
|
+
];
|
|
441
|
+
function globToRegExp(glob) {
|
|
442
|
+
const body = glob.split("*").map((part) => part.replace(/[.+?^${}()|[\]\\]/g, "\\$&")).join("[^/]*");
|
|
443
|
+
return new RegExp(`^${body}$`, "i");
|
|
444
|
+
}
|
|
445
|
+
var SECRET_FILE_MATCHERS = REQUIRED_SECRET_PATTERNS.map(globToRegExp);
|
|
446
|
+
var ENV_TEMPLATE_RE = /^\.env(?:\..+)?\.(?:example|sample|template|dist)$/i;
|
|
447
|
+
function isTrackedSecretFile(file) {
|
|
448
|
+
const base = file.slice(file.lastIndexOf("/") + 1);
|
|
449
|
+
if (ENV_TEMPLATE_RE.test(base)) return false;
|
|
450
|
+
return SECRET_FILE_MATCHERS.some((matcher) => matcher.test(base));
|
|
451
|
+
}
|
|
452
|
+
async function detectSafety(rootDir, trackedFiles) {
|
|
453
|
+
const gitignorePath = path4.join(rootDir, ".gitignore");
|
|
454
|
+
const hasGitignore = await fileExists(gitignorePath);
|
|
455
|
+
const gitignore = hasGitignore ? await readFile2(gitignorePath, "utf8") : "";
|
|
456
|
+
const lines = gitignore.split(/\r?\n/).map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
|
|
457
|
+
const ignoredSecretPatterns = REQUIRED_SECRET_PATTERNS.filter(
|
|
458
|
+
(pattern) => lines.includes(pattern)
|
|
459
|
+
);
|
|
460
|
+
const trackedSensitiveFiles = (trackedFiles ?? []).filter(isTrackedSecretFile);
|
|
461
|
+
const hookPaths = [".husky", ".git/hooks/pre-commit", "git-hooks/pre-commit"];
|
|
462
|
+
const hasHooks = (await Promise.all(hookPaths.map((item) => fileExists(path4.join(rootDir, item))))).some(Boolean);
|
|
463
|
+
const guardCandidates = [
|
|
464
|
+
".husky/pre-commit",
|
|
465
|
+
".husky/pre-push",
|
|
466
|
+
"git-hooks/pre-commit",
|
|
467
|
+
"git-hooks/pre-push"
|
|
468
|
+
];
|
|
469
|
+
const guardContents = await Promise.all(
|
|
470
|
+
guardCandidates.map(
|
|
471
|
+
async (item) => await fileExists(path4.join(rootDir, item)) ? readFile2(path4.join(rootDir, item), "utf8") : ""
|
|
472
|
+
)
|
|
473
|
+
);
|
|
474
|
+
return {
|
|
475
|
+
hasGitignore,
|
|
476
|
+
ignoredSecretPatterns,
|
|
477
|
+
missingSecretPatterns: REQUIRED_SECRET_PATTERNS.filter(
|
|
478
|
+
(pattern) => !ignoredSecretPatterns.includes(pattern)
|
|
479
|
+
),
|
|
480
|
+
trackedSensitiveFiles,
|
|
481
|
+
...trackedFiles === void 0 ? { trackedFilesUnknown: true } : {},
|
|
482
|
+
hasHooks,
|
|
483
|
+
hasMainBranchGuard: guardContents.some(
|
|
484
|
+
(content) => content.includes("main") || content.includes("master")
|
|
485
|
+
)
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// src/scanner/detect-services.ts
|
|
490
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
491
|
+
import path5 from "path";
|
|
492
|
+
async function detectProjectManagement(rootDir) {
|
|
493
|
+
const tools = [];
|
|
494
|
+
const mcpConfigPaths = [
|
|
495
|
+
path5.join(rootDir, ".cursor", "mcp.json"),
|
|
496
|
+
path5.join(rootDir, "mcp.json")
|
|
497
|
+
];
|
|
498
|
+
for (const configPath of mcpConfigPaths) {
|
|
499
|
+
if (!await fileExists(configPath)) continue;
|
|
500
|
+
try {
|
|
501
|
+
const raw = await readFile3(configPath, "utf8");
|
|
502
|
+
const lower = raw.toLowerCase();
|
|
503
|
+
if (lower.includes("clickup")) tools.push("clickup");
|
|
504
|
+
if (lower.includes("jira") || lower.includes("atlassian")) tools.push("jira");
|
|
505
|
+
if (lower.includes("linear")) tools.push("linear");
|
|
506
|
+
if (lower.includes("asana")) tools.push("asana");
|
|
507
|
+
if (lower.includes("youtrack")) tools.push("youtrack");
|
|
508
|
+
if (lower.includes("shortcut")) tools.push("shortcut");
|
|
509
|
+
} catch {
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
if (await fileExists(path5.join(rootDir, ".github", "ISSUE_TEMPLATE"))) {
|
|
513
|
+
tools.push("github-issues");
|
|
514
|
+
}
|
|
515
|
+
if (await fileExists(path5.join(rootDir, ".github", "projects"))) {
|
|
516
|
+
tools.push("github-projects");
|
|
517
|
+
}
|
|
518
|
+
return [...new Set(tools)];
|
|
519
|
+
}
|
|
520
|
+
async function detectServices(rootDir) {
|
|
521
|
+
const hasPrisma = await fileExists(path5.join(rootDir, "prisma/schema.prisma"));
|
|
522
|
+
const hasSequelize = await fileExists(path5.join(rootDir, "sequelize"));
|
|
523
|
+
const hasDrizzle = await fileExists(path5.join(rootDir, "drizzle.config.ts"));
|
|
524
|
+
const hasKnex = await fileExists(path5.join(rootDir, "knexfile.ts"));
|
|
525
|
+
const hasTypeorm = await fileExists(path5.join(rootDir, "ormconfig.json"));
|
|
526
|
+
const database = hasPrisma || hasSequelize || hasDrizzle || hasKnex || hasTypeorm ? "postgresql" : void 0;
|
|
527
|
+
const orm = hasPrisma ? "prisma" : hasDrizzle ? "drizzle" : hasSequelize ? "sequelize" : hasKnex ? "knex" : hasTypeorm ? "typeorm" : void 0;
|
|
528
|
+
const projectManagement = await detectProjectManagement(rootDir);
|
|
529
|
+
return {
|
|
530
|
+
database,
|
|
531
|
+
orm,
|
|
532
|
+
projectManagement: projectManagement.length > 0 ? projectManagement : void 0
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// src/scanner/detect-stack.ts
|
|
537
|
+
import { readFile as readFile4, readdir as readdir2 } from "fs/promises";
|
|
538
|
+
import path6 from "path";
|
|
539
|
+
var PROJECT_MARKERS = [
|
|
540
|
+
"package.json",
|
|
541
|
+
"requirements.txt",
|
|
542
|
+
"pyproject.toml",
|
|
543
|
+
"go.mod",
|
|
544
|
+
"Gemfile",
|
|
545
|
+
"pom.xml",
|
|
546
|
+
"build.gradle",
|
|
547
|
+
"composer.json",
|
|
548
|
+
"Cargo.toml",
|
|
549
|
+
"pnpm-workspace.yaml",
|
|
550
|
+
"pubspec.yaml"
|
|
551
|
+
];
|
|
552
|
+
var SUBDIR_SCAN_SKIP = /* @__PURE__ */ new Set(["node_modules", ".git", "build", ".dart_tool"]);
|
|
553
|
+
async function scanSubdirectoryMarkers(rootDir) {
|
|
554
|
+
let entries;
|
|
555
|
+
try {
|
|
556
|
+
entries = await readdir2(rootDir, { withFileTypes: true });
|
|
557
|
+
} catch {
|
|
558
|
+
return [];
|
|
559
|
+
}
|
|
560
|
+
const dirs = entries.filter((entry) => entry.isDirectory() && !SUBDIR_SCAN_SKIP.has(entry.name)).map((entry) => entry.name);
|
|
561
|
+
const candidates = dirs.flatMap((dir) => PROJECT_MARKERS.map((marker) => ({ dir, marker })));
|
|
562
|
+
const found = await Promise.all(
|
|
563
|
+
candidates.map(async (candidate) => {
|
|
564
|
+
const exists = await fileExists(path6.join(rootDir, candidate.dir, candidate.marker));
|
|
565
|
+
return exists ? candidate : null;
|
|
566
|
+
})
|
|
567
|
+
);
|
|
568
|
+
return found.filter((hit) => hit !== null);
|
|
569
|
+
}
|
|
570
|
+
function isFlutterPubspec(content) {
|
|
571
|
+
return /^\s*flutter:\s*$/m.test(content) || /sdk:\s*flutter\b/.test(content);
|
|
572
|
+
}
|
|
573
|
+
var DART_VALIDATION_COMMANDS = [
|
|
574
|
+
"dart analyze --fatal-infos",
|
|
575
|
+
"dart format --output=none --set-exit-if-changed ."
|
|
576
|
+
];
|
|
577
|
+
async function detectDartFlavor(pubspecDir) {
|
|
578
|
+
let content = "";
|
|
579
|
+
try {
|
|
580
|
+
content = await readFile4(path6.join(pubspecDir, "pubspec.yaml"), "utf8");
|
|
581
|
+
} catch {
|
|
582
|
+
}
|
|
583
|
+
return { isFlutter: isFlutterPubspec(content) };
|
|
584
|
+
}
|
|
585
|
+
async function detectStackFromSubdirHits(rootDir, hits) {
|
|
586
|
+
const packageManagerEvidence = hits.map((hit) => ({
|
|
587
|
+
source: "file",
|
|
588
|
+
value: `${hit.dir}/${hit.marker}`
|
|
589
|
+
}));
|
|
590
|
+
const dartHits = hits.filter((hit) => hit.marker === "pubspec.yaml");
|
|
591
|
+
if (dartHits.length > 0) {
|
|
592
|
+
const flavors = await Promise.all(
|
|
593
|
+
dartHits.map((hit) => detectDartFlavor(path6.join(rootDir, hit.dir)))
|
|
594
|
+
);
|
|
595
|
+
const testCommands = Array.from(
|
|
596
|
+
new Set(flavors.map((flavor) => flavor.isFlutter ? "flutter test" : "dart test"))
|
|
597
|
+
);
|
|
598
|
+
return {
|
|
599
|
+
language: "dart",
|
|
600
|
+
framework: flavors.some((flavor) => flavor.isFlutter) ? "flutter" : "dart",
|
|
601
|
+
workspaces: true,
|
|
602
|
+
testCommands,
|
|
603
|
+
validationCommands: DART_VALIDATION_COMMANDS,
|
|
604
|
+
hasProjectFiles: true,
|
|
605
|
+
packageManagerEvidence
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
const nodeHit = hits.find((hit) => hit.marker === "package.json");
|
|
609
|
+
if (nodeHit) {
|
|
610
|
+
const subRoot = path6.join(rootDir, nodeHit.dir);
|
|
611
|
+
const packageJson = await readJson(path6.join(subRoot, "package.json")) ?? {};
|
|
612
|
+
const scripts = packageJson.scripts ?? {};
|
|
613
|
+
const packageManager = await detectPackageManager(subRoot, packageJson);
|
|
614
|
+
const commands = commandsForScripts(scripts, packageManager.packageManager);
|
|
615
|
+
return {
|
|
616
|
+
language: "node",
|
|
617
|
+
framework: "node",
|
|
618
|
+
packageManager: packageManager.packageManager,
|
|
619
|
+
packageManagerEvidence: [...packageManagerEvidence, ...packageManager.evidence],
|
|
620
|
+
scripts,
|
|
621
|
+
workspaces: true,
|
|
622
|
+
...commands,
|
|
623
|
+
hasProjectFiles: true
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
const languageByMarker = [
|
|
627
|
+
{ marker: "pyproject.toml", language: "python", testCommands: [], validationCommands: [] },
|
|
628
|
+
{ marker: "requirements.txt", language: "python", testCommands: [], validationCommands: [] },
|
|
629
|
+
{ marker: "go.mod", language: "go", testCommands: ["go test ./..."], validationCommands: [] },
|
|
630
|
+
{
|
|
631
|
+
marker: "Cargo.toml",
|
|
632
|
+
language: "rust",
|
|
633
|
+
testCommands: ["cargo test"],
|
|
634
|
+
validationCommands: ["cargo check"]
|
|
635
|
+
},
|
|
636
|
+
{ marker: "composer.json", language: "php", testCommands: [], validationCommands: [] }
|
|
637
|
+
];
|
|
638
|
+
for (const candidate of languageByMarker) {
|
|
639
|
+
if (hits.some((hit) => hit.marker === candidate.marker)) {
|
|
640
|
+
return {
|
|
641
|
+
language: candidate.language,
|
|
642
|
+
framework: candidate.language,
|
|
643
|
+
workspaces: true,
|
|
644
|
+
testCommands: candidate.testCommands,
|
|
645
|
+
validationCommands: candidate.validationCommands,
|
|
646
|
+
hasProjectFiles: true,
|
|
647
|
+
packageManagerEvidence
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
return {
|
|
652
|
+
language: "unknown",
|
|
653
|
+
workspaces: true,
|
|
654
|
+
testCommands: [],
|
|
655
|
+
validationCommands: [],
|
|
656
|
+
hasProjectFiles: true,
|
|
657
|
+
packageManagerEvidence
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
var KNOWN_PACKAGE_MANAGERS = ["npm", "pnpm", "yarn", "bun"];
|
|
661
|
+
function isDetectionEvidenceArray(value) {
|
|
662
|
+
return Array.isArray(value) && value.every(
|
|
663
|
+
(item) => item && typeof item === "object" && typeof item.value === "string" && typeof item.source === "string"
|
|
664
|
+
);
|
|
665
|
+
}
|
|
666
|
+
async function readConfirmedStack(rootDir) {
|
|
667
|
+
const configuration = await readJson(
|
|
668
|
+
path6.join(rootDir, REPOSITORY_PROFILE_REL)
|
|
669
|
+
);
|
|
670
|
+
const configured = configuration?.stack;
|
|
671
|
+
const language = configured?.language;
|
|
672
|
+
if (!configured || typeof language !== "string" || !language || language === "unknown") {
|
|
673
|
+
return void 0;
|
|
674
|
+
}
|
|
675
|
+
const testCommands = Array.isArray(configured.testCommands) ? configured.testCommands.filter((item) => typeof item === "string") : [];
|
|
676
|
+
const validationCommands = Array.isArray(configured.validationCommands) ? configured.validationCommands.filter((item) => typeof item === "string") : [];
|
|
677
|
+
if (testCommands.length === 0 && validationCommands.length === 0) return void 0;
|
|
678
|
+
const packageManager = typeof configured.packageManager === "string" && KNOWN_PACKAGE_MANAGERS.includes(configured.packageManager) ? configured.packageManager : void 0;
|
|
679
|
+
const scripts = configured.scripts && typeof configured.scripts === "object" && !Array.isArray(configured.scripts) ? configured.scripts : void 0;
|
|
680
|
+
const packageManagerEvidence = isDetectionEvidenceArray(
|
|
681
|
+
configured.packageManagerEvidence
|
|
682
|
+
) ? configured.packageManagerEvidence : [{ source: "configuration", value: `${REPOSITORY_PROFILE_REL}#stack.language` }];
|
|
683
|
+
return {
|
|
684
|
+
language,
|
|
685
|
+
framework: typeof configured.framework === "string" ? configured.framework : void 0,
|
|
686
|
+
packageManager,
|
|
687
|
+
scripts,
|
|
688
|
+
workspaces: configured.workspaces === true,
|
|
689
|
+
testCommands,
|
|
690
|
+
validationCommands,
|
|
691
|
+
hasProjectFiles: true,
|
|
692
|
+
packageManagerEvidence
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
var LOCKFILES = [
|
|
696
|
+
["pnpm-lock.yaml", "pnpm"],
|
|
697
|
+
["yarn.lock", "yarn"],
|
|
698
|
+
["bun.lockb", "bun"],
|
|
699
|
+
["bun.lock", "bun"],
|
|
700
|
+
["package-lock.json", "npm"]
|
|
701
|
+
];
|
|
702
|
+
async function detectPackageManager(rootDir, packageJson) {
|
|
703
|
+
const configured = packageJson.packageManager?.split("@")[0];
|
|
704
|
+
if (configured && ["npm", "pnpm", "yarn", "bun"].includes(configured)) {
|
|
705
|
+
return {
|
|
706
|
+
packageManager: configured,
|
|
707
|
+
evidence: [{ source: "configuration", value: `package.json#packageManager=${configured}` }]
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
for (const [lockfile, packageManager] of LOCKFILES) {
|
|
711
|
+
if (await fileExists(path6.join(rootDir, lockfile))) {
|
|
712
|
+
return {
|
|
713
|
+
packageManager,
|
|
714
|
+
evidence: [{ source: "file", value: lockfile }]
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
return { evidence: [] };
|
|
719
|
+
}
|
|
720
|
+
function commandsForScripts(scripts, packageManager) {
|
|
721
|
+
const runner = packageManager ?? "npm";
|
|
722
|
+
const command = (name) => runner === "npm" ? `npm run ${name}` : `${runner} ${name}`;
|
|
723
|
+
const testCommands = Object.keys(scripts).filter((name) => name === "test" || name.startsWith("test:")).map(command);
|
|
724
|
+
const validationCommands = ["lint", "typecheck", "check", "build"].filter((name) => scripts[name]).map(command);
|
|
725
|
+
return { testCommands, validationCommands };
|
|
726
|
+
}
|
|
727
|
+
async function detectStack(rootDir) {
|
|
728
|
+
const confirmed = await readConfirmedStack(rootDir);
|
|
729
|
+
if (confirmed) return confirmed;
|
|
730
|
+
const hasAnyProjectMarker = (await Promise.all(PROJECT_MARKERS.map((item) => fileExists(path6.join(rootDir, item))))).some(Boolean);
|
|
731
|
+
if (!hasAnyProjectMarker) {
|
|
732
|
+
const subdirHits = await scanSubdirectoryMarkers(rootDir);
|
|
733
|
+
if (subdirHits.length > 0) {
|
|
734
|
+
return detectStackFromSubdirHits(rootDir, subdirHits);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
const hasPackageJson = await fileExists(path6.join(rootDir, "package.json"));
|
|
738
|
+
if (hasPackageJson) {
|
|
739
|
+
const packageJson = await readJson(path6.join(rootDir, "package.json")) ?? {};
|
|
740
|
+
const scripts = packageJson.scripts ?? {};
|
|
741
|
+
const packageManager = await detectPackageManager(rootDir, packageJson);
|
|
742
|
+
const commands = commandsForScripts(scripts, packageManager.packageManager);
|
|
743
|
+
const hasNextConfig = await fileExists(path6.join(rootDir, "next.config.js")) || await fileExists(path6.join(rootDir, "next.config.mjs")) || await fileExists(path6.join(rootDir, "next.config.ts"));
|
|
744
|
+
const hasNestConfig = await fileExists(path6.join(rootDir, "nest-cli.json"));
|
|
745
|
+
return {
|
|
746
|
+
language: "node",
|
|
747
|
+
framework: hasNextConfig ? "nextjs" : hasNestConfig ? "nestjs" : "node",
|
|
748
|
+
packageManager: packageManager.packageManager,
|
|
749
|
+
packageManagerEvidence: packageManager.evidence,
|
|
750
|
+
scripts,
|
|
751
|
+
workspaces: packageJson.workspaces !== void 0 || await fileExists(path6.join(rootDir, "pnpm-workspace.yaml")),
|
|
752
|
+
...commands,
|
|
753
|
+
hasProjectFiles: hasAnyProjectMarker
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
if (await fileExists(path6.join(rootDir, "pyproject.toml"))) {
|
|
757
|
+
return {
|
|
758
|
+
language: "python",
|
|
759
|
+
framework: "python",
|
|
760
|
+
workspaces: false,
|
|
761
|
+
testCommands: [],
|
|
762
|
+
validationCommands: [],
|
|
763
|
+
hasProjectFiles: hasAnyProjectMarker
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
if (await fileExists(path6.join(rootDir, "go.mod"))) {
|
|
767
|
+
return {
|
|
768
|
+
language: "go",
|
|
769
|
+
framework: "go",
|
|
770
|
+
workspaces: false,
|
|
771
|
+
testCommands: ["go test ./..."],
|
|
772
|
+
validationCommands: [],
|
|
773
|
+
hasProjectFiles: hasAnyProjectMarker
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
if (await fileExists(path6.join(rootDir, "Cargo.toml"))) {
|
|
777
|
+
return {
|
|
778
|
+
language: "rust",
|
|
779
|
+
framework: "rust",
|
|
780
|
+
workspaces: false,
|
|
781
|
+
testCommands: ["cargo test"],
|
|
782
|
+
validationCommands: ["cargo check"],
|
|
783
|
+
hasProjectFiles: hasAnyProjectMarker
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
if (await fileExists(path6.join(rootDir, "composer.json"))) {
|
|
787
|
+
return {
|
|
788
|
+
language: "php",
|
|
789
|
+
framework: "php",
|
|
790
|
+
workspaces: false,
|
|
791
|
+
testCommands: [],
|
|
792
|
+
validationCommands: [],
|
|
793
|
+
hasProjectFiles: hasAnyProjectMarker
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
if (await fileExists(path6.join(rootDir, "pubspec.yaml"))) {
|
|
797
|
+
const { isFlutter } = await detectDartFlavor(rootDir);
|
|
798
|
+
return {
|
|
799
|
+
language: "dart",
|
|
800
|
+
framework: isFlutter ? "flutter" : "dart",
|
|
801
|
+
workspaces: false,
|
|
802
|
+
testCommands: [isFlutter ? "flutter test" : "dart test"],
|
|
803
|
+
validationCommands: DART_VALIDATION_COMMANDS,
|
|
804
|
+
hasProjectFiles: hasAnyProjectMarker
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
return {
|
|
808
|
+
language: "unknown",
|
|
809
|
+
workspaces: false,
|
|
810
|
+
testCommands: [],
|
|
811
|
+
validationCommands: [],
|
|
812
|
+
hasProjectFiles: hasAnyProjectMarker
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// src/scanner/scan.ts
|
|
817
|
+
var GREENFIELD_SAFE_FILES = /* @__PURE__ */ new Set([
|
|
818
|
+
".git",
|
|
819
|
+
".gitignore",
|
|
820
|
+
"LICENSE",
|
|
821
|
+
"README.md",
|
|
822
|
+
".cursor",
|
|
823
|
+
".vscode"
|
|
824
|
+
]);
|
|
825
|
+
function isGreenfieldByEntries(entries) {
|
|
826
|
+
const meaningful = entries.filter((entry) => !GREENFIELD_SAFE_FILES.has(entry));
|
|
827
|
+
return meaningful.length === 0;
|
|
828
|
+
}
|
|
829
|
+
async function runScanner(rootDir) {
|
|
830
|
+
const normalizedRoot = path7.resolve(rootDir);
|
|
831
|
+
const entries = await listDirectory(normalizedRoot);
|
|
832
|
+
const stack = await detectStack(normalizedRoot);
|
|
833
|
+
const purpose = await detectPurpose(normalizedRoot, stack);
|
|
834
|
+
const [git, ide, infra, services, context, agentKit, trackedFiles] = await Promise.all([
|
|
835
|
+
detectGit(normalizedRoot),
|
|
836
|
+
detectIde(normalizedRoot),
|
|
837
|
+
detectInfra(normalizedRoot),
|
|
838
|
+
detectServices(normalizedRoot),
|
|
839
|
+
detectContext(normalizedRoot),
|
|
840
|
+
detectAgentKit(normalizedRoot),
|
|
841
|
+
listTrackedFiles(normalizedRoot)
|
|
842
|
+
]);
|
|
843
|
+
const safety = await detectSafety(normalizedRoot, git.mode === "none" ? [] : trackedFiles);
|
|
844
|
+
const isGreenfield = isGreenfieldByEntries(entries) && purpose.value === "unknown";
|
|
845
|
+
return {
|
|
846
|
+
rootDir: normalizedRoot,
|
|
847
|
+
isGreenfield,
|
|
848
|
+
purpose,
|
|
849
|
+
stack,
|
|
850
|
+
git,
|
|
851
|
+
ide,
|
|
852
|
+
infra,
|
|
853
|
+
services,
|
|
854
|
+
context,
|
|
855
|
+
agentKit,
|
|
856
|
+
safety,
|
|
857
|
+
quality: {
|
|
858
|
+
testCommands: stack.testCommands,
|
|
859
|
+
validationCommands: stack.validationCommands,
|
|
860
|
+
ci: infra.ci,
|
|
861
|
+
hasTests: stack.testCommands.length > 0,
|
|
862
|
+
ciRunCommands: infra.ciRunCommands
|
|
863
|
+
}
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
export {
|
|
868
|
+
detectIde,
|
|
869
|
+
REPOSITORY_PROFILE_REL,
|
|
870
|
+
detectGitDirty,
|
|
871
|
+
REQUIRED_SECRET_PATTERNS,
|
|
872
|
+
KIT_OWNED_IGNORE_PATTERNS,
|
|
873
|
+
runScanner
|
|
874
|
+
};
|