@maintainer-pro/ai-cli 0.1.9 → 0.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs 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,8 +72,10 @@ __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,
78
+ isAiServerApp: () => isAiServerApp,
76
79
  isDevMode: () => isDevMode,
77
80
  isIgnoredRelative: () => isIgnoredRelative,
78
81
  isInsideWorkspace: () => isInsideWorkspace,
@@ -105,7 +108,9 @@ __export(index_exports, {
105
108
  sanitizeProjectId: () => sanitizeProjectId,
106
109
  saveChatAttachments: () => saveChatAttachments,
107
110
  toNextRoute: () => toNextRoute,
111
+ unionHostApps: () => unionHostApps,
108
112
  upsertManagedIgnoreFile: () => upsertManagedIgnoreFile,
113
+ withoutAiServerApp: () => withoutAiServerApp,
109
114
  writeCollaboraterApps: () => writeCollaboraterApps,
110
115
  writeHostAppsCache: () => writeHostAppsCache
111
116
  });
@@ -2601,9 +2606,9 @@ var inspectJsonSchema = import_zod.z.object({
2601
2606
  name: import_zod.z.string().max(200).optional(),
2602
2607
  summary: import_zod.z.string().max(800).optional(),
2603
2608
  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()
2609
+ ui: import_zod.z.string().min(1).max(200).optional(),
2610
+ backend: import_zod.z.string().min(1).max(200).optional(),
2611
+ app: import_zod.z.string().min(1).max(200).optional()
2607
2612
  }).optional(),
