@maintainer-pro/ai-cli 0.1.9 → 0.1.11
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 +310 -46
- package/dist/index.d.cts +18 -1
- package/dist/index.d.ts +18 -1
- package/dist/index.js +307 -46
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -62,6 +62,7 @@ __export(index_exports, {
|
|
|
62
62
|
createToolValidator: () => createToolValidator,
|
|
63
63
|
defaultAiServerApp: () => defaultAiServerApp,
|
|
64
64
|
detectHostAppsFromFiles: () => detectHostAppsFromFiles,
|
|
65
|
+
detectHostAppsFromFolders: () => detectHostAppsFromFolders,
|
|
65
66
|
ensureAiServerApp: () => ensureAiServerApp,
|
|
66
67
|
ensureProjectDataDir: () => ensureProjectDataDir,
|
|
67
68
|
ensureSingleHost: () => ensureSingleHost,
|
|
@@ -71,6 +72,7 @@ __export(index_exports, {
|
|
|
71
72
|
getProviderPreference: () => getProviderPreference,
|
|
72
73
|
hostAppsCachePath: () => hostAppsCachePath2,
|
|
73
74
|
hostAppsFingerprint: () => hostAppsFingerprint,
|
|
75
|
+
hostAppsFingerprintForFolders: () => hostAppsFingerprintForFolders,
|
|
74
76
|
inspectAndRepairWorkspace: () => inspectAndRepairWorkspace,
|
|
75
77
|
inspectConfigOnly: () => inspectConfigOnly,
|
|
76
78
|
isDevMode: () => isDevMode,
|
|
@@ -105,6 +107,7 @@ __export(index_exports, {
|
|
|
105
107
|
sanitizeProjectId: () => sanitizeProjectId,
|
|
106
108
|
saveChatAttachments: () => saveChatAttachments,
|
|
107
109
|
toNextRoute: () => toNextRoute,
|
|
110
|
+
unionHostApps: () => unionHostApps,
|
|
108
111
|
upsertManagedIgnoreFile: () => upsertManagedIgnoreFile,
|
|
109
112
|
writeCollaboraterApps: () => writeCollaboraterApps,
|
|
110
113
|
writeHostAppsCache: () => writeHostAppsCache
|
|
@@ -2601,9 +2604,9 @@ var inspectJsonSchema = import_zod.z.object({
|
|
|
2601
2604
|
name: import_zod.z.string().max(200).optional(),
|
|
2602
2605
|
summary: import_zod.z.string().max(800).optional(),
|
|
2603
2606
|
scripts: import_zod.z.object({
|
|
2604
|
-
ui: import_zod.z.string().min(1).max(
|
|
2605
|
-
backend: import_zod.z.string().min(1).max(
|
|
2606
|
-
app: import_zod.z.string().min(1).max(
|
|
2607
|
+
ui: import_zod.z.string().min(1).max(200).optional(),
|
|
2608
|
+
backend: import_zod.z.string().min(1).max(200).optional(),
|
|
2609
|
+
app: import_zod.z.string().min(1).max(200).optional()
|
|
2607
2610
|
}).optional(),
|
|
2608
2611
|
ports: import_zod.z.object({
|
|
2609
2612
|
ui: import_zod.z.number().int().min(1).max(65535).optional(),
|
|
@@ -2631,12 +2634,12 @@ function setupSystemPrompt(input) {
|
|
|
2631
2634
|
Workspace: ${input.workspaceDir}
|
|
2632
2635
|
App name: ${input.appName || "the app"}
|
|
2633
2636
|
|
|
2634
|
-
Inspect this repository (package.json, README, lockfiles, existing env files, how the UI and API start).
|
|
2637
|
+
Inspect this repository (package.json, README, lockfiles, Maven/Gradle/sbt files, Spring/Play config, existing env files, how the UI and API start).
|
|
2635
2638
|
You MAY edit files to fix setup problems that would block local run or the chat widget (env keys, widget mount, missing config).
|
|
2636
2639
|
Do NOT start long-lived dev servers, tunnels, or install global tools.
|
|
2637
2640
|
Do NOT run \`npm run dev\` or similar.
|
|
2638
2641
|
|
|
2639
|
-
Script names you report MUST already exist in package.json. Omit chat/ai-server scripts.
|
|
2642
|
+
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.
|
|
2640
2643
|
|
|
2641
2644
|
End with a short human summary AND one \`\`\`json fence with:
|
|
2642
2645
|
|
|
@@ -2708,7 +2711,21 @@ ${lock}`);
|
|
|
2708
2711
|
"vite.config.mjs",
|
|
2709
2712
|
"next.config.js",
|
|
2710
2713
|
"next.config.mjs",
|
|
2711
|
-
"next.config.ts"
|
|
2714
|
+
"next.config.ts",
|
|
2715
|
+
"pom.xml",
|
|
2716
|
+
"build.gradle",
|
|
2717
|
+
"build.gradle.kts",
|
|
2718
|
+
"build.sbt",
|
|
2719
|
+
"application.yml",
|
|
2720
|
+
"application.yaml",
|
|
2721
|
+
"application.properties",
|
|
2722
|
+
"application.conf",
|
|
2723
|
+
"src/main/resources/application.yml",
|
|
2724
|
+
"src/main/resources/application.yaml",
|
|
2725
|
+
"src/main/resources/application.properties",
|
|
2726
|
+
"src/main/resources/application.conf",
|
|
2727
|
+
"src/main/resources/application-dev.yml",
|
|
2728
|
+
"conf/application.conf"
|
|
2712
2729
|
]) {
|
|
2713
2730
|
const body = readSnippet(import_node_path7.default.join(folder, rel), 2500);
|
|
2714
2731
|
if (body) parts.push(`### ${rel}
|
|
@@ -2736,7 +2753,7 @@ DO NOT edit, create, or delete any files.
|
|
|
2736
2753
|
DO NOT run shell commands, install packages, or start servers.
|
|
2737
2754
|
Only infer how this project runs locally from the excerpts.
|
|
2738
2755
|
|
|
2739
|
-
Script names you report MUST appear in package.json scripts
|
|
2756
|
+
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.
|
|
2740
2757
|
|
|
2741
2758
|
Reply with a short human summary AND one \`\`\`json fence with:
|
|
2742
2759
|
|
|
@@ -2744,8 +2761,8 @@ Reply with a short human summary AND one \`\`\`json fence with:
|
|
|
2744
2761
|
"kind": "next" | "vite" | "html" | "empty" | "other",
|
|
2745
2762
|
"name": "package or folder name",
|
|
2746
2763
|
"summary": "one sentence about this project",
|
|
2747
|
-
"scripts": { "ui": "dev:client", "backend": "
|
|
2748
|
-
"ports": { "ui": 5173, "backend":
|
|
2764
|
+
"scripts": { "ui": "dev:client", "backend": "sbt run" },
|
|
2765
|
+
"ports": { "ui": 5173, "backend": 9000 },
|
|
2749
2766
|
"issues": ["uncertainties"],
|
|
2750
2767
|
"fixes": [],
|
|
2751
2768
|
"ready": true
|
|
@@ -2908,7 +2925,8 @@ function normalizeHostApp(raw) {
|
|
|
2908
2925
|
row.source
|
|
2909
2926
|
) ? row.source : "manual",
|
|
2910
2927
|
locked: row.locked === true || id === AI_SERVER_APP_ID || role === "ai-server",
|
|
2911
|
-
host: role !== "ai-server" && id !== AI_SERVER_APP_ID && row.host === true
|
|
2928
|
+
host: role !== "ai-server" && id !== AI_SERVER_APP_ID && row.host === true,
|
|
2929
|
+
folderPath: typeof row.folderPath === "string" && row.folderPath.trim() ? row.folderPath.trim().slice(0, 500) : null
|
|
2912
2930
|
};
|
|
2913
2931
|
}
|
|
2914
2932
|
function ensureSingleHost(apps) {
|
|
@@ -3021,19 +3039,151 @@ function configPort(folder, files) {
|
|
|
3021
3039
|
}
|
|
3022
3040
|
return 0;
|
|
3023
3041
|
}
|
|
3024
|
-
|
|
3042
|
+
var JVM_CONFIG_FILES = [
|
|
3043
|
+
"pom.xml",
|
|
3044
|
+
"build.gradle",
|
|
3045
|
+
"build.gradle.kts",
|
|
3046
|
+
"settings.gradle",
|
|
3047
|
+
"settings.gradle.kts",
|
|
3048
|
+
"build.sbt",
|
|
3049
|
+
"gradle.properties",
|
|
3050
|
+
"application.yml",
|
|
3051
|
+
"application.yaml",
|
|
3052
|
+
"application.properties",
|
|
3053
|
+
"application.conf",
|
|
3054
|
+
"src/main/resources/application.yml",
|
|
3055
|
+
"src/main/resources/application.yaml",
|
|
3056
|
+
"src/main/resources/application.properties",
|
|
3057
|
+
"src/main/resources/application.conf",
|
|
3058
|
+
"src/main/resources/application-dev.yml",
|
|
3059
|
+
"src/main/resources/application-dev.yaml",
|
|
3060
|
+
"src/main/resources/application-dev.properties",
|
|
3061
|
+
"src/main/resources/application-local.yml",
|
|
3062
|
+
"conf/application.conf",
|
|
3063
|
+
"conf/application.yml"
|
|
3064
|
+
];
|
|
3065
|
+
var FINGERPRINT_FILES = [
|
|
3066
|
+
"package.json",
|
|
3067
|
+
...ENV_FILES,
|
|
3068
|
+
"vite.config.ts",
|
|
3069
|
+
"vite.config.js",
|
|
3070
|
+
"vite.config.mjs",
|
|
3071
|
+
"next.config.js",
|
|
3072
|
+
"next.config.mjs",
|
|
3073
|
+
"next.config.ts",
|
|
3074
|
+
...JVM_CONFIG_FILES
|
|
3075
|
+
];
|
|
3076
|
+
function fileExists(folder, rel) {
|
|
3077
|
+
return import_node_fs4.default.existsSync(import_node_path8.default.join(folder, rel));
|
|
3078
|
+
}
|
|
3079
|
+
function jvmPortFromText(text) {
|
|
3080
|
+
const patterns = [
|
|
3081
|
+
/(?:^|\n)\s*server\.port\s*[:=]\s*["']?(\d{2,5})/im,
|
|
3082
|
+
/(?:^|\n)\s*SERVER_PORT\s*[:=]\s*["']?(\d{2,5})/im,
|
|
3083
|
+
/(?:^|\n)\s*play\.http\.port\s*[:=]\s*["']?(\d{2,5})/im,
|
|
3084
|
+
/(?:^|\n)\s*play\.server\.http\.port\s*[:=]\s*["']?(\d{2,5})/im,
|
|
3085
|
+
/(?:^|\n)\s*http\.port\s*[:=]\s*["']?(\d{2,5})/im,
|
|
3086
|
+
/-Dserver\.port=(\d{2,5})/i,
|
|
3087
|
+
/systemProperty\s*\(?\s*["']server\.port["']\s*,\s*["']?(\d{2,5})/i,
|
|
3088
|
+
/["']play\.http\.port["']\s*->\s*["']?(\d{2,5})/i
|
|
3089
|
+
];
|
|
3090
|
+
for (const pattern of patterns) {
|
|
3091
|
+
const match = text.match(pattern);
|
|
3092
|
+
const port = parsePort(match?.[1]);
|
|
3093
|
+
if (port) return port;
|
|
3094
|
+
}
|
|
3095
|
+
const yaml = text.match(
|
|
3096
|
+
/(?:^|\n)server:\s*\n(?:[ \t].*\n)*?[ \t]+port:\s*["']?(\d{2,5})/i
|
|
3097
|
+
);
|
|
3098
|
+
return parsePort(yaml?.[1]);
|
|
3099
|
+
}
|
|
3100
|
+
function looksLike(haystack, ...needles) {
|
|
3101
|
+
const lower = haystack.toLowerCase();
|
|
3102
|
+
return needles.some((needle) => lower.includes(needle.toLowerCase()));
|
|
3103
|
+
}
|
|
3104
|
+
function wrapperCommand(folder, unixName, winName, fallback) {
|
|
3105
|
+
if (process.platform === "win32") {
|
|
3106
|
+
return fileExists(folder, winName) ? `.\\${winName}` : fallback;
|
|
3107
|
+
}
|
|
3108
|
+
return fileExists(folder, unixName) ? `./${unixName}` : fallback;
|
|
3109
|
+
}
|
|
3110
|
+
function detectJvmHostApp(folder, opts = {}) {
|
|
3111
|
+
const resolved = import_node_path8.default.resolve(folder);
|
|
3112
|
+
const texts = [];
|
|
3113
|
+
let foundMarker = false;
|
|
3114
|
+
for (const rel of JVM_CONFIG_FILES) {
|
|
3115
|
+
const text = readText(import_node_path8.default.join(resolved, rel));
|
|
3116
|
+
if (!text) continue;
|
|
3117
|
+
foundMarker = true;
|
|
3118
|
+
texts.push(text);
|
|
3119
|
+
}
|
|
3120
|
+
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");
|
|
3121
|
+
const hasMaven = fileExists(resolved, "pom.xml");
|
|
3122
|
+
const hasSbt = fileExists(resolved, "build.sbt");
|
|
3123
|
+
if (!foundMarker && !hasGradle && !hasMaven && !hasSbt) return null;
|
|
3124
|
+
const blob = texts.join("\n");
|
|
3125
|
+
const { merged } = readProjectEnvLayers(resolved);
|
|
3126
|
+
const envPort = parsePort(merged.SERVER_PORT) || parsePort(merged.PLAY_HTTP_PORT) || parsePort(merged.HTTP_PORT) || parsePort(merged.PORT);
|
|
3127
|
+
let port = envPort || jvmPortFromText(blob);
|
|
3128
|
+
const isPlay = looksLike(
|
|
3129
|
+
blob,
|
|
3130
|
+
"play.http.port",
|
|
3131
|
+
"enablePlugins(Play",
|
|
3132
|
+
"com.typesafe.play",
|
|
3133
|
+
"play-scala",
|
|
3134
|
+
"play-java"
|
|
3135
|
+
);
|
|
3136
|
+
const isSpring = looksLike(
|
|
3137
|
+
blob,
|
|
3138
|
+
"spring-boot",
|
|
3139
|
+
"springframework.boot",
|
|
3140
|
+
"org.springframework"
|
|
3141
|
+
);
|
|
3142
|
+
const isQuarkus = looksLike(blob, "quarkus", "io.quarkus");
|
|
3143
|
+
if (!port) {
|
|
3144
|
+
port = isPlay ? 9e3 : 8080;
|
|
3145
|
+
}
|
|
3146
|
+
let startCommand = null;
|
|
3147
|
+
if (hasGradle) {
|
|
3148
|
+
const gradle = wrapperCommand(resolved, "gradlew", "gradlew.bat", "gradle");
|
|
3149
|
+
startCommand = isQuarkus ? `${gradle} quarkusDev` : isPlay ? `${gradle} run` : `${gradle} bootRun`;
|
|
3150
|
+
} else if (hasMaven) {
|
|
3151
|
+
const mvn = wrapperCommand(resolved, "mvnw", "mvnw.cmd", "mvn");
|
|
3152
|
+
startCommand = isQuarkus ? `${mvn} quarkus:dev` : `${mvn} spring-boot:run`;
|
|
3153
|
+
} else if (hasSbt) {
|
|
3154
|
+
startCommand = "sbt run";
|
|
3155
|
+
}
|
|
3156
|
+
const framework = isPlay ? "Play" : isSpring ? "Spring" : isQuarkus ? "Quarkus" : hasSbt ? "Scala" : "JVM";
|
|
3157
|
+
return {
|
|
3158
|
+
id: "backend",
|
|
3159
|
+
name: opts.appName ? `${opts.appName} API` : `${framework} backend`,
|
|
3160
|
+
role: "backend",
|
|
3161
|
+
port,
|
|
3162
|
+
startCommand,
|
|
3163
|
+
source: envPort ? "env" : "package",
|
|
3164
|
+
folderPath: resolved
|
|
3165
|
+
};
|
|
3166
|
+
}
|
|
3167
|
+
function uniqueWorkspaceDirs(primary, extra) {
|
|
3168
|
+
const out = [];
|
|
3169
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3170
|
+
for (const raw of [primary, ...extra || []]) {
|
|
3171
|
+
if (!raw || typeof raw !== "string") continue;
|
|
3172
|
+
const resolved = import_node_path8.default.resolve(raw.trim());
|
|
3173
|
+
const key = process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
3174
|
+
if (seen.has(key)) continue;
|
|
3175
|
+
seen.add(key);
|
|
3176
|
+
out.push(resolved);
|
|
3177
|
+
}
|
|
3178
|
+
return out;
|
|
3179
|
+
}
|
|
3180
|
+
function folderSlug(folder) {
|
|
3181
|
+
return import_node_path8.default.basename(import_node_path8.default.resolve(folder)).replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 24) || "app";
|
|
3182
|
+
}
|
|
3183
|
+
function fingerprintOneFolder(folder) {
|
|
3025
3184
|
const resolved = import_node_path8.default.resolve(folder);
|
|
3026
3185
|
const parts = [];
|
|
3027
|
-
for (const rel of
|
|
3028
|
-
"package.json",
|
|
3029
|
-
...ENV_FILES,
|
|
3030
|
-
"vite.config.ts",
|
|
3031
|
-
"vite.config.js",
|
|
3032
|
-
"vite.config.mjs",
|
|
3033
|
-
"next.config.js",
|
|
3034
|
-
"next.config.mjs",
|
|
3035
|
-
"next.config.ts"
|
|
3036
|
-
]) {
|
|
3186
|
+
for (const rel of FINGERPRINT_FILES) {
|
|
3037
3187
|
const file = import_node_path8.default.join(resolved, rel);
|
|
3038
3188
|
if (!import_node_fs4.default.existsSync(file)) continue;
|
|
3039
3189
|
try {
|
|
@@ -3042,7 +3192,16 @@ function hostAppsFingerprint(folder) {
|
|
|
3042
3192
|
} catch {
|
|
3043
3193
|
}
|
|
3044
3194
|
}
|
|
3045
|
-
return
|
|
3195
|
+
return parts.join("|") || resolved;
|
|
3196
|
+
}
|
|
3197
|
+
function hostAppsFingerprint(folder) {
|
|
3198
|
+
const resolved = import_node_path8.default.resolve(folder);
|
|
3199
|
+
return (0, import_node_crypto4.createHash)("sha256").update(fingerprintOneFolder(resolved)).digest("hex").slice(0, 32);
|
|
3200
|
+
}
|
|
3201
|
+
function hostAppsFingerprintForFolders(folders) {
|
|
3202
|
+
const dirs = uniqueWorkspaceDirs(folders[0] || "", folders.slice(1));
|
|
3203
|
+
const parts = dirs.map((dir) => `${dir}:${fingerprintOneFolder(dir)}`);
|
|
3204
|
+
return (0, import_node_crypto4.createHash)("sha256").update(parts.join("||") || "empty").digest("hex").slice(0, 32);
|
|
3046
3205
|
}
|
|
3047
3206
|
function pickScript(scripts, names) {
|
|
3048
3207
|
return names.find((name) => scripts[name] && !isChatScript(name, scripts[name])) || null;
|
|
@@ -3061,9 +3220,14 @@ function detectHostAppsFromFiles(folder, opts = {}) {
|
|
|
3061
3220
|
source: envAi ? "env" : "default"
|
|
3062
3221
|
});
|
|
3063
3222
|
const envPorts = {
|
|
3064
|
-
PORT: layers.filter((layer) => layer.file !== ".env.example").map((layer) => ({
|
|
3223
|
+
PORT: layers.filter((layer) => layer.file !== ".env.example").map((layer) => ({
|
|
3224
|
+
file: layer.file,
|
|
3225
|
+
port: parsePort(layer.values.PORT) || parsePort(layer.values.SERVER_PORT) || parsePort(layer.values.PLAY_HTTP_PORT)
|
|
3226
|
+
})).filter((row) => row.port),
|
|
3065
3227
|
APP_URL: parsePort(merged.APP_URL || merged.NEXT_PUBLIC_APP_URL || merged.PUBLIC_URL),
|
|
3066
|
-
API_PORT: parsePort(
|
|
3228
|
+
API_PORT: parsePort(
|
|
3229
|
+
merged.API_PORT || merged.API_URL || merged.VITE_API_URL || merged.SERVER_PORT || merged.PLAY_HTTP_PORT || merged.HTTP_PORT
|
|
3230
|
+
)
|
|
3067
3231
|
};
|
|
3068
3232
|
const portValues = [...new Set(envPorts.PORT.map((row) => row.port))];
|
|
3069
3233
|
if (portValues.length > 1) {
|
|
@@ -3108,7 +3272,8 @@ function detectHostAppsFromFiles(folder, opts = {}) {
|
|
|
3108
3272
|
port,
|
|
3109
3273
|
startCommand: `npm run ${uiScript}`,
|
|
3110
3274
|
source: uiFromEnv ? "env" : scriptPort ? "package" : "package",
|
|
3111
|
-
host: true
|
|
3275
|
+
host: true,
|
|
3276
|
+
folderPath: resolved
|
|
3112
3277
|
});
|
|
3113
3278
|
} else if (uiFromEnv) {
|
|
3114
3279
|
apps.push({
|
|
@@ -3118,7 +3283,8 @@ function detectHostAppsFromFiles(folder, opts = {}) {
|
|
|
3118
3283
|
port: uiFromEnv,
|
|
3119
3284
|
startCommand: scripts.dev ? "npm run dev" : null,
|
|
3120
3285
|
source: "env",
|
|
3121
|
-
host: true
|
|
3286
|
+
host: true,
|
|
3287
|
+
folderPath: resolved
|
|
3122
3288
|
});
|
|
3123
3289
|
}
|
|
3124
3290
|
if (apiScript && apiScript !== uiScript) {
|
|
@@ -3130,19 +3296,83 @@ function detectHostAppsFromFiles(folder, opts = {}) {
|
|
|
3130
3296
|
role: "backend",
|
|
3131
3297
|
port,
|
|
3132
3298
|
startCommand: `npm run ${apiScript}`,
|
|
3133
|
-
source: envPorts.API_PORT ? "env" : "package"
|
|
3299
|
+
source: envPorts.API_PORT ? "env" : "package",
|
|
3300
|
+
folderPath: resolved
|
|
3134
3301
|
});
|
|
3135
3302
|
}
|
|
3136
|
-
const
|
|
3303
|
+
const hasHostApp = apps.some((app) => app.role !== "ai-server");
|
|
3304
|
+
const usedPorts = new Set(
|
|
3305
|
+
apps.filter((app) => app.role !== "ai-server").map((app) => app.port)
|
|
3306
|
+
);
|
|
3307
|
+
const jvm = detectJvmHostApp(resolved, { appName: opts.appName });
|
|
3308
|
+
if (jvm && !usedPorts.has(jvm.port)) {
|
|
3309
|
+
apps.push({
|
|
3310
|
+
...jvm,
|
|
3311
|
+
id: apps.some((app) => app.id === "backend") ? `backend-${folderSlug(resolved)}` : "backend",
|
|
3312
|
+
host: !hasHostApp
|
|
3313
|
+
});
|
|
3314
|
+
} else if (jvm && usedPorts.has(jvm.port) && !hasHostApp) {
|
|
3315
|
+
reasons.push(
|
|
3316
|
+
`JVM config suggests port ${jvm.port}, which is already used by another detected app`
|
|
3317
|
+
);
|
|
3318
|
+
}
|
|
3319
|
+
const tagged = apps.map(
|
|
3320
|
+
(app) => app.role === "ai-server" || app.folderPath ? app : { ...app, folderPath: resolved }
|
|
3321
|
+
);
|
|
3322
|
+
const hostApps = tagged.filter((app) => app.role !== "ai-server");
|
|
3137
3323
|
if (pkg && Object.keys(scripts).length && hostApps.length === 0) {
|
|
3138
3324
|
reasons.push("Found package.json scripts but no host app port in env or config");
|
|
3139
3325
|
}
|
|
3140
3326
|
return {
|
|
3141
|
-
apps: ensureAiServerApp(
|
|
3327
|
+
apps: ensureAiServerApp(tagged, aiPort),
|
|
3142
3328
|
reasons,
|
|
3143
3329
|
confused: reasons.length > 0
|
|
3144
3330
|
};
|
|
3145
3331
|
}
|
|
3332
|
+
function detectHostAppsFromFolders(folders, opts = {}) {
|
|
3333
|
+
const dirs = uniqueWorkspaceDirs(folders[0] || "", folders.slice(1));
|
|
3334
|
+
const reasons = [];
|
|
3335
|
+
let confused = false;
|
|
3336
|
+
const apps = [];
|
|
3337
|
+
const usedIds = /* @__PURE__ */ new Set();
|
|
3338
|
+
const usedPortSet = /* @__PURE__ */ new Set();
|
|
3339
|
+
const primary = dirs[0];
|
|
3340
|
+
for (const folder of dirs) {
|
|
3341
|
+
const detected = detectHostAppsFromFiles(folder, opts);
|
|
3342
|
+
confused = confused || detected.confused;
|
|
3343
|
+
const base = import_node_path8.default.basename(folder);
|
|
3344
|
+
for (const reason of detected.reasons) {
|
|
3345
|
+
reasons.push(dirs.length > 1 ? `${base}: ${reason}` : reason);
|
|
3346
|
+
}
|
|
3347
|
+
for (const app of detected.apps) {
|
|
3348
|
+
if (app.role === "ai-server") {
|
|
3349
|
+
if (!apps.some((row) => row.role === "ai-server")) apps.push(app);
|
|
3350
|
+
continue;
|
|
3351
|
+
}
|
|
3352
|
+
let id = app.id;
|
|
3353
|
+
if (usedIds.has(id)) id = `${app.id}-${folderSlug(folder)}`;
|
|
3354
|
+
usedIds.add(id);
|
|
3355
|
+
if (usedPortSet.has(app.port)) {
|
|
3356
|
+
reasons.push(
|
|
3357
|
+
`${base}: port ${app.port} is also used by another attached folder`
|
|
3358
|
+
);
|
|
3359
|
+
confused = true;
|
|
3360
|
+
}
|
|
3361
|
+
usedPortSet.add(app.port);
|
|
3362
|
+
apps.push({
|
|
3363
|
+
...app,
|
|
3364
|
+
id,
|
|
3365
|
+
folderPath: import_node_path8.default.resolve(folder),
|
|
3366
|
+
name: dirs.length > 1 && primary && folder !== primary && !app.name.includes(base) ? `${app.name} (${base})` : app.name
|
|
3367
|
+
});
|
|
3368
|
+
}
|
|
3369
|
+
}
|
|
3370
|
+
return {
|
|
3371
|
+
apps: ensureAiServerApp(apps, opts.preferredAiPort),
|
|
3372
|
+
reasons,
|
|
3373
|
+
confused
|
|
3374
|
+
};
|
|
3375
|
+
}
|
|
3146
3376
|
function readHostAppsCache(folder, extra) {
|
|
3147
3377
|
try {
|
|
3148
3378
|
const raw = JSON.parse(
|
|
@@ -3187,12 +3417,37 @@ function mergeDesiredHostApps(desired, detected) {
|
|
|
3187
3417
|
...local,
|
|
3188
3418
|
...app,
|
|
3189
3419
|
startCommand: app.startCommand || local?.startCommand || null,
|
|
3420
|
+
folderPath: app.folderPath || local?.folderPath || null,
|
|
3190
3421
|
locked: app.locked || app.id === AI_SERVER_APP_ID || app.role === "ai-server",
|
|
3191
3422
|
host: app.role === "ai-server" || app.id === AI_SERVER_APP_ID ? false : app.host === true || app.host !== false && local?.host === true
|
|
3192
3423
|
};
|
|
3193
3424
|
});
|
|
3194
3425
|
return ensureAiServerApp(merged);
|
|
3195
3426
|
}
|
|
3427
|
+
function unionHostApps(desired, detected) {
|
|
3428
|
+
const merged = mergeDesiredHostApps(desired, detected);
|
|
3429
|
+
const seenIds = new Set(merged.map((app) => app.id));
|
|
3430
|
+
const seenPorts = new Set(merged.map((app) => app.port));
|
|
3431
|
+
for (const app of detected) {
|
|
3432
|
+
if (app.role === "ai-server") continue;
|
|
3433
|
+
if (seenIds.has(app.id) || seenPorts.has(app.port)) continue;
|
|
3434
|
+
seenIds.add(app.id);
|
|
3435
|
+
seenPorts.add(app.port);
|
|
3436
|
+
merged.push(app);
|
|
3437
|
+
}
|
|
3438
|
+
return ensureAiServerApp(merged);
|
|
3439
|
+
}
|
|
3440
|
+
function startCommandFromScript(script) {
|
|
3441
|
+
if (!script || !script.trim()) return null;
|
|
3442
|
+
const value = script.trim();
|
|
3443
|
+
if (/\s/.test(value) || /[\\/]/.test(value) || value.includes(":")) {
|
|
3444
|
+
if (/^(npm|pnpm|yarn|npx)\s/i.test(value)) return value;
|
|
3445
|
+
if (/^(sbt|mvn|gradle|gradlew|mvnw|\.\\|\.\/)/i.test(value) || /\s/.test(value)) {
|
|
3446
|
+
return value;
|
|
3447
|
+
}
|
|
3448
|
+
}
|
|
3449
|
+
return `npm run ${value}`;
|
|
3450
|
+
}
|
|
3196
3451
|
function appsFromInspectPorts(ports, scripts, name) {
|
|
3197
3452
|
const apps = [];
|
|
3198
3453
|
if (ports.ui || scripts.ui) {
|
|
@@ -3201,7 +3456,7 @@ function appsFromInspectPorts(ports, scripts, name) {
|
|
|
3201
3456
|
name,
|
|
3202
3457
|
role: "ui",
|
|
3203
3458
|
port: parsePort(ports.ui, 5173),
|
|
3204
|
-
startCommand: scripts.ui
|
|
3459
|
+
startCommand: startCommandFromScript(scripts.ui),
|
|
3205
3460
|
source: "ai"
|
|
3206
3461
|
});
|
|
3207
3462
|
}
|
|
@@ -3210,8 +3465,8 @@ function appsFromInspectPorts(ports, scripts, name) {
|
|
|
3210
3465
|
id: "backend",
|
|
3211
3466
|
name: "Backend",
|
|
3212
3467
|
role: "backend",
|
|
3213
|
-
port: parsePort(ports.backend,
|
|
3214
|
-
startCommand: scripts.backend
|
|
3468
|
+
port: parsePort(ports.backend, 8080),
|
|
3469
|
+
startCommand: startCommandFromScript(scripts.backend),
|
|
3215
3470
|
source: "ai"
|
|
3216
3471
|
});
|
|
3217
3472
|
}
|
|
@@ -3221,7 +3476,7 @@ function appsFromInspectPorts(ports, scripts, name) {
|
|
|
3221
3476
|
name,
|
|
3222
3477
|
role: "app",
|
|
3223
3478
|
port: parsePort(ports.app, 3e3),
|
|
3224
|
-
startCommand: scripts.app
|
|
3479
|
+
startCommand: startCommandFromScript(scripts.app),
|
|
3225
3480
|
source: "ai"
|
|
3226
3481
|
});
|
|
3227
3482
|
}
|
|
@@ -3299,11 +3554,12 @@ function projectSummaryFromDetect(folder, apps, opts = {}) {
|
|
|
3299
3554
|
return `${name}: ${hostApps.map((app) => `${app.name} on ${app.port}${app.startCommand ? ` (${app.startCommand})` : ""}`).join(", ")}.`;
|
|
3300
3555
|
}
|
|
3301
3556
|
async function proposeHostAppsFromConfig(input) {
|
|
3302
|
-
const
|
|
3557
|
+
const dirs = uniqueWorkspaceDirs(input.workspaceDir, input.workspaceDirs);
|
|
3558
|
+
const folder = dirs[0] || import_node_path8.default.resolve(input.workspaceDir);
|
|
3303
3559
|
const preferredAi = parsePort(input.preferredAiPort, AI_SERVER_DEFAULT_PORT);
|
|
3304
|
-
const fingerprint =
|
|
3560
|
+
const fingerprint = hostAppsFingerprintForFolders(dirs);
|
|
3305
3561
|
const allowAi = input.allowAi !== false;
|
|
3306
|
-
const detected =
|
|
3562
|
+
const detected = detectHostAppsFromFolders(dirs, {
|
|
3307
3563
|
preferredAiPort: preferredAi,
|
|
3308
3564
|
appName: input.appName
|
|
3309
3565
|
});
|
|
@@ -3322,8 +3578,9 @@ async function proposeHostAppsFromConfig(input) {
|
|
|
3322
3578
|
appName: input.appName,
|
|
3323
3579
|
extraContext: [
|
|
3324
3580
|
"File-based detection was uncertain or incomplete.",
|
|
3581
|
+
dirs.length > 1 ? `This sandbox also has folders: ${dirs.slice(1).join(", ")}` : "",
|
|
3325
3582
|
...reasons
|
|
3326
|
-
].join("\n")
|
|
3583
|
+
].filter(Boolean).join("\n")
|
|
3327
3584
|
});
|
|
3328
3585
|
usedAi = true;
|
|
3329
3586
|
const fromAi = appsFromInspectPorts(
|
|
@@ -3332,9 +3589,12 @@ async function proposeHostAppsFromConfig(input) {
|
|
|
3332
3589
|
inspected.name || input.appName || "App"
|
|
3333
3590
|
);
|
|
3334
3591
|
if (fromAi.length) {
|
|
3335
|
-
apps =
|
|
3336
|
-
|
|
3337
|
-
|
|
3592
|
+
apps = unionHostApps(
|
|
3593
|
+
ensureAiServerApp(
|
|
3594
|
+
[defaultAiServerApp(preferredAi), ...fromAi],
|
|
3595
|
+
preferredAi
|
|
3596
|
+
),
|
|
3597
|
+
detected.apps
|
|
3338
3598
|
);
|
|
3339
3599
|
}
|
|
3340
3600
|
if (inspected.summary) projectSummary = inspected.summary;
|
|
@@ -3347,9 +3607,9 @@ async function proposeHostAppsFromConfig(input) {
|
|
|
3347
3607
|
);
|
|
3348
3608
|
}
|
|
3349
3609
|
}
|
|
3350
|
-
const alternatives =
|
|
3351
|
-
appName: input.appName
|
|
3352
|
-
|
|
3610
|
+
const alternatives = dirs.flatMap(
|
|
3611
|
+
(dir) => collectScriptAlternatives(dir, apps, { appName: input.appName })
|
|
3612
|
+
);
|
|
3353
3613
|
const confidence = proposalConfidence(apps, reasons, usedAi);
|
|
3354
3614
|
const needsReview = confidence !== "high" || detected.confused || apps.filter((app) => app.role !== "ai-server").length === 0;
|
|
3355
3615
|
return {
|
|
@@ -3364,11 +3624,12 @@ async function proposeHostAppsFromConfig(input) {
|
|
|
3364
3624
|
};
|
|
3365
3625
|
}
|
|
3366
3626
|
async function resolveHostApps(input) {
|
|
3367
|
-
const
|
|
3627
|
+
const dirs = uniqueWorkspaceDirs(input.workspaceDir, input.workspaceDirs);
|
|
3628
|
+
const folder = dirs[0] || import_node_path8.default.resolve(input.workspaceDir);
|
|
3368
3629
|
const loc = { sandboxId: input.sandboxId, dataDir: input.dataDir };
|
|
3369
|
-
const fingerprint =
|
|
3630
|
+
const fingerprint = hostAppsFingerprintForFolders(dirs);
|
|
3370
3631
|
const preferredAi = parsePort(input.preferredAiPort, AI_SERVER_DEFAULT_PORT);
|
|
3371
|
-
const detected =
|
|
3632
|
+
const detected = detectHostAppsFromFolders(dirs, {
|
|
3372
3633
|
preferredAiPort: preferredAi,
|
|
3373
3634
|
appName: input.appName
|
|
3374
3635
|
});
|
|
@@ -3504,6 +3765,7 @@ Return the real local ports and npm script names.`
|
|
|
3504
3765
|
createToolValidator,
|
|
3505
3766
|
defaultAiServerApp,
|
|
3506
3767
|
detectHostAppsFromFiles,
|
|
3768
|
+
detectHostAppsFromFolders,
|
|
3507
3769
|
ensureAiServerApp,
|
|
3508
3770
|
ensureProjectDataDir,
|
|
3509
3771
|
ensureSingleHost,
|
|
@@ -3513,6 +3775,7 @@ Return the real local ports and npm script names.`
|
|
|
3513
3775
|
getProviderPreference,
|
|
3514
3776
|
hostAppsCachePath,
|
|
3515
3777
|
hostAppsFingerprint,
|
|
3778
|
+
hostAppsFingerprintForFolders,
|
|
3516
3779
|
inspectAndRepairWorkspace,
|
|
3517
3780
|
inspectConfigOnly,
|
|
3518
3781
|
isDevMode,
|
|
@@ -3547,6 +3810,7 @@ Return the real local ports and npm script names.`
|
|
|
3547
3810
|
sanitizeProjectId,
|
|
3548
3811
|
saveChatAttachments,
|
|
3549
3812
|
toNextRoute,
|
|
3813
|
+
unionHostApps,
|
|
3550
3814
|
upsertManagedIgnoreFile,
|
|
3551
3815
|
writeCollaboraterApps,
|
|
3552
3816
|
writeHostAppsCache
|
package/dist/index.d.cts
CHANGED
|
@@ -538,9 +538,13 @@ type HostApp = {
|
|
|
538
538
|
locked?: boolean;
|
|
539
539
|
/** Share URL — the page the browser opens. */
|
|
540
540
|
host?: boolean;
|
|
541
|
+
/** Project folder this process should start in (multi-folder sandboxes). */
|
|
542
|
+
folderPath?: string | null;
|
|
541
543
|
};
|
|
542
544
|
type HostAppsResolveInput = {
|
|
543
545
|
workspaceDir: string;
|
|
546
|
+
/** Extra attached folders for the same sandbox (UI + JVM backend, etc.). */
|
|
547
|
+
workspaceDirs?: string[];
|
|
544
548
|
appName?: string;
|
|
545
549
|
preferredAiPort?: number;
|
|
546
550
|
/** Desired list from Maintainer Pro — used as source of truth when present. */
|
|
@@ -582,6 +586,8 @@ type SetupProposal = {
|
|
|
582
586
|
};
|
|
583
587
|
type ProposeHostAppsInput = {
|
|
584
588
|
workspaceDir: string;
|
|
589
|
+
/** Extra attached folders for the same sandbox. */
|
|
590
|
+
workspaceDirs?: string[];
|
|
585
591
|
appName?: string;
|
|
586
592
|
preferredAiPort?: number;
|
|
587
593
|
/**
|
|
@@ -608,6 +614,7 @@ declare function readProjectEnvLayers(folder: string): {
|
|
|
608
614
|
}>;
|
|
609
615
|
};
|
|
610
616
|
declare function hostAppsFingerprint(folder: string): string;
|
|
617
|
+
declare function hostAppsFingerprintForFolders(folders: string[]): string;
|
|
611
618
|
declare function detectHostAppsFromFiles(folder: string, opts?: {
|
|
612
619
|
preferredAiPort?: number;
|
|
613
620
|
appName?: string;
|
|
@@ -616,6 +623,14 @@ declare function detectHostAppsFromFiles(folder: string, opts?: {
|
|
|
616
623
|
reasons: string[];
|
|
617
624
|
confused: boolean;
|
|
618
625
|
};
|
|
626
|
+
declare function detectHostAppsFromFolders(folders: string[], opts?: {
|
|
627
|
+
preferredAiPort?: number;
|
|
628
|
+
appName?: string;
|
|
629
|
+
}): {
|
|
630
|
+
apps: HostApp[];
|
|
631
|
+
reasons: string[];
|
|
632
|
+
confused: boolean;
|
|
633
|
+
};
|
|
619
634
|
declare function readHostAppsCache(folder: string, extra?: {
|
|
620
635
|
sandboxId?: string | null;
|
|
621
636
|
dataDir?: string | null;
|
|
@@ -633,6 +648,8 @@ declare function writeHostAppsCache(folder: string, apps: HostApp[], extra?: Rec
|
|
|
633
648
|
/** @deprecated Use writeHostAppsCache. */
|
|
634
649
|
declare const writeCollaboraterApps: typeof writeHostAppsCache;
|
|
635
650
|
declare function mergeDesiredHostApps(desired: HostApp[], detected: HostApp[]): HostApp[];
|
|
651
|
+
/** Keep saved apps, and append newly detected ones that are not already listed. */
|
|
652
|
+
declare function unionHostApps(desired: HostApp[], detected: HostApp[]): HostApp[];
|
|
636
653
|
/**
|
|
637
654
|
* Fast, interactive onboarding propose: file detect first, optional read-only AI.
|
|
638
655
|
* Never edits the repository.
|
|
@@ -645,4 +662,4 @@ declare function proposeHostAppsFromConfig(input: ProposeHostAppsInput): Promise
|
|
|
645
662
|
*/
|
|
646
663
|
declare function resolveHostApps(input: HostAppsResolveInput): Promise<HostAppsResolveResult>;
|
|
647
664
|
|
|
648
|
-
export { ACCESS_IGNORE_BEGIN, ACCESS_IGNORE_END, AI_SERVER_APP_ID, AI_SERVER_DEFAULT_PORT, type AiCliProviderId, type AiProvider, type AiResponse, COLLABORATER_DIR, type CachedChatMessage, type CachedConversation, type CallAiOptions, type ChatAttachment, type ChatHandlerOptions, type ChatHandlers, type ChatMessage, type ChatSenderType, type ChatStoreLike, type ChatUser, type ClientContext, DEFAULT_AI_IGNORE_PATHS, HOST_APPS_FILE, type HostApp, type HostAppAlternative, type HostAppRole, type HostAppSource, type HostAppsResolveInput, type HostAppsResolveResult, type LocalStoredMessage, MAINTAINER_PRO_HOME_DIR, type MaintainerProStoreOptions, PROMPT_SECTION, type ParentChainEntry, type ParentChainSource, type PriorConversationOptions, type ProjectDataInput, type ProposeHostAppsInput, type ProviderPreference, type SetupProposal, type SetupProposalConfidence, type SyncedChatStore, type SyncedChatStoreOptions, type SyncedStoreStats, type ToolCall, type ToolSchemaMap, WORKING_PROVIDER, type WorkingTurnEvent, type WorkspaceInspectInput, type WorkspaceInspectResult, type WorkspaceKind, buildClaudeUserPrompt, buildConversationPrompt, buildCursorPrompt, buildPriorConversationsContext, callAi, collectParentChain, commandExists, createAntigravityProvider, createBuiltinProviders, createChatHandler, createClaudeProvider, createCursorProvider, createDefaultSystemPrompt, createInfoLogger, createLocalDirectoryStore, createLogger, createMaintainerProStore, createMaintainerProStoreFromEnv, createSyncedChatStore, createToolValidator, defaultAiServerApp, detectHostAppsFromFiles, ensureAiServerApp, ensureProjectDataDir, ensureSingleHost, formatAccessPolicyPromptSection, formatClientContext, formatParentChainContext, getProviderPreference, hostAppsCachePath, hostAppsFingerprint, inspectAndRepairWorkspace, inspectConfigOnly, isDevMode, isIgnoredRelative, isInsideWorkspace, isPathAllowed, maintainerProHome, mergeDesiredHostApps, normalizeHostApp, normalizeHostApps, normalizeIgnorePaths, parentChainForTurn, parseAiResponse, parseIgnorePathsEnv, parsePort, previewText, hostAppsCachePath$1 as projectHostAppsPath, projectIdForFolder, projectUploadsDir, proposeHostAppsFromConfig, providerLabel, readCollaboraterApps, readHostAppsCache, readProjectEnvLayers, renderManagedIgnoreBlock, resolveCliBinary, resolveHostApps, resolveIgnorePaths, resolveLogLevel, resolveProjectDataDir, resolveProvider, sanitizeProjectId, saveChatAttachments, toNextRoute, upsertManagedIgnoreFile, writeCollaboraterApps, writeHostAppsCache };
|
|
665
|
+
export { ACCESS_IGNORE_BEGIN, ACCESS_IGNORE_END, AI_SERVER_APP_ID, AI_SERVER_DEFAULT_PORT, type AiCliProviderId, type AiProvider, type AiResponse, COLLABORATER_DIR, type CachedChatMessage, type CachedConversation, type CallAiOptions, type ChatAttachment, type ChatHandlerOptions, type ChatHandlers, type ChatMessage, type ChatSenderType, type ChatStoreLike, type ChatUser, type ClientContext, DEFAULT_AI_IGNORE_PATHS, HOST_APPS_FILE, type HostApp, type HostAppAlternative, type HostAppRole, type HostAppSource, type HostAppsResolveInput, type HostAppsResolveResult, type LocalStoredMessage, MAINTAINER_PRO_HOME_DIR, type MaintainerProStoreOptions, PROMPT_SECTION, type ParentChainEntry, type ParentChainSource, type PriorConversationOptions, type ProjectDataInput, type ProposeHostAppsInput, type ProviderPreference, type SetupProposal, type SetupProposalConfidence, type SyncedChatStore, type SyncedChatStoreOptions, type SyncedStoreStats, type ToolCall, type ToolSchemaMap, WORKING_PROVIDER, type WorkingTurnEvent, type WorkspaceInspectInput, type WorkspaceInspectResult, type WorkspaceKind, buildClaudeUserPrompt, buildConversationPrompt, buildCursorPrompt, buildPriorConversationsContext, callAi, collectParentChain, commandExists, createAntigravityProvider, createBuiltinProviders, createChatHandler, createClaudeProvider, createCursorProvider, createDefaultSystemPrompt, createInfoLogger, createLocalDirectoryStore, createLogger, createMaintainerProStore, createMaintainerProStoreFromEnv, createSyncedChatStore, createToolValidator, defaultAiServerApp, detectHostAppsFromFiles, detectHostAppsFromFolders, ensureAiServerApp, ensureProjectDataDir, ensureSingleHost, formatAccessPolicyPromptSection, formatClientContext, formatParentChainContext, getProviderPreference, hostAppsCachePath, hostAppsFingerprint, hostAppsFingerprintForFolders, inspectAndRepairWorkspace, inspectConfigOnly, isDevMode, isIgnoredRelative, isInsideWorkspace, isPathAllowed, maintainerProHome, mergeDesiredHostApps, normalizeHostApp, normalizeHostApps, normalizeIgnorePaths, parentChainForTurn, parseAiResponse, parseIgnorePathsEnv, parsePort, previewText, hostAppsCachePath$1 as projectHostAppsPath, projectIdForFolder, projectUploadsDir, proposeHostAppsFromConfig, providerLabel, readCollaboraterApps, readHostAppsCache, readProjectEnvLayers, renderManagedIgnoreBlock, resolveCliBinary, resolveHostApps, resolveIgnorePaths, resolveLogLevel, resolveProjectDataDir, resolveProvider, sanitizeProjectId, saveChatAttachments, toNextRoute, unionHostApps, upsertManagedIgnoreFile, writeCollaboraterApps, writeHostAppsCache };
|
package/dist/index.d.ts
CHANGED
|
@@ -538,9 +538,13 @@ type HostApp = {
|
|
|
538
538
|
locked?: boolean;
|
|
539
539
|
/** Share URL — the page the browser opens. */
|
|
540
540
|
host?: boolean;
|
|
541
|
+
/** Project folder this process should start in (multi-folder sandboxes). */
|
|
542
|
+
folderPath?: string | null;
|
|
541
543
|
};
|
|
542
544
|
type HostAppsResolveInput = {
|
|
543
545
|
workspaceDir: string;
|
|
546
|
+
/** Extra attached folders for the same sandbox (UI + JVM backend, etc.). */
|
|
547
|
+
workspaceDirs?: string[];
|
|
544
548
|
appName?: string;
|
|
545
549
|
preferredAiPort?: number;
|
|
546
550
|
/** Desired list from Maintainer Pro — used as source of truth when present. */
|
|
@@ -582,6 +586,8 @@ type SetupProposal = {
|
|
|
582
586
|
};
|
|
583
587
|
type ProposeHostAppsInput = {
|
|
584
588
|
workspaceDir: string;
|
|
589
|
+
/** Extra attached folders for the same sandbox. */
|
|
590
|
+
workspaceDirs?: string[];
|
|
585
591
|
appName?: string;
|
|
586
592
|
preferredAiPort?: number;
|
|
587
593
|
/**
|
|
@@ -608,6 +614,7 @@ declare function readProjectEnvLayers(folder: string): {
|
|
|
608
614
|
}>;
|
|
609
615
|
};
|
|
610
616
|
declare function hostAppsFingerprint(folder: string): string;
|
|
617
|
+
declare function hostAppsFingerprintForFolders(folders: string[]): string;
|
|
611
618
|
declare function detectHostAppsFromFiles(folder: string, opts?: {
|
|
612
619
|
preferredAiPort?: number;
|
|
613
620
|
appName?: string;
|
|
@@ -616,6 +623,14 @@ declare function detectHostAppsFromFiles(folder: string, opts?: {
|
|
|
616
623
|
reasons: string[];
|
|
617
624
|
confused: boolean;
|
|
618
625
|
};
|
|
626
|
+
declare function detectHostAppsFromFolders(folders: string[], opts?: {
|
|
627
|
+
preferredAiPort?: number;
|
|
628
|
+
appName?: string;
|
|
629
|
+
}): {
|
|
630
|
+
apps: HostApp[];
|
|
631
|
+
reasons: string[];
|
|
632
|
+
confused: boolean;
|
|
633
|
+
};
|
|
619
634
|
declare function readHostAppsCache(folder: string, extra?: {
|
|
620
635
|
sandboxId?: string | null;
|
|
621
636
|
dataDir?: string | null;
|
|
@@ -633,6 +648,8 @@ declare function writeHostAppsCache(folder: string, apps: HostApp[], extra?: Rec
|
|
|
633
648
|
/** @deprecated Use writeHostAppsCache. */
|
|
634
649
|
declare const writeCollaboraterApps: typeof writeHostAppsCache;
|
|
635
650
|
declare function mergeDesiredHostApps(desired: HostApp[], detected: HostApp[]): HostApp[];
|
|
651
|
+
/** Keep saved apps, and append newly detected ones that are not already listed. */
|
|
652
|
+
declare function unionHostApps(desired: HostApp[], detected: HostApp[]): HostApp[];
|
|
636
653
|
/**
|
|
637
654
|
* Fast, interactive onboarding propose: file detect first, optional read-only AI.
|
|
638
655
|
* Never edits the repository.
|
|
@@ -645,4 +662,4 @@ declare function proposeHostAppsFromConfig(input: ProposeHostAppsInput): Promise
|
|
|
645
662
|
*/
|
|
646
663
|
declare function resolveHostApps(input: HostAppsResolveInput): Promise<HostAppsResolveResult>;
|
|
647
664
|
|
|
648
|
-
export { ACCESS_IGNORE_BEGIN, ACCESS_IGNORE_END, AI_SERVER_APP_ID, AI_SERVER_DEFAULT_PORT, type AiCliProviderId, type AiProvider, type AiResponse, COLLABORATER_DIR, type CachedChatMessage, type CachedConversation, type CallAiOptions, type ChatAttachment, type ChatHandlerOptions, type ChatHandlers, type ChatMessage, type ChatSenderType, type ChatStoreLike, type ChatUser, type ClientContext, DEFAULT_AI_IGNORE_PATHS, HOST_APPS_FILE, type HostApp, type HostAppAlternative, type HostAppRole, type HostAppSource, type HostAppsResolveInput, type HostAppsResolveResult, type LocalStoredMessage, MAINTAINER_PRO_HOME_DIR, type MaintainerProStoreOptions, PROMPT_SECTION, type ParentChainEntry, type ParentChainSource, type PriorConversationOptions, type ProjectDataInput, type ProposeHostAppsInput, type ProviderPreference, type SetupProposal, type SetupProposalConfidence, type SyncedChatStore, type SyncedChatStoreOptions, type SyncedStoreStats, type ToolCall, type ToolSchemaMap, WORKING_PROVIDER, type WorkingTurnEvent, type WorkspaceInspectInput, type WorkspaceInspectResult, type WorkspaceKind, buildClaudeUserPrompt, buildConversationPrompt, buildCursorPrompt, buildPriorConversationsContext, callAi, collectParentChain, commandExists, createAntigravityProvider, createBuiltinProviders, createChatHandler, createClaudeProvider, createCursorProvider, createDefaultSystemPrompt, createInfoLogger, createLocalDirectoryStore, createLogger, createMaintainerProStore, createMaintainerProStoreFromEnv, createSyncedChatStore, createToolValidator, defaultAiServerApp, detectHostAppsFromFiles, ensureAiServerApp, ensureProjectDataDir, ensureSingleHost, formatAccessPolicyPromptSection, formatClientContext, formatParentChainContext, getProviderPreference, hostAppsCachePath, hostAppsFingerprint, inspectAndRepairWorkspace, inspectConfigOnly, isDevMode, isIgnoredRelative, isInsideWorkspace, isPathAllowed, maintainerProHome, mergeDesiredHostApps, normalizeHostApp, normalizeHostApps, normalizeIgnorePaths, parentChainForTurn, parseAiResponse, parseIgnorePathsEnv, parsePort, previewText, hostAppsCachePath$1 as projectHostAppsPath, projectIdForFolder, projectUploadsDir, proposeHostAppsFromConfig, providerLabel, readCollaboraterApps, readHostAppsCache, readProjectEnvLayers, renderManagedIgnoreBlock, resolveCliBinary, resolveHostApps, resolveIgnorePaths, resolveLogLevel, resolveProjectDataDir, resolveProvider, sanitizeProjectId, saveChatAttachments, toNextRoute, upsertManagedIgnoreFile, writeCollaboraterApps, writeHostAppsCache };
|
|
665
|
+
export { ACCESS_IGNORE_BEGIN, ACCESS_IGNORE_END, AI_SERVER_APP_ID, AI_SERVER_DEFAULT_PORT, type AiCliProviderId, type AiProvider, type AiResponse, COLLABORATER_DIR, type CachedChatMessage, type CachedConversation, type CallAiOptions, type ChatAttachment, type ChatHandlerOptions, type ChatHandlers, type ChatMessage, type ChatSenderType, type ChatStoreLike, type ChatUser, type ClientContext, DEFAULT_AI_IGNORE_PATHS, HOST_APPS_FILE, type HostApp, type HostAppAlternative, type HostAppRole, type HostAppSource, type HostAppsResolveInput, type HostAppsResolveResult, type LocalStoredMessage, MAINTAINER_PRO_HOME_DIR, type MaintainerProStoreOptions, PROMPT_SECTION, type ParentChainEntry, type ParentChainSource, type PriorConversationOptions, type ProjectDataInput, type ProposeHostAppsInput, type ProviderPreference, type SetupProposal, type SetupProposalConfidence, type SyncedChatStore, type SyncedChatStoreOptions, type SyncedStoreStats, type ToolCall, type ToolSchemaMap, WORKING_PROVIDER, type WorkingTurnEvent, type WorkspaceInspectInput, type WorkspaceInspectResult, type WorkspaceKind, buildClaudeUserPrompt, buildConversationPrompt, buildCursorPrompt, buildPriorConversationsContext, callAi, collectParentChain, commandExists, createAntigravityProvider, createBuiltinProviders, createChatHandler, createClaudeProvider, createCursorProvider, createDefaultSystemPrompt, createInfoLogger, createLocalDirectoryStore, createLogger, createMaintainerProStore, createMaintainerProStoreFromEnv, createSyncedChatStore, createToolValidator, defaultAiServerApp, detectHostAppsFromFiles, detectHostAppsFromFolders, ensureAiServerApp, ensureProjectDataDir, ensureSingleHost, formatAccessPolicyPromptSection, formatClientContext, formatParentChainContext, getProviderPreference, hostAppsCachePath, hostAppsFingerprint, hostAppsFingerprintForFolders, inspectAndRepairWorkspace, inspectConfigOnly, isDevMode, isIgnoredRelative, isInsideWorkspace, isPathAllowed, maintainerProHome, mergeDesiredHostApps, normalizeHostApp, normalizeHostApps, normalizeIgnorePaths, parentChainForTurn, parseAiResponse, parseIgnorePathsEnv, parsePort, previewText, hostAppsCachePath$1 as projectHostAppsPath, projectIdForFolder, projectUploadsDir, proposeHostAppsFromConfig, providerLabel, readCollaboraterApps, readHostAppsCache, readProjectEnvLayers, renderManagedIgnoreBlock, resolveCliBinary, resolveHostApps, resolveIgnorePaths, resolveLogLevel, resolveProjectDataDir, resolveProvider, sanitizeProjectId, saveChatAttachments, toNextRoute, unionHostApps, upsertManagedIgnoreFile, writeCollaboraterApps, writeHostAppsCache };
|
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
|
|
@@ -2795,7 +2809,8 @@ function normalizeHostApp(raw) {
|
|
|
2795
2809
|
row.source
|
|
2796
2810
|
) ? row.source : "manual",
|
|
2797
2811
|
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
|
|
2812
|
+
host: role !== "ai-server" && id !== AI_SERVER_APP_ID && row.host === true,
|
|
2813
|
+
folderPath: typeof row.folderPath === "string" && row.folderPath.trim() ? row.folderPath.trim().slice(0, 500) : null
|
|
2799
2814
|
};
|
|
2800
2815
|
}
|
|
2801
2816
|
function ensureSingleHost(apps) {
|
|
@@ -2908,19 +2923,151 @@ function configPort(folder, files) {
|
|
|
2908
2923
|
}
|
|
2909
2924
|
return 0;
|
|
2910
2925
|
}
|
|
2911
|
-
|
|
2926
|
+
var JVM_CONFIG_FILES = [
|
|
2927
|
+
"pom.xml",
|
|
2928
|
+
"build.gradle",
|
|
2929
|
+
"build.gradle.kts",
|
|
2930
|
+
"settings.gradle",
|
|
2931
|
+
"settings.gradle.kts",
|
|
2932
|
+
"build.sbt",
|
|
2933
|
+
"gradle.properties",
|
|
2934
|
+
"application.yml",
|
|
2935
|
+
"application.yaml",
|
|
2936
|
+
"application.properties",
|
|
2937
|
+
"application.conf",
|
|
2938
|
+
"src/main/resources/application.yml",
|
|
2939
|
+
"src/main/resources/application.yaml",
|
|
2940
|
+
"src/main/resources/application.properties",
|
|
2941
|
+
"src/main/resources/application.conf",
|
|
2942
|
+
"src/main/resources/application-dev.yml",
|
|
2943
|
+
"src/main/resources/application-dev.yaml",
|
|
2944
|
+
"src/main/resources/application-dev.properties",
|
|
2945
|
+
"src/main/resources/application-local.yml",
|
|
2946
|
+
"conf/application.conf",
|
|
2947
|
+
"conf/application.yml"
|
|
2948
|
+
];
|
|
2949
|
+
var FINGERPRINT_FILES = [
|
|
2950
|
+
"package.json",
|
|
2951
|
+
...ENV_FILES,
|
|
2952
|
+
"vite.config.ts",
|
|
2953
|
+
"vite.config.js",
|
|
2954
|
+
"vite.config.mjs",
|
|
2955
|
+
"next.config.js",
|
|
2956
|
+
"next.config.mjs",
|
|
2957
|
+
"next.config.ts",
|
|
2958
|
+
...JVM_CONFIG_FILES
|
|
2959
|
+
];
|
|
2960
|
+
function fileExists(folder, rel) {
|
|
2961
|
+
return fs7.existsSync(path8.join(folder, rel));
|
|
2962
|
+
}
|
|
2963
|
+
function jvmPortFromText(text) {
|
|
2964
|
+
const patterns = [
|
|
2965
|
+
/(?:^|\n)\s*server\.port\s*[:=]\s*["']?(\d{2,5})/im,
|
|
2966
|
+
/(?:^|\n)\s*SERVER_PORT\s*[:=]\s*["']?(\d{2,5})/im,
|
|
2967
|
+
/(?:^|\n)\s*play\.http\.port\s*[:=]\s*["']?(\d{2,5})/im,
|
|
2968
|
+
/(?:^|\n)\s*play\.server\.http\.port\s*[:=]\s*["']?(\d{2,5})/im,
|
|
2969
|
+
/(?:^|\n)\s*http\.port\s*[:=]\s*["']?(\d{2,5})/im,
|
|
2970
|
+
/-Dserver\.port=(\d{2,5})/i,
|
|
2971
|
+
/systemProperty\s*\(?\s*["']server\.port["']\s*,\s*["']?(\d{2,5})/i,
|
|
2972
|
+
/["']play\.http\.port["']\s*->\s*["']?(\d{2,5})/i
|
|
2973
|
+
];
|
|
2974
|
+
for (const pattern of patterns) {
|
|
2975
|
+
const match = text.match(pattern);
|
|
2976
|
+
const port = parsePort(match?.[1]);
|
|
2977
|
+
if (port) return port;
|
|
2978
|
+
}
|
|
2979
|
+
const yaml = text.match(
|
|
2980
|
+
/(?:^|\n)server:\s*\n(?:[ \t].*\n)*?[ \t]+port:\s*["']?(\d{2,5})/i
|
|
2981
|
+
);
|
|
2982
|
+
return parsePort(yaml?.[1]);
|
|
2983
|
+
}
|
|
2984
|
+
function looksLike(haystack, ...needles) {
|
|
2985
|
+
const lower = haystack.toLowerCase();
|
|
2986
|
+
return needles.some((needle) => lower.includes(needle.toLowerCase()));
|
|
2987
|
+
}
|
|
2988
|
+
function wrapperCommand(folder, unixName, winName, fallback) {
|
|
2989
|
+
if (process.platform === "win32") {
|
|
2990
|
+
return fileExists(folder, winName) ? `.\\${winName}` : fallback;
|
|
2991
|
+
}
|
|
2992
|
+
return fileExists(folder, unixName) ? `./${unixName}` : fallback;
|
|
2993
|
+
}
|
|
2994
|
+
function detectJvmHostApp(folder, opts = {}) {
|
|
2995
|
+
const resolved = path8.resolve(folder);
|
|
2996
|
+
const texts = [];
|
|
2997
|
+
let foundMarker = false;
|
|
2998
|
+
for (const rel of JVM_CONFIG_FILES) {
|
|
2999
|
+
const text = readText(path8.join(resolved, rel));
|
|
3000
|
+
if (!text) continue;
|
|
3001
|
+
foundMarker = true;
|
|
3002
|
+
texts.push(text);
|
|
3003
|
+
}
|
|
3004
|
+
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");
|
|
3005
|
+
const hasMaven = fileExists(resolved, "pom.xml");
|
|
3006
|
+
const hasSbt = fileExists(resolved, "build.sbt");
|
|
3007
|
+
if (!foundMarker && !hasGradle && !hasMaven && !hasSbt) return null;
|
|
3008
|
+
const blob = texts.join("\n");
|
|
3009
|
+
const { merged } = readProjectEnvLayers(resolved);
|
|
3010
|
+
const envPort = parsePort(merged.SERVER_PORT) || parsePort(merged.PLAY_HTTP_PORT) || parsePort(merged.HTTP_PORT) || parsePort(merged.PORT);
|
|
3011
|
+
let port = envPort || jvmPortFromText(blob);
|
|
3012
|
+
const isPlay = looksLike(
|
|
3013
|
+
blob,
|
|
3014
|
+
"play.http.port",
|
|
3015
|
+
"enablePlugins(Play",
|
|
3016
|
+
"com.typesafe.play",
|
|
3017
|
+
"play-scala",
|
|
3018
|
+
"play-java"
|
|
3019
|
+
);
|
|
3020
|
+
const isSpring = looksLike(
|
|
3021
|
+
blob,
|
|
3022
|
+
"spring-boot",
|
|
3023
|
+
"springframework.boot",
|
|
3024
|
+
"org.springframework"
|
|
3025
|
+
);
|
|
3026
|
+
const isQuarkus = looksLike(blob, "quarkus", "io.quarkus");
|
|
3027
|
+
if (!port) {
|
|
3028
|
+
port = isPlay ? 9e3 : 8080;
|
|
3029
|
+
}
|
|
3030
|
+
let startCommand = null;
|
|
3031
|
+
if (hasGradle) {
|
|
3032
|
+
const gradle = wrapperCommand(resolved, "gradlew", "gradlew.bat", "gradle");
|
|
3033
|
+
startCommand = isQuarkus ? `${gradle} quarkusDev` : isPlay ? `${gradle} run` : `${gradle} bootRun`;
|
|
3034
|
+
} else if (hasMaven) {
|
|
3035
|
+
const mvn = wrapperCommand(resolved, "mvnw", "mvnw.cmd", "mvn");
|
|
3036
|
+
startCommand = isQuarkus ? `${mvn} quarkus:dev` : `${mvn} spring-boot:run`;
|
|
3037
|
+
} else if (hasSbt) {
|
|
3038
|
+
startCommand = "sbt run";
|
|
3039
|
+
}
|
|
3040
|
+
const framework = isPlay ? "Play" : isSpring ? "Spring" : isQuarkus ? "Quarkus" : hasSbt ? "Scala" : "JVM";
|
|
3041
|
+
return {
|
|
3042
|
+
id: "backend",
|
|
3043
|
+
name: opts.appName ? `${opts.appName} API` : `${framework} backend`,
|
|
3044
|
+
role: "backend",
|
|
3045
|
+
port,
|
|
3046
|
+
startCommand,
|
|
3047
|
+
source: envPort ? "env" : "package",
|
|
3048
|
+
folderPath: resolved
|
|
3049
|
+
};
|
|
3050
|
+
}
|
|
3051
|
+
function uniqueWorkspaceDirs(primary, extra) {
|
|
3052
|
+
const out = [];
|
|
3053
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3054
|
+
for (const raw of [primary, ...extra || []]) {
|
|
3055
|
+
if (!raw || typeof raw !== "string") continue;
|
|
3056
|
+
const resolved = path8.resolve(raw.trim());
|
|
3057
|
+
const key = process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
3058
|
+
if (seen.has(key)) continue;
|
|
3059
|
+
seen.add(key);
|
|
3060
|
+
out.push(resolved);
|
|
3061
|
+
}
|
|
3062
|
+
return out;
|
|
3063
|
+
}
|
|
3064
|
+
function folderSlug(folder) {
|
|
3065
|
+
return path8.basename(path8.resolve(folder)).replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 24) || "app";
|
|
3066
|
+
}
|
|
3067
|
+
function fingerprintOneFolder(folder) {
|
|
2912
3068
|
const resolved = path8.resolve(folder);
|
|
2913
3069
|
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
|
-
]) {
|
|
3070
|
+
for (const rel of FINGERPRINT_FILES) {
|
|
2924
3071
|
const file = path8.join(resolved, rel);
|
|
2925
3072
|
if (!fs7.existsSync(file)) continue;
|
|
2926
3073
|
try {
|
|
@@ -2929,7 +3076,16 @@ function hostAppsFingerprint(folder) {
|
|
|
2929
3076
|
} catch {
|
|
2930
3077
|
}
|
|
2931
3078
|
}
|
|
2932
|
-
return
|
|
3079
|
+
return parts.join("|") || resolved;
|
|
3080
|
+
}
|
|
3081
|
+
function hostAppsFingerprint(folder) {
|
|
3082
|
+
const resolved = path8.resolve(folder);
|
|
3083
|
+
return createHash2("sha256").update(fingerprintOneFolder(resolved)).digest("hex").slice(0, 32);
|
|
3084
|
+
}
|
|
3085
|
+
function hostAppsFingerprintForFolders(folders) {
|
|
3086
|
+
const dirs = uniqueWorkspaceDirs(folders[0] || "", folders.slice(1));
|
|
3087
|
+
const parts = dirs.map((dir) => `${dir}:${fingerprintOneFolder(dir)}`);
|
|
3088
|
+
return createHash2("sha256").update(parts.join("||") || "empty").digest("hex").slice(0, 32);
|
|
2933
3089
|
}
|
|
2934
3090
|
function pickScript(scripts, names) {
|
|
2935
3091
|
return names.find((name) => scripts[name] && !isChatScript(name, scripts[name])) || null;
|
|
@@ -2948,9 +3104,14 @@ function detectHostAppsFromFiles(folder, opts = {}) {
|
|
|
2948
3104
|
source: envAi ? "env" : "default"
|
|
2949
3105
|
});
|
|
2950
3106
|
const envPorts = {
|
|
2951
|
-
PORT: layers.filter((layer) => layer.file !== ".env.example").map((layer) => ({
|
|
3107
|
+
PORT: layers.filter((layer) => layer.file !== ".env.example").map((layer) => ({
|
|
3108
|
+
file: layer.file,
|
|
3109
|
+
port: parsePort(layer.values.PORT) || parsePort(layer.values.SERVER_PORT) || parsePort(layer.values.PLAY_HTTP_PORT)
|
|
3110
|
+
})).filter((row) => row.port),
|
|
2952
3111
|
APP_URL: parsePort(merged.APP_URL || merged.NEXT_PUBLIC_APP_URL || merged.PUBLIC_URL),
|
|
2953
|
-
API_PORT: parsePort(
|
|
3112
|
+
API_PORT: parsePort(
|
|
3113
|
+
merged.API_PORT || merged.API_URL || merged.VITE_API_URL || merged.SERVER_PORT || merged.PLAY_HTTP_PORT || merged.HTTP_PORT
|
|
3114
|
+
)
|
|
2954
3115
|
};
|
|
2955
3116
|
const portValues = [...new Set(envPorts.PORT.map((row) => row.port))];
|
|
2956
3117
|
if (portValues.length > 1) {
|
|
@@ -2995,7 +3156,8 @@ function detectHostAppsFromFiles(folder, opts = {}) {
|
|
|
2995
3156
|
port,
|
|
2996
3157
|
startCommand: `npm run ${uiScript}`,
|
|
2997
3158
|
source: uiFromEnv ? "env" : scriptPort ? "package" : "package",
|
|
2998
|
-
host: true
|
|
3159
|
+
host: true,
|
|
3160
|
+
folderPath: resolved
|
|
2999
3161
|
});
|
|
3000
3162
|
} else if (uiFromEnv) {
|
|
3001
3163
|
apps.push({
|
|
@@ -3005,7 +3167,8 @@ function detectHostAppsFromFiles(folder, opts = {}) {
|
|
|
3005
3167
|
port: uiFromEnv,
|
|
3006
3168
|
startCommand: scripts.dev ? "npm run dev" : null,
|
|
3007
3169
|
source: "env",
|
|
3008
|
-
host: true
|
|
3170
|
+
host: true,
|
|
3171
|
+
folderPath: resolved
|
|
3009
3172
|
});
|
|
3010
3173
|
}
|
|
3011
3174
|
if (apiScript && apiScript !== uiScript) {
|
|
@@ -3017,19 +3180,83 @@ function detectHostAppsFromFiles(folder, opts = {}) {
|
|
|
3017
3180
|
role: "backend",
|
|
3018
3181
|
port,
|
|
3019
3182
|
startCommand: `npm run ${apiScript}`,
|
|
3020
|
-
source: envPorts.API_PORT ? "env" : "package"
|
|
3183
|
+
source: envPorts.API_PORT ? "env" : "package",
|
|
3184
|
+
folderPath: resolved
|
|
3021
3185
|
});
|
|
3022
3186
|
}
|
|
3023
|
-
const
|
|
3187
|
+
const hasHostApp = apps.some((app) => app.role !== "ai-server");
|
|
3188
|
+
const usedPorts = new Set(
|
|
3189
|
+
apps.filter((app) => app.role !== "ai-server").map((app) => app.port)
|
|
3190
|
+
);
|
|
3191
|
+
const jvm = detectJvmHostApp(resolved, { appName: opts.appName });
|
|
3192
|
+
if (jvm && !usedPorts.has(jvm.port)) {
|
|
3193
|
+
apps.push({
|
|
3194
|
+
...jvm,
|
|
3195
|
+
id: apps.some((app) => app.id === "backend") ? `backend-${folderSlug(resolved)}` : "backend",
|
|
3196
|
+
host: !hasHostApp
|
|
3197
|
+
});
|
|
3198
|
+
} else if (jvm && usedPorts.has(jvm.port) && !hasHostApp) {
|
|
3199
|
+
reasons.push(
|
|
3200
|
+
`JVM config suggests port ${jvm.port}, which is already used by another detected app`
|
|
3201
|
+
);
|
|
3202
|
+
}
|
|
3203
|
+
const tagged = apps.map(
|
|
3204
|
+
(app) => app.role === "ai-server" || app.folderPath ? app : { ...app, folderPath: resolved }
|
|
3205
|
+
);
|
|
3206
|
+
const hostApps = tagged.filter((app) => app.role !== "ai-server");
|
|
3024
3207
|
if (pkg && Object.keys(scripts).length && hostApps.length === 0) {
|
|
3025
3208
|
reasons.push("Found package.json scripts but no host app port in env or config");
|
|
3026
3209
|
}
|
|
3027
3210
|
return {
|
|
3028
|
-
apps: ensureAiServerApp(
|
|
3211
|
+
apps: ensureAiServerApp(tagged, aiPort),
|
|
3029
3212
|
reasons,
|
|
3030
3213
|
confused: reasons.length > 0
|
|
3031
3214
|
};
|
|
3032
3215
|
}
|
|
3216
|
+
function detectHostAppsFromFolders(folders, opts = {}) {
|
|
3217
|
+
const dirs = uniqueWorkspaceDirs(folders[0] || "", folders.slice(1));
|
|
3218
|
+
const reasons = [];
|
|
3219
|
+
let confused = false;
|
|
3220
|
+
const apps = [];
|
|
3221
|
+
const usedIds = /* @__PURE__ */ new Set();
|
|
3222
|
+
const usedPortSet = /* @__PURE__ */ new Set();
|
|
3223
|
+
const primary = dirs[0];
|
|
3224
|
+
for (const folder of dirs) {
|
|
3225
|
+
const detected = detectHostAppsFromFiles(folder, opts);
|
|
3226
|
+
confused = confused || detected.confused;
|
|
3227
|
+
const base = path8.basename(folder);
|
|
3228
|
+
for (const reason of detected.reasons) {
|
|
3229
|
+
reasons.push(dirs.length > 1 ? `${base}: ${reason}` : reason);
|
|
3230
|
+
}
|
|
3231
|
+
for (const app of detected.apps) {
|
|
3232
|
+
if (app.role === "ai-server") {
|
|
3233
|
+
if (!apps.some((row) => row.role === "ai-server")) apps.push(app);
|
|
3234
|
+
continue;
|
|
3235
|
+
}
|
|
3236
|
+
let id = app.id;
|
|
3237
|
+
if (usedIds.has(id)) id = `${app.id}-${folderSlug(folder)}`;
|
|
3238
|
+
usedIds.add(id);
|
|
3239
|
+
if (usedPortSet.has(app.port)) {
|
|
3240
|
+
reasons.push(
|
|
3241
|
+
`${base}: port ${app.port} is also used by another attached folder`
|
|
3242
|
+
);
|
|
3243
|
+
confused = true;
|
|
3244
|
+
}
|
|
3245
|
+
usedPortSet.add(app.port);
|
|
3246
|
+
apps.push({
|
|
3247
|
+
...app,
|
|
3248
|
+
id,
|
|
3249
|
+
folderPath: path8.resolve(folder),
|
|
3250
|
+
name: dirs.length > 1 && primary && folder !== primary && !app.name.includes(base) ? `${app.name} (${base})` : app.name
|
|
3251
|
+
});
|
|
3252
|
+
}
|
|
3253
|
+
}
|
|
3254
|
+
return {
|
|
3255
|
+
apps: ensureAiServerApp(apps, opts.preferredAiPort),
|
|
3256
|
+
reasons,
|
|
3257
|
+
confused
|
|
3258
|
+
};
|
|
3259
|
+
}
|
|
3033
3260
|
function readHostAppsCache(folder, extra) {
|
|
3034
3261
|
try {
|
|
3035
3262
|
const raw = JSON.parse(
|
|
@@ -3074,12 +3301,37 @@ function mergeDesiredHostApps(desired, detected) {
|
|
|
3074
3301
|
...local,
|
|
3075
3302
|
...app,
|
|
3076
3303
|
startCommand: app.startCommand || local?.startCommand || null,
|
|
3304
|
+
folderPath: app.folderPath || local?.folderPath || null,
|
|
3077
3305
|
locked: app.locked || app.id === AI_SERVER_APP_ID || app.role === "ai-server",
|
|
3078
3306
|
host: app.role === "ai-server" || app.id === AI_SERVER_APP_ID ? false : app.host === true || app.host !== false && local?.host === true
|
|
3079
3307
|
};
|
|
3080
3308
|
});
|
|
3081
3309
|
return ensureAiServerApp(merged);
|
|
3082
3310
|
}
|
|
3311
|
+
function unionHostApps(desired, detected) {
|
|
3312
|
+
const merged = mergeDesiredHostApps(desired, detected);
|
|
3313
|
+
const seenIds = new Set(merged.map((app) => app.id));
|
|
3314
|
+
const seenPorts = new Set(merged.map((app) => app.port));
|
|
3315
|
+
for (const app of detected) {
|
|
3316
|
+
if (app.role === "ai-server") continue;
|
|
3317
|
+
if (seenIds.has(app.id) || seenPorts.has(app.port)) continue;
|
|
3318
|
+
seenIds.add(app.id);
|
|
3319
|
+
seenPorts.add(app.port);
|
|
3320
|
+
merged.push(app);
|
|
3321
|
+
}
|
|
3322
|
+
return ensureAiServerApp(merged);
|
|
3323
|
+
}
|
|
3324
|
+
function startCommandFromScript(script) {
|
|
3325
|
+
if (!script || !script.trim()) return null;
|
|
3326
|
+
const value = script.trim();
|
|
3327
|
+
if (/\s/.test(value) || /[\\/]/.test(value) || value.includes(":")) {
|
|
3328
|
+
if (/^(npm|pnpm|yarn|npx)\s/i.test(value)) return value;
|
|
3329
|
+
if (/^(sbt|mvn|gradle|gradlew|mvnw|\.\\|\.\/)/i.test(value) || /\s/.test(value)) {
|
|
3330
|
+
return value;
|
|
3331
|
+
}
|
|
3332
|
+
}
|
|
3333
|
+
return `npm run ${value}`;
|
|
3334
|
+
}
|
|
3083
3335
|
function appsFromInspectPorts(ports, scripts, name) {
|
|
3084
3336
|
const apps = [];
|
|
3085
3337
|
if (ports.ui || scripts.ui) {
|
|
@@ -3088,7 +3340,7 @@ function appsFromInspectPorts(ports, scripts, name) {
|
|
|
3088
3340
|
name,
|
|
3089
3341
|
role: "ui",
|
|
3090
3342
|
port: parsePort(ports.ui, 5173),
|
|
3091
|
-
startCommand: scripts.ui
|
|
3343
|
+
startCommand: startCommandFromScript(scripts.ui),
|
|
3092
3344
|
source: "ai"
|
|
3093
3345
|
});
|
|
3094
3346
|
}
|
|
@@ -3097,8 +3349,8 @@ function appsFromInspectPorts(ports, scripts, name) {
|
|
|
3097
3349
|
id: "backend",
|
|
3098
3350
|
name: "Backend",
|
|
3099
3351
|
role: "backend",
|
|
3100
|
-
port: parsePort(ports.backend,
|
|
3101
|
-
startCommand: scripts.backend
|
|
3352
|
+
port: parsePort(ports.backend, 8080),
|
|
3353
|
+
startCommand: startCommandFromScript(scripts.backend),
|
|
3102
3354
|
source: "ai"
|
|
3103
3355
|
});
|
|
3104
3356
|
}
|
|
@@ -3108,7 +3360,7 @@ function appsFromInspectPorts(ports, scripts, name) {
|
|
|
3108
3360
|
name,
|
|
3109
3361
|
role: "app",
|
|
3110
3362
|
port: parsePort(ports.app, 3e3),
|
|
3111
|
-
startCommand: scripts.app
|
|
3363
|
+
startCommand: startCommandFromScript(scripts.app),
|
|
3112
3364
|
source: "ai"
|
|
3113
3365
|
});
|
|
3114
3366
|
}
|
|
@@ -3186,11 +3438,12 @@ function projectSummaryFromDetect(folder, apps, opts = {}) {
|
|
|
3186
3438
|
return `${name}: ${hostApps.map((app) => `${app.name} on ${app.port}${app.startCommand ? ` (${app.startCommand})` : ""}`).join(", ")}.`;
|
|
3187
3439
|
}
|
|
3188
3440
|
async function proposeHostAppsFromConfig(input) {
|
|
3189
|
-
const
|
|
3441
|
+
const dirs = uniqueWorkspaceDirs(input.workspaceDir, input.workspaceDirs);
|
|
3442
|
+
const folder = dirs[0] || path8.resolve(input.workspaceDir);
|
|
3190
3443
|
const preferredAi = parsePort(input.preferredAiPort, AI_SERVER_DEFAULT_PORT);
|
|
3191
|
-
const fingerprint =
|
|
3444
|
+
const fingerprint = hostAppsFingerprintForFolders(dirs);
|
|
3192
3445
|
const allowAi = input.allowAi !== false;
|
|
3193
|
-
const detected =
|
|
3446
|
+
const detected = detectHostAppsFromFolders(dirs, {
|
|
3194
3447
|
preferredAiPort: preferredAi,
|
|
3195
3448
|
appName: input.appName
|
|
3196
3449
|
});
|
|
@@ -3209,8 +3462,9 @@ async function proposeHostAppsFromConfig(input) {
|
|
|
3209
3462
|
appName: input.appName,
|
|
3210
3463
|
extraContext: [
|
|
3211
3464
|
"File-based detection was uncertain or incomplete.",
|
|
3465
|
+
dirs.length > 1 ? `This sandbox also has folders: ${dirs.slice(1).join(", ")}` : "",
|
|
3212
3466
|
...reasons
|
|
3213
|
-
].join("\n")
|
|
3467
|
+
].filter(Boolean).join("\n")
|
|
3214
3468
|
});
|
|
3215
3469
|
usedAi = true;
|
|
3216
3470
|
const fromAi = appsFromInspectPorts(
|
|
@@ -3219,9 +3473,12 @@ async function proposeHostAppsFromConfig(input) {
|
|
|
3219
3473
|
inspected.name || input.appName || "App"
|
|
3220
3474
|
);
|
|
3221
3475
|
if (fromAi.length) {
|
|
3222
|
-
apps =
|
|
3223
|
-
|
|
3224
|
-
|
|
3476
|
+
apps = unionHostApps(
|
|
3477
|
+
ensureAiServerApp(
|
|
3478
|
+
[defaultAiServerApp(preferredAi), ...fromAi],
|
|
3479
|
+
preferredAi
|
|
3480
|
+
),
|
|
3481
|
+
detected.apps
|
|
3225
3482
|
);
|
|
3226
3483
|
}
|
|
3227
3484
|
if (inspected.summary) projectSummary = inspected.summary;
|
|
@@ -3234,9 +3491,9 @@ async function proposeHostAppsFromConfig(input) {
|
|
|
3234
3491
|
);
|
|
3235
3492
|
}
|
|
3236
3493
|
}
|
|
3237
|
-
const alternatives =
|
|
3238
|
-
appName: input.appName
|
|
3239
|
-
|
|
3494
|
+
const alternatives = dirs.flatMap(
|
|
3495
|
+
(dir) => collectScriptAlternatives(dir, apps, { appName: input.appName })
|
|
3496
|
+
);
|
|
3240
3497
|
const confidence = proposalConfidence(apps, reasons, usedAi);
|
|
3241
3498
|
const needsReview = confidence !== "high" || detected.confused || apps.filter((app) => app.role !== "ai-server").length === 0;
|
|
3242
3499
|
return {
|
|
@@ -3251,11 +3508,12 @@ async function proposeHostAppsFromConfig(input) {
|
|
|
3251
3508
|
};
|
|
3252
3509
|
}
|
|
3253
3510
|
async function resolveHostApps(input) {
|
|
3254
|
-
const
|
|
3511
|
+
const dirs = uniqueWorkspaceDirs(input.workspaceDir, input.workspaceDirs);
|
|
3512
|
+
const folder = dirs[0] || path8.resolve(input.workspaceDir);
|
|
3255
3513
|
const loc = { sandboxId: input.sandboxId, dataDir: input.dataDir };
|
|
3256
|
-
const fingerprint =
|
|
3514
|
+
const fingerprint = hostAppsFingerprintForFolders(dirs);
|
|
3257
3515
|
const preferredAi = parsePort(input.preferredAiPort, AI_SERVER_DEFAULT_PORT);
|
|
3258
|
-
const detected =
|
|
3516
|
+
const detected = detectHostAppsFromFolders(dirs, {
|
|
3259
3517
|
preferredAiPort: preferredAi,
|
|
3260
3518
|
appName: input.appName
|
|
3261
3519
|
});
|
|
@@ -3390,6 +3648,7 @@ export {
|
|
|
3390
3648
|
createToolValidator,
|
|
3391
3649
|
defaultAiServerApp,
|
|
3392
3650
|
detectHostAppsFromFiles,
|
|
3651
|
+
detectHostAppsFromFolders,
|
|
3393
3652
|
ensureAiServerApp,
|
|
3394
3653
|
ensureProjectDataDir,
|
|
3395
3654
|
ensureSingleHost,
|
|
@@ -3399,6 +3658,7 @@ export {
|
|
|
3399
3658
|
getProviderPreference,
|
|
3400
3659
|
hostAppsCachePath2 as hostAppsCachePath,
|
|
3401
3660
|
hostAppsFingerprint,
|
|
3661
|
+
hostAppsFingerprintForFolders,
|
|
3402
3662
|
inspectAndRepairWorkspace,
|
|
3403
3663
|
inspectConfigOnly,
|
|
3404
3664
|
isDevMode,
|
|
@@ -3433,6 +3693,7 @@ export {
|
|
|
3433
3693
|
sanitizeProjectId,
|
|
3434
3694
|
saveChatAttachments,
|
|
3435
3695
|
toNextRoute,
|
|
3696
|
+
unionHostApps,
|
|
3436
3697
|
upsertManagedIgnoreFile,
|
|
3437
3698
|
writeCollaboraterApps,
|
|
3438
3699
|
writeHostAppsCache
|