devsurface 0.4.0 → 0.6.0
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/CHANGELOG.md +21 -0
- package/README.md +78 -8
- package/action/dist/index.js +28 -0
- package/dist/cli/index.js +795 -139
- package/dist/cli/index.js.map +1 -1
- package/package.json +21 -2
- package/src/web/dist/assets/index-BJNqXAsU.js +10 -0
- package/src/web/dist/assets/{index-Bj8suDpq.css → index-Cmz0HGAE.css} +1 -1
- package/src/web/dist/index.html +2 -2
- package/src/web/dist/assets/index-BO8glxtu.js +0 -10
package/dist/cli/index.js
CHANGED
|
@@ -7,8 +7,8 @@ import { Command } from "commander";
|
|
|
7
7
|
import pc from "picocolors";
|
|
8
8
|
|
|
9
9
|
// src/core/doctor/index.ts
|
|
10
|
-
import { promises as
|
|
11
|
-
import
|
|
10
|
+
import { promises as fs11 } from "fs";
|
|
11
|
+
import path11 from "path";
|
|
12
12
|
|
|
13
13
|
// src/core/documentation.ts
|
|
14
14
|
function extractScriptReferences(content) {
|
|
@@ -32,8 +32,8 @@ function extractScriptReferences(content) {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
// src/core/scanner/index.ts
|
|
35
|
-
import { promises as
|
|
36
|
-
import
|
|
35
|
+
import { promises as fs10 } from "fs";
|
|
36
|
+
import path10 from "path";
|
|
37
37
|
|
|
38
38
|
// src/core/config/load.ts
|
|
39
39
|
import { promises as fs } from "fs";
|
|
@@ -75,6 +75,12 @@ var defaultConfig = {
|
|
|
75
75
|
services: {
|
|
76
76
|
docker: true
|
|
77
77
|
},
|
|
78
|
+
setupGuide: [
|
|
79
|
+
"Copy .env.example to .env",
|
|
80
|
+
"Fill in required environment values",
|
|
81
|
+
"Install dependencies",
|
|
82
|
+
"Start the dev server"
|
|
83
|
+
],
|
|
78
84
|
docs: ""
|
|
79
85
|
};
|
|
80
86
|
|
|
@@ -123,6 +129,33 @@ function toGroups(value, warnings) {
|
|
|
123
129
|
}
|
|
124
130
|
return groups;
|
|
125
131
|
}
|
|
132
|
+
var MAX_SETUP_GUIDE_STEPS = 24;
|
|
133
|
+
var MAX_SETUP_GUIDE_STEP_LENGTH = 200;
|
|
134
|
+
function toSetupGuide(value, warnings) {
|
|
135
|
+
if (value === void 0) {
|
|
136
|
+
return void 0;
|
|
137
|
+
}
|
|
138
|
+
if (!Array.isArray(value)) {
|
|
139
|
+
warnings.push("setupGuide must be an array of strings.");
|
|
140
|
+
return void 0;
|
|
141
|
+
}
|
|
142
|
+
const steps = [];
|
|
143
|
+
for (const entry of value) {
|
|
144
|
+
if (typeof entry !== "string") {
|
|
145
|
+
warnings.push("setupGuide entries must be strings.");
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
const trimmed = entry.trim();
|
|
149
|
+
if (trimmed.length === 0) {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
steps.push(trimmed.slice(0, MAX_SETUP_GUIDE_STEP_LENGTH));
|
|
153
|
+
}
|
|
154
|
+
if (steps.length > MAX_SETUP_GUIDE_STEPS) {
|
|
155
|
+
warnings.push(`setupGuide may contain at most ${MAX_SETUP_GUIDE_STEPS} steps.`);
|
|
156
|
+
}
|
|
157
|
+
return steps.slice(0, MAX_SETUP_GUIDE_STEPS);
|
|
158
|
+
}
|
|
126
159
|
function toPorts(value, warnings) {
|
|
127
160
|
if (value === void 0) {
|
|
128
161
|
return void 0;
|
|
@@ -177,6 +210,7 @@ function validateConfig(raw) {
|
|
|
177
210
|
ports: toPorts(raw.ports, warnings),
|
|
178
211
|
env,
|
|
179
212
|
services,
|
|
213
|
+
setupGuide: toSetupGuide(raw.setupGuide ?? raw.setup_guide, warnings),
|
|
180
214
|
docs
|
|
181
215
|
},
|
|
182
216
|
warnings
|
|
@@ -836,9 +870,64 @@ async function detectGit(root) {
|
|
|
836
870
|
}
|
|
837
871
|
}
|
|
838
872
|
|
|
839
|
-
// src/core/scanner/
|
|
873
|
+
// src/core/scanner/language.ts
|
|
840
874
|
import { promises as fs6 } from "fs";
|
|
841
875
|
import path6 from "path";
|
|
876
|
+
var languageFiles = [
|
|
877
|
+
{ language: "python", candidates: ["requirements.txt", "pyproject.toml", "Pipfile"] },
|
|
878
|
+
{ language: "go", candidates: ["go.mod"] },
|
|
879
|
+
{ language: "java", candidates: ["pom.xml", "build.gradle", "build.gradle.kts"] }
|
|
880
|
+
];
|
|
881
|
+
function isWithinRoot5(root, target) {
|
|
882
|
+
const relative = path6.relative(path6.resolve(root), path6.resolve(target));
|
|
883
|
+
return relative === "" || !relative.startsWith("..") && !path6.isAbsolute(relative);
|
|
884
|
+
}
|
|
885
|
+
async function safeFile(root, candidate) {
|
|
886
|
+
const filePath = path6.join(root, candidate);
|
|
887
|
+
try {
|
|
888
|
+
const [realRoot, stat, realPath] = await Promise.all([
|
|
889
|
+
fs6.realpath(root),
|
|
890
|
+
fs6.stat(filePath),
|
|
891
|
+
fs6.realpath(filePath)
|
|
892
|
+
]);
|
|
893
|
+
if (stat.isFile() && isWithinRoot5(realRoot, realPath)) {
|
|
894
|
+
return realPath;
|
|
895
|
+
}
|
|
896
|
+
} catch {
|
|
897
|
+
return null;
|
|
898
|
+
}
|
|
899
|
+
return null;
|
|
900
|
+
}
|
|
901
|
+
async function detectProjectLanguage(root, packageJson) {
|
|
902
|
+
const detected = [];
|
|
903
|
+
const files = [];
|
|
904
|
+
if (packageJson !== null) {
|
|
905
|
+
detected.push("node");
|
|
906
|
+
files.push(packageJson.path);
|
|
907
|
+
}
|
|
908
|
+
for (const definition of languageFiles) {
|
|
909
|
+
let found = false;
|
|
910
|
+
for (const candidate of definition.candidates) {
|
|
911
|
+
const file = await safeFile(root, candidate);
|
|
912
|
+
if (file !== null) {
|
|
913
|
+
found = true;
|
|
914
|
+
files.push(file);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
if (found) {
|
|
918
|
+
detected.push(definition.language);
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
return {
|
|
922
|
+
primary: detected[0] ?? null,
|
|
923
|
+
detected,
|
|
924
|
+
files
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
// src/core/scanner/packageManager.ts
|
|
929
|
+
import { promises as fs7 } from "fs";
|
|
930
|
+
import path7 from "path";
|
|
842
931
|
var lockFiles = [
|
|
843
932
|
{ file: "pnpm-lock.yaml", manager: "pnpm" },
|
|
844
933
|
{ file: "yarn.lock", manager: "yarn" },
|
|
@@ -848,7 +937,7 @@ var lockFiles = [
|
|
|
848
937
|
];
|
|
849
938
|
async function exists(filePath) {
|
|
850
939
|
try {
|
|
851
|
-
await
|
|
940
|
+
await fs7.access(filePath);
|
|
852
941
|
return true;
|
|
853
942
|
} catch {
|
|
854
943
|
return false;
|
|
@@ -856,7 +945,7 @@ async function exists(filePath) {
|
|
|
856
945
|
}
|
|
857
946
|
async function detectPackageManager(root) {
|
|
858
947
|
for (const lockFile of lockFiles) {
|
|
859
|
-
if (await exists(
|
|
948
|
+
if (await exists(path7.join(root, lockFile.file))) {
|
|
860
949
|
return lockFile.manager;
|
|
861
950
|
}
|
|
862
951
|
}
|
|
@@ -864,23 +953,23 @@ async function detectPackageManager(root) {
|
|
|
864
953
|
}
|
|
865
954
|
|
|
866
955
|
// src/core/scanner/packageJson.ts
|
|
867
|
-
import { promises as
|
|
868
|
-
import
|
|
869
|
-
function
|
|
870
|
-
const relative =
|
|
871
|
-
return relative === "" || !relative.startsWith("..") && !
|
|
956
|
+
import { promises as fs8 } from "fs";
|
|
957
|
+
import path8 from "path";
|
|
958
|
+
function isWithinRoot6(root, target) {
|
|
959
|
+
const relative = path8.relative(root, target);
|
|
960
|
+
return relative === "" || !relative.startsWith("..") && !path8.isAbsolute(relative);
|
|
872
961
|
}
|
|
873
962
|
async function readPackageJson(root) {
|
|
874
|
-
const packageJsonPath =
|
|
963
|
+
const packageJsonPath = path8.join(root, "package.json");
|
|
875
964
|
try {
|
|
876
965
|
const [realRoot, realPackageJsonPath] = await Promise.all([
|
|
877
|
-
|
|
878
|
-
|
|
966
|
+
fs8.realpath(root),
|
|
967
|
+
fs8.realpath(packageJsonPath)
|
|
879
968
|
]);
|
|
880
|
-
if (!
|
|
969
|
+
if (!isWithinRoot6(realRoot, realPackageJsonPath)) {
|
|
881
970
|
return null;
|
|
882
971
|
}
|
|
883
|
-
const content = await
|
|
972
|
+
const content = await fs8.readFile(realPackageJsonPath, "utf8");
|
|
884
973
|
const data = JSON.parse(content);
|
|
885
974
|
return { path: realPackageJsonPath, data };
|
|
886
975
|
} catch {
|
|
@@ -966,6 +1055,235 @@ async function detectPorts(ports) {
|
|
|
966
1055
|
);
|
|
967
1056
|
}
|
|
968
1057
|
|
|
1058
|
+
// src/core/scanner/presets.ts
|
|
1059
|
+
import { promises as fs9 } from "fs";
|
|
1060
|
+
import path9 from "path";
|
|
1061
|
+
function dependencyNames(packageJson) {
|
|
1062
|
+
const data = packageJson?.data;
|
|
1063
|
+
return new Set(
|
|
1064
|
+
Object.keys({
|
|
1065
|
+
...data?.dependencies,
|
|
1066
|
+
...data?.devDependencies,
|
|
1067
|
+
...data?.optionalDependencies,
|
|
1068
|
+
...data?.peerDependencies
|
|
1069
|
+
})
|
|
1070
|
+
);
|
|
1071
|
+
}
|
|
1072
|
+
function hasAnyDependency(dependencies, names) {
|
|
1073
|
+
return names.some((name) => dependencies.has(name));
|
|
1074
|
+
}
|
|
1075
|
+
async function readIfPresent2(root, candidate) {
|
|
1076
|
+
const filePath = path9.join(root, candidate);
|
|
1077
|
+
try {
|
|
1078
|
+
const [realRoot, realPath] = await Promise.all([fs9.realpath(root), fs9.realpath(filePath)]);
|
|
1079
|
+
const relative = path9.relative(realRoot, realPath);
|
|
1080
|
+
if (relative.startsWith("..") || path9.isAbsolute(relative)) {
|
|
1081
|
+
return null;
|
|
1082
|
+
}
|
|
1083
|
+
return await fs9.readFile(realPath, "utf8");
|
|
1084
|
+
} catch {
|
|
1085
|
+
return null;
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
function completePreset(draft) {
|
|
1089
|
+
return {
|
|
1090
|
+
name: draft.name,
|
|
1091
|
+
label: draft.label,
|
|
1092
|
+
commands: draft.commands ?? {},
|
|
1093
|
+
groups: draft.groups ?? {},
|
|
1094
|
+
ports: draft.ports ?? []
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
1097
|
+
function nodePresets(framework, packageJson) {
|
|
1098
|
+
const detected = new Set(framework?.detected ?? []);
|
|
1099
|
+
const dependencies = dependencyNames(packageJson);
|
|
1100
|
+
const presets = [];
|
|
1101
|
+
if (detected.has("Next.js")) {
|
|
1102
|
+
presets.push({
|
|
1103
|
+
name: "next",
|
|
1104
|
+
label: "Next.js",
|
|
1105
|
+
commands: {
|
|
1106
|
+
"next:dev": "next dev",
|
|
1107
|
+
"next:build": "next build",
|
|
1108
|
+
"next:start": "next start"
|
|
1109
|
+
},
|
|
1110
|
+
groups: {
|
|
1111
|
+
"Next.js": ["next:dev", "next:build", "next:start"]
|
|
1112
|
+
},
|
|
1113
|
+
ports: [3e3]
|
|
1114
|
+
});
|
|
1115
|
+
}
|
|
1116
|
+
if (detected.has("Vite")) {
|
|
1117
|
+
presets.push({
|
|
1118
|
+
name: "vite",
|
|
1119
|
+
label: "Vite",
|
|
1120
|
+
commands: {
|
|
1121
|
+
"vite:dev": "vite --host 127.0.0.1",
|
|
1122
|
+
"vite:build": "vite build",
|
|
1123
|
+
"vite:preview": "vite preview --host 127.0.0.1"
|
|
1124
|
+
},
|
|
1125
|
+
groups: {
|
|
1126
|
+
Vite: ["vite:dev", "vite:build", "vite:preview"]
|
|
1127
|
+
},
|
|
1128
|
+
ports: [5173, 4173]
|
|
1129
|
+
});
|
|
1130
|
+
}
|
|
1131
|
+
if (detected.has("NestJS")) {
|
|
1132
|
+
presets.push({
|
|
1133
|
+
name: "nestjs",
|
|
1134
|
+
label: "NestJS",
|
|
1135
|
+
commands: {
|
|
1136
|
+
"nest:start": "nest start --watch",
|
|
1137
|
+
"nest:build": "nest build"
|
|
1138
|
+
},
|
|
1139
|
+
groups: {
|
|
1140
|
+
NestJS: ["nest:start", "nest:build"]
|
|
1141
|
+
},
|
|
1142
|
+
ports: [3e3]
|
|
1143
|
+
});
|
|
1144
|
+
}
|
|
1145
|
+
if (detected.has("Remix")) {
|
|
1146
|
+
presets.push({
|
|
1147
|
+
name: "remix",
|
|
1148
|
+
label: "Remix",
|
|
1149
|
+
commands: {
|
|
1150
|
+
"remix:dev": "remix vite:dev",
|
|
1151
|
+
"remix:build": "remix vite:build"
|
|
1152
|
+
},
|
|
1153
|
+
groups: {
|
|
1154
|
+
Remix: ["remix:dev", "remix:build"]
|
|
1155
|
+
},
|
|
1156
|
+
ports: [5173]
|
|
1157
|
+
});
|
|
1158
|
+
}
|
|
1159
|
+
if (detected.has("Express") || detected.has("Fastify")) {
|
|
1160
|
+
presets.push({
|
|
1161
|
+
name: detected.has("Fastify") ? "fastify" : "express",
|
|
1162
|
+
label: detected.has("Fastify") ? "Fastify" : "Express",
|
|
1163
|
+
ports: [3e3]
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
1166
|
+
if (detected.has("Prisma") || hasAnyDependency(dependencies, ["prisma", "@prisma/client"])) {
|
|
1167
|
+
presets.push({
|
|
1168
|
+
name: "prisma",
|
|
1169
|
+
label: "Prisma",
|
|
1170
|
+
commands: {
|
|
1171
|
+
"prisma:migrate": "prisma migrate dev",
|
|
1172
|
+
"prisma:studio": "prisma studio"
|
|
1173
|
+
},
|
|
1174
|
+
groups: {
|
|
1175
|
+
Database: ["prisma:migrate", "prisma:studio"]
|
|
1176
|
+
},
|
|
1177
|
+
ports: [5555]
|
|
1178
|
+
});
|
|
1179
|
+
}
|
|
1180
|
+
return presets.map(completePreset);
|
|
1181
|
+
}
|
|
1182
|
+
async function pythonPresets(root, language) {
|
|
1183
|
+
if (!language.detected.includes("python")) {
|
|
1184
|
+
return [];
|
|
1185
|
+
}
|
|
1186
|
+
const [requirements, pyproject, pipfile] = await Promise.all([
|
|
1187
|
+
readIfPresent2(root, "requirements.txt"),
|
|
1188
|
+
readIfPresent2(root, "pyproject.toml"),
|
|
1189
|
+
readIfPresent2(root, "Pipfile")
|
|
1190
|
+
]);
|
|
1191
|
+
const manifest = [requirements, pyproject, pipfile].filter(Boolean).join("\n").toLowerCase();
|
|
1192
|
+
const commands = {};
|
|
1193
|
+
const groups = {};
|
|
1194
|
+
const ports = [];
|
|
1195
|
+
if (requirements !== null) {
|
|
1196
|
+
commands["python:install"] = "python -m pip install -r requirements.txt";
|
|
1197
|
+
groups.Setup = ["python:install"];
|
|
1198
|
+
}
|
|
1199
|
+
if (manifest.includes("uvicorn") || manifest.includes("fastapi")) {
|
|
1200
|
+
commands["python:dev"] = "uvicorn main:app --reload --host 127.0.0.1";
|
|
1201
|
+
groups.Python = [...groups.Python ?? [], "python:dev"];
|
|
1202
|
+
ports.push(8e3);
|
|
1203
|
+
}
|
|
1204
|
+
if (manifest.includes("flask")) {
|
|
1205
|
+
commands["flask:dev"] = "flask --app app run --host 127.0.0.1";
|
|
1206
|
+
groups.Python = [...groups.Python ?? [], "flask:dev"];
|
|
1207
|
+
ports.push(5e3);
|
|
1208
|
+
}
|
|
1209
|
+
if (manifest.includes("django") || await readIfPresent2(root, "manage.py") !== null) {
|
|
1210
|
+
commands["django:dev"] = "python manage.py runserver 127.0.0.1:8000";
|
|
1211
|
+
commands["django:migrate"] = "python manage.py migrate";
|
|
1212
|
+
groups.Python = [...groups.Python ?? [], "django:dev", "django:migrate"];
|
|
1213
|
+
ports.push(8e3);
|
|
1214
|
+
}
|
|
1215
|
+
return [
|
|
1216
|
+
completePreset({
|
|
1217
|
+
name: "python",
|
|
1218
|
+
label: "Python",
|
|
1219
|
+
commands,
|
|
1220
|
+
groups,
|
|
1221
|
+
ports
|
|
1222
|
+
})
|
|
1223
|
+
];
|
|
1224
|
+
}
|
|
1225
|
+
function goPresets(language) {
|
|
1226
|
+
if (!language.detected.includes("go")) {
|
|
1227
|
+
return [];
|
|
1228
|
+
}
|
|
1229
|
+
return [
|
|
1230
|
+
completePreset({
|
|
1231
|
+
name: "go",
|
|
1232
|
+
label: "Go",
|
|
1233
|
+
commands: {
|
|
1234
|
+
"go:run": "go run .",
|
|
1235
|
+
"go:build": "go build ./...",
|
|
1236
|
+
"go:test": "go test ./..."
|
|
1237
|
+
},
|
|
1238
|
+
groups: {
|
|
1239
|
+
Go: ["go:run", "go:build", "go:test"]
|
|
1240
|
+
}
|
|
1241
|
+
})
|
|
1242
|
+
];
|
|
1243
|
+
}
|
|
1244
|
+
async function javaPresets(language) {
|
|
1245
|
+
if (!language.detected.includes("java")) {
|
|
1246
|
+
return [];
|
|
1247
|
+
}
|
|
1248
|
+
const hasMaven = language.files.some((file) => path9.basename(file) === "pom.xml");
|
|
1249
|
+
const hasGradle = language.files.some((file) => path9.basename(file).startsWith("build.gradle"));
|
|
1250
|
+
const commands = {};
|
|
1251
|
+
const groups = {};
|
|
1252
|
+
if (hasMaven) {
|
|
1253
|
+
commands["maven:test"] = "mvn test";
|
|
1254
|
+
commands["maven:package"] = "mvn package";
|
|
1255
|
+
groups.Maven = ["maven:test", "maven:package"];
|
|
1256
|
+
}
|
|
1257
|
+
if (hasGradle) {
|
|
1258
|
+
commands["gradle:test"] = "gradle test";
|
|
1259
|
+
commands["gradle:build"] = "gradle build";
|
|
1260
|
+
groups.Gradle = ["gradle:test", "gradle:build"];
|
|
1261
|
+
}
|
|
1262
|
+
return [completePreset({ name: "java", label: "Java", commands, groups })];
|
|
1263
|
+
}
|
|
1264
|
+
async function detectPresets(options) {
|
|
1265
|
+
return [
|
|
1266
|
+
...nodePresets(options.framework, options.packageJson),
|
|
1267
|
+
...await pythonPresets(options.root, options.language),
|
|
1268
|
+
...goPresets(options.language),
|
|
1269
|
+
...await javaPresets(options.language)
|
|
1270
|
+
].filter(
|
|
1271
|
+
(preset) => Object.keys(preset.commands).length > 0 || Object.keys(preset.groups).length > 0 || preset.ports.length > 0
|
|
1272
|
+
);
|
|
1273
|
+
}
|
|
1274
|
+
function mergePresetCommands(presets) {
|
|
1275
|
+
return Object.assign({}, ...presets.map((preset) => preset.commands));
|
|
1276
|
+
}
|
|
1277
|
+
function mergePresetGroups(presets) {
|
|
1278
|
+
const groups = {};
|
|
1279
|
+
for (const preset of presets) {
|
|
1280
|
+
for (const [group, commands] of Object.entries(preset.groups)) {
|
|
1281
|
+
groups[group] = [...groups[group] ?? [], ...commands];
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
return groups;
|
|
1285
|
+
}
|
|
1286
|
+
|
|
969
1287
|
// src/core/scanner/scripts.ts
|
|
970
1288
|
function extractScripts(packageJson) {
|
|
971
1289
|
if (!packageJson?.data.scripts || typeof packageJson.data.scripts !== "object" || Array.isArray(packageJson.data.scripts)) {
|
|
@@ -980,17 +1298,17 @@ function extractScripts(packageJson) {
|
|
|
980
1298
|
}
|
|
981
1299
|
|
|
982
1300
|
// src/core/scanner/index.ts
|
|
983
|
-
function
|
|
984
|
-
const relative =
|
|
985
|
-
return relative === "" || !relative.startsWith("..") && !
|
|
1301
|
+
function isWithinRoot7(root, target) {
|
|
1302
|
+
const relative = path10.relative(path10.resolve(root), path10.resolve(target));
|
|
1303
|
+
return relative === "" || !relative.startsWith("..") && !path10.isAbsolute(relative);
|
|
986
1304
|
}
|
|
987
1305
|
async function findFirstFile(root, candidates) {
|
|
988
|
-
const resolvedRoot = await
|
|
1306
|
+
const resolvedRoot = await fs10.realpath(root).catch(() => path10.resolve(root));
|
|
989
1307
|
for (const candidate of candidates) {
|
|
990
|
-
const filePath =
|
|
1308
|
+
const filePath = path10.join(root, candidate);
|
|
991
1309
|
try {
|
|
992
|
-
const [stat, realPath] = await Promise.all([
|
|
993
|
-
if (stat.isFile() &&
|
|
1310
|
+
const [stat, realPath] = await Promise.all([fs10.stat(filePath), fs10.realpath(filePath)]);
|
|
1311
|
+
if (stat.isFile() && isWithinRoot7(resolvedRoot, realPath)) {
|
|
994
1312
|
return { path: realPath, exists: true };
|
|
995
1313
|
}
|
|
996
1314
|
} catch {
|
|
@@ -1002,15 +1320,25 @@ function configuredPorts(configPorts) {
|
|
|
1002
1320
|
return Array.isArray(configPorts) ? configPorts : [];
|
|
1003
1321
|
}
|
|
1004
1322
|
async function scanProject(root = process.cwd()) {
|
|
1005
|
-
const resolvedRoot = await
|
|
1323
|
+
const resolvedRoot = await fs10.realpath(root).catch(() => path10.resolve(root));
|
|
1006
1324
|
const config = await loadConfig(resolvedRoot);
|
|
1007
1325
|
const packageJson = await readPackageJson(resolvedRoot);
|
|
1008
1326
|
const scripts = extractScripts(packageJson) ?? {};
|
|
1009
1327
|
const framework = detectFramework(packageJson);
|
|
1328
|
+
const language = await detectProjectLanguage(resolvedRoot, packageJson);
|
|
1329
|
+
const presets = await detectPresets({
|
|
1330
|
+
root: resolvedRoot,
|
|
1331
|
+
packageJson,
|
|
1332
|
+
framework,
|
|
1333
|
+
language
|
|
1334
|
+
});
|
|
1335
|
+
const presetCommands = mergePresetCommands(presets);
|
|
1336
|
+
const presetGroups = mergePresetGroups(presets);
|
|
1010
1337
|
const portsToProbe = [
|
|
1011
1338
|
...configuredPorts(config?.config.ports),
|
|
1012
1339
|
...inferPortsFromScripts(scripts),
|
|
1013
|
-
...defaultPortsForFramework(framework)
|
|
1340
|
+
...defaultPortsForFramework(framework),
|
|
1341
|
+
...presets.flatMap((preset) => preset.ports)
|
|
1014
1342
|
];
|
|
1015
1343
|
const [packageManager, env, docker, git, ports, readme, license] = await Promise.all([
|
|
1016
1344
|
detectPackageManager(resolvedRoot),
|
|
@@ -1023,14 +1351,18 @@ async function scanProject(root = process.cwd()) {
|
|
|
1023
1351
|
]);
|
|
1024
1352
|
return {
|
|
1025
1353
|
root: resolvedRoot,
|
|
1026
|
-
projectName: config?.config.name ?? packageJson?.data.name ??
|
|
1354
|
+
projectName: config?.config.name ?? packageJson?.data.name ?? path10.basename(resolvedRoot),
|
|
1027
1355
|
packageJson,
|
|
1028
1356
|
packageManager: packageManager ?? (packageJson ? "npm" : null),
|
|
1357
|
+
language,
|
|
1029
1358
|
scripts,
|
|
1030
1359
|
env,
|
|
1031
1360
|
docker,
|
|
1032
1361
|
git,
|
|
1033
1362
|
framework,
|
|
1363
|
+
presets,
|
|
1364
|
+
presetCommands,
|
|
1365
|
+
presetGroups,
|
|
1034
1366
|
ports: ports ?? [],
|
|
1035
1367
|
readme,
|
|
1036
1368
|
license,
|
|
@@ -1041,18 +1373,18 @@ async function scanProject(root = process.cwd()) {
|
|
|
1041
1373
|
// src/core/doctor/index.ts
|
|
1042
1374
|
async function pathExists(filePath) {
|
|
1043
1375
|
try {
|
|
1044
|
-
await
|
|
1376
|
+
await fs11.access(filePath);
|
|
1045
1377
|
return true;
|
|
1046
1378
|
} catch {
|
|
1047
1379
|
return false;
|
|
1048
1380
|
}
|
|
1049
1381
|
}
|
|
1050
|
-
async function
|
|
1382
|
+
async function readIfPresent3(filePath) {
|
|
1051
1383
|
if (filePath === null) {
|
|
1052
1384
|
return null;
|
|
1053
1385
|
}
|
|
1054
1386
|
try {
|
|
1055
|
-
return await
|
|
1387
|
+
return await fs11.readFile(filePath, "utf8");
|
|
1056
1388
|
} catch {
|
|
1057
1389
|
return null;
|
|
1058
1390
|
}
|
|
@@ -1068,7 +1400,9 @@ async function runDoctor(root = process.cwd(), scan) {
|
|
|
1068
1400
|
warning("config-warning", "warning", "Config warning", configWarning, result.config?.path)
|
|
1069
1401
|
);
|
|
1070
1402
|
}
|
|
1071
|
-
|
|
1403
|
+
const isNodeProject = result.language.detected.includes("node");
|
|
1404
|
+
const hasKnownProjectLanguage = result.language.detected.length > 0;
|
|
1405
|
+
if (result.packageJson === null && !hasKnownProjectLanguage) {
|
|
1072
1406
|
warnings.push(
|
|
1073
1407
|
warning(
|
|
1074
1408
|
"missing-package-json",
|
|
@@ -1079,7 +1413,7 @@ async function runDoctor(root = process.cwd(), scan) {
|
|
|
1079
1413
|
);
|
|
1080
1414
|
return warnings;
|
|
1081
1415
|
}
|
|
1082
|
-
if (!await pathExists(
|
|
1416
|
+
if (isNodeProject && !await pathExists(path11.join(root, "node_modules", ".bin"))) {
|
|
1083
1417
|
warnings.push(
|
|
1084
1418
|
warning(
|
|
1085
1419
|
"missing-node-modules",
|
|
@@ -1126,7 +1460,7 @@ async function runDoctor(root = process.cwd(), scan) {
|
|
|
1126
1460
|
warning("missing-readme", "warning", "No README", "No README.md or README file was found.")
|
|
1127
1461
|
);
|
|
1128
1462
|
} else {
|
|
1129
|
-
const readme = await
|
|
1463
|
+
const readme = await readIfPresent3(result.readme.path);
|
|
1130
1464
|
if (readme !== null) {
|
|
1131
1465
|
const references = extractScriptReferences(readme);
|
|
1132
1466
|
const missingScripts = references.filter((script) => result.scripts[script] === void 0);
|
|
@@ -1162,7 +1496,7 @@ async function runDoctor(root = process.cwd(), scan) {
|
|
|
1162
1496
|
)
|
|
1163
1497
|
);
|
|
1164
1498
|
}
|
|
1165
|
-
if (result.scripts.test === void 0) {
|
|
1499
|
+
if (isNodeProject && result.scripts.test === void 0) {
|
|
1166
1500
|
warnings.push(
|
|
1167
1501
|
warning(
|
|
1168
1502
|
"missing-test-script",
|
|
@@ -1172,7 +1506,7 @@ async function runDoctor(root = process.cwd(), scan) {
|
|
|
1172
1506
|
)
|
|
1173
1507
|
);
|
|
1174
1508
|
}
|
|
1175
|
-
if (result.scripts.build === void 0) {
|
|
1509
|
+
if (isNodeProject && result.scripts.build === void 0) {
|
|
1176
1510
|
warnings.push(
|
|
1177
1511
|
warning(
|
|
1178
1512
|
"missing-build-script",
|
|
@@ -1233,25 +1567,204 @@ async function doctorCommand(cwd = process.cwd()) {
|
|
|
1233
1567
|
}
|
|
1234
1568
|
|
|
1235
1569
|
// src/cli/commands/init.ts
|
|
1236
|
-
import { promises as
|
|
1237
|
-
import
|
|
1570
|
+
import { promises as fs12 } from "fs";
|
|
1571
|
+
import path12 from "path";
|
|
1238
1572
|
import pc2 from "picocolors";
|
|
1239
1573
|
async function initCommand(cwd = process.cwd()) {
|
|
1240
|
-
const configPath =
|
|
1574
|
+
const configPath = path12.join(cwd, CONFIG_FILE_NAME);
|
|
1241
1575
|
try {
|
|
1242
|
-
await
|
|
1576
|
+
await fs12.access(configPath);
|
|
1243
1577
|
console.log(pc2.yellow(`${CONFIG_FILE_NAME} already exists.`));
|
|
1244
1578
|
return;
|
|
1245
1579
|
} catch {
|
|
1246
|
-
await
|
|
1580
|
+
await fs12.writeFile(configPath, `${JSON.stringify(defaultConfig, null, 2)}
|
|
1247
1581
|
`, "utf8");
|
|
1248
1582
|
console.log(pc2.green(`Created ${CONFIG_FILE_NAME}.`));
|
|
1249
1583
|
}
|
|
1250
1584
|
}
|
|
1251
1585
|
|
|
1252
|
-
// src/cli/commands/
|
|
1586
|
+
// src/cli/commands/onboard.ts
|
|
1253
1587
|
import pc3 from "picocolors";
|
|
1254
1588
|
|
|
1589
|
+
// src/core/onboarding/index.ts
|
|
1590
|
+
function hasWarning(warnings, id) {
|
|
1591
|
+
return warnings.some((warning2) => warning2.id === id);
|
|
1592
|
+
}
|
|
1593
|
+
function pickStartAction(scan) {
|
|
1594
|
+
if (scan.scripts.dev !== void 0) {
|
|
1595
|
+
return { kind: "run-script", label: "Start dev server", target: "dev" };
|
|
1596
|
+
}
|
|
1597
|
+
if (scan.scripts.start !== void 0) {
|
|
1598
|
+
return { kind: "run-script", label: "Start app", target: "start" };
|
|
1599
|
+
}
|
|
1600
|
+
const configuredCommands = {
|
|
1601
|
+
...scan.presetCommands,
|
|
1602
|
+
...scan.config?.config.commands
|
|
1603
|
+
};
|
|
1604
|
+
for (const name of ["dev", "start", "serve"]) {
|
|
1605
|
+
if (configuredCommands[name] !== void 0) {
|
|
1606
|
+
return { kind: "run-command", label: `Run ${name}`, target: name };
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
return null;
|
|
1610
|
+
}
|
|
1611
|
+
function dockerStep(scan) {
|
|
1612
|
+
const docker = scan.docker;
|
|
1613
|
+
if (docker === null) {
|
|
1614
|
+
return null;
|
|
1615
|
+
}
|
|
1616
|
+
const runningServices = docker.services.filter((service) => service.status === "running");
|
|
1617
|
+
const allRunning = docker.services.length > 0 && runningServices.length === docker.services.length;
|
|
1618
|
+
if (docker.daemonStatus !== "running") {
|
|
1619
|
+
return {
|
|
1620
|
+
id: "docker-start",
|
|
1621
|
+
title: "Start Docker services",
|
|
1622
|
+
description: docker.message ?? "A Docker Compose file was found, but the Docker engine is not running.",
|
|
1623
|
+
status: "manual",
|
|
1624
|
+
blocking: false,
|
|
1625
|
+
action: { kind: "docker", label: "Open Services" }
|
|
1626
|
+
};
|
|
1627
|
+
}
|
|
1628
|
+
if (docker.services.length === 0 || allRunning) {
|
|
1629
|
+
return {
|
|
1630
|
+
id: "docker-start",
|
|
1631
|
+
title: "Start Docker services",
|
|
1632
|
+
description: docker.services.length === 0 ? "Docker is running. No Compose services need to be started." : "All Docker Compose services are running.",
|
|
1633
|
+
status: "done",
|
|
1634
|
+
blocking: false
|
|
1635
|
+
};
|
|
1636
|
+
}
|
|
1637
|
+
return {
|
|
1638
|
+
id: "docker-start",
|
|
1639
|
+
title: "Start Docker services",
|
|
1640
|
+
description: `${runningServices.length}/${docker.services.length} Compose services running. Start the rest in Services.`,
|
|
1641
|
+
status: "todo",
|
|
1642
|
+
blocking: false,
|
|
1643
|
+
action: { kind: "docker", label: "Open Services" }
|
|
1644
|
+
};
|
|
1645
|
+
}
|
|
1646
|
+
function buildOnboardingPlan(scan, warnings) {
|
|
1647
|
+
const steps = [];
|
|
1648
|
+
const isNodeProject = scan.language.detected.includes("node");
|
|
1649
|
+
if (isNodeProject && scan.packageJson !== null) {
|
|
1650
|
+
const needsInstall = hasWarning(warnings, "missing-node-modules");
|
|
1651
|
+
steps.push({
|
|
1652
|
+
id: "install-dependencies",
|
|
1653
|
+
title: "Install dependencies",
|
|
1654
|
+
description: needsInstall ? "node_modules is missing. Install dependencies before running scripts." : "Dependencies are installed.",
|
|
1655
|
+
status: needsInstall ? "todo" : "done",
|
|
1656
|
+
blocking: true,
|
|
1657
|
+
action: needsInstall ? { kind: "install", label: "Install" } : void 0
|
|
1658
|
+
});
|
|
1659
|
+
}
|
|
1660
|
+
if (scan.env?.hasExample) {
|
|
1661
|
+
const missingLocal = !scan.env.hasLocal;
|
|
1662
|
+
steps.push({
|
|
1663
|
+
id: "create-env",
|
|
1664
|
+
title: "Create .env file",
|
|
1665
|
+
description: missingLocal ? ".env.example exists but the local .env file is missing." : ".env is present.",
|
|
1666
|
+
status: missingLocal ? "todo" : "done",
|
|
1667
|
+
blocking: true,
|
|
1668
|
+
action: missingLocal ? { kind: "env-copy", label: "Copy .env" } : void 0
|
|
1669
|
+
});
|
|
1670
|
+
if (scan.env.hasLocal) {
|
|
1671
|
+
const unset = [.../* @__PURE__ */ new Set([...scan.env.missingKeys, ...scan.env.emptyKeys])];
|
|
1672
|
+
steps.push({
|
|
1673
|
+
id: "fill-env",
|
|
1674
|
+
title: "Fill in environment values",
|
|
1675
|
+
description: unset.length > 0 ? `Set values for: ${unset.join(", ")}. Values are intentionally hidden.` : "All environment keys from the example are present.",
|
|
1676
|
+
status: unset.length > 0 ? "manual" : "done",
|
|
1677
|
+
blocking: true
|
|
1678
|
+
});
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
const docker = dockerStep(scan);
|
|
1682
|
+
if (docker !== null) {
|
|
1683
|
+
steps.push(docker);
|
|
1684
|
+
}
|
|
1685
|
+
const portsInUse = scan.ports.filter((probe) => probe.inUse);
|
|
1686
|
+
if (portsInUse.length > 0) {
|
|
1687
|
+
steps.push({
|
|
1688
|
+
id: "free-ports",
|
|
1689
|
+
title: "Resolve port conflicts",
|
|
1690
|
+
description: `Already in use: ${portsInUse.map((probe) => probe.port).join(", ")}. Stop the conflicting process or change the port.`,
|
|
1691
|
+
status: "manual",
|
|
1692
|
+
blocking: false
|
|
1693
|
+
});
|
|
1694
|
+
}
|
|
1695
|
+
for (const [index, entry] of (scan.config?.config.setupGuide ?? []).entries()) {
|
|
1696
|
+
steps.push({
|
|
1697
|
+
id: `guide-${index}`,
|
|
1698
|
+
title: entry,
|
|
1699
|
+
description: "From the project setup guide.",
|
|
1700
|
+
status: "manual",
|
|
1701
|
+
blocking: false
|
|
1702
|
+
});
|
|
1703
|
+
}
|
|
1704
|
+
const docs = scan.config?.config.docs;
|
|
1705
|
+
if (typeof docs === "string" && docs.length > 0 && isSafeHttpUrl(docs)) {
|
|
1706
|
+
steps.push({
|
|
1707
|
+
id: "read-docs",
|
|
1708
|
+
title: "Read the project docs",
|
|
1709
|
+
description: docs,
|
|
1710
|
+
status: "manual",
|
|
1711
|
+
blocking: false,
|
|
1712
|
+
action: { kind: "open-docs", label: "Open docs", target: docs }
|
|
1713
|
+
});
|
|
1714
|
+
}
|
|
1715
|
+
const startAction = pickStartAction(scan);
|
|
1716
|
+
if (startAction !== null) {
|
|
1717
|
+
steps.push({
|
|
1718
|
+
id: "start-app",
|
|
1719
|
+
title: "Start the app",
|
|
1720
|
+
description: "Run the development server once setup is complete.",
|
|
1721
|
+
status: "todo",
|
|
1722
|
+
blocking: false,
|
|
1723
|
+
action: startAction
|
|
1724
|
+
});
|
|
1725
|
+
}
|
|
1726
|
+
const blocking = steps.filter((step) => step.blocking);
|
|
1727
|
+
const blockingDone = blocking.filter((step) => step.status === "done");
|
|
1728
|
+
const readiness = blocking.length === 0 ? 100 : Math.round(blockingDone.length / blocking.length * 100);
|
|
1729
|
+
const ready = readiness === 100;
|
|
1730
|
+
const remaining = blocking.length - blockingDone.length;
|
|
1731
|
+
const summary = ready ? "Project is ready to run." : `${remaining} setup step${remaining === 1 ? "" : "s"} remaining before the project is ready.`;
|
|
1732
|
+
return { steps, readiness, ready, summary };
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
// src/cli/commands/onboard.ts
|
|
1736
|
+
function statusGlyph(status) {
|
|
1737
|
+
if (status === "done") {
|
|
1738
|
+
return pc3.green("[x]");
|
|
1739
|
+
}
|
|
1740
|
+
if (status === "todo") {
|
|
1741
|
+
return pc3.yellow("[ ]");
|
|
1742
|
+
}
|
|
1743
|
+
return pc3.cyan("[~]");
|
|
1744
|
+
}
|
|
1745
|
+
async function onboardCommand(cwd = process.cwd()) {
|
|
1746
|
+
const scan = await scanProject(cwd);
|
|
1747
|
+
const warnings = await runDoctor(cwd, scan);
|
|
1748
|
+
const plan = buildOnboardingPlan(scan, warnings);
|
|
1749
|
+
console.log(pc3.bold(`Onboarding ${safeDisplayText(scan.projectName)}`));
|
|
1750
|
+
console.log(`${plan.readiness}% ready \u2014 ${safeDisplayText(plan.summary)}`);
|
|
1751
|
+
console.log("");
|
|
1752
|
+
if (plan.steps.length === 0) {
|
|
1753
|
+
console.log(pc3.green("No onboarding steps detected."));
|
|
1754
|
+
return;
|
|
1755
|
+
}
|
|
1756
|
+
for (const step of plan.steps) {
|
|
1757
|
+
console.log(`${statusGlyph(step.status)} ${pc3.bold(safeDisplayText(step.title))}`);
|
|
1758
|
+
console.log(` ${safeDisplayText(step.description)}`);
|
|
1759
|
+
if (step.action && step.status !== "done") {
|
|
1760
|
+
console.log(pc3.dim(` \u2192 ${safeDisplayText(step.action.label)}`));
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
// src/cli/commands/run.ts
|
|
1766
|
+
import pc4 from "picocolors";
|
|
1767
|
+
|
|
1255
1768
|
// src/core/process/runner.ts
|
|
1256
1769
|
import spawn2 from "cross-spawn";
|
|
1257
1770
|
|
|
@@ -1430,38 +1943,74 @@ async function runPackageScriptToTerminal(options) {
|
|
|
1430
1943
|
});
|
|
1431
1944
|
});
|
|
1432
1945
|
}
|
|
1946
|
+
async function runConfiguredCommandToTerminal(options) {
|
|
1947
|
+
const resolvedCommand = await resolveConfiguredCommand(options.cwd, options.command);
|
|
1948
|
+
if (resolvedCommand === null) {
|
|
1949
|
+
return 1;
|
|
1950
|
+
}
|
|
1951
|
+
return await new Promise((resolve) => {
|
|
1952
|
+
const child = spawn2(resolvedCommand.command, resolvedCommand.args, {
|
|
1953
|
+
cwd: options.cwd,
|
|
1954
|
+
stdio: "inherit",
|
|
1955
|
+
windowsHide: true
|
|
1956
|
+
});
|
|
1957
|
+
child.on("error", () => {
|
|
1958
|
+
resolve(1);
|
|
1959
|
+
});
|
|
1960
|
+
child.on("close", (code) => {
|
|
1961
|
+
resolve(code ?? 1);
|
|
1962
|
+
});
|
|
1963
|
+
});
|
|
1964
|
+
}
|
|
1433
1965
|
|
|
1434
1966
|
// src/cli/commands/run.ts
|
|
1435
1967
|
async function runCommand(script, cwd = process.cwd()) {
|
|
1436
1968
|
const scan = await scanProject(cwd);
|
|
1437
|
-
if (scan.
|
|
1438
|
-
|
|
1439
|
-
|
|
1969
|
+
if (scan.scripts[script] !== void 0) {
|
|
1970
|
+
const exitCode = await runPackageScriptToTerminal({
|
|
1971
|
+
cwd,
|
|
1972
|
+
packageManager: scan.packageManager,
|
|
1973
|
+
script
|
|
1974
|
+
});
|
|
1975
|
+
process.exitCode = exitCode;
|
|
1440
1976
|
return;
|
|
1441
1977
|
}
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1978
|
+
const configuredCommand = scan.config?.config.commands?.[script] ?? scan.presetCommands[script];
|
|
1979
|
+
if (configuredCommand !== void 0) {
|
|
1980
|
+
if (isDangerousCommand(configuredCommand)) {
|
|
1981
|
+
console.error(pc4.red(`Refusing to run dangerous command "${safeDisplayText(script)}".`));
|
|
1982
|
+
process.exitCode = 1;
|
|
1983
|
+
return;
|
|
1984
|
+
}
|
|
1985
|
+
const exitCode = await runConfiguredCommandToTerminal({
|
|
1986
|
+
cwd,
|
|
1987
|
+
command: configuredCommand
|
|
1988
|
+
});
|
|
1989
|
+
process.exitCode = exitCode;
|
|
1445
1990
|
return;
|
|
1446
1991
|
}
|
|
1447
|
-
const
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1992
|
+
const available = [
|
|
1993
|
+
...Object.keys(scan.scripts),
|
|
1994
|
+
...Object.keys(scan.config?.config.commands ?? {}),
|
|
1995
|
+
...Object.keys(scan.presetCommands)
|
|
1996
|
+
];
|
|
1997
|
+
const hint = available.length > 0 ? ` Available commands: ${safeDisplayList(available)}.` : "";
|
|
1998
|
+
console.error(pc4.red(`Command "${safeDisplayText(script)}" was not found.${hint}`));
|
|
1999
|
+
process.exitCode = 1;
|
|
1453
2000
|
}
|
|
1454
2001
|
|
|
1455
2002
|
// src/cli/commands/scan.ts
|
|
1456
|
-
import
|
|
2003
|
+
import pc5 from "picocolors";
|
|
1457
2004
|
function formatList(values) {
|
|
1458
2005
|
return safeDisplayList(values);
|
|
1459
2006
|
}
|
|
1460
2007
|
function printScanResult(scan) {
|
|
1461
|
-
console.log(
|
|
2008
|
+
console.log(pc5.bold(`Project: ${safeDisplayText(scan.projectName)}`));
|
|
2009
|
+
console.log(`Language: ${formatList(scan.language.detected) || "unknown"}`);
|
|
1462
2010
|
console.log(`Type: ${safeDisplayText(scan.framework?.type ?? "Unknown")}`);
|
|
1463
2011
|
console.log(`Manager: ${safeDisplayText(scan.packageManager ?? "unknown")}`);
|
|
1464
2012
|
console.log(`Scripts: ${formatList(Object.keys(scan.scripts))}`);
|
|
2013
|
+
console.log(`Presets: ${formatList(scan.presets.map((preset) => preset.label)) || "none"}`);
|
|
1465
2014
|
console.log(`Git: ${safeDisplayText(scan.git?.branch ?? "not detected")}`);
|
|
1466
2015
|
console.log(`README: ${scan.readme.exists ? "found" : "missing"}`);
|
|
1467
2016
|
console.log(`LICENSE: ${scan.license.exists ? "found" : "missing"}`);
|
|
@@ -1483,35 +2032,35 @@ async function scanCommand(cwd = process.cwd()) {
|
|
|
1483
2032
|
}
|
|
1484
2033
|
|
|
1485
2034
|
// src/cli/commands/start.ts
|
|
1486
|
-
import
|
|
2035
|
+
import pc6 from "picocolors";
|
|
1487
2036
|
|
|
1488
2037
|
// node_modules/open/index.js
|
|
1489
2038
|
import process7 from "process";
|
|
1490
2039
|
import { Buffer as Buffer2 } from "buffer";
|
|
1491
|
-
import
|
|
2040
|
+
import path13 from "path";
|
|
1492
2041
|
import { fileURLToPath } from "url";
|
|
1493
2042
|
import { promisify as promisify5 } from "util";
|
|
1494
2043
|
import childProcess from "child_process";
|
|
1495
|
-
import
|
|
2044
|
+
import fs17, { constants as fsConstants2 } from "fs/promises";
|
|
1496
2045
|
|
|
1497
2046
|
// node_modules/wsl-utils/index.js
|
|
1498
2047
|
import process3 from "process";
|
|
1499
|
-
import
|
|
2048
|
+
import fs16, { constants as fsConstants } from "fs/promises";
|
|
1500
2049
|
|
|
1501
2050
|
// node_modules/is-wsl/index.js
|
|
1502
2051
|
import process2 from "process";
|
|
1503
2052
|
import os2 from "os";
|
|
1504
|
-
import
|
|
2053
|
+
import fs15 from "fs";
|
|
1505
2054
|
|
|
1506
2055
|
// node_modules/is-inside-container/index.js
|
|
1507
|
-
import
|
|
2056
|
+
import fs14 from "fs";
|
|
1508
2057
|
|
|
1509
2058
|
// node_modules/is-docker/index.js
|
|
1510
|
-
import
|
|
2059
|
+
import fs13 from "fs";
|
|
1511
2060
|
var isDockerCached;
|
|
1512
2061
|
function hasDockerEnv() {
|
|
1513
2062
|
try {
|
|
1514
|
-
|
|
2063
|
+
fs13.statSync("/.dockerenv");
|
|
1515
2064
|
return true;
|
|
1516
2065
|
} catch {
|
|
1517
2066
|
return false;
|
|
@@ -1519,7 +2068,7 @@ function hasDockerEnv() {
|
|
|
1519
2068
|
}
|
|
1520
2069
|
function hasDockerCGroup() {
|
|
1521
2070
|
try {
|
|
1522
|
-
return
|
|
2071
|
+
return fs13.readFileSync("/proc/self/cgroup", "utf8").includes("docker");
|
|
1523
2072
|
} catch {
|
|
1524
2073
|
return false;
|
|
1525
2074
|
}
|
|
@@ -1535,7 +2084,7 @@ function isDocker() {
|
|
|
1535
2084
|
var cachedResult;
|
|
1536
2085
|
var hasContainerEnv = () => {
|
|
1537
2086
|
try {
|
|
1538
|
-
|
|
2087
|
+
fs14.statSync("/run/.containerenv");
|
|
1539
2088
|
return true;
|
|
1540
2089
|
} catch {
|
|
1541
2090
|
return false;
|
|
@@ -1560,12 +2109,12 @@ var isWsl = () => {
|
|
|
1560
2109
|
return true;
|
|
1561
2110
|
}
|
|
1562
2111
|
try {
|
|
1563
|
-
if (
|
|
2112
|
+
if (fs15.readFileSync("/proc/version", "utf8").toLowerCase().includes("microsoft")) {
|
|
1564
2113
|
return !isInsideContainer();
|
|
1565
2114
|
}
|
|
1566
2115
|
} catch {
|
|
1567
2116
|
}
|
|
1568
|
-
if (
|
|
2117
|
+
if (fs15.existsSync("/proc/sys/fs/binfmt_misc/WSLInterop") || fs15.existsSync("/run/WSL")) {
|
|
1569
2118
|
return !isInsideContainer();
|
|
1570
2119
|
}
|
|
1571
2120
|
return false;
|
|
@@ -1583,14 +2132,14 @@ var wslDrivesMountPoint = /* @__PURE__ */ (() => {
|
|
|
1583
2132
|
const configFilePath = "/etc/wsl.conf";
|
|
1584
2133
|
let isConfigFileExists = false;
|
|
1585
2134
|
try {
|
|
1586
|
-
await
|
|
2135
|
+
await fs16.access(configFilePath, fsConstants.F_OK);
|
|
1587
2136
|
isConfigFileExists = true;
|
|
1588
2137
|
} catch {
|
|
1589
2138
|
}
|
|
1590
2139
|
if (!isConfigFileExists) {
|
|
1591
2140
|
return defaultMountPoint;
|
|
1592
2141
|
}
|
|
1593
|
-
const configContent = await
|
|
2142
|
+
const configContent = await fs16.readFile(configFilePath, { encoding: "utf8" });
|
|
1594
2143
|
const configMountPoint = /(?<!#.*)root\s*=\s*(?<mountPoint>.*)/g.exec(configContent);
|
|
1595
2144
|
if (!configMountPoint) {
|
|
1596
2145
|
return defaultMountPoint;
|
|
@@ -1744,8 +2293,8 @@ async function defaultBrowser2() {
|
|
|
1744
2293
|
|
|
1745
2294
|
// node_modules/open/index.js
|
|
1746
2295
|
var execFile5 = promisify5(childProcess.execFile);
|
|
1747
|
-
var __dirname =
|
|
1748
|
-
var localXdgOpenPath =
|
|
2296
|
+
var __dirname = path13.dirname(fileURLToPath(import.meta.url));
|
|
2297
|
+
var localXdgOpenPath = path13.join(__dirname, "xdg-open");
|
|
1749
2298
|
var { platform, arch } = process7;
|
|
1750
2299
|
async function getWindowsDefaultBrowserFromWsl() {
|
|
1751
2300
|
const powershellPath = await powerShellPath();
|
|
@@ -1895,7 +2444,7 @@ var baseOpen = async (options) => {
|
|
|
1895
2444
|
const isBundled = !__dirname || __dirname === "/";
|
|
1896
2445
|
let exeLocalXdgOpen = false;
|
|
1897
2446
|
try {
|
|
1898
|
-
await
|
|
2447
|
+
await fs17.access(localXdgOpenPath, fsConstants2.X_OK);
|
|
1899
2448
|
exeLocalXdgOpen = true;
|
|
1900
2449
|
} catch {
|
|
1901
2450
|
}
|
|
@@ -2000,8 +2549,8 @@ defineLazyProperty(apps, "browserPrivate", () => "browserPrivate");
|
|
|
2000
2549
|
var open_default = open;
|
|
2001
2550
|
|
|
2002
2551
|
// src/server/index.ts
|
|
2003
|
-
import { promises as
|
|
2004
|
-
import
|
|
2552
|
+
import { promises as fs21 } from "fs";
|
|
2553
|
+
import path17 from "path";
|
|
2005
2554
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2006
2555
|
import { createAdaptorServer } from "@hono/node-server";
|
|
2007
2556
|
import { serveStatic } from "@hono/node-server/serve-static";
|
|
@@ -2151,16 +2700,16 @@ var ProcessManager = class extends EventEmitter {
|
|
|
2151
2700
|
|
|
2152
2701
|
// src/core/hub/registry.ts
|
|
2153
2702
|
import { createHash } from "crypto";
|
|
2154
|
-
import { promises as
|
|
2703
|
+
import { promises as fs19 } from "fs";
|
|
2155
2704
|
import os3 from "os";
|
|
2156
|
-
import
|
|
2705
|
+
import path15 from "path";
|
|
2157
2706
|
|
|
2158
2707
|
// src/core/hub/workspaceRoots.ts
|
|
2159
|
-
import { promises as
|
|
2160
|
-
import
|
|
2161
|
-
function
|
|
2162
|
-
const relative =
|
|
2163
|
-
return relative === "" || !relative.startsWith("..") && !
|
|
2708
|
+
import { promises as fs18 } from "fs";
|
|
2709
|
+
import path14 from "path";
|
|
2710
|
+
function isWithinRoot8(root, target) {
|
|
2711
|
+
const relative = path14.relative(root, target);
|
|
2712
|
+
return relative === "" || !relative.startsWith("..") && !path14.isAbsolute(relative);
|
|
2164
2713
|
}
|
|
2165
2714
|
async function configuredWorkspaceRoots() {
|
|
2166
2715
|
const raw = process.env.DEVSURFACE_WORKSPACE_ROOTS;
|
|
@@ -2174,7 +2723,7 @@ async function configuredWorkspaceRoots() {
|
|
|
2174
2723
|
continue;
|
|
2175
2724
|
}
|
|
2176
2725
|
try {
|
|
2177
|
-
roots.push(await
|
|
2726
|
+
roots.push(await fs18.realpath(path14.resolve(trimmed)));
|
|
2178
2727
|
} catch {
|
|
2179
2728
|
}
|
|
2180
2729
|
}
|
|
@@ -2186,7 +2735,7 @@ async function assertWithinWorkspaceRoots(targetPath) {
|
|
|
2186
2735
|
return;
|
|
2187
2736
|
}
|
|
2188
2737
|
for (const root of roots) {
|
|
2189
|
-
if (
|
|
2738
|
+
if (isWithinRoot8(root, targetPath)) {
|
|
2190
2739
|
return;
|
|
2191
2740
|
}
|
|
2192
2741
|
}
|
|
@@ -2195,16 +2744,16 @@ async function assertWithinWorkspaceRoots(targetPath) {
|
|
|
2195
2744
|
|
|
2196
2745
|
// src/core/hub/registry.ts
|
|
2197
2746
|
function workspaceId(realPath) {
|
|
2198
|
-
const base =
|
|
2747
|
+
const base = path15.basename(realPath).replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 32) || "workspace";
|
|
2199
2748
|
const hash = createHash("sha256").update(realPath).digest("hex").slice(0, 6);
|
|
2200
2749
|
return `${base}-${hash}`;
|
|
2201
2750
|
}
|
|
2202
2751
|
function defaultDataDir() {
|
|
2203
|
-
return process.env.DEVSURFACE_DATA_DIR ??
|
|
2752
|
+
return process.env.DEVSURFACE_DATA_DIR ?? path15.join(os3.homedir(), ".devsurface");
|
|
2204
2753
|
}
|
|
2205
2754
|
async function readPackageName(dirPath) {
|
|
2206
2755
|
try {
|
|
2207
|
-
const raw = JSON.parse(await
|
|
2756
|
+
const raw = JSON.parse(await fs19.readFile(path15.join(dirPath, "package.json"), "utf8"));
|
|
2208
2757
|
return typeof raw?.name === "string" && raw.name.length > 0 ? raw.name : null;
|
|
2209
2758
|
} catch {
|
|
2210
2759
|
return null;
|
|
@@ -2215,7 +2764,7 @@ var WorkspaceRegistry = class {
|
|
|
2215
2764
|
seeded = false;
|
|
2216
2765
|
constructor(dataDir) {
|
|
2217
2766
|
const dir = dataDir ?? defaultDataDir();
|
|
2218
|
-
this.filePath =
|
|
2767
|
+
this.filePath = path15.join(dir, "workspaces.json");
|
|
2219
2768
|
}
|
|
2220
2769
|
async list() {
|
|
2221
2770
|
await this.seedFromEnv();
|
|
@@ -2229,7 +2778,7 @@ var WorkspaceRegistry = class {
|
|
|
2229
2778
|
if (existing) {
|
|
2230
2779
|
return existing;
|
|
2231
2780
|
}
|
|
2232
|
-
const name = await readPackageName(realDir) ??
|
|
2781
|
+
const name = await readPackageName(realDir) ?? path15.basename(realDir);
|
|
2233
2782
|
const entry = {
|
|
2234
2783
|
id: workspaceId(realDir),
|
|
2235
2784
|
name,
|
|
@@ -2251,7 +2800,7 @@ var WorkspaceRegistry = class {
|
|
|
2251
2800
|
}
|
|
2252
2801
|
async findByPath(dirPath) {
|
|
2253
2802
|
try {
|
|
2254
|
-
const realDir = await
|
|
2803
|
+
const realDir = await fs19.realpath(path15.resolve(dirPath));
|
|
2255
2804
|
const entries = await this.read();
|
|
2256
2805
|
return entries.find((entry) => entry.path === realDir) ?? null;
|
|
2257
2806
|
} catch {
|
|
@@ -2279,9 +2828,9 @@ var WorkspaceRegistry = class {
|
|
|
2279
2828
|
}
|
|
2280
2829
|
}
|
|
2281
2830
|
async resolveDir(dirPath) {
|
|
2282
|
-
const resolved =
|
|
2283
|
-
const realDir = await
|
|
2284
|
-
const stat = await
|
|
2831
|
+
const resolved = path15.resolve(dirPath);
|
|
2832
|
+
const realDir = await fs19.realpath(resolved);
|
|
2833
|
+
const stat = await fs19.stat(realDir);
|
|
2285
2834
|
if (!stat.isDirectory()) {
|
|
2286
2835
|
throw new Error(`${dirPath} is not a directory.`);
|
|
2287
2836
|
}
|
|
@@ -2289,7 +2838,7 @@ var WorkspaceRegistry = class {
|
|
|
2289
2838
|
}
|
|
2290
2839
|
async read() {
|
|
2291
2840
|
try {
|
|
2292
|
-
const content = await
|
|
2841
|
+
const content = await fs19.readFile(this.filePath, "utf8");
|
|
2293
2842
|
const parsed = JSON.parse(content);
|
|
2294
2843
|
return Array.isArray(parsed) ? parsed : [];
|
|
2295
2844
|
} catch {
|
|
@@ -2297,8 +2846,11 @@ var WorkspaceRegistry = class {
|
|
|
2297
2846
|
}
|
|
2298
2847
|
}
|
|
2299
2848
|
async write(entries) {
|
|
2300
|
-
await
|
|
2301
|
-
await
|
|
2849
|
+
await fs19.mkdir(path15.dirname(this.filePath), { recursive: true });
|
|
2850
|
+
await fs19.writeFile(this.filePath, JSON.stringify(entries, null, 2) + "\n", {
|
|
2851
|
+
encoding: "utf8",
|
|
2852
|
+
mode: 384
|
|
2853
|
+
});
|
|
2302
2854
|
}
|
|
2303
2855
|
async seedFromEnv() {
|
|
2304
2856
|
if (this.seeded) {
|
|
@@ -2380,12 +2932,12 @@ var Hub = class {
|
|
|
2380
2932
|
|
|
2381
2933
|
// src/server/routes/api.ts
|
|
2382
2934
|
import { constants as constants2, existsSync } from "fs";
|
|
2383
|
-
import { promises as
|
|
2384
|
-
import
|
|
2935
|
+
import { promises as fs20 } from "fs";
|
|
2936
|
+
import path16 from "path";
|
|
2385
2937
|
import spawn4 from "cross-spawn";
|
|
2386
2938
|
|
|
2387
2939
|
// src/version.ts
|
|
2388
|
-
var DEV_SURFACE_VERSION = "0.
|
|
2940
|
+
var DEV_SURFACE_VERSION = "0.6.0";
|
|
2389
2941
|
|
|
2390
2942
|
// src/server/localAccess.ts
|
|
2391
2943
|
var LOCAL_HOSTNAMES = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "::1"]);
|
|
@@ -2571,9 +3123,9 @@ function isAllowedTerminalCommand(command) {
|
|
|
2571
3123
|
}
|
|
2572
3124
|
|
|
2573
3125
|
// src/server/routes/api.ts
|
|
2574
|
-
function
|
|
2575
|
-
const relative =
|
|
2576
|
-
return relative === "" || !relative.startsWith("..") && !
|
|
3126
|
+
function isWithinRoot9(root, target) {
|
|
3127
|
+
const relative = path16.relative(path16.resolve(root), path16.resolve(target));
|
|
3128
|
+
return relative === "" || !relative.startsWith("..") && !path16.isAbsolute(relative);
|
|
2577
3129
|
}
|
|
2578
3130
|
function isAllowedMutationOrigin(requestUrl, origin) {
|
|
2579
3131
|
if (origin === null) {
|
|
@@ -2605,35 +3157,35 @@ function hasMutationIntent(intent) {
|
|
|
2605
3157
|
return intent === "dashboard";
|
|
2606
3158
|
}
|
|
2607
3159
|
async function realPathWithinRoot(root, target) {
|
|
2608
|
-
if (!
|
|
3160
|
+
if (!isWithinRoot9(root, target)) {
|
|
2609
3161
|
return false;
|
|
2610
3162
|
}
|
|
2611
3163
|
try {
|
|
2612
|
-
const [realRoot, realTarget] = await Promise.all([
|
|
2613
|
-
return
|
|
3164
|
+
const [realRoot, realTarget] = await Promise.all([fs20.realpath(root), fs20.realpath(target)]);
|
|
3165
|
+
return isWithinRoot9(realRoot, realTarget);
|
|
2614
3166
|
} catch {
|
|
2615
3167
|
return false;
|
|
2616
3168
|
}
|
|
2617
3169
|
}
|
|
2618
3170
|
async function writableDestinationWithinRoot(root, destination) {
|
|
2619
|
-
if (!
|
|
3171
|
+
if (!isWithinRoot9(root, destination)) {
|
|
2620
3172
|
return false;
|
|
2621
3173
|
}
|
|
2622
3174
|
try {
|
|
2623
3175
|
const [realRoot, realParent] = await Promise.all([
|
|
2624
|
-
|
|
2625
|
-
|
|
3176
|
+
fs20.realpath(root),
|
|
3177
|
+
fs20.realpath(path16.dirname(destination))
|
|
2626
3178
|
]);
|
|
2627
|
-
return
|
|
3179
|
+
return isWithinRoot9(realRoot, realParent);
|
|
2628
3180
|
} catch {
|
|
2629
3181
|
return false;
|
|
2630
3182
|
}
|
|
2631
3183
|
}
|
|
2632
3184
|
async function copyFileExclusive(source, destination) {
|
|
2633
|
-
const content = await
|
|
3185
|
+
const content = await fs20.readFile(source);
|
|
2634
3186
|
let handle2 = null;
|
|
2635
3187
|
try {
|
|
2636
|
-
handle2 = await
|
|
3188
|
+
handle2 = await fs20.open(
|
|
2637
3189
|
destination,
|
|
2638
3190
|
constants2.O_CREAT | constants2.O_EXCL | constants2.O_WRONLY,
|
|
2639
3191
|
384
|
|
@@ -2660,15 +3212,15 @@ function resolveCommandPromptExecutable() {
|
|
|
2660
3212
|
return process.env.ComSpec ?? "cmd.exe";
|
|
2661
3213
|
}
|
|
2662
3214
|
function findExecutable(command) {
|
|
2663
|
-
if (
|
|
3215
|
+
if (path16.isAbsolute(command)) {
|
|
2664
3216
|
return existsSync(command) ? command : null;
|
|
2665
3217
|
}
|
|
2666
3218
|
const pathValue = process.env.PATH ?? "";
|
|
2667
|
-
for (const directory of pathValue.split(
|
|
3219
|
+
for (const directory of pathValue.split(path16.delimiter)) {
|
|
2668
3220
|
if (directory.length === 0) {
|
|
2669
3221
|
continue;
|
|
2670
3222
|
}
|
|
2671
|
-
const candidate =
|
|
3223
|
+
const candidate = path16.join(directory, command);
|
|
2672
3224
|
if (existsSync(candidate)) {
|
|
2673
3225
|
return candidate;
|
|
2674
3226
|
}
|
|
@@ -2746,6 +3298,11 @@ function handleDockerError(error, context) {
|
|
|
2746
3298
|
}
|
|
2747
3299
|
throw error;
|
|
2748
3300
|
}
|
|
3301
|
+
async function onboardingForRoot(root) {
|
|
3302
|
+
const scan = await scanProject(root);
|
|
3303
|
+
const warnings = await runDoctor(root, scan);
|
|
3304
|
+
return buildOnboardingPlan(scan, warnings);
|
|
3305
|
+
}
|
|
2749
3306
|
function registerWorkspaceRoutes(app, resolveWorkspace) {
|
|
2750
3307
|
app.get("/api/workspaces/:id/project", async (context) => {
|
|
2751
3308
|
const ws = await resolveWorkspace(context.req.param("id"));
|
|
@@ -2757,6 +3314,11 @@ function registerWorkspaceRoutes(app, resolveWorkspace) {
|
|
|
2757
3314
|
if (!ws) return context.json({ error: "Workspace not found." }, 404);
|
|
2758
3315
|
return context.json(await runDoctor(ws.root));
|
|
2759
3316
|
});
|
|
3317
|
+
app.get("/api/workspaces/:id/onboarding", async (context) => {
|
|
3318
|
+
const ws = await resolveWorkspace(context.req.param("id"));
|
|
3319
|
+
if (!ws) return context.json({ error: "Workspace not found." }, 404);
|
|
3320
|
+
return context.json(await onboardingForRoot(ws.root));
|
|
3321
|
+
});
|
|
2760
3322
|
app.get("/api/workspaces/:id/processes", async (context) => {
|
|
2761
3323
|
const ws = await resolveWorkspace(context.req.param("id"));
|
|
2762
3324
|
if (!ws) return context.json({ error: "Workspace not found." }, 404);
|
|
@@ -2851,7 +3413,7 @@ function registerWorkspaceRoutes(app, resolveWorkspace) {
|
|
|
2851
3413
|
if (!ws) return context.json({ error: "Workspace not found." }, 404);
|
|
2852
3414
|
const name = decodeURIComponent(context.req.param("name"));
|
|
2853
3415
|
const scan = await scanProject(ws.root);
|
|
2854
|
-
const configuredCommand = scan.config?.config.commands?.[name] ?? null;
|
|
3416
|
+
const configuredCommand = scan.config?.config.commands?.[name] ?? scan.presetCommands[name] ?? null;
|
|
2855
3417
|
if (configuredCommand === null) {
|
|
2856
3418
|
return context.json({ error: `Configured command "${name}" was not found.` }, 404);
|
|
2857
3419
|
}
|
|
@@ -2885,7 +3447,7 @@ function registerWorkspaceRoutes(app, resolveWorkspace) {
|
|
|
2885
3447
|
app.post("/api/workspaces/:id/open/package", async (context) => {
|
|
2886
3448
|
const ws = await resolveWorkspace(context.req.param("id"));
|
|
2887
3449
|
if (!ws) return context.json({ error: "Workspace not found." }, 404);
|
|
2888
|
-
const packagePath =
|
|
3450
|
+
const packagePath = path16.join(ws.root, "package.json");
|
|
2889
3451
|
if (!await realPathWithinRoot(ws.root, packagePath)) {
|
|
2890
3452
|
return context.json({ error: "package.json was not found inside the project root." }, 404);
|
|
2891
3453
|
}
|
|
@@ -2907,7 +3469,7 @@ function registerWorkspaceRoutes(app, resolveWorkspace) {
|
|
|
2907
3469
|
if (examplePath === null) {
|
|
2908
3470
|
return context.json({ error: ".env.example was not found." }, 404);
|
|
2909
3471
|
}
|
|
2910
|
-
const destination = localPath ??
|
|
3472
|
+
const destination = localPath ?? path16.join(ws.root, scan.config?.config.env?.local ?? ".env");
|
|
2911
3473
|
if (!await realPathWithinRoot(ws.root, examplePath) || !await writableDestinationWithinRoot(ws.root, destination)) {
|
|
2912
3474
|
return context.json({ error: "Refusing to copy env files outside the project root." }, 400);
|
|
2913
3475
|
}
|
|
@@ -2979,6 +3541,11 @@ function registerHubApiRoutes(app, options) {
|
|
|
2979
3541
|
if (entries.length === 0) return context.json({ error: "No workspaces registered." }, 404);
|
|
2980
3542
|
return context.json(await runDoctor(hub.ensure(entries[0]).root));
|
|
2981
3543
|
});
|
|
3544
|
+
app.get("/api/onboarding", async (context) => {
|
|
3545
|
+
const entries = await hub.registry.list();
|
|
3546
|
+
if (entries.length === 0) return context.json({ error: "No workspaces registered." }, 404);
|
|
3547
|
+
return context.json(await onboardingForRoot(hub.ensure(entries[0]).root));
|
|
3548
|
+
});
|
|
2982
3549
|
app.get("/api/processes", async (context) => {
|
|
2983
3550
|
const entries = await hub.registry.list();
|
|
2984
3551
|
if (entries.length === 0) return context.json([]);
|
|
@@ -3082,23 +3649,34 @@ function setupHubWebSocket(server, hub) {
|
|
|
3082
3649
|
}
|
|
3083
3650
|
|
|
3084
3651
|
// src/server/index.ts
|
|
3652
|
+
function warnIfContainerRootsUnset(host) {
|
|
3653
|
+
if (host !== "0.0.0.0" && host !== "::") {
|
|
3654
|
+
return;
|
|
3655
|
+
}
|
|
3656
|
+
if (process.env.DEVSURFACE_WORKSPACE_ROOTS?.trim()) {
|
|
3657
|
+
return;
|
|
3658
|
+
}
|
|
3659
|
+
console.warn(
|
|
3660
|
+
"Warning: DEVSURFACE_WORKSPACE_ROOTS is unset in container mode. Any loopback or private-network client can register arbitrary directories as workspaces. Set DEVSURFACE_WORKSPACE_ROOTS to restrict workspace registration."
|
|
3661
|
+
);
|
|
3662
|
+
}
|
|
3085
3663
|
async function fileExists(filePath) {
|
|
3086
3664
|
try {
|
|
3087
|
-
await
|
|
3665
|
+
await fs21.access(filePath);
|
|
3088
3666
|
return true;
|
|
3089
3667
|
} catch {
|
|
3090
3668
|
return false;
|
|
3091
3669
|
}
|
|
3092
3670
|
}
|
|
3093
3671
|
async function findWebDistDir() {
|
|
3094
|
-
const moduleDir =
|
|
3672
|
+
const moduleDir = path17.dirname(fileURLToPath2(import.meta.url));
|
|
3095
3673
|
const candidates = [
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3674
|
+
path17.join(moduleDir, "..", "web", "dist"),
|
|
3675
|
+
path17.join(moduleDir, "..", "..", "src", "web", "dist"),
|
|
3676
|
+
path17.join(moduleDir, "web", "dist")
|
|
3099
3677
|
];
|
|
3100
3678
|
for (const candidate of candidates) {
|
|
3101
|
-
if (await fileExists(
|
|
3679
|
+
if (await fileExists(path17.join(candidate, "index.html"))) {
|
|
3102
3680
|
return candidate;
|
|
3103
3681
|
}
|
|
3104
3682
|
}
|
|
@@ -3170,7 +3748,7 @@ async function mountWebUi(app) {
|
|
|
3170
3748
|
app.use("/assets/*", serveStatic({ root: webDistDir }));
|
|
3171
3749
|
app.get("/favicon.svg", serveStatic({ root: webDistDir }));
|
|
3172
3750
|
app.get("*", async (context) => {
|
|
3173
|
-
const html = await
|
|
3751
|
+
const html = await fs21.readFile(path17.join(webDistDir, "index.html"), "utf8");
|
|
3174
3752
|
return context.html(html);
|
|
3175
3753
|
});
|
|
3176
3754
|
} else {
|
|
@@ -3197,6 +3775,7 @@ async function startHubServer(options) {
|
|
|
3197
3775
|
const port = options.port ?? DEFAULT_PORT;
|
|
3198
3776
|
const hub = new Hub({ dataDir: options.dataDir });
|
|
3199
3777
|
hub.attachCleanupHandlers();
|
|
3778
|
+
warnIfContainerRootsUnset(host);
|
|
3200
3779
|
if (options.initialWorkspace) {
|
|
3201
3780
|
await hub.registry.add(options.initialWorkspace);
|
|
3202
3781
|
}
|
|
@@ -3272,7 +3851,7 @@ function dashboardUrl(workspaceId2, port = DEFAULT_PORT, host = DEFAULT_HOST) {
|
|
|
3272
3851
|
async function startCommand(options) {
|
|
3273
3852
|
const cwd = options.cwd ?? process.cwd();
|
|
3274
3853
|
const port = options.port ?? 4567;
|
|
3275
|
-
console.log(
|
|
3854
|
+
console.log(pc6.bold(`DevSurface v${DEV_SURFACE_VERSION}`));
|
|
3276
3855
|
console.log("Scanning project...\n");
|
|
3277
3856
|
const scan = await scanProject(cwd);
|
|
3278
3857
|
printScanResult(scan);
|
|
@@ -3280,7 +3859,7 @@ async function startCommand(options) {
|
|
|
3280
3859
|
if (warnings.length > 0) {
|
|
3281
3860
|
console.log("\nWarnings:");
|
|
3282
3861
|
for (const item of warnings) {
|
|
3283
|
-
const marker = item.severity === "error" ?
|
|
3862
|
+
const marker = item.severity === "error" ? pc6.red("!") : pc6.yellow("!");
|
|
3284
3863
|
console.log(` ${marker} ${item.title}`);
|
|
3285
3864
|
}
|
|
3286
3865
|
}
|
|
@@ -3289,8 +3868,8 @@ async function startCommand(options) {
|
|
|
3289
3868
|
const registered = await registerWorkspaceRemotely(cwd, port);
|
|
3290
3869
|
if (registered) {
|
|
3291
3870
|
const url = dashboardUrl(registered.id, port);
|
|
3292
|
-
console.log(`Workspace ${
|
|
3293
|
-
console.log(`Dashboard -> ${
|
|
3871
|
+
console.log(`Workspace ${pc6.cyan(registered.name)} attached.`);
|
|
3872
|
+
console.log(`Dashboard -> ${pc6.cyan(url)}`);
|
|
3294
3873
|
if (options.openBrowser !== false) {
|
|
3295
3874
|
await open_default(url);
|
|
3296
3875
|
}
|
|
@@ -3304,13 +3883,13 @@ async function startCommand(options) {
|
|
|
3304
3883
|
initialWorkspace: cwd
|
|
3305
3884
|
});
|
|
3306
3885
|
console.log(`
|
|
3307
|
-
Dashboard running at -> ${
|
|
3886
|
+
Dashboard running at -> ${pc6.cyan(server.url)}`);
|
|
3308
3887
|
}
|
|
3309
3888
|
|
|
3310
3889
|
// src/cli/commands/serve.ts
|
|
3311
|
-
import
|
|
3890
|
+
import pc7 from "picocolors";
|
|
3312
3891
|
async function serveCommand(options) {
|
|
3313
|
-
console.log(
|
|
3892
|
+
console.log(pc7.bold(`DevSurface Hub v${DEV_SURFACE_VERSION}`));
|
|
3314
3893
|
console.log("Starting hub server...\n");
|
|
3315
3894
|
const server = await startHubServer({
|
|
3316
3895
|
port: options.port,
|
|
@@ -3320,7 +3899,7 @@ async function serveCommand(options) {
|
|
|
3320
3899
|
if (summaries.length > 0) {
|
|
3321
3900
|
console.log(`Registered workspaces: ${summaries.length}`);
|
|
3322
3901
|
for (const ws of summaries) {
|
|
3323
|
-
console.log(` ${
|
|
3902
|
+
console.log(` ${pc7.cyan(ws.name)} -> ${ws.path}`);
|
|
3324
3903
|
}
|
|
3325
3904
|
} else {
|
|
3326
3905
|
console.log(
|
|
@@ -3328,17 +3907,17 @@ async function serveCommand(options) {
|
|
|
3328
3907
|
);
|
|
3329
3908
|
}
|
|
3330
3909
|
console.log(`
|
|
3331
|
-
Hub running at -> ${
|
|
3910
|
+
Hub running at -> ${pc7.cyan(server.url)}`);
|
|
3332
3911
|
}
|
|
3333
3912
|
|
|
3334
3913
|
// src/cli/commands/workspace.ts
|
|
3335
|
-
import
|
|
3336
|
-
import
|
|
3914
|
+
import path18 from "path";
|
|
3915
|
+
import pc8 from "picocolors";
|
|
3337
3916
|
async function workspaceAddCommand(dirPath) {
|
|
3338
3917
|
const registry = new WorkspaceRegistry();
|
|
3339
|
-
const target =
|
|
3918
|
+
const target = path18.resolve(dirPath ?? process.cwd());
|
|
3340
3919
|
const entry = await registry.add(target);
|
|
3341
|
-
console.log(`Added workspace ${
|
|
3920
|
+
console.log(`Added workspace ${pc8.cyan(entry.name)} (${entry.id}) -> ${entry.path}`);
|
|
3342
3921
|
}
|
|
3343
3922
|
async function workspaceListCommand() {
|
|
3344
3923
|
const registry = new WorkspaceRegistry();
|
|
@@ -3352,7 +3931,7 @@ async function workspaceListCommand() {
|
|
|
3352
3931
|
console.log(`${entries.length} workspace${entries.length === 1 ? "" : "s"}:
|
|
3353
3932
|
`);
|
|
3354
3933
|
for (const entry of entries) {
|
|
3355
|
-
console.log(` ${
|
|
3934
|
+
console.log(` ${pc8.cyan(entry.name)} (${entry.id})`);
|
|
3356
3935
|
console.log(` ${entry.path}`);
|
|
3357
3936
|
}
|
|
3358
3937
|
}
|
|
@@ -3360,13 +3939,85 @@ async function workspaceRemoveCommand(id) {
|
|
|
3360
3939
|
const registry = new WorkspaceRegistry();
|
|
3361
3940
|
const removed = await registry.remove(id);
|
|
3362
3941
|
if (removed) {
|
|
3363
|
-
console.log(`Removed workspace ${
|
|
3942
|
+
console.log(`Removed workspace ${pc8.cyan(id)}.`);
|
|
3364
3943
|
} else {
|
|
3365
3944
|
console.error(`Workspace "${id}" not found.`);
|
|
3366
3945
|
process.exitCode = 1;
|
|
3367
3946
|
}
|
|
3368
3947
|
}
|
|
3369
3948
|
|
|
3949
|
+
// src/cli/updateCheck.ts
|
|
3950
|
+
var REGISTRY_LATEST_URL = "https://registry.npmjs.org/devsurface/latest";
|
|
3951
|
+
var UPDATE_CHECK_TIMEOUT_MS = 900;
|
|
3952
|
+
function parseVersion(version) {
|
|
3953
|
+
const match = version.match(/^v?(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/);
|
|
3954
|
+
if (!match) {
|
|
3955
|
+
return null;
|
|
3956
|
+
}
|
|
3957
|
+
return [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
3958
|
+
}
|
|
3959
|
+
function isNewerVersion(latestVersion, currentVersion) {
|
|
3960
|
+
const latest = parseVersion(latestVersion);
|
|
3961
|
+
const current = parseVersion(currentVersion);
|
|
3962
|
+
if (latest === null || current === null) {
|
|
3963
|
+
return false;
|
|
3964
|
+
}
|
|
3965
|
+
for (let index = 0; index < latest.length; index += 1) {
|
|
3966
|
+
if (latest[index] > current[index]) {
|
|
3967
|
+
return true;
|
|
3968
|
+
}
|
|
3969
|
+
if (latest[index] < current[index]) {
|
|
3970
|
+
return false;
|
|
3971
|
+
}
|
|
3972
|
+
}
|
|
3973
|
+
return false;
|
|
3974
|
+
}
|
|
3975
|
+
function formatUpdateNotice(info) {
|
|
3976
|
+
return `Update available: v${info.latestVersion}
|
|
3977
|
+
Run: npx devsurface@latest`;
|
|
3978
|
+
}
|
|
3979
|
+
function shouldCheckForUpdates() {
|
|
3980
|
+
return process.env.DEVSURFACE_UPDATE_CHECK !== "0" && process.env.CI !== "true";
|
|
3981
|
+
}
|
|
3982
|
+
async function checkForUpdate(currentVersion, fetchImpl = fetch) {
|
|
3983
|
+
if (!shouldCheckForUpdates()) {
|
|
3984
|
+
return null;
|
|
3985
|
+
}
|
|
3986
|
+
const controller = new AbortController();
|
|
3987
|
+
const timeout = setTimeout(() => controller.abort(), UPDATE_CHECK_TIMEOUT_MS);
|
|
3988
|
+
try {
|
|
3989
|
+
const response = await fetchImpl(REGISTRY_LATEST_URL, {
|
|
3990
|
+
headers: {
|
|
3991
|
+
accept: "application/json"
|
|
3992
|
+
},
|
|
3993
|
+
signal: controller.signal
|
|
3994
|
+
});
|
|
3995
|
+
if (!response.ok) {
|
|
3996
|
+
return null;
|
|
3997
|
+
}
|
|
3998
|
+
const body = await response.json();
|
|
3999
|
+
const latestVersion = typeof body.version === "string" ? body.version : null;
|
|
4000
|
+
if (latestVersion === null || !isNewerVersion(latestVersion, currentVersion)) {
|
|
4001
|
+
return null;
|
|
4002
|
+
}
|
|
4003
|
+
return {
|
|
4004
|
+
currentVersion,
|
|
4005
|
+
latestVersion
|
|
4006
|
+
};
|
|
4007
|
+
} catch {
|
|
4008
|
+
return null;
|
|
4009
|
+
} finally {
|
|
4010
|
+
clearTimeout(timeout);
|
|
4011
|
+
}
|
|
4012
|
+
}
|
|
4013
|
+
async function printUpdateNotice(currentVersion) {
|
|
4014
|
+
const update = await checkForUpdate(currentVersion);
|
|
4015
|
+
if (update !== null) {
|
|
4016
|
+
console.log(`
|
|
4017
|
+
${formatUpdateNotice(update)}`);
|
|
4018
|
+
}
|
|
4019
|
+
}
|
|
4020
|
+
|
|
3370
4021
|
// src/cli/index.ts
|
|
3371
4022
|
var program = new Command();
|
|
3372
4023
|
function toPort(value) {
|
|
@@ -3377,7 +4028,9 @@ function toPort(value) {
|
|
|
3377
4028
|
return port;
|
|
3378
4029
|
}
|
|
3379
4030
|
function handle(command) {
|
|
3380
|
-
command.
|
|
4031
|
+
command.then(async () => {
|
|
4032
|
+
await printUpdateNotice(DEV_SURFACE_VERSION);
|
|
4033
|
+
}).catch((error) => {
|
|
3381
4034
|
const message = error instanceof Error ? error.message : String(error);
|
|
3382
4035
|
console.error(message);
|
|
3383
4036
|
process.exitCode = 1;
|
|
@@ -3416,6 +4069,9 @@ program.command("scan").description("Print detected project info.").action(() =>
|
|
|
3416
4069
|
program.command("doctor").description("Print setup health warnings.").action(() => {
|
|
3417
4070
|
handle(doctorCommand(process.cwd()));
|
|
3418
4071
|
});
|
|
4072
|
+
program.command("onboard").description("Print a guided setup checklist with readiness score.").action(() => {
|
|
4073
|
+
handle(onboardCommand(process.cwd()));
|
|
4074
|
+
});
|
|
3419
4075
|
program.command("init").description("Create a starter devsurface.config.json.").action(() => {
|
|
3420
4076
|
handle(initCommand(process.cwd()));
|
|
3421
4077
|
});
|