devsurface 0.7.1 → 0.8.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 +25 -0
- package/README.md +34 -2
- package/dist/cli/index.js +1134 -99
- package/dist/cli/index.js.map +1 -1
- package/package.json +1 -1
- package/src/web/dist/assets/index-BTHWQ_nU.js +10 -0
- package/src/web/dist/assets/index-DkWxxx8o.css +1 -0
- package/src/web/dist/index.html +2 -2
- package/src/web/dist/assets/index-CrNWwfPe.css +0 -1
- package/src/web/dist/assets/index-DOK7baFH.js +0 -10
package/dist/cli/index.js
CHANGED
|
@@ -1087,6 +1087,141 @@ async function detectPorts(ports) {
|
|
|
1087
1087
|
);
|
|
1088
1088
|
}
|
|
1089
1089
|
|
|
1090
|
+
// src/core/scanner/portOwner.ts
|
|
1091
|
+
import spawn2 from "cross-spawn";
|
|
1092
|
+
var LOOKUP_TIMEOUT_MS = 3e3;
|
|
1093
|
+
function captureCommand(command, args) {
|
|
1094
|
+
return new Promise((resolve) => {
|
|
1095
|
+
let settled = false;
|
|
1096
|
+
const finish = (value) => {
|
|
1097
|
+
if (!settled) {
|
|
1098
|
+
settled = true;
|
|
1099
|
+
resolve(value);
|
|
1100
|
+
}
|
|
1101
|
+
};
|
|
1102
|
+
const child = spawn2(command, args, {
|
|
1103
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
1104
|
+
windowsHide: true
|
|
1105
|
+
});
|
|
1106
|
+
const chunks = [];
|
|
1107
|
+
const timer = setTimeout(() => {
|
|
1108
|
+
child.kill();
|
|
1109
|
+
finish(null);
|
|
1110
|
+
}, LOOKUP_TIMEOUT_MS);
|
|
1111
|
+
child.stdout?.on("data", (chunk) => {
|
|
1112
|
+
chunks.push(chunk);
|
|
1113
|
+
});
|
|
1114
|
+
child.on("error", () => {
|
|
1115
|
+
clearTimeout(timer);
|
|
1116
|
+
finish(null);
|
|
1117
|
+
});
|
|
1118
|
+
child.on("close", (code) => {
|
|
1119
|
+
clearTimeout(timer);
|
|
1120
|
+
finish(code === 0 ? Buffer.concat(chunks).toString("utf8") : null);
|
|
1121
|
+
});
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
1124
|
+
function parseNetstatListeners(output) {
|
|
1125
|
+
const owners = /* @__PURE__ */ new Map();
|
|
1126
|
+
for (const line of output.split(/\r?\n/)) {
|
|
1127
|
+
const columns = line.trim().split(/\s+/);
|
|
1128
|
+
if (columns.length < 5 || columns[0].toUpperCase() !== "TCP") {
|
|
1129
|
+
continue;
|
|
1130
|
+
}
|
|
1131
|
+
if (columns[3].toUpperCase() !== "LISTENING") {
|
|
1132
|
+
continue;
|
|
1133
|
+
}
|
|
1134
|
+
const portMatch = /[.:](\d+)$/.exec(columns[1]);
|
|
1135
|
+
const pid = Number(columns[4]);
|
|
1136
|
+
if (portMatch === null || !Number.isInteger(pid) || pid <= 0) {
|
|
1137
|
+
continue;
|
|
1138
|
+
}
|
|
1139
|
+
const port = Number(portMatch[1]);
|
|
1140
|
+
if (!owners.has(port)) {
|
|
1141
|
+
owners.set(port, pid);
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
return owners;
|
|
1145
|
+
}
|
|
1146
|
+
function parseTasklistName(output) {
|
|
1147
|
+
const line = output.split(/\r?\n/).find((candidate) => candidate.trim().startsWith('"'));
|
|
1148
|
+
if (line === void 0) {
|
|
1149
|
+
return null;
|
|
1150
|
+
}
|
|
1151
|
+
const match = /^"([^"]+)"/.exec(line.trim());
|
|
1152
|
+
return match === null ? null : match[1];
|
|
1153
|
+
}
|
|
1154
|
+
function parseLsofOwner(output) {
|
|
1155
|
+
let pid = null;
|
|
1156
|
+
let name = null;
|
|
1157
|
+
for (const line of output.split(/\r?\n/)) {
|
|
1158
|
+
if (line.startsWith("p") && pid === null) {
|
|
1159
|
+
const value = Number(line.slice(1));
|
|
1160
|
+
if (Number.isInteger(value) && value > 0) {
|
|
1161
|
+
pid = value;
|
|
1162
|
+
}
|
|
1163
|
+
} else if (line.startsWith("c") && name === null) {
|
|
1164
|
+
name = line.slice(1) || null;
|
|
1165
|
+
}
|
|
1166
|
+
if (pid !== null && name !== null) {
|
|
1167
|
+
break;
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
return pid === null ? null : { pid, name };
|
|
1171
|
+
}
|
|
1172
|
+
async function findOwnersWindows(ports) {
|
|
1173
|
+
const owners = /* @__PURE__ */ new Map();
|
|
1174
|
+
const netstat = await captureCommand("netstat", ["-ano", "-p", "tcp"]);
|
|
1175
|
+
if (netstat === null) {
|
|
1176
|
+
return owners;
|
|
1177
|
+
}
|
|
1178
|
+
const listeners = parseNetstatListeners(netstat);
|
|
1179
|
+
const nameCache = /* @__PURE__ */ new Map();
|
|
1180
|
+
for (const port of ports) {
|
|
1181
|
+
const pid = listeners.get(port);
|
|
1182
|
+
if (pid === void 0) {
|
|
1183
|
+
continue;
|
|
1184
|
+
}
|
|
1185
|
+
if (!nameCache.has(pid)) {
|
|
1186
|
+
const tasklist = await captureCommand("tasklist", [
|
|
1187
|
+
"/FI",
|
|
1188
|
+
`PID eq ${pid}`,
|
|
1189
|
+
"/FO",
|
|
1190
|
+
"CSV",
|
|
1191
|
+
"/NH"
|
|
1192
|
+
]);
|
|
1193
|
+
nameCache.set(pid, tasklist === null ? null : parseTasklistName(tasklist));
|
|
1194
|
+
}
|
|
1195
|
+
owners.set(port, { pid, name: nameCache.get(pid) ?? null });
|
|
1196
|
+
}
|
|
1197
|
+
return owners;
|
|
1198
|
+
}
|
|
1199
|
+
async function findOwnersUnix(ports) {
|
|
1200
|
+
const owners = /* @__PURE__ */ new Map();
|
|
1201
|
+
for (const port of ports) {
|
|
1202
|
+
const output = await captureCommand("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-FpcL"]);
|
|
1203
|
+
if (output === null) {
|
|
1204
|
+
continue;
|
|
1205
|
+
}
|
|
1206
|
+
const owner = parseLsofOwner(output);
|
|
1207
|
+
if (owner !== null) {
|
|
1208
|
+
owners.set(port, owner);
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
return owners;
|
|
1212
|
+
}
|
|
1213
|
+
async function findPortOwners(ports) {
|
|
1214
|
+
const unique = Array.from(new Set(ports)).slice(0, 16);
|
|
1215
|
+
if (unique.length === 0) {
|
|
1216
|
+
return /* @__PURE__ */ new Map();
|
|
1217
|
+
}
|
|
1218
|
+
try {
|
|
1219
|
+
return process.platform === "win32" ? await findOwnersWindows(unique) : await findOwnersUnix(unique);
|
|
1220
|
+
} catch {
|
|
1221
|
+
return /* @__PURE__ */ new Map();
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1090
1225
|
// src/core/scanner/presets.ts
|
|
1091
1226
|
import { promises as fs9 } from "fs";
|
|
1092
1227
|
import path9 from "path";
|
|
@@ -1381,6 +1516,15 @@ async function scanProject(root = process.cwd()) {
|
|
|
1381
1516
|
findFirstFile(resolvedRoot, ["README.md", "README"]),
|
|
1382
1517
|
findFirstFile(resolvedRoot, ["LICENSE", "LICENSE.md", "COPYING"])
|
|
1383
1518
|
]);
|
|
1519
|
+
const busyPorts = (ports ?? []).filter((probe) => probe.inUse).map((probe) => probe.port);
|
|
1520
|
+
if (busyPorts.length > 0) {
|
|
1521
|
+
const owners = await findPortOwners(busyPorts);
|
|
1522
|
+
for (const probe of ports ?? []) {
|
|
1523
|
+
if (probe.inUse) {
|
|
1524
|
+
probe.owner = owners.get(probe.port) ?? null;
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1384
1528
|
return {
|
|
1385
1529
|
root: resolvedRoot,
|
|
1386
1530
|
projectName: config?.config.name ?? packageJson?.data.name ?? path10.basename(resolvedRoot),
|
|
@@ -1813,11 +1957,127 @@ async function onboardCommand(cwd = process.cwd()) {
|
|
|
1813
1957
|
}
|
|
1814
1958
|
}
|
|
1815
1959
|
|
|
1816
|
-
// src/cli/commands/
|
|
1960
|
+
// src/cli/commands/passport.ts
|
|
1961
|
+
import { promises as fs13 } from "fs";
|
|
1962
|
+
import path13 from "path";
|
|
1817
1963
|
import pc4 from "picocolors";
|
|
1818
1964
|
|
|
1965
|
+
// src/core/explain/index.ts
|
|
1966
|
+
var NAME_INTENTS = [
|
|
1967
|
+
{
|
|
1968
|
+
keys: ["dev", "develop", "serve", "watch", "start:dev"],
|
|
1969
|
+
explanation: "Starts the development server so you can preview the app in your browser while you work."
|
|
1970
|
+
},
|
|
1971
|
+
{ keys: ["start"], explanation: "Starts the application." },
|
|
1972
|
+
{
|
|
1973
|
+
keys: ["build", "compile", "bundle", "dist"],
|
|
1974
|
+
explanation: "Builds the app into optimized files ready for production."
|
|
1975
|
+
},
|
|
1976
|
+
{
|
|
1977
|
+
keys: ["preview"],
|
|
1978
|
+
explanation: "Runs the finished production build locally so you can preview it."
|
|
1979
|
+
},
|
|
1980
|
+
{
|
|
1981
|
+
keys: ["test", "tests", "spec", "unit"],
|
|
1982
|
+
explanation: "Runs the automated tests to check the code still works."
|
|
1983
|
+
},
|
|
1984
|
+
{
|
|
1985
|
+
keys: ["e2e", "integration"],
|
|
1986
|
+
explanation: "Runs end-to-end tests that drive the app like a real user."
|
|
1987
|
+
},
|
|
1988
|
+
{
|
|
1989
|
+
keys: ["lint"],
|
|
1990
|
+
explanation: "Checks the code for style problems and common mistakes."
|
|
1991
|
+
},
|
|
1992
|
+
{
|
|
1993
|
+
keys: ["format", "fmt", "prettier"],
|
|
1994
|
+
explanation: "Automatically reformats the code to a consistent style."
|
|
1995
|
+
},
|
|
1996
|
+
{
|
|
1997
|
+
keys: ["typecheck", "tsc", "types"],
|
|
1998
|
+
explanation: "Checks the code for type errors."
|
|
1999
|
+
},
|
|
2000
|
+
{
|
|
2001
|
+
keys: ["migrate", "migration", "migrations"],
|
|
2002
|
+
explanation: "Applies pending database changes (migrations)."
|
|
2003
|
+
},
|
|
2004
|
+
{ keys: ["seed"], explanation: "Fills the database with starter sample data." },
|
|
2005
|
+
{
|
|
2006
|
+
keys: ["clean"],
|
|
2007
|
+
explanation: "Deletes generated files and leftover build output."
|
|
2008
|
+
},
|
|
2009
|
+
{
|
|
2010
|
+
keys: ["deploy", "release", "publish"],
|
|
2011
|
+
explanation: "Publishes or deploys the project \u2014 double-check before running this one."
|
|
2012
|
+
},
|
|
2013
|
+
{
|
|
2014
|
+
keys: ["install", "setup", "bootstrap"],
|
|
2015
|
+
explanation: "Installs the tools and packages the project needs."
|
|
2016
|
+
},
|
|
2017
|
+
{
|
|
2018
|
+
keys: ["storybook"],
|
|
2019
|
+
explanation: "Starts Storybook to preview UI components on their own."
|
|
2020
|
+
},
|
|
2021
|
+
{ keys: ["docs"], explanation: "Builds or serves the project documentation." }
|
|
2022
|
+
];
|
|
2023
|
+
var TOOL_INTENTS = [
|
|
2024
|
+
{
|
|
2025
|
+
test: /nodemon|ts-node-dev/,
|
|
2026
|
+
explanation: "Runs the app and restarts it automatically whenever you change a file."
|
|
2027
|
+
},
|
|
2028
|
+
{
|
|
2029
|
+
test: /vite\s+build|webpack|rollup|esbuild|\btsup\b|\bparcel\b/,
|
|
2030
|
+
explanation: "Builds the app into optimized files ready for production."
|
|
2031
|
+
},
|
|
2032
|
+
{
|
|
2033
|
+
test: /next\s+dev|vite|remix\s+dev|astro\s+dev/,
|
|
2034
|
+
explanation: "Starts the development server so you can preview the app in your browser."
|
|
2035
|
+
},
|
|
2036
|
+
{
|
|
2037
|
+
test: /playwright|cypress/,
|
|
2038
|
+
explanation: "Runs end-to-end tests that drive the app like a real user."
|
|
2039
|
+
},
|
|
2040
|
+
{
|
|
2041
|
+
test: /vitest|\bjest\b|mocha|\bava\b|pytest|\bgo\s+test\b/,
|
|
2042
|
+
explanation: "Runs the automated tests to check the code works."
|
|
2043
|
+
},
|
|
2044
|
+
{
|
|
2045
|
+
test: /eslint|biome\s+lint|ruff|flake8/,
|
|
2046
|
+
explanation: "Checks the code for style problems and common mistakes."
|
|
2047
|
+
},
|
|
2048
|
+
{
|
|
2049
|
+
test: /prettier|biome\s+format|black\b/,
|
|
2050
|
+
explanation: "Reformats the code to a consistent style."
|
|
2051
|
+
},
|
|
2052
|
+
{ test: /tsc\b/, explanation: "Compiles and type-checks the TypeScript code." },
|
|
2053
|
+
{ test: /prisma/, explanation: "Manages the database schema and data with Prisma." },
|
|
2054
|
+
{
|
|
2055
|
+
test: /docker[-\s]compose|\bdocker\b/,
|
|
2056
|
+
explanation: "Starts or manages the project\u2019s Docker containers."
|
|
2057
|
+
},
|
|
2058
|
+
{ test: /\bgo\s+run\b/, explanation: "Runs the Go program." },
|
|
2059
|
+
{ test: /\bgo\s+build\b/, explanation: "Builds the Go program into an executable." },
|
|
2060
|
+
{
|
|
2061
|
+
test: /uvicorn|flask\s+run|manage\.py\s+runserver/,
|
|
2062
|
+
explanation: "Starts the development server so you can preview the app in your browser."
|
|
2063
|
+
}
|
|
2064
|
+
];
|
|
2065
|
+
function explainScript(name, command = "") {
|
|
2066
|
+
const baseName = name.toLowerCase().split(/[:/]/)[0];
|
|
2067
|
+
const intent = NAME_INTENTS.find((entry) => entry.keys.includes(baseName));
|
|
2068
|
+
if (intent !== void 0) {
|
|
2069
|
+
return intent.explanation;
|
|
2070
|
+
}
|
|
2071
|
+
const lowerCommand = command.toLowerCase();
|
|
2072
|
+
const tool = TOOL_INTENTS.find((entry) => entry.test.test(lowerCommand));
|
|
2073
|
+
if (tool !== void 0) {
|
|
2074
|
+
return tool.explanation;
|
|
2075
|
+
}
|
|
2076
|
+
return `Runs the project\u2019s \u201C${name}\u201D command.`;
|
|
2077
|
+
}
|
|
2078
|
+
|
|
1819
2079
|
// src/core/process/runner.ts
|
|
1820
|
-
import
|
|
2080
|
+
import spawn3 from "cross-spawn";
|
|
1821
2081
|
|
|
1822
2082
|
// src/core/security/dangerousCommand.ts
|
|
1823
2083
|
var DANGEROUS_COMMAND = /\b(rm\s+-rf|docker\s+volume\s+rm|drop\s+database|prisma\s+migrate\s+reset|git\s+clean\s+-fdx?)\b/i;
|
|
@@ -1981,7 +2241,7 @@ async function runPackageScriptToTerminal(options) {
|
|
|
1981
2241
|
return 1;
|
|
1982
2242
|
}
|
|
1983
2243
|
return await new Promise((resolve) => {
|
|
1984
|
-
const child =
|
|
2244
|
+
const child = spawn3(runCommand2.command, runCommand2.args, {
|
|
1985
2245
|
cwd: options.cwd,
|
|
1986
2246
|
stdio: "inherit",
|
|
1987
2247
|
windowsHide: true
|
|
@@ -2000,7 +2260,7 @@ async function runConfiguredCommandToTerminal(options) {
|
|
|
2000
2260
|
return 1;
|
|
2001
2261
|
}
|
|
2002
2262
|
return await new Promise((resolve) => {
|
|
2003
|
-
const child =
|
|
2263
|
+
const child = spawn3(resolvedCommand.command, resolvedCommand.args, {
|
|
2004
2264
|
cwd: options.cwd,
|
|
2005
2265
|
stdio: "inherit",
|
|
2006
2266
|
windowsHide: true
|
|
@@ -2014,7 +2274,656 @@ async function runConfiguredCommandToTerminal(options) {
|
|
|
2014
2274
|
});
|
|
2015
2275
|
}
|
|
2016
2276
|
|
|
2277
|
+
// src/core/passport/index.ts
|
|
2278
|
+
function escapeHtml(value) {
|
|
2279
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
2280
|
+
}
|
|
2281
|
+
var LANGUAGE_LABELS = {
|
|
2282
|
+
node: "JavaScript / TypeScript",
|
|
2283
|
+
python: "Python",
|
|
2284
|
+
go: "Go",
|
|
2285
|
+
java: "Java"
|
|
2286
|
+
};
|
|
2287
|
+
var STACK_ROLES = [
|
|
2288
|
+
{ names: ["next"], role: "Web framework" },
|
|
2289
|
+
{ names: ["react", "react-dom"], role: "User interface library" },
|
|
2290
|
+
{ names: ["vue"], role: "User interface library" },
|
|
2291
|
+
{ names: ["svelte"], role: "User interface library" },
|
|
2292
|
+
{ names: ["express"], role: "Web server framework" },
|
|
2293
|
+
{ names: ["fastify"], role: "Web server framework" },
|
|
2294
|
+
{ names: ["hono"], role: "Web server framework" },
|
|
2295
|
+
{ names: ["@nestjs/core"], role: "Web server framework" },
|
|
2296
|
+
{ names: ["@remix-run/react"], role: "Web framework" },
|
|
2297
|
+
{ names: ["prisma", "@prisma/client"], role: "Database toolkit" },
|
|
2298
|
+
{ names: ["mongoose"], role: "MongoDB database layer" },
|
|
2299
|
+
{ names: ["pg"], role: "PostgreSQL database driver" },
|
|
2300
|
+
{ names: ["redis", "ioredis"], role: "Redis cache client" },
|
|
2301
|
+
{ names: ["typescript"], role: "Typed JavaScript" },
|
|
2302
|
+
{ names: ["vite"], role: "Build tool and dev server" },
|
|
2303
|
+
{ names: ["webpack"], role: "Build tool" },
|
|
2304
|
+
{ names: ["tailwindcss"], role: "CSS styling framework" },
|
|
2305
|
+
{ names: ["vitest", "jest", "mocha"], role: "Test runner" },
|
|
2306
|
+
{ names: ["playwright", "@playwright/test", "cypress"], role: "Browser testing" },
|
|
2307
|
+
{ names: ["eslint"], role: "Code quality checks" },
|
|
2308
|
+
{ names: ["prettier"], role: "Code formatting" },
|
|
2309
|
+
{ names: ["commander", "yargs"], role: "Command-line framework" },
|
|
2310
|
+
{ names: ["ws", "socket.io"], role: "Real-time connections" },
|
|
2311
|
+
{ names: ["zod"], role: "Data validation" },
|
|
2312
|
+
{ names: ["axios"], role: "HTTP requests" },
|
|
2313
|
+
{ names: ["dotenv"], role: "Loads .env settings" }
|
|
2314
|
+
];
|
|
2315
|
+
function describeProject(scan) {
|
|
2316
|
+
const configured = scan.config?.config.description;
|
|
2317
|
+
if (typeof configured === "string" && configured.trim().length > 0) {
|
|
2318
|
+
return configured.trim();
|
|
2319
|
+
}
|
|
2320
|
+
const packageDescription = scan.packageJson?.data.description;
|
|
2321
|
+
if (typeof packageDescription === "string" && packageDescription.trim().length > 0) {
|
|
2322
|
+
return packageDescription.trim();
|
|
2323
|
+
}
|
|
2324
|
+
const parts = [];
|
|
2325
|
+
if (scan.framework !== null) {
|
|
2326
|
+
parts.push(`built with ${scan.framework.type}`);
|
|
2327
|
+
}
|
|
2328
|
+
const languages = scan.language.detected.map((language) => LANGUAGE_LABELS[language] ?? language).join(", ");
|
|
2329
|
+
if (languages.length > 0) {
|
|
2330
|
+
parts.push(`written in ${languages}`);
|
|
2331
|
+
}
|
|
2332
|
+
if (scan.docker !== null && scan.docker.composeFiles.length > 0) {
|
|
2333
|
+
parts.push("with Docker services");
|
|
2334
|
+
}
|
|
2335
|
+
if (parts.length === 0) {
|
|
2336
|
+
return "A software project.";
|
|
2337
|
+
}
|
|
2338
|
+
return `A software project ${parts.join(", ")}.`;
|
|
2339
|
+
}
|
|
2340
|
+
function startCommand(scan) {
|
|
2341
|
+
if (scan.scripts.dev !== void 0) {
|
|
2342
|
+
return getPackageRunCommand(scan.packageManager, "dev").displayCommand;
|
|
2343
|
+
}
|
|
2344
|
+
if (scan.scripts.start !== void 0) {
|
|
2345
|
+
return getPackageRunCommand(scan.packageManager, "start").displayCommand;
|
|
2346
|
+
}
|
|
2347
|
+
const configured = { ...scan.presetCommands, ...scan.config?.config.commands ?? {} };
|
|
2348
|
+
for (const name of ["dev", "start", "serve"]) {
|
|
2349
|
+
if (configured[name] !== void 0) {
|
|
2350
|
+
return configured[name];
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2353
|
+
return null;
|
|
2354
|
+
}
|
|
2355
|
+
function commandForStep(step, scan) {
|
|
2356
|
+
const action = step.action;
|
|
2357
|
+
if (action === void 0) {
|
|
2358
|
+
return null;
|
|
2359
|
+
}
|
|
2360
|
+
if (action.kind === "install") {
|
|
2361
|
+
return getPackageInstallCommand(scan.packageManager).displayCommand;
|
|
2362
|
+
}
|
|
2363
|
+
if (action.kind === "env-copy") {
|
|
2364
|
+
return "cp .env.example .env";
|
|
2365
|
+
}
|
|
2366
|
+
if (action.kind === "run-script" && action.target !== void 0) {
|
|
2367
|
+
return getPackageRunCommand(scan.packageManager, action.target).displayCommand;
|
|
2368
|
+
}
|
|
2369
|
+
if (action.kind === "run-command" && action.target !== void 0) {
|
|
2370
|
+
const configured = { ...scan.presetCommands, ...scan.config?.config.commands ?? {} };
|
|
2371
|
+
return configured[action.target] ?? null;
|
|
2372
|
+
}
|
|
2373
|
+
if (action.kind === "docker") {
|
|
2374
|
+
return "docker compose up -d";
|
|
2375
|
+
}
|
|
2376
|
+
return null;
|
|
2377
|
+
}
|
|
2378
|
+
function statusBadge(status) {
|
|
2379
|
+
if (status === "done") {
|
|
2380
|
+
return '<span class="badge badge-done">Done</span>';
|
|
2381
|
+
}
|
|
2382
|
+
if (status === "todo") {
|
|
2383
|
+
return '<span class="badge badge-todo">To do</span>';
|
|
2384
|
+
}
|
|
2385
|
+
return '<span class="badge badge-manual">Needs you</span>';
|
|
2386
|
+
}
|
|
2387
|
+
function severityBadge(severity) {
|
|
2388
|
+
if (severity === "error") {
|
|
2389
|
+
return '<span class="badge badge-error">Error</span>';
|
|
2390
|
+
}
|
|
2391
|
+
if (severity === "warning") {
|
|
2392
|
+
return '<span class="badge badge-todo">Warning</span>';
|
|
2393
|
+
}
|
|
2394
|
+
return '<span class="badge badge-manual">Info</span>';
|
|
2395
|
+
}
|
|
2396
|
+
function heroBadges(scan) {
|
|
2397
|
+
const badges = [];
|
|
2398
|
+
if (scan.framework !== null) {
|
|
2399
|
+
badges.push(scan.framework.type);
|
|
2400
|
+
}
|
|
2401
|
+
for (const language of scan.language.detected) {
|
|
2402
|
+
badges.push(LANGUAGE_LABELS[language] ?? language);
|
|
2403
|
+
}
|
|
2404
|
+
if (scan.packageManager !== null) {
|
|
2405
|
+
badges.push(scan.packageManager);
|
|
2406
|
+
}
|
|
2407
|
+
if (scan.git?.branch != null) {
|
|
2408
|
+
badges.push(`branch: ${scan.git.branch}`);
|
|
2409
|
+
}
|
|
2410
|
+
return badges.map((badge) => `<span class="badge badge-hero">${escapeHtml(badge)}</span>`).join("\n ");
|
|
2411
|
+
}
|
|
2412
|
+
function commandRow(command, note) {
|
|
2413
|
+
const escaped = escapeHtml(command);
|
|
2414
|
+
return ` <div class="recipe-row">
|
|
2415
|
+
<div class="recipe-note">${escapeHtml(note)}</div>
|
|
2416
|
+
<div class="recipe-command">
|
|
2417
|
+
<code>${escaped}</code>
|
|
2418
|
+
<button type="button" class="copy" data-copy="${escaped}">Copy</button>
|
|
2419
|
+
</div>
|
|
2420
|
+
</div>`;
|
|
2421
|
+
}
|
|
2422
|
+
function quickStartSection(scan) {
|
|
2423
|
+
const rows = [];
|
|
2424
|
+
if (scan.language.detected.includes("node") && scan.packageJson !== null) {
|
|
2425
|
+
rows.push(
|
|
2426
|
+
commandRow(
|
|
2427
|
+
getPackageInstallCommand(scan.packageManager).displayCommand,
|
|
2428
|
+
"Install the packages the project needs"
|
|
2429
|
+
)
|
|
2430
|
+
);
|
|
2431
|
+
}
|
|
2432
|
+
if (scan.env?.hasExample) {
|
|
2433
|
+
rows.push(
|
|
2434
|
+
commandRow("cp .env.example .env", "Create your local settings file (Windows: use copy)")
|
|
2435
|
+
);
|
|
2436
|
+
}
|
|
2437
|
+
if (scan.docker !== null && scan.docker.composeFiles.length > 0) {
|
|
2438
|
+
rows.push(commandRow("docker compose up -d", "Start the background services"));
|
|
2439
|
+
}
|
|
2440
|
+
const start = startCommand(scan);
|
|
2441
|
+
if (start !== null) {
|
|
2442
|
+
rows.push(commandRow(start, "Start the app"));
|
|
2443
|
+
}
|
|
2444
|
+
if (rows.length === 0) {
|
|
2445
|
+
return "";
|
|
2446
|
+
}
|
|
2447
|
+
return ` <section id="quick-start">
|
|
2448
|
+
<h2>Quick start</h2>
|
|
2449
|
+
<p class="muted">On a fresh machine, run these in a terminal from the project folder, top to bottom.</p>
|
|
2450
|
+
${rows.join("\n")}
|
|
2451
|
+
</section>`;
|
|
2452
|
+
}
|
|
2453
|
+
function requirementsSection(scan) {
|
|
2454
|
+
const requirements = [];
|
|
2455
|
+
if (scan.language.detected.includes("node")) {
|
|
2456
|
+
const nodeRange = scan.packageJson?.data.engines?.node;
|
|
2457
|
+
requirements.push({
|
|
2458
|
+
name: "Node.js",
|
|
2459
|
+
detail: typeof nodeRange === "string" && nodeRange.trim().length > 0 ? `version ${nodeRange.trim()}` : "any recent LTS version"
|
|
2460
|
+
});
|
|
2461
|
+
if (scan.packageManager !== null && scan.packageManager !== "npm") {
|
|
2462
|
+
requirements.push({
|
|
2463
|
+
name: scan.packageManager,
|
|
2464
|
+
detail: "the package manager this project uses"
|
|
2465
|
+
});
|
|
2466
|
+
}
|
|
2467
|
+
}
|
|
2468
|
+
if (scan.language.detected.includes("python")) {
|
|
2469
|
+
requirements.push({ name: "Python", detail: "a recent Python 3 version" });
|
|
2470
|
+
}
|
|
2471
|
+
if (scan.language.detected.includes("go")) {
|
|
2472
|
+
requirements.push({ name: "Go", detail: "a recent Go toolchain" });
|
|
2473
|
+
}
|
|
2474
|
+
if (scan.language.detected.includes("java")) {
|
|
2475
|
+
requirements.push({ name: "Java", detail: "a JDK matching the build files" });
|
|
2476
|
+
}
|
|
2477
|
+
if (scan.docker !== null && scan.docker.composeFiles.length > 0) {
|
|
2478
|
+
requirements.push({
|
|
2479
|
+
name: "Docker Desktop",
|
|
2480
|
+
detail: "runs the background services \u2014 start it before the quick start"
|
|
2481
|
+
});
|
|
2482
|
+
}
|
|
2483
|
+
if (requirements.length === 0) {
|
|
2484
|
+
return "";
|
|
2485
|
+
}
|
|
2486
|
+
const items = requirements.map(
|
|
2487
|
+
(requirement) => ` <li><strong>${escapeHtml(requirement.name)}</strong> \u2014 ${escapeHtml(requirement.detail)}</li>`
|
|
2488
|
+
).join("\n");
|
|
2489
|
+
return ` <section id="requirements">
|
|
2490
|
+
<h2>What you need installed</h2>
|
|
2491
|
+
<ul class="plain-list">
|
|
2492
|
+
${items}
|
|
2493
|
+
</ul>
|
|
2494
|
+
</section>`;
|
|
2495
|
+
}
|
|
2496
|
+
function stepsSection(plan, scan) {
|
|
2497
|
+
if (plan.steps.length === 0) {
|
|
2498
|
+
return '<p class="muted">Nothing special to set up. Open the project and start exploring.</p>';
|
|
2499
|
+
}
|
|
2500
|
+
const items = plan.steps.map((step) => {
|
|
2501
|
+
const command = commandForStep(step, scan);
|
|
2502
|
+
const commandHtml = command !== null && step.status !== "done" ? `
|
|
2503
|
+
<div class="recipe-command"><code>${escapeHtml(command)}</code><button type="button" class="copy" data-copy="${escapeHtml(command)}">Copy</button></div>` : "";
|
|
2504
|
+
return ` <li class="step step-${step.status}">
|
|
2505
|
+
<div class="step-heading">${statusBadge(step.status)}<strong>${escapeHtml(step.title)}</strong></div>
|
|
2506
|
+
<p>${escapeHtml(step.description)}</p>${commandHtml}
|
|
2507
|
+
</li>`;
|
|
2508
|
+
}).join("\n");
|
|
2509
|
+
return `<ol class="steps">
|
|
2510
|
+
${items}
|
|
2511
|
+
</ol>`;
|
|
2512
|
+
}
|
|
2513
|
+
function stackSection(scan) {
|
|
2514
|
+
const data = scan.packageJson?.data;
|
|
2515
|
+
if (data === void 0 || data === null) {
|
|
2516
|
+
return "";
|
|
2517
|
+
}
|
|
2518
|
+
const all = { ...data.dependencies, ...data.devDependencies };
|
|
2519
|
+
const names = Object.keys(all);
|
|
2520
|
+
if (names.length === 0) {
|
|
2521
|
+
return "";
|
|
2522
|
+
}
|
|
2523
|
+
const seenRoles = /* @__PURE__ */ new Set();
|
|
2524
|
+
const highlights = [];
|
|
2525
|
+
for (const entry of STACK_ROLES) {
|
|
2526
|
+
const found = entry.names.find((name) => all[name] !== void 0);
|
|
2527
|
+
if (found !== void 0 && !seenRoles.has(entry.role)) {
|
|
2528
|
+
seenRoles.add(entry.role);
|
|
2529
|
+
highlights.push({ name: found, role: entry.role });
|
|
2530
|
+
}
|
|
2531
|
+
if (highlights.length >= 8) {
|
|
2532
|
+
break;
|
|
2533
|
+
}
|
|
2534
|
+
}
|
|
2535
|
+
if (highlights.length === 0) {
|
|
2536
|
+
return "";
|
|
2537
|
+
}
|
|
2538
|
+
const remaining = names.length - highlights.length;
|
|
2539
|
+
const cells = highlights.map(
|
|
2540
|
+
(highlight) => ` <div class="stack-cell">
|
|
2541
|
+
<code>${escapeHtml(highlight.name)}</code>
|
|
2542
|
+
<span>${escapeHtml(highlight.role)}</span>
|
|
2543
|
+
</div>`
|
|
2544
|
+
).join("\n");
|
|
2545
|
+
const more = remaining > 0 ? `
|
|
2546
|
+
<p class="muted">\u2026plus ${remaining} more package${remaining === 1 ? "" : "s"} doing supporting work.</p>` : "";
|
|
2547
|
+
return ` <section id="stack">
|
|
2548
|
+
<h2>The tech stack, translated</h2>
|
|
2549
|
+
<div class="stack-grid">
|
|
2550
|
+
${cells}
|
|
2551
|
+
</div>${more}
|
|
2552
|
+
</section>`;
|
|
2553
|
+
}
|
|
2554
|
+
function scriptsSection(scan) {
|
|
2555
|
+
const entries = Object.entries(scan.scripts);
|
|
2556
|
+
if (entries.length === 0) {
|
|
2557
|
+
return '<p class="muted">No package scripts detected.</p>';
|
|
2558
|
+
}
|
|
2559
|
+
const rows = entries.map(
|
|
2560
|
+
([name, command]) => ` <tr>
|
|
2561
|
+
<td><code>${escapeHtml(name)}</code></td>
|
|
2562
|
+
<td>${escapeHtml(explainScript(name, command))}</td>
|
|
2563
|
+
<td class="raw"><code>${escapeHtml(command)}</code></td>
|
|
2564
|
+
</tr>`
|
|
2565
|
+
).join("\n");
|
|
2566
|
+
return `<table>
|
|
2567
|
+
<thead><tr><th>Command</th><th>What it does</th><th>Exactly what runs</th></tr></thead>
|
|
2568
|
+
<tbody>
|
|
2569
|
+
${rows}
|
|
2570
|
+
</tbody>
|
|
2571
|
+
</table>`;
|
|
2572
|
+
}
|
|
2573
|
+
function envSection(scan) {
|
|
2574
|
+
const env = scan.env;
|
|
2575
|
+
if (env === null || !env.hasExample && !env.hasLocal) {
|
|
2576
|
+
return '<p class="muted">This project does not use a .env settings file.</p>';
|
|
2577
|
+
}
|
|
2578
|
+
if (env.keys.length === 0 && env.exampleKeys.length === 0) {
|
|
2579
|
+
return '<p class="muted">Environment files exist but declare no keys.</p>';
|
|
2580
|
+
}
|
|
2581
|
+
const keys = env.keys.length > 0 ? env.keys.map((key) => ({
|
|
2582
|
+
key: key.key,
|
|
2583
|
+
state: key.present ? key.empty ? "empty" : "set" : "missing"
|
|
2584
|
+
})) : env.exampleKeys.map((key) => ({ key, state: "missing" }));
|
|
2585
|
+
const rows = keys.map((entry) => {
|
|
2586
|
+
const badge = entry.state === "set" ? '<span class="badge badge-done">Set</span>' : entry.state === "empty" ? '<span class="badge badge-todo">Empty</span>' : '<span class="badge badge-error">Missing</span>';
|
|
2587
|
+
return ` <tr><td><code>${escapeHtml(entry.key)}</code></td><td>${badge}</td></tr>`;
|
|
2588
|
+
}).join("\n");
|
|
2589
|
+
return `<p class="muted">Only key names are shown. Values never leave your machine.</p>
|
|
2590
|
+
<table>
|
|
2591
|
+
<thead><tr><th>Setting</th><th>Status</th></tr></thead>
|
|
2592
|
+
<tbody>
|
|
2593
|
+
${rows}
|
|
2594
|
+
</tbody>
|
|
2595
|
+
</table>`;
|
|
2596
|
+
}
|
|
2597
|
+
function portsSection(scan) {
|
|
2598
|
+
if (scan.ports.length === 0) {
|
|
2599
|
+
return '<p class="muted">No specific network ports detected.</p>';
|
|
2600
|
+
}
|
|
2601
|
+
const items = scan.ports.map((probe) => {
|
|
2602
|
+
const badge = probe.inUse ? '<span class="badge badge-todo">Busy right now</span>' : '<span class="badge badge-done">Free</span>';
|
|
2603
|
+
return ` <li><code>${probe.port}</code> ${badge}</li>`;
|
|
2604
|
+
}).join("\n");
|
|
2605
|
+
return `<p class="muted">The app expects these ports. \u201CBusy\u201D means another program is using it.</p>
|
|
2606
|
+
<ul class="plain-list">
|
|
2607
|
+
${items}
|
|
2608
|
+
</ul>`;
|
|
2609
|
+
}
|
|
2610
|
+
function dockerSection(scan) {
|
|
2611
|
+
const docker = scan.docker;
|
|
2612
|
+
if (docker === null || docker.composeFiles.length === 0) {
|
|
2613
|
+
return "";
|
|
2614
|
+
}
|
|
2615
|
+
const services = docker.services.length > 0 ? `<ul class="plain-list">
|
|
2616
|
+
${docker.services.map((service) => ` <li><code>${escapeHtml(service.name)}</code></li>`).join("\n")}
|
|
2617
|
+
</ul>` : '<p class="muted">Service list is available when Docker is running.</p>';
|
|
2618
|
+
return ` <section id="services">
|
|
2619
|
+
<h2>Background services (Docker)</h2>
|
|
2620
|
+
<p class="muted">This project uses Docker to run helper services (like databases) in the background.</p>
|
|
2621
|
+
${services}
|
|
2622
|
+
</section>`;
|
|
2623
|
+
}
|
|
2624
|
+
function healthSection(warnings) {
|
|
2625
|
+
if (warnings.length === 0) {
|
|
2626
|
+
return '<p class="muted">No setup problems detected. Looking good.</p>';
|
|
2627
|
+
}
|
|
2628
|
+
const items = warnings.map(
|
|
2629
|
+
(warning2) => ` <li>
|
|
2630
|
+
<div class="step-heading">${severityBadge(warning2.severity)}<strong>${escapeHtml(warning2.title)}</strong></div>
|
|
2631
|
+
<p>${escapeHtml(warning2.message)}</p>
|
|
2632
|
+
</li>`
|
|
2633
|
+
).join("\n");
|
|
2634
|
+
return `<ul class="steps">
|
|
2635
|
+
${items}
|
|
2636
|
+
</ul>`;
|
|
2637
|
+
}
|
|
2638
|
+
function troubleshootingSection(scan) {
|
|
2639
|
+
const tips = [];
|
|
2640
|
+
if (scan.language.detected.includes("node")) {
|
|
2641
|
+
const manager = scan.packageManager ?? "npm";
|
|
2642
|
+
tips.push({
|
|
2643
|
+
symptom: `\u201Ccommand not found\u201D when running ${manager}`,
|
|
2644
|
+
fix: manager === "npm" ? "Node.js is not installed (or not on your PATH). Install the LTS version from the official Node.js site, then reopen your terminal." : `Install Node.js first, then install ${manager} (it is a separate tool). Reopen your terminal afterwards.`
|
|
2645
|
+
});
|
|
2646
|
+
}
|
|
2647
|
+
if (scan.ports.length > 0) {
|
|
2648
|
+
tips.push({
|
|
2649
|
+
symptom: "The app says a port is already in use (EADDRINUSE)",
|
|
2650
|
+
fix: "Another program is using that port \u2014 often an old copy of this same app. Close other dev servers or restart your computer, then try again."
|
|
2651
|
+
});
|
|
2652
|
+
}
|
|
2653
|
+
if (scan.env?.hasExample) {
|
|
2654
|
+
tips.push({
|
|
2655
|
+
symptom: "The app starts, then crashes or complains about missing configuration",
|
|
2656
|
+
fix: "Open the .env file and fill in any empty values. The \u201CSettings this project needs\u201D list above shows which keys must be set."
|
|
2657
|
+
});
|
|
2658
|
+
}
|
|
2659
|
+
if (scan.docker !== null && scan.docker.composeFiles.length > 0) {
|
|
2660
|
+
tips.push({
|
|
2661
|
+
symptom: "Docker commands fail or hang",
|
|
2662
|
+
fix: "Docker Desktop is probably not running. Start it, wait for the whale icon to settle, then run the command again."
|
|
2663
|
+
});
|
|
2664
|
+
}
|
|
2665
|
+
tips.push({
|
|
2666
|
+
symptom: "Something else is wrong",
|
|
2667
|
+
fix: "Run \u201Cnpx devsurface\u201D in the project folder \u2014 it opens a live dashboard that checks your setup and pinpoints what is missing."
|
|
2668
|
+
});
|
|
2669
|
+
const items = tips.map(
|
|
2670
|
+
(tip) => ` <li>
|
|
2671
|
+
<strong>${escapeHtml(tip.symptom)}</strong>
|
|
2672
|
+
<p>${escapeHtml(tip.fix)}</p>
|
|
2673
|
+
</li>`
|
|
2674
|
+
).join("\n");
|
|
2675
|
+
return ` <section id="troubleshooting">
|
|
2676
|
+
<h2>If something goes wrong</h2>
|
|
2677
|
+
<ul class="steps">
|
|
2678
|
+
${items}
|
|
2679
|
+
</ul>
|
|
2680
|
+
</section>`;
|
|
2681
|
+
}
|
|
2682
|
+
function glossarySection(scan) {
|
|
2683
|
+
const terms = [
|
|
2684
|
+
[
|
|
2685
|
+
"Terminal",
|
|
2686
|
+
"The text window where you type commands. On Windows use PowerShell; on Mac use Terminal."
|
|
2687
|
+
],
|
|
2688
|
+
["Command", "A line of text you type into the terminal and run by pressing Enter."],
|
|
2689
|
+
[
|
|
2690
|
+
"Dependency",
|
|
2691
|
+
"A ready-made package of code this project reuses instead of writing from scratch."
|
|
2692
|
+
],
|
|
2693
|
+
["Port", "A numbered door on your computer that a running app listens on, like 3000 or 8080."],
|
|
2694
|
+
[
|
|
2695
|
+
"localhost",
|
|
2696
|
+
"Your own computer. http://localhost:3000 means \u201Cthe app running on my machine, door 3000\u201D."
|
|
2697
|
+
]
|
|
2698
|
+
];
|
|
2699
|
+
if (scan.env !== null && (scan.env.hasExample || scan.env.hasLocal)) {
|
|
2700
|
+
terms.splice(3, 0, [
|
|
2701
|
+
".env file",
|
|
2702
|
+
"A private settings file with keys and secret values. It stays on your machine and is never shared."
|
|
2703
|
+
]);
|
|
2704
|
+
}
|
|
2705
|
+
const cells = terms.map(
|
|
2706
|
+
([term, definition]) => ` <div class="stack-cell">
|
|
2707
|
+
<strong>${escapeHtml(term)}</strong>
|
|
2708
|
+
<span>${escapeHtml(definition)}</span>
|
|
2709
|
+
</div>`
|
|
2710
|
+
).join("\n");
|
|
2711
|
+
return ` <section id="glossary">
|
|
2712
|
+
<h2>Words you will meet</h2>
|
|
2713
|
+
<div class="stack-grid glossary-grid">
|
|
2714
|
+
${cells}
|
|
2715
|
+
</div>
|
|
2716
|
+
</section>`;
|
|
2717
|
+
}
|
|
2718
|
+
var PASSPORT_CSS = `
|
|
2719
|
+
:root {
|
|
2720
|
+
color-scheme: light;
|
|
2721
|
+
--bg: #F0EEE6; --card: #FAF9F5; --ink: #141413; --muted: #73706A;
|
|
2722
|
+
--line: #E3DFD3; --accent: #CC785C; --accent-deep: #B15D41;
|
|
2723
|
+
--done: #3D7A4E; --warn: #96690F; --bad: #B4392A;
|
|
2724
|
+
}
|
|
2725
|
+
* { box-sizing: border-box; }
|
|
2726
|
+
body {
|
|
2727
|
+
margin: 0; padding: 40px 18px 24px; background: var(--bg); color: var(--ink);
|
|
2728
|
+
font: 16px/1.62 "Styrene A", "Segoe UI", system-ui, -apple-system, sans-serif;
|
|
2729
|
+
}
|
|
2730
|
+
main { max-width: 880px; margin: 0 auto; }
|
|
2731
|
+
h1, h2 {
|
|
2732
|
+
font-family: "Tiempos Headline", Georgia, "Times New Roman", serif;
|
|
2733
|
+
font-weight: 500; letter-spacing: -0.01em;
|
|
2734
|
+
}
|
|
2735
|
+
h1 { margin: 6px 0 10px; font-size: 40px; line-height: 1.15; }
|
|
2736
|
+
h2 { margin: 0 0 14px; font-size: 24px; }
|
|
2737
|
+
p { margin: 6px 0; }
|
|
2738
|
+
.muted { color: var(--muted); }
|
|
2739
|
+
.kicker {
|
|
2740
|
+
margin: 0; color: var(--accent-deep); font-size: 13px; font-weight: 700;
|
|
2741
|
+
letter-spacing: 0.14em; text-transform: uppercase;
|
|
2742
|
+
}
|
|
2743
|
+
.lede { font-size: 18.5px; max-width: 60ch; }
|
|
2744
|
+
nav.toc { display: flex; flex-wrap: wrap; gap: 8px; margin: 0 0 22px; }
|
|
2745
|
+
nav.toc a {
|
|
2746
|
+
color: var(--ink); text-decoration: none; font-size: 13px; font-weight: 600;
|
|
2747
|
+
border: 1px solid var(--line); border-radius: 999px; padding: 6px 14px; background: var(--card);
|
|
2748
|
+
}
|
|
2749
|
+
nav.toc a:hover { border-color: var(--accent); color: var(--accent-deep); }
|
|
2750
|
+
header.hero, section {
|
|
2751
|
+
background: var(--card); border: 1px solid var(--line); border-radius: 14px;
|
|
2752
|
+
padding: 28px 32px; margin-bottom: 20px;
|
|
2753
|
+
}
|
|
2754
|
+
.badges { margin-top: 14px; display: flex; flex-wrap: wrap; gap: 8px; }
|
|
2755
|
+
.badge {
|
|
2756
|
+
display: inline-block; border-radius: 999px; padding: 2px 11px;
|
|
2757
|
+
font-size: 12.5px; font-weight: 600; border: 1px solid var(--line); background: var(--bg);
|
|
2758
|
+
}
|
|
2759
|
+
.badge-hero { color: var(--accent-deep); border-color: #E5C8BA; background: #F7EDE7; }
|
|
2760
|
+
.badge-done { color: var(--done); border-color: #C4D9C9; background: #EDF4EE; }
|
|
2761
|
+
.badge-todo { color: var(--warn); border-color: #E4D2A8; background: #F8F1DE; }
|
|
2762
|
+
.badge-manual { color: var(--accent-deep); border-color: #E5C8BA; background: #F7EDE7; }
|
|
2763
|
+
.badge-error { color: var(--bad); border-color: #E7C2BB; background: #F9ECE9; }
|
|
2764
|
+
.readiness { margin-top: 18px; }
|
|
2765
|
+
.readiness-track { height: 10px; border-radius: 999px; background: #E6E2D6; overflow: hidden; }
|
|
2766
|
+
.readiness-fill { height: 100%; border-radius: 999px; background: var(--accent); }
|
|
2767
|
+
ol.steps, ul.steps { margin: 0; padding: 0 0 0 2px; list-style: none; }
|
|
2768
|
+
.steps li { border-top: 1px solid var(--line); padding: 14px 2px; }
|
|
2769
|
+
.steps li:first-child { border-top: none; padding-top: 4px; }
|
|
2770
|
+
.step-heading { display: flex; align-items: center; gap: 10px; margin-bottom: 2px; }
|
|
2771
|
+
.recipe-row { border-top: 1px solid var(--line); padding: 12px 0; }
|
|
2772
|
+
.recipe-row:first-of-type { border-top: none; }
|
|
2773
|
+
.recipe-note { font-size: 14px; color: var(--muted); margin-bottom: 6px; }
|
|
2774
|
+
.recipe-command {
|
|
2775
|
+
display: flex; align-items: center; gap: 10px; padding: 10px 14px; margin-top: 8px;
|
|
2776
|
+
border-radius: 9px; background: #191817; color: #F5F1E8; overflow-x: auto;
|
|
2777
|
+
}
|
|
2778
|
+
.recipe-command code { flex: 1 1 auto; white-space: pre; }
|
|
2779
|
+
button.copy {
|
|
2780
|
+
flex: 0 0 auto; border: 1px solid #4A4742; border-radius: 6px; cursor: pointer;
|
|
2781
|
+
background: transparent; color: #D8D2C4; font-size: 12px; font-weight: 600; padding: 4px 10px;
|
|
2782
|
+
}
|
|
2783
|
+
button.copy:hover { border-color: var(--accent); color: #F5F1E8; }
|
|
2784
|
+
code { font: 13.5px/1.5 ui-monospace, Consolas, "Cascadia Mono", monospace; }
|
|
2785
|
+
table { width: 100%; border-collapse: collapse; }
|
|
2786
|
+
th, td { text-align: left; padding: 9px 10px; border-top: 1px solid var(--line); vertical-align: top; }
|
|
2787
|
+
thead th { border-top: none; font-size: 13px; color: var(--muted); font-weight: 600; }
|
|
2788
|
+
td.raw { overflow-wrap: anywhere; }
|
|
2789
|
+
ul.plain-list { margin: 8px 0 0; padding-left: 4px; list-style: none; }
|
|
2790
|
+
ul.plain-list li { padding: 5px 0; }
|
|
2791
|
+
.stack-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); gap: 12px; }
|
|
2792
|
+
.stack-cell {
|
|
2793
|
+
border: 1px solid var(--line); border-radius: 10px; background: var(--bg);
|
|
2794
|
+
padding: 12px 14px; display: flex; flex-direction: column; gap: 4px;
|
|
2795
|
+
}
|
|
2796
|
+
.stack-cell span { color: var(--muted); font-size: 13.5px; }
|
|
2797
|
+
.glossary-grid { grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); }
|
|
2798
|
+
footer { text-align: center; color: var(--muted); font-size: 13px; padding: 10px 0 26px; }
|
|
2799
|
+
@media print {
|
|
2800
|
+
body { background: #fff; padding: 0; }
|
|
2801
|
+
header.hero, section { border: none; padding: 12px 0; break-inside: avoid; }
|
|
2802
|
+
nav.toc, button.copy { display: none; }
|
|
2803
|
+
}
|
|
2804
|
+
`;
|
|
2805
|
+
var COPY_SCRIPT = `
|
|
2806
|
+
document.addEventListener('click', function (event) {
|
|
2807
|
+
var button = event.target.closest('button.copy');
|
|
2808
|
+
if (!button || !navigator.clipboard) return;
|
|
2809
|
+
navigator.clipboard.writeText(button.getAttribute('data-copy') || '').then(function () {
|
|
2810
|
+
var original = button.textContent;
|
|
2811
|
+
button.textContent = 'Copied!';
|
|
2812
|
+
setTimeout(function () { button.textContent = original; }, 1600);
|
|
2813
|
+
});
|
|
2814
|
+
});
|
|
2815
|
+
`;
|
|
2816
|
+
function renderPassportHtml(options) {
|
|
2817
|
+
const { scan, warnings, plan, version } = options;
|
|
2818
|
+
const generatedAt = options.generatedAt ?? /* @__PURE__ */ new Date();
|
|
2819
|
+
const name = escapeHtml(scan.projectName);
|
|
2820
|
+
const generatedOn = generatedAt.toISOString().slice(0, 10);
|
|
2821
|
+
const quickStart = quickStartSection(scan);
|
|
2822
|
+
const requirements = requirementsSection(scan);
|
|
2823
|
+
const stack = stackSection(scan);
|
|
2824
|
+
const docker = dockerSection(scan);
|
|
2825
|
+
const readinessLabel = plan.ready ? "Ready to run" : `${plan.readiness}% ready \u2014 ${escapeHtml(plan.summary)}`;
|
|
2826
|
+
const tocEntries = [];
|
|
2827
|
+
if (quickStart !== "") tocEntries.push(["#quick-start", "Quick start"]);
|
|
2828
|
+
if (requirements !== "") tocEntries.push(["#requirements", "What you need"]);
|
|
2829
|
+
tocEntries.push(["#setup", "Setup steps"]);
|
|
2830
|
+
if (stack !== "") tocEntries.push(["#stack", "Tech stack"]);
|
|
2831
|
+
tocEntries.push(["#commands", "Commands"]);
|
|
2832
|
+
tocEntries.push(["#settings", "Settings"]);
|
|
2833
|
+
tocEntries.push(["#troubleshooting", "Troubleshooting"]);
|
|
2834
|
+
tocEntries.push(["#glossary", "Glossary"]);
|
|
2835
|
+
const toc = tocEntries.map(([href, label]) => ` <a href="${href}">${label}</a>`).join("\n");
|
|
2836
|
+
return `<!doctype html>
|
|
2837
|
+
<html lang="en">
|
|
2838
|
+
<head>
|
|
2839
|
+
<meta charset="utf-8">
|
|
2840
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
2841
|
+
<meta name="color-scheme" content="light">
|
|
2842
|
+
<title>${name} \u2014 Project Passport</title>
|
|
2843
|
+
<style>${PASSPORT_CSS}</style>
|
|
2844
|
+
</head>
|
|
2845
|
+
<body>
|
|
2846
|
+
<main>
|
|
2847
|
+
<header class="hero">
|
|
2848
|
+
<p class="kicker">Project Passport</p>
|
|
2849
|
+
<h1>${name}</h1>
|
|
2850
|
+
<p class="lede">${escapeHtml(describeProject(scan))}</p>
|
|
2851
|
+
<div class="badges">
|
|
2852
|
+
${heroBadges(scan)}
|
|
2853
|
+
</div>
|
|
2854
|
+
<div class="readiness">
|
|
2855
|
+
<p class="muted">${readinessLabel}</p>
|
|
2856
|
+
<div class="readiness-track"><div class="readiness-fill" style="width:${plan.readiness}%"></div></div>
|
|
2857
|
+
</div>
|
|
2858
|
+
</header>
|
|
2859
|
+
|
|
2860
|
+
<nav class="toc">
|
|
2861
|
+
${toc}
|
|
2862
|
+
</nav>
|
|
2863
|
+
|
|
2864
|
+
${quickStart}
|
|
2865
|
+
${requirements}
|
|
2866
|
+
<section id="setup">
|
|
2867
|
+
<h2>Setup, step by step</h2>
|
|
2868
|
+
<p class="muted">The detailed version of the quick start. Anything marked \u201CDone\u201D was already true when this passport was generated.</p>
|
|
2869
|
+
${stepsSection(plan, scan)}
|
|
2870
|
+
</section>
|
|
2871
|
+
|
|
2872
|
+
${stack}
|
|
2873
|
+
<section id="commands">
|
|
2874
|
+
<h2>Every command, explained</h2>
|
|
2875
|
+
${scriptsSection(scan)}
|
|
2876
|
+
</section>
|
|
2877
|
+
|
|
2878
|
+
<section id="settings">
|
|
2879
|
+
<h2>Settings this project needs</h2>
|
|
2880
|
+
${envSection(scan)}
|
|
2881
|
+
</section>
|
|
2882
|
+
|
|
2883
|
+
<section id="ports">
|
|
2884
|
+
<h2>Network ports</h2>
|
|
2885
|
+
${portsSection(scan)}
|
|
2886
|
+
</section>
|
|
2887
|
+
|
|
2888
|
+
${docker}
|
|
2889
|
+
<section id="health">
|
|
2890
|
+
<h2>Health check</h2>
|
|
2891
|
+
${healthSection(warnings)}
|
|
2892
|
+
</section>
|
|
2893
|
+
|
|
2894
|
+
${troubleshootingSection(scan)}
|
|
2895
|
+
${glossarySection(scan)}
|
|
2896
|
+
<footer>
|
|
2897
|
+
Generated by DevSurface v${escapeHtml(version)} on ${generatedOn} \xB7
|
|
2898
|
+
Everything was collected locally. No secrets, tokens, or .env values are included.
|
|
2899
|
+
</footer>
|
|
2900
|
+
</main>
|
|
2901
|
+
<script>${COPY_SCRIPT}</script>
|
|
2902
|
+
</body>
|
|
2903
|
+
</html>
|
|
2904
|
+
`;
|
|
2905
|
+
}
|
|
2906
|
+
|
|
2907
|
+
// src/version.ts
|
|
2908
|
+
var DEV_SURFACE_VERSION = "0.7.1";
|
|
2909
|
+
|
|
2910
|
+
// src/cli/commands/passport.ts
|
|
2911
|
+
async function passportCommand(cwd = process.cwd(), outFile) {
|
|
2912
|
+
const scan = await scanProject(cwd);
|
|
2913
|
+
const warnings = await runDoctor(cwd, scan);
|
|
2914
|
+
const plan = buildOnboardingPlan(scan, warnings);
|
|
2915
|
+
const html = renderPassportHtml({ scan, warnings, plan, version: DEV_SURFACE_VERSION });
|
|
2916
|
+
const target = path13.resolve(cwd, outFile ?? "devsurface-passport.html");
|
|
2917
|
+
await fs13.writeFile(target, html, "utf8");
|
|
2918
|
+
console.log(pc4.bold(`Passport created for ${safeDisplayText(scan.projectName)}`));
|
|
2919
|
+
console.log(`Saved to ${safeDisplayText(target)}`);
|
|
2920
|
+
console.log(
|
|
2921
|
+
pc4.dim("Open it in any browser or share it \u2014 it works offline and contains no secrets.")
|
|
2922
|
+
);
|
|
2923
|
+
}
|
|
2924
|
+
|
|
2017
2925
|
// src/cli/commands/run.ts
|
|
2926
|
+
import pc5 from "picocolors";
|
|
2018
2927
|
async function runCommand(script, cwd = process.cwd()) {
|
|
2019
2928
|
const scan = await scanProject(cwd);
|
|
2020
2929
|
if (scan.scripts[script] !== void 0) {
|
|
@@ -2029,7 +2938,7 @@ async function runCommand(script, cwd = process.cwd()) {
|
|
|
2029
2938
|
const configuredCommand = scan.config?.config.commands?.[script] ?? scan.presetCommands[script];
|
|
2030
2939
|
if (configuredCommand !== void 0) {
|
|
2031
2940
|
if (isDangerousCommand(configuredCommand)) {
|
|
2032
|
-
console.error(
|
|
2941
|
+
console.error(pc5.red(`Refusing to run dangerous command "${safeDisplayText(script)}".`));
|
|
2033
2942
|
process.exitCode = 1;
|
|
2034
2943
|
return;
|
|
2035
2944
|
}
|
|
@@ -2046,17 +2955,17 @@ async function runCommand(script, cwd = process.cwd()) {
|
|
|
2046
2955
|
...Object.keys(scan.presetCommands)
|
|
2047
2956
|
];
|
|
2048
2957
|
const hint = available.length > 0 ? ` Available commands: ${safeDisplayList(available)}.` : "";
|
|
2049
|
-
console.error(
|
|
2958
|
+
console.error(pc5.red(`Command "${safeDisplayText(script)}" was not found.${hint}`));
|
|
2050
2959
|
process.exitCode = 1;
|
|
2051
2960
|
}
|
|
2052
2961
|
|
|
2053
2962
|
// src/cli/commands/scan.ts
|
|
2054
|
-
import
|
|
2963
|
+
import pc6 from "picocolors";
|
|
2055
2964
|
function formatList(values) {
|
|
2056
2965
|
return safeDisplayList(values);
|
|
2057
2966
|
}
|
|
2058
2967
|
function printScanResult(scan) {
|
|
2059
|
-
console.log(
|
|
2968
|
+
console.log(pc6.bold(`Project: ${safeDisplayText(scan.projectName)}`));
|
|
2060
2969
|
console.log(`Language: ${formatList(scan.language.detected) || "unknown"}`);
|
|
2061
2970
|
console.log(`Type: ${safeDisplayText(scan.framework?.type ?? "Unknown")}`);
|
|
2062
2971
|
console.log(`Manager: ${safeDisplayText(scan.packageManager ?? "unknown")}`);
|
|
@@ -2083,35 +2992,35 @@ async function scanCommand(cwd = process.cwd()) {
|
|
|
2083
2992
|
}
|
|
2084
2993
|
|
|
2085
2994
|
// src/cli/commands/start.ts
|
|
2086
|
-
import
|
|
2995
|
+
import pc7 from "picocolors";
|
|
2087
2996
|
|
|
2088
2997
|
// node_modules/open/index.js
|
|
2089
2998
|
import process7 from "process";
|
|
2090
2999
|
import { Buffer as Buffer2 } from "buffer";
|
|
2091
|
-
import
|
|
3000
|
+
import path14 from "path";
|
|
2092
3001
|
import { fileURLToPath } from "url";
|
|
2093
3002
|
import { promisify as promisify5 } from "util";
|
|
2094
3003
|
import childProcess from "child_process";
|
|
2095
|
-
import
|
|
3004
|
+
import fs18, { constants as fsConstants2 } from "fs/promises";
|
|
2096
3005
|
|
|
2097
3006
|
// node_modules/wsl-utils/index.js
|
|
2098
3007
|
import process3 from "process";
|
|
2099
|
-
import
|
|
3008
|
+
import fs17, { constants as fsConstants } from "fs/promises";
|
|
2100
3009
|
|
|
2101
3010
|
// node_modules/is-wsl/index.js
|
|
2102
3011
|
import process2 from "process";
|
|
2103
3012
|
import os2 from "os";
|
|
2104
|
-
import
|
|
3013
|
+
import fs16 from "fs";
|
|
2105
3014
|
|
|
2106
3015
|
// node_modules/is-inside-container/index.js
|
|
2107
|
-
import
|
|
3016
|
+
import fs15 from "fs";
|
|
2108
3017
|
|
|
2109
3018
|
// node_modules/is-docker/index.js
|
|
2110
|
-
import
|
|
3019
|
+
import fs14 from "fs";
|
|
2111
3020
|
var isDockerCached;
|
|
2112
3021
|
function hasDockerEnv() {
|
|
2113
3022
|
try {
|
|
2114
|
-
|
|
3023
|
+
fs14.statSync("/.dockerenv");
|
|
2115
3024
|
return true;
|
|
2116
3025
|
} catch {
|
|
2117
3026
|
return false;
|
|
@@ -2119,7 +3028,7 @@ function hasDockerEnv() {
|
|
|
2119
3028
|
}
|
|
2120
3029
|
function hasDockerCGroup() {
|
|
2121
3030
|
try {
|
|
2122
|
-
return
|
|
3031
|
+
return fs14.readFileSync("/proc/self/cgroup", "utf8").includes("docker");
|
|
2123
3032
|
} catch {
|
|
2124
3033
|
return false;
|
|
2125
3034
|
}
|
|
@@ -2135,7 +3044,7 @@ function isDocker() {
|
|
|
2135
3044
|
var cachedResult;
|
|
2136
3045
|
var hasContainerEnv = () => {
|
|
2137
3046
|
try {
|
|
2138
|
-
|
|
3047
|
+
fs15.statSync("/run/.containerenv");
|
|
2139
3048
|
return true;
|
|
2140
3049
|
} catch {
|
|
2141
3050
|
return false;
|
|
@@ -2160,12 +3069,12 @@ var isWsl = () => {
|
|
|
2160
3069
|
return true;
|
|
2161
3070
|
}
|
|
2162
3071
|
try {
|
|
2163
|
-
if (
|
|
3072
|
+
if (fs16.readFileSync("/proc/version", "utf8").toLowerCase().includes("microsoft")) {
|
|
2164
3073
|
return !isInsideContainer();
|
|
2165
3074
|
}
|
|
2166
3075
|
} catch {
|
|
2167
3076
|
}
|
|
2168
|
-
if (
|
|
3077
|
+
if (fs16.existsSync("/proc/sys/fs/binfmt_misc/WSLInterop") || fs16.existsSync("/run/WSL")) {
|
|
2169
3078
|
return !isInsideContainer();
|
|
2170
3079
|
}
|
|
2171
3080
|
return false;
|
|
@@ -2183,14 +3092,14 @@ var wslDrivesMountPoint = /* @__PURE__ */ (() => {
|
|
|
2183
3092
|
const configFilePath = "/etc/wsl.conf";
|
|
2184
3093
|
let isConfigFileExists = false;
|
|
2185
3094
|
try {
|
|
2186
|
-
await
|
|
3095
|
+
await fs17.access(configFilePath, fsConstants.F_OK);
|
|
2187
3096
|
isConfigFileExists = true;
|
|
2188
3097
|
} catch {
|
|
2189
3098
|
}
|
|
2190
3099
|
if (!isConfigFileExists) {
|
|
2191
3100
|
return defaultMountPoint;
|
|
2192
3101
|
}
|
|
2193
|
-
const configContent = await
|
|
3102
|
+
const configContent = await fs17.readFile(configFilePath, { encoding: "utf8" });
|
|
2194
3103
|
const configMountPoint = /(?<!#.*)root\s*=\s*(?<mountPoint>.*)/g.exec(configContent);
|
|
2195
3104
|
if (!configMountPoint) {
|
|
2196
3105
|
return defaultMountPoint;
|
|
@@ -2344,8 +3253,8 @@ async function defaultBrowser2() {
|
|
|
2344
3253
|
|
|
2345
3254
|
// node_modules/open/index.js
|
|
2346
3255
|
var execFile5 = promisify5(childProcess.execFile);
|
|
2347
|
-
var __dirname =
|
|
2348
|
-
var localXdgOpenPath =
|
|
3256
|
+
var __dirname = path14.dirname(fileURLToPath(import.meta.url));
|
|
3257
|
+
var localXdgOpenPath = path14.join(__dirname, "xdg-open");
|
|
2349
3258
|
var { platform, arch } = process7;
|
|
2350
3259
|
async function getWindowsDefaultBrowserFromWsl() {
|
|
2351
3260
|
const powershellPath = await powerShellPath();
|
|
@@ -2495,7 +3404,7 @@ var baseOpen = async (options) => {
|
|
|
2495
3404
|
const isBundled = !__dirname || __dirname === "/";
|
|
2496
3405
|
let exeLocalXdgOpen = false;
|
|
2497
3406
|
try {
|
|
2498
|
-
await
|
|
3407
|
+
await fs18.access(localXdgOpenPath, fsConstants2.X_OK);
|
|
2499
3408
|
exeLocalXdgOpen = true;
|
|
2500
3409
|
} catch {
|
|
2501
3410
|
}
|
|
@@ -2600,8 +3509,8 @@ defineLazyProperty(apps, "browserPrivate", () => "browserPrivate");
|
|
|
2600
3509
|
var open_default = open;
|
|
2601
3510
|
|
|
2602
3511
|
// src/server/index.ts
|
|
2603
|
-
import { promises as
|
|
2604
|
-
import
|
|
3512
|
+
import { promises as fs23 } from "fs";
|
|
3513
|
+
import path19 from "path";
|
|
2605
3514
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2606
3515
|
import { createAdaptorServer } from "@hono/node-server";
|
|
2607
3516
|
import { serveStatic } from "@hono/node-server/serve-static";
|
|
@@ -2609,7 +3518,7 @@ import { Hono } from "hono";
|
|
|
2609
3518
|
|
|
2610
3519
|
// src/core/process/manager.ts
|
|
2611
3520
|
import { EventEmitter } from "events";
|
|
2612
|
-
import
|
|
3521
|
+
import spawn4 from "cross-spawn";
|
|
2613
3522
|
var LOG_MESSAGE_LIMIT = 16384;
|
|
2614
3523
|
var LOG_ENTRY_LIMIT = 1e3;
|
|
2615
3524
|
function killChildProcessTree(child) {
|
|
@@ -2618,7 +3527,7 @@ function killChildProcessTree(child) {
|
|
|
2618
3527
|
return;
|
|
2619
3528
|
}
|
|
2620
3529
|
if (process.platform === "win32") {
|
|
2621
|
-
const result =
|
|
3530
|
+
const result = spawn4.sync("taskkill", ["/pid", String(child.pid), "/T", "/F"], {
|
|
2622
3531
|
stdio: "ignore",
|
|
2623
3532
|
windowsHide: true
|
|
2624
3533
|
});
|
|
@@ -2634,7 +3543,7 @@ var ProcessManager = class extends EventEmitter {
|
|
|
2634
3543
|
logs = [];
|
|
2635
3544
|
cleanupInstalled = false;
|
|
2636
3545
|
start(options) {
|
|
2637
|
-
const child =
|
|
3546
|
+
const child = spawn4(options.command, options.args, {
|
|
2638
3547
|
cwd: options.cwd,
|
|
2639
3548
|
shell: false,
|
|
2640
3549
|
windowsHide: true
|
|
@@ -2751,16 +3660,16 @@ var ProcessManager = class extends EventEmitter {
|
|
|
2751
3660
|
|
|
2752
3661
|
// src/core/hub/registry.ts
|
|
2753
3662
|
import { createHash } from "crypto";
|
|
2754
|
-
import { promises as
|
|
3663
|
+
import { promises as fs20 } from "fs";
|
|
2755
3664
|
import os3 from "os";
|
|
2756
|
-
import
|
|
3665
|
+
import path16 from "path";
|
|
2757
3666
|
|
|
2758
3667
|
// src/core/hub/workspaceRoots.ts
|
|
2759
|
-
import { promises as
|
|
2760
|
-
import
|
|
3668
|
+
import { promises as fs19 } from "fs";
|
|
3669
|
+
import path15 from "path";
|
|
2761
3670
|
function isWithinRoot8(root, target) {
|
|
2762
|
-
const relative =
|
|
2763
|
-
return relative === "" || !relative.startsWith("..") && !
|
|
3671
|
+
const relative = path15.relative(root, target);
|
|
3672
|
+
return relative === "" || !relative.startsWith("..") && !path15.isAbsolute(relative);
|
|
2764
3673
|
}
|
|
2765
3674
|
async function configuredWorkspaceRoots() {
|
|
2766
3675
|
const raw = process.env.DEVSURFACE_WORKSPACE_ROOTS;
|
|
@@ -2774,7 +3683,7 @@ async function configuredWorkspaceRoots() {
|
|
|
2774
3683
|
continue;
|
|
2775
3684
|
}
|
|
2776
3685
|
try {
|
|
2777
|
-
roots.push(await
|
|
3686
|
+
roots.push(await fs19.realpath(path15.resolve(trimmed)));
|
|
2778
3687
|
} catch {
|
|
2779
3688
|
}
|
|
2780
3689
|
}
|
|
@@ -2795,16 +3704,16 @@ async function assertWithinWorkspaceRoots(targetPath) {
|
|
|
2795
3704
|
|
|
2796
3705
|
// src/core/hub/registry.ts
|
|
2797
3706
|
function workspaceId(realPath) {
|
|
2798
|
-
const base =
|
|
3707
|
+
const base = path16.basename(realPath).replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 32) || "workspace";
|
|
2799
3708
|
const hash = createHash("sha256").update(realPath).digest("hex").slice(0, 6);
|
|
2800
3709
|
return `${base}-${hash}`;
|
|
2801
3710
|
}
|
|
2802
3711
|
function defaultDataDir() {
|
|
2803
|
-
return process.env.DEVSURFACE_DATA_DIR ??
|
|
3712
|
+
return process.env.DEVSURFACE_DATA_DIR ?? path16.join(os3.homedir(), ".devsurface");
|
|
2804
3713
|
}
|
|
2805
3714
|
async function readPackageName(dirPath) {
|
|
2806
3715
|
try {
|
|
2807
|
-
const raw = JSON.parse(await
|
|
3716
|
+
const raw = JSON.parse(await fs20.readFile(path16.join(dirPath, "package.json"), "utf8"));
|
|
2808
3717
|
return typeof raw?.name === "string" && raw.name.length > 0 ? raw.name : null;
|
|
2809
3718
|
} catch {
|
|
2810
3719
|
return null;
|
|
@@ -2815,7 +3724,7 @@ var WorkspaceRegistry = class {
|
|
|
2815
3724
|
seeded = false;
|
|
2816
3725
|
constructor(dataDir) {
|
|
2817
3726
|
const dir = dataDir ?? defaultDataDir();
|
|
2818
|
-
this.filePath =
|
|
3727
|
+
this.filePath = path16.join(dir, "workspaces.json");
|
|
2819
3728
|
}
|
|
2820
3729
|
async list() {
|
|
2821
3730
|
await this.seedFromEnv();
|
|
@@ -2829,7 +3738,7 @@ var WorkspaceRegistry = class {
|
|
|
2829
3738
|
if (existing) {
|
|
2830
3739
|
return existing;
|
|
2831
3740
|
}
|
|
2832
|
-
const name = await readPackageName(realDir) ??
|
|
3741
|
+
const name = await readPackageName(realDir) ?? path16.basename(realDir);
|
|
2833
3742
|
const entry = {
|
|
2834
3743
|
id: workspaceId(realDir),
|
|
2835
3744
|
name,
|
|
@@ -2851,7 +3760,7 @@ var WorkspaceRegistry = class {
|
|
|
2851
3760
|
}
|
|
2852
3761
|
async findByPath(dirPath) {
|
|
2853
3762
|
try {
|
|
2854
|
-
const realDir = await
|
|
3763
|
+
const realDir = await fs20.realpath(path16.resolve(dirPath));
|
|
2855
3764
|
const entries = await this.read();
|
|
2856
3765
|
return entries.find((entry) => entry.path === realDir) ?? null;
|
|
2857
3766
|
} catch {
|
|
@@ -2879,9 +3788,9 @@ var WorkspaceRegistry = class {
|
|
|
2879
3788
|
}
|
|
2880
3789
|
}
|
|
2881
3790
|
async resolveDir(dirPath) {
|
|
2882
|
-
const resolved =
|
|
2883
|
-
const realDir = await
|
|
2884
|
-
const stat = await
|
|
3791
|
+
const resolved = path16.resolve(dirPath);
|
|
3792
|
+
const realDir = await fs20.realpath(resolved);
|
|
3793
|
+
const stat = await fs20.stat(realDir);
|
|
2885
3794
|
if (!stat.isDirectory()) {
|
|
2886
3795
|
throw new Error(`${dirPath} is not a directory.`);
|
|
2887
3796
|
}
|
|
@@ -2889,7 +3798,7 @@ var WorkspaceRegistry = class {
|
|
|
2889
3798
|
}
|
|
2890
3799
|
async read() {
|
|
2891
3800
|
try {
|
|
2892
|
-
const content = await
|
|
3801
|
+
const content = await fs20.readFile(this.filePath, "utf8");
|
|
2893
3802
|
const parsed = JSON.parse(content);
|
|
2894
3803
|
return Array.isArray(parsed) ? parsed : [];
|
|
2895
3804
|
} catch {
|
|
@@ -2897,8 +3806,8 @@ var WorkspaceRegistry = class {
|
|
|
2897
3806
|
}
|
|
2898
3807
|
}
|
|
2899
3808
|
async write(entries) {
|
|
2900
|
-
await
|
|
2901
|
-
await
|
|
3809
|
+
await fs20.mkdir(path16.dirname(this.filePath), { recursive: true });
|
|
3810
|
+
await fs20.writeFile(this.filePath, JSON.stringify(entries, null, 2) + "\n", {
|
|
2902
3811
|
encoding: "utf8",
|
|
2903
3812
|
mode: 384
|
|
2904
3813
|
});
|
|
@@ -2982,13 +3891,92 @@ var Hub = class {
|
|
|
2982
3891
|
};
|
|
2983
3892
|
|
|
2984
3893
|
// src/server/routes/api.ts
|
|
2985
|
-
import { constants as
|
|
2986
|
-
import { promises as
|
|
2987
|
-
import
|
|
2988
|
-
import
|
|
3894
|
+
import { constants as constants3, existsSync } from "fs";
|
|
3895
|
+
import { promises as fs22 } from "fs";
|
|
3896
|
+
import path18 from "path";
|
|
3897
|
+
import spawn5 from "cross-spawn";
|
|
2989
3898
|
|
|
2990
|
-
// src/
|
|
2991
|
-
|
|
3899
|
+
// src/core/env/write.ts
|
|
3900
|
+
import { constants as constants2, promises as fs21 } from "fs";
|
|
3901
|
+
import path17 from "path";
|
|
3902
|
+
function isValidEnvKey(key) {
|
|
3903
|
+
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(key) && key.length <= 128;
|
|
3904
|
+
}
|
|
3905
|
+
function isValidEnvValue(value) {
|
|
3906
|
+
if (value.length > 4096) {
|
|
3907
|
+
return false;
|
|
3908
|
+
}
|
|
3909
|
+
for (const character of value) {
|
|
3910
|
+
const code = character.codePointAt(0) ?? 0;
|
|
3911
|
+
if (code < 32 || code === 127) {
|
|
3912
|
+
return false;
|
|
3913
|
+
}
|
|
3914
|
+
}
|
|
3915
|
+
return true;
|
|
3916
|
+
}
|
|
3917
|
+
function formatEnvLine(key, value) {
|
|
3918
|
+
if (value === "" || /[\s#'"\\]/.test(value)) {
|
|
3919
|
+
const escaped = value.replaceAll("\\", "\\\\").replaceAll('"', '\\"');
|
|
3920
|
+
return `${key}="${escaped}"`;
|
|
3921
|
+
}
|
|
3922
|
+
return `${key}=${value}`;
|
|
3923
|
+
}
|
|
3924
|
+
function applyEnvValue(content, key, value) {
|
|
3925
|
+
const lines = content.split(/\r?\n/);
|
|
3926
|
+
const prefix = new RegExp(`^\\s*(?:export\\s+)?${key}\\s*=`);
|
|
3927
|
+
const index = lines.findIndex((line) => prefix.test(line));
|
|
3928
|
+
if (index >= 0) {
|
|
3929
|
+
lines[index] = formatEnvLine(key, value);
|
|
3930
|
+
return { content: lines.join("\n"), action: "updated" };
|
|
3931
|
+
}
|
|
3932
|
+
const body = content.length === 0 || content.endsWith("\n") ? content : `${content}
|
|
3933
|
+
`;
|
|
3934
|
+
return { content: `${body}${formatEnvLine(key, value)}
|
|
3935
|
+
`, action: "added" };
|
|
3936
|
+
}
|
|
3937
|
+
function isWithinRoot9(root, target) {
|
|
3938
|
+
const relative = path17.relative(path17.resolve(root), path17.resolve(target));
|
|
3939
|
+
return relative === "" || !relative.startsWith("..") && !path17.isAbsolute(relative);
|
|
3940
|
+
}
|
|
3941
|
+
async function setEnvValue(options) {
|
|
3942
|
+
const { root, key, value } = options;
|
|
3943
|
+
if (!isValidEnvKey(key)) {
|
|
3944
|
+
return { ok: false, error: "Invalid key name." };
|
|
3945
|
+
}
|
|
3946
|
+
if (!isValidEnvValue(value)) {
|
|
3947
|
+
return { ok: false, error: "Value must be a single line under 4096 characters." };
|
|
3948
|
+
}
|
|
3949
|
+
const target = options.localPath ?? path17.join(root, ".env");
|
|
3950
|
+
if (!isWithinRoot9(root, target)) {
|
|
3951
|
+
return { ok: false, error: "The env file must live inside the project." };
|
|
3952
|
+
}
|
|
3953
|
+
try {
|
|
3954
|
+
const realParent = await fs21.realpath(path17.dirname(target));
|
|
3955
|
+
const realRoot = await fs21.realpath(root);
|
|
3956
|
+
if (!isWithinRoot9(realRoot, realParent)) {
|
|
3957
|
+
return { ok: false, error: "The env file must live inside the project." };
|
|
3958
|
+
}
|
|
3959
|
+
let current = "";
|
|
3960
|
+
try {
|
|
3961
|
+
current = await fs21.readFile(target, "utf8");
|
|
3962
|
+
} catch {
|
|
3963
|
+
}
|
|
3964
|
+
const next = applyEnvValue(current, key, value);
|
|
3965
|
+
const handle2 = await fs21.open(
|
|
3966
|
+
target,
|
|
3967
|
+
constants2.O_CREAT | constants2.O_WRONLY | constants2.O_TRUNC,
|
|
3968
|
+
384
|
|
3969
|
+
);
|
|
3970
|
+
try {
|
|
3971
|
+
await handle2.writeFile(next.content, "utf8");
|
|
3972
|
+
} finally {
|
|
3973
|
+
await handle2.close();
|
|
3974
|
+
}
|
|
3975
|
+
return { ok: true, action: next.action };
|
|
3976
|
+
} catch {
|
|
3977
|
+
return { ok: false, error: "Could not write the env file." };
|
|
3978
|
+
}
|
|
3979
|
+
}
|
|
2992
3980
|
|
|
2993
3981
|
// src/server/localAccess.ts
|
|
2994
3982
|
var LOCAL_HOSTNAMES = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "::1"]);
|
|
@@ -3174,9 +4162,9 @@ function isAllowedTerminalCommand(command) {
|
|
|
3174
4162
|
}
|
|
3175
4163
|
|
|
3176
4164
|
// src/server/routes/api.ts
|
|
3177
|
-
function
|
|
3178
|
-
const relative =
|
|
3179
|
-
return relative === "" || !relative.startsWith("..") && !
|
|
4165
|
+
function isWithinRoot10(root, target) {
|
|
4166
|
+
const relative = path18.relative(path18.resolve(root), path18.resolve(target));
|
|
4167
|
+
return relative === "" || !relative.startsWith("..") && !path18.isAbsolute(relative);
|
|
3180
4168
|
}
|
|
3181
4169
|
function isAllowedMutationOrigin(requestUrl, origin) {
|
|
3182
4170
|
if (origin === null) {
|
|
@@ -3208,37 +4196,37 @@ function hasMutationIntent(intent) {
|
|
|
3208
4196
|
return intent === "dashboard";
|
|
3209
4197
|
}
|
|
3210
4198
|
async function realPathWithinRoot(root, target) {
|
|
3211
|
-
if (!
|
|
4199
|
+
if (!isWithinRoot10(root, target)) {
|
|
3212
4200
|
return false;
|
|
3213
4201
|
}
|
|
3214
4202
|
try {
|
|
3215
|
-
const [realRoot, realTarget] = await Promise.all([
|
|
3216
|
-
return
|
|
4203
|
+
const [realRoot, realTarget] = await Promise.all([fs22.realpath(root), fs22.realpath(target)]);
|
|
4204
|
+
return isWithinRoot10(realRoot, realTarget);
|
|
3217
4205
|
} catch {
|
|
3218
4206
|
return false;
|
|
3219
4207
|
}
|
|
3220
4208
|
}
|
|
3221
4209
|
async function writableDestinationWithinRoot(root, destination) {
|
|
3222
|
-
if (!
|
|
4210
|
+
if (!isWithinRoot10(root, destination)) {
|
|
3223
4211
|
return false;
|
|
3224
4212
|
}
|
|
3225
4213
|
try {
|
|
3226
4214
|
const [realRoot, realParent] = await Promise.all([
|
|
3227
|
-
|
|
3228
|
-
|
|
4215
|
+
fs22.realpath(root),
|
|
4216
|
+
fs22.realpath(path18.dirname(destination))
|
|
3229
4217
|
]);
|
|
3230
|
-
return
|
|
4218
|
+
return isWithinRoot10(realRoot, realParent);
|
|
3231
4219
|
} catch {
|
|
3232
4220
|
return false;
|
|
3233
4221
|
}
|
|
3234
4222
|
}
|
|
3235
4223
|
async function copyFileExclusive(source, destination) {
|
|
3236
|
-
const content = await
|
|
4224
|
+
const content = await fs22.readFile(source);
|
|
3237
4225
|
let handle2 = null;
|
|
3238
4226
|
try {
|
|
3239
|
-
handle2 = await
|
|
4227
|
+
handle2 = await fs22.open(
|
|
3240
4228
|
destination,
|
|
3241
|
-
|
|
4229
|
+
constants3.O_CREAT | constants3.O_EXCL | constants3.O_WRONLY,
|
|
3242
4230
|
384
|
|
3243
4231
|
);
|
|
3244
4232
|
await handle2.writeFile(content);
|
|
@@ -3263,15 +4251,15 @@ function resolveCommandPromptExecutable() {
|
|
|
3263
4251
|
return process.env.ComSpec ?? "cmd.exe";
|
|
3264
4252
|
}
|
|
3265
4253
|
function findExecutable(command) {
|
|
3266
|
-
if (
|
|
4254
|
+
if (path18.isAbsolute(command)) {
|
|
3267
4255
|
return existsSync(command) ? command : null;
|
|
3268
4256
|
}
|
|
3269
4257
|
const pathValue = process.env.PATH ?? "";
|
|
3270
|
-
for (const directory of pathValue.split(
|
|
4258
|
+
for (const directory of pathValue.split(path18.delimiter)) {
|
|
3271
4259
|
if (directory.length === 0) {
|
|
3272
4260
|
continue;
|
|
3273
4261
|
}
|
|
3274
|
-
const candidate =
|
|
4262
|
+
const candidate = path18.join(directory, command);
|
|
3275
4263
|
if (existsSync(candidate)) {
|
|
3276
4264
|
return candidate;
|
|
3277
4265
|
}
|
|
@@ -3279,7 +4267,7 @@ function findExecutable(command) {
|
|
|
3279
4267
|
return null;
|
|
3280
4268
|
}
|
|
3281
4269
|
function launchDetached(command, args, root) {
|
|
3282
|
-
const child =
|
|
4270
|
+
const child = spawn5(command, args, {
|
|
3283
4271
|
cwd: root,
|
|
3284
4272
|
detached: true,
|
|
3285
4273
|
stdio: "ignore",
|
|
@@ -3354,6 +4342,33 @@ async function onboardingForRoot(root) {
|
|
|
3354
4342
|
const warnings = await runDoctor(root, scan);
|
|
3355
4343
|
return buildOnboardingPlan(scan, warnings);
|
|
3356
4344
|
}
|
|
4345
|
+
async function passportResponse(root, context) {
|
|
4346
|
+
const scan = await scanProject(root);
|
|
4347
|
+
const warnings = await runDoctor(root, scan);
|
|
4348
|
+
const plan = buildOnboardingPlan(scan, warnings);
|
|
4349
|
+
const html = renderPassportHtml({ scan, warnings, plan, version: DEV_SURFACE_VERSION });
|
|
4350
|
+
if (context.req.query("download") === "1") {
|
|
4351
|
+
const safeName = scan.projectName.replace(/[^a-zA-Z0-9._-]+/g, "-").slice(0, 60) || "project";
|
|
4352
|
+
context.header("Content-Disposition", `attachment; filename="${safeName}-passport.html"`);
|
|
4353
|
+
}
|
|
4354
|
+
return await context.html(html);
|
|
4355
|
+
}
|
|
4356
|
+
async function handleEnvSet(root, body) {
|
|
4357
|
+
if (body === null || typeof body.key !== "string" || typeof body.value !== "string") {
|
|
4358
|
+
return { status: 400, payload: { error: "key and value are required." } };
|
|
4359
|
+
}
|
|
4360
|
+
const scan = await scanProject(root);
|
|
4361
|
+
const result = await setEnvValue({
|
|
4362
|
+
root,
|
|
4363
|
+
localPath: scan.env?.localPath ?? null,
|
|
4364
|
+
key: body.key,
|
|
4365
|
+
value: body.value
|
|
4366
|
+
});
|
|
4367
|
+
if (!result.ok) {
|
|
4368
|
+
return { status: 400, payload: { error: result.error } };
|
|
4369
|
+
}
|
|
4370
|
+
return { status: 200, payload: { status: result.action, key: body.key } };
|
|
4371
|
+
}
|
|
3357
4372
|
function registerWorkspaceRoutes(app, resolveWorkspace) {
|
|
3358
4373
|
app.get("/api/workspaces/:id/project", async (context) => {
|
|
3359
4374
|
const ws = await resolveWorkspace(context.req.param("id"));
|
|
@@ -3370,6 +4385,11 @@ function registerWorkspaceRoutes(app, resolveWorkspace) {
|
|
|
3370
4385
|
if (!ws) return context.json({ error: "Workspace not found." }, 404);
|
|
3371
4386
|
return context.json(await onboardingForRoot(ws.root));
|
|
3372
4387
|
});
|
|
4388
|
+
app.get("/api/workspaces/:id/passport", async (context) => {
|
|
4389
|
+
const ws = await resolveWorkspace(context.req.param("id"));
|
|
4390
|
+
if (!ws) return context.json({ error: "Workspace not found." }, 404);
|
|
4391
|
+
return passportResponse(ws.root, context);
|
|
4392
|
+
});
|
|
3373
4393
|
app.get("/api/workspaces/:id/processes", async (context) => {
|
|
3374
4394
|
const ws = await resolveWorkspace(context.req.param("id"));
|
|
3375
4395
|
if (!ws) return context.json({ error: "Workspace not found." }, 404);
|
|
@@ -3498,7 +4518,7 @@ function registerWorkspaceRoutes(app, resolveWorkspace) {
|
|
|
3498
4518
|
app.post("/api/workspaces/:id/open/package", async (context) => {
|
|
3499
4519
|
const ws = await resolveWorkspace(context.req.param("id"));
|
|
3500
4520
|
if (!ws) return context.json({ error: "Workspace not found." }, 404);
|
|
3501
|
-
const packagePath =
|
|
4521
|
+
const packagePath = path18.join(ws.root, "package.json");
|
|
3502
4522
|
if (!await realPathWithinRoot(ws.root, packagePath)) {
|
|
3503
4523
|
return context.json({ error: "package.json was not found inside the project root." }, 404);
|
|
3504
4524
|
}
|
|
@@ -3520,7 +4540,7 @@ function registerWorkspaceRoutes(app, resolveWorkspace) {
|
|
|
3520
4540
|
if (examplePath === null) {
|
|
3521
4541
|
return context.json({ error: ".env.example was not found." }, 404);
|
|
3522
4542
|
}
|
|
3523
|
-
const destination = localPath ??
|
|
4543
|
+
const destination = localPath ?? path18.join(ws.root, scan.config?.config.env?.local ?? ".env");
|
|
3524
4544
|
if (!await realPathWithinRoot(ws.root, examplePath) || !await writableDestinationWithinRoot(ws.root, destination)) {
|
|
3525
4545
|
return context.json({ error: "Refusing to copy env files outside the project root." }, 400);
|
|
3526
4546
|
}
|
|
@@ -3530,6 +4550,13 @@ function registerWorkspaceRoutes(app, resolveWorkspace) {
|
|
|
3530
4550
|
}
|
|
3531
4551
|
return context.json({ copied: true });
|
|
3532
4552
|
});
|
|
4553
|
+
app.post("/api/workspaces/:id/env/set", async (context) => {
|
|
4554
|
+
const ws = await resolveWorkspace(context.req.param("id"));
|
|
4555
|
+
if (!ws) return context.json({ error: "Workspace not found." }, 404);
|
|
4556
|
+
const body = await context.req.json().catch(() => null);
|
|
4557
|
+
const result = await handleEnvSet(ws.root, body);
|
|
4558
|
+
return context.json(result.payload, result.status);
|
|
4559
|
+
});
|
|
3533
4560
|
app.delete("/api/workspaces/:id/run/:pid", async (context) => {
|
|
3534
4561
|
const ws = await resolveWorkspace(context.req.param("id"));
|
|
3535
4562
|
if (!ws) return context.json({ error: "Workspace not found." }, 404);
|
|
@@ -3597,6 +4624,11 @@ function registerHubApiRoutes(app, options) {
|
|
|
3597
4624
|
if (entries.length === 0) return context.json({ error: "No workspaces registered." }, 404);
|
|
3598
4625
|
return context.json(await onboardingForRoot(hub.ensure(entries[0]).root));
|
|
3599
4626
|
});
|
|
4627
|
+
app.get("/api/passport", async (context) => {
|
|
4628
|
+
const entries = await hub.registry.list();
|
|
4629
|
+
if (entries.length === 0) return context.json({ error: "No workspaces registered." }, 404);
|
|
4630
|
+
return passportResponse(hub.ensure(entries[0]).root, context);
|
|
4631
|
+
});
|
|
3600
4632
|
app.get("/api/processes", async (context) => {
|
|
3601
4633
|
const entries = await hub.registry.list();
|
|
3602
4634
|
if (entries.length === 0) return context.json([]);
|
|
@@ -3713,21 +4745,21 @@ function warnIfContainerRootsUnset(host) {
|
|
|
3713
4745
|
}
|
|
3714
4746
|
async function fileExists(filePath) {
|
|
3715
4747
|
try {
|
|
3716
|
-
await
|
|
4748
|
+
await fs23.access(filePath);
|
|
3717
4749
|
return true;
|
|
3718
4750
|
} catch {
|
|
3719
4751
|
return false;
|
|
3720
4752
|
}
|
|
3721
4753
|
}
|
|
3722
4754
|
async function findWebDistDir() {
|
|
3723
|
-
const moduleDir =
|
|
4755
|
+
const moduleDir = path19.dirname(fileURLToPath2(import.meta.url));
|
|
3724
4756
|
const candidates = [
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
4757
|
+
path19.join(moduleDir, "..", "web", "dist"),
|
|
4758
|
+
path19.join(moduleDir, "..", "..", "src", "web", "dist"),
|
|
4759
|
+
path19.join(moduleDir, "web", "dist")
|
|
3728
4760
|
];
|
|
3729
4761
|
for (const candidate of candidates) {
|
|
3730
|
-
if (await fileExists(
|
|
4762
|
+
if (await fileExists(path19.join(candidate, "index.html"))) {
|
|
3731
4763
|
return candidate;
|
|
3732
4764
|
}
|
|
3733
4765
|
}
|
|
@@ -3799,7 +4831,7 @@ async function mountWebUi(app) {
|
|
|
3799
4831
|
app.use("/assets/*", serveStatic({ root: webDistDir }));
|
|
3800
4832
|
app.get("/favicon.svg", serveStatic({ root: webDistDir }));
|
|
3801
4833
|
app.get("*", async (context) => {
|
|
3802
|
-
const html = await
|
|
4834
|
+
const html = await fs23.readFile(path19.join(webDistDir, "index.html"), "utf8");
|
|
3803
4835
|
return context.html(html);
|
|
3804
4836
|
});
|
|
3805
4837
|
} else {
|
|
@@ -3899,10 +4931,10 @@ function dashboardUrl(workspaceId2, port = DEFAULT_PORT, host = DEFAULT_HOST) {
|
|
|
3899
4931
|
}
|
|
3900
4932
|
|
|
3901
4933
|
// src/cli/commands/start.ts
|
|
3902
|
-
async function
|
|
4934
|
+
async function startCommand2(options) {
|
|
3903
4935
|
const cwd = options.cwd ?? process.cwd();
|
|
3904
4936
|
const port = options.port ?? 4567;
|
|
3905
|
-
console.log(
|
|
4937
|
+
console.log(pc7.bold(`DevSurface v${DEV_SURFACE_VERSION}`));
|
|
3906
4938
|
console.log("Scanning project...\n");
|
|
3907
4939
|
const scan = await scanProject(cwd);
|
|
3908
4940
|
printScanResult(scan);
|
|
@@ -3910,7 +4942,7 @@ async function startCommand(options) {
|
|
|
3910
4942
|
if (warnings.length > 0) {
|
|
3911
4943
|
console.log("\nWarnings:");
|
|
3912
4944
|
for (const item of warnings) {
|
|
3913
|
-
const marker = item.severity === "error" ?
|
|
4945
|
+
const marker = item.severity === "error" ? pc7.red("!") : pc7.yellow("!");
|
|
3914
4946
|
console.log(` ${marker} ${item.title}`);
|
|
3915
4947
|
}
|
|
3916
4948
|
}
|
|
@@ -3919,8 +4951,8 @@ async function startCommand(options) {
|
|
|
3919
4951
|
const registered = await registerWorkspaceRemotely(cwd, port);
|
|
3920
4952
|
if (registered) {
|
|
3921
4953
|
const url = dashboardUrl(registered.id, port);
|
|
3922
|
-
console.log(`Workspace ${
|
|
3923
|
-
console.log(`Dashboard -> ${
|
|
4954
|
+
console.log(`Workspace ${pc7.cyan(registered.name)} attached.`);
|
|
4955
|
+
console.log(`Dashboard -> ${pc7.cyan(url)}`);
|
|
3924
4956
|
if (options.openBrowser !== false) {
|
|
3925
4957
|
await open_default(url);
|
|
3926
4958
|
}
|
|
@@ -3934,13 +4966,13 @@ async function startCommand(options) {
|
|
|
3934
4966
|
initialWorkspace: cwd
|
|
3935
4967
|
});
|
|
3936
4968
|
console.log(`
|
|
3937
|
-
Dashboard running at -> ${
|
|
4969
|
+
Dashboard running at -> ${pc7.cyan(server.url)}`);
|
|
3938
4970
|
}
|
|
3939
4971
|
|
|
3940
4972
|
// src/cli/commands/serve.ts
|
|
3941
|
-
import
|
|
4973
|
+
import pc8 from "picocolors";
|
|
3942
4974
|
async function serveCommand(options) {
|
|
3943
|
-
console.log(
|
|
4975
|
+
console.log(pc8.bold(`DevSurface Hub v${DEV_SURFACE_VERSION}`));
|
|
3944
4976
|
console.log("Starting hub server...\n");
|
|
3945
4977
|
const server = await startHubServer({
|
|
3946
4978
|
port: options.port,
|
|
@@ -3950,7 +4982,7 @@ async function serveCommand(options) {
|
|
|
3950
4982
|
if (summaries.length > 0) {
|
|
3951
4983
|
console.log(`Registered workspaces: ${summaries.length}`);
|
|
3952
4984
|
for (const ws of summaries) {
|
|
3953
|
-
console.log(` ${
|
|
4985
|
+
console.log(` ${pc8.cyan(ws.name)} -> ${ws.path}`);
|
|
3954
4986
|
}
|
|
3955
4987
|
} else {
|
|
3956
4988
|
console.log(
|
|
@@ -3958,17 +4990,17 @@ async function serveCommand(options) {
|
|
|
3958
4990
|
);
|
|
3959
4991
|
}
|
|
3960
4992
|
console.log(`
|
|
3961
|
-
Hub running at -> ${
|
|
4993
|
+
Hub running at -> ${pc8.cyan(server.url)}`);
|
|
3962
4994
|
}
|
|
3963
4995
|
|
|
3964
4996
|
// src/cli/commands/workspace.ts
|
|
3965
|
-
import
|
|
3966
|
-
import
|
|
4997
|
+
import path20 from "path";
|
|
4998
|
+
import pc9 from "picocolors";
|
|
3967
4999
|
async function workspaceAddCommand(dirPath) {
|
|
3968
5000
|
const registry = new WorkspaceRegistry();
|
|
3969
|
-
const target =
|
|
5001
|
+
const target = path20.resolve(dirPath ?? process.cwd());
|
|
3970
5002
|
const entry = await registry.add(target);
|
|
3971
|
-
console.log(`Added workspace ${
|
|
5003
|
+
console.log(`Added workspace ${pc9.cyan(entry.name)} (${entry.id}) -> ${entry.path}`);
|
|
3972
5004
|
}
|
|
3973
5005
|
async function workspaceListCommand() {
|
|
3974
5006
|
const registry = new WorkspaceRegistry();
|
|
@@ -3982,7 +5014,7 @@ async function workspaceListCommand() {
|
|
|
3982
5014
|
console.log(`${entries.length} workspace${entries.length === 1 ? "" : "s"}:
|
|
3983
5015
|
`);
|
|
3984
5016
|
for (const entry of entries) {
|
|
3985
|
-
console.log(` ${
|
|
5017
|
+
console.log(` ${pc9.cyan(entry.name)} (${entry.id})`);
|
|
3986
5018
|
console.log(` ${entry.path}`);
|
|
3987
5019
|
}
|
|
3988
5020
|
}
|
|
@@ -3990,7 +5022,7 @@ async function workspaceRemoveCommand(id) {
|
|
|
3990
5022
|
const registry = new WorkspaceRegistry();
|
|
3991
5023
|
const removed = await registry.remove(id);
|
|
3992
5024
|
if (removed) {
|
|
3993
|
-
console.log(`Removed workspace ${
|
|
5025
|
+
console.log(`Removed workspace ${pc9.cyan(id)}.`);
|
|
3994
5026
|
} else {
|
|
3995
5027
|
console.error(`Workspace "${id}" not found.`);
|
|
3996
5028
|
process.exitCode = 1;
|
|
@@ -4087,9 +5119,9 @@ function handle(command) {
|
|
|
4087
5119
|
process.exitCode = 1;
|
|
4088
5120
|
});
|
|
4089
5121
|
}
|
|
4090
|
-
program.name("devsurface").description("Turn any Node.js repository into a local developer control panel.").version(DEV_SURFACE_VERSION).option("-p, --port <port>", "dashboard port", toPort, 4567).option("--no-open", "do not open the browser automatically").action((options) => {
|
|
5122
|
+
program.name("devsurface").description("Turn any Node.js repository into a local developer control panel.").version(DEV_SURFACE_VERSION).enablePositionalOptions().option("-p, --port <port>", "dashboard port", toPort, 4567).option("--no-open", "do not open the browser automatically").action((options) => {
|
|
4091
5123
|
handle(
|
|
4092
|
-
|
|
5124
|
+
startCommand2({
|
|
4093
5125
|
cwd: process.cwd(),
|
|
4094
5126
|
port: options.port,
|
|
4095
5127
|
openBrowser: options.open
|
|
@@ -4123,6 +5155,9 @@ program.command("doctor").description("Print setup health warnings.").action(()
|
|
|
4123
5155
|
program.command("onboard").description("Print a guided setup checklist with readiness score.").action(() => {
|
|
4124
5156
|
handle(onboardCommand(process.cwd()));
|
|
4125
5157
|
});
|
|
5158
|
+
program.command("passport").description("Generate a shareable HTML onboarding report (Project Passport).").option("-o, --out <file>", "output file path", "devsurface-passport.html").action((options) => {
|
|
5159
|
+
handle(passportCommand(process.cwd(), options.out));
|
|
5160
|
+
});
|
|
4126
5161
|
program.command("init").description("Create a starter devsurface.config.json.").action(() => {
|
|
4127
5162
|
handle(initCommand(process.cwd()));
|
|
4128
5163
|
});
|