2608
2613
  ports: import_zod.z.object({
2609
2614
  ui: import_zod.z.number().int().min(1).max(65535).optional(),
@@ -2631,12 +2636,12 @@ function setupSystemPrompt(input) {
2631
2636
  Workspace: ${input.workspaceDir}
2632
2637
  App name: ${input.appName || "the app"}
2633
2638
 
2634
- Inspect this repository (package.json, README, lockfiles, existing env files, how the UI and API start).
2639
+ Inspect this repository (package.json, README, lockfiles, Maven/Gradle/sbt files, Spring/Play config, existing env files, how the UI and API start).
2635
2640
  You MAY edit files to fix setup problems that would block local run or the chat widget (env keys, widget mount, missing config).
2636
2641
  Do NOT start long-lived dev servers, tunnels, or install global tools.
2637
2642
  Do NOT run \`npm run dev\` or similar.
2638
2643
 
2639
- Script names you report MUST already exist in package.json. Omit chat/ai-server scripts.
2644
+ 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
2645
 
2641
2646
  End with a short human summary AND one \`\`\`json fence with:
2642
2647
 
@@ -2708,7 +2713,21 @@ ${lock}`);
2708
2713
  "vite.config.mjs",
2709
2714
  "next.config.js",
2710
2715
  "next.config.mjs",
2711
- "next.config.ts"
2716
+ "next.config.ts",
2717
+ "pom.xml",
2718
+ "build.gradle",
2719
+ "build.gradle.kts",
2720
+ "build.sbt",
2721
+ "application.yml",
2722
+ "application.yaml",
2723
+ "application.properties",
2724
+ "application.conf",
2725
+ "src/main/resources/application.yml",
2726
+ "src/main/resources/application.yaml",
2727
+ "src/main/resources/application.properties",
2728
+ "src/main/resources/application.conf",
2729
+ "src/main/resources/application-dev.yml",
2730
+ "conf/application.conf"
2712
2731
  ]) {
2713
2732
  const body = readSnippet(import_node_path7.default.join(folder, rel), 2500);
2714
2733
  if (body) parts.push(`### ${rel}
@@ -2736,7 +2755,7 @@ DO NOT edit, create, or delete any files.
2736
2755
  DO NOT run shell commands, install packages, or start servers.
2737
2756
  Only infer how this project runs locally from the excerpts.
2738
2757
 
2739
- Script names you report MUST appear in package.json scripts. Omit chat/ai-server scripts.
2758
+ 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
2759
 
2741
2760
  Reply with a short human summary AND one \`\`\`json fence with:
2742
2761
 
@@ -2744,8 +2763,8 @@ Reply with a short human summary AND one \`\`\`json fence with:
2744
2763
  "kind": "next" | "vite" | "html" | "empty" | "other",
2745
2764
  "name": "package or folder name",
2746
2765
  "summary": "one sentence about this project",
2747
- "scripts": { "ui": "dev:client", "backend": "dev:server" },
2748
- "ports": { "ui": 5173, "backend": 4100 },
2766
+ "scripts": { "ui": "dev:client", "backend": "sbt run" },
2767
+ "ports": { "ui": 5173, "backend": 9000 },
2749
2768
  "issues": ["uncertainties"],
2750
2769
  "fixes": [],
2751
2770
  "ready": true
@@ -2878,6 +2897,9 @@ function dataInput(folder, extra) {
2878
2897
  function hostAppsCachePath2(folder, extra) {
2879
2898
  return hostAppsCachePath(dataInput(folder, extra));
2880
2899
  }
2900
+ function isAiServerApp(app) {
2901
+ return Boolean(app && (app.role === "ai-server" || app.id === AI_SERVER_APP_ID));
2902
+ }
2881
2903
  function defaultAiServerApp(port = AI_SERVER_DEFAULT_PORT) {
2882
2904
  return {
2883
2905
  id: AI_SERVER_APP_ID,
@@ -2908,7 +2930,8 @@ function normalizeHostApp(raw) {
2908
2930
  row.source
2909
2931
  ) ? row.source : "manual",
2910
2932
  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
2933
+ host: role !== "ai-server" && id !== AI_SERVER_APP_ID && row.host === true,
2934
+ folderPath: typeof row.folderPath === "string" && row.folderPath.trim() ? row.folderPath.trim().slice(0, 500) : null
2912
2935
  };
2913
2936
  }
2914
2937
  function ensureSingleHost(apps) {
@@ -2938,24 +2961,17 @@ function normalizeHostApps(raw) {
2938
2961
  const apps = [];
2939
2962
  for (const item of raw) {
2940
2963
  const app = normalizeHostApp(item);
2941
- if (!app || seen.has(app.id)) continue;
2964
+ if (!app || isAiServerApp(app) || seen.has(app.id)) continue;
2942
2965
  seen.add(app.id);
2943
2966
  apps.push(app);
2944
2967
  }
2945
2968
  return apps;
2946
2969
  }
2947
- function ensureAiServerApp(apps, preferredPort = AI_SERVER_DEFAULT_PORT) {
2948
- const next = normalizeHostApps(apps);
2949
- const existing = next.find((app) => app.id === AI_SERVER_APP_ID || app.role === "ai-server");
2950
- if (existing) {
2951
- existing.id = AI_SERVER_APP_ID;
2952
- existing.role = "ai-server";
2953
- existing.locked = true;
2954
- existing.host = false;
2955
- existing.name = existing.name || "AI server";
2956
- return ensureSingleHost([existing, ...next.filter((app) => app !== existing)]);
2957
- }
2958
- return ensureSingleHost([defaultAiServerApp(preferredPort), ...next]);
2970
+ function withoutAiServerApp(apps) {
2971
+ return ensureSingleHost(normalizeHostApps(apps).filter((app) => !isAiServerApp(app)));
2972
+ }
2973
+ function ensureAiServerApp(apps, _preferredPort = AI_SERVER_DEFAULT_PORT) {
2974
+ return withoutAiServerApp(apps);
2959
2975
  }
2960
2976
  function readText(file) {
2961
2977
  try {
@@ -3021,19 +3037,151 @@ function configPort(folder, files) {
3021
3037
  }
3022
3038
  return 0;
3023
3039
  }
3024
- function hostAppsFingerprint(folder) {
3040
+ var JVM_CONFIG_FILES = [
3041
+ "pom.xml",
3042
+ "build.gradle",
3043
+ "build.gradle.kts",
3044
+ "settings.gradle",
3045
+ "settings.gradle.kts",
3046
+ "build.sbt",
3047
+ "gradle.properties",
3048
+ "application.yml",
3049
+ "application.yaml",
3050
+ "application.properties",
3051
+ "application.conf",
3052
+ "src/main/resources/application.yml",
3053
+ "src/main/resources/application.yaml",
3054
+ "src/main/resources/application.properties",
3055
+ "src/main/resources/application.conf",
3056
+ "src/main/resources/application-dev.yml",
3057
+ "src/main/resources/application-dev.yaml",
3058
+ "src/main/resources/application-dev.properties",
3059
+ "src/main/resources/application-local.yml",
3060
+ "conf/application.conf",
3061
+ "conf/application.yml"
3062
+ ];
3063
+ var FINGERPRINT_FILES = [
3064
+ "package.json",
3065
+ ...ENV_FILES,
3066
+ "vite.config.ts",
3067
+ "vite.config.js",
3068
+ "vite.config.mjs",
3069
+ "next.config.js",
3070
+ "next.config.mjs",
3071
+ "next.config.ts",
3072
+ ...JVM_CONFIG_FILES
3073
+ ];
3074
+ function fileExists(folder, rel) {
3075
+ return import_node_fs4.default.existsSync(import_node_path8.default.join(folder, rel));
3076
+ }
3077
+ function jvmPortFromText(text) {
3078
+ const patterns = [
3079
+ /(?:^|\n)\s*server\.port\s*[:=]\s*["']?(\d{2,5})/im,
3080
+ /(?:^|\n)\s*SERVER_PORT\s*[:=]\s*["']?(\d{2,5})/im,
3081
+ /(?:^|\n)\s*play\.http\.port\s*[:=]\s*["']?(\d{2,5})/im,
3082
+ /(?:^|\n)\s*play\.server\.http\.port\s*[:=]\s*["']?(\d{2,5})/im,
3083
+ /(?:^|\n)\s*http\.port\s*[:=]\s*["']?(\d{2,5})/im,
3084
+ /-Dserver\.port=(\d{2,5})/i,
3085
+ /systemProperty\s*\(?\s*["']server\.port["']\s*,\s*["']?(\d{2,5})/i,
3086
+ /["']play\.http\.port["']\s*->\s*["']?(\d{2,5})/i
3087
+ ];
3088
+ for (const pattern of patterns) {
3089
+ const match = text.match(pattern);
3090
+ const port = parsePort(match?.[1]);
3091
+ if (port) return port;
3092
+ }
3093
+ const yaml = text.match(
3094
+ /(?:^|\n)server:\s*\n(?:[ \t].*\n)*?[ \t]+port:\s*["']?(\d{2,5})/i
3095
+ );
3096
+ return parsePort(yaml?.[1]);
3097
+ }
3098
+ function looksLike(haystack, ...needles) {
3099
+ const lower = haystack.toLowerCase();
3100
+ return needles.some((needle) => lower.includes(needle.toLowerCase()));
3101
+ }
3102
+ function wrapperCommand(folder, unixName, winName, fallback) {
3103
+ if (process.platform === "win32") {
3104
+ return fileExists(folder, winName) ? `.\\${winName}` : fallback;
3105
+ }
3106
+ return fileExists(folder, unixName) ? `./${unixName}` : fallback;
3107
+ }
3108
+ function detectJvmHostApp(folder, opts = {}) {
3109
+ const resolved = import_node_path8.default.resolve(folder);
3110
+ const texts = [];
3111
+ let foundMarker = false;
3112
+ for (const rel of JVM_CONFIG_FILES) {
3113
+ const text = readText(import_node_path8.default.join(resolved, rel));
3114
+ if (!text) continue;
3115
+ foundMarker = true;
3116
+ texts.push(text);
3117
+ }
3118
+ 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");
3119
+ const hasMaven = fileExists(resolved, "pom.xml");
3120
+ const hasSbt = fileExists(resolved, "build.sbt");
3121
+ if (!foundMarker && !hasGradle && !hasMaven && !hasSbt) return null;
3122
+ const blob = texts.join("\n");
3123
+ const { merged } = readProjectEnvLayers(resolved);
3124
+ const envPort = parsePort(merged.SERVER_PORT) || parsePort(merged.PLAY_HTTP_PORT) || parsePort(merged.HTTP_PORT) || parsePort(merged.PORT);
3125
+ let port = envPort || jvmPortFromText(blob);
3126
+ const isPlay = looksLike(
3127
+ blob,
3128
+ "play.http.port",
3129
+ "enablePlugins(Play",
3130
+ "com.typesafe.play",
3131
+ "play-scala",
3132
+ "play-java"
3133
+ );
3134
+ const isSpring = looksLike(
3135
+ blob,
3136
+ "spring-boot",
3137
+ "springframework.boot",
3138
+ "org.springframework"
3139
+ );
3140
+ const isQuarkus = looksLike(blob, "quarkus", "io.quarkus");
3141
+ if (!port) {
3142
+ port = isPlay ? 9e3 : 8080;
3143
+ }
3144
+ let startCommand = null;
3145
+ if (hasGradle) {
3146
+ const gradle = wrapperCommand(resolved, "gradlew", "gradlew.bat", "gradle");
3147
+ startCommand = isQuarkus ? `${gradle} quarkusDev` : isPlay ? `${gradle} run` : `${gradle} bootRun`;
3148
+ } else if (hasMaven) {
3149
+ const mvn = wrapperCommand(resolved, "mvnw", "mvnw.cmd", "mvn");
3150
+ startCommand = isQuarkus ? `${mvn} quarkus:dev` : `${mvn} spring-boot:run`;
3151
+ } else if (hasSbt) {
3152
+ startCommand = "sbt run";
3153
+ }
3154
+ const framework = isPlay ? "Play" : isSpring ? "Spring" : isQuarkus ? "Quarkus" : hasSbt ? "Scala" : "JVM";
3155
+ return {
3156
+ id: "backend",
3157
+ name: opts.appName ? `${opts.appName} API` : `${framework} backend`,
3158
+ role: "backend",
3159
+ port,
3160
+ startCommand,
3161
+ source: envPort ? "env" : "package",
3162
+ folderPath: resolved
3163
+ };
3164
+ }
3165
+ function uniqueWorkspaceDirs(primary, extra) {
3166
+ const out = [];
3167
+ const seen = /* @__PURE__ */ new Set();
3168
+ for (const raw of [primary, ...extra || []]) {
3169
+ if (!raw || typeof raw !== "string") continue;
3170
+ const resolved = import_node_path8.default.resolve(raw.trim());
3171
+ const key = process.platform === "win32" ? resolved.toLowerCase() : resolved;
3172
+ if (seen.has(key)) continue;
3173
+ seen.add(key);
3174
+ out.push(resolved);
3175
+ }
3176
+ return out;
3177
+ }
3178
+ function folderSlug(folder) {
3179
+ return import_node_path8.default.basename(import_node_path8.default.resolve(folder)).replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 24) || "app";
3180
+ }
3181
+ function fingerprintOneFolder(folder) {
3025
3182
  const resolved = import_node_path8.default.resolve(folder);
3026
3183
  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
- ]) {
3184
+ for (const rel of FINGERPRINT_FILES) {
3037
3185
  const file = import_node_path8.default.join(resolved, rel);
3038
3186
  if (!import_node_fs4.default.existsSync(file)) continue;
3039
3187
  try {
@@ -3042,7 +3190,16 @@ function hostAppsFingerprint(folder) {
3042
3190
  } catch {
3043
3191
  }
3044
3192
  }
3045
- return (0, import_node_crypto4.createHash)("sha256").update(parts.join("|") || resolved).digest("hex").slice(0, 32);
3193
+ return parts.join("|") || resolved;
3194
+ }
3195
+ function hostAppsFingerprint(folder) {
3196
+ const resolved = import_node_path8.default.resolve(folder);
3197
+ return (0, import_node_crypto4.createHash)("sha256").update(fingerprintOneFolder(resolved)).digest("hex").slice(0, 32);
3198
+ }
3199
+ function hostAppsFingerprintForFolders(folders) {
3200
+ const dirs = uniqueWorkspaceDirs(folders[0] || "", folders.slice(1));
3201
+ const parts = dirs.map((dir) => `${dir}:${fingerprintOneFolder(dir)}`);
3202
+ return (0, import_node_crypto4.createHash)("sha256").update(parts.join("||") || "empty").digest("hex").slice(0, 32);
3046
3203
  }
3047
3204
  function pickScript(scripts, names) {
3048
3205
  return names.find((name) => scripts[name] && !isChatScript(name, scripts[name])) || null;
@@ -3056,14 +3213,15 @@ function detectHostAppsFromFiles(folder, opts = {}) {
3056
3213
  const apps = [];
3057
3214
  const envAi = parsePort(merged.AI_SERVER_PORT) || parsePort(merged.AI_SERVER_URL);
3058
3215
  const aiPort = envAi || parsePort(opts.preferredAiPort, AI_SERVER_DEFAULT_PORT);
3059
- apps.push({
3060
- ...defaultAiServerApp(aiPort),
3061
- source: envAi ? "env" : "default"
3062
- });
3063
3216
  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),
3217
+ PORT: layers.filter((layer) => layer.file !== ".env.example").map((layer) => ({
3218
+ file: layer.file,
3219
+ port: parsePort(layer.values.PORT) || parsePort(layer.values.SERVER_PORT) || parsePort(layer.values.PLAY_HTTP_PORT)
3220
+ })).filter((row) => row.port),
3065
3221
  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)
3222
+ API_PORT: parsePort(
3223
+ merged.API_PORT || merged.API_URL || merged.VITE_API_URL || merged.SERVER_PORT || merged.PLAY_HTTP_PORT || merged.HTTP_PORT
3224
+ )
3067
3225
  };
3068
3226
  const portValues = [...new Set(envPorts.PORT.map((row) => row.port))];
3069
3227
  if (portValues.length > 1) {
@@ -3108,7 +3266,8 @@ function detectHostAppsFromFiles(folder, opts = {}) {
3108
3266
  port,
3109
3267
  startCommand: `npm run ${uiScript}`,
3110
3268
  source: uiFromEnv ? "env" : scriptPort ? "package" : "package",
3111
- host: true
3269
+ host: true,
3270
+ folderPath: resolved
3112
3271
  });
3113
3272
  } else if (uiFromEnv) {
3114
3273
  apps.push({
@@ -3118,7 +3277,8 @@ function detectHostAppsFromFiles(folder, opts = {}) {
3118
3277
  port: uiFromEnv,
3119
3278
  startCommand: scripts.dev ? "npm run dev" : null,
3120
3279
  source: "env",
3121
- host: true
3280
+ host: true,
3281
+ folderPath: resolved
3122
3282
  });
3123
3283
  }
3124
3284
  if (apiScript && apiScript !== uiScript) {
@@ -3130,19 +3290,80 @@ function detectHostAppsFromFiles(folder, opts = {}) {
3130
3290
  role: "backend",
3131
3291
  port,
3132
3292
  startCommand: `npm run ${apiScript}`,
3133
- source: envPorts.API_PORT ? "env" : "package"
3293
+ source: envPorts.API_PORT ? "env" : "package",
3294
+ folderPath: resolved
3134
3295
  });
3135
3296
  }
3136
- const hostApps = apps.filter((app) => app.role !== "ai-server");
3297
+ const hasHostApp = apps.some((app) => app.role !== "ai-server");
3298
+ const usedPorts = new Set(
3299
+ apps.filter((app) => app.role !== "ai-server").map((app) => app.port)
3300
+ );
3301
+ const jvm = detectJvmHostApp(resolved, { appName: opts.appName });
3302
+ if (jvm && !usedPorts.has(jvm.port)) {
3303
+ apps.push({
3304
+ ...jvm,
3305
+ id: apps.some((app) => app.id === "backend") ? `backend-${folderSlug(resolved)}` : "backend",
3306
+ host: !hasHostApp
3307
+ });
3308
+ } else if (jvm && usedPorts.has(jvm.port) && !hasHostApp) {
3309
+ reasons.push(
3310
+ `JVM config suggests port ${jvm.port}, which is already used by another detected app`
3311
+ );
3312
+ }
3313
+ const tagged = apps.map(
3314
+ (app) => app.role === "ai-server" || app.folderPath ? app : { ...app, folderPath: resolved }
3315
+ );
3316
+ const hostApps = tagged.filter((app) => app.role !== "ai-server");
3137
3317
  if (pkg && Object.keys(scripts).length && hostApps.length === 0) {
3138
3318
  reasons.push("Found package.json scripts but no host app port in env or config");
3139
3319
  }
3140
3320
  return {
3141
- apps: ensureAiServerApp(apps, aiPort),
3321
+ apps: withoutAiServerApp(tagged),
3142
3322
  reasons,
3143
3323
  confused: reasons.length > 0
3144
3324
  };
3145
3325
  }
3326
+ function detectHostAppsFromFolders(folders, opts = {}) {
3327
+ const dirs = uniqueWorkspaceDirs(folders[0] || "", folders.slice(1));
3328
+ const reasons = [];
3329
+ let confused = false;
3330
+ const apps = [];
3331
+ const usedIds = /* @__PURE__ */ new Set();
3332
+ const usedPortSet = /* @__PURE__ */ new Set();
3333
+ const primary = dirs[0];
3334
+ for (const folder of dirs) {
3335
+ const detected = detectHostAppsFromFiles(folder, opts);
3336
+ confused = confused || detected.confused;
3337
+ const base = import_node_path8.default.basename(folder);
3338
+ for (const reason of detected.reasons) {
3339
+ reasons.push(dirs.length > 1 ? `${base}: ${reason}` : reason);
3340
+ }
3341
+ for (const app of detected.apps) {
3342
+ if (isAiServerApp(app)) continue;
3343
+ let id = app.id;
3344
+ if (usedIds.has(id)) id = `${app.id}-${folderSlug(folder)}`;
3345
+ usedIds.add(id);
3346
+ if (usedPortSet.has(app.port)) {
3347
+ reasons.push(
3348
+ `${base}: port ${app.port} is also used by another attached folder`
3349
+ );
3350
+ confused = true;
3351
+ }
3352
+ usedPortSet.add(app.port);
3353
+ apps.push({
3354
+ ...app,
3355
+ id,
3356
+ folderPath: import_node_path8.default.resolve(folder),
3357
+ name: dirs.length > 1 && primary && folder !== primary && !app.name.includes(base) ? `${app.name} (${base})` : app.name
3358
+ });
3359
+ }
3360
+ }
3361
+ return {
3362
+ apps: withoutAiServerApp(apps),
3363
+ reasons,
3364
+ confused
3365
+ };
3366
+ }
3146
3367
  function readHostAppsCache(folder, extra) {
3147
3368
  try {
3148
3369
  const raw = JSON.parse(
@@ -3187,12 +3408,37 @@ function mergeDesiredHostApps(desired, detected) {
3187
3408
  ...local,
3188
3409
  ...app,
3189
3410
  startCommand: app.startCommand || local?.startCommand || null,
3411
+ folderPath: app.folderPath || local?.folderPath || null,
3190
3412
  locked: app.locked || app.id === AI_SERVER_APP_ID || app.role === "ai-server",
3191
3413
  host: app.role === "ai-server" || app.id === AI_SERVER_APP_ID ? false : app.host === true || app.host !== false && local?.host === true
3192
3414
  };
3193
3415
  });
3194
3416
  return ensureAiServerApp(merged);
3195
3417
  }
3418
+ function unionHostApps(desired, detected) {
3419
+ const merged = mergeDesiredHostApps(desired, detected);
3420
+ const seenIds = new Set(merged.map((app) => app.id));
3421
+ const seenPorts = new Set(merged.map((app) => app.port));
3422
+ for (const app of detected) {
3423
+ if (app.role === "ai-server") continue;
3424
+ if (seenIds.has(app.id) || seenPorts.has(app.port)) continue;
3425
+ seenIds.add(app.id);
3426
+ seenPorts.add(app.port);
3427
+ merged.push(app);
3428
+ }
3429
+ return ensureAiServerApp(merged);
3430
+ }
3431
+ function startCommandFromScript(script) {
3432
+ if (!script || !script.trim()) return null;
3433
+ const value = script.trim();
3434
+ if (/\s/.test(value) || /[\\/]/.test(value) || value.includes(":")) {
3435
+ if (/^(npm|pnpm|yarn|npx)\s/i.test(value)) return value;
3436
+ if (/^(sbt|mvn|gradle|gradlew|mvnw|\.\\|\.\/)/i.test(value) || /\s/.test(value)) {
3437
+ return value;
3438
+ }
3439
+ }
3440
+ return `npm run ${value}`;
3441
+ }
3196
3442
  function appsFromInspectPorts(ports, scripts, name) {
3197
3443
  const apps = [];
3198
3444
  if (ports.ui || scripts.ui) {
@@ -3201,7 +3447,7 @@ function appsFromInspectPorts(ports, scripts, name) {
3201
3447
  name,
3202
3448
  role: "ui",
3203
3449
  port: parsePort(ports.ui, 5173),
3204
- startCommand: scripts.ui ? `npm run ${scripts.ui}` : null,
3450
+ startCommand: startCommandFromScript(scripts.ui),
3205
3451
  source: "ai"
3206
3452
  });
3207
3453
  }
@@ -3210,8 +3456,8 @@ function appsFromInspectPorts(ports, scripts, name) {
3210
3456
  id: "backend",
3211
3457
  name: "Backend",
3212
3458
  role: "backend",
3213
- port: parsePort(ports.backend, 4100),
3214
- startCommand: scripts.backend ? `npm run ${scripts.backend}` : null,
3459
+ port: parsePort(ports.backend, 8080),
3460
+ startCommand: startCommandFromScript(scripts.backend),
3215
3461
  source: "ai"
3216
3462
  });
3217
3463
  }
@@ -3221,7 +3467,7 @@ function appsFromInspectPorts(ports, scripts, name) {
3221
3467
  name,
3222
3468
  role: "app",
3223
3469
  port: parsePort(ports.app, 3e3),
3224
- startCommand: scripts.app ? `npm run ${scripts.app}` : null,
3470
+ startCommand: startCommandFromScript(scripts.app),
3225
3471
  source: "ai"
3226
3472
  });
3227
3473
  }
@@ -3299,11 +3545,12 @@ function projectSummaryFromDetect(folder, apps, opts = {}) {
3299
3545
  return `${name}: ${hostApps.map((app) => `${app.name} on ${app.port}${app.startCommand ? ` (${app.startCommand})` : ""}`).join(", ")}.`;
3300
3546
  }
3301
3547
  async function proposeHostAppsFromConfig(input) {
3302
- const folder = import_node_path8.default.resolve(input.workspaceDir);
3548
+ const dirs = uniqueWorkspaceDirs(input.workspaceDir, input.workspaceDirs);
3549
+ const folder = dirs[0] || import_node_path8.default.resolve(input.workspaceDir);
3303
3550
  const preferredAi = parsePort(input.preferredAiPort, AI_SERVER_DEFAULT_PORT);
3304
- const fingerprint = hostAppsFingerprint(folder);
3551
+ const fingerprint = hostAppsFingerprintForFolders(dirs);
3305
3552
  const allowAi = input.allowAi !== false;
3306
- const detected = detectHostAppsFromFiles(folder, {
3553
+ const detected = detectHostAppsFromFolders(dirs, {
3307
3554
  preferredAiPort: preferredAi,
3308
3555
  appName: input.appName
3309
3556
  });
@@ -3322,8 +3569,9 @@ async function proposeHostAppsFromConfig(input) {
3322
3569
  appName: input.appName,
3323
3570
  extraContext: [
3324
3571
  "File-based detection was uncertain or incomplete.",
3572
+ dirs.length > 1 ? `This sandbox also has folders: ${dirs.slice(1).join(", ")}` : "",
3325
3573
  ...reasons
3326
- ].join("\n")
3574
+ ].filter(Boolean).join("\n")
3327
3575
  });
3328
3576
  usedAi = true;
3329
3577
  const fromAi = appsFromInspectPorts(
@@ -3332,10 +3580,7 @@ async function proposeHostAppsFromConfig(input) {
3332
3580
  inspected.name || input.appName || "App"
3333
3581
  );
3334
3582
  if (fromAi.length) {
3335
- apps = ensureAiServerApp(
3336
- [defaultAiServerApp(preferredAi), ...fromAi],
3337
- preferredAi
3338
- );
3583
+ apps = unionHostApps(fromAi, detected.apps);
3339
3584
  }
3340
3585
  if (inspected.summary) projectSummary = inspected.summary;
3341
3586
  if (inspected.issues?.length) {
@@ -3347,9 +3592,9 @@ async function proposeHostAppsFromConfig(input) {
3347
3592
  );
3348
3593
  }
3349
3594
  }
3350
- const alternatives = collectScriptAlternatives(folder, apps, {
3351
- appName: input.appName
3352
- });
3595
+ const alternatives = dirs.flatMap(
3596
+ (dir) => collectScriptAlternatives(dir, apps, { appName: input.appName })
3597
+ );
3353
3598
  const confidence = proposalConfidence(apps, reasons, usedAi);
3354
3599
  const needsReview = confidence !== "high" || detected.confused || apps.filter((app) => app.role !== "ai-server").length === 0;
3355
3600
  return {
@@ -3364,11 +3609,12 @@ async function proposeHostAppsFromConfig(input) {
3364
3609
  };
3365
3610
  }
3366
3611
  async function resolveHostApps(input) {
3367
- const folder = import_node_path8.default.resolve(input.workspaceDir);
3612
+ const dirs = uniqueWorkspaceDirs(input.workspaceDir, input.workspaceDirs);
3613
+ const folder = dirs[0] || import_node_path8.default.resolve(input.workspaceDir);
3368
3614
  const loc = { sandboxId: input.sandboxId, dataDir: input.dataDir };
3369
- const fingerprint = hostAppsFingerprint(folder);
3615
+ const fingerprint = hostAppsFingerprintForFolders(dirs);
3370
3616
  const preferredAi = parsePort(input.preferredAiPort, AI_SERVER_DEFAULT_PORT);
3371
- const detected = detectHostAppsFromFiles(folder, {
3617
+ const detected = detectHostAppsFromFolders(dirs, {
3372
3618
  preferredAiPort: preferredAi,
3373
3619
  appName: input.appName
3374
3620
  });
@@ -3418,10 +3664,7 @@ Return the real local ports and npm script names.`
3418
3664
  inspected.scripts,
3419
3665
  inspected.name || input.appName || "App"
3420
3666
  );
3421
- const apps2 = ensureAiServerApp(
3422
- [defaultAiServerApp(preferredAi), ...fromAi],
3423
- preferredAi
3424
- );
3667
+ const apps2 = withoutAiServerApp(fromAi);
3425
3668
  writeHostAppsCache(
3426
3669
  folder,
3427
3670
  apps2,
@@ -3504,6 +3747,7 @@ Return the real local ports and npm script names.`
3504
3747
  createToolValidator,
3505
3748
  defaultAiServerApp,
3506
3749
  detectHostAppsFromFiles,
3750
+ detectHostAppsFromFolders,
3507
3751
  ensureAiServerApp,
3508
3752
  ensureProjectDataDir,
3509
3753
  ensureSingleHost,
@@ -3513,8 +3757,10 @@ Return the real local ports and npm script names.`
3513
3757
  getProviderPreference,
3514
3758
  hostAppsCachePath,
3515
3759
  hostAppsFingerprint,
3760
+ hostAppsFingerprintForFolders,
3516
3761
  inspectAndRepairWorkspace,
3517
3762
  inspectConfigOnly,
3763
+ isAiServerApp,
3518
3764
  isDevMode,
3519
3765
  isIgnoredRelative,
3520
3766
  isInsideWorkspace,
@@ -3547,7 +3793,9 @@ Return the real local ports and npm script names.`
3547
3793
  sanitizeProjectId,
3548
3794
  saveChatAttachments,
3549
3795
  toNextRoute,
3796
+ unionHostApps,
3550
3797
  upsertManagedIgnoreFile,
3798
+ withoutAiServerApp,
3551
3799
  writeCollaboraterApps,
3552
3800
  writeHostAppsCache
3553
3801
  });