@maintainer-pro/ai-cli 0.1.8 → 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,
@@ -79,7 +81,6 @@ __export(index_exports, {
79
81
  isPathAllowed: () => isPathAllowed,
80
82
  maintainerProHome: () => maintainerProHome,
81
83
  mergeDesiredHostApps: () => mergeDesiredHostApps,
82
- normalizeEnvMaps: () => normalizeEnvMaps,
83
84
  normalizeHostApp: () => normalizeHostApp,
84
85
  normalizeHostApps: () => normalizeHostApps,
85
86
  normalizeIgnorePaths: () => normalizeIgnorePaths,
@@ -106,6 +107,7 @@ __export(index_exports, {
106
107
  sanitizeProjectId: () => sanitizeProjectId,
107
108
  saveChatAttachments: () => saveChatAttachments,
108
109
  toNextRoute: () => toNextRoute,
110
+ unionHostApps: () => unionHostApps,
109
111
  upsertManagedIgnoreFile: () => upsertManagedIgnoreFile,
110
112
  writeCollaboraterApps: () => writeCollaboraterApps,
111
113
  writeHostAppsCache: () => writeHostAppsCache
@@ -2602,9 +2604,9 @@ var inspectJsonSchema = import_zod.z.object({
2602
2604
  name: import_zod.z.string().max(200).optional(),
2603
2605
  summary: import_zod.z.string().max(800).optional(),
2604
2606
  scripts: import_zod.z.object({
2605
- ui: import_zod.z.string().min(1).max(64).optional(),
2606
- backend: import_zod.z.string().min(1).max(64).optional(),
2607
- 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()
2608
2610
  }).optional(),
2609
2611
  ports: import_zod.z.object({
2610
2612
  ui: import_zod.z.number().int().min(1).max(65535).optional(),
@@ -2632,12 +2634,12 @@ function setupSystemPrompt(input) {
2632
2634
  Workspace: ${input.workspaceDir}
2633
2635
  App name: ${input.appName || "the app"}
2634
2636
 
2635
- 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).
2636
2638
  You MAY edit files to fix setup problems that would block local run or the chat widget (env keys, widget mount, missing config).
2637
2639
  Do NOT start long-lived dev servers, tunnels, or install global tools.
2638
2640
  Do NOT run \`npm run dev\` or similar.
2639
2641
 
2640
- 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.
2641
2643
 
2642
2644
  End with a short human summary AND one \`\`\`json fence with:
2643
2645
 
@@ -2709,7 +2711,21 @@ ${lock}`);
2709
2711
  "vite.config.mjs",
2710
2712
  "next.config.js",
2711
2713
  "next.config.mjs",
2712
- "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"
2713
2729
  ]) {
2714
2730
  const body = readSnippet(import_node_path7.default.join(folder, rel), 2500);
2715
2731
  if (body) parts.push(`### ${rel}
@@ -2737,7 +2753,7 @@ DO NOT edit, create, or delete any files.
2737
2753
  DO NOT run shell commands, install packages, or start servers.
2738
2754
  Only infer how this project runs locally from the excerpts.
2739
2755
 
2740
- 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.
2741
2757
 
2742
2758
  Reply with a short human summary AND one \`\`\`json fence with:
2743
2759
 
@@ -2745,8 +2761,8 @@ Reply with a short human summary AND one \`\`\`json fence with:
2745
2761
  "kind": "next" | "vite" | "html" | "empty" | "other",
2746
2762
  "name": "package or folder name",
2747
2763
  "summary": "one sentence about this project",
2748
- "scripts": { "ui": "dev:client", "backend": "dev:server" },
2749
- "ports": { "ui": 5173, "backend": 4100 },
2764
+ "scripts": { "ui": "dev:client", "backend": "sbt run" },
2765
+ "ports": { "ui": 5173, "backend": 9000 },
2750
2766
  "issues": ["uncertainties"],
2751
2767
  "fixes": [],
2752
2768
  "ready": true
@@ -2910,25 +2926,9 @@ function normalizeHostApp(raw) {
2910
2926
  ) ? row.source : "manual",
2911
2927
  locked: row.locked === true || id === AI_SERVER_APP_ID || role === "ai-server",
2912
2928
  host: role !== "ai-server" && id !== AI_SERVER_APP_ID && row.host === true,
2913
- envMaps: normalizeEnvMaps(row.envMaps)
2929
+ folderPath: typeof row.folderPath === "string" && row.folderPath.trim() ? row.folderPath.trim().slice(0, 500) : null
2914
2930
  };
2915
2931
  }
2916
- function normalizeEnvMaps(raw) {
2917
- if (!Array.isArray(raw)) return [];
2918
- const seen = /* @__PURE__ */ new Set();
2919
- const out = [];
2920
- for (const item of raw) {
2921
- if (!item || typeof item !== "object") continue;
2922
- const row = item;
2923
- const key = String(row.key || "").trim();
2924
- const sourceAppId = String(row.sourceAppId || "").trim().slice(0, 64);
2925
- if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key) || key.length > 80) continue;
2926
- if (!sourceAppId || seen.has(key)) continue;
2927
- seen.add(key);
2928
- out.push({ key, sourceAppId });
2929
- }
2930
- return out.slice(0, 30);
2931
- }
2932
2932
  function ensureSingleHost(apps) {
2933
2933
  const next = apps.map((app) => ({
2934
2934
  ...app,
@@ -3039,19 +3039,151 @@ function configPort(folder, files) {
3039
3039
  }
3040
3040
  return 0;
3041
3041
  }
3042
- 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) {
3043
3184
  const resolved = import_node_path8.default.resolve(folder);
3044
3185
  const parts = [];
3045
- for (const rel of [
3046
- "package.json",
3047
- ...ENV_FILES,
3048
- "vite.config.ts",
3049
- "vite.config.js",
3050
- "vite.config.mjs",
3051
- "next.config.js",
3052
- "next.config.mjs",
3053
- "next.config.ts"
3054
- ]) {
3186
+ for (const rel of FINGERPRINT_FILES) {
3055
3187
  const file = import_node_path8.default.join(resolved, rel);
3056
3188
  if (!import_node_fs4.default.existsSync(file)) continue;
3057
3189
  try {
@@ -3060,7 +3192,16 @@ function hostAppsFingerprint(folder) {
3060
3192
  } catch {
3061
3193
  }
3062
3194
  }
3063
- 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);
3064
3205
  }
3065
3206
  function pickScript(scripts, names) {
3066
3207
  return names.find((name) => scripts[name] && !isChatScript(name, scripts[name])) || null;
@@ -3079,9 +3220,14 @@ function detectHostAppsFromFiles(folder, opts = {}) {
3079
3220
  source: envAi ? "env" : "default"
3080
3221
  });
3081
3222
  const envPorts = {
3082
- 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),
3083
3227
  APP_URL: parsePort(merged.APP_URL || merged.NEXT_PUBLIC_APP_URL || merged.PUBLIC_URL),
3084
- 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
+ )
3085
3231
  };
3086
3232
  const portValues = [...new Set(envPorts.PORT.map((row) => row.port))];
3087
3233
  if (portValues.length > 1) {
@@ -3126,7 +3272,8 @@ function detectHostAppsFromFiles(folder, opts = {}) {
3126
3272
  port,
3127
3273
  startCommand: `npm run ${uiScript}`,
3128
3274
  source: uiFromEnv ? "env" : scriptPort ? "package" : "package",
3129
- host: true
3275
+ host: true,
3276
+ folderPath: resolved
3130
3277
  });
3131
3278
  } else if (uiFromEnv) {
3132
3279
  apps.push({
@@ -3136,7 +3283,8 @@ function detectHostAppsFromFiles(folder, opts = {}) {
3136
3283
  port: uiFromEnv,
3137
3284
  startCommand: scripts.dev ? "npm run dev" : null,
3138
3285
  source: "env",
3139
- host: true
3286
+ host: true,
3287
+ folderPath: resolved
3140
3288
  });
3141
3289
  }
3142
3290
  if (apiScript && apiScript !== uiScript) {
@@ -3148,19 +3296,83 @@ function detectHostAppsFromFiles(folder, opts = {}) {
3148
3296
  role: "backend",
3149
3297
  port,
3150
3298
  startCommand: `npm run ${apiScript}`,
3151
- source: envPorts.API_PORT ? "env" : "package"
3299
+ source: envPorts.API_PORT ? "env" : "package",
3300
+ folderPath: resolved
3152
3301
  });
3153
3302
  }
3154
- 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");
3155
3323
  if (pkg && Object.keys(scripts).length && hostApps.length === 0) {
3156
3324
  reasons.push("Found package.json scripts but no host app port in env or config");
3157
3325
  }
3158
3326
  return {
3159
- apps: ensureAiServerApp(apps, aiPort),
3327
+ apps: ensureAiServerApp(tagged, aiPort),
3160
3328
  reasons,
3161
3329
  confused: reasons.length > 0
3162
3330
  };
3163
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
+ }
3164
3376
  function readHostAppsCache(folder, extra) {
3165
3377
  try {
3166
3378
  const raw = JSON.parse(
@@ -3205,13 +3417,37 @@ function mergeDesiredHostApps(desired, detected) {
3205
3417
  ...local,
3206
3418
  ...app,
3207
3419
  startCommand: app.startCommand || local?.startCommand || null,
3420
+ folderPath: app.folderPath || local?.folderPath || null,
3208
3421
  locked: app.locked || app.id === AI_SERVER_APP_ID || app.role === "ai-server",
3209
- host: app.role === "ai-server" || app.id === AI_SERVER_APP_ID ? false : app.host === true || app.host !== false && local?.host === true,
3210
- envMaps: app.envMaps && app.envMaps.length ? app.envMaps : local?.envMaps || []
3422
+ host: app.role === "ai-server" || app.id === AI_SERVER_APP_ID ? false : app.host === true || app.host !== false && local?.host === true
3211
3423
  };
3212
3424
  });
3213
3425
  return ensureAiServerApp(merged);
3214
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
+ }
3215
3451
  function appsFromInspectPorts(ports, scripts, name) {
3216
3452
  const apps = [];
3217
3453
  if (ports.ui || scripts.ui) {
@@ -3220,7 +3456,7 @@ function appsFromInspectPorts(ports, scripts, name) {
3220
3456
  name,
3221
3457
  role: "ui",
3222
3458
  port: parsePort(ports.ui, 5173),
3223
- startCommand: scripts.ui ? `npm run ${scripts.ui}` : null,
3459
+ startCommand: startCommandFromScript(scripts.ui),
3224
3460
  source: "ai"
3225
3461
  });
3226
3462
  }
@@ -3229,8 +3465,8 @@ function appsFromInspectPorts(ports, scripts, name) {
3229
3465
  id: "backend",
3230
3466
  name: "Backend",
3231
3467
  role: "backend",
3232
- port: parsePort(ports.backend, 4100),
3233
- startCommand: scripts.backend ? `npm run ${scripts.backend}` : null,
3468
+ port: parsePort(ports.backend, 8080),
3469
+ startCommand: startCommandFromScript(scripts.backend),
3234
3470
  source: "ai"
3235
3471
  });
3236
3472
  }
@@ -3240,7 +3476,7 @@ function appsFromInspectPorts(ports, scripts, name) {
3240
3476
  name,
3241
3477
  role: "app",
3242
3478
  port: parsePort(ports.app, 3e3),
3243
- startCommand: scripts.app ? `npm run ${scripts.app}` : null,
3479
+ startCommand: startCommandFromScript(scripts.app),
3244
3480
  source: "ai"
3245
3481
  });
3246
3482
  }
@@ -3318,11 +3554,12 @@ function projectSummaryFromDetect(folder, apps, opts = {}) {
3318
3554
  return `${name}: ${hostApps.map((app) => `${app.name} on ${app.port}${app.startCommand ? ` (${app.startCommand})` : ""}`).join(", ")}.`;
3319
3555
  }
3320
3556
  async function proposeHostAppsFromConfig(input) {
3321
- 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);
3322
3559
  const preferredAi = parsePort(input.preferredAiPort, AI_SERVER_DEFAULT_PORT);
3323
- const fingerprint = hostAppsFingerprint(folder);
3560
+ const fingerprint = hostAppsFingerprintForFolders(dirs);
3324
3561
  const allowAi = input.allowAi !== false;
3325
- const detected = detectHostAppsFromFiles(folder, {
3562
+ const detected = detectHostAppsFromFolders(dirs, {
3326
3563
  preferredAiPort: preferredAi,
3327
3564
  appName: input.appName
3328
3565
  });
@@ -3341,8 +3578,9 @@ async function proposeHostAppsFromConfig(input) {
3341
3578
  appName: input.appName,
3342
3579
  extraContext: [
3343
3580
  "File-based detection was uncertain or incomplete.",
3581
+ dirs.length > 1 ? `This sandbox also has folders: ${dirs.slice(1).join(", ")}` : "",
3344
3582
  ...reasons
3345
- ].join("\n")
3583
+ ].filter(Boolean).join("\n")
3346
3584
  });
3347
3585
  usedAi = true;
3348
3586
  const fromAi = appsFromInspectPorts(
@@ -3351,9 +3589,12 @@ async function proposeHostAppsFromConfig(input) {
3351
3589
  inspected.name || input.appName || "App"
3352
3590
  );
3353
3591
  if (fromAi.length) {
3354
- apps = ensureAiServerApp(
3355
- [defaultAiServerApp(preferredAi), ...fromAi],
3356
- preferredAi
3592
+ apps = unionHostApps(
3593
+ ensureAiServerApp(
3594
+ [defaultAiServerApp(preferredAi), ...fromAi],
3595
+ preferredAi
3596
+ ),
3597
+ detected.apps
3357
3598
  );
3358
3599
  }
3359
3600
  if (inspected.summary) projectSummary = inspected.summary;
@@ -3366,9 +3607,9 @@ async function proposeHostAppsFromConfig(input) {
3366
3607
  );
3367
3608
  }
3368
3609
  }
3369
- const alternatives = collectScriptAlternatives(folder, apps, {
3370
- appName: input.appName
3371
- });
3610
+ const alternatives = dirs.flatMap(
3611
+ (dir) => collectScriptAlternatives(dir, apps, { appName: input.appName })
3612
+ );
3372
3613
  const confidence = proposalConfidence(apps, reasons, usedAi);
3373
3614
  const needsReview = confidence !== "high" || detected.confused || apps.filter((app) => app.role !== "ai-server").length === 0;
3374
3615
  return {
@@ -3383,11 +3624,12 @@ async function proposeHostAppsFromConfig(input) {
3383
3624
  };
3384
3625
  }
3385
3626
  async function resolveHostApps(input) {
3386
- 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);
3387
3629
  const loc = { sandboxId: input.sandboxId, dataDir: input.dataDir };
3388
- const fingerprint = hostAppsFingerprint(folder);
3630
+ const fingerprint = hostAppsFingerprintForFolders(dirs);
3389
3631
  const preferredAi = parsePort(input.preferredAiPort, AI_SERVER_DEFAULT_PORT);
3390
- const detected = detectHostAppsFromFiles(folder, {
3632
+ const detected = detectHostAppsFromFolders(dirs, {
3391
3633
  preferredAiPort: preferredAi,
3392
3634
  appName: input.appName
3393
3635
  });
@@ -3523,6 +3765,7 @@ Return the real local ports and npm script names.`
3523
3765
  createToolValidator,
3524
3766
  defaultAiServerApp,
3525
3767
  detectHostAppsFromFiles,
3768
+ detectHostAppsFromFolders,
3526
3769
  ensureAiServerApp,
3527
3770
  ensureProjectDataDir,
3528
3771
  ensureSingleHost,
@@ -3532,6 +3775,7 @@ Return the real local ports and npm script names.`
3532
3775
  getProviderPreference,
3533
3776
  hostAppsCachePath,
3534
3777
  hostAppsFingerprint,
3778
+ hostAppsFingerprintForFolders,
3535
3779
  inspectAndRepairWorkspace,
3536
3780
  inspectConfigOnly,
3537
3781
  isDevMode,
@@ -3540,7 +3784,6 @@ Return the real local ports and npm script names.`
3540
3784
  isPathAllowed,
3541
3785
  maintainerProHome,
3542
3786
  mergeDesiredHostApps,
3543
- normalizeEnvMaps,
3544
3787
  normalizeHostApp,
3545
3788
  normalizeHostApps,
3546
3789
  normalizeIgnorePaths,
@@ -3567,6 +3810,7 @@ Return the real local ports and npm script names.`
3567
3810
  sanitizeProjectId,
3568
3811
  saveChatAttachments,
3569
3812
  toNextRoute,
3813
+ unionHostApps,
3570
3814
  upsertManagedIgnoreFile,
3571
3815
  writeCollaboraterApps,
3572
3816
  writeHostAppsCache
package/dist/index.d.cts CHANGED
@@ -528,10 +528,6 @@ declare const AI_SERVER_APP_ID = "ai-server";
528
528
  declare const AI_SERVER_DEFAULT_PORT = 3100;
529
529
  type HostAppRole = "ai-server" | "ui" | "backend" | "app" | "custom";
530
530
  type HostAppSource = "default" | "env" | "package" | "ai" | "manual";
531
- type HostEnvMap = {
532
- key: string;
533
- sourceAppId: string;
534
- };
535
531
  type HostApp = {
536
532
  id: string;
537
533
  name: string;
@@ -540,12 +536,15 @@ type HostApp = {
540
536
  startCommand?: string | null;
541
537
  source: HostAppSource;
542
538
  locked?: boolean;
543
- /** Share URL / CORS UI — the page the browser opens. */
539
+ /** Share URL — the page the browser opens. */
544
540
  host?: boolean;
545
- envMaps?: HostEnvMap[];
541
+ /** Project folder this process should start in (multi-folder sandboxes). */
542
+ folderPath?: string | null;
546
543
  };
547
544
  type HostAppsResolveInput = {
548
545
  workspaceDir: string;
546
+ /** Extra attached folders for the same sandbox (UI + JVM backend, etc.). */
547
+ workspaceDirs?: string[];
549
548
  appName?: string;
550
549
  preferredAiPort?: number;
551
550
  /** Desired list from Maintainer Pro — used as source of truth when present. */
@@ -587,6 +586,8 @@ type SetupProposal = {
587
586
  };
588
587
  type ProposeHostAppsInput = {
589
588
  workspaceDir: string;
589
+ /** Extra attached folders for the same sandbox. */
590
+ workspaceDirs?: string[];
590
591
  appName?: string;
591
592
  preferredAiPort?: number;
592
593
  /**
@@ -602,7 +603,6 @@ declare function hostAppsCachePath(folder: string, extra?: {
602
603
  }): string;
603
604
  declare function defaultAiServerApp(port?: number): HostApp;
604
605
  declare function normalizeHostApp(raw: unknown): HostApp | null;
605
- declare function normalizeEnvMaps(raw: unknown): HostEnvMap[];
606
606
  declare function ensureSingleHost(apps: HostApp[]): HostApp[];
607
607
  declare function normalizeHostApps(raw: unknown): HostApp[];
608
608
  declare function ensureAiServerApp(apps: HostApp[], preferredPort?: number): HostApp[];
@@ -614,6 +614,7 @@ declare function readProjectEnvLayers(folder: string): {
614
614
  }>;
615
615
  };
616
616
  declare function hostAppsFingerprint(folder: string): string;
617
+ declare function hostAppsFingerprintForFolders(folders: string[]): string;
617
618
  declare function detectHostAppsFromFiles(folder: string, opts?: {
618
619
  preferredAiPort?: number;
619
620
  appName?: string;
@@ -622,6 +623,14 @@ declare function detectHostAppsFromFiles(folder: string, opts?: {
622
623
  reasons: string[];
623
624
  confused: boolean;
624
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
+ };
625
634
  declare function readHostAppsCache(folder: string, extra?: {
626
635
  sandboxId?: string | null;
627
636
  dataDir?: string | null;
@@ -639,6 +648,8 @@ declare function writeHostAppsCache(folder: string, apps: HostApp[], extra?: Rec
639
648
  /** @deprecated Use writeHostAppsCache. */
640
649
  declare const writeCollaboraterApps: typeof writeHostAppsCache;
641
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[];
642
653
  /**
643
654
  * Fast, interactive onboarding propose: file detect first, optional read-only AI.
644
655
  * Never edits the repository.
@@ -651,4 +662,4 @@ declare function proposeHostAppsFromConfig(input: ProposeHostAppsInput): Promise
651
662
  */
652
663
  declare function resolveHostApps(input: HostAppsResolveInput): Promise<HostAppsResolveResult>;
653
664
 
654
- 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 HostEnvMap, 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, normalizeEnvMaps, 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
@@ -528,10 +528,6 @@ declare const AI_SERVER_APP_ID = "ai-server";
528
528
  declare const AI_SERVER_DEFAULT_PORT = 3100;
529
529
  type HostAppRole = "ai-server" | "ui" | "backend" | "app" | "custom";
530
530
  type HostAppSource = "default" | "env" | "package" | "ai" | "manual";
531
- type HostEnvMap = {
532
- key: string;
533
- sourceAppId: string;
534
- };
535
531
  type HostApp = {
536
532
  id: string;
537
533
  name: string;
@@ -540,12 +536,15 @@ type HostApp = {
540
536
  startCommand?: string | null;
541
537
  source: HostAppSource;
542
538
  locked?: boolean;
543
- /** Share URL / CORS UI — the page the browser opens. */
539
+ /** Share URL — the page the browser opens. */
544
540
  host?: boolean;
545
- envMaps?: HostEnvMap[];
541
+ /** Project folder this process should start in (multi-folder sandboxes). */
542
+ folderPath?: string | null;
546
543
  };
547
544
  type HostAppsResolveInput = {
548
545
  workspaceDir: string;
546
+ /** Extra attached folders for the same sandbox (UI + JVM backend, etc.). */
547
+ workspaceDirs?: string[];
549
548
  appName?: string;
550
549
  preferredAiPort?: number;
551
550
  /** Desired list from Maintainer Pro — used as source of truth when present. */
@@ -587,6 +586,8 @@ type SetupProposal = {
587
586
  };
588
587
  type ProposeHostAppsInput = {
589
588
  workspaceDir: string;
589
+ /** Extra attached folders for the same sandbox. */
590
+ workspaceDirs?: string[];
590
591
  appName?: string;
591
592
  preferredAiPort?: number;
592
593
  /**
@@ -602,7 +603,6 @@ declare function hostAppsCachePath(folder: string, extra?: {
602
603
  }): string;
603
604
  declare function defaultAiServerApp(port?: number): HostApp;
604
605
  declare function normalizeHostApp(raw: unknown): HostApp | null;
605
- declare function normalizeEnvMaps(raw: unknown): HostEnvMap[];
606
606
  declare function ensureSingleHost(apps: HostApp[]): HostApp[];
607
607
  declare function normalizeHostApps(raw: unknown): HostApp[];
608
608
  declare function ensureAiServerApp(apps: HostApp[], preferredPort?: number): HostApp[];
@@ -614,6 +614,7 @@ declare function readProjectEnvLayers(folder: string): {
614
614
  }>;
615
615
  };
616
616
  declare function hostAppsFingerprint(folder: string): string;
617
+ declare function hostAppsFingerprintForFolders(folders: string[]): string;
617
618
  declare function detectHostAppsFromFiles(folder: string, opts?: {
618
619
  preferredAiPort?: number;
619
620
  appName?: string;
@@ -622,6 +623,14 @@ declare function detectHostAppsFromFiles(folder: string, opts?: {
622
623
  reasons: string[];
623
624
  confused: boolean;
624
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
+ };
625
634
  declare function readHostAppsCache(folder: string, extra?: {
626
635
  sandboxId?: string | null;
627
636
  dataDir?: string | null;
@@ -639,6 +648,8 @@ declare function writeHostAppsCache(folder: string, apps: HostApp[], extra?: Rec
639
648
  /** @deprecated Use writeHostAppsCache. */
640
649
  declare const writeCollaboraterApps: typeof writeHostAppsCache;
641
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[];
642
653
  /**
643
654
  * Fast, interactive onboarding propose: file detect first, optional read-only AI.
644
655
  * Never edits the repository.
@@ -651,4 +662,4 @@ declare function proposeHostAppsFromConfig(input: ProposeHostAppsInput): Promise
651
662
  */
652
663
  declare function resolveHostApps(input: HostAppsResolveInput): Promise<HostAppsResolveResult>;
653
664
 
654
- 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 HostEnvMap, 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, normalizeEnvMaps, 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
@@ -2796,25 +2810,9 @@ function normalizeHostApp(raw) {
2796
2810
  ) ? row.source : "manual",
2797
2811
  locked: row.locked === true || id === AI_SERVER_APP_ID || role === "ai-server",
2798
2812
  host: role !== "ai-server" && id !== AI_SERVER_APP_ID && row.host === true,
2799
- envMaps: normalizeEnvMaps(row.envMaps)
2813
+ folderPath: typeof row.folderPath === "string" && row.folderPath.trim() ? row.folderPath.trim().slice(0, 500) : null
2800
2814
  };
2801
2815
  }
2802
- function normalizeEnvMaps(raw) {
2803
- if (!Array.isArray(raw)) return [];
2804
- const seen = /* @__PURE__ */ new Set();
2805
- const out = [];
2806
- for (const item of raw) {
2807
- if (!item || typeof item !== "object") continue;
2808
- const row = item;
2809
- const key = String(row.key || "").trim();
2810
- const sourceAppId = String(row.sourceAppId || "").trim().slice(0, 64);
2811
- if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key) || key.length > 80) continue;
2812
- if (!sourceAppId || seen.has(key)) continue;
2813
- seen.add(key);
2814
- out.push({ key, sourceAppId });
2815
- }
2816
- return out.slice(0, 30);
2817
- }
2818
2816
  function ensureSingleHost(apps) {
2819
2817
  const next = apps.map((app) => ({
2820
2818
  ...app,
@@ -2925,19 +2923,151 @@ function configPort(folder, files) {
2925
2923
  }
2926
2924
  return 0;
2927
2925
  }
2928
- 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) {
2929
3068
  const resolved = path8.resolve(folder);
2930
3069
  const parts = [];
2931
- for (const rel of [
2932
- "package.json",
2933
- ...ENV_FILES,
2934
- "vite.config.ts",
2935
- "vite.config.js",
2936
- "vite.config.mjs",
2937
- "next.config.js",
2938
- "next.config.mjs",
2939
- "next.config.ts"
2940
- ]) {
3070
+ for (const rel of FINGERPRINT_FILES) {
2941
3071
  const file = path8.join(resolved, rel);
2942
3072
  if (!fs7.existsSync(file)) continue;
2943
3073
  try {
@@ -2946,7 +3076,16 @@ function hostAppsFingerprint(folder) {
2946
3076
  } catch {
2947
3077
  }
2948
3078
  }
2949
- 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);
2950
3089
  }
2951
3090
  function pickScript(scripts, names) {
2952
3091
  return names.find((name) => scripts[name] && !isChatScript(name, scripts[name])) || null;
@@ -2965,9 +3104,14 @@ function detectHostAppsFromFiles(folder, opts = {}) {
2965
3104
  source: envAi ? "env" : "default"
2966
3105
  });
2967
3106
  const envPorts = {
2968
- 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),
2969
3111
  APP_URL: parsePort(merged.APP_URL || merged.NEXT_PUBLIC_APP_URL || merged.PUBLIC_URL),
2970
- 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
+ )
2971
3115
  };
2972
3116
  const portValues = [...new Set(envPorts.PORT.map((row) => row.port))];
2973
3117
  if (portValues.length > 1) {
@@ -3012,7 +3156,8 @@ function detectHostAppsFromFiles(folder, opts = {}) {
3012
3156
  port,
3013
3157
  startCommand: `npm run ${uiScript}`,
3014
3158
  source: uiFromEnv ? "env" : scriptPort ? "package" : "package",
3015
- host: true
3159
+ host: true,
3160
+ folderPath: resolved
3016
3161
  });
3017
3162
  } else if (uiFromEnv) {
3018
3163
  apps.push({
@@ -3022,7 +3167,8 @@ function detectHostAppsFromFiles(folder, opts = {}) {
3022
3167
  port: uiFromEnv,
3023
3168
  startCommand: scripts.dev ? "npm run dev" : null,
3024
3169
  source: "env",
3025
- host: true
3170
+ host: true,
3171
+ folderPath: resolved
3026
3172
  });
3027
3173
  }
3028
3174
  if (apiScript && apiScript !== uiScript) {
@@ -3034,19 +3180,83 @@ function detectHostAppsFromFiles(folder, opts = {}) {
3034
3180
  role: "backend",
3035
3181
  port,
3036
3182
  startCommand: `npm run ${apiScript}`,
3037
- source: envPorts.API_PORT ? "env" : "package"
3183
+ source: envPorts.API_PORT ? "env" : "package",
3184
+ folderPath: resolved
3038
3185
  });
3039
3186
  }
3040
- 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");
3041
3207
  if (pkg && Object.keys(scripts).length && hostApps.length === 0) {
3042
3208
  reasons.push("Found package.json scripts but no host app port in env or config");
3043
3209
  }
3044
3210
  return {
3045
- apps: ensureAiServerApp(apps, aiPort),
3211
+ apps: ensureAiServerApp(tagged, aiPort),
3046
3212
  reasons,
3047
3213
  confused: reasons.length > 0
3048
3214
  };
3049
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
+ }
3050
3260
  function readHostAppsCache(folder, extra) {
3051
3261
  try {
3052
3262
  const raw = JSON.parse(
@@ -3091,13 +3301,37 @@ function mergeDesiredHostApps(desired, detected) {
3091
3301
  ...local,
3092
3302
  ...app,
3093
3303
  startCommand: app.startCommand || local?.startCommand || null,
3304
+ folderPath: app.folderPath || local?.folderPath || null,
3094
3305
  locked: app.locked || app.id === AI_SERVER_APP_ID || app.role === "ai-server",
3095
- host: app.role === "ai-server" || app.id === AI_SERVER_APP_ID ? false : app.host === true || app.host !== false && local?.host === true,
3096
- envMaps: app.envMaps && app.envMaps.length ? app.envMaps : local?.envMaps || []
3306
+ host: app.role === "ai-server" || app.id === AI_SERVER_APP_ID ? false : app.host === true || app.host !== false && local?.host === true
3097
3307
  };
3098
3308
  });
3099
3309
  return ensureAiServerApp(merged);
3100
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
+ }
3101
3335
  function appsFromInspectPorts(ports, scripts, name) {
3102
3336
  const apps = [];
3103
3337
  if (ports.ui || scripts.ui) {
@@ -3106,7 +3340,7 @@ function appsFromInspectPorts(ports, scripts, name) {
3106
3340
  name,
3107
3341
  role: "ui",
3108
3342
  port: parsePort(ports.ui, 5173),
3109
- startCommand: scripts.ui ? `npm run ${scripts.ui}` : null,
3343
+ startCommand: startCommandFromScript(scripts.ui),
3110
3344
  source: "ai"
3111
3345
  });
3112
3346
  }
@@ -3115,8 +3349,8 @@ function appsFromInspectPorts(ports, scripts, name) {
3115
3349
  id: "backend",
3116
3350
  name: "Backend",
3117
3351
  role: "backend",
3118
- port: parsePort(ports.backend, 4100),
3119
- startCommand: scripts.backend ? `npm run ${scripts.backend}` : null,
3352
+ port: parsePort(ports.backend, 8080),
3353
+ startCommand: startCommandFromScript(scripts.backend),
3120
3354
  source: "ai"
3121
3355
  });
3122
3356
  }
@@ -3126,7 +3360,7 @@ function appsFromInspectPorts(ports, scripts, name) {
3126
3360
  name,
3127
3361
  role: "app",
3128
3362
  port: parsePort(ports.app, 3e3),
3129
- startCommand: scripts.app ? `npm run ${scripts.app}` : null,
3363
+ startCommand: startCommandFromScript(scripts.app),
3130
3364
  source: "ai"
3131
3365
  });
3132
3366
  }
@@ -3204,11 +3438,12 @@ function projectSummaryFromDetect(folder, apps, opts = {}) {
3204
3438
  return `${name}: ${hostApps.map((app) => `${app.name} on ${app.port}${app.startCommand ? ` (${app.startCommand})` : ""}`).join(", ")}.`;
3205
3439
  }
3206
3440
  async function proposeHostAppsFromConfig(input) {
3207
- const folder = path8.resolve(input.workspaceDir);
3441
+ const dirs = uniqueWorkspaceDirs(input.workspaceDir, input.workspaceDirs);
3442
+ const folder = dirs[0] || path8.resolve(input.workspaceDir);
3208
3443
  const preferredAi = parsePort(input.preferredAiPort, AI_SERVER_DEFAULT_PORT);
3209
- const fingerprint = hostAppsFingerprint(folder);
3444
+ const fingerprint = hostAppsFingerprintForFolders(dirs);
3210
3445
  const allowAi = input.allowAi !== false;
3211
- const detected = detectHostAppsFromFiles(folder, {
3446
+ const detected = detectHostAppsFromFolders(dirs, {
3212
3447
  preferredAiPort: preferredAi,
3213
3448
  appName: input.appName
3214
3449
  });
@@ -3227,8 +3462,9 @@ async function proposeHostAppsFromConfig(input) {
3227
3462
  appName: input.appName,
3228
3463
  extraContext: [
3229
3464
  "File-based detection was uncertain or incomplete.",
3465
+ dirs.length > 1 ? `This sandbox also has folders: ${dirs.slice(1).join(", ")}` : "",
3230
3466
  ...reasons
3231
- ].join("\n")
3467
+ ].filter(Boolean).join("\n")
3232
3468
  });
3233
3469
  usedAi = true;
3234
3470
  const fromAi = appsFromInspectPorts(
@@ -3237,9 +3473,12 @@ async function proposeHostAppsFromConfig(input) {
3237
3473
  inspected.name || input.appName || "App"
3238
3474
  );
3239
3475
  if (fromAi.length) {
3240
- apps = ensureAiServerApp(
3241
- [defaultAiServerApp(preferredAi), ...fromAi],
3242
- preferredAi
3476
+ apps = unionHostApps(
3477
+ ensureAiServerApp(
3478
+ [defaultAiServerApp(preferredAi), ...fromAi],
3479
+ preferredAi
3480
+ ),
3481
+ detected.apps
3243
3482
  );
3244
3483
  }
3245
3484
  if (inspected.summary) projectSummary = inspected.summary;
@@ -3252,9 +3491,9 @@ async function proposeHostAppsFromConfig(input) {
3252
3491
  );
3253
3492
  }
3254
3493
  }
3255
- const alternatives = collectScriptAlternatives(folder, apps, {
3256
- appName: input.appName
3257
- });
3494
+ const alternatives = dirs.flatMap(
3495
+ (dir) => collectScriptAlternatives(dir, apps, { appName: input.appName })
3496
+ );
3258
3497
  const confidence = proposalConfidence(apps, reasons, usedAi);
3259
3498
  const needsReview = confidence !== "high" || detected.confused || apps.filter((app) => app.role !== "ai-server").length === 0;
3260
3499
  return {
@@ -3269,11 +3508,12 @@ async function proposeHostAppsFromConfig(input) {
3269
3508
  };
3270
3509
  }
3271
3510
  async function resolveHostApps(input) {
3272
- const folder = path8.resolve(input.workspaceDir);
3511
+ const dirs = uniqueWorkspaceDirs(input.workspaceDir, input.workspaceDirs);
3512
+ const folder = dirs[0] || path8.resolve(input.workspaceDir);
3273
3513
  const loc = { sandboxId: input.sandboxId, dataDir: input.dataDir };
3274
- const fingerprint = hostAppsFingerprint(folder);
3514
+ const fingerprint = hostAppsFingerprintForFolders(dirs);
3275
3515
  const preferredAi = parsePort(input.preferredAiPort, AI_SERVER_DEFAULT_PORT);
3276
- const detected = detectHostAppsFromFiles(folder, {
3516
+ const detected = detectHostAppsFromFolders(dirs, {
3277
3517
  preferredAiPort: preferredAi,
3278
3518
  appName: input.appName
3279
3519
  });
@@ -3408,6 +3648,7 @@ export {
3408
3648
  createToolValidator,
3409
3649
  defaultAiServerApp,
3410
3650
  detectHostAppsFromFiles,
3651
+ detectHostAppsFromFolders,
3411
3652
  ensureAiServerApp,
3412
3653
  ensureProjectDataDir,
3413
3654
  ensureSingleHost,
@@ -3417,6 +3658,7 @@ export {
3417
3658
  getProviderPreference,
3418
3659
  hostAppsCachePath2 as hostAppsCachePath,
3419
3660
  hostAppsFingerprint,
3661
+ hostAppsFingerprintForFolders,
3420
3662
  inspectAndRepairWorkspace,
3421
3663
  inspectConfigOnly,
3422
3664
  isDevMode,
@@ -3425,7 +3667,6 @@ export {
3425
3667
  isPathAllowed,
3426
3668
  maintainerProHome,
3427
3669
  mergeDesiredHostApps,
3428
- normalizeEnvMaps,
3429
3670
  normalizeHostApp,
3430
3671
  normalizeHostApps,
3431
3672
  normalizeIgnorePaths,
@@ -3452,6 +3693,7 @@ export {
3452
3693
  sanitizeProjectId,
3453
3694
  saveChatAttachments,
3454
3695
  toNextRoute,
3696
+ unionHostApps,
3455
3697
  upsertManagedIgnoreFile,
3456
3698
  writeCollaboraterApps,
3457
3699
  writeHostAppsCache
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maintainer-pro/ai-cli",
3
- "version": "0.1.8",
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",