bootproof 0.1.0 → 0.4.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 +873 -109
- package/dist/agent-plan.d.ts +44 -0
- package/dist/agent-plan.js +826 -0
- package/dist/agent-run.d.ts +117 -0
- package/dist/agent-run.js +459 -0
- package/dist/ai-repair.d.ts +58 -0
- package/dist/ai-repair.js +380 -0
- package/dist/cli.js +936 -38
- package/dist/diagnosis.js +114 -17
- package/dist/diff.d.ts +29 -0
- package/dist/diff.js +569 -0
- package/dist/exec.d.ts +30 -2
- package/dist/exec.js +332 -37
- package/dist/external-health.d.ts +16 -0
- package/dist/external-health.js +214 -0
- package/dist/infer.js +489 -41
- package/dist/plan.d.ts +2 -0
- package/dist/plan.js +49 -7
- package/dist/proof.d.ts +78 -2
- package/dist/proof.js +266 -13
- package/dist/receipt.d.ts +52 -0
- package/dist/receipt.js +356 -0
- package/dist/redact.d.ts +4 -0
- package/dist/redact.js +86 -2
- package/dist/registry.d.ts +82 -30
- package/dist/registry.js +355 -53
- package/dist/remote.d.ts +12 -1
- package/dist/remote.js +62 -18
- package/dist/repair-playbooks.d.ts +24 -0
- package/dist/repair-playbooks.js +593 -0
- package/dist/repair-safety.d.ts +130 -0
- package/dist/repair-safety.js +766 -0
- package/dist/repair.d.ts +142 -0
- package/dist/repair.js +1566 -0
- package/dist/run.d.ts +6 -1
- package/dist/run.js +385 -46
- package/dist/sbom.d.ts +22 -0
- package/dist/sbom.js +99 -0
- package/dist/taxonomy.d.ts +8 -2
- package/dist/taxonomy.js +428 -8
- package/dist/types.d.ts +57 -2
- package/docs/AGENT_IN_THE_LOOP.md +171 -0
- package/docs/AGENT_RUN_RECEIPTS.md +38 -0
- package/docs/CI_ACTION.md +71 -5
- package/docs/DETERMINISTIC_REPAIR_SAFETY_MODEL.md +705 -0
- package/docs/FAILURE_TAXONOMY.md +30 -1
- package/docs/HONESTY_CONTRACT.md +55 -4
- package/docs/LAUNCH_PLAYBOOK.md +232 -0
- package/docs/REAL_REPO_EVIDENCE.md +77 -0
- package/docs/REAL_WORLD_FIXTURES.md +105 -0
- package/docs/REGISTRY.md +48 -28
- package/docs/RELEASE_CHECKLIST.md +9 -1
- package/docs/REPAIR_RECEIPT.md +224 -0
- package/docs/agent-loop-gap-analysis.md +188 -0
- package/docs/examples/registry-seeds/advertised-port-mismatch.json +28 -0
- package/docs/examples/registry-seeds/airbyte-abctl-external-orchestrator.json +36 -0
- package/docs/examples/registry-seeds/go-ollama-service.json +36 -0
- package/docs/examples/registry-seeds/laravel-vite-sqlite.json +36 -0
- package/docs/examples/registry-seeds/monorepo-ambiguous-health.json +29 -0
- package/docs/examples/registry-seeds/php-composer.json +33 -0
- package/docs/examples/registry-seeds/rails-bundler.json +32 -0
- package/docs/examples/registry-seeds/sentry-devenv-direnv.json +41 -0
- package/docs/schemas/action-verdict-v1.schema.json +64 -0
- package/docs/schemas/agent-plan-v1.schema.json +148 -0
- package/docs/schemas/agent-run-receipts-v1.schema.json +192 -0
- package/docs/schemas/ai-repair-suggestion-v1.schema.json +70 -0
- package/docs/schemas/ci-context-v1.schema.json +63 -0
- package/docs/schemas/diff-result-v1.schema.json +66 -0
- package/docs/schemas/federated-receipt-v1.schema.json +51 -0
- package/docs/schemas/registry-entry-v1.schema.json +95 -0
- package/docs/schemas/registry-seed-example-v1.schema.json +102 -0
- package/docs/schemas/repair-action-v1.schema.json +136 -0
- package/docs/schemas/repair-receipt-v1.schema.json +221 -0
- package/package.json +13 -6
package/dist/infer.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { parse } from "yaml";
|
|
4
|
+
import { repoComposeRepairFile } from "./plan.js";
|
|
3
5
|
function readJson(p) {
|
|
4
6
|
try {
|
|
5
7
|
return JSON.parse(fs.readFileSync(p, "utf8"));
|
|
@@ -30,6 +32,125 @@ function readText(repo, rel) {
|
|
|
30
32
|
function present(repo, paths) {
|
|
31
33
|
return paths.filter(rel => exists(repo, rel));
|
|
32
34
|
}
|
|
35
|
+
const REPO_COMPOSE_FILES = [
|
|
36
|
+
"docker-compose.yml",
|
|
37
|
+
"docker-compose.yaml",
|
|
38
|
+
"compose.yaml",
|
|
39
|
+
"compose.yml",
|
|
40
|
+
"docker-compose.dev.yml",
|
|
41
|
+
"docker-compose.dev.yaml",
|
|
42
|
+
"docker/docker-compose.yml",
|
|
43
|
+
"docker/docker-compose.yaml",
|
|
44
|
+
];
|
|
45
|
+
const VITE_CONFIG_FILES = [
|
|
46
|
+
"vite.config.js",
|
|
47
|
+
"vite.config.ts",
|
|
48
|
+
"vite.config.mjs",
|
|
49
|
+
"vite.config.cjs",
|
|
50
|
+
];
|
|
51
|
+
function detectRepoComposeFile(repo) {
|
|
52
|
+
return REPO_COMPOSE_FILES.find(file => exists(repo, file)) ?? null;
|
|
53
|
+
}
|
|
54
|
+
function composePublishedPort(value) {
|
|
55
|
+
if (typeof value === "number")
|
|
56
|
+
return null;
|
|
57
|
+
if (typeof value === "object" && value !== null) {
|
|
58
|
+
const item = value;
|
|
59
|
+
const host = Number(item.published);
|
|
60
|
+
const container = Number(item.target);
|
|
61
|
+
return Number.isInteger(host) && Number.isInteger(container) ? { host, container } : null;
|
|
62
|
+
}
|
|
63
|
+
if (typeof value !== "string")
|
|
64
|
+
return null;
|
|
65
|
+
const normalized = value.replace(/\$\{[^}:]+:-?(\d+)\}/g, "$1").split("/")[0];
|
|
66
|
+
const parts = normalized.split(":").map(part => part.trim());
|
|
67
|
+
if (parts.length < 2)
|
|
68
|
+
return null;
|
|
69
|
+
const host = Number(parts.at(-2));
|
|
70
|
+
const container = Number(parts.at(-1));
|
|
71
|
+
return Number.isInteger(host) && Number.isInteger(container) ? { host, container } : null;
|
|
72
|
+
}
|
|
73
|
+
function composeHealthPath(service, containerPort) {
|
|
74
|
+
const test = service.healthcheck?.test;
|
|
75
|
+
const text = Array.isArray(test) ? test.join(" ") : typeof test === "string" ? test : "";
|
|
76
|
+
const url = text.match(/https?:\/\/(?:localhost|127\.0\.0\.1):(\d{2,5})(\/[^\s"'\\]*)?/i);
|
|
77
|
+
if (!url || Number(url[1]) !== containerPort)
|
|
78
|
+
return "/";
|
|
79
|
+
return url[2] || "/";
|
|
80
|
+
}
|
|
81
|
+
function sourceBuildUsesRepo(repo, composeFile, build) {
|
|
82
|
+
const context = typeof build === "string"
|
|
83
|
+
? build
|
|
84
|
+
: typeof build === "object" && build !== null
|
|
85
|
+
? String(build.context ?? ".")
|
|
86
|
+
: null;
|
|
87
|
+
if (!context || /^(?:https?|git):/i.test(context))
|
|
88
|
+
return false;
|
|
89
|
+
const composeDir = path.dirname(path.join(repo, composeFile));
|
|
90
|
+
const resolved = path.resolve(composeDir, context);
|
|
91
|
+
const root = path.resolve(repo);
|
|
92
|
+
return resolved === root || resolved.startsWith(`${root}${path.sep}`);
|
|
93
|
+
}
|
|
94
|
+
function detectComposeApplications(repo, composeFile) {
|
|
95
|
+
if (!composeFile)
|
|
96
|
+
return [];
|
|
97
|
+
try {
|
|
98
|
+
const document = parse(readText(repo, composeFile));
|
|
99
|
+
const applications = [];
|
|
100
|
+
for (const [name, service] of Object.entries(document?.services ?? {})) {
|
|
101
|
+
const source = sourceBuildUsesRepo(repo, composeFile, service.build) ? "build" : service.image ? "image" : null;
|
|
102
|
+
if (!source)
|
|
103
|
+
continue;
|
|
104
|
+
const healthCandidates = (Array.isArray(service.ports) ? service.ports : [])
|
|
105
|
+
.map(composePublishedPort)
|
|
106
|
+
.filter((port) => Boolean(port))
|
|
107
|
+
.filter(port => ![3306, 5432, 6379, 27017].includes(port.container))
|
|
108
|
+
.map(port => `http://localhost:${port.host}${composeHealthPath(service, port.container)}`);
|
|
109
|
+
if (healthCandidates.length)
|
|
110
|
+
applications.push({ name, source, healthCandidates: [...new Set(healthCandidates)] });
|
|
111
|
+
}
|
|
112
|
+
return applications;
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return [];
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function applyBootProofComposeOverride(repo, repoComposeFile, applications) {
|
|
119
|
+
if (!repoComposeFile)
|
|
120
|
+
return applications;
|
|
121
|
+
const repairFile = repoComposeRepairFile(repoComposeFile);
|
|
122
|
+
const overridePath = path.join(repo, repairFile);
|
|
123
|
+
if (!fs.existsSync(overridePath))
|
|
124
|
+
return applications;
|
|
125
|
+
try {
|
|
126
|
+
const document = parse(readText(repo, repairFile));
|
|
127
|
+
return applications.map(application => {
|
|
128
|
+
const service = document.services?.[application.name];
|
|
129
|
+
const port = (Array.isArray(service?.ports) ? service.ports : [])
|
|
130
|
+
.map(composePublishedPort)
|
|
131
|
+
.find((candidate) => Boolean(candidate));
|
|
132
|
+
if (!port)
|
|
133
|
+
return application;
|
|
134
|
+
return {
|
|
135
|
+
...application,
|
|
136
|
+
healthCandidates: application.healthCandidates.map(candidate => {
|
|
137
|
+
try {
|
|
138
|
+
const url = new URL(candidate);
|
|
139
|
+
url.hostname = "localhost";
|
|
140
|
+
url.port = String(port.host);
|
|
141
|
+
return url.toString();
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
return candidate;
|
|
145
|
+
}
|
|
146
|
+
}),
|
|
147
|
+
};
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
return applications;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
33
154
|
function packageManagerFromField(field) {
|
|
34
155
|
if (!field)
|
|
35
156
|
return null;
|
|
@@ -77,6 +198,45 @@ function pickAppCommand(pkg, pm) {
|
|
|
77
198
|
}
|
|
78
199
|
return { command: null, source: "no dev/start/serve/preview script found", script: null };
|
|
79
200
|
}
|
|
201
|
+
function shellSegmentExecutable(segment) {
|
|
202
|
+
let remaining = segment.trim();
|
|
203
|
+
while (/^[A-Za-z_][A-Za-z0-9_]*=(?:"[^"]*"|'[^']*'|[^\s]+)\s+/.test(remaining)) {
|
|
204
|
+
remaining = remaining.replace(/^[A-Za-z_][A-Za-z0-9_]*=(?:"[^"]*"|'[^']*'|[^\s]+)\s+/, "");
|
|
205
|
+
}
|
|
206
|
+
return remaining.match(/^([A-Za-z0-9_.-]+)/)?.[1] ?? null;
|
|
207
|
+
}
|
|
208
|
+
function projectCliFromScript(scriptCommand, packageManager) {
|
|
209
|
+
const standardCommands = new Set([
|
|
210
|
+
"npm", "pnpm", "yarn", "bun", "node", "npx", "corepack",
|
|
211
|
+
"go", "ruby", "bundle", "make", "python", "python3", "php", "composer",
|
|
212
|
+
"cd", "echo", "env", "export", "sh", "bash", "true", "false", "test",
|
|
213
|
+
]);
|
|
214
|
+
if (packageManager !== "unknown")
|
|
215
|
+
standardCommands.add(packageManager);
|
|
216
|
+
for (const segment of scriptCommand.split(/\s*(?:&&|\|\||;)\s*/)) {
|
|
217
|
+
const executable = shellSegmentExecutable(segment);
|
|
218
|
+
if (executable && !standardCommands.has(executable))
|
|
219
|
+
return executable;
|
|
220
|
+
}
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
function executableAvailable(repo, executable) {
|
|
224
|
+
const localBin = path.join(repo, "node_modules", ".bin", executable);
|
|
225
|
+
if (exists(repo, path.relative(repo, localBin)))
|
|
226
|
+
return true;
|
|
227
|
+
const extensions = process.platform === "win32"
|
|
228
|
+
? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";")
|
|
229
|
+
: [""];
|
|
230
|
+
for (const directory of (process.env.PATH ?? "").split(path.delimiter).filter(Boolean)) {
|
|
231
|
+
for (const extension of extensions) {
|
|
232
|
+
if (fs.existsSync(path.join(directory, `${executable}${extension.toLowerCase()}`))
|
|
233
|
+
|| fs.existsSync(path.join(directory, `${executable}${extension.toUpperCase()}`))) {
|
|
234
|
+
return true;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
80
240
|
function detectNestedFrontend(repo) {
|
|
81
241
|
const preferred = ["superset-frontend", "frontend", "web", "ui", "client"];
|
|
82
242
|
for (const dir of preferred) {
|
|
@@ -86,40 +246,176 @@ function detectNestedFrontend(repo) {
|
|
|
86
246
|
}
|
|
87
247
|
return null;
|
|
88
248
|
}
|
|
89
|
-
function
|
|
249
|
+
function detectMakeCommand(repo, makefile) {
|
|
250
|
+
for (const target of ["run", "serve", "server", "start", "dev"]) {
|
|
251
|
+
if (new RegExp(`^${target}:`, "m").test(makefile)) {
|
|
252
|
+
return { command: `make ${target}`, source: `Makefile target: ${target}` };
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
function detectGoEntrypoint(repo) {
|
|
258
|
+
const candidates = [];
|
|
259
|
+
if (exists(repo, "main.go"))
|
|
260
|
+
candidates.push("main.go");
|
|
261
|
+
try {
|
|
262
|
+
for (const name of fs.readdirSync(path.join(repo, "cmd"))) {
|
|
263
|
+
if (exists(repo, `cmd/${name}/main.go`))
|
|
264
|
+
candidates.push(`cmd/${name}/main.go`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
catch { /* no cmd directory */ }
|
|
268
|
+
if (candidates.length !== 1)
|
|
269
|
+
return null;
|
|
270
|
+
const entry = candidates[0];
|
|
271
|
+
const sourceText = readText(repo, entry);
|
|
272
|
+
const packagePath = entry === "main.go" ? "." : `./${path.posix.dirname(entry)}`;
|
|
273
|
+
return {
|
|
274
|
+
commandBase: `go run ${packagePath}`,
|
|
275
|
+
source: `Go main package: ${entry}`,
|
|
276
|
+
sourceText,
|
|
277
|
+
dataDirFlag: /(?:String|StringVar)\(\s*["']data["']/m.test(sourceText),
|
|
278
|
+
portFlag: /(?:Int|IntVar)\(\s*["']port["']/m.test(sourceText),
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
function detectGoService(repo) {
|
|
282
|
+
if (!exists(repo, "go.mod")) {
|
|
283
|
+
return {
|
|
284
|
+
ollamaLike: false,
|
|
285
|
+
command: null,
|
|
286
|
+
commandSource: null,
|
|
287
|
+
port: null,
|
|
288
|
+
healthCandidates: [],
|
|
289
|
+
markers: [],
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
const moduleFile = readText(repo, "go.mod");
|
|
293
|
+
const mainFile = readText(repo, "main.go");
|
|
294
|
+
const commandFile = readText(repo, "cmd/cmd.go");
|
|
295
|
+
const routesFile = readText(repo, "server/routes.go");
|
|
296
|
+
const envConfigFile = readText(repo, "envconfig/config.go");
|
|
297
|
+
const evidence = [moduleFile, mainFile, commandFile, routesFile, envConfigFile].join("\n");
|
|
298
|
+
const exactModule = /^module\s+github\.com\/ollama\/ollama\s*$/m.test(moduleFile);
|
|
299
|
+
const ollamaModule = /^module\s+\S*ollama(?:\/\S*)?\s*$/mi.test(moduleFile);
|
|
300
|
+
const markers = [
|
|
301
|
+
exactModule || ollamaModule ? "ollama module" : null,
|
|
302
|
+
/\bOLLAMA_HOST\b/.test(evidence) ? "OLLAMA_HOST" : null,
|
|
303
|
+
/\b11434\b/.test(evidence) ? "port 11434" : null,
|
|
304
|
+
/["'`]\/api\/tags["'`]/.test(evidence) ? "/api/tags" : null,
|
|
305
|
+
/\bUse:\s*["']serve["']|\bollama serve\b/i.test(evidence) ? "serve command" : null,
|
|
306
|
+
].filter((marker) => marker !== null);
|
|
307
|
+
const hasRootEntrypoint = exists(repo, "main.go");
|
|
308
|
+
const hasServeEvidence = markers.includes("serve command");
|
|
309
|
+
const strongOllamaEvidence = exactModule || markers.length >= 3;
|
|
310
|
+
const ollamaLike = hasRootEntrypoint && hasServeEvidence && strongOllamaEvidence;
|
|
311
|
+
return {
|
|
312
|
+
ollamaLike,
|
|
313
|
+
command: ollamaLike ? "go run . serve" : null,
|
|
314
|
+
commandSource: ollamaLike ? "Ollama Go service entrypoint: main.go + serve command" : null,
|
|
315
|
+
port: ollamaLike ? 11434 : null,
|
|
316
|
+
healthCandidates: ollamaLike
|
|
317
|
+
? [
|
|
318
|
+
"http://127.0.0.1:11434/",
|
|
319
|
+
"http://localhost:11434/",
|
|
320
|
+
"http://127.0.0.1:11434/api/tags",
|
|
321
|
+
"http://localhost:11434/api/tags",
|
|
322
|
+
]
|
|
323
|
+
: [],
|
|
324
|
+
markers,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
function detectRubyCommand(repo) {
|
|
328
|
+
if (!exists(repo, "Gemfile") || !exists(repo, "bin/rails"))
|
|
329
|
+
return null;
|
|
330
|
+
return { commandBase: "bundle exec rails server -b 127.0.0.1", source: "Rails entrypoint: bin/rails" };
|
|
331
|
+
}
|
|
332
|
+
function detectArchitecture(repo, pkg, nestedFrontend, repoComposeFile) {
|
|
90
333
|
const makefile = readText(repo, "Makefile");
|
|
91
334
|
const pyproject = readText(repo, "pyproject.toml");
|
|
92
335
|
const setupPy = readText(repo, "setup.py");
|
|
93
|
-
const
|
|
336
|
+
const requirements = readText(repo, "requirements.txt");
|
|
337
|
+
const compose = repoComposeFile ? readText(repo, repoComposeFile) : "";
|
|
338
|
+
const composer = readJson(path.join(repo, "composer.json"));
|
|
94
339
|
const rootDeps = { ...(pkg?.dependencies ?? {}), ...(pkg?.devDependencies ?? {}) };
|
|
95
340
|
const nestedDeps = { ...(nestedFrontend?.pkg?.dependencies ?? {}), ...(nestedFrontend?.pkg?.devDependencies ?? {}) };
|
|
96
|
-
const backendMarkers = present(repo, ["pyproject.toml", "setup.py", "go.mod", "go.work", "Makefile", "superset/app.py", "superset/config.py"]);
|
|
341
|
+
const backendMarkers = present(repo, ["pyproject.toml", "setup.py", "requirements.txt", "manage.py", "go.mod", "go.work", "Gemfile", "config/database.yml", "Makefile", "superset/app.py", "superset/config.py", "artisan", "composer.json"]);
|
|
97
342
|
if (isDirectory(repo, "pkg"))
|
|
98
343
|
backendMarkers.push("pkg/");
|
|
99
|
-
const frontendMarkers = present(repo, ["package.json", "yarn.lock", "pnpm-lock.yaml", "nx.json"]);
|
|
344
|
+
const frontendMarkers = present(repo, ["package.json", "yarn.lock", "pnpm-lock.yaml", "nx.json", ...VITE_CONFIG_FILES]);
|
|
100
345
|
if (isDirectory(repo, "public"))
|
|
101
346
|
frontendMarkers.push("public/");
|
|
102
347
|
if (isDirectory(repo, "packages"))
|
|
103
348
|
frontendMarkers.push("packages/");
|
|
104
349
|
if (nestedFrontend)
|
|
105
350
|
frontendMarkers.push(`${nestedFrontend.dir}/package.json`);
|
|
106
|
-
const serviceMarkers = present(repo, [
|
|
107
|
-
const
|
|
351
|
+
const serviceMarkers = present(repo, [...REPO_COMPOSE_FILES, "docker-compose-light.yml"]);
|
|
352
|
+
const hasFlaskBackend = (exists(repo, "pyproject.toml") || exists(repo, "setup.py")) &&
|
|
108
353
|
(exists(repo, "superset/app.py") || exists(repo, "superset/config.py") || /\bflask\b/i.test(pyproject + setupPy + makefile));
|
|
109
|
-
const hasFlask =
|
|
354
|
+
const hasFlask = hasFlaskBackend && (/\bflask\b/i.test(pyproject + setupPy + makefile) || exists(repo, "superset/app.py"));
|
|
355
|
+
const hasDjango = exists(repo, "manage.py") && /\bdjango\b/i.test(pyproject + requirements);
|
|
356
|
+
const hasLargePythonNodeHybrid = exists(repo, "pyproject.toml")
|
|
357
|
+
&& exists(repo, "Makefile")
|
|
358
|
+
&& exists(repo, "package.json")
|
|
359
|
+
&& exists(repo, "pnpm-lock.yaml");
|
|
360
|
+
const hasPythonBackend = hasFlaskBackend || hasDjango || hasLargePythonNodeHybrid;
|
|
110
361
|
const hasGoBackend = exists(repo, "go.mod") || exists(repo, "go.work");
|
|
111
|
-
const
|
|
362
|
+
const hasRubyBackend = exists(repo, "Gemfile");
|
|
363
|
+
const hasLaravel = exists(repo, "artisan") && Boolean(composer);
|
|
364
|
+
const hasPhpBackend = hasLaravel;
|
|
365
|
+
const viteConfig = VITE_CONFIG_FILES.find(file => exists(repo, file)) ?? null;
|
|
366
|
+
const hasLaravelViteFrontend = hasLaravel && Boolean(pkg) && Boolean(viteConfig);
|
|
367
|
+
const makeCommand = detectMakeCommand(repo, makefile);
|
|
368
|
+
const goEntrypoint = detectGoEntrypoint(repo);
|
|
369
|
+
const goService = detectGoService(repo);
|
|
370
|
+
const rubyCommand = detectRubyCommand(repo);
|
|
371
|
+
const hasMakeDrivenBackend = Boolean(makefile && /^[A-Za-z0-9_.-]+:\s*(?:[^=]|$)/m.test(makefile));
|
|
372
|
+
const hasNodeFrontend = Boolean(pkg) && (isDirectory(repo, "public")
|
|
373
|
+
|| isDirectory(repo, "packages")
|
|
374
|
+
|| exists(repo, "nx.json")
|
|
375
|
+
|| hasGoBackend
|
|
376
|
+
|| hasRubyBackend
|
|
377
|
+
|| hasLaravelViteFrontend
|
|
378
|
+
|| hasLargePythonNodeHybrid);
|
|
112
379
|
const hasReact = Boolean(rootDeps.react || nestedDeps.react);
|
|
113
380
|
const hasReactFrontend = Boolean(nestedFrontend && hasReact);
|
|
114
381
|
const hasCelery = /\bcelery\b/i.test(pyproject + setupPy + makefile + compose);
|
|
115
382
|
const hasCompose = serviceMarkers.length > 0;
|
|
383
|
+
if (hasLargePythonNodeHybrid) {
|
|
384
|
+
if (isDirectory(repo, "src"))
|
|
385
|
+
backendMarkers.push("src/");
|
|
386
|
+
if (isDirectory(repo, "static"))
|
|
387
|
+
frontendMarkers.push("static/");
|
|
388
|
+
if (isDirectory(repo, "devservices"))
|
|
389
|
+
serviceMarkers.push("devservices/");
|
|
390
|
+
}
|
|
391
|
+
if (hasGoBackend) {
|
|
392
|
+
if (exists(repo, "main.go"))
|
|
393
|
+
backendMarkers.push("main.go");
|
|
394
|
+
if (isDirectory(repo, "cmd"))
|
|
395
|
+
backendMarkers.push("cmd/");
|
|
396
|
+
if (isDirectory(repo, "server"))
|
|
397
|
+
backendMarkers.push("server/");
|
|
398
|
+
backendMarkers.push(...goService.markers);
|
|
399
|
+
}
|
|
116
400
|
const stack = [];
|
|
117
401
|
if (hasPythonBackend)
|
|
118
402
|
stack.push("python-backend");
|
|
119
403
|
if (hasFlask)
|
|
120
404
|
stack.push("flask");
|
|
405
|
+
if (hasDjango)
|
|
406
|
+
stack.push("django");
|
|
121
407
|
if (hasGoBackend)
|
|
122
408
|
stack.push("go-backend");
|
|
409
|
+
if (hasRubyBackend)
|
|
410
|
+
stack.push("ruby-backend");
|
|
411
|
+
if (hasPhpBackend)
|
|
412
|
+
stack.push("php-backend");
|
|
413
|
+
if (hasLaravel)
|
|
414
|
+
stack.push("laravel");
|
|
415
|
+
if (hasLargePythonNodeHybrid
|
|
416
|
+
|| (hasMakeDrivenBackend && !hasPythonBackend && !hasGoBackend && !hasRubyBackend && !hasPhpBackend)) {
|
|
417
|
+
stack.push("make-driven");
|
|
418
|
+
}
|
|
123
419
|
if (hasNodeFrontend)
|
|
124
420
|
stack.push("node-frontend");
|
|
125
421
|
if (hasReactFrontend)
|
|
@@ -128,7 +424,7 @@ function detectArchitecture(repo, pkg, nestedFrontend) {
|
|
|
128
424
|
stack.push("react");
|
|
129
425
|
if (rootDeps.next)
|
|
130
426
|
stack.push("nextjs");
|
|
131
|
-
if (rootDeps.vite)
|
|
427
|
+
if (rootDeps.vite || viteConfig)
|
|
132
428
|
stack.push("vite");
|
|
133
429
|
if (rootDeps.express)
|
|
134
430
|
stack.push("express");
|
|
@@ -142,6 +438,10 @@ function detectArchitecture(repo, pkg, nestedFrontend) {
|
|
|
142
438
|
stack.push("docker-compose");
|
|
143
439
|
if (hasCelery)
|
|
144
440
|
stack.push("celery");
|
|
441
|
+
if (hasLargePythonNodeHybrid)
|
|
442
|
+
stack.push("large-hybrid-app");
|
|
443
|
+
if (hasLargePythonNodeHybrid && isDirectory(repo, "devservices"))
|
|
444
|
+
stack.push("devservices-backed");
|
|
145
445
|
const setupSteps = [];
|
|
146
446
|
if (/^\s*superset db upgrade\s*$/m.test(makefile))
|
|
147
447
|
setupSteps.push("superset db upgrade");
|
|
@@ -150,7 +450,6 @@ function detectArchitecture(repo, pkg, nestedFrontend) {
|
|
|
150
450
|
const flaskCommand = makefile.match(/^\s*(flask run[^\n]*)$/m)?.[1].trim() ?? null;
|
|
151
451
|
const frontendMakeCommand = makefile.match(/^\s*(cd\s+[^;\n]+;\s*(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?(?:dev-server|dev|start)[^\n]*)$/m)?.[1].trim() ?? null;
|
|
152
452
|
const workerCommand = makefile.match(/^\s*(celery\s+--app=[^\n]*\sworker[^\n]*)$/m)?.[1].trim() ?? null;
|
|
153
|
-
const makeRunCommand = hasGoBackend && /^run:\s/m.test(makefile) ? "make run" : null;
|
|
154
453
|
return {
|
|
155
454
|
backendMarkers: [...new Set(backendMarkers)],
|
|
156
455
|
frontendMarkers: [...new Set(frontendMarkers)],
|
|
@@ -160,29 +459,51 @@ function detectArchitecture(repo, pkg, nestedFrontend) {
|
|
|
160
459
|
flaskCommand,
|
|
161
460
|
frontendMakeCommand,
|
|
162
461
|
workerCommand,
|
|
163
|
-
|
|
462
|
+
makeCommand,
|
|
463
|
+
goEntrypoint,
|
|
464
|
+
goService,
|
|
465
|
+
rubyCommand,
|
|
164
466
|
hasPythonBackend,
|
|
467
|
+
hasLargePythonNodeHybrid,
|
|
165
468
|
hasFlask,
|
|
469
|
+
hasDjango,
|
|
166
470
|
hasGoBackend,
|
|
471
|
+
hasRubyBackend,
|
|
472
|
+
hasPhpBackend,
|
|
473
|
+
hasLaravel,
|
|
474
|
+
hasLaravelViteFrontend,
|
|
475
|
+
sailCommand: hasLaravel && exists(repo, "vendor/bin/sail") ? "./vendor/bin/sail up" : null,
|
|
476
|
+
hasMakeDrivenBackend,
|
|
167
477
|
hasNodeFrontend,
|
|
168
478
|
};
|
|
169
479
|
}
|
|
170
|
-
function detectPort(pkg, repo, commands) {
|
|
171
|
-
const sources = [
|
|
480
|
+
function detectPort(pkg, repo, commands, options = {}) {
|
|
481
|
+
const sources = [
|
|
482
|
+
options.ignorePackageScripts ? "" : JSON.stringify(pkg?.scripts ?? {}),
|
|
483
|
+
readText(repo, "Makefile"),
|
|
484
|
+
...commands.filter((v) => Boolean(v)),
|
|
485
|
+
].join("\n");
|
|
172
486
|
const m = sources.match(/(?:-p|--port)(?:=|\s+|[\\"]+)(\d{2,5})/);
|
|
173
487
|
if (m)
|
|
174
488
|
return { port: Number(m[1]), evidence: `port flag in command evidence: ${m[0].replace(/\\"/g, "").trim()}` };
|
|
489
|
+
const goDefault = sources.match(/(?:SetDefault\(\s*["']port["']\s*,|(?:Int|IntVar)\(\s*["']port["']\s*,)\s*(\d{2,5})/);
|
|
490
|
+
if (goDefault)
|
|
491
|
+
return { port: Number(goDefault[1]), evidence: "port default in Go entrypoint" };
|
|
492
|
+
const listen = sources.match(/ListenAndServe\(\s*["']:(\d{2,5})["']/);
|
|
493
|
+
if (listen)
|
|
494
|
+
return { port: Number(listen[1]), evidence: "HTTP listen address in source" };
|
|
175
495
|
const envEx = readText(repo, ".env.example");
|
|
176
|
-
const pm = envEx.match(/^PORT=(\d{2,5})/m);
|
|
496
|
+
const pm = envEx.match(/^(?:APP_)?PORT=(\d{2,5})/m);
|
|
177
497
|
if (pm)
|
|
178
|
-
return { port: Number(pm[1]), evidence: "
|
|
179
|
-
|
|
498
|
+
return { port: Number(pm[1]), evidence: `${pm[0].split("=")[0]} in .env.example` };
|
|
499
|
+
const defaultPort = options.defaultPort ?? 3000;
|
|
500
|
+
return { port: defaultPort, evidence: `default assumption (${defaultPort}); not evidence-based` };
|
|
180
501
|
}
|
|
181
|
-
function detectServices(pkg, repo) {
|
|
502
|
+
function detectServices(pkg, repo, repoComposeFile) {
|
|
182
503
|
const out = [];
|
|
183
504
|
const envEx = readText(repo, ".env.example") + readText(repo, ".env.sample");
|
|
184
505
|
const schema = readText(repo, "prisma/schema.prisma");
|
|
185
|
-
const compose =
|
|
506
|
+
const compose = repoComposeFile ? readText(repo, repoComposeFile) : "";
|
|
186
507
|
const pyproject = readText(repo, "pyproject.toml");
|
|
187
508
|
const deps = { ...(pkg?.dependencies ?? {}), ...(pkg?.devDependencies ?? {}) };
|
|
188
509
|
if (/postgres(ql)?:\/\//i.test(envEx) || /provider\s*=\s*"postgresql"/.test(schema) || deps.pg || /^\s{0,4}postgres:\s*$/m.test(compose))
|
|
@@ -229,15 +550,16 @@ function workspacePatterns(repo, pkg) {
|
|
|
229
550
|
return [...new Set(patterns)];
|
|
230
551
|
}
|
|
231
552
|
function expandWorkspacePattern(repo, pattern) {
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
553
|
+
const normalizedPattern = pattern.replace(/\\/g, "/");
|
|
554
|
+
if (!normalizedPattern.includes("*"))
|
|
555
|
+
return exists(repo, `${normalizedPattern}/package.json`) ? [normalizedPattern] : [];
|
|
556
|
+
const beforeStar = normalizedPattern.slice(0, normalizedPattern.indexOf("*"));
|
|
235
557
|
const base = beforeStar.replace(/\/$/, "");
|
|
236
|
-
const suffix =
|
|
558
|
+
const suffix = normalizedPattern.slice(normalizedPattern.indexOf("*") + 1).replace(/^\//, "");
|
|
237
559
|
const baseAbs = path.join(repo, base);
|
|
238
560
|
try {
|
|
239
561
|
return fs.readdirSync(baseAbs)
|
|
240
|
-
.map(name => path.join(base, name, suffix))
|
|
562
|
+
.map(name => path.posix.join(base, name, suffix))
|
|
241
563
|
.filter(dir => exists(repo, `${dir}/package.json`));
|
|
242
564
|
}
|
|
243
565
|
catch {
|
|
@@ -245,6 +567,7 @@ function expandWorkspacePattern(repo, pattern) {
|
|
|
245
567
|
}
|
|
246
568
|
}
|
|
247
569
|
const TEST_PATH = /(^|[-_/])(e2e|tests?|test-plugins|fixtures?|examples?|samples?|demos?|mocks?)([-_/]|$)/i;
|
|
570
|
+
const DOCUMENTATION_PATH = /(^|[\/])(storybook|docs?)([\/]|$)/i;
|
|
248
571
|
const SCAFFOLD_NAME = /^create-|-(template|example|fixture|sandbox|sample|demo|mock)$/i;
|
|
249
572
|
function scoreWorkspace(repo, dir, wpkg, isRoot) {
|
|
250
573
|
const { command } = pickAppCommand(wpkg, "npm");
|
|
@@ -293,6 +616,10 @@ function scoreWorkspace(repo, dir, wpkg, isRoot) {
|
|
|
293
616
|
score -= 10;
|
|
294
617
|
reasons.push("test/example path downranked");
|
|
295
618
|
}
|
|
619
|
+
if (DOCUMENTATION_PATH.test(`${dir}/${wpkg?.name ?? ""}`)) {
|
|
620
|
+
score -= 3;
|
|
621
|
+
reasons.push("documentation/storybook downranked");
|
|
622
|
+
}
|
|
296
623
|
if (wpkg?.private !== true && (wpkg?.main || wpkg?.exports) && !command) {
|
|
297
624
|
score -= 2;
|
|
298
625
|
reasons.push("looks like a publishable library");
|
|
@@ -343,41 +670,147 @@ export function inferRepo(repoPath, opts = {}) {
|
|
|
343
670
|
const rootRepo = path.resolve(repoPath);
|
|
344
671
|
const rootPkg = opts.workspace ? readJson(path.join(rootRepo, "package.json")) : pkg;
|
|
345
672
|
const nestedFrontend = detectNestedFrontend(repo);
|
|
346
|
-
const
|
|
673
|
+
const repoComposeFile = detectRepoComposeFile(repo);
|
|
674
|
+
const composeApplicationServices = applyBootProofComposeOverride(repo, repoComposeFile, detectComposeApplications(repo, repoComposeFile));
|
|
675
|
+
const sourceComposeApplications = composeApplicationServices.filter(service => service.source === "build");
|
|
676
|
+
const composeHealthCandidates = sourceComposeApplications.length === 1
|
|
677
|
+
? sourceComposeApplications[0].healthCandidates
|
|
678
|
+
: [];
|
|
679
|
+
const architecture = detectArchitecture(repo, pkg, nestedFrontend, repoComposeFile);
|
|
347
680
|
const pm = detectPackageManager(repo, pkg, nestedFrontend?.dir ?? null);
|
|
348
681
|
const rootApp = pickAppCommand(pkg, pm.pm);
|
|
349
|
-
const
|
|
682
|
+
const rootScriptText = rootApp.script ? String(pkg?.scripts?.[rootApp.script] ?? "") : "";
|
|
683
|
+
const projectCliCommand = architecture.hasLargePythonNodeHybrid && rootScriptText
|
|
684
|
+
? projectCliFromScript(rootScriptText, pm.pm)
|
|
685
|
+
: null;
|
|
686
|
+
const projectCliReady = projectCliCommand === null
|
|
687
|
+
? null
|
|
688
|
+
: executableAvailable(repo, projectCliCommand);
|
|
689
|
+
const assetDevServerCommand = architecture.hasLaravelViteFrontend && /\bvite\b/i.test(rootScriptText)
|
|
690
|
+
? rootApp.command
|
|
691
|
+
: null;
|
|
692
|
+
const commandEvidence = [
|
|
693
|
+
architecture.flaskCommand,
|
|
694
|
+
architecture.makeCommand?.command ?? null,
|
|
695
|
+
architecture.goEntrypoint?.sourceText ?? null,
|
|
696
|
+
architecture.goService.command,
|
|
697
|
+
architecture.rubyCommand?.commandBase ?? null,
|
|
698
|
+
architecture.sailCommand,
|
|
699
|
+
];
|
|
700
|
+
const { port, evidence: portEvidence } = detectPort(pkg, repo, commandEvidence, {
|
|
701
|
+
ignorePackageScripts: architecture.hasLaravel,
|
|
702
|
+
defaultPort: architecture.goService.port
|
|
703
|
+
?? (architecture.sailCommand ? 80 : architecture.hasLaravel ? 8000 : 3000),
|
|
704
|
+
});
|
|
705
|
+
const goCommand = architecture.goService.command ?? (architecture.goEntrypoint
|
|
706
|
+
? [
|
|
707
|
+
architecture.goEntrypoint.commandBase,
|
|
708
|
+
architecture.goEntrypoint.portFlag ? `--port ${port}` : "",
|
|
709
|
+
architecture.goEntrypoint.dataDirFlag ? "--data .bootproof/runtime/go-app" : "",
|
|
710
|
+
].filter(Boolean).join(" ")
|
|
711
|
+
: null);
|
|
712
|
+
const rubyCommand = architecture.rubyCommand ? `${architecture.rubyCommand.commandBase} -p ${port}` : null;
|
|
713
|
+
const djangoCommand = architecture.hasDjango ? `python manage.py runserver 127.0.0.1:${port}` : null;
|
|
714
|
+
const laravelCommand = architecture.hasLaravel
|
|
715
|
+
? architecture.sailCommand ?? `php artisan serve --host=127.0.0.1 --port=${port}`
|
|
716
|
+
: null;
|
|
717
|
+
const repositoryBackendCommand = architecture.makeCommand?.command ?? goCommand ?? rubyCommand;
|
|
718
|
+
const backendCommand = architecture.flaskCommand ?? djangoCommand ?? laravelCommand ?? repositoryBackendCommand;
|
|
350
719
|
const nestedFrontendCommand = architecture.frontendMakeCommand;
|
|
351
720
|
const frontendCommand = nestedFrontendCommand ?? rootApp.command;
|
|
352
|
-
const appCommand = architecture.flaskCommand
|
|
721
|
+
const appCommand = architecture.flaskCommand
|
|
722
|
+
?? djangoCommand
|
|
723
|
+
?? laravelCommand
|
|
724
|
+
?? (architecture.hasGoBackend && architecture.hasNodeFrontend && rootApp.command ? rootApp.command : null)
|
|
725
|
+
?? repositoryBackendCommand
|
|
726
|
+
?? rootApp.command;
|
|
727
|
+
const selectedProjectCliCommand = rootApp.command === appCommand ? projectCliCommand : null;
|
|
728
|
+
const selectedProjectCliReady = selectedProjectCliCommand === null ? null : projectCliReady;
|
|
353
729
|
const appCommandSource = architecture.flaskCommand
|
|
354
730
|
? `Makefile Flask command: ${architecture.flaskCommand}`
|
|
355
|
-
:
|
|
356
|
-
|
|
731
|
+
: djangoCommand && appCommand === djangoCommand
|
|
732
|
+
? "Django entrypoint: manage.py"
|
|
733
|
+
: laravelCommand && appCommand === laravelCommand
|
|
734
|
+
? architecture.sailCommand
|
|
735
|
+
? "Laravel Sail entrypoint: vendor/bin/sail"
|
|
736
|
+
: "Laravel entrypoint: artisan"
|
|
737
|
+
: architecture.makeCommand && appCommand === architecture.makeCommand.command
|
|
738
|
+
? architecture.makeCommand.source
|
|
739
|
+
: goCommand && appCommand === goCommand
|
|
740
|
+
? architecture.goService.commandSource ?? architecture.goEntrypoint?.source ?? rootApp.source
|
|
741
|
+
: rubyCommand && appCommand === rubyCommand
|
|
742
|
+
? architecture.rubyCommand?.source ?? rootApp.source
|
|
743
|
+
: selectedProjectCliCommand && selectedProjectCliReady === false
|
|
744
|
+
? `${rootApp.source}; project CLI ${selectedProjectCliCommand} readiness not established`
|
|
745
|
+
: rootApp.source;
|
|
746
|
+
const recognizedApplication = architecture.hasPythonBackend ||
|
|
747
|
+
architecture.hasGoBackend ||
|
|
748
|
+
architecture.hasRubyBackend ||
|
|
749
|
+
architecture.hasPhpBackend ||
|
|
750
|
+
architecture.hasMakeDrivenBackend ||
|
|
751
|
+
composeApplicationServices.some(service => service.source === "build");
|
|
357
752
|
const workspaces = opts.workspace ? [] : rankWorkspaces(rootRepo, rootPkg);
|
|
358
753
|
const notApp = looksLikeLibrary(pkg, appCommand, workspaces.some(candidate => candidate.dir !== "."), recognizedApplication);
|
|
359
|
-
const { port, evidence: portEvidence } = detectPort(pkg, repo, [backendCommand, frontendCommand]);
|
|
360
754
|
const env = detectEnv(repo);
|
|
361
|
-
const services = detectServices(pkg, repo);
|
|
755
|
+
const services = detectServices(pkg, repo, repoComposeFile);
|
|
362
756
|
const rootDeps = { ...(pkg?.dependencies ?? {}), ...(pkg?.devDependencies ?? {}) };
|
|
363
|
-
const
|
|
757
|
+
const preparationCommands = [];
|
|
758
|
+
const nodePreparationRequired = Boolean(rootApp.command &&
|
|
759
|
+
!architecture.hasLaravel &&
|
|
364
760
|
(Object.keys(rootDeps).length > 0 || exists(repo, "yarn.lock") || exists(repo, "pnpm-lock.yaml") || exists(repo, "package-lock.json") || exists(repo, "nx.json")));
|
|
761
|
+
if (nodePreparationRequired) {
|
|
762
|
+
const command = installCommand(pm.pm, pm.packageDir);
|
|
763
|
+
if (command)
|
|
764
|
+
preparationCommands.push({ id: "install", kind: "install", command, description: "install Node dependencies", source: pm.evidence });
|
|
765
|
+
}
|
|
766
|
+
if (goCommand && appCommand === goCommand && exists(repo, "go.sum")) {
|
|
767
|
+
preparationCommands.push({ id: "go-modules", kind: "install", command: "go mod download", description: "download declared Go modules", source: "go.sum present" });
|
|
768
|
+
}
|
|
769
|
+
if (rubyCommand && appCommand === rubyCommand) {
|
|
770
|
+
preparationCommands.push({ id: "bundle-install", kind: "install", command: "bundle install", description: "install declared Ruby gems", source: "Gemfile and bin/rails present" });
|
|
771
|
+
}
|
|
772
|
+
if (laravelCommand && appCommand === laravelCommand) {
|
|
773
|
+
preparationCommands.push({ id: "composer-install", kind: "install", command: "composer install", description: "install declared PHP dependencies", source: "composer.json and artisan present" });
|
|
774
|
+
}
|
|
775
|
+
const dependencyInstallRequired = preparationCommands.length > 0;
|
|
365
776
|
const incompleteAppCommand = Boolean(architecture.hasGoBackend && architecture.hasNodeFrontend && rootApp.command);
|
|
366
777
|
const multiAppCommand = Boolean(rootApp.command && /\b(?:turbo|nx)\s+run\s+dev\b[^\n]*--parallel\b/i.test(rootApp.source));
|
|
367
778
|
const commandScope = multiAppCommand
|
|
368
779
|
? "multi-workspace development pipeline; no single application health target selected"
|
|
369
780
|
: incompleteAppCommand
|
|
370
781
|
? "frontend/dev pipeline only; Go backend markers also detected"
|
|
371
|
-
: architecture.
|
|
372
|
-
?
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
782
|
+
: architecture.hasLargePythonNodeHybrid && rootApp.command === appCommand
|
|
783
|
+
? selectedProjectCliCommand && selectedProjectCliReady === false
|
|
784
|
+
? `large Python/Node hybrid package script; project CLI ${selectedProjectCliCommand} readiness not established`
|
|
785
|
+
: "large Python/Node hybrid package script"
|
|
786
|
+
: architecture.hasFlask && nestedFrontend
|
|
787
|
+
? "Python/Flask backend command; React frontend and worker require separate orchestration"
|
|
788
|
+
: djangoCommand && appCommand === djangoCommand
|
|
789
|
+
? "Django application command"
|
|
790
|
+
: laravelCommand && appCommand === laravelCommand
|
|
791
|
+
? architecture.sailCommand
|
|
792
|
+
? "Laravel application through Sail"
|
|
793
|
+
: "Laravel application command; Vite is an asset development server only"
|
|
794
|
+
: goCommand && appCommand === goCommand && nestedFrontend
|
|
795
|
+
? "Go application command serving repository-embedded frontend assets"
|
|
796
|
+
: rubyCommand && appCommand === rubyCommand
|
|
797
|
+
? "Rails application command"
|
|
798
|
+
: architecture.makeCommand && appCommand === architecture.makeCommand.command
|
|
799
|
+
? "repository-defined Make target"
|
|
800
|
+
: appCommand
|
|
801
|
+
? "application command"
|
|
802
|
+
: "no runnable command selected";
|
|
376
803
|
const healthCandidates = notApp
|
|
377
804
|
? []
|
|
378
|
-
:
|
|
379
|
-
?
|
|
380
|
-
:
|
|
805
|
+
: !appCommand && composeHealthCandidates.length
|
|
806
|
+
? composeHealthCandidates
|
|
807
|
+
: !appCommand
|
|
808
|
+
? []
|
|
809
|
+
: architecture.goService.ollamaLike
|
|
810
|
+
? architecture.goService.healthCandidates
|
|
811
|
+
: architecture.hasGoBackend && architecture.hasNodeFrontend
|
|
812
|
+
? [`http://localhost:${port}/api/health`, `http://localhost:${port}/`]
|
|
813
|
+
: [`http://localhost:${port}/`];
|
|
381
814
|
let confidence = 0;
|
|
382
815
|
if (appCommand)
|
|
383
816
|
confidence += 35;
|
|
@@ -391,6 +824,8 @@ export function inferRepo(repoPath, opts = {}) {
|
|
|
391
824
|
confidence += 10;
|
|
392
825
|
if (services.length || env.required.length)
|
|
393
826
|
confidence += 5;
|
|
827
|
+
if (selectedProjectCliCommand && selectedProjectCliReady === false)
|
|
828
|
+
confidence = Math.min(confidence, 60);
|
|
394
829
|
return {
|
|
395
830
|
repoPath: repo,
|
|
396
831
|
isApplication: !notApp,
|
|
@@ -399,22 +834,35 @@ export function inferRepo(repoPath, opts = {}) {
|
|
|
399
834
|
backendMarkers: architecture.backendMarkers,
|
|
400
835
|
frontendMarkers: architecture.frontendMarkers,
|
|
401
836
|
serviceMarkers: architecture.serviceMarkers,
|
|
837
|
+
repoComposeFile,
|
|
838
|
+
composeApplicationServices,
|
|
839
|
+
composeHealthCandidates,
|
|
402
840
|
setupSteps: architecture.setupSteps,
|
|
403
841
|
packageManager: pm.pm,
|
|
404
842
|
packageManagerEvidence: pm.evidence,
|
|
405
843
|
packageManagerVersion: pm.version ?? pkg?.engines?.[pm.pm] ?? rootPkg?.engines?.[pm.pm] ?? null,
|
|
406
|
-
installCommand:
|
|
844
|
+
installCommand: preparationCommands.find(command => command.kind === "install")?.command ?? null,
|
|
845
|
+
preparationCommands,
|
|
407
846
|
dependencyInstallRequired,
|
|
408
847
|
appCommand,
|
|
409
848
|
appCommandSource,
|
|
849
|
+
selectedPackageScriptName: rootApp.command === appCommand ? rootApp.script : null,
|
|
850
|
+
selectedPackageScriptCommand: rootApp.command === appCommand ? rootScriptText || null : null,
|
|
851
|
+
projectCliCommand: selectedProjectCliCommand,
|
|
852
|
+
projectCliReady: selectedProjectCliReady,
|
|
410
853
|
backendCommand,
|
|
411
854
|
frontendCommand,
|
|
855
|
+
asset_dev_server_command: assetDevServerCommand,
|
|
412
856
|
workerCommand: architecture.workerCommand,
|
|
413
857
|
commandScope,
|
|
414
858
|
incompleteAppCommand,
|
|
415
859
|
multiAppCommand,
|
|
416
860
|
port,
|
|
417
|
-
portEvidence
|
|
861
|
+
portEvidence: architecture.goService.port
|
|
862
|
+
? "known Ollama service port from repository evidence"
|
|
863
|
+
: portEvidence,
|
|
864
|
+
observedPort: null,
|
|
865
|
+
healthCandidateSource: architecture.goService.ollamaLike ? "known_service" : "inferred",
|
|
418
866
|
healthCandidates,
|
|
419
867
|
services,
|
|
420
868
|
requiredEnv: env.required,
|