devsurface 0.4.0 → 0.5.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 +9 -0
- package/README.md +72 -6
- package/dist/cli/index.js +532 -122
- package/dist/cli/index.js.map +1 -1
- package/package.json +21 -2
- package/src/web/dist/assets/{index-BO8glxtu.js → index-DOLQwdCe.js} +1 -1
- package/src/web/dist/index.html +1 -1
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";
|
|
@@ -836,9 +836,64 @@ async function detectGit(root) {
|
|
|
836
836
|
}
|
|
837
837
|
}
|
|
838
838
|
|
|
839
|
-
// src/core/scanner/
|
|
839
|
+
// src/core/scanner/language.ts
|
|
840
840
|
import { promises as fs6 } from "fs";
|
|
841
841
|
import path6 from "path";
|
|
842
|
+
var languageFiles = [
|
|
843
|
+
{ language: "python", candidates: ["requirements.txt", "pyproject.toml", "Pipfile"] },
|
|
844
|
+
{ language: "go", candidates: ["go.mod"] },
|
|
845
|
+
{ language: "java", candidates: ["pom.xml", "build.gradle", "build.gradle.kts"] }
|
|
846
|
+
];
|
|
847
|
+
function isWithinRoot5(root, target) {
|
|
848
|
+
const relative = path6.relative(path6.resolve(root), path6.resolve(target));
|
|
849
|
+
return relative === "" || !relative.startsWith("..") && !path6.isAbsolute(relative);
|
|
850
|
+
}
|
|
851
|
+
async function safeFile(root, candidate) {
|
|
852
|
+
const filePath = path6.join(root, candidate);
|
|
853
|
+
try {
|
|
854
|
+
const [realRoot, stat, realPath] = await Promise.all([
|
|
855
|
+
fs6.realpath(root),
|
|
856
|
+
fs6.stat(filePath),
|
|
857
|
+
fs6.realpath(filePath)
|
|
858
|
+
]);
|
|
859
|
+
if (stat.isFile() && isWithinRoot5(realRoot, realPath)) {
|
|
860
|
+
return realPath;
|
|
861
|
+
}
|
|
862
|
+
} catch {
|
|
863
|
+
return null;
|
|
864
|
+
}
|
|
865
|
+
return null;
|
|
866
|
+
}
|
|
867
|
+
async function detectProjectLanguage(root, packageJson) {
|
|
868
|
+
const detected = [];
|
|
869
|
+
const files = [];
|
|
870
|
+
if (packageJson !== null) {
|
|
871
|
+
detected.push("node");
|
|
872
|
+
files.push(packageJson.path);
|
|
873
|
+
}
|
|
874
|
+
for (const definition of languageFiles) {
|
|
875
|
+
let found = false;
|
|
876
|
+
for (const candidate of definition.candidates) {
|
|
877
|
+
const file = await safeFile(root, candidate);
|
|
878
|
+
if (file !== null) {
|
|
879
|
+
found = true;
|
|
880
|
+
files.push(file);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
if (found) {
|
|
884
|
+
detected.push(definition.language);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
return {
|
|
888
|
+
primary: detected[0] ?? null,
|
|
889
|
+
detected,
|
|
890
|
+
files
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
// src/core/scanner/packageManager.ts
|
|
895
|
+
import { promises as fs7 } from "fs";
|
|
896
|
+
import path7 from "path";
|
|
842
897
|
var lockFiles = [
|
|
843
898
|
{ file: "pnpm-lock.yaml", manager: "pnpm" },
|
|
844
899
|
{ file: "yarn.lock", manager: "yarn" },
|
|
@@ -848,7 +903,7 @@ var lockFiles = [
|
|
|
848
903
|
];
|
|
849
904
|
async function exists(filePath) {
|
|
850
905
|
try {
|
|
851
|
-
await
|
|
906
|
+
await fs7.access(filePath);
|
|
852
907
|
return true;
|
|
853
908
|
} catch {
|
|
854
909
|
return false;
|
|
@@ -856,7 +911,7 @@ async function exists(filePath) {
|
|
|
856
911
|
}
|
|
857
912
|
async function detectPackageManager(root) {
|
|
858
913
|
for (const lockFile of lockFiles) {
|
|
859
|
-
if (await exists(
|
|
914
|
+
if (await exists(path7.join(root, lockFile.file))) {
|
|
860
915
|
return lockFile.manager;
|
|
861
916
|
}
|
|
862
917
|
}
|
|
@@ -864,23 +919,23 @@ async function detectPackageManager(root) {
|
|
|
864
919
|
}
|
|
865
920
|
|
|
866
921
|
// src/core/scanner/packageJson.ts
|
|
867
|
-
import { promises as
|
|
868
|
-
import
|
|
869
|
-
function
|
|
870
|
-
const relative =
|
|
871
|
-
return relative === "" || !relative.startsWith("..") && !
|
|
922
|
+
import { promises as fs8 } from "fs";
|
|
923
|
+
import path8 from "path";
|
|
924
|
+
function isWithinRoot6(root, target) {
|
|
925
|
+
const relative = path8.relative(root, target);
|
|
926
|
+
return relative === "" || !relative.startsWith("..") && !path8.isAbsolute(relative);
|
|
872
927
|
}
|
|
873
928
|
async function readPackageJson(root) {
|
|
874
|
-
const packageJsonPath =
|
|
929
|
+
const packageJsonPath = path8.join(root, "package.json");
|
|
875
930
|
try {
|
|
876
931
|
const [realRoot, realPackageJsonPath] = await Promise.all([
|
|
877
|
-
|
|
878
|
-
|
|
932
|
+
fs8.realpath(root),
|
|
933
|
+
fs8.realpath(packageJsonPath)
|
|
879
934
|
]);
|
|
880
|
-
if (!
|
|
935
|
+
if (!isWithinRoot6(realRoot, realPackageJsonPath)) {
|
|
881
936
|
return null;
|
|
882
937
|
}
|
|
883
|
-
const content = await
|
|
938
|
+
const content = await fs8.readFile(realPackageJsonPath, "utf8");
|
|
884
939
|
const data = JSON.parse(content);
|
|
885
940
|
return { path: realPackageJsonPath, data };
|
|
886
941
|
} catch {
|
|
@@ -966,6 +1021,235 @@ async function detectPorts(ports) {
|
|
|
966
1021
|
);
|
|
967
1022
|
}
|
|
968
1023
|
|
|
1024
|
+
// src/core/scanner/presets.ts
|
|
1025
|
+
import { promises as fs9 } from "fs";
|
|
1026
|
+
import path9 from "path";
|
|
1027
|
+
function dependencyNames(packageJson) {
|
|
1028
|
+
const data = packageJson?.data;
|
|
1029
|
+
return new Set(
|
|
1030
|
+
Object.keys({
|
|
1031
|
+
...data?.dependencies,
|
|
1032
|
+
...data?.devDependencies,
|
|
1033
|
+
...data?.optionalDependencies,
|
|
1034
|
+
...data?.peerDependencies
|
|
1035
|
+
})
|
|
1036
|
+
);
|
|
1037
|
+
}
|
|
1038
|
+
function hasAnyDependency(dependencies, names) {
|
|
1039
|
+
return names.some((name) => dependencies.has(name));
|
|
1040
|
+
}
|
|
1041
|
+
async function readIfPresent2(root, candidate) {
|
|
1042
|
+
const filePath = path9.join(root, candidate);
|
|
1043
|
+
try {
|
|
1044
|
+
const [realRoot, realPath] = await Promise.all([fs9.realpath(root), fs9.realpath(filePath)]);
|
|
1045
|
+
const relative = path9.relative(realRoot, realPath);
|
|
1046
|
+
if (relative.startsWith("..") || path9.isAbsolute(relative)) {
|
|
1047
|
+
return null;
|
|
1048
|
+
}
|
|
1049
|
+
return await fs9.readFile(realPath, "utf8");
|
|
1050
|
+
} catch {
|
|
1051
|
+
return null;
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
function completePreset(draft) {
|
|
1055
|
+
return {
|
|
1056
|
+
name: draft.name,
|
|
1057
|
+
label: draft.label,
|
|
1058
|
+
commands: draft.commands ?? {},
|
|
1059
|
+
groups: draft.groups ?? {},
|
|
1060
|
+
ports: draft.ports ?? []
|
|
1061
|
+
};
|
|
1062
|
+
}
|
|
1063
|
+
function nodePresets(framework, packageJson) {
|
|
1064
|
+
const detected = new Set(framework?.detected ?? []);
|
|
1065
|
+
const dependencies = dependencyNames(packageJson);
|
|
1066
|
+
const presets = [];
|
|
1067
|
+
if (detected.has("Next.js")) {
|
|
1068
|
+
presets.push({
|
|
1069
|
+
name: "next",
|
|
1070
|
+
label: "Next.js",
|
|
1071
|
+
commands: {
|
|
1072
|
+
"next:dev": "next dev",
|
|
1073
|
+
"next:build": "next build",
|
|
1074
|
+
"next:start": "next start"
|
|
1075
|
+
},
|
|
1076
|
+
groups: {
|
|
1077
|
+
"Next.js": ["next:dev", "next:build", "next:start"]
|
|
1078
|
+
},
|
|
1079
|
+
ports: [3e3]
|
|
1080
|
+
});
|
|
1081
|
+
}
|
|
1082
|
+
if (detected.has("Vite")) {
|
|
1083
|
+
presets.push({
|
|
1084
|
+
name: "vite",
|
|
1085
|
+
label: "Vite",
|
|
1086
|
+
commands: {
|
|
1087
|
+
"vite:dev": "vite --host 127.0.0.1",
|
|
1088
|
+
"vite:build": "vite build",
|
|
1089
|
+
"vite:preview": "vite preview --host 127.0.0.1"
|
|
1090
|
+
},
|
|
1091
|
+
groups: {
|
|
1092
|
+
Vite: ["vite:dev", "vite:build", "vite:preview"]
|
|
1093
|
+
},
|
|
1094
|
+
ports: [5173, 4173]
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
if (detected.has("NestJS")) {
|
|
1098
|
+
presets.push({
|
|
1099
|
+
name: "nestjs",
|
|
1100
|
+
label: "NestJS",
|
|
1101
|
+
commands: {
|
|
1102
|
+
"nest:start": "nest start --watch",
|
|
1103
|
+
"nest:build": "nest build"
|
|
1104
|
+
},
|
|
1105
|
+
groups: {
|
|
1106
|
+
NestJS: ["nest:start", "nest:build"]
|
|
1107
|
+
},
|
|
1108
|
+
ports: [3e3]
|
|
1109
|
+
});
|
|
1110
|
+
}
|
|
1111
|
+
if (detected.has("Remix")) {
|
|
1112
|
+
presets.push({
|
|
1113
|
+
name: "remix",
|
|
1114
|
+
label: "Remix",
|
|
1115
|
+
commands: {
|
|
1116
|
+
"remix:dev": "remix vite:dev",
|
|
1117
|
+
"remix:build": "remix vite:build"
|
|
1118
|
+
},
|
|
1119
|
+
groups: {
|
|
1120
|
+
Remix: ["remix:dev", "remix:build"]
|
|
1121
|
+
},
|
|
1122
|
+
ports: [5173]
|
|
1123
|
+
});
|
|
1124
|
+
}
|
|
1125
|
+
if (detected.has("Express") || detected.has("Fastify")) {
|
|
1126
|
+
presets.push({
|
|
1127
|
+
name: detected.has("Fastify") ? "fastify" : "express",
|
|
1128
|
+
label: detected.has("Fastify") ? "Fastify" : "Express",
|
|
1129
|
+
ports: [3e3]
|
|
1130
|
+
});
|
|
1131
|
+
}
|
|
1132
|
+
if (detected.has("Prisma") || hasAnyDependency(dependencies, ["prisma", "@prisma/client"])) {
|
|
1133
|
+
presets.push({
|
|
1134
|
+
name: "prisma",
|
|
1135
|
+
label: "Prisma",
|
|
1136
|
+
commands: {
|
|
1137
|
+
"prisma:migrate": "prisma migrate dev",
|
|
1138
|
+
"prisma:studio": "prisma studio"
|
|
1139
|
+
},
|
|
1140
|
+
groups: {
|
|
1141
|
+
Database: ["prisma:migrate", "prisma:studio"]
|
|
1142
|
+
},
|
|
1143
|
+
ports: [5555]
|
|
1144
|
+
});
|
|
1145
|
+
}
|
|
1146
|
+
return presets.map(completePreset);
|
|
1147
|
+
}
|
|
1148
|
+
async function pythonPresets(root, language) {
|
|
1149
|
+
if (!language.detected.includes("python")) {
|
|
1150
|
+
return [];
|
|
1151
|
+
}
|
|
1152
|
+
const [requirements, pyproject, pipfile] = await Promise.all([
|
|
1153
|
+
readIfPresent2(root, "requirements.txt"),
|
|
1154
|
+
readIfPresent2(root, "pyproject.toml"),
|
|
1155
|
+
readIfPresent2(root, "Pipfile")
|
|
1156
|
+
]);
|
|
1157
|
+
const manifest = [requirements, pyproject, pipfile].filter(Boolean).join("\n").toLowerCase();
|
|
1158
|
+
const commands = {};
|
|
1159
|
+
const groups = {};
|
|
1160
|
+
const ports = [];
|
|
1161
|
+
if (requirements !== null) {
|
|
1162
|
+
commands["python:install"] = "python -m pip install -r requirements.txt";
|
|
1163
|
+
groups.Setup = ["python:install"];
|
|
1164
|
+
}
|
|
1165
|
+
if (manifest.includes("uvicorn") || manifest.includes("fastapi")) {
|
|
1166
|
+
commands["python:dev"] = "uvicorn main:app --reload --host 127.0.0.1";
|
|
1167
|
+
groups.Python = [...groups.Python ?? [], "python:dev"];
|
|
1168
|
+
ports.push(8e3);
|
|
1169
|
+
}
|
|
1170
|
+
if (manifest.includes("flask")) {
|
|
1171
|
+
commands["flask:dev"] = "flask --app app run --host 127.0.0.1";
|
|
1172
|
+
groups.Python = [...groups.Python ?? [], "flask:dev"];
|
|
1173
|
+
ports.push(5e3);
|
|
1174
|
+
}
|
|
1175
|
+
if (manifest.includes("django") || await readIfPresent2(root, "manage.py") !== null) {
|
|
1176
|
+
commands["django:dev"] = "python manage.py runserver 127.0.0.1:8000";
|
|
1177
|
+
commands["django:migrate"] = "python manage.py migrate";
|
|
1178
|
+
groups.Python = [...groups.Python ?? [], "django:dev", "django:migrate"];
|
|
1179
|
+
ports.push(8e3);
|
|
1180
|
+
}
|
|
1181
|
+
return [
|
|
1182
|
+
completePreset({
|
|
1183
|
+
name: "python",
|
|
1184
|
+
label: "Python",
|
|
1185
|
+
commands,
|
|
1186
|
+
groups,
|
|
1187
|
+
ports
|
|
1188
|
+
})
|
|
1189
|
+
];
|
|
1190
|
+
}
|
|
1191
|
+
function goPresets(language) {
|
|
1192
|
+
if (!language.detected.includes("go")) {
|
|
1193
|
+
return [];
|
|
1194
|
+
}
|
|
1195
|
+
return [
|
|
1196
|
+
completePreset({
|
|
1197
|
+
name: "go",
|
|
1198
|
+
label: "Go",
|
|
1199
|
+
commands: {
|
|
1200
|
+
"go:run": "go run .",
|
|
1201
|
+
"go:build": "go build ./...",
|
|
1202
|
+
"go:test": "go test ./..."
|
|
1203
|
+
},
|
|
1204
|
+
groups: {
|
|
1205
|
+
Go: ["go:run", "go:build", "go:test"]
|
|
1206
|
+
}
|
|
1207
|
+
})
|
|
1208
|
+
];
|
|
1209
|
+
}
|
|
1210
|
+
async function javaPresets(language) {
|
|
1211
|
+
if (!language.detected.includes("java")) {
|
|
1212
|
+
return [];
|
|
1213
|
+
}
|
|
1214
|
+
const hasMaven = language.files.some((file) => path9.basename(file) === "pom.xml");
|
|
1215
|
+
const hasGradle = language.files.some((file) => path9.basename(file).startsWith("build.gradle"));
|
|
1216
|
+
const commands = {};
|
|
1217
|
+
const groups = {};
|
|
1218
|
+
if (hasMaven) {
|
|
1219
|
+
commands["maven:test"] = "mvn test";
|
|
1220
|
+
commands["maven:package"] = "mvn package";
|
|
1221
|
+
groups.Maven = ["maven:test", "maven:package"];
|
|
1222
|
+
}
|
|
1223
|
+
if (hasGradle) {
|
|
1224
|
+
commands["gradle:test"] = "gradle test";
|
|
1225
|
+
commands["gradle:build"] = "gradle build";
|
|
1226
|
+
groups.Gradle = ["gradle:test", "gradle:build"];
|
|
1227
|
+
}
|
|
1228
|
+
return [completePreset({ name: "java", label: "Java", commands, groups })];
|
|
1229
|
+
}
|
|
1230
|
+
async function detectPresets(options) {
|
|
1231
|
+
return [
|
|
1232
|
+
...nodePresets(options.framework, options.packageJson),
|
|
1233
|
+
...await pythonPresets(options.root, options.language),
|
|
1234
|
+
...goPresets(options.language),
|
|
1235
|
+
...await javaPresets(options.language)
|
|
1236
|
+
].filter(
|
|
1237
|
+
(preset) => Object.keys(preset.commands).length > 0 || Object.keys(preset.groups).length > 0 || preset.ports.length > 0
|
|
1238
|
+
);
|
|
1239
|
+
}
|
|
1240
|
+
function mergePresetCommands(presets) {
|
|
1241
|
+
return Object.assign({}, ...presets.map((preset) => preset.commands));
|
|
1242
|
+
}
|
|
1243
|
+
function mergePresetGroups(presets) {
|
|
1244
|
+
const groups = {};
|
|
1245
|
+
for (const preset of presets) {
|
|
1246
|
+
for (const [group, commands] of Object.entries(preset.groups)) {
|
|
1247
|
+
groups[group] = [...groups[group] ?? [], ...commands];
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
return groups;
|
|
1251
|
+
}
|
|
1252
|
+
|
|
969
1253
|
// src/core/scanner/scripts.ts
|
|
970
1254
|
function extractScripts(packageJson) {
|
|
971
1255
|
if (!packageJson?.data.scripts || typeof packageJson.data.scripts !== "object" || Array.isArray(packageJson.data.scripts)) {
|
|
@@ -980,17 +1264,17 @@ function extractScripts(packageJson) {
|
|
|
980
1264
|
}
|
|
981
1265
|
|
|
982
1266
|
// src/core/scanner/index.ts
|
|
983
|
-
function
|
|
984
|
-
const relative =
|
|
985
|
-
return relative === "" || !relative.startsWith("..") && !
|
|
1267
|
+
function isWithinRoot7(root, target) {
|
|
1268
|
+
const relative = path10.relative(path10.resolve(root), path10.resolve(target));
|
|
1269
|
+
return relative === "" || !relative.startsWith("..") && !path10.isAbsolute(relative);
|
|
986
1270
|
}
|
|
987
1271
|
async function findFirstFile(root, candidates) {
|
|
988
|
-
const resolvedRoot = await
|
|
1272
|
+
const resolvedRoot = await fs10.realpath(root).catch(() => path10.resolve(root));
|
|
989
1273
|
for (const candidate of candidates) {
|
|
990
|
-
const filePath =
|
|
1274
|
+
const filePath = path10.join(root, candidate);
|
|
991
1275
|
try {
|
|
992
|
-
const [stat, realPath] = await Promise.all([
|
|
993
|
-
if (stat.isFile() &&
|
|
1276
|
+
const [stat, realPath] = await Promise.all([fs10.stat(filePath), fs10.realpath(filePath)]);
|
|
1277
|
+
if (stat.isFile() && isWithinRoot7(resolvedRoot, realPath)) {
|
|
994
1278
|
return { path: realPath, exists: true };
|
|
995
1279
|
}
|
|
996
1280
|
} catch {
|
|
@@ -1002,15 +1286,25 @@ function configuredPorts(configPorts) {
|
|
|
1002
1286
|
return Array.isArray(configPorts) ? configPorts : [];
|
|
1003
1287
|
}
|
|
1004
1288
|
async function scanProject(root = process.cwd()) {
|
|
1005
|
-
const resolvedRoot = await
|
|
1289
|
+
const resolvedRoot = await fs10.realpath(root).catch(() => path10.resolve(root));
|
|
1006
1290
|
const config = await loadConfig(resolvedRoot);
|
|
1007
1291
|
const packageJson = await readPackageJson(resolvedRoot);
|
|
1008
1292
|
const scripts = extractScripts(packageJson) ?? {};
|
|
1009
1293
|
const framework = detectFramework(packageJson);
|
|
1294
|
+
const language = await detectProjectLanguage(resolvedRoot, packageJson);
|
|
1295
|
+
const presets = await detectPresets({
|
|
1296
|
+
root: resolvedRoot,
|
|
1297
|
+
packageJson,
|
|
1298
|
+
framework,
|
|
1299
|
+
language
|
|
1300
|
+
});
|
|
1301
|
+
const presetCommands = mergePresetCommands(presets);
|
|
1302
|
+
const presetGroups = mergePresetGroups(presets);
|
|
1010
1303
|
const portsToProbe = [
|
|
1011
1304
|
...configuredPorts(config?.config.ports),
|
|
1012
1305
|
...inferPortsFromScripts(scripts),
|
|
1013
|
-
...defaultPortsForFramework(framework)
|
|
1306
|
+
...defaultPortsForFramework(framework),
|
|
1307
|
+
...presets.flatMap((preset) => preset.ports)
|
|
1014
1308
|
];
|
|
1015
1309
|
const [packageManager, env, docker, git, ports, readme, license] = await Promise.all([
|
|
1016
1310
|
detectPackageManager(resolvedRoot),
|
|
@@ -1023,14 +1317,18 @@ async function scanProject(root = process.cwd()) {
|
|
|
1023
1317
|
]);
|
|
1024
1318
|
return {
|
|
1025
1319
|
root: resolvedRoot,
|
|
1026
|
-
projectName: config?.config.name ?? packageJson?.data.name ??
|
|
1320
|
+
projectName: config?.config.name ?? packageJson?.data.name ?? path10.basename(resolvedRoot),
|
|
1027
1321
|
packageJson,
|
|
1028
1322
|
packageManager: packageManager ?? (packageJson ? "npm" : null),
|
|
1323
|
+
language,
|
|
1029
1324
|
scripts,
|
|
1030
1325
|
env,
|
|
1031
1326
|
docker,
|
|
1032
1327
|
git,
|
|
1033
1328
|
framework,
|
|
1329
|
+
presets,
|
|
1330
|
+
presetCommands,
|
|
1331
|
+
presetGroups,
|
|
1034
1332
|
ports: ports ?? [],
|
|
1035
1333
|
readme,
|
|
1036
1334
|
license,
|
|
@@ -1041,18 +1339,18 @@ async function scanProject(root = process.cwd()) {
|
|
|
1041
1339
|
// src/core/doctor/index.ts
|
|
1042
1340
|
async function pathExists(filePath) {
|
|
1043
1341
|
try {
|
|
1044
|
-
await
|
|
1342
|
+
await fs11.access(filePath);
|
|
1045
1343
|
return true;
|
|
1046
1344
|
} catch {
|
|
1047
1345
|
return false;
|
|
1048
1346
|
}
|
|
1049
1347
|
}
|
|
1050
|
-
async function
|
|
1348
|
+
async function readIfPresent3(filePath) {
|
|
1051
1349
|
if (filePath === null) {
|
|
1052
1350
|
return null;
|
|
1053
1351
|
}
|
|
1054
1352
|
try {
|
|
1055
|
-
return await
|
|
1353
|
+
return await fs11.readFile(filePath, "utf8");
|
|
1056
1354
|
} catch {
|
|
1057
1355
|
return null;
|
|
1058
1356
|
}
|
|
@@ -1068,7 +1366,9 @@ async function runDoctor(root = process.cwd(), scan) {
|
|
|
1068
1366
|
warning("config-warning", "warning", "Config warning", configWarning, result.config?.path)
|
|
1069
1367
|
);
|
|
1070
1368
|
}
|
|
1071
|
-
|
|
1369
|
+
const isNodeProject = result.language.detected.includes("node");
|
|
1370
|
+
const hasKnownProjectLanguage = result.language.detected.length > 0;
|
|
1371
|
+
if (result.packageJson === null && !hasKnownProjectLanguage) {
|
|
1072
1372
|
warnings.push(
|
|
1073
1373
|
warning(
|
|
1074
1374
|
"missing-package-json",
|
|
@@ -1079,7 +1379,7 @@ async function runDoctor(root = process.cwd(), scan) {
|
|
|
1079
1379
|
);
|
|
1080
1380
|
return warnings;
|
|
1081
1381
|
}
|
|
1082
|
-
if (!await pathExists(
|
|
1382
|
+
if (isNodeProject && !await pathExists(path11.join(root, "node_modules", ".bin"))) {
|
|
1083
1383
|
warnings.push(
|
|
1084
1384
|
warning(
|
|
1085
1385
|
"missing-node-modules",
|
|
@@ -1126,7 +1426,7 @@ async function runDoctor(root = process.cwd(), scan) {
|
|
|
1126
1426
|
warning("missing-readme", "warning", "No README", "No README.md or README file was found.")
|
|
1127
1427
|
);
|
|
1128
1428
|
} else {
|
|
1129
|
-
const readme = await
|
|
1429
|
+
const readme = await readIfPresent3(result.readme.path);
|
|
1130
1430
|
if (readme !== null) {
|
|
1131
1431
|
const references = extractScriptReferences(readme);
|
|
1132
1432
|
const missingScripts = references.filter((script) => result.scripts[script] === void 0);
|
|
@@ -1162,7 +1462,7 @@ async function runDoctor(root = process.cwd(), scan) {
|
|
|
1162
1462
|
)
|
|
1163
1463
|
);
|
|
1164
1464
|
}
|
|
1165
|
-
if (result.scripts.test === void 0) {
|
|
1465
|
+
if (isNodeProject && result.scripts.test === void 0) {
|
|
1166
1466
|
warnings.push(
|
|
1167
1467
|
warning(
|
|
1168
1468
|
"missing-test-script",
|
|
@@ -1172,7 +1472,7 @@ async function runDoctor(root = process.cwd(), scan) {
|
|
|
1172
1472
|
)
|
|
1173
1473
|
);
|
|
1174
1474
|
}
|
|
1175
|
-
if (result.scripts.build === void 0) {
|
|
1475
|
+
if (isNodeProject && result.scripts.build === void 0) {
|
|
1176
1476
|
warnings.push(
|
|
1177
1477
|
warning(
|
|
1178
1478
|
"missing-build-script",
|
|
@@ -1233,17 +1533,17 @@ async function doctorCommand(cwd = process.cwd()) {
|
|
|
1233
1533
|
}
|
|
1234
1534
|
|
|
1235
1535
|
// src/cli/commands/init.ts
|
|
1236
|
-
import { promises as
|
|
1237
|
-
import
|
|
1536
|
+
import { promises as fs12 } from "fs";
|
|
1537
|
+
import path12 from "path";
|
|
1238
1538
|
import pc2 from "picocolors";
|
|
1239
1539
|
async function initCommand(cwd = process.cwd()) {
|
|
1240
|
-
const configPath =
|
|
1540
|
+
const configPath = path12.join(cwd, CONFIG_FILE_NAME);
|
|
1241
1541
|
try {
|
|
1242
|
-
await
|
|
1542
|
+
await fs12.access(configPath);
|
|
1243
1543
|
console.log(pc2.yellow(`${CONFIG_FILE_NAME} already exists.`));
|
|
1244
1544
|
return;
|
|
1245
1545
|
} catch {
|
|
1246
|
-
await
|
|
1546
|
+
await fs12.writeFile(configPath, `${JSON.stringify(defaultConfig, null, 2)}
|
|
1247
1547
|
`, "utf8");
|
|
1248
1548
|
console.log(pc2.green(`Created ${CONFIG_FILE_NAME}.`));
|
|
1249
1549
|
}
|
|
@@ -1430,26 +1730,60 @@ async function runPackageScriptToTerminal(options) {
|
|
|
1430
1730
|
});
|
|
1431
1731
|
});
|
|
1432
1732
|
}
|
|
1733
|
+
async function runConfiguredCommandToTerminal(options) {
|
|
1734
|
+
const resolvedCommand = await resolveConfiguredCommand(options.cwd, options.command);
|
|
1735
|
+
if (resolvedCommand === null) {
|
|
1736
|
+
return 1;
|
|
1737
|
+
}
|
|
1738
|
+
return await new Promise((resolve) => {
|
|
1739
|
+
const child = spawn2(resolvedCommand.command, resolvedCommand.args, {
|
|
1740
|
+
cwd: options.cwd,
|
|
1741
|
+
stdio: "inherit",
|
|
1742
|
+
windowsHide: true
|
|
1743
|
+
});
|
|
1744
|
+
child.on("error", () => {
|
|
1745
|
+
resolve(1);
|
|
1746
|
+
});
|
|
1747
|
+
child.on("close", (code) => {
|
|
1748
|
+
resolve(code ?? 1);
|
|
1749
|
+
});
|
|
1750
|
+
});
|
|
1751
|
+
}
|
|
1433
1752
|
|
|
1434
1753
|
// src/cli/commands/run.ts
|
|
1435
1754
|
async function runCommand(script, cwd = process.cwd()) {
|
|
1436
1755
|
const scan = await scanProject(cwd);
|
|
1437
|
-
if (scan.
|
|
1438
|
-
|
|
1439
|
-
|
|
1756
|
+
if (scan.scripts[script] !== void 0) {
|
|
1757
|
+
const exitCode = await runPackageScriptToTerminal({
|
|
1758
|
+
cwd,
|
|
1759
|
+
packageManager: scan.packageManager,
|
|
1760
|
+
script
|
|
1761
|
+
});
|
|
1762
|
+
process.exitCode = exitCode;
|
|
1440
1763
|
return;
|
|
1441
1764
|
}
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1765
|
+
const configuredCommand = scan.config?.config.commands?.[script] ?? scan.presetCommands[script];
|
|
1766
|
+
if (configuredCommand !== void 0) {
|
|
1767
|
+
if (isDangerousCommand(configuredCommand)) {
|
|
1768
|
+
console.error(pc3.red(`Refusing to run dangerous command "${safeDisplayText(script)}".`));
|
|
1769
|
+
process.exitCode = 1;
|
|
1770
|
+
return;
|
|
1771
|
+
}
|
|
1772
|
+
const exitCode = await runConfiguredCommandToTerminal({
|
|
1773
|
+
cwd,
|
|
1774
|
+
command: configuredCommand
|
|
1775
|
+
});
|
|
1776
|
+
process.exitCode = exitCode;
|
|
1445
1777
|
return;
|
|
1446
1778
|
}
|
|
1447
|
-
const
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1779
|
+
const available = [
|
|
1780
|
+
...Object.keys(scan.scripts),
|
|
1781
|
+
...Object.keys(scan.config?.config.commands ?? {}),
|
|
1782
|
+
...Object.keys(scan.presetCommands)
|
|
1783
|
+
];
|
|
1784
|
+
const hint = available.length > 0 ? ` Available commands: ${safeDisplayList(available)}.` : "";
|
|
1785
|
+
console.error(pc3.red(`Command "${safeDisplayText(script)}" was not found.${hint}`));
|
|
1786
|
+
process.exitCode = 1;
|
|
1453
1787
|
}
|
|
1454
1788
|
|
|
1455
1789
|
// src/cli/commands/scan.ts
|
|
@@ -1459,9 +1793,11 @@ function formatList(values) {
|
|
|
1459
1793
|
}
|
|
1460
1794
|
function printScanResult(scan) {
|
|
1461
1795
|
console.log(pc4.bold(`Project: ${safeDisplayText(scan.projectName)}`));
|
|
1796
|
+
console.log(`Language: ${formatList(scan.language.detected) || "unknown"}`);
|
|
1462
1797
|
console.log(`Type: ${safeDisplayText(scan.framework?.type ?? "Unknown")}`);
|
|
1463
1798
|
console.log(`Manager: ${safeDisplayText(scan.packageManager ?? "unknown")}`);
|
|
1464
1799
|
console.log(`Scripts: ${formatList(Object.keys(scan.scripts))}`);
|
|
1800
|
+
console.log(`Presets: ${formatList(scan.presets.map((preset) => preset.label)) || "none"}`);
|
|
1465
1801
|
console.log(`Git: ${safeDisplayText(scan.git?.branch ?? "not detected")}`);
|
|
1466
1802
|
console.log(`README: ${scan.readme.exists ? "found" : "missing"}`);
|
|
1467
1803
|
console.log(`LICENSE: ${scan.license.exists ? "found" : "missing"}`);
|
|
@@ -1488,30 +1824,30 @@ import pc5 from "picocolors";
|
|
|
1488
1824
|
// node_modules/open/index.js
|
|
1489
1825
|
import process7 from "process";
|
|
1490
1826
|
import { Buffer as Buffer2 } from "buffer";
|
|
1491
|
-
import
|
|
1827
|
+
import path13 from "path";
|
|
1492
1828
|
import { fileURLToPath } from "url";
|
|
1493
1829
|
import { promisify as promisify5 } from "util";
|
|
1494
1830
|
import childProcess from "child_process";
|
|
1495
|
-
import
|
|
1831
|
+
import fs17, { constants as fsConstants2 } from "fs/promises";
|
|
1496
1832
|
|
|
1497
1833
|
// node_modules/wsl-utils/index.js
|
|
1498
1834
|
import process3 from "process";
|
|
1499
|
-
import
|
|
1835
|
+
import fs16, { constants as fsConstants } from "fs/promises";
|
|
1500
1836
|
|
|
1501
1837
|
// node_modules/is-wsl/index.js
|
|
1502
1838
|
import process2 from "process";
|
|
1503
1839
|
import os2 from "os";
|
|
1504
|
-
import
|
|
1840
|
+
import fs15 from "fs";
|
|
1505
1841
|
|
|
1506
1842
|
// node_modules/is-inside-container/index.js
|
|
1507
|
-
import
|
|
1843
|
+
import fs14 from "fs";
|
|
1508
1844
|
|
|
1509
1845
|
// node_modules/is-docker/index.js
|
|
1510
|
-
import
|
|
1846
|
+
import fs13 from "fs";
|
|
1511
1847
|
var isDockerCached;
|
|
1512
1848
|
function hasDockerEnv() {
|
|
1513
1849
|
try {
|
|
1514
|
-
|
|
1850
|
+
fs13.statSync("/.dockerenv");
|
|
1515
1851
|
return true;
|
|
1516
1852
|
} catch {
|
|
1517
1853
|
return false;
|
|
@@ -1519,7 +1855,7 @@ function hasDockerEnv() {
|
|
|
1519
1855
|
}
|
|
1520
1856
|
function hasDockerCGroup() {
|
|
1521
1857
|
try {
|
|
1522
|
-
return
|
|
1858
|
+
return fs13.readFileSync("/proc/self/cgroup", "utf8").includes("docker");
|
|
1523
1859
|
} catch {
|
|
1524
1860
|
return false;
|
|
1525
1861
|
}
|
|
@@ -1535,7 +1871,7 @@ function isDocker() {
|
|
|
1535
1871
|
var cachedResult;
|
|
1536
1872
|
var hasContainerEnv = () => {
|
|
1537
1873
|
try {
|
|
1538
|
-
|
|
1874
|
+
fs14.statSync("/run/.containerenv");
|
|
1539
1875
|
return true;
|
|
1540
1876
|
} catch {
|
|
1541
1877
|
return false;
|
|
@@ -1560,12 +1896,12 @@ var isWsl = () => {
|
|
|
1560
1896
|
return true;
|
|
1561
1897
|
}
|
|
1562
1898
|
try {
|
|
1563
|
-
if (
|
|
1899
|
+
if (fs15.readFileSync("/proc/version", "utf8").toLowerCase().includes("microsoft")) {
|
|
1564
1900
|
return !isInsideContainer();
|
|
1565
1901
|
}
|
|
1566
1902
|
} catch {
|
|
1567
1903
|
}
|
|
1568
|
-
if (
|
|
1904
|
+
if (fs15.existsSync("/proc/sys/fs/binfmt_misc/WSLInterop") || fs15.existsSync("/run/WSL")) {
|
|
1569
1905
|
return !isInsideContainer();
|
|
1570
1906
|
}
|
|
1571
1907
|
return false;
|
|
@@ -1583,14 +1919,14 @@ var wslDrivesMountPoint = /* @__PURE__ */ (() => {
|
|
|
1583
1919
|
const configFilePath = "/etc/wsl.conf";
|
|
1584
1920
|
let isConfigFileExists = false;
|
|
1585
1921
|
try {
|
|
1586
|
-
await
|
|
1922
|
+
await fs16.access(configFilePath, fsConstants.F_OK);
|
|
1587
1923
|
isConfigFileExists = true;
|
|
1588
1924
|
} catch {
|
|
1589
1925
|
}
|
|
1590
1926
|
if (!isConfigFileExists) {
|
|
1591
1927
|
return defaultMountPoint;
|
|
1592
1928
|
}
|
|
1593
|
-
const configContent = await
|
|
1929
|
+
const configContent = await fs16.readFile(configFilePath, { encoding: "utf8" });
|
|
1594
1930
|
const configMountPoint = /(?<!#.*)root\s*=\s*(?<mountPoint>.*)/g.exec(configContent);
|
|
1595
1931
|
if (!configMountPoint) {
|
|
1596
1932
|
return defaultMountPoint;
|
|
@@ -1744,8 +2080,8 @@ async function defaultBrowser2() {
|
|
|
1744
2080
|
|
|
1745
2081
|
// node_modules/open/index.js
|
|
1746
2082
|
var execFile5 = promisify5(childProcess.execFile);
|
|
1747
|
-
var __dirname =
|
|
1748
|
-
var localXdgOpenPath =
|
|
2083
|
+
var __dirname = path13.dirname(fileURLToPath(import.meta.url));
|
|
2084
|
+
var localXdgOpenPath = path13.join(__dirname, "xdg-open");
|
|
1749
2085
|
var { platform, arch } = process7;
|
|
1750
2086
|
async function getWindowsDefaultBrowserFromWsl() {
|
|
1751
2087
|
const powershellPath = await powerShellPath();
|
|
@@ -1895,7 +2231,7 @@ var baseOpen = async (options) => {
|
|
|
1895
2231
|
const isBundled = !__dirname || __dirname === "/";
|
|
1896
2232
|
let exeLocalXdgOpen = false;
|
|
1897
2233
|
try {
|
|
1898
|
-
await
|
|
2234
|
+
await fs17.access(localXdgOpenPath, fsConstants2.X_OK);
|
|
1899
2235
|
exeLocalXdgOpen = true;
|
|
1900
2236
|
} catch {
|
|
1901
2237
|
}
|
|
@@ -2000,8 +2336,8 @@ defineLazyProperty(apps, "browserPrivate", () => "browserPrivate");
|
|
|
2000
2336
|
var open_default = open;
|
|
2001
2337
|
|
|
2002
2338
|
// src/server/index.ts
|
|
2003
|
-
import { promises as
|
|
2004
|
-
import
|
|
2339
|
+
import { promises as fs21 } from "fs";
|
|
2340
|
+
import path17 from "path";
|
|
2005
2341
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2006
2342
|
import { createAdaptorServer } from "@hono/node-server";
|
|
2007
2343
|
import { serveStatic } from "@hono/node-server/serve-static";
|
|
@@ -2151,16 +2487,16 @@ var ProcessManager = class extends EventEmitter {
|
|
|
2151
2487
|
|
|
2152
2488
|
// src/core/hub/registry.ts
|
|
2153
2489
|
import { createHash } from "crypto";
|
|
2154
|
-
import { promises as
|
|
2490
|
+
import { promises as fs19 } from "fs";
|
|
2155
2491
|
import os3 from "os";
|
|
2156
|
-
import
|
|
2492
|
+
import path15 from "path";
|
|
2157
2493
|
|
|
2158
2494
|
// src/core/hub/workspaceRoots.ts
|
|
2159
|
-
import { promises as
|
|
2160
|
-
import
|
|
2161
|
-
function
|
|
2162
|
-
const relative =
|
|
2163
|
-
return relative === "" || !relative.startsWith("..") && !
|
|
2495
|
+
import { promises as fs18 } from "fs";
|
|
2496
|
+
import path14 from "path";
|
|
2497
|
+
function isWithinRoot8(root, target) {
|
|
2498
|
+
const relative = path14.relative(root, target);
|
|
2499
|
+
return relative === "" || !relative.startsWith("..") && !path14.isAbsolute(relative);
|
|
2164
2500
|
}
|
|
2165
2501
|
async function configuredWorkspaceRoots() {
|
|
2166
2502
|
const raw = process.env.DEVSURFACE_WORKSPACE_ROOTS;
|
|
@@ -2174,7 +2510,7 @@ async function configuredWorkspaceRoots() {
|
|
|
2174
2510
|
continue;
|
|
2175
2511
|
}
|
|
2176
2512
|
try {
|
|
2177
|
-
roots.push(await
|
|
2513
|
+
roots.push(await fs18.realpath(path14.resolve(trimmed)));
|
|
2178
2514
|
} catch {
|
|
2179
2515
|
}
|
|
2180
2516
|
}
|
|
@@ -2186,7 +2522,7 @@ async function assertWithinWorkspaceRoots(targetPath) {
|
|
|
2186
2522
|
return;
|
|
2187
2523
|
}
|
|
2188
2524
|
for (const root of roots) {
|
|
2189
|
-
if (
|
|
2525
|
+
if (isWithinRoot8(root, targetPath)) {
|
|
2190
2526
|
return;
|
|
2191
2527
|
}
|
|
2192
2528
|
}
|
|
@@ -2195,16 +2531,16 @@ async function assertWithinWorkspaceRoots(targetPath) {
|
|
|
2195
2531
|
|
|
2196
2532
|
// src/core/hub/registry.ts
|
|
2197
2533
|
function workspaceId(realPath) {
|
|
2198
|
-
const base =
|
|
2534
|
+
const base = path15.basename(realPath).replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 32) || "workspace";
|
|
2199
2535
|
const hash = createHash("sha256").update(realPath).digest("hex").slice(0, 6);
|
|
2200
2536
|
return `${base}-${hash}`;
|
|
2201
2537
|
}
|
|
2202
2538
|
function defaultDataDir() {
|
|
2203
|
-
return process.env.DEVSURFACE_DATA_DIR ??
|
|
2539
|
+
return process.env.DEVSURFACE_DATA_DIR ?? path15.join(os3.homedir(), ".devsurface");
|
|
2204
2540
|
}
|
|
2205
2541
|
async function readPackageName(dirPath) {
|
|
2206
2542
|
try {
|
|
2207
|
-
const raw = JSON.parse(await
|
|
2543
|
+
const raw = JSON.parse(await fs19.readFile(path15.join(dirPath, "package.json"), "utf8"));
|
|
2208
2544
|
return typeof raw?.name === "string" && raw.name.length > 0 ? raw.name : null;
|
|
2209
2545
|
} catch {
|
|
2210
2546
|
return null;
|
|
@@ -2215,7 +2551,7 @@ var WorkspaceRegistry = class {
|
|
|
2215
2551
|
seeded = false;
|
|
2216
2552
|
constructor(dataDir) {
|
|
2217
2553
|
const dir = dataDir ?? defaultDataDir();
|
|
2218
|
-
this.filePath =
|
|
2554
|
+
this.filePath = path15.join(dir, "workspaces.json");
|
|
2219
2555
|
}
|
|
2220
2556
|
async list() {
|
|
2221
2557
|
await this.seedFromEnv();
|
|
@@ -2229,7 +2565,7 @@ var WorkspaceRegistry = class {
|
|
|
2229
2565
|
if (existing) {
|
|
2230
2566
|
return existing;
|
|
2231
2567
|
}
|
|
2232
|
-
const name = await readPackageName(realDir) ??
|
|
2568
|
+
const name = await readPackageName(realDir) ?? path15.basename(realDir);
|
|
2233
2569
|
const entry = {
|
|
2234
2570
|
id: workspaceId(realDir),
|
|
2235
2571
|
name,
|
|
@@ -2251,7 +2587,7 @@ var WorkspaceRegistry = class {
|
|
|
2251
2587
|
}
|
|
2252
2588
|
async findByPath(dirPath) {
|
|
2253
2589
|
try {
|
|
2254
|
-
const realDir = await
|
|
2590
|
+
const realDir = await fs19.realpath(path15.resolve(dirPath));
|
|
2255
2591
|
const entries = await this.read();
|
|
2256
2592
|
return entries.find((entry) => entry.path === realDir) ?? null;
|
|
2257
2593
|
} catch {
|
|
@@ -2279,9 +2615,9 @@ var WorkspaceRegistry = class {
|
|
|
2279
2615
|
}
|
|
2280
2616
|
}
|
|
2281
2617
|
async resolveDir(dirPath) {
|
|
2282
|
-
const resolved =
|
|
2283
|
-
const realDir = await
|
|
2284
|
-
const stat = await
|
|
2618
|
+
const resolved = path15.resolve(dirPath);
|
|
2619
|
+
const realDir = await fs19.realpath(resolved);
|
|
2620
|
+
const stat = await fs19.stat(realDir);
|
|
2285
2621
|
if (!stat.isDirectory()) {
|
|
2286
2622
|
throw new Error(`${dirPath} is not a directory.`);
|
|
2287
2623
|
}
|
|
@@ -2289,7 +2625,7 @@ var WorkspaceRegistry = class {
|
|
|
2289
2625
|
}
|
|
2290
2626
|
async read() {
|
|
2291
2627
|
try {
|
|
2292
|
-
const content = await
|
|
2628
|
+
const content = await fs19.readFile(this.filePath, "utf8");
|
|
2293
2629
|
const parsed = JSON.parse(content);
|
|
2294
2630
|
return Array.isArray(parsed) ? parsed : [];
|
|
2295
2631
|
} catch {
|
|
@@ -2297,8 +2633,8 @@ var WorkspaceRegistry = class {
|
|
|
2297
2633
|
}
|
|
2298
2634
|
}
|
|
2299
2635
|
async write(entries) {
|
|
2300
|
-
await
|
|
2301
|
-
await
|
|
2636
|
+
await fs19.mkdir(path15.dirname(this.filePath), { recursive: true });
|
|
2637
|
+
await fs19.writeFile(this.filePath, JSON.stringify(entries, null, 2) + "\n", "utf8");
|
|
2302
2638
|
}
|
|
2303
2639
|
async seedFromEnv() {
|
|
2304
2640
|
if (this.seeded) {
|
|
@@ -2380,12 +2716,12 @@ var Hub = class {
|
|
|
2380
2716
|
|
|
2381
2717
|
// src/server/routes/api.ts
|
|
2382
2718
|
import { constants as constants2, existsSync } from "fs";
|
|
2383
|
-
import { promises as
|
|
2384
|
-
import
|
|
2719
|
+
import { promises as fs20 } from "fs";
|
|
2720
|
+
import path16 from "path";
|
|
2385
2721
|
import spawn4 from "cross-spawn";
|
|
2386
2722
|
|
|
2387
2723
|
// src/version.ts
|
|
2388
|
-
var DEV_SURFACE_VERSION = "0.
|
|
2724
|
+
var DEV_SURFACE_VERSION = "0.5.0";
|
|
2389
2725
|
|
|
2390
2726
|
// src/server/localAccess.ts
|
|
2391
2727
|
var LOCAL_HOSTNAMES = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "::1"]);
|
|
@@ -2571,9 +2907,9 @@ function isAllowedTerminalCommand(command) {
|
|
|
2571
2907
|
}
|
|
2572
2908
|
|
|
2573
2909
|
// src/server/routes/api.ts
|
|
2574
|
-
function
|
|
2575
|
-
const relative =
|
|
2576
|
-
return relative === "" || !relative.startsWith("..") && !
|
|
2910
|
+
function isWithinRoot9(root, target) {
|
|
2911
|
+
const relative = path16.relative(path16.resolve(root), path16.resolve(target));
|
|
2912
|
+
return relative === "" || !relative.startsWith("..") && !path16.isAbsolute(relative);
|
|
2577
2913
|
}
|
|
2578
2914
|
function isAllowedMutationOrigin(requestUrl, origin) {
|
|
2579
2915
|
if (origin === null) {
|
|
@@ -2605,35 +2941,35 @@ function hasMutationIntent(intent) {
|
|
|
2605
2941
|
return intent === "dashboard";
|
|
2606
2942
|
}
|
|
2607
2943
|
async function realPathWithinRoot(root, target) {
|
|
2608
|
-
if (!
|
|
2944
|
+
if (!isWithinRoot9(root, target)) {
|
|
2609
2945
|
return false;
|
|
2610
2946
|
}
|
|
2611
2947
|
try {
|
|
2612
|
-
const [realRoot, realTarget] = await Promise.all([
|
|
2613
|
-
return
|
|
2948
|
+
const [realRoot, realTarget] = await Promise.all([fs20.realpath(root), fs20.realpath(target)]);
|
|
2949
|
+
return isWithinRoot9(realRoot, realTarget);
|
|
2614
2950
|
} catch {
|
|
2615
2951
|
return false;
|
|
2616
2952
|
}
|
|
2617
2953
|
}
|
|
2618
2954
|
async function writableDestinationWithinRoot(root, destination) {
|
|
2619
|
-
if (!
|
|
2955
|
+
if (!isWithinRoot9(root, destination)) {
|
|
2620
2956
|
return false;
|
|
2621
2957
|
}
|
|
2622
2958
|
try {
|
|
2623
2959
|
const [realRoot, realParent] = await Promise.all([
|
|
2624
|
-
|
|
2625
|
-
|
|
2960
|
+
fs20.realpath(root),
|
|
2961
|
+
fs20.realpath(path16.dirname(destination))
|
|
2626
2962
|
]);
|
|
2627
|
-
return
|
|
2963
|
+
return isWithinRoot9(realRoot, realParent);
|
|
2628
2964
|
} catch {
|
|
2629
2965
|
return false;
|
|
2630
2966
|
}
|
|
2631
2967
|
}
|
|
2632
2968
|
async function copyFileExclusive(source, destination) {
|
|
2633
|
-
const content = await
|
|
2969
|
+
const content = await fs20.readFile(source);
|
|
2634
2970
|
let handle2 = null;
|
|
2635
2971
|
try {
|
|
2636
|
-
handle2 = await
|
|
2972
|
+
handle2 = await fs20.open(
|
|
2637
2973
|
destination,
|
|
2638
2974
|
constants2.O_CREAT | constants2.O_EXCL | constants2.O_WRONLY,
|
|
2639
2975
|
384
|
|
@@ -2660,15 +2996,15 @@ function resolveCommandPromptExecutable() {
|
|
|
2660
2996
|
return process.env.ComSpec ?? "cmd.exe";
|
|
2661
2997
|
}
|
|
2662
2998
|
function findExecutable(command) {
|
|
2663
|
-
if (
|
|
2999
|
+
if (path16.isAbsolute(command)) {
|
|
2664
3000
|
return existsSync(command) ? command : null;
|
|
2665
3001
|
}
|
|
2666
3002
|
const pathValue = process.env.PATH ?? "";
|
|
2667
|
-
for (const directory of pathValue.split(
|
|
3003
|
+
for (const directory of pathValue.split(path16.delimiter)) {
|
|
2668
3004
|
if (directory.length === 0) {
|
|
2669
3005
|
continue;
|
|
2670
3006
|
}
|
|
2671
|
-
const candidate =
|
|
3007
|
+
const candidate = path16.join(directory, command);
|
|
2672
3008
|
if (existsSync(candidate)) {
|
|
2673
3009
|
return candidate;
|
|
2674
3010
|
}
|
|
@@ -2851,7 +3187,7 @@ function registerWorkspaceRoutes(app, resolveWorkspace) {
|
|
|
2851
3187
|
if (!ws) return context.json({ error: "Workspace not found." }, 404);
|
|
2852
3188
|
const name = decodeURIComponent(context.req.param("name"));
|
|
2853
3189
|
const scan = await scanProject(ws.root);
|
|
2854
|
-
const configuredCommand = scan.config?.config.commands?.[name] ?? null;
|
|
3190
|
+
const configuredCommand = scan.config?.config.commands?.[name] ?? scan.presetCommands[name] ?? null;
|
|
2855
3191
|
if (configuredCommand === null) {
|
|
2856
3192
|
return context.json({ error: `Configured command "${name}" was not found.` }, 404);
|
|
2857
3193
|
}
|
|
@@ -2885,7 +3221,7 @@ function registerWorkspaceRoutes(app, resolveWorkspace) {
|
|
|
2885
3221
|
app.post("/api/workspaces/:id/open/package", async (context) => {
|
|
2886
3222
|
const ws = await resolveWorkspace(context.req.param("id"));
|
|
2887
3223
|
if (!ws) return context.json({ error: "Workspace not found." }, 404);
|
|
2888
|
-
const packagePath =
|
|
3224
|
+
const packagePath = path16.join(ws.root, "package.json");
|
|
2889
3225
|
if (!await realPathWithinRoot(ws.root, packagePath)) {
|
|
2890
3226
|
return context.json({ error: "package.json was not found inside the project root." }, 404);
|
|
2891
3227
|
}
|
|
@@ -2907,7 +3243,7 @@ function registerWorkspaceRoutes(app, resolveWorkspace) {
|
|
|
2907
3243
|
if (examplePath === null) {
|
|
2908
3244
|
return context.json({ error: ".env.example was not found." }, 404);
|
|
2909
3245
|
}
|
|
2910
|
-
const destination = localPath ??
|
|
3246
|
+
const destination = localPath ?? path16.join(ws.root, scan.config?.config.env?.local ?? ".env");
|
|
2911
3247
|
if (!await realPathWithinRoot(ws.root, examplePath) || !await writableDestinationWithinRoot(ws.root, destination)) {
|
|
2912
3248
|
return context.json({ error: "Refusing to copy env files outside the project root." }, 400);
|
|
2913
3249
|
}
|
|
@@ -3084,21 +3420,21 @@ function setupHubWebSocket(server, hub) {
|
|
|
3084
3420
|
// src/server/index.ts
|
|
3085
3421
|
async function fileExists(filePath) {
|
|
3086
3422
|
try {
|
|
3087
|
-
await
|
|
3423
|
+
await fs21.access(filePath);
|
|
3088
3424
|
return true;
|
|
3089
3425
|
} catch {
|
|
3090
3426
|
return false;
|
|
3091
3427
|
}
|
|
3092
3428
|
}
|
|
3093
3429
|
async function findWebDistDir() {
|
|
3094
|
-
const moduleDir =
|
|
3430
|
+
const moduleDir = path17.dirname(fileURLToPath2(import.meta.url));
|
|
3095
3431
|
const candidates = [
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3432
|
+
path17.join(moduleDir, "..", "web", "dist"),
|
|
3433
|
+
path17.join(moduleDir, "..", "..", "src", "web", "dist"),
|
|
3434
|
+
path17.join(moduleDir, "web", "dist")
|
|
3099
3435
|
];
|
|
3100
3436
|
for (const candidate of candidates) {
|
|
3101
|
-
if (await fileExists(
|
|
3437
|
+
if (await fileExists(path17.join(candidate, "index.html"))) {
|
|
3102
3438
|
return candidate;
|
|
3103
3439
|
}
|
|
3104
3440
|
}
|
|
@@ -3170,7 +3506,7 @@ async function mountWebUi(app) {
|
|
|
3170
3506
|
app.use("/assets/*", serveStatic({ root: webDistDir }));
|
|
3171
3507
|
app.get("/favicon.svg", serveStatic({ root: webDistDir }));
|
|
3172
3508
|
app.get("*", async (context) => {
|
|
3173
|
-
const html = await
|
|
3509
|
+
const html = await fs21.readFile(path17.join(webDistDir, "index.html"), "utf8");
|
|
3174
3510
|
return context.html(html);
|
|
3175
3511
|
});
|
|
3176
3512
|
} else {
|
|
@@ -3332,11 +3668,11 @@ Hub running at -> ${pc6.cyan(server.url)}`);
|
|
|
3332
3668
|
}
|
|
3333
3669
|
|
|
3334
3670
|
// src/cli/commands/workspace.ts
|
|
3335
|
-
import
|
|
3671
|
+
import path18 from "path";
|
|
3336
3672
|
import pc7 from "picocolors";
|
|
3337
3673
|
async function workspaceAddCommand(dirPath) {
|
|
3338
3674
|
const registry = new WorkspaceRegistry();
|
|
3339
|
-
const target =
|
|
3675
|
+
const target = path18.resolve(dirPath ?? process.cwd());
|
|
3340
3676
|
const entry = await registry.add(target);
|
|
3341
3677
|
console.log(`Added workspace ${pc7.cyan(entry.name)} (${entry.id}) -> ${entry.path}`);
|
|
3342
3678
|
}
|
|
@@ -3367,6 +3703,78 @@ async function workspaceRemoveCommand(id) {
|
|
|
3367
3703
|
}
|
|
3368
3704
|
}
|
|
3369
3705
|
|
|
3706
|
+
// src/cli/updateCheck.ts
|
|
3707
|
+
var REGISTRY_LATEST_URL = "https://registry.npmjs.org/devsurface/latest";
|
|
3708
|
+
var UPDATE_CHECK_TIMEOUT_MS = 900;
|
|
3709
|
+
function parseVersion(version) {
|
|
3710
|
+
const match = version.match(/^v?(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/);
|
|
3711
|
+
if (!match) {
|
|
3712
|
+
return null;
|
|
3713
|
+
}
|
|
3714
|
+
return [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
3715
|
+
}
|
|
3716
|
+
function isNewerVersion(latestVersion, currentVersion) {
|
|
3717
|
+
const latest = parseVersion(latestVersion);
|
|
3718
|
+
const current = parseVersion(currentVersion);
|
|
3719
|
+
if (latest === null || current === null) {
|
|
3720
|
+
return false;
|
|
3721
|
+
}
|
|
3722
|
+
for (let index = 0; index < latest.length; index += 1) {
|
|
3723
|
+
if (latest[index] > current[index]) {
|
|
3724
|
+
return true;
|
|
3725
|
+
}
|
|
3726
|
+
if (latest[index] < current[index]) {
|
|
3727
|
+
return false;
|
|
3728
|
+
}
|
|
3729
|
+
}
|
|
3730
|
+
return false;
|
|
3731
|
+
}
|
|
3732
|
+
function formatUpdateNotice(info) {
|
|
3733
|
+
return `Update available: v${info.latestVersion}
|
|
3734
|
+
Run: npx devsurface@latest`;
|
|
3735
|
+
}
|
|
3736
|
+
function shouldCheckForUpdates() {
|
|
3737
|
+
return process.env.DEVSURFACE_UPDATE_CHECK !== "0" && process.env.CI !== "true";
|
|
3738
|
+
}
|
|
3739
|
+
async function checkForUpdate(currentVersion, fetchImpl = fetch) {
|
|
3740
|
+
if (!shouldCheckForUpdates()) {
|
|
3741
|
+
return null;
|
|
3742
|
+
}
|
|
3743
|
+
const controller = new AbortController();
|
|
3744
|
+
const timeout = setTimeout(() => controller.abort(), UPDATE_CHECK_TIMEOUT_MS);
|
|
3745
|
+
try {
|
|
3746
|
+
const response = await fetchImpl(REGISTRY_LATEST_URL, {
|
|
3747
|
+
headers: {
|
|
3748
|
+
accept: "application/json"
|
|
3749
|
+
},
|
|
3750
|
+
signal: controller.signal
|
|
3751
|
+
});
|
|
3752
|
+
if (!response.ok) {
|
|
3753
|
+
return null;
|
|
3754
|
+
}
|
|
3755
|
+
const body = await response.json();
|
|
3756
|
+
const latestVersion = typeof body.version === "string" ? body.version : null;
|
|
3757
|
+
if (latestVersion === null || !isNewerVersion(latestVersion, currentVersion)) {
|
|
3758
|
+
return null;
|
|
3759
|
+
}
|
|
3760
|
+
return {
|
|
3761
|
+
currentVersion,
|
|
3762
|
+
latestVersion
|
|
3763
|
+
};
|
|
3764
|
+
} catch {
|
|
3765
|
+
return null;
|
|
3766
|
+
} finally {
|
|
3767
|
+
clearTimeout(timeout);
|
|
3768
|
+
}
|
|
3769
|
+
}
|
|
3770
|
+
async function printUpdateNotice(currentVersion) {
|
|
3771
|
+
const update = await checkForUpdate(currentVersion);
|
|
3772
|
+
if (update !== null) {
|
|
3773
|
+
console.log(`
|
|
3774
|
+
${formatUpdateNotice(update)}`);
|
|
3775
|
+
}
|
|
3776
|
+
}
|
|
3777
|
+
|
|
3370
3778
|
// src/cli/index.ts
|
|
3371
3779
|
var program = new Command();
|
|
3372
3780
|
function toPort(value) {
|
|
@@ -3377,7 +3785,9 @@ function toPort(value) {
|
|
|
3377
3785
|
return port;
|
|
3378
3786
|
}
|
|
3379
3787
|
function handle(command) {
|
|
3380
|
-
command.
|
|
3788
|
+
command.then(async () => {
|
|
3789
|
+
await printUpdateNotice(DEV_SURFACE_VERSION);
|
|
3790
|
+
}).catch((error) => {
|
|
3381
3791
|
const message = error instanceof Error ? error.message : String(error);
|
|
3382
3792
|
console.error(message);
|
|
3383
3793
|
process.exitCode = 1;
|