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/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 fs9 } from "fs";
11
- import path9 from "path";
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 fs8 } from "fs";
36
- import path8 from "path";
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/packageManager.ts
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 fs6.access(filePath);
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(path6.join(root, lockFile.file))) {
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 fs7 } from "fs";
868
- import path7 from "path";
869
- function isWithinRoot5(root, target) {
870
- const relative = path7.relative(root, target);
871
- return relative === "" || !relative.startsWith("..") && !path7.isAbsolute(relative);
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 = path7.join(root, "package.json");
929
+ const packageJsonPath = path8.join(root, "package.json");
875
930
  try {
876
931
  const [realRoot, realPackageJsonPath] = await Promise.all([
877
- fs7.realpath(root),
878
- fs7.realpath(packageJsonPath)
932
+ fs8.realpath(root),
933
+ fs8.realpath(packageJsonPath)
879
934
  ]);
880
- if (!isWithinRoot5(realRoot, realPackageJsonPath)) {
935
+ if (!isWithinRoot6(realRoot, realPackageJsonPath)) {
881
936
  return null;
882
937
  }
883
- const content = await fs7.readFile(realPackageJsonPath, "utf8");
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 isWithinRoot6(root, target) {
984
- const relative = path8.relative(path8.resolve(root), path8.resolve(target));
985
- return relative === "" || !relative.startsWith("..") && !path8.isAbsolute(relative);
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 fs8.realpath(root).catch(() => path8.resolve(root));
1272
+ const resolvedRoot = await fs10.realpath(root).catch(() => path10.resolve(root));
989
1273
  for (const candidate of candidates) {
990
- const filePath = path8.join(root, candidate);
1274
+ const filePath = path10.join(root, candidate);
991
1275
  try {
992
- const [stat, realPath] = await Promise.all([fs8.stat(filePath), fs8.realpath(filePath)]);
993
- if (stat.isFile() && isWithinRoot6(resolvedRoot, realPath)) {
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 fs8.realpath(root).catch(() => path8.resolve(root));
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 ?? path8.basename(resolvedRoot),
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 fs9.access(filePath);
1342
+ await fs11.access(filePath);
1045
1343
  return true;
1046
1344
  } catch {
1047
1345
  return false;
1048
1346
  }
1049
1347
  }
1050
- async function readIfPresent2(filePath) {
1348
+ async function readIfPresent3(filePath) {
1051
1349
  if (filePath === null) {
1052
1350
  return null;
1053
1351
  }
1054
1352
  try {
1055
- return await fs9.readFile(filePath, "utf8");
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
- if (result.packageJson === null) {
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(path9.join(root, "node_modules", ".bin"))) {
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 readIfPresent2(result.readme.path);
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 fs10 } from "fs";
1237
- import path10 from "path";
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 = path10.join(cwd, CONFIG_FILE_NAME);
1540
+ const configPath = path12.join(cwd, CONFIG_FILE_NAME);
1241
1541
  try {
1242
- await fs10.access(configPath);
1542
+ await fs12.access(configPath);
1243
1543
  console.log(pc2.yellow(`${CONFIG_FILE_NAME} already exists.`));
1244
1544
  return;
1245
1545
  } catch {
1246
- await fs10.writeFile(configPath, `${JSON.stringify(defaultConfig, null, 2)}
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.packageJson === null) {
1438
- console.error(pc3.red("No package.json was found in this directory."));
1439
- process.exitCode = 1;
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
- if (scan.scripts[script] === void 0) {
1443
- console.error(pc3.red(`Script "${script}" was not found in package.json.`));
1444
- process.exitCode = 1;
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 exitCode = await runPackageScriptToTerminal({
1448
- cwd,
1449
- packageManager: scan.packageManager,
1450
- script
1451
- });
1452
- process.exitCode = exitCode;
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 path11 from "path";
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 fs15, { constants as fsConstants2 } from "fs/promises";
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 fs14, { constants as fsConstants } from "fs/promises";
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 fs13 from "fs";
1840
+ import fs15 from "fs";
1505
1841
 
1506
1842
  // node_modules/is-inside-container/index.js
1507
- import fs12 from "fs";
1843
+ import fs14 from "fs";
1508
1844
 
1509
1845
  // node_modules/is-docker/index.js
1510
- import fs11 from "fs";
1846
+ import fs13 from "fs";
1511
1847
  var isDockerCached;
1512
1848
  function hasDockerEnv() {
1513
1849
  try {
1514
- fs11.statSync("/.dockerenv");
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 fs11.readFileSync("/proc/self/cgroup", "utf8").includes("docker");
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
- fs12.statSync("/run/.containerenv");
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 (fs13.readFileSync("/proc/version", "utf8").toLowerCase().includes("microsoft")) {
1899
+ if (fs15.readFileSync("/proc/version", "utf8").toLowerCase().includes("microsoft")) {
1564
1900
  return !isInsideContainer();
1565
1901
  }
1566
1902
  } catch {
1567
1903
  }
1568
- if (fs13.existsSync("/proc/sys/fs/binfmt_misc/WSLInterop") || fs13.existsSync("/run/WSL")) {
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 fs14.access(configFilePath, fsConstants.F_OK);
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 fs14.readFile(configFilePath, { encoding: "utf8" });
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 = path11.dirname(fileURLToPath(import.meta.url));
1748
- var localXdgOpenPath = path11.join(__dirname, "xdg-open");
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 fs15.access(localXdgOpenPath, fsConstants2.X_OK);
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 fs19 } from "fs";
2004
- import path15 from "path";
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 fs17 } from "fs";
2490
+ import { promises as fs19 } from "fs";
2155
2491
  import os3 from "os";
2156
- import path13 from "path";
2492
+ import path15 from "path";
2157
2493
 
2158
2494
  // src/core/hub/workspaceRoots.ts
2159
- import { promises as fs16 } from "fs";
2160
- import path12 from "path";
2161
- function isWithinRoot7(root, target) {
2162
- const relative = path12.relative(root, target);
2163
- return relative === "" || !relative.startsWith("..") && !path12.isAbsolute(relative);
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 fs16.realpath(path12.resolve(trimmed)));
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 (isWithinRoot7(root, targetPath)) {
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 = path13.basename(realPath).replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 32) || "workspace";
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 ?? path13.join(os3.homedir(), ".devsurface");
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 fs17.readFile(path13.join(dirPath, "package.json"), "utf8"));
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 = path13.join(dir, "workspaces.json");
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) ?? path13.basename(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 fs17.realpath(path13.resolve(dirPath));
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 = path13.resolve(dirPath);
2283
- const realDir = await fs17.realpath(resolved);
2284
- const stat = await fs17.stat(realDir);
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 fs17.readFile(this.filePath, "utf8");
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 fs17.mkdir(path13.dirname(this.filePath), { recursive: true });
2301
- await fs17.writeFile(this.filePath, JSON.stringify(entries, null, 2) + "\n", "utf8");
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 fs18 } from "fs";
2384
- import path14 from "path";
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.4.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 isWithinRoot8(root, target) {
2575
- const relative = path14.relative(path14.resolve(root), path14.resolve(target));
2576
- return relative === "" || !relative.startsWith("..") && !path14.isAbsolute(relative);
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 (!isWithinRoot8(root, target)) {
2944
+ if (!isWithinRoot9(root, target)) {
2609
2945
  return false;
2610
2946
  }
2611
2947
  try {
2612
- const [realRoot, realTarget] = await Promise.all([fs18.realpath(root), fs18.realpath(target)]);
2613
- return isWithinRoot8(realRoot, realTarget);
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 (!isWithinRoot8(root, destination)) {
2955
+ if (!isWithinRoot9(root, destination)) {
2620
2956
  return false;
2621
2957
  }
2622
2958
  try {
2623
2959
  const [realRoot, realParent] = await Promise.all([
2624
- fs18.realpath(root),
2625
- fs18.realpath(path14.dirname(destination))
2960
+ fs20.realpath(root),
2961
+ fs20.realpath(path16.dirname(destination))
2626
2962
  ]);
2627
- return isWithinRoot8(realRoot, realParent);
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 fs18.readFile(source);
2969
+ const content = await fs20.readFile(source);
2634
2970
  let handle2 = null;
2635
2971
  try {
2636
- handle2 = await fs18.open(
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 (path14.isAbsolute(command)) {
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(path14.delimiter)) {
3003
+ for (const directory of pathValue.split(path16.delimiter)) {
2668
3004
  if (directory.length === 0) {
2669
3005
  continue;
2670
3006
  }
2671
- const candidate = path14.join(directory, command);
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 = path14.join(ws.root, "package.json");
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 ?? path14.join(ws.root, scan.config?.config.env?.local ?? ".env");
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 fs19.access(filePath);
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 = path15.dirname(fileURLToPath2(import.meta.url));
3430
+ const moduleDir = path17.dirname(fileURLToPath2(import.meta.url));
3095
3431
  const candidates = [
3096
- path15.join(moduleDir, "..", "web", "dist"),
3097
- path15.join(moduleDir, "..", "..", "src", "web", "dist"),
3098
- path15.join(moduleDir, "web", "dist")
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(path15.join(candidate, "index.html"))) {
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 fs19.readFile(path15.join(webDistDir, "index.html"), "utf8");
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 path16 from "path";
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 = path16.resolve(dirPath ?? process.cwd());
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.catch((error) => {
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;