@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 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(64).optional(),
2605
- backend: import_zod.z.string().min(1).max(64).optional(),
2606
- app: import_zod.z.string().min(1).max(64).optional()
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. Omit chat/ai-server 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": "dev:server" },
2748
- "ports": { "ui": 5173, "backend": 4100 },
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
- function hostAppsFingerprint(folder) {
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 (0, import_node_crypto4.createHash)("sha256").update(parts.join("|") || resolved).digest("hex").slice(0, 32);
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) => ({ file: layer.file, port: parsePort(layer.values.PORT) })).filter((row) => row.port),
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(merged.API_PORT || merged.API_URL || merged.VITE_API_URL)
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 hostApps = apps.filter((app) => app.role !== "ai-server");
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(apps, aiPort),
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 ? `npm run ${scripts.ui}` : null,
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, 4100),
3214
- startCommand: scripts.backend ? `npm run ${scripts.backend}` : null,
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 ? `npm run ${scripts.app}` : null,
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 folder = import_node_path8.default.resolve(input.workspaceDir);
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 = hostAppsFingerprint(folder);
3560
+ const fingerprint = hostAppsFingerprintForFolders(dirs);
3305
3561
  const allowAi = input.allowAi !== false;
3306
- const detected = detectHostAppsFromFiles(folder, {
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 = ensureAiServerApp(
3336
- [defaultAiServerApp(preferredAi), ...fromAi],
3337
- preferredAi
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 = collectScriptAlternatives(folder, apps, {
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 folder = import_node_path8.default.resolve(input.workspaceDir);
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 = hostAppsFingerprint(folder);
3630
+ const fingerprint = hostAppsFingerprintForFolders(dirs);
3370
3631
  const preferredAi = parsePort(input.preferredAiPort, AI_SERVER_DEFAULT_PORT);
3371
- const detected = detectHostAppsFromFiles(folder, {
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(64).optional(),
2492
- backend: z.string().min(1).max(64).optional(),
2493
- app: z.string().min(1).max(64).optional()
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. Omit chat/ai-server 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": "dev:server" },
2635
- "ports": { "ui": 5173, "backend": 4100 },
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
- function hostAppsFingerprint(folder) {
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 createHash2("sha256").update(parts.join("|") || resolved).digest("hex").slice(0, 32);
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) => ({ file: layer.file, port: parsePort(layer.values.PORT) })).filter((row) => row.port),
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(merged.API_PORT || merged.API_URL || merged.VITE_API_URL)
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 hostApps = apps.filter((app) => app.role !== "ai-server");
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(apps, aiPort),
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 ? `npm run ${scripts.ui}` : null,
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, 4100),
3101
- startCommand: scripts.backend ? `npm run ${scripts.backend}` : null,
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 ? `npm run ${scripts.app}` : null,
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 folder = path8.resolve(input.workspaceDir);
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 = hostAppsFingerprint(folder);
3444
+ const fingerprint = hostAppsFingerprintForFolders(dirs);
3192
3445
  const allowAi = input.allowAi !== false;
3193
- const detected = detectHostAppsFromFiles(folder, {
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 = ensureAiServerApp(
3223
- [defaultAiServerApp(preferredAi), ...fromAi],
3224
- preferredAi
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 = collectScriptAlternatives(folder, apps, {
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 folder = path8.resolve(input.workspaceDir);
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 = hostAppsFingerprint(folder);
3514
+ const fingerprint = hostAppsFingerprintForFolders(dirs);
3257
3515
  const preferredAi = parsePort(input.preferredAiPort, AI_SERVER_DEFAULT_PORT);
3258
- const detected = detectHostAppsFromFiles(folder, {
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maintainer-pro/ai-cli",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "description": "Maintainer Pro server SDK: Claude/Cursor CLI, chat HTTP handlers, and optional SQL persistence",
5
5
  "keywords": [
6
6
  "maintainer-pro",