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