@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 +310 -66
- package/dist/index.d.cts +19 -8
- package/dist/index.d.ts +19 -8
- package/dist/index.js +307 -65
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -62,6 +62,7 @@ __export(index_exports, {
|
|
|
62
62
|
createToolValidator: () => createToolValidator,
|
|
63
63
|
defaultAiServerApp: () => defaultAiServerApp,
|
|
64
64
|
detectHostAppsFromFiles: () => detectHostAppsFromFiles,
|
|
65
|
+
detectHostAppsFromFolders: () => detectHostAppsFromFolders,
|
|
65
66
|
ensureAiServerApp: () => ensureAiServerApp,
|
|
66
67
|
ensureProjectDataDir: () => ensureProjectDataDir,
|
|
67
68
|
ensureSingleHost: () => ensureSingleHost,
|
|
@@ -71,6 +72,7 @@ __export(index_exports, {
|
|
|
71
72
|
getProviderPreference: () => getProviderPreference,
|
|
72
73
|
hostAppsCachePath: () => hostAppsCachePath2,
|
|
73
74
|
hostAppsFingerprint: () => hostAppsFingerprint,
|
|
75
|
+
hostAppsFingerprintForFolders: () => hostAppsFingerprintForFolders,
|
|
74
76
|
inspectAndRepairWorkspace: () => inspectAndRepairWorkspace,
|
|
75
77
|
inspectConfigOnly: () => inspectConfigOnly,
|
|
76
78
|
isDevMode: () => isDevMode,
|
|
@@ -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(
|
|
2606
|
-
backend: import_zod.z.string().min(1).max(
|
|
2607
|
-
app: import_zod.z.string().min(1).max(
|
|
2607
|
+
ui: import_zod.z.string().min(1).max(200).optional(),
|
|
2608
|
+
backend: import_zod.z.string().min(1).max(200).optional(),
|
|
2609
|
+
app: import_zod.z.string().min(1).max(200).optional()
|
|
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
|
|
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": "
|
|
2749
|
-
"ports": { "ui": 5173, "backend":
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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) => ({
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
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,
|
|
3233
|
-
startCommand: scripts.backend
|
|
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
|
|
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
|
|
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 =
|
|
3560
|
+
const fingerprint = hostAppsFingerprintForFolders(dirs);
|
|
3324
3561
|
const allowAi = input.allowAi !== false;
|
|
3325
|
-
const detected =
|
|
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 =
|
|
3355
|
-
|
|
3356
|
-
|
|
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 =
|
|
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
|
|
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 =
|
|
3630
|
+
const fingerprint = hostAppsFingerprintForFolders(dirs);
|
|
3389
3631
|
const preferredAi = parsePort(input.preferredAiPort, AI_SERVER_DEFAULT_PORT);
|
|
3390
|
-
const detected =
|
|
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
|
|
539
|
+
/** Share URL — the page the browser opens. */
|
|
544
540
|
host?: boolean;
|
|
545
|
-
|
|
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
|
|
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
|
|
539
|
+
/** Share URL — the page the browser opens. */
|
|
544
540
|
host?: boolean;
|
|
545
|
-
|
|
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
|
|
665
|
+
export { ACCESS_IGNORE_BEGIN, ACCESS_IGNORE_END, AI_SERVER_APP_ID, AI_SERVER_DEFAULT_PORT, type AiCliProviderId, type AiProvider, type AiResponse, COLLABORATER_DIR, type CachedChatMessage, type CachedConversation, type CallAiOptions, type ChatAttachment, type ChatHandlerOptions, type ChatHandlers, type ChatMessage, type ChatSenderType, type ChatStoreLike, type ChatUser, type ClientContext, DEFAULT_AI_IGNORE_PATHS, HOST_APPS_FILE, type HostApp, type HostAppAlternative, type HostAppRole, type HostAppSource, type HostAppsResolveInput, type HostAppsResolveResult, type LocalStoredMessage, MAINTAINER_PRO_HOME_DIR, type MaintainerProStoreOptions, PROMPT_SECTION, type ParentChainEntry, type ParentChainSource, type PriorConversationOptions, type ProjectDataInput, type ProposeHostAppsInput, type ProviderPreference, type SetupProposal, type SetupProposalConfidence, type SyncedChatStore, type SyncedChatStoreOptions, type SyncedStoreStats, type ToolCall, type ToolSchemaMap, WORKING_PROVIDER, type WorkingTurnEvent, type WorkspaceInspectInput, type WorkspaceInspectResult, type WorkspaceKind, buildClaudeUserPrompt, buildConversationPrompt, buildCursorPrompt, buildPriorConversationsContext, callAi, collectParentChain, commandExists, createAntigravityProvider, createBuiltinProviders, createChatHandler, createClaudeProvider, createCursorProvider, createDefaultSystemPrompt, createInfoLogger, createLocalDirectoryStore, createLogger, createMaintainerProStore, createMaintainerProStoreFromEnv, createSyncedChatStore, createToolValidator, defaultAiServerApp, detectHostAppsFromFiles, detectHostAppsFromFolders, ensureAiServerApp, ensureProjectDataDir, ensureSingleHost, formatAccessPolicyPromptSection, formatClientContext, formatParentChainContext, getProviderPreference, hostAppsCachePath, hostAppsFingerprint, hostAppsFingerprintForFolders, inspectAndRepairWorkspace, inspectConfigOnly, isDevMode, isIgnoredRelative, isInsideWorkspace, isPathAllowed, maintainerProHome, mergeDesiredHostApps, normalizeHostApp, normalizeHostApps, normalizeIgnorePaths, parentChainForTurn, parseAiResponse, parseIgnorePathsEnv, parsePort, previewText, hostAppsCachePath$1 as projectHostAppsPath, projectIdForFolder, projectUploadsDir, proposeHostAppsFromConfig, providerLabel, readCollaboraterApps, readHostAppsCache, readProjectEnvLayers, renderManagedIgnoreBlock, resolveCliBinary, resolveHostApps, resolveIgnorePaths, resolveLogLevel, resolveProjectDataDir, resolveProvider, sanitizeProjectId, saveChatAttachments, toNextRoute, unionHostApps, upsertManagedIgnoreFile, writeCollaboraterApps, writeHostAppsCache };
|
package/dist/index.js
CHANGED
|
@@ -2488,9 +2488,9 @@ var inspectJsonSchema = z.object({
|
|
|
2488
2488
|
name: z.string().max(200).optional(),
|
|
2489
2489
|
summary: z.string().max(800).optional(),
|
|
2490
2490
|
scripts: z.object({
|
|
2491
|
-
ui: z.string().min(1).max(
|
|
2492
|
-
backend: z.string().min(1).max(
|
|
2493
|
-
app: z.string().min(1).max(
|
|
2491
|
+
ui: z.string().min(1).max(200).optional(),
|
|
2492
|
+
backend: z.string().min(1).max(200).optional(),
|
|
2493
|
+
app: z.string().min(1).max(200).optional()
|
|
2494
2494
|
}).optional(),
|
|
2495
2495
|
ports: z.object({
|
|
2496
2496
|
ui: z.number().int().min(1).max(65535).optional(),
|
|
@@ -2518,12 +2518,12 @@ function setupSystemPrompt(input) {
|
|
|
2518
2518
|
Workspace: ${input.workspaceDir}
|
|
2519
2519
|
App name: ${input.appName || "the app"}
|
|
2520
2520
|
|
|
2521
|
-
Inspect this repository (package.json, README, lockfiles, existing env files, how the UI and API start).
|
|
2521
|
+
Inspect this repository (package.json, README, lockfiles, Maven/Gradle/sbt files, Spring/Play config, existing env files, how the UI and API start).
|
|
2522
2522
|
You MAY edit files to fix setup problems that would block local run or the chat widget (env keys, widget mount, missing config).
|
|
2523
2523
|
Do NOT start long-lived dev servers, tunnels, or install global tools.
|
|
2524
2524
|
Do NOT run \`npm run dev\` or similar.
|
|
2525
2525
|
|
|
2526
|
-
Script names you report MUST already exist in package.json. Omit chat/ai-server scripts.
|
|
2526
|
+
Script names you report MUST already exist in package.json, OR be a real JVM start command (./gradlew bootRun, mvn spring-boot:run, sbt run). Omit chat/ai-server scripts.
|
|
2527
2527
|
|
|
2528
2528
|
End with a short human summary AND one \`\`\`json fence with:
|
|
2529
2529
|
|
|
@@ -2595,7 +2595,21 @@ ${lock}`);
|
|
|
2595
2595
|
"vite.config.mjs",
|
|
2596
2596
|
"next.config.js",
|
|
2597
2597
|
"next.config.mjs",
|
|
2598
|
-
"next.config.ts"
|
|
2598
|
+
"next.config.ts",
|
|
2599
|
+
"pom.xml",
|
|
2600
|
+
"build.gradle",
|
|
2601
|
+
"build.gradle.kts",
|
|
2602
|
+
"build.sbt",
|
|
2603
|
+
"application.yml",
|
|
2604
|
+
"application.yaml",
|
|
2605
|
+
"application.properties",
|
|
2606
|
+
"application.conf",
|
|
2607
|
+
"src/main/resources/application.yml",
|
|
2608
|
+
"src/main/resources/application.yaml",
|
|
2609
|
+
"src/main/resources/application.properties",
|
|
2610
|
+
"src/main/resources/application.conf",
|
|
2611
|
+
"src/main/resources/application-dev.yml",
|
|
2612
|
+
"conf/application.conf"
|
|
2599
2613
|
]) {
|
|
2600
2614
|
const body = readSnippet(path7.join(folder, rel), 2500);
|
|
2601
2615
|
if (body) parts.push(`### ${rel}
|
|
@@ -2623,7 +2637,7 @@ DO NOT edit, create, or delete any files.
|
|
|
2623
2637
|
DO NOT run shell commands, install packages, or start servers.
|
|
2624
2638
|
Only infer how this project runs locally from the excerpts.
|
|
2625
2639
|
|
|
2626
|
-
Script names you report MUST appear in package.json scripts
|
|
2640
|
+
Script names you report MUST appear in package.json scripts, OR be a JVM start command such as \`./gradlew bootRun\`, \`mvn spring-boot:run\`, or \`sbt run\`. Omit chat/ai-server scripts.
|
|
2627
2641
|
|
|
2628
2642
|
Reply with a short human summary AND one \`\`\`json fence with:
|
|
2629
2643
|
|
|
@@ -2631,8 +2645,8 @@ Reply with a short human summary AND one \`\`\`json fence with:
|
|
|
2631
2645
|
"kind": "next" | "vite" | "html" | "empty" | "other",
|
|
2632
2646
|
"name": "package or folder name",
|
|
2633
2647
|
"summary": "one sentence about this project",
|
|
2634
|
-
"scripts": { "ui": "dev:client", "backend": "
|
|
2635
|
-
"ports": { "ui": 5173, "backend":
|
|
2648
|
+
"scripts": { "ui": "dev:client", "backend": "sbt run" },
|
|
2649
|
+
"ports": { "ui": 5173, "backend": 9000 },
|
|
2636
2650
|
"issues": ["uncertainties"],
|
|
2637
2651
|
"fixes": [],
|
|
2638
2652
|
"ready": true
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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) => ({
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
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,
|
|
3119
|
-
startCommand: scripts.backend
|
|
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
|
|
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
|
|
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 =
|
|
3444
|
+
const fingerprint = hostAppsFingerprintForFolders(dirs);
|
|
3210
3445
|
const allowAi = input.allowAi !== false;
|
|
3211
|
-
const detected =
|
|
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 =
|
|
3241
|
-
|
|
3242
|
-
|
|
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 =
|
|
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
|
|
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 =
|
|
3514
|
+
const fingerprint = hostAppsFingerprintForFolders(dirs);
|
|
3275
3515
|
const preferredAi = parsePort(input.preferredAiPort, AI_SERVER_DEFAULT_PORT);
|
|
3276
|
-
const detected =
|
|
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
|