@maintainer-pro/ai-cli 0.1.9 → 0.1.12
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/dist/index.cjs +316 -68
- package/dist/index.d.cts +26 -2
- package/dist/index.d.ts +26 -2
- package/dist/index.js +311 -68
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2488,9 +2488,9 @@ var inspectJsonSchema = z.object({
|
|
|
2488
2488
|
name: z.string().max(200).optional(),
|
|
2489
2489
|
summary: z.string().max(800).optional(),
|
|
2490
2490
|
scripts: z.object({
|
|
2491
|
-
ui: z.string().min(1).max(
|
|
2492
|
-
backend: z.string().min(1).max(
|
|
2493
|
-
app: z.string().min(1).max(
|
|
2491
|
+
ui: z.string().min(1).max(200).optional(),
|
|
2492
|
+
backend: z.string().min(1).max(200).optional(),
|
|
2493
|
+
app: z.string().min(1).max(200).optional()
|
|
2494
2494
|
}).optional(),
|
|
2495
2495
|
ports: z.object({
|
|
2496
2496
|
ui: z.number().int().min(1).max(65535).optional(),
|
|
@@ -2518,12 +2518,12 @@ function setupSystemPrompt(input) {
|
|
|
2518
2518
|
Workspace: ${input.workspaceDir}
|
|
2519
2519
|
App name: ${input.appName || "the app"}
|
|
2520
2520
|
|
|
2521
|
-
Inspect this repository (package.json, README, lockfiles, existing env files, how the UI and API start).
|
|
2521
|
+
Inspect this repository (package.json, README, lockfiles, Maven/Gradle/sbt files, Spring/Play config, existing env files, how the UI and API start).
|
|
2522
2522
|
You MAY edit files to fix setup problems that would block local run or the chat widget (env keys, widget mount, missing config).
|
|
2523
2523
|
Do NOT start long-lived dev servers, tunnels, or install global tools.
|
|
2524
2524
|
Do NOT run \`npm run dev\` or similar.
|
|
2525
2525
|
|
|
2526
|
-
Script names you report MUST already exist in package.json. Omit chat/ai-server scripts.
|
|
2526
|
+
Script names you report MUST already exist in package.json, OR be a real JVM start command (./gradlew bootRun, mvn spring-boot:run, sbt run). Omit chat/ai-server scripts.
|
|
2527
2527
|
|
|
2528
2528
|
End with a short human summary AND one \`\`\`json fence with:
|
|
2529
2529
|
|
|
@@ -2595,7 +2595,21 @@ ${lock}`);
|
|
|
2595
2595
|
"vite.config.mjs",
|
|
2596
2596
|
"next.config.js",
|
|
2597
2597
|
"next.config.mjs",
|
|
2598
|
-
"next.config.ts"
|
|
2598
|
+
"next.config.ts",
|
|
2599
|
+
"pom.xml",
|
|
2600
|
+
"build.gradle",
|
|
2601
|
+
"build.gradle.kts",
|
|
2602
|
+
"build.sbt",
|
|
2603
|
+
"application.yml",
|
|
2604
|
+
"application.yaml",
|
|
2605
|
+
"application.properties",
|
|
2606
|
+
"application.conf",
|
|
2607
|
+
"src/main/resources/application.yml",
|
|
2608
|
+
"src/main/resources/application.yaml",
|
|
2609
|
+
"src/main/resources/application.properties",
|
|
2610
|
+
"src/main/resources/application.conf",
|
|
2611
|
+
"src/main/resources/application-dev.yml",
|
|
2612
|
+
"conf/application.conf"
|
|
2599
2613
|
]) {
|
|
2600
2614
|
const body = readSnippet(path7.join(folder, rel), 2500);
|
|
2601
2615
|
if (body) parts.push(`### ${rel}
|
|
@@ -2623,7 +2637,7 @@ DO NOT edit, create, or delete any files.
|
|
|
2623
2637
|
DO NOT run shell commands, install packages, or start servers.
|
|
2624
2638
|
Only infer how this project runs locally from the excerpts.
|
|
2625
2639
|
|
|
2626
|
-
Script names you report MUST appear in package.json scripts
|
|
2640
|
+
Script names you report MUST appear in package.json scripts, OR be a JVM start command such as \`./gradlew bootRun\`, \`mvn spring-boot:run\`, or \`sbt run\`. Omit chat/ai-server scripts.
|
|
2627
2641
|
|
|
2628
2642
|
Reply with a short human summary AND one \`\`\`json fence with:
|
|
2629
2643
|
|
|
@@ -2631,8 +2645,8 @@ Reply with a short human summary AND one \`\`\`json fence with:
|
|
|
2631
2645
|
"kind": "next" | "vite" | "html" | "empty" | "other",
|
|
2632
2646
|
"name": "package or folder name",
|
|
2633
2647
|
"summary": "one sentence about this project",
|
|
2634
|
-
"scripts": { "ui": "dev:client", "backend": "
|
|
2635
|
-
"ports": { "ui": 5173, "backend":
|
|
2648
|
+
"scripts": { "ui": "dev:client", "backend": "sbt run" },
|
|
2649
|
+
"ports": { "ui": 5173, "backend": 9000 },
|
|
2636
2650
|
"issues": ["uncertainties"],
|
|
2637
2651
|
"fixes": [],
|
|
2638
2652
|
"ready": true
|
|
@@ -2765,6 +2779,9 @@ function dataInput(folder, extra) {
|
|
|
2765
2779
|
function hostAppsCachePath2(folder, extra) {
|
|
2766
2780
|
return hostAppsCachePath(dataInput(folder, extra));
|
|
2767
2781
|
}
|
|
2782
|
+
function isAiServerApp(app) {
|
|
2783
|
+
return Boolean(app && (app.role === "ai-server" || app.id === AI_SERVER_APP_ID));
|
|
2784
|
+
}
|
|
2768
2785
|
function defaultAiServerApp(port = AI_SERVER_DEFAULT_PORT) {
|
|
2769
2786
|
return {
|
|
2770
2787
|
id: AI_SERVER_APP_ID,
|
|
@@ -2795,7 +2812,8 @@ function normalizeHostApp(raw) {
|
|
|
2795
2812
|
row.source
|
|
2796
2813
|
) ? row.source : "manual",
|
|
2797
2814
|
locked: row.locked === true || id === AI_SERVER_APP_ID || role === "ai-server",
|
|
2798
|
-
host: role !== "ai-server" && id !== AI_SERVER_APP_ID && row.host === true
|
|
2815
|
+
host: role !== "ai-server" && id !== AI_SERVER_APP_ID && row.host === true,
|
|
2816
|
+
folderPath: typeof row.folderPath === "string" && row.folderPath.trim() ? row.folderPath.trim().slice(0, 500) : null
|
|
2799
2817
|
};
|
|
2800
2818
|
}
|
|
2801
2819
|
function ensureSingleHost(apps) {
|
|
@@ -2825,24 +2843,17 @@ function normalizeHostApps(raw) {
|
|
|
2825
2843
|
const apps = [];
|
|
2826
2844
|
for (const item of raw) {
|
|
2827
2845
|
const app = normalizeHostApp(item);
|
|
2828
|
-
if (!app || seen.has(app.id)) continue;
|
|
2846
|
+
if (!app || isAiServerApp(app) || seen.has(app.id)) continue;
|
|
2829
2847
|
seen.add(app.id);
|
|
2830
2848
|
apps.push(app);
|
|
2831
2849
|
}
|
|
2832
2850
|
return apps;
|
|
2833
2851
|
}
|
|
2834
|
-
function
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
existing.role = "ai-server";
|
|
2840
|
-
existing.locked = true;
|
|
2841
|
-
existing.host = false;
|
|
2842
|
-
existing.name = existing.name || "AI server";
|
|
2843
|
-
return ensureSingleHost([existing, ...next.filter((app) => app !== existing)]);
|
|
2844
|
-
}
|
|
2845
|
-
return ensureSingleHost([defaultAiServerApp(preferredPort), ...next]);
|
|
2852
|
+
function withoutAiServerApp(apps) {
|
|
2853
|
+
return ensureSingleHost(normalizeHostApps(apps).filter((app) => !isAiServerApp(app)));
|
|
2854
|
+
}
|
|
2855
|
+
function ensureAiServerApp(apps, _preferredPort = AI_SERVER_DEFAULT_PORT) {
|
|
2856
|
+
return withoutAiServerApp(apps);
|
|
2846
2857
|
}
|
|
2847
2858
|
function readText(file) {
|
|
2848
2859
|
try {
|
|
@@ -2908,19 +2919,151 @@ function configPort(folder, files) {
|
|
|
2908
2919
|
}
|
|
2909
2920
|
return 0;
|
|
2910
2921
|
}
|
|
2911
|
-
|
|
2922
|
+
var JVM_CONFIG_FILES = [
|
|
2923
|
+
"pom.xml",
|
|
2924
|
+
"build.gradle",
|
|
2925
|
+
"build.gradle.kts",
|
|
2926
|
+
"settings.gradle",
|
|
2927
|
+
"settings.gradle.kts",
|
|
2928
|
+
"build.sbt",
|
|
2929
|
+
"gradle.properties",
|
|
2930
|
+
"application.yml",
|
|
2931
|
+
"application.yaml",
|
|
2932
|
+
"application.properties",
|
|
2933
|
+
"application.conf",
|
|
2934
|
+
"src/main/resources/application.yml",
|
|
2935
|
+
"src/main/resources/application.yaml",
|
|
2936
|
+
"src/main/resources/application.properties",
|
|
2937
|
+
"src/main/resources/application.conf",
|
|
2938
|
+
"src/main/resources/application-dev.yml",
|
|
2939
|
+
"src/main/resources/application-dev.yaml",
|
|
2940
|
+
"src/main/resources/application-dev.properties",
|
|
2941
|
+
"src/main/resources/application-local.yml",
|
|
2942
|
+
"conf/application.conf",
|
|
2943
|
+
"conf/application.yml"
|
|
2944
|
+
];
|
|
2945
|
+
var FINGERPRINT_FILES = [
|
|
2946
|
+
"package.json",
|
|
2947
|
+
...ENV_FILES,
|
|
2948
|
+
"vite.config.ts",
|
|
2949
|
+
"vite.config.js",
|
|
2950
|
+
"vite.config.mjs",
|
|
2951
|
+
"next.config.js",
|
|
2952
|
+
"next.config.mjs",
|
|
2953
|
+
"next.config.ts",
|
|
2954
|
+
...JVM_CONFIG_FILES
|
|
2955
|
+
];
|
|
2956
|
+
function fileExists(folder, rel) {
|
|
2957
|
+
return fs7.existsSync(path8.join(folder, rel));
|
|
2958
|
+
}
|
|
2959
|
+
function jvmPortFromText(text) {
|
|
2960
|
+
const patterns = [
|
|
2961
|
+
/(?:^|\n)\s*server\.port\s*[:=]\s*["']?(\d{2,5})/im,
|
|
2962
|
+
/(?:^|\n)\s*SERVER_PORT\s*[:=]\s*["']?(\d{2,5})/im,
|
|
2963
|
+
/(?:^|\n)\s*play\.http\.port\s*[:=]\s*["']?(\d{2,5})/im,
|
|
2964
|
+
/(?:^|\n)\s*play\.server\.http\.port\s*[:=]\s*["']?(\d{2,5})/im,
|
|
2965
|
+
/(?:^|\n)\s*http\.port\s*[:=]\s*["']?(\d{2,5})/im,
|
|
2966
|
+
/-Dserver\.port=(\d{2,5})/i,
|
|
2967
|
+
/systemProperty\s*\(?\s*["']server\.port["']\s*,\s*["']?(\d{2,5})/i,
|
|
2968
|
+
/["']play\.http\.port["']\s*->\s*["']?(\d{2,5})/i
|
|
2969
|
+
];
|
|
2970
|
+
for (const pattern of patterns) {
|
|
2971
|
+
const match = text.match(pattern);
|
|
2972
|
+
const port = parsePort(match?.[1]);
|
|
2973
|
+
if (port) return port;
|
|
2974
|
+
}
|
|
2975
|
+
const yaml = text.match(
|
|
2976
|
+
/(?:^|\n)server:\s*\n(?:[ \t].*\n)*?[ \t]+port:\s*["']?(\d{2,5})/i
|
|
2977
|
+
);
|
|
2978
|
+
return parsePort(yaml?.[1]);
|
|
2979
|
+
}
|
|
2980
|
+
function looksLike(haystack, ...needles) {
|
|
2981
|
+
const lower = haystack.toLowerCase();
|
|
2982
|
+
return needles.some((needle) => lower.includes(needle.toLowerCase()));
|
|
2983
|
+
}
|
|
2984
|
+
function wrapperCommand(folder, unixName, winName, fallback) {
|
|
2985
|
+
if (process.platform === "win32") {
|
|
2986
|
+
return fileExists(folder, winName) ? `.\\${winName}` : fallback;
|
|
2987
|
+
}
|
|
2988
|
+
return fileExists(folder, unixName) ? `./${unixName}` : fallback;
|
|
2989
|
+
}
|
|
2990
|
+
function detectJvmHostApp(folder, opts = {}) {
|
|
2991
|
+
const resolved = path8.resolve(folder);
|
|
2992
|
+
const texts = [];
|
|
2993
|
+
let foundMarker = false;
|
|
2994
|
+
for (const rel of JVM_CONFIG_FILES) {
|
|
2995
|
+
const text = readText(path8.join(resolved, rel));
|
|
2996
|
+
if (!text) continue;
|
|
2997
|
+
foundMarker = true;
|
|
2998
|
+
texts.push(text);
|
|
2999
|
+
}
|
|
3000
|
+
const hasGradle = fileExists(resolved, "build.gradle") || fileExists(resolved, "build.gradle.kts") || fileExists(resolved, "settings.gradle") || fileExists(resolved, "settings.gradle.kts") || fileExists(resolved, "gradlew") || fileExists(resolved, "gradlew.bat");
|
|
3001
|
+
const hasMaven = fileExists(resolved, "pom.xml");
|
|
3002
|
+
const hasSbt = fileExists(resolved, "build.sbt");
|
|
3003
|
+
if (!foundMarker && !hasGradle && !hasMaven && !hasSbt) return null;
|
|
3004
|
+
const blob = texts.join("\n");
|
|
3005
|
+
const { merged } = readProjectEnvLayers(resolved);
|
|
3006
|
+
const envPort = parsePort(merged.SERVER_PORT) || parsePort(merged.PLAY_HTTP_PORT) || parsePort(merged.HTTP_PORT) || parsePort(merged.PORT);
|
|
3007
|
+
let port = envPort || jvmPortFromText(blob);
|
|
3008
|
+
const isPlay = looksLike(
|
|
3009
|
+
blob,
|
|
3010
|
+
"play.http.port",
|
|
3011
|
+
"enablePlugins(Play",
|
|
3012
|
+
"com.typesafe.play",
|
|
3013
|
+
"play-scala",
|
|
3014
|
+
"play-java"
|
|
3015
|
+
);
|
|
3016
|
+
const isSpring = looksLike(
|
|
3017
|
+
blob,
|
|
3018
|
+
"spring-boot",
|
|
3019
|
+
"springframework.boot",
|
|
3020
|
+
"org.springframework"
|
|
3021
|
+
);
|
|
3022
|
+
const isQuarkus = looksLike(blob, "quarkus", "io.quarkus");
|
|
3023
|
+
if (!port) {
|
|
3024
|
+
port = isPlay ? 9e3 : 8080;
|
|
3025
|
+
}
|
|
3026
|
+
let startCommand = null;
|
|
3027
|
+
if (hasGradle) {
|
|
3028
|
+
const gradle = wrapperCommand(resolved, "gradlew", "gradlew.bat", "gradle");
|
|
3029
|
+
startCommand = isQuarkus ? `${gradle} quarkusDev` : isPlay ? `${gradle} run` : `${gradle} bootRun`;
|
|
3030
|
+
} else if (hasMaven) {
|
|
3031
|
+
const mvn = wrapperCommand(resolved, "mvnw", "mvnw.cmd", "mvn");
|
|
3032
|
+
startCommand = isQuarkus ? `${mvn} quarkus:dev` : `${mvn} spring-boot:run`;
|
|
3033
|
+
} else if (hasSbt) {
|
|
3034
|
+
startCommand = "sbt run";
|
|
3035
|
+
}
|
|
3036
|
+
const framework = isPlay ? "Play" : isSpring ? "Spring" : isQuarkus ? "Quarkus" : hasSbt ? "Scala" : "JVM";
|
|
3037
|
+
return {
|
|
3038
|
+
id: "backend",
|
|
3039
|
+
name: opts.appName ? `${opts.appName} API` : `${framework} backend`,
|
|
3040
|
+
role: "backend",
|
|
3041
|
+
port,
|
|
3042
|
+
startCommand,
|
|
3043
|
+
source: envPort ? "env" : "package",
|
|
3044
|
+
folderPath: resolved
|
|
3045
|
+
};
|
|
3046
|
+
}
|
|
3047
|
+
function uniqueWorkspaceDirs(primary, extra) {
|
|
3048
|
+
const out = [];
|
|
3049
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3050
|
+
for (const raw of [primary, ...extra || []]) {
|
|
3051
|
+
if (!raw || typeof raw !== "string") continue;
|
|
3052
|
+
const resolved = path8.resolve(raw.trim());
|
|
3053
|
+
const key = process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
3054
|
+
if (seen.has(key)) continue;
|
|
3055
|
+
seen.add(key);
|
|
3056
|
+
out.push(resolved);
|
|
3057
|
+
}
|
|
3058
|
+
return out;
|
|
3059
|
+
}
|
|
3060
|
+
function folderSlug(folder) {
|
|
3061
|
+
return path8.basename(path8.resolve(folder)).replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 24) || "app";
|
|
3062
|
+
}
|
|
3063
|
+
function fingerprintOneFolder(folder) {
|
|
2912
3064
|
const resolved = path8.resolve(folder);
|
|
2913
3065
|
const parts = [];
|
|
2914
|
-
for (const rel of
|
|
2915
|
-
"package.json",
|
|
2916
|
-
...ENV_FILES,
|
|
2917
|
-
"vite.config.ts",
|
|
2918
|
-
"vite.config.js",
|
|
2919
|
-
"vite.config.mjs",
|
|
2920
|
-
"next.config.js",
|
|
2921
|
-
"next.config.mjs",
|
|
2922
|
-
"next.config.ts"
|
|
2923
|
-
]) {
|
|
3066
|
+
for (const rel of FINGERPRINT_FILES) {
|
|
2924
3067
|
const file = path8.join(resolved, rel);
|
|
2925
3068
|
if (!fs7.existsSync(file)) continue;
|
|
2926
3069
|
try {
|
|
@@ -2929,7 +3072,16 @@ function hostAppsFingerprint(folder) {
|
|
|
2929
3072
|
} catch {
|
|
2930
3073
|
}
|
|
2931
3074
|
}
|
|
2932
|
-
return
|
|
3075
|
+
return parts.join("|") || resolved;
|
|
3076
|
+
}
|
|
3077
|
+
function hostAppsFingerprint(folder) {
|
|
3078
|
+
const resolved = path8.resolve(folder);
|
|
3079
|
+
return createHash2("sha256").update(fingerprintOneFolder(resolved)).digest("hex").slice(0, 32);
|
|
3080
|
+
}
|
|
3081
|
+
function hostAppsFingerprintForFolders(folders) {
|
|
3082
|
+
const dirs = uniqueWorkspaceDirs(folders[0] || "", folders.slice(1));
|
|
3083
|
+
const parts = dirs.map((dir) => `${dir}:${fingerprintOneFolder(dir)}`);
|
|
3084
|
+
return createHash2("sha256").update(parts.join("||") || "empty").digest("hex").slice(0, 32);
|
|
2933
3085
|
}
|
|
2934
3086
|
function pickScript(scripts, names) {
|
|
2935
3087
|
return names.find((name) => scripts[name] && !isChatScript(name, scripts[name])) || null;
|
|
@@ -2943,14 +3095,15 @@ function detectHostAppsFromFiles(folder, opts = {}) {
|
|
|
2943
3095
|
const apps = [];
|
|
2944
3096
|
const envAi = parsePort(merged.AI_SERVER_PORT) || parsePort(merged.AI_SERVER_URL);
|
|
2945
3097
|
const aiPort = envAi || parsePort(opts.preferredAiPort, AI_SERVER_DEFAULT_PORT);
|
|
2946
|
-
apps.push({
|
|
2947
|
-
...defaultAiServerApp(aiPort),
|
|
2948
|
-
source: envAi ? "env" : "default"
|
|
2949
|
-
});
|
|
2950
3098
|
const envPorts = {
|
|
2951
|
-
PORT: layers.filter((layer) => layer.file !== ".env.example").map((layer) => ({
|
|
3099
|
+
PORT: layers.filter((layer) => layer.file !== ".env.example").map((layer) => ({
|
|
3100
|
+
file: layer.file,
|
|
3101
|
+
port: parsePort(layer.values.PORT) || parsePort(layer.values.SERVER_PORT) || parsePort(layer.values.PLAY_HTTP_PORT)
|
|
3102
|
+
})).filter((row) => row.port),
|
|
2952
3103
|
APP_URL: parsePort(merged.APP_URL || merged.NEXT_PUBLIC_APP_URL || merged.PUBLIC_URL),
|
|
2953
|
-
API_PORT: parsePort(
|
|
3104
|
+
API_PORT: parsePort(
|
|
3105
|
+
merged.API_PORT || merged.API_URL || merged.VITE_API_URL || merged.SERVER_PORT || merged.PLAY_HTTP_PORT || merged.HTTP_PORT
|
|
3106
|
+
)
|
|
2954
3107
|
};
|
|
2955
3108
|
const portValues = [...new Set(envPorts.PORT.map((row) => row.port))];
|
|
2956
3109
|
if (portValues.length > 1) {
|
|
@@ -2995,7 +3148,8 @@ function detectHostAppsFromFiles(folder, opts = {}) {
|
|
|
2995
3148
|
port,
|
|
2996
3149
|
startCommand: `npm run ${uiScript}`,
|
|
2997
3150
|
source: uiFromEnv ? "env" : scriptPort ? "package" : "package",
|
|
2998
|
-
host: true
|
|
3151
|
+
host: true,
|
|
3152
|
+
folderPath: resolved
|
|
2999
3153
|
});
|
|
3000
3154
|
} else if (uiFromEnv) {
|
|
3001
3155
|
apps.push({
|
|
@@ -3005,7 +3159,8 @@ function detectHostAppsFromFiles(folder, opts = {}) {
|
|
|
3005
3159
|
port: uiFromEnv,
|
|
3006
3160
|
startCommand: scripts.dev ? "npm run dev" : null,
|
|
3007
3161
|
source: "env",
|
|
3008
|
-
host: true
|
|
3162
|
+
host: true,
|
|
3163
|
+
folderPath: resolved
|
|
3009
3164
|
});
|
|
3010
3165
|
}
|
|
3011
3166
|
if (apiScript && apiScript !== uiScript) {
|
|
@@ -3017,19 +3172,80 @@ function detectHostAppsFromFiles(folder, opts = {}) {
|
|
|
3017
3172
|
role: "backend",
|
|
3018
3173
|
port,
|
|
3019
3174
|
startCommand: `npm run ${apiScript}`,
|
|
3020
|
-
source: envPorts.API_PORT ? "env" : "package"
|
|
3175
|
+
source: envPorts.API_PORT ? "env" : "package",
|
|
3176
|
+
folderPath: resolved
|
|
3021
3177
|
});
|
|
3022
3178
|
}
|
|
3023
|
-
const
|
|
3179
|
+
const hasHostApp = apps.some((app) => app.role !== "ai-server");
|
|
3180
|
+
const usedPorts = new Set(
|
|
3181
|
+
apps.filter((app) => app.role !== "ai-server").map((app) => app.port)
|
|
3182
|
+
);
|
|
3183
|
+
const jvm = detectJvmHostApp(resolved, { appName: opts.appName });
|
|
3184
|
+
if (jvm && !usedPorts.has(jvm.port)) {
|
|
3185
|
+
apps.push({
|
|
3186
|
+
...jvm,
|
|
3187
|
+
id: apps.some((app) => app.id === "backend") ? `backend-${folderSlug(resolved)}` : "backend",
|
|
3188
|
+
host: !hasHostApp
|
|
3189
|
+
});
|
|
3190
|
+
} else if (jvm && usedPorts.has(jvm.port) && !hasHostApp) {
|
|
3191
|
+
reasons.push(
|
|
3192
|
+
`JVM config suggests port ${jvm.port}, which is already used by another detected app`
|
|
3193
|
+
);
|
|
3194
|
+
}
|
|
3195
|
+
const tagged = apps.map(
|
|
3196
|
+
(app) => app.role === "ai-server" || app.folderPath ? app : { ...app, folderPath: resolved }
|
|
3197
|
+
);
|
|
3198
|
+
const hostApps = tagged.filter((app) => app.role !== "ai-server");
|
|
3024
3199
|
if (pkg && Object.keys(scripts).length && hostApps.length === 0) {
|
|
3025
3200
|
reasons.push("Found package.json scripts but no host app port in env or config");
|
|
3026
3201
|
}
|
|
3027
3202
|
return {
|
|
3028
|
-
apps:
|
|
3203
|
+
apps: withoutAiServerApp(tagged),
|
|
3029
3204
|
reasons,
|
|
3030
3205
|
confused: reasons.length > 0
|
|
3031
3206
|
};
|
|
3032
3207
|
}
|
|
3208
|
+
function detectHostAppsFromFolders(folders, opts = {}) {
|
|
3209
|
+
const dirs = uniqueWorkspaceDirs(folders[0] || "", folders.slice(1));
|
|
3210
|
+
const reasons = [];
|
|
3211
|
+
let confused = false;
|
|
3212
|
+
const apps = [];
|
|
3213
|
+
const usedIds = /* @__PURE__ */ new Set();
|
|
3214
|
+
const usedPortSet = /* @__PURE__ */ new Set();
|
|
3215
|
+
const primary = dirs[0];
|
|
3216
|
+
for (const folder of dirs) {
|
|
3217
|
+
const detected = detectHostAppsFromFiles(folder, opts);
|
|
3218
|
+
confused = confused || detected.confused;
|
|
3219
|
+
const base = path8.basename(folder);
|
|
3220
|
+
for (const reason of detected.reasons) {
|
|
3221
|
+
reasons.push(dirs.length > 1 ? `${base}: ${reason}` : reason);
|
|
3222
|
+
}
|
|
3223
|
+
for (const app of detected.apps) {
|
|
3224
|
+
if (isAiServerApp(app)) continue;
|
|
3225
|
+
let id = app.id;
|
|
3226
|
+
if (usedIds.has(id)) id = `${app.id}-${folderSlug(folder)}`;
|
|
3227
|
+
usedIds.add(id);
|
|
3228
|
+
if (usedPortSet.has(app.port)) {
|
|
3229
|
+
reasons.push(
|
|
3230
|
+
`${base}: port ${app.port} is also used by another attached folder`
|
|
3231
|
+
);
|
|
3232
|
+
confused = true;
|
|
3233
|
+
}
|
|
3234
|
+
usedPortSet.add(app.port);
|
|
3235
|
+
apps.push({
|
|
3236
|
+
...app,
|
|
3237
|
+
id,
|
|
3238
|
+
folderPath: path8.resolve(folder),
|
|
3239
|
+
name: dirs.length > 1 && primary && folder !== primary && !app.name.includes(base) ? `${app.name} (${base})` : app.name
|
|
3240
|
+
});
|
|
3241
|
+
}
|
|
3242
|
+
}
|
|
3243
|
+
return {
|
|
3244
|
+
apps: withoutAiServerApp(apps),
|
|
3245
|
+
reasons,
|
|
3246
|
+
confused
|
|
3247
|
+
};
|
|
3248
|
+
}
|
|
3033
3249
|
function readHostAppsCache(folder, extra) {
|
|
3034
3250
|
try {
|
|
3035
3251
|
const raw = JSON.parse(
|
|
@@ -3074,12 +3290,37 @@ function mergeDesiredHostApps(desired, detected) {
|
|
|
3074
3290
|
...local,
|
|
3075
3291
|
...app,
|
|
3076
3292
|
startCommand: app.startCommand || local?.startCommand || null,
|
|
3293
|
+
folderPath: app.folderPath || local?.folderPath || null,
|
|
3077
3294
|
locked: app.locked || app.id === AI_SERVER_APP_ID || app.role === "ai-server",
|
|
3078
3295
|
host: app.role === "ai-server" || app.id === AI_SERVER_APP_ID ? false : app.host === true || app.host !== false && local?.host === true
|
|
3079
3296
|
};
|
|
3080
3297
|
});
|
|
3081
3298
|
return ensureAiServerApp(merged);
|
|
3082
3299
|
}
|
|
3300
|
+
function unionHostApps(desired, detected) {
|
|
3301
|
+
const merged = mergeDesiredHostApps(desired, detected);
|
|
3302
|
+
const seenIds = new Set(merged.map((app) => app.id));
|
|
3303
|
+
const seenPorts = new Set(merged.map((app) => app.port));
|
|
3304
|
+
for (const app of detected) {
|
|
3305
|
+
if (app.role === "ai-server") continue;
|
|
3306
|
+
if (seenIds.has(app.id) || seenPorts.has(app.port)) continue;
|
|
3307
|
+
seenIds.add(app.id);
|
|
3308
|
+
seenPorts.add(app.port);
|
|
3309
|
+
merged.push(app);
|
|
3310
|
+
}
|
|
3311
|
+
return ensureAiServerApp(merged);
|
|
3312
|
+
}
|
|
3313
|
+
function startCommandFromScript(script) {
|
|
3314
|
+
if (!script || !script.trim()) return null;
|
|
3315
|
+
const value = script.trim();
|
|
3316
|
+
if (/\s/.test(value) || /[\\/]/.test(value) || value.includes(":")) {
|
|
3317
|
+
if (/^(npm|pnpm|yarn|npx)\s/i.test(value)) return value;
|
|
3318
|
+
if (/^(sbt|mvn|gradle|gradlew|mvnw|\.\\|\.\/)/i.test(value) || /\s/.test(value)) {
|
|
3319
|
+
return value;
|
|
3320
|
+
}
|
|
3321
|
+
}
|
|
3322
|
+
return `npm run ${value}`;
|
|
3323
|
+
}
|
|
3083
3324
|
function appsFromInspectPorts(ports, scripts, name) {
|
|
3084
3325
|
const apps = [];
|
|
3085
3326
|
if (ports.ui || scripts.ui) {
|
|
@@ -3088,7 +3329,7 @@ function appsFromInspectPorts(ports, scripts, name) {
|
|
|
3088
3329
|
name,
|
|
3089
3330
|
role: "ui",
|
|
3090
3331
|
port: parsePort(ports.ui, 5173),
|
|
3091
|
-
startCommand: scripts.ui
|
|
3332
|
+
startCommand: startCommandFromScript(scripts.ui),
|
|
3092
3333
|
source: "ai"
|
|
3093
3334
|
});
|
|
3094
3335
|
}
|
|
@@ -3097,8 +3338,8 @@ function appsFromInspectPorts(ports, scripts, name) {
|
|
|
3097
3338
|
id: "backend",
|
|
3098
3339
|
name: "Backend",
|
|
3099
3340
|
role: "backend",
|
|
3100
|
-
port: parsePort(ports.backend,
|
|
3101
|
-
startCommand: scripts.backend
|
|
3341
|
+
port: parsePort(ports.backend, 8080),
|
|
3342
|
+
startCommand: startCommandFromScript(scripts.backend),
|
|
3102
3343
|
source: "ai"
|
|
3103
3344
|
});
|
|
3104
3345
|
}
|
|
@@ -3108,7 +3349,7 @@ function appsFromInspectPorts(ports, scripts, name) {
|
|
|
3108
3349
|
name,
|
|
3109
3350
|
role: "app",
|
|
3110
3351
|
port: parsePort(ports.app, 3e3),
|
|
3111
|
-
startCommand: scripts.app
|
|
3352
|
+
startCommand: startCommandFromScript(scripts.app),
|
|
3112
3353
|
source: "ai"
|
|
3113
3354
|
});
|
|
3114
3355
|
}
|
|
@@ -3186,11 +3427,12 @@ function projectSummaryFromDetect(folder, apps, opts = {}) {
|
|
|
3186
3427
|
return `${name}: ${hostApps.map((app) => `${app.name} on ${app.port}${app.startCommand ? ` (${app.startCommand})` : ""}`).join(", ")}.`;
|
|
3187
3428
|
}
|
|
3188
3429
|
async function proposeHostAppsFromConfig(input) {
|
|
3189
|
-
const
|
|
3430
|
+
const dirs = uniqueWorkspaceDirs(input.workspaceDir, input.workspaceDirs);
|
|
3431
|
+
const folder = dirs[0] || path8.resolve(input.workspaceDir);
|
|
3190
3432
|
const preferredAi = parsePort(input.preferredAiPort, AI_SERVER_DEFAULT_PORT);
|
|
3191
|
-
const fingerprint =
|
|
3433
|
+
const fingerprint = hostAppsFingerprintForFolders(dirs);
|
|
3192
3434
|
const allowAi = input.allowAi !== false;
|
|
3193
|
-
const detected =
|
|
3435
|
+
const detected = detectHostAppsFromFolders(dirs, {
|
|
3194
3436
|
preferredAiPort: preferredAi,
|
|
3195
3437
|
appName: input.appName
|
|
3196
3438
|
});
|
|
@@ -3209,8 +3451,9 @@ async function proposeHostAppsFromConfig(input) {
|
|
|
3209
3451
|
appName: input.appName,
|
|
3210
3452
|
extraContext: [
|
|
3211
3453
|
"File-based detection was uncertain or incomplete.",
|
|
3454
|
+
dirs.length > 1 ? `This sandbox also has folders: ${dirs.slice(1).join(", ")}` : "",
|
|
3212
3455
|
...reasons
|
|
3213
|
-
].join("\n")
|
|
3456
|
+
].filter(Boolean).join("\n")
|
|
3214
3457
|
});
|
|
3215
3458
|
usedAi = true;
|
|
3216
3459
|
const fromAi = appsFromInspectPorts(
|
|
@@ -3219,10 +3462,7 @@ async function proposeHostAppsFromConfig(input) {
|
|
|
3219
3462
|
inspected.name || input.appName || "App"
|
|
3220
3463
|
);
|
|
3221
3464
|
if (fromAi.length) {
|
|
3222
|
-
apps =
|
|
3223
|
-
[defaultAiServerApp(preferredAi), ...fromAi],
|
|
3224
|
-
preferredAi
|
|
3225
|
-
);
|
|
3465
|
+
apps = unionHostApps(fromAi, detected.apps);
|
|
3226
3466
|
}
|
|
3227
3467
|
if (inspected.summary) projectSummary = inspected.summary;
|
|
3228
3468
|
if (inspected.issues?.length) {
|
|
@@ -3234,9 +3474,9 @@ async function proposeHostAppsFromConfig(input) {
|
|
|
3234
3474
|
);
|
|
3235
3475
|
}
|
|
3236
3476
|
}
|
|
3237
|
-
const alternatives =
|
|
3238
|
-
appName: input.appName
|
|
3239
|
-
|
|
3477
|
+
const alternatives = dirs.flatMap(
|
|
3478
|
+
(dir) => collectScriptAlternatives(dir, apps, { appName: input.appName })
|
|
3479
|
+
);
|
|
3240
3480
|
const confidence = proposalConfidence(apps, reasons, usedAi);
|
|
3241
3481
|
const needsReview = confidence !== "high" || detected.confused || apps.filter((app) => app.role !== "ai-server").length === 0;
|
|
3242
3482
|
return {
|
|
@@ -3251,11 +3491,12 @@ async function proposeHostAppsFromConfig(input) {
|
|
|
3251
3491
|
};
|
|
3252
3492
|
}
|
|
3253
3493
|
async function resolveHostApps(input) {
|
|
3254
|
-
const
|
|
3494
|
+
const dirs = uniqueWorkspaceDirs(input.workspaceDir, input.workspaceDirs);
|
|
3495
|
+
const folder = dirs[0] || path8.resolve(input.workspaceDir);
|
|
3255
3496
|
const loc = { sandboxId: input.sandboxId, dataDir: input.dataDir };
|
|
3256
|
-
const fingerprint =
|
|
3497
|
+
const fingerprint = hostAppsFingerprintForFolders(dirs);
|
|
3257
3498
|
const preferredAi = parsePort(input.preferredAiPort, AI_SERVER_DEFAULT_PORT);
|
|
3258
|
-
const detected =
|
|
3499
|
+
const detected = detectHostAppsFromFolders(dirs, {
|
|
3259
3500
|
preferredAiPort: preferredAi,
|
|
3260
3501
|
appName: input.appName
|
|
3261
3502
|
});
|
|
@@ -3305,10 +3546,7 @@ Return the real local ports and npm script names.`
|
|
|
3305
3546
|
inspected.scripts,
|
|
3306
3547
|
inspected.name || input.appName || "App"
|
|
3307
3548
|
);
|
|
3308
|
-
const apps2 =
|
|
3309
|
-
[defaultAiServerApp(preferredAi), ...fromAi],
|
|
3310
|
-
preferredAi
|
|
3311
|
-
);
|
|
3549
|
+
const apps2 = withoutAiServerApp(fromAi);
|
|
3312
3550
|
writeHostAppsCache(
|
|
3313
3551
|
folder,
|
|
3314
3552
|
apps2,
|
|
@@ -3390,6 +3628,7 @@ export {
|
|
|
3390
3628
|
createToolValidator,
|
|
3391
3629
|
defaultAiServerApp,
|
|
3392
3630
|
detectHostAppsFromFiles,
|
|
3631
|
+
detectHostAppsFromFolders,
|
|
3393
3632
|
ensureAiServerApp,
|
|
3394
3633
|
ensureProjectDataDir,
|
|
3395
3634
|
ensureSingleHost,
|
|
@@ -3399,8 +3638,10 @@ export {
|
|
|
3399
3638
|
getProviderPreference,
|
|
3400
3639
|
hostAppsCachePath2 as hostAppsCachePath,
|
|
3401
3640
|
hostAppsFingerprint,
|
|
3641
|
+
hostAppsFingerprintForFolders,
|
|
3402
3642
|
inspectAndRepairWorkspace,
|
|
3403
3643
|
inspectConfigOnly,
|
|
3644
|
+
isAiServerApp,
|
|
3404
3645
|
isDevMode,
|
|
3405
3646
|
isIgnoredRelative,
|
|
3406
3647
|
isInsideWorkspace,
|
|
@@ -3433,7 +3674,9 @@ export {
|
|
|
3433
3674
|
sanitizeProjectId,
|
|
3434
3675
|
saveChatAttachments,
|
|
3435
3676
|
toNextRoute,
|
|
3677
|
+
unionHostApps,
|
|
3436
3678
|
upsertManagedIgnoreFile,
|
|
3679
|
+
withoutAiServerApp,
|
|
3437
3680
|
writeCollaboraterApps,
|
|
3438
3681
|
writeHostAppsCache
|
|
3439
3682
|
};
|