devsurface 0.1.0 → 0.3.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 +23 -0
- package/README.md +110 -15
- package/SECURITY.md +9 -0
- package/action/dist/index.js +644 -0
- package/action.yml +39 -0
- package/dist/cli/index.js +1312 -236
- package/dist/cli/index.js.map +1 -1
- package/package.json +9 -5
- package/src/web/dist/assets/index-7njY8n4D.js +10 -0
- package/src/web/dist/assets/index-DvunFIw4.css +1 -0
- package/src/web/dist/index.html +2 -2
- package/src/web/dist/assets/index-BPOLPimA.js +0 -10
- package/src/web/dist/assets/index-Ch_lsiJZ.css +0 -1
package/dist/cli/index.js
CHANGED
|
@@ -10,6 +10,27 @@ import pc from "picocolors";
|
|
|
10
10
|
import { promises as fs9 } from "fs";
|
|
11
11
|
import path9 from "path";
|
|
12
12
|
|
|
13
|
+
// src/core/documentation.ts
|
|
14
|
+
function extractScriptReferences(content) {
|
|
15
|
+
const references = /* @__PURE__ */ new Set();
|
|
16
|
+
const commandRegexes = [
|
|
17
|
+
/\bnpm\s+run\s+([A-Za-z0-9:_-]+)/g,
|
|
18
|
+
/\bpnpm\s+run\s+([A-Za-z0-9:_-]+)/g,
|
|
19
|
+
/\bbun\s+run\s+([A-Za-z0-9:_-]+)/g,
|
|
20
|
+
/\byarn\s+run\s+([A-Za-z0-9:_-]+)/g,
|
|
21
|
+
/\bnpm\s+(test|start|build)\b/g,
|
|
22
|
+
/\bpnpm\s+(test|start|build)\b/g,
|
|
23
|
+
/\byarn\s+(test|start|build)\b/g,
|
|
24
|
+
/\bbun\s+(test|start|build)\b/g
|
|
25
|
+
];
|
|
26
|
+
for (const regex of commandRegexes) {
|
|
27
|
+
for (const match of content.matchAll(regex)) {
|
|
28
|
+
references.add(match[1]);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return Array.from(references);
|
|
32
|
+
}
|
|
33
|
+
|
|
13
34
|
// src/core/scanner/index.ts
|
|
14
35
|
import { promises as fs8 } from "fs";
|
|
15
36
|
import path8 from "path";
|
|
@@ -18,6 +39,16 @@ import path8 from "path";
|
|
|
18
39
|
import { promises as fs } from "fs";
|
|
19
40
|
import path from "path";
|
|
20
41
|
|
|
42
|
+
// src/core/security/url.ts
|
|
43
|
+
function isSafeHttpUrl(value) {
|
|
44
|
+
try {
|
|
45
|
+
const url = new URL(value);
|
|
46
|
+
return url.protocol === "http:" || url.protocol === "https:";
|
|
47
|
+
} catch {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
21
52
|
// src/core/config/defaults.ts
|
|
22
53
|
var CONFIG_FILE_NAME = "devsurface.config.json";
|
|
23
54
|
var defaultConfig = {
|
|
@@ -48,6 +79,11 @@ var defaultConfig = {
|
|
|
48
79
|
};
|
|
49
80
|
|
|
50
81
|
// src/core/config/load.ts
|
|
82
|
+
var MAX_CONFIGURED_PORTS = 32;
|
|
83
|
+
function isWithinRoot(root, target) {
|
|
84
|
+
const relative = path.relative(root, target);
|
|
85
|
+
return relative === "" || !relative.startsWith("..") && !path.isAbsolute(relative);
|
|
86
|
+
}
|
|
51
87
|
function isRecord(value) {
|
|
52
88
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
53
89
|
}
|
|
@@ -101,7 +137,10 @@ function toPorts(value, warnings) {
|
|
|
101
137
|
if (ports.length !== value.length) {
|
|
102
138
|
warnings.push("ports may only contain integers between 1 and 65535.");
|
|
103
139
|
}
|
|
104
|
-
|
|
140
|
+
if (ports.length > MAX_CONFIGURED_PORTS) {
|
|
141
|
+
warnings.push(`ports may contain at most ${MAX_CONFIGURED_PORTS} entries.`);
|
|
142
|
+
}
|
|
143
|
+
return ports.slice(0, MAX_CONFIGURED_PORTS);
|
|
105
144
|
}
|
|
106
145
|
function validateConfig(raw) {
|
|
107
146
|
const warnings = [];
|
|
@@ -121,6 +160,14 @@ function validateConfig(raw) {
|
|
|
121
160
|
if (raw.services !== void 0 && !isRecord(raw.services)) {
|
|
122
161
|
warnings.push("services must be an object.");
|
|
123
162
|
}
|
|
163
|
+
let docs;
|
|
164
|
+
if (typeof raw.docs === "string" && raw.docs.length > 0) {
|
|
165
|
+
if (isSafeHttpUrl(raw.docs)) {
|
|
166
|
+
docs = raw.docs;
|
|
167
|
+
} else {
|
|
168
|
+
warnings.push("docs must be an http or https URL.");
|
|
169
|
+
}
|
|
170
|
+
}
|
|
124
171
|
return {
|
|
125
172
|
config: {
|
|
126
173
|
name: typeof raw.name === "string" ? raw.name : void 0,
|
|
@@ -130,7 +177,7 @@ function validateConfig(raw) {
|
|
|
130
177
|
ports: toPorts(raw.ports, warnings),
|
|
131
178
|
env,
|
|
132
179
|
services,
|
|
133
|
-
docs
|
|
180
|
+
docs
|
|
134
181
|
},
|
|
135
182
|
warnings
|
|
136
183
|
};
|
|
@@ -138,10 +185,17 @@ function validateConfig(raw) {
|
|
|
138
185
|
async function loadConfig(root) {
|
|
139
186
|
const configPath = path.join(root, CONFIG_FILE_NAME);
|
|
140
187
|
try {
|
|
141
|
-
const
|
|
188
|
+
const [realRoot, realConfigPath] = await Promise.all([
|
|
189
|
+
fs.realpath(root),
|
|
190
|
+
fs.realpath(configPath)
|
|
191
|
+
]);
|
|
192
|
+
if (!isWithinRoot(realRoot, realConfigPath)) {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
const content = await fs.readFile(realConfigPath, "utf8");
|
|
142
196
|
const parsed = JSON.parse(content);
|
|
143
197
|
const { config, warnings } = validateConfig(parsed);
|
|
144
|
-
return { path:
|
|
198
|
+
return { path: realConfigPath, config, warnings };
|
|
145
199
|
} catch (error) {
|
|
146
200
|
const code = typeof error === "object" && error !== null && "code" in error ? error.code : void 0;
|
|
147
201
|
if (code === "ENOENT") {
|
|
@@ -158,7 +212,7 @@ async function loadConfig(root) {
|
|
|
158
212
|
}
|
|
159
213
|
}
|
|
160
214
|
|
|
161
|
-
// src/core/
|
|
215
|
+
// src/core/docker/compose.ts
|
|
162
216
|
import { promises as fs3 } from "fs";
|
|
163
217
|
import os from "os";
|
|
164
218
|
import path3 from "path";
|
|
@@ -169,7 +223,7 @@ import { parse as parseYaml } from "yaml";
|
|
|
169
223
|
import { constants } from "fs";
|
|
170
224
|
import { promises as fs2 } from "fs";
|
|
171
225
|
import path2 from "path";
|
|
172
|
-
function
|
|
226
|
+
function isWithinRoot2(root, target) {
|
|
173
227
|
const resolvedRoot = path2.resolve(root);
|
|
174
228
|
const resolvedTarget = path2.resolve(target);
|
|
175
229
|
const relative = path2.relative(resolvedRoot, resolvedTarget);
|
|
@@ -189,7 +243,7 @@ function executableNames(command) {
|
|
|
189
243
|
return extensions.map((extension) => `${command}${extension}`);
|
|
190
244
|
}
|
|
191
245
|
async function executableOutsideRoot(root, candidate) {
|
|
192
|
-
if (
|
|
246
|
+
if (isWithinRoot2(root, candidate)) {
|
|
193
247
|
return null;
|
|
194
248
|
}
|
|
195
249
|
try {
|
|
@@ -197,7 +251,7 @@ async function executableOutsideRoot(root, candidate) {
|
|
|
197
251
|
fs2.realpath(root),
|
|
198
252
|
fs2.realpath(candidate)
|
|
199
253
|
]);
|
|
200
|
-
if (
|
|
254
|
+
if (isWithinRoot2(realRoot, realCandidate)) {
|
|
201
255
|
return null;
|
|
202
256
|
}
|
|
203
257
|
await fs2.access(realCandidate, constants.X_OK);
|
|
@@ -212,7 +266,7 @@ async function resolveExecutableOutsideRoot(root, command) {
|
|
|
212
266
|
}
|
|
213
267
|
for (const entry of pathEntries(process.env.PATH ?? "")) {
|
|
214
268
|
const directory = path2.resolve(entry);
|
|
215
|
-
if (
|
|
269
|
+
if (isWithinRoot2(root, directory)) {
|
|
216
270
|
continue;
|
|
217
271
|
}
|
|
218
272
|
for (const executableName of executableNames(command)) {
|
|
@@ -225,50 +279,38 @@ async function resolveExecutableOutsideRoot(root, command) {
|
|
|
225
279
|
return null;
|
|
226
280
|
}
|
|
227
281
|
|
|
228
|
-
// src/core/
|
|
282
|
+
// src/core/docker/compose.ts
|
|
229
283
|
var COMPOSE_FILES = ["docker-compose.yml", "docker-compose.yaml", "compose.yml", "compose.yaml"];
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
return await fileExists(filePath) ? filePath : null;
|
|
243
|
-
})
|
|
244
|
-
);
|
|
245
|
-
return checks.filter((filePath) => filePath !== null);
|
|
246
|
-
}
|
|
247
|
-
async function extractServices(composePath) {
|
|
248
|
-
try {
|
|
249
|
-
const content = await fs3.readFile(composePath, "utf8");
|
|
250
|
-
const parsed = parseYaml(content);
|
|
251
|
-
if (typeof parsed === "object" && parsed !== null && "services" in parsed && typeof parsed.services === "object" && parsed.services !== null) {
|
|
252
|
-
return Object.keys(parsed.services);
|
|
253
|
-
}
|
|
254
|
-
} catch {
|
|
255
|
-
return [];
|
|
256
|
-
}
|
|
257
|
-
return [];
|
|
258
|
-
}
|
|
259
|
-
function isWithinRoot2(root, target) {
|
|
284
|
+
var COMMAND_OUTPUT_LIMIT = 2e5;
|
|
285
|
+
var ESC = String.fromCharCode(27);
|
|
286
|
+
var ANSI_CSI_SEQUENCE = new RegExp(`${ESC}\\[[0-?]*[ -/]*[@-~]`, "g");
|
|
287
|
+
var DockerOperationError = class extends Error {
|
|
288
|
+
constructor(code, message) {
|
|
289
|
+
super(message);
|
|
290
|
+
this.code = code;
|
|
291
|
+
this.name = "DockerOperationError";
|
|
292
|
+
}
|
|
293
|
+
code;
|
|
294
|
+
};
|
|
295
|
+
function isWithinRoot3(root, target) {
|
|
260
296
|
const relative = path3.relative(path3.resolve(root), path3.resolve(target));
|
|
261
297
|
return relative === "" || !relative.startsWith("..") && !path3.isAbsolute(relative);
|
|
262
298
|
}
|
|
263
299
|
function dockerCommandCwd(root) {
|
|
264
300
|
const candidates = [os.homedir(), os.tmpdir(), path3.parse(path3.resolve(root)).root];
|
|
265
|
-
return candidates.find((candidate) => !
|
|
301
|
+
return candidates.find((candidate) => !isWithinRoot3(root, candidate)) ?? os.homedir();
|
|
302
|
+
}
|
|
303
|
+
function appendBounded(current, chunk, limit) {
|
|
304
|
+
const combined = current + chunk;
|
|
305
|
+
return combined.length <= limit ? combined : combined.slice(-limit);
|
|
266
306
|
}
|
|
267
|
-
async
|
|
307
|
+
var runDockerCommand = async (root, args, options = {}) => {
|
|
268
308
|
const dockerExecutable = await resolveExecutableOutsideRoot(root, "docker");
|
|
269
309
|
if (dockerExecutable === null) {
|
|
270
|
-
return { code: null, stdout: "", stderr: "" };
|
|
310
|
+
return { code: null, stdout: "", stderr: "", error: "not-found" };
|
|
271
311
|
}
|
|
312
|
+
const timeoutMs = options.timeoutMs ?? 5e3;
|
|
313
|
+
const outputLimit = options.outputLimit ?? COMMAND_OUTPUT_LIMIT;
|
|
272
314
|
return await new Promise((resolve) => {
|
|
273
315
|
const child = spawn(dockerExecutable, args, {
|
|
274
316
|
cwd: dockerCommandCwd(root),
|
|
@@ -277,36 +319,152 @@ async function runDockerCommand(root, args, timeoutMs = 2500) {
|
|
|
277
319
|
let settled = false;
|
|
278
320
|
let stdout = "";
|
|
279
321
|
let stderr = "";
|
|
280
|
-
const
|
|
322
|
+
const finish = (result) => {
|
|
323
|
+
if (settled) {
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
281
326
|
settled = true;
|
|
327
|
+
clearTimeout(timeout);
|
|
328
|
+
resolve(result);
|
|
329
|
+
};
|
|
330
|
+
const timeout = setTimeout(() => {
|
|
282
331
|
child.kill();
|
|
283
|
-
|
|
332
|
+
finish({ code: null, stdout, stderr, error: "timeout" });
|
|
284
333
|
}, timeoutMs);
|
|
285
334
|
child.stdout?.on("data", (chunk) => {
|
|
286
|
-
stdout
|
|
335
|
+
stdout = appendBounded(stdout, chunk.toString(), outputLimit);
|
|
287
336
|
});
|
|
288
337
|
child.stderr?.on("data", (chunk) => {
|
|
289
|
-
stderr
|
|
338
|
+
stderr = appendBounded(stderr, chunk.toString(), outputLimit);
|
|
290
339
|
});
|
|
291
340
|
child.on("error", () => {
|
|
292
|
-
|
|
293
|
-
if (!settled) {
|
|
294
|
-
settled = true;
|
|
295
|
-
resolve({ code: null, stdout, stderr });
|
|
296
|
-
}
|
|
341
|
+
finish({ code: null, stdout, stderr, error: "spawn" });
|
|
297
342
|
});
|
|
298
343
|
child.on("close", (code) => {
|
|
299
|
-
|
|
300
|
-
if (!settled) {
|
|
301
|
-
settled = true;
|
|
302
|
-
resolve({ code, stdout, stderr });
|
|
303
|
-
}
|
|
344
|
+
finish({ code, stdout, stderr, error: null });
|
|
304
345
|
});
|
|
305
346
|
});
|
|
347
|
+
};
|
|
348
|
+
async function findComposeFiles(root) {
|
|
349
|
+
const resolvedRoot = await fs3.realpath(root).catch(() => path3.resolve(root));
|
|
350
|
+
const matches = [];
|
|
351
|
+
for (const file of COMPOSE_FILES) {
|
|
352
|
+
const candidate = path3.join(root, file);
|
|
353
|
+
try {
|
|
354
|
+
const [stat, realCandidate] = await Promise.all([fs3.stat(candidate), fs3.realpath(candidate)]);
|
|
355
|
+
if (stat.isFile() && isWithinRoot3(resolvedRoot, realCandidate)) {
|
|
356
|
+
matches.push(realCandidate);
|
|
357
|
+
}
|
|
358
|
+
} catch {
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return matches;
|
|
362
|
+
}
|
|
363
|
+
async function serviceNamesFromFiles(composeFiles) {
|
|
364
|
+
const names = /* @__PURE__ */ new Set();
|
|
365
|
+
for (const composeFile of composeFiles) {
|
|
366
|
+
try {
|
|
367
|
+
const parsed = parseYaml(await fs3.readFile(composeFile, "utf8"));
|
|
368
|
+
if (typeof parsed !== "object" || parsed === null || !("services" in parsed) || typeof parsed.services !== "object" || parsed.services === null || Array.isArray(parsed.services)) {
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
for (const name of Object.keys(parsed.services)) {
|
|
372
|
+
if (name.length > 0) {
|
|
373
|
+
names.add(name);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
} catch {
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return Array.from(names);
|
|
380
|
+
}
|
|
381
|
+
function composeArgs(root, composeFiles, args) {
|
|
382
|
+
return [
|
|
383
|
+
"compose",
|
|
384
|
+
...composeFiles.flatMap((composeFile) => ["-f", composeFile]),
|
|
385
|
+
"--project-directory",
|
|
386
|
+
root,
|
|
387
|
+
...args
|
|
388
|
+
];
|
|
389
|
+
}
|
|
390
|
+
function cleanMessage(value) {
|
|
391
|
+
return value.replace(ANSI_CSI_SEQUENCE, "").trim().slice(-1e3);
|
|
306
392
|
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
393
|
+
function resultMessage(result) {
|
|
394
|
+
return cleanMessage(result.stderr || result.stdout);
|
|
395
|
+
}
|
|
396
|
+
function daemonStatus(result, platform2) {
|
|
397
|
+
if (result.error === "not-found") {
|
|
398
|
+
return {
|
|
399
|
+
status: "not-installed",
|
|
400
|
+
running: false,
|
|
401
|
+
message: "Docker CLI was not found. Install Docker and refresh this page."
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
if (result.code === 0) {
|
|
405
|
+
return { status: "running", running: true, message: null };
|
|
406
|
+
}
|
|
407
|
+
if (platform2 === "win32" || platform2 === "darwin") {
|
|
408
|
+
return {
|
|
409
|
+
status: "stopped",
|
|
410
|
+
running: false,
|
|
411
|
+
message: "Docker is installed, but its engine is not responding. Start Docker Desktop and refresh."
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
const detail = resultMessage(result);
|
|
415
|
+
return {
|
|
416
|
+
status: result.error === "timeout" ? "unknown" : "stopped",
|
|
417
|
+
running: false,
|
|
418
|
+
message: detail ? `Docker is installed, but its daemon is not responding: ${detail}` : "Docker is installed, but its daemon is not responding. Start Docker and refresh."
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
function exitCodeFromRow(record) {
|
|
422
|
+
if (typeof record.ExitCode === "number") {
|
|
423
|
+
return record.ExitCode;
|
|
424
|
+
}
|
|
425
|
+
if (typeof record.ExitCode === "string" && /^\d+$/.test(record.ExitCode)) {
|
|
426
|
+
return Number(record.ExitCode);
|
|
427
|
+
}
|
|
428
|
+
if (typeof record.Status === "string") {
|
|
429
|
+
const match = /\bExited\s+\((\d+)\)/i.exec(record.Status);
|
|
430
|
+
if (match?.[1]) {
|
|
431
|
+
return Number(match[1]);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
return null;
|
|
435
|
+
}
|
|
436
|
+
function serviceStatusFromRow(record) {
|
|
437
|
+
const state = typeof record.State === "string" ? record.State.toLowerCase() : "";
|
|
438
|
+
const exitCode = exitCodeFromRow(record);
|
|
439
|
+
if (state === "running") {
|
|
440
|
+
return "running";
|
|
441
|
+
}
|
|
442
|
+
if (exitCode !== null && exitCode > 0) {
|
|
443
|
+
return "error";
|
|
444
|
+
}
|
|
445
|
+
if (state === "created" || state === "exited" || state === "stopped") {
|
|
446
|
+
return "stopped";
|
|
447
|
+
}
|
|
448
|
+
if (state === "dead" || state === "restarting" || state === "paused") {
|
|
449
|
+
return "error";
|
|
450
|
+
}
|
|
451
|
+
return "unknown";
|
|
452
|
+
}
|
|
453
|
+
function addComposeStatusRow(statuses, row) {
|
|
454
|
+
if (typeof row !== "object" || row === null) {
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
const record = row;
|
|
458
|
+
if (typeof record.Service !== "string") {
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
const detail = typeof record.Status === "string" && record.Status.trim() ? record.Status.trim() : typeof record.State === "string" && record.State.trim() ? record.State.trim() : null;
|
|
462
|
+
statuses.set(record.Service, {
|
|
463
|
+
name: record.Service,
|
|
464
|
+
status: serviceStatusFromRow(record),
|
|
465
|
+
statusDetail: detail,
|
|
466
|
+
containerId: typeof record.ID === "string" && record.ID.length > 0 ? record.ID : null
|
|
467
|
+
});
|
|
310
468
|
}
|
|
311
469
|
function parseComposePs(output) {
|
|
312
470
|
const statuses = /* @__PURE__ */ new Map();
|
|
@@ -316,82 +474,181 @@ function parseComposePs(output) {
|
|
|
316
474
|
}
|
|
317
475
|
try {
|
|
318
476
|
const parsed = JSON.parse(compactOutput);
|
|
319
|
-
const
|
|
320
|
-
for (const row of rows2) {
|
|
477
|
+
for (const row of Array.isArray(parsed) ? parsed : [parsed]) {
|
|
321
478
|
addComposeStatusRow(statuses, row);
|
|
322
479
|
}
|
|
323
480
|
return statuses;
|
|
324
481
|
} catch {
|
|
325
482
|
}
|
|
326
|
-
const
|
|
327
|
-
for (const row of rows) {
|
|
483
|
+
for (const line of compactOutput.split(/\r?\n/)) {
|
|
328
484
|
try {
|
|
329
|
-
|
|
330
|
-
addComposeStatusRow(statuses, parsed);
|
|
485
|
+
addComposeStatusRow(statuses, JSON.parse(line));
|
|
331
486
|
} catch {
|
|
332
|
-
return
|
|
487
|
+
return /* @__PURE__ */ new Map();
|
|
333
488
|
}
|
|
334
489
|
}
|
|
335
490
|
return statuses;
|
|
336
491
|
}
|
|
337
|
-
function
|
|
338
|
-
|
|
339
|
-
|
|
492
|
+
function unknownServices(serviceNames) {
|
|
493
|
+
return serviceNames.map((name) => ({
|
|
494
|
+
name,
|
|
495
|
+
status: "unknown",
|
|
496
|
+
statusDetail: null,
|
|
497
|
+
containerId: null
|
|
498
|
+
}));
|
|
499
|
+
}
|
|
500
|
+
function commandFailureMessage(action, result) {
|
|
501
|
+
const detail = resultMessage(result);
|
|
502
|
+
return detail ? `Docker Compose ${action} failed: ${detail}` : `Docker Compose ${action} failed.`;
|
|
503
|
+
}
|
|
504
|
+
var DockerComposeController = class {
|
|
505
|
+
constructor(root, options = {}) {
|
|
506
|
+
this.root = root;
|
|
507
|
+
this.runner = options.runner ?? runDockerCommand;
|
|
508
|
+
this.platform = options.platform ?? process.platform;
|
|
509
|
+
}
|
|
510
|
+
root;
|
|
511
|
+
runner;
|
|
512
|
+
platform;
|
|
513
|
+
async definition() {
|
|
514
|
+
const composeFiles = await findComposeFiles(this.root);
|
|
515
|
+
if (composeFiles.length === 0) {
|
|
516
|
+
return null;
|
|
517
|
+
}
|
|
518
|
+
return {
|
|
519
|
+
composeFiles,
|
|
520
|
+
serviceNames: await serviceNamesFromFiles(composeFiles)
|
|
521
|
+
};
|
|
340
522
|
}
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
523
|
+
async requireService(service) {
|
|
524
|
+
const definition = await this.definition();
|
|
525
|
+
if (definition === null) {
|
|
526
|
+
throw new DockerOperationError("compose-not-found", "No Docker Compose file was found.");
|
|
527
|
+
}
|
|
528
|
+
if (!definition.serviceNames.includes(service)) {
|
|
529
|
+
throw new DockerOperationError(
|
|
530
|
+
"service-not-found",
|
|
531
|
+
`Docker Compose service "${service}" was not found.`
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
return definition;
|
|
344
535
|
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
status
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
536
|
+
async requireDaemon() {
|
|
537
|
+
const result = await this.runner(this.root, ["info"], { timeoutMs: 5e3 });
|
|
538
|
+
const daemon = daemonStatus(result, this.platform);
|
|
539
|
+
if (daemon.status === "not-installed") {
|
|
540
|
+
throw new DockerOperationError("docker-not-installed", daemon.message ?? "Docker not found.");
|
|
541
|
+
}
|
|
542
|
+
if (!daemon.running) {
|
|
543
|
+
throw new DockerOperationError(
|
|
544
|
+
"docker-not-running",
|
|
545
|
+
daemon.message ?? "Docker is not running."
|
|
546
|
+
);
|
|
547
|
+
}
|
|
355
548
|
}
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
})
|
|
549
|
+
async inspect() {
|
|
550
|
+
const definition = await this.definition();
|
|
551
|
+
if (definition === null) {
|
|
552
|
+
return null;
|
|
553
|
+
}
|
|
554
|
+
const infoResult = await this.runner(this.root, ["info"], { timeoutMs: 5e3 });
|
|
555
|
+
const daemon = daemonStatus(infoResult, this.platform);
|
|
556
|
+
if (!daemon.running) {
|
|
557
|
+
return {
|
|
558
|
+
composeFiles: definition.composeFiles,
|
|
559
|
+
services: unknownServices(definition.serviceNames),
|
|
560
|
+
dockerRunning: false,
|
|
561
|
+
daemonStatus: daemon.status,
|
|
562
|
+
message: daemon.message
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
const ps = await this.runner(
|
|
566
|
+
this.root,
|
|
567
|
+
composeArgs(this.root, definition.composeFiles, ["ps", "--all", "--format", "json"]),
|
|
568
|
+
{ timeoutMs: 8e3 }
|
|
569
|
+
);
|
|
570
|
+
if (ps.code !== 0 || ps.error !== null) {
|
|
571
|
+
return {
|
|
572
|
+
composeFiles: definition.composeFiles,
|
|
573
|
+
services: definition.serviceNames.map((name) => ({
|
|
574
|
+
name,
|
|
575
|
+
status: "error",
|
|
576
|
+
statusDetail: null,
|
|
577
|
+
containerId: null
|
|
578
|
+
})),
|
|
579
|
+
dockerRunning: true,
|
|
580
|
+
daemonStatus: "running",
|
|
581
|
+
message: commandFailureMessage("status check", ps)
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
const statuses = parseComposePs(ps.stdout);
|
|
585
|
+
return {
|
|
586
|
+
composeFiles: definition.composeFiles,
|
|
587
|
+
services: definition.serviceNames.map(
|
|
588
|
+
(name) => statuses.get(name) ?? {
|
|
589
|
+
name,
|
|
590
|
+
status: "stopped",
|
|
591
|
+
statusDetail: null,
|
|
592
|
+
containerId: null
|
|
593
|
+
}
|
|
594
|
+
),
|
|
595
|
+
dockerRunning: true,
|
|
596
|
+
daemonStatus: "running",
|
|
597
|
+
message: null
|
|
598
|
+
};
|
|
362
599
|
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
name: service,
|
|
375
|
-
status: "stopped",
|
|
376
|
-
containerId: null
|
|
600
|
+
async action(service, action) {
|
|
601
|
+
const definition = await this.requireService(service);
|
|
602
|
+
await this.requireDaemon();
|
|
603
|
+
const composeCommand = action === "start" ? ["up", "-d", "--", service] : ["stop", "--", service];
|
|
604
|
+
const result = await this.runner(
|
|
605
|
+
this.root,
|
|
606
|
+
composeArgs(this.root, definition.composeFiles, composeCommand),
|
|
607
|
+
{ timeoutMs: 6e4 }
|
|
608
|
+
);
|
|
609
|
+
if (result.code !== 0 || result.error !== null) {
|
|
610
|
+
throw new DockerOperationError("command-failed", commandFailureMessage(action, result));
|
|
377
611
|
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
return null;
|
|
612
|
+
return {
|
|
613
|
+
service,
|
|
614
|
+
action,
|
|
615
|
+
output: cleanMessage(result.stdout || result.stderr)
|
|
616
|
+
};
|
|
384
617
|
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
618
|
+
async start(service) {
|
|
619
|
+
return await this.action(service, "start");
|
|
620
|
+
}
|
|
621
|
+
async stop(service) {
|
|
622
|
+
return await this.action(service, "stop");
|
|
623
|
+
}
|
|
624
|
+
async logs(service) {
|
|
625
|
+
const definition = await this.requireService(service);
|
|
626
|
+
await this.requireDaemon();
|
|
627
|
+
const result = await this.runner(
|
|
628
|
+
this.root,
|
|
629
|
+
composeArgs(this.root, definition.composeFiles, [
|
|
630
|
+
"logs",
|
|
631
|
+
"--no-color",
|
|
632
|
+
"--tail",
|
|
633
|
+
"200",
|
|
634
|
+
"--",
|
|
635
|
+
service
|
|
636
|
+
]),
|
|
637
|
+
{ timeoutMs: 15e3, outputLimit: COMMAND_OUTPUT_LIMIT }
|
|
638
|
+
);
|
|
639
|
+
if (result.code !== 0 || result.error !== null) {
|
|
640
|
+
throw new DockerOperationError("command-failed", commandFailureMessage("logs", result));
|
|
641
|
+
}
|
|
642
|
+
return {
|
|
643
|
+
service,
|
|
644
|
+
logs: appendBounded("", `${result.stdout}${result.stderr}`, COMMAND_OUTPUT_LIMIT)
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
};
|
|
648
|
+
|
|
649
|
+
// src/core/scanner/docker.ts
|
|
650
|
+
async function detectDocker(root) {
|
|
651
|
+
return await new DockerComposeController(root).inspect();
|
|
395
652
|
}
|
|
396
653
|
|
|
397
654
|
// src/core/scanner/env.ts
|
|
@@ -525,7 +782,7 @@ function detectFramework(packageJson) {
|
|
|
525
782
|
// src/core/scanner/git.ts
|
|
526
783
|
import { promises as fs5 } from "fs";
|
|
527
784
|
import path5 from "path";
|
|
528
|
-
function
|
|
785
|
+
function isWithinRoot4(root, target) {
|
|
529
786
|
const resolvedRoot = path5.resolve(root);
|
|
530
787
|
const resolvedTarget = path5.resolve(target);
|
|
531
788
|
const relative = path5.relative(resolvedRoot, resolvedTarget);
|
|
@@ -544,14 +801,14 @@ async function resolveGitDirectory(root) {
|
|
|
544
801
|
if (match) {
|
|
545
802
|
const gitDir = match[1].trim();
|
|
546
803
|
const resolvedGitDir = path5.isAbsolute(gitDir) ? path5.resolve(gitDir) : path5.resolve(root, gitDir);
|
|
547
|
-
if (!
|
|
804
|
+
if (!isWithinRoot4(root, resolvedGitDir)) {
|
|
548
805
|
return null;
|
|
549
806
|
}
|
|
550
807
|
const [realRoot, realGitDir] = await Promise.all([
|
|
551
808
|
fs5.realpath(root),
|
|
552
809
|
fs5.realpath(resolvedGitDir)
|
|
553
810
|
]);
|
|
554
|
-
return
|
|
811
|
+
return isWithinRoot4(realRoot, realGitDir) ? resolvedGitDir : null;
|
|
555
812
|
}
|
|
556
813
|
}
|
|
557
814
|
} catch {
|
|
@@ -609,12 +866,23 @@ async function detectPackageManager(root) {
|
|
|
609
866
|
// src/core/scanner/packageJson.ts
|
|
610
867
|
import { promises as fs7 } from "fs";
|
|
611
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);
|
|
872
|
+
}
|
|
612
873
|
async function readPackageJson(root) {
|
|
613
874
|
const packageJsonPath = path7.join(root, "package.json");
|
|
614
875
|
try {
|
|
615
|
-
const
|
|
876
|
+
const [realRoot, realPackageJsonPath] = await Promise.all([
|
|
877
|
+
fs7.realpath(root),
|
|
878
|
+
fs7.realpath(packageJsonPath)
|
|
879
|
+
]);
|
|
880
|
+
if (!isWithinRoot5(realRoot, realPackageJsonPath)) {
|
|
881
|
+
return null;
|
|
882
|
+
}
|
|
883
|
+
const content = await fs7.readFile(realPackageJsonPath, "utf8");
|
|
616
884
|
const data = JSON.parse(content);
|
|
617
|
-
return { path:
|
|
885
|
+
return { path: realPackageJsonPath, data };
|
|
618
886
|
} catch {
|
|
619
887
|
return null;
|
|
620
888
|
}
|
|
@@ -622,6 +890,8 @@ async function readPackageJson(root) {
|
|
|
622
890
|
|
|
623
891
|
// src/core/scanner/ports.ts
|
|
624
892
|
import net from "net";
|
|
893
|
+
var MAX_PORT_PROBES = 64;
|
|
894
|
+
var PORT_PROBE_CONCURRENCY = 16;
|
|
625
895
|
function uniquePorts(ports) {
|
|
626
896
|
return Array.from(
|
|
627
897
|
new Set(ports.filter((port) => Number.isInteger(port) && port > 0 && port < 65536))
|
|
@@ -675,11 +945,25 @@ async function probePort(port) {
|
|
|
675
945
|
});
|
|
676
946
|
}
|
|
677
947
|
async function detectPorts(ports) {
|
|
678
|
-
const normalized = uniquePorts(ports);
|
|
948
|
+
const normalized = uniquePorts(ports).slice(0, MAX_PORT_PROBES);
|
|
679
949
|
if (normalized.length === 0) {
|
|
680
950
|
return null;
|
|
681
951
|
}
|
|
682
|
-
|
|
952
|
+
const results = [];
|
|
953
|
+
let nextIndex = 0;
|
|
954
|
+
async function worker() {
|
|
955
|
+
while (nextIndex < normalized.length) {
|
|
956
|
+
const port = normalized[nextIndex];
|
|
957
|
+
nextIndex += 1;
|
|
958
|
+
results.push(await probePort(port));
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
await Promise.all(
|
|
962
|
+
Array.from({ length: Math.min(PORT_PROBE_CONCURRENCY, normalized.length) }, () => worker())
|
|
963
|
+
);
|
|
964
|
+
return results.sort(
|
|
965
|
+
(left, right) => normalized.indexOf(left.port) - normalized.indexOf(right.port)
|
|
966
|
+
);
|
|
683
967
|
}
|
|
684
968
|
|
|
685
969
|
// src/core/scanner/scripts.ts
|
|
@@ -696,13 +980,18 @@ function extractScripts(packageJson) {
|
|
|
696
980
|
}
|
|
697
981
|
|
|
698
982
|
// 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);
|
|
986
|
+
}
|
|
699
987
|
async function findFirstFile(root, candidates) {
|
|
988
|
+
const resolvedRoot = await fs8.realpath(root).catch(() => path8.resolve(root));
|
|
700
989
|
for (const candidate of candidates) {
|
|
701
990
|
const filePath = path8.join(root, candidate);
|
|
702
991
|
try {
|
|
703
|
-
const stat = await fs8.stat(filePath);
|
|
704
|
-
if (stat.isFile()) {
|
|
705
|
-
return { path:
|
|
992
|
+
const [stat, realPath] = await Promise.all([fs8.stat(filePath), fs8.realpath(filePath)]);
|
|
993
|
+
if (stat.isFile() && isWithinRoot6(resolvedRoot, realPath)) {
|
|
994
|
+
return { path: realPath, exists: true };
|
|
706
995
|
}
|
|
707
996
|
} catch {
|
|
708
997
|
}
|
|
@@ -713,8 +1002,9 @@ function configuredPorts(configPorts) {
|
|
|
713
1002
|
return Array.isArray(configPorts) ? configPorts : [];
|
|
714
1003
|
}
|
|
715
1004
|
async function scanProject(root = process.cwd()) {
|
|
716
|
-
const
|
|
717
|
-
const
|
|
1005
|
+
const resolvedRoot = await fs8.realpath(root).catch(() => path8.resolve(root));
|
|
1006
|
+
const config = await loadConfig(resolvedRoot);
|
|
1007
|
+
const packageJson = await readPackageJson(resolvedRoot);
|
|
718
1008
|
const scripts = extractScripts(packageJson) ?? {};
|
|
719
1009
|
const framework = detectFramework(packageJson);
|
|
720
1010
|
const portsToProbe = [
|
|
@@ -723,17 +1013,17 @@ async function scanProject(root = process.cwd()) {
|
|
|
723
1013
|
...defaultPortsForFramework(framework)
|
|
724
1014
|
];
|
|
725
1015
|
const [packageManager, env, docker, git, ports, readme, license] = await Promise.all([
|
|
726
|
-
detectPackageManager(
|
|
727
|
-
detectEnv(
|
|
728
|
-
detectDocker(
|
|
729
|
-
detectGit(
|
|
1016
|
+
detectPackageManager(resolvedRoot),
|
|
1017
|
+
detectEnv(resolvedRoot, config?.config),
|
|
1018
|
+
detectDocker(resolvedRoot),
|
|
1019
|
+
detectGit(resolvedRoot),
|
|
730
1020
|
detectPorts(portsToProbe),
|
|
731
|
-
findFirstFile(
|
|
732
|
-
findFirstFile(
|
|
1021
|
+
findFirstFile(resolvedRoot, ["README.md", "README"]),
|
|
1022
|
+
findFirstFile(resolvedRoot, ["LICENSE", "LICENSE.md", "COPYING"])
|
|
733
1023
|
]);
|
|
734
1024
|
return {
|
|
735
|
-
root,
|
|
736
|
-
projectName: config?.config.name ?? packageJson?.data.name ?? path8.basename(
|
|
1025
|
+
root: resolvedRoot,
|
|
1026
|
+
projectName: config?.config.name ?? packageJson?.data.name ?? path8.basename(resolvedRoot),
|
|
737
1027
|
packageJson,
|
|
738
1028
|
packageManager: packageManager ?? (packageJson ? "npm" : null),
|
|
739
1029
|
scripts,
|
|
@@ -767,25 +1057,6 @@ async function readIfPresent2(filePath) {
|
|
|
767
1057
|
return null;
|
|
768
1058
|
}
|
|
769
1059
|
}
|
|
770
|
-
function extractReadmeScriptReferences(readmeContent) {
|
|
771
|
-
const references = /* @__PURE__ */ new Set();
|
|
772
|
-
const commandRegexes = [
|
|
773
|
-
/\bnpm\s+run\s+([A-Za-z0-9:_-]+)/g,
|
|
774
|
-
/\bpnpm\s+run\s+([A-Za-z0-9:_-]+)/g,
|
|
775
|
-
/\bbun\s+run\s+([A-Za-z0-9:_-]+)/g,
|
|
776
|
-
/\byarn\s+run\s+([A-Za-z0-9:_-]+)/g,
|
|
777
|
-
/\bnpm\s+(test|start|build)\b/g,
|
|
778
|
-
/\bpnpm\s+(test|start|build)\b/g,
|
|
779
|
-
/\byarn\s+(test|start|build)\b/g,
|
|
780
|
-
/\bbun\s+(test|start|build)\b/g
|
|
781
|
-
];
|
|
782
|
-
for (const regex of commandRegexes) {
|
|
783
|
-
for (const match of readmeContent.matchAll(regex)) {
|
|
784
|
-
references.add(match[1]);
|
|
785
|
-
}
|
|
786
|
-
}
|
|
787
|
-
return Array.from(references);
|
|
788
|
-
}
|
|
789
1060
|
function warning(id, severity, title, message, target) {
|
|
790
1061
|
return { id, severity, title, message, target };
|
|
791
1062
|
}
|
|
@@ -857,7 +1128,7 @@ async function runDoctor(root = process.cwd(), scan) {
|
|
|
857
1128
|
} else {
|
|
858
1129
|
const readme = await readIfPresent2(result.readme.path);
|
|
859
1130
|
if (readme !== null) {
|
|
860
|
-
const references =
|
|
1131
|
+
const references = extractScriptReferences(readme);
|
|
861
1132
|
const missingScripts = references.filter((script) => result.scripts[script] === void 0);
|
|
862
1133
|
if (missingScripts.length > 0) {
|
|
863
1134
|
warnings.push(
|
|
@@ -887,7 +1158,7 @@ async function runDoctor(root = process.cwd(), scan) {
|
|
|
887
1158
|
"docker-not-running",
|
|
888
1159
|
"warning",
|
|
889
1160
|
"Docker Compose found but Docker is not running",
|
|
890
|
-
"A compose file exists, but
|
|
1161
|
+
result.docker.message ?? "A compose file exists, but Docker is not available."
|
|
891
1162
|
)
|
|
892
1163
|
);
|
|
893
1164
|
}
|
|
@@ -911,12 +1182,34 @@ async function runDoctor(root = process.cwd(), scan) {
|
|
|
911
1182
|
)
|
|
912
1183
|
);
|
|
913
1184
|
}
|
|
914
|
-
if (!result.license.exists) {
|
|
915
|
-
warnings.push(warning("missing-license", "info", "No LICENSE", "No LICENSE file was found."));
|
|
916
|
-
}
|
|
917
1185
|
return warnings;
|
|
918
1186
|
}
|
|
919
1187
|
|
|
1188
|
+
// src/core/security/text.ts
|
|
1189
|
+
var ESC2 = String.fromCharCode(27);
|
|
1190
|
+
var BEL = String.fromCharCode(7);
|
|
1191
|
+
var OSC_SEQUENCE = new RegExp(`${ESC2}\\][\\s\\S]*?(?:${BEL}|${ESC2}\\\\)`, "g");
|
|
1192
|
+
var CSI_SEQUENCE = new RegExp(`${ESC2}\\[[0-?]*[ -/]*[@-~]`, "g");
|
|
1193
|
+
var ESCAPE_SEQUENCE = new RegExp(`${ESC2}[@-Z\\\\-_]`, "g");
|
|
1194
|
+
function stripControlCharacters(value) {
|
|
1195
|
+
let result = "";
|
|
1196
|
+
for (const character of value) {
|
|
1197
|
+
const code = character.charCodeAt(0);
|
|
1198
|
+
if (code > 31 && code < 127 || code > 159) {
|
|
1199
|
+
result += character;
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
return result;
|
|
1203
|
+
}
|
|
1204
|
+
function safeDisplayText(value) {
|
|
1205
|
+
return stripControlCharacters(
|
|
1206
|
+
String(value).replace(OSC_SEQUENCE, "").replace(CSI_SEQUENCE, "").replace(ESCAPE_SEQUENCE, "")
|
|
1207
|
+
);
|
|
1208
|
+
}
|
|
1209
|
+
function safeDisplayList(values) {
|
|
1210
|
+
return values.length > 0 ? values.map((value) => safeDisplayText(value)).join(", ") : "none";
|
|
1211
|
+
}
|
|
1212
|
+
|
|
920
1213
|
// src/cli/commands/doctor.ts
|
|
921
1214
|
function colorSeverity(severity) {
|
|
922
1215
|
if (severity === "error") {
|
|
@@ -934,8 +1227,8 @@ async function doctorCommand(cwd = process.cwd()) {
|
|
|
934
1227
|
return;
|
|
935
1228
|
}
|
|
936
1229
|
for (const item of warnings) {
|
|
937
|
-
console.log(`${colorSeverity(item.severity)} ${pc.bold(item.title)}`);
|
|
938
|
-
console.log(` ${item.message}`);
|
|
1230
|
+
console.log(`${colorSeverity(item.severity)} ${pc.bold(safeDisplayText(item.title))}`);
|
|
1231
|
+
console.log(` ${safeDisplayText(item.message)}`);
|
|
939
1232
|
}
|
|
940
1233
|
}
|
|
941
1234
|
|
|
@@ -961,6 +1254,14 @@ import pc3 from "picocolors";
|
|
|
961
1254
|
|
|
962
1255
|
// src/core/process/runner.ts
|
|
963
1256
|
import spawn2 from "cross-spawn";
|
|
1257
|
+
|
|
1258
|
+
// src/core/security/dangerousCommand.ts
|
|
1259
|
+
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;
|
|
1260
|
+
function isDangerousCommand(command) {
|
|
1261
|
+
return DANGEROUS_COMMAND.test(command);
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
// src/core/process/runner.ts
|
|
964
1265
|
function getPackageRunCommand(packageManager, script) {
|
|
965
1266
|
const manager = packageManager ?? "npm";
|
|
966
1267
|
const args = ["run", script];
|
|
@@ -1021,6 +1322,95 @@ async function resolvePackageInstallCommand(options) {
|
|
|
1021
1322
|
command: executable
|
|
1022
1323
|
};
|
|
1023
1324
|
}
|
|
1325
|
+
function splitCommandLine(command) {
|
|
1326
|
+
const tokens = [];
|
|
1327
|
+
let current = "";
|
|
1328
|
+
let quote = null;
|
|
1329
|
+
for (let index = 0; index < command.length; index += 1) {
|
|
1330
|
+
const character = command[index] ?? "";
|
|
1331
|
+
if (quote !== null) {
|
|
1332
|
+
if (character === quote) {
|
|
1333
|
+
quote = null;
|
|
1334
|
+
} else if (character === "\\" && quote === '"') {
|
|
1335
|
+
const next = command[index + 1];
|
|
1336
|
+
if (next === '"' || next === "\\") {
|
|
1337
|
+
index += 1;
|
|
1338
|
+
current += next ?? "";
|
|
1339
|
+
} else {
|
|
1340
|
+
current += character;
|
|
1341
|
+
}
|
|
1342
|
+
} else {
|
|
1343
|
+
current += character;
|
|
1344
|
+
}
|
|
1345
|
+
continue;
|
|
1346
|
+
}
|
|
1347
|
+
if (character === '"' || character === "'") {
|
|
1348
|
+
quote = character;
|
|
1349
|
+
continue;
|
|
1350
|
+
}
|
|
1351
|
+
if (/\s/.test(character)) {
|
|
1352
|
+
if (current.length > 0) {
|
|
1353
|
+
tokens.push(current);
|
|
1354
|
+
current = "";
|
|
1355
|
+
}
|
|
1356
|
+
continue;
|
|
1357
|
+
}
|
|
1358
|
+
current += character;
|
|
1359
|
+
}
|
|
1360
|
+
if (current.length > 0) {
|
|
1361
|
+
tokens.push(current);
|
|
1362
|
+
}
|
|
1363
|
+
return tokens;
|
|
1364
|
+
}
|
|
1365
|
+
function containsShellMetacharacters(command) {
|
|
1366
|
+
let quote = null;
|
|
1367
|
+
for (let index = 0; index < command.length; index += 1) {
|
|
1368
|
+
const character = command[index] ?? "";
|
|
1369
|
+
if (quote !== null) {
|
|
1370
|
+
if (character === quote) {
|
|
1371
|
+
quote = null;
|
|
1372
|
+
}
|
|
1373
|
+
continue;
|
|
1374
|
+
}
|
|
1375
|
+
if (character === '"' || character === "'") {
|
|
1376
|
+
quote = character;
|
|
1377
|
+
continue;
|
|
1378
|
+
}
|
|
1379
|
+
if (character === "\n" || character === "\r") {
|
|
1380
|
+
return true;
|
|
1381
|
+
}
|
|
1382
|
+
if (";|&<>".includes(character)) {
|
|
1383
|
+
return true;
|
|
1384
|
+
}
|
|
1385
|
+
if (character === "`") {
|
|
1386
|
+
return true;
|
|
1387
|
+
}
|
|
1388
|
+
if (character === "$" && (command[index + 1] === "(" || command[index + 1] === "{")) {
|
|
1389
|
+
return true;
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
return false;
|
|
1393
|
+
}
|
|
1394
|
+
async function resolveConfiguredCommand(cwd, command) {
|
|
1395
|
+
const trimmed = command.trim();
|
|
1396
|
+
if (trimmed.length === 0 || containsShellMetacharacters(trimmed)) {
|
|
1397
|
+
return null;
|
|
1398
|
+
}
|
|
1399
|
+
const tokens = splitCommandLine(trimmed);
|
|
1400
|
+
if (tokens.length === 0) {
|
|
1401
|
+
return null;
|
|
1402
|
+
}
|
|
1403
|
+
const [executableName, ...args] = tokens;
|
|
1404
|
+
const executable = await resolveExecutableOutsideRoot(cwd, executableName);
|
|
1405
|
+
if (executable === null) {
|
|
1406
|
+
return null;
|
|
1407
|
+
}
|
|
1408
|
+
return {
|
|
1409
|
+
command: executable,
|
|
1410
|
+
args,
|
|
1411
|
+
displayCommand: trimmed
|
|
1412
|
+
};
|
|
1413
|
+
}
|
|
1024
1414
|
async function runPackageScriptToTerminal(options) {
|
|
1025
1415
|
const runCommand2 = await resolvePackageRunCommand(options);
|
|
1026
1416
|
if (runCommand2 === null) {
|
|
@@ -1065,14 +1455,14 @@ async function runCommand(script, cwd = process.cwd()) {
|
|
|
1065
1455
|
// src/cli/commands/scan.ts
|
|
1066
1456
|
import pc4 from "picocolors";
|
|
1067
1457
|
function formatList(values) {
|
|
1068
|
-
return values
|
|
1458
|
+
return safeDisplayList(values);
|
|
1069
1459
|
}
|
|
1070
1460
|
function printScanResult(scan) {
|
|
1071
|
-
console.log(pc4.bold(`Project: ${scan.projectName}`));
|
|
1072
|
-
console.log(`Type: ${scan.framework?.type ?? "Unknown"}`);
|
|
1073
|
-
console.log(`Manager: ${scan.packageManager ?? "unknown"}`);
|
|
1461
|
+
console.log(pc4.bold(`Project: ${safeDisplayText(scan.projectName)}`));
|
|
1462
|
+
console.log(`Type: ${safeDisplayText(scan.framework?.type ?? "Unknown")}`);
|
|
1463
|
+
console.log(`Manager: ${safeDisplayText(scan.packageManager ?? "unknown")}`);
|
|
1074
1464
|
console.log(`Scripts: ${formatList(Object.keys(scan.scripts))}`);
|
|
1075
|
-
console.log(`Git: ${scan.git?.branch ?? "not detected"}`);
|
|
1465
|
+
console.log(`Git: ${safeDisplayText(scan.git?.branch ?? "not detected")}`);
|
|
1076
1466
|
console.log(`README: ${scan.readme.exists ? "found" : "missing"}`);
|
|
1077
1467
|
console.log(`LICENSE: ${scan.license.exists ? "found" : "missing"}`);
|
|
1078
1468
|
if (scan.env !== null) {
|
|
@@ -1096,17 +1486,549 @@ async function scanCommand(cwd = process.cwd()) {
|
|
|
1096
1486
|
import pc5 from "picocolors";
|
|
1097
1487
|
|
|
1098
1488
|
// src/server/index.ts
|
|
1099
|
-
import { promises as
|
|
1100
|
-
import
|
|
1101
|
-
import { fileURLToPath } from "url";
|
|
1102
|
-
import {
|
|
1489
|
+
import { promises as fs17 } from "fs";
|
|
1490
|
+
import path13 from "path";
|
|
1491
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1492
|
+
import { createAdaptorServer } from "@hono/node-server";
|
|
1103
1493
|
import { serveStatic } from "@hono/node-server/serve-static";
|
|
1104
1494
|
import { Hono } from "hono";
|
|
1105
|
-
|
|
1495
|
+
|
|
1496
|
+
// node_modules/open/index.js
|
|
1497
|
+
import process7 from "process";
|
|
1498
|
+
import { Buffer as Buffer2 } from "buffer";
|
|
1499
|
+
import path11 from "path";
|
|
1500
|
+
import { fileURLToPath } from "url";
|
|
1501
|
+
import { promisify as promisify5 } from "util";
|
|
1502
|
+
import childProcess from "child_process";
|
|
1503
|
+
import fs15, { constants as fsConstants2 } from "fs/promises";
|
|
1504
|
+
|
|
1505
|
+
// node_modules/wsl-utils/index.js
|
|
1506
|
+
import process3 from "process";
|
|
1507
|
+
import fs14, { constants as fsConstants } from "fs/promises";
|
|
1508
|
+
|
|
1509
|
+
// node_modules/is-wsl/index.js
|
|
1510
|
+
import process2 from "process";
|
|
1511
|
+
import os2 from "os";
|
|
1512
|
+
import fs13 from "fs";
|
|
1513
|
+
|
|
1514
|
+
// node_modules/is-inside-container/index.js
|
|
1515
|
+
import fs12 from "fs";
|
|
1516
|
+
|
|
1517
|
+
// node_modules/is-docker/index.js
|
|
1518
|
+
import fs11 from "fs";
|
|
1519
|
+
var isDockerCached;
|
|
1520
|
+
function hasDockerEnv() {
|
|
1521
|
+
try {
|
|
1522
|
+
fs11.statSync("/.dockerenv");
|
|
1523
|
+
return true;
|
|
1524
|
+
} catch {
|
|
1525
|
+
return false;
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
function hasDockerCGroup() {
|
|
1529
|
+
try {
|
|
1530
|
+
return fs11.readFileSync("/proc/self/cgroup", "utf8").includes("docker");
|
|
1531
|
+
} catch {
|
|
1532
|
+
return false;
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
function isDocker() {
|
|
1536
|
+
if (isDockerCached === void 0) {
|
|
1537
|
+
isDockerCached = hasDockerEnv() || hasDockerCGroup();
|
|
1538
|
+
}
|
|
1539
|
+
return isDockerCached;
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
// node_modules/is-inside-container/index.js
|
|
1543
|
+
var cachedResult;
|
|
1544
|
+
var hasContainerEnv = () => {
|
|
1545
|
+
try {
|
|
1546
|
+
fs12.statSync("/run/.containerenv");
|
|
1547
|
+
return true;
|
|
1548
|
+
} catch {
|
|
1549
|
+
return false;
|
|
1550
|
+
}
|
|
1551
|
+
};
|
|
1552
|
+
function isInsideContainer() {
|
|
1553
|
+
if (cachedResult === void 0) {
|
|
1554
|
+
cachedResult = hasContainerEnv() || isDocker();
|
|
1555
|
+
}
|
|
1556
|
+
return cachedResult;
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
// node_modules/is-wsl/index.js
|
|
1560
|
+
var isWsl = () => {
|
|
1561
|
+
if (process2.platform !== "linux") {
|
|
1562
|
+
return false;
|
|
1563
|
+
}
|
|
1564
|
+
if (os2.release().toLowerCase().includes("microsoft")) {
|
|
1565
|
+
if (isInsideContainer()) {
|
|
1566
|
+
return false;
|
|
1567
|
+
}
|
|
1568
|
+
return true;
|
|
1569
|
+
}
|
|
1570
|
+
try {
|
|
1571
|
+
if (fs13.readFileSync("/proc/version", "utf8").toLowerCase().includes("microsoft")) {
|
|
1572
|
+
return !isInsideContainer();
|
|
1573
|
+
}
|
|
1574
|
+
} catch {
|
|
1575
|
+
}
|
|
1576
|
+
if (fs13.existsSync("/proc/sys/fs/binfmt_misc/WSLInterop") || fs13.existsSync("/run/WSL")) {
|
|
1577
|
+
return !isInsideContainer();
|
|
1578
|
+
}
|
|
1579
|
+
return false;
|
|
1580
|
+
};
|
|
1581
|
+
var is_wsl_default = process2.env.__IS_WSL_TEST__ ? isWsl : isWsl();
|
|
1582
|
+
|
|
1583
|
+
// node_modules/wsl-utils/index.js
|
|
1584
|
+
var wslDrivesMountPoint = /* @__PURE__ */ (() => {
|
|
1585
|
+
const defaultMountPoint = "/mnt/";
|
|
1586
|
+
let mountPoint;
|
|
1587
|
+
return async function() {
|
|
1588
|
+
if (mountPoint) {
|
|
1589
|
+
return mountPoint;
|
|
1590
|
+
}
|
|
1591
|
+
const configFilePath = "/etc/wsl.conf";
|
|
1592
|
+
let isConfigFileExists = false;
|
|
1593
|
+
try {
|
|
1594
|
+
await fs14.access(configFilePath, fsConstants.F_OK);
|
|
1595
|
+
isConfigFileExists = true;
|
|
1596
|
+
} catch {
|
|
1597
|
+
}
|
|
1598
|
+
if (!isConfigFileExists) {
|
|
1599
|
+
return defaultMountPoint;
|
|
1600
|
+
}
|
|
1601
|
+
const configContent = await fs14.readFile(configFilePath, { encoding: "utf8" });
|
|
1602
|
+
const configMountPoint = /(?<!#.*)root\s*=\s*(?<mountPoint>.*)/g.exec(configContent);
|
|
1603
|
+
if (!configMountPoint) {
|
|
1604
|
+
return defaultMountPoint;
|
|
1605
|
+
}
|
|
1606
|
+
mountPoint = configMountPoint.groups.mountPoint.trim();
|
|
1607
|
+
mountPoint = mountPoint.endsWith("/") ? mountPoint : `${mountPoint}/`;
|
|
1608
|
+
return mountPoint;
|
|
1609
|
+
};
|
|
1610
|
+
})();
|
|
1611
|
+
var powerShellPathFromWsl = async () => {
|
|
1612
|
+
const mountPoint = await wslDrivesMountPoint();
|
|
1613
|
+
return `${mountPoint}c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe`;
|
|
1614
|
+
};
|
|
1615
|
+
var powerShellPath = async () => {
|
|
1616
|
+
if (is_wsl_default) {
|
|
1617
|
+
return powerShellPathFromWsl();
|
|
1618
|
+
}
|
|
1619
|
+
return `${process3.env.SYSTEMROOT || process3.env.windir || String.raw`C:\Windows`}\\System32\\WindowsPowerShell\\v1.0\\powershell.exe`;
|
|
1620
|
+
};
|
|
1621
|
+
|
|
1622
|
+
// node_modules/define-lazy-prop/index.js
|
|
1623
|
+
function defineLazyProperty(object, propertyName, valueGetter) {
|
|
1624
|
+
const define = (value) => Object.defineProperty(object, propertyName, { value, enumerable: true, writable: true });
|
|
1625
|
+
Object.defineProperty(object, propertyName, {
|
|
1626
|
+
configurable: true,
|
|
1627
|
+
enumerable: true,
|
|
1628
|
+
get() {
|
|
1629
|
+
const result = valueGetter();
|
|
1630
|
+
define(result);
|
|
1631
|
+
return result;
|
|
1632
|
+
},
|
|
1633
|
+
set(value) {
|
|
1634
|
+
define(value);
|
|
1635
|
+
}
|
|
1636
|
+
});
|
|
1637
|
+
return object;
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
// node_modules/default-browser/index.js
|
|
1641
|
+
import { promisify as promisify4 } from "util";
|
|
1642
|
+
import process6 from "process";
|
|
1643
|
+
import { execFile as execFile4 } from "child_process";
|
|
1644
|
+
|
|
1645
|
+
// node_modules/default-browser-id/index.js
|
|
1646
|
+
import { promisify } from "util";
|
|
1647
|
+
import process4 from "process";
|
|
1648
|
+
import { execFile } from "child_process";
|
|
1649
|
+
var execFileAsync = promisify(execFile);
|
|
1650
|
+
async function defaultBrowserId() {
|
|
1651
|
+
if (process4.platform !== "darwin") {
|
|
1652
|
+
throw new Error("macOS only");
|
|
1653
|
+
}
|
|
1654
|
+
const { stdout } = await execFileAsync("defaults", ["read", "com.apple.LaunchServices/com.apple.launchservices.secure", "LSHandlers"]);
|
|
1655
|
+
const match = /LSHandlerRoleAll = "(?!-)(?<id>[^"]+?)";\s+?LSHandlerURLScheme = (?:http|https);/.exec(stdout);
|
|
1656
|
+
const browserId = match?.groups.id ?? "com.apple.Safari";
|
|
1657
|
+
if (browserId === "com.apple.safari") {
|
|
1658
|
+
return "com.apple.Safari";
|
|
1659
|
+
}
|
|
1660
|
+
return browserId;
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
// node_modules/run-applescript/index.js
|
|
1664
|
+
import process5 from "process";
|
|
1665
|
+
import { promisify as promisify2 } from "util";
|
|
1666
|
+
import { execFile as execFile2, execFileSync } from "child_process";
|
|
1667
|
+
var execFileAsync2 = promisify2(execFile2);
|
|
1668
|
+
async function runAppleScript(script, { humanReadableOutput = true, signal } = {}) {
|
|
1669
|
+
if (process5.platform !== "darwin") {
|
|
1670
|
+
throw new Error("macOS only");
|
|
1671
|
+
}
|
|
1672
|
+
const outputArguments = humanReadableOutput ? [] : ["-ss"];
|
|
1673
|
+
const execOptions = {};
|
|
1674
|
+
if (signal) {
|
|
1675
|
+
execOptions.signal = signal;
|
|
1676
|
+
}
|
|
1677
|
+
const { stdout } = await execFileAsync2("osascript", ["-e", script, outputArguments], execOptions);
|
|
1678
|
+
return stdout.trim();
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
// node_modules/bundle-name/index.js
|
|
1682
|
+
async function bundleName(bundleId) {
|
|
1683
|
+
return runAppleScript(`tell application "Finder" to set app_path to application file id "${bundleId}" as string
|
|
1684
|
+
tell application "System Events" to get value of property list item "CFBundleName" of property list file (app_path & ":Contents:Info.plist")`);
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
// node_modules/default-browser/windows.js
|
|
1688
|
+
import { promisify as promisify3 } from "util";
|
|
1689
|
+
import { execFile as execFile3 } from "child_process";
|
|
1690
|
+
var execFileAsync3 = promisify3(execFile3);
|
|
1691
|
+
var windowsBrowserProgIds = {
|
|
1692
|
+
MSEdgeHTM: { name: "Edge", id: "com.microsoft.edge" },
|
|
1693
|
+
// The missing `L` is correct.
|
|
1694
|
+
MSEdgeBHTML: { name: "Edge Beta", id: "com.microsoft.edge.beta" },
|
|
1695
|
+
MSEdgeDHTML: { name: "Edge Dev", id: "com.microsoft.edge.dev" },
|
|
1696
|
+
AppXq0fevzme2pys62n3e0fbqa7peapykr8v: { name: "Edge", id: "com.microsoft.edge.old" },
|
|
1697
|
+
ChromeHTML: { name: "Chrome", id: "com.google.chrome" },
|
|
1698
|
+
ChromeBHTML: { name: "Chrome Beta", id: "com.google.chrome.beta" },
|
|
1699
|
+
ChromeDHTML: { name: "Chrome Dev", id: "com.google.chrome.dev" },
|
|
1700
|
+
ChromiumHTM: { name: "Chromium", id: "org.chromium.Chromium" },
|
|
1701
|
+
BraveHTML: { name: "Brave", id: "com.brave.Browser" },
|
|
1702
|
+
BraveBHTML: { name: "Brave Beta", id: "com.brave.Browser.beta" },
|
|
1703
|
+
BraveDHTML: { name: "Brave Dev", id: "com.brave.Browser.dev" },
|
|
1704
|
+
BraveSSHTM: { name: "Brave Nightly", id: "com.brave.Browser.nightly" },
|
|
1705
|
+
FirefoxURL: { name: "Firefox", id: "org.mozilla.firefox" },
|
|
1706
|
+
OperaStable: { name: "Opera", id: "com.operasoftware.Opera" },
|
|
1707
|
+
VivaldiHTM: { name: "Vivaldi", id: "com.vivaldi.Vivaldi" },
|
|
1708
|
+
"IE.HTTP": { name: "Internet Explorer", id: "com.microsoft.ie" }
|
|
1709
|
+
};
|
|
1710
|
+
var _windowsBrowserProgIdMap = new Map(Object.entries(windowsBrowserProgIds));
|
|
1711
|
+
var UnknownBrowserError = class extends Error {
|
|
1712
|
+
};
|
|
1713
|
+
async function defaultBrowser(_execFileAsync = execFileAsync3) {
|
|
1714
|
+
const { stdout } = await _execFileAsync("reg", [
|
|
1715
|
+
"QUERY",
|
|
1716
|
+
" HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\Shell\\Associations\\UrlAssociations\\http\\UserChoice",
|
|
1717
|
+
"/v",
|
|
1718
|
+
"ProgId"
|
|
1719
|
+
]);
|
|
1720
|
+
const match = /ProgId\s*REG_SZ\s*(?<id>\S+)/.exec(stdout);
|
|
1721
|
+
if (!match) {
|
|
1722
|
+
throw new UnknownBrowserError(`Cannot find Windows browser in stdout: ${JSON.stringify(stdout)}`);
|
|
1723
|
+
}
|
|
1724
|
+
const { id } = match.groups;
|
|
1725
|
+
const dotIndex = id.lastIndexOf(".");
|
|
1726
|
+
const hyphenIndex = id.lastIndexOf("-");
|
|
1727
|
+
const baseIdByDot = dotIndex === -1 ? void 0 : id.slice(0, dotIndex);
|
|
1728
|
+
const baseIdByHyphen = hyphenIndex === -1 ? void 0 : id.slice(0, hyphenIndex);
|
|
1729
|
+
return windowsBrowserProgIds[id] ?? windowsBrowserProgIds[baseIdByDot] ?? windowsBrowserProgIds[baseIdByHyphen] ?? { name: id, id };
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
// node_modules/default-browser/index.js
|
|
1733
|
+
var execFileAsync4 = promisify4(execFile4);
|
|
1734
|
+
var titleize = (string) => string.toLowerCase().replaceAll(/(?:^|\s|-)\S/g, (x) => x.toUpperCase());
|
|
1735
|
+
async function defaultBrowser2() {
|
|
1736
|
+
if (process6.platform === "darwin") {
|
|
1737
|
+
const id = await defaultBrowserId();
|
|
1738
|
+
const name = await bundleName(id);
|
|
1739
|
+
return { name, id };
|
|
1740
|
+
}
|
|
1741
|
+
if (process6.platform === "linux") {
|
|
1742
|
+
const { stdout } = await execFileAsync4("xdg-mime", ["query", "default", "x-scheme-handler/http"]);
|
|
1743
|
+
const id = stdout.trim();
|
|
1744
|
+
const name = titleize(id.replace(/.desktop$/, "").replace("-", " "));
|
|
1745
|
+
return { name, id };
|
|
1746
|
+
}
|
|
1747
|
+
if (process6.platform === "win32") {
|
|
1748
|
+
return defaultBrowser();
|
|
1749
|
+
}
|
|
1750
|
+
throw new Error("Only macOS, Linux, and Windows are supported");
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
// node_modules/open/index.js
|
|
1754
|
+
var execFile5 = promisify5(childProcess.execFile);
|
|
1755
|
+
var __dirname = path11.dirname(fileURLToPath(import.meta.url));
|
|
1756
|
+
var localXdgOpenPath = path11.join(__dirname, "xdg-open");
|
|
1757
|
+
var { platform, arch } = process7;
|
|
1758
|
+
async function getWindowsDefaultBrowserFromWsl() {
|
|
1759
|
+
const powershellPath = await powerShellPath();
|
|
1760
|
+
const rawCommand = String.raw`(Get-ItemProperty -Path "HKCU:\Software\Microsoft\Windows\Shell\Associations\UrlAssociations\http\UserChoice").ProgId`;
|
|
1761
|
+
const encodedCommand = Buffer2.from(rawCommand, "utf16le").toString("base64");
|
|
1762
|
+
const { stdout } = await execFile5(
|
|
1763
|
+
powershellPath,
|
|
1764
|
+
[
|
|
1765
|
+
"-NoProfile",
|
|
1766
|
+
"-NonInteractive",
|
|
1767
|
+
"-ExecutionPolicy",
|
|
1768
|
+
"Bypass",
|
|
1769
|
+
"-EncodedCommand",
|
|
1770
|
+
encodedCommand
|
|
1771
|
+
],
|
|
1772
|
+
{ encoding: "utf8" }
|
|
1773
|
+
);
|
|
1774
|
+
const progId = stdout.trim();
|
|
1775
|
+
const browserMap = {
|
|
1776
|
+
ChromeHTML: "com.google.chrome",
|
|
1777
|
+
BraveHTML: "com.brave.Browser",
|
|
1778
|
+
MSEdgeHTM: "com.microsoft.edge",
|
|
1779
|
+
FirefoxURL: "org.mozilla.firefox"
|
|
1780
|
+
};
|
|
1781
|
+
return browserMap[progId] ? { id: browserMap[progId] } : {};
|
|
1782
|
+
}
|
|
1783
|
+
var pTryEach = async (array, mapper) => {
|
|
1784
|
+
let latestError;
|
|
1785
|
+
for (const item of array) {
|
|
1786
|
+
try {
|
|
1787
|
+
return await mapper(item);
|
|
1788
|
+
} catch (error) {
|
|
1789
|
+
latestError = error;
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
throw latestError;
|
|
1793
|
+
};
|
|
1794
|
+
var baseOpen = async (options) => {
|
|
1795
|
+
options = {
|
|
1796
|
+
wait: false,
|
|
1797
|
+
background: false,
|
|
1798
|
+
newInstance: false,
|
|
1799
|
+
allowNonzeroExitCode: false,
|
|
1800
|
+
...options
|
|
1801
|
+
};
|
|
1802
|
+
if (Array.isArray(options.app)) {
|
|
1803
|
+
return pTryEach(options.app, (singleApp) => baseOpen({
|
|
1804
|
+
...options,
|
|
1805
|
+
app: singleApp
|
|
1806
|
+
}));
|
|
1807
|
+
}
|
|
1808
|
+
let { name: app, arguments: appArguments = [] } = options.app ?? {};
|
|
1809
|
+
appArguments = [...appArguments];
|
|
1810
|
+
if (Array.isArray(app)) {
|
|
1811
|
+
return pTryEach(app, (appName) => baseOpen({
|
|
1812
|
+
...options,
|
|
1813
|
+
app: {
|
|
1814
|
+
name: appName,
|
|
1815
|
+
arguments: appArguments
|
|
1816
|
+
}
|
|
1817
|
+
}));
|
|
1818
|
+
}
|
|
1819
|
+
if (app === "browser" || app === "browserPrivate") {
|
|
1820
|
+
const ids = {
|
|
1821
|
+
"com.google.chrome": "chrome",
|
|
1822
|
+
"google-chrome.desktop": "chrome",
|
|
1823
|
+
"com.brave.Browser": "brave",
|
|
1824
|
+
"org.mozilla.firefox": "firefox",
|
|
1825
|
+
"firefox.desktop": "firefox",
|
|
1826
|
+
"com.microsoft.msedge": "edge",
|
|
1827
|
+
"com.microsoft.edge": "edge",
|
|
1828
|
+
"com.microsoft.edgemac": "edge",
|
|
1829
|
+
"microsoft-edge.desktop": "edge"
|
|
1830
|
+
};
|
|
1831
|
+
const flags = {
|
|
1832
|
+
chrome: "--incognito",
|
|
1833
|
+
brave: "--incognito",
|
|
1834
|
+
firefox: "--private-window",
|
|
1835
|
+
edge: "--inPrivate"
|
|
1836
|
+
};
|
|
1837
|
+
const browser = is_wsl_default ? await getWindowsDefaultBrowserFromWsl() : await defaultBrowser2();
|
|
1838
|
+
if (browser.id in ids) {
|
|
1839
|
+
const browserName = ids[browser.id];
|
|
1840
|
+
if (app === "browserPrivate") {
|
|
1841
|
+
appArguments.push(flags[browserName]);
|
|
1842
|
+
}
|
|
1843
|
+
return baseOpen({
|
|
1844
|
+
...options,
|
|
1845
|
+
app: {
|
|
1846
|
+
name: apps[browserName],
|
|
1847
|
+
arguments: appArguments
|
|
1848
|
+
}
|
|
1849
|
+
});
|
|
1850
|
+
}
|
|
1851
|
+
throw new Error(`${browser.name} is not supported as a default browser`);
|
|
1852
|
+
}
|
|
1853
|
+
let command;
|
|
1854
|
+
const cliArguments = [];
|
|
1855
|
+
const childProcessOptions = {};
|
|
1856
|
+
if (platform === "darwin") {
|
|
1857
|
+
command = "open";
|
|
1858
|
+
if (options.wait) {
|
|
1859
|
+
cliArguments.push("--wait-apps");
|
|
1860
|
+
}
|
|
1861
|
+
if (options.background) {
|
|
1862
|
+
cliArguments.push("--background");
|
|
1863
|
+
}
|
|
1864
|
+
if (options.newInstance) {
|
|
1865
|
+
cliArguments.push("--new");
|
|
1866
|
+
}
|
|
1867
|
+
if (app) {
|
|
1868
|
+
cliArguments.push("-a", app);
|
|
1869
|
+
}
|
|
1870
|
+
} else if (platform === "win32" || is_wsl_default && !isInsideContainer() && !app) {
|
|
1871
|
+
command = await powerShellPath();
|
|
1872
|
+
cliArguments.push(
|
|
1873
|
+
"-NoProfile",
|
|
1874
|
+
"-NonInteractive",
|
|
1875
|
+
"-ExecutionPolicy",
|
|
1876
|
+
"Bypass",
|
|
1877
|
+
"-EncodedCommand"
|
|
1878
|
+
);
|
|
1879
|
+
if (!is_wsl_default) {
|
|
1880
|
+
childProcessOptions.windowsVerbatimArguments = true;
|
|
1881
|
+
}
|
|
1882
|
+
const encodedArguments = ["Start"];
|
|
1883
|
+
if (options.wait) {
|
|
1884
|
+
encodedArguments.push("-Wait");
|
|
1885
|
+
}
|
|
1886
|
+
if (app) {
|
|
1887
|
+
encodedArguments.push(`"\`"${app}\`""`);
|
|
1888
|
+
if (options.target) {
|
|
1889
|
+
appArguments.push(options.target);
|
|
1890
|
+
}
|
|
1891
|
+
} else if (options.target) {
|
|
1892
|
+
encodedArguments.push(`"${options.target}"`);
|
|
1893
|
+
}
|
|
1894
|
+
if (appArguments.length > 0) {
|
|
1895
|
+
appArguments = appArguments.map((argument) => `"\`"${argument}\`""`);
|
|
1896
|
+
encodedArguments.push("-ArgumentList", appArguments.join(","));
|
|
1897
|
+
}
|
|
1898
|
+
options.target = Buffer2.from(encodedArguments.join(" "), "utf16le").toString("base64");
|
|
1899
|
+
} else {
|
|
1900
|
+
if (app) {
|
|
1901
|
+
command = app;
|
|
1902
|
+
} else {
|
|
1903
|
+
const isBundled = !__dirname || __dirname === "/";
|
|
1904
|
+
let exeLocalXdgOpen = false;
|
|
1905
|
+
try {
|
|
1906
|
+
await fs15.access(localXdgOpenPath, fsConstants2.X_OK);
|
|
1907
|
+
exeLocalXdgOpen = true;
|
|
1908
|
+
} catch {
|
|
1909
|
+
}
|
|
1910
|
+
const useSystemXdgOpen = process7.versions.electron ?? (platform === "android" || isBundled || !exeLocalXdgOpen);
|
|
1911
|
+
command = useSystemXdgOpen ? "xdg-open" : localXdgOpenPath;
|
|
1912
|
+
}
|
|
1913
|
+
if (appArguments.length > 0) {
|
|
1914
|
+
cliArguments.push(...appArguments);
|
|
1915
|
+
}
|
|
1916
|
+
if (!options.wait) {
|
|
1917
|
+
childProcessOptions.stdio = "ignore";
|
|
1918
|
+
childProcessOptions.detached = true;
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
if (platform === "darwin" && appArguments.length > 0) {
|
|
1922
|
+
cliArguments.push("--args", ...appArguments);
|
|
1923
|
+
}
|
|
1924
|
+
if (options.target) {
|
|
1925
|
+
cliArguments.push(options.target);
|
|
1926
|
+
}
|
|
1927
|
+
const subprocess = childProcess.spawn(command, cliArguments, childProcessOptions);
|
|
1928
|
+
if (options.wait) {
|
|
1929
|
+
return new Promise((resolve, reject) => {
|
|
1930
|
+
subprocess.once("error", reject);
|
|
1931
|
+
subprocess.once("close", (exitCode) => {
|
|
1932
|
+
if (!options.allowNonzeroExitCode && exitCode > 0) {
|
|
1933
|
+
reject(new Error(`Exited with code ${exitCode}`));
|
|
1934
|
+
return;
|
|
1935
|
+
}
|
|
1936
|
+
resolve(subprocess);
|
|
1937
|
+
});
|
|
1938
|
+
});
|
|
1939
|
+
}
|
|
1940
|
+
subprocess.unref();
|
|
1941
|
+
return subprocess;
|
|
1942
|
+
};
|
|
1943
|
+
var open = (target, options) => {
|
|
1944
|
+
if (typeof target !== "string") {
|
|
1945
|
+
throw new TypeError("Expected a `target`");
|
|
1946
|
+
}
|
|
1947
|
+
return baseOpen({
|
|
1948
|
+
...options,
|
|
1949
|
+
target
|
|
1950
|
+
});
|
|
1951
|
+
};
|
|
1952
|
+
function detectArchBinary(binary) {
|
|
1953
|
+
if (typeof binary === "string" || Array.isArray(binary)) {
|
|
1954
|
+
return binary;
|
|
1955
|
+
}
|
|
1956
|
+
const { [arch]: archBinary } = binary;
|
|
1957
|
+
if (!archBinary) {
|
|
1958
|
+
throw new Error(`${arch} is not supported`);
|
|
1959
|
+
}
|
|
1960
|
+
return archBinary;
|
|
1961
|
+
}
|
|
1962
|
+
function detectPlatformBinary({ [platform]: platformBinary }, { wsl }) {
|
|
1963
|
+
if (wsl && is_wsl_default) {
|
|
1964
|
+
return detectArchBinary(wsl);
|
|
1965
|
+
}
|
|
1966
|
+
if (!platformBinary) {
|
|
1967
|
+
throw new Error(`${platform} is not supported`);
|
|
1968
|
+
}
|
|
1969
|
+
return detectArchBinary(platformBinary);
|
|
1970
|
+
}
|
|
1971
|
+
var apps = {};
|
|
1972
|
+
defineLazyProperty(apps, "chrome", () => detectPlatformBinary({
|
|
1973
|
+
darwin: "google chrome",
|
|
1974
|
+
win32: "chrome",
|
|
1975
|
+
linux: ["google-chrome", "google-chrome-stable", "chromium"]
|
|
1976
|
+
}, {
|
|
1977
|
+
wsl: {
|
|
1978
|
+
ia32: "/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe",
|
|
1979
|
+
x64: ["/mnt/c/Program Files/Google/Chrome/Application/chrome.exe", "/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe"]
|
|
1980
|
+
}
|
|
1981
|
+
}));
|
|
1982
|
+
defineLazyProperty(apps, "brave", () => detectPlatformBinary({
|
|
1983
|
+
darwin: "brave browser",
|
|
1984
|
+
win32: "brave",
|
|
1985
|
+
linux: ["brave-browser", "brave"]
|
|
1986
|
+
}, {
|
|
1987
|
+
wsl: {
|
|
1988
|
+
ia32: "/mnt/c/Program Files (x86)/BraveSoftware/Brave-Browser/Application/brave.exe",
|
|
1989
|
+
x64: ["/mnt/c/Program Files/BraveSoftware/Brave-Browser/Application/brave.exe", "/mnt/c/Program Files (x86)/BraveSoftware/Brave-Browser/Application/brave.exe"]
|
|
1990
|
+
}
|
|
1991
|
+
}));
|
|
1992
|
+
defineLazyProperty(apps, "firefox", () => detectPlatformBinary({
|
|
1993
|
+
darwin: "firefox",
|
|
1994
|
+
win32: String.raw`C:\Program Files\Mozilla Firefox\firefox.exe`,
|
|
1995
|
+
linux: "firefox"
|
|
1996
|
+
}, {
|
|
1997
|
+
wsl: "/mnt/c/Program Files/Mozilla Firefox/firefox.exe"
|
|
1998
|
+
}));
|
|
1999
|
+
defineLazyProperty(apps, "edge", () => detectPlatformBinary({
|
|
2000
|
+
darwin: "microsoft edge",
|
|
2001
|
+
win32: "msedge",
|
|
2002
|
+
linux: ["microsoft-edge", "microsoft-edge-dev"]
|
|
2003
|
+
}, {
|
|
2004
|
+
wsl: "/mnt/c/Program Files (x86)/Microsoft/Edge/Application/msedge.exe"
|
|
2005
|
+
}));
|
|
2006
|
+
defineLazyProperty(apps, "browser", () => "browser");
|
|
2007
|
+
defineLazyProperty(apps, "browserPrivate", () => "browserPrivate");
|
|
2008
|
+
var open_default = open;
|
|
1106
2009
|
|
|
1107
2010
|
// src/core/process/manager.ts
|
|
1108
2011
|
import { EventEmitter } from "events";
|
|
1109
2012
|
import spawn3 from "cross-spawn";
|
|
2013
|
+
var LOG_MESSAGE_LIMIT = 16384;
|
|
2014
|
+
var LOG_ENTRY_LIMIT = 1e3;
|
|
2015
|
+
function killChildProcessTree(child) {
|
|
2016
|
+
if (child.pid === void 0) {
|
|
2017
|
+
child.kill();
|
|
2018
|
+
return;
|
|
2019
|
+
}
|
|
2020
|
+
if (process.platform === "win32") {
|
|
2021
|
+
const result = spawn3.sync("taskkill", ["/pid", String(child.pid), "/T", "/F"], {
|
|
2022
|
+
stdio: "ignore",
|
|
2023
|
+
windowsHide: true
|
|
2024
|
+
});
|
|
2025
|
+
if (result.error) {
|
|
2026
|
+
child.kill();
|
|
2027
|
+
}
|
|
2028
|
+
return;
|
|
2029
|
+
}
|
|
2030
|
+
child.kill();
|
|
2031
|
+
}
|
|
1110
2032
|
var ProcessManager = class extends EventEmitter {
|
|
1111
2033
|
processes = /* @__PURE__ */ new Map();
|
|
1112
2034
|
logs = [];
|
|
@@ -1114,7 +2036,7 @@ var ProcessManager = class extends EventEmitter {
|
|
|
1114
2036
|
start(options) {
|
|
1115
2037
|
const child = spawn3(options.command, options.args, {
|
|
1116
2038
|
cwd: options.cwd,
|
|
1117
|
-
shell:
|
|
2039
|
+
shell: false,
|
|
1118
2040
|
windowsHide: true
|
|
1119
2041
|
});
|
|
1120
2042
|
const pid = String(child.pid ?? `${Date.now()}-${Math.random().toString(16).slice(2)}`);
|
|
@@ -1163,7 +2085,7 @@ var ProcessManager = class extends EventEmitter {
|
|
|
1163
2085
|
}
|
|
1164
2086
|
record.status = "stopped";
|
|
1165
2087
|
record.endedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1166
|
-
record.child
|
|
2088
|
+
killChildProcessTree(record.child);
|
|
1167
2089
|
this.emitSystem(record, "Stopped by DevSurface");
|
|
1168
2090
|
this.emit("process", this.snapshot(record));
|
|
1169
2091
|
return true;
|
|
@@ -1179,7 +2101,7 @@ var ProcessManager = class extends EventEmitter {
|
|
|
1179
2101
|
if (record.status === "running") {
|
|
1180
2102
|
record.status = "stopped";
|
|
1181
2103
|
record.endedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1182
|
-
record.child
|
|
2104
|
+
killChildProcessTree(record.child);
|
|
1183
2105
|
}
|
|
1184
2106
|
}
|
|
1185
2107
|
}
|
|
@@ -1197,16 +2119,17 @@ var ProcessManager = class extends EventEmitter {
|
|
|
1197
2119
|
});
|
|
1198
2120
|
}
|
|
1199
2121
|
emitLog(record, stream, message) {
|
|
2122
|
+
const boundedMessage = message.length <= LOG_MESSAGE_LIMIT ? message : message.slice(-LOG_MESSAGE_LIMIT);
|
|
1200
2123
|
const event = {
|
|
1201
2124
|
pid: record.pid,
|
|
1202
2125
|
script: record.script,
|
|
1203
2126
|
stream,
|
|
1204
|
-
message,
|
|
2127
|
+
message: boundedMessage,
|
|
1205
2128
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1206
2129
|
};
|
|
1207
2130
|
this.logs.push(event);
|
|
1208
|
-
if (this.logs.length >
|
|
1209
|
-
this.logs.splice(0, this.logs.length -
|
|
2131
|
+
if (this.logs.length > LOG_ENTRY_LIMIT) {
|
|
2132
|
+
this.logs.splice(0, this.logs.length - LOG_ENTRY_LIMIT);
|
|
1210
2133
|
}
|
|
1211
2134
|
this.emit("log", event);
|
|
1212
2135
|
}
|
|
@@ -1228,10 +2151,9 @@ var ProcessManager = class extends EventEmitter {
|
|
|
1228
2151
|
|
|
1229
2152
|
// src/server/routes/api.ts
|
|
1230
2153
|
import { constants as constants2, existsSync } from "fs";
|
|
1231
|
-
import { promises as
|
|
1232
|
-
import
|
|
2154
|
+
import { promises as fs16 } from "fs";
|
|
2155
|
+
import path12 from "path";
|
|
1233
2156
|
import spawn4 from "cross-spawn";
|
|
1234
|
-
import open from "open";
|
|
1235
2157
|
|
|
1236
2158
|
// src/server/localAccess.ts
|
|
1237
2159
|
var LOCAL_HOSTNAMES = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "::1"]);
|
|
@@ -1272,10 +2194,30 @@ function isSameOrigin(requestUrl, origin) {
|
|
|
1272
2194
|
}
|
|
1273
2195
|
}
|
|
1274
2196
|
|
|
2197
|
+
// src/server/mutationToken.ts
|
|
2198
|
+
import { randomBytes, timingSafeEqual } from "crypto";
|
|
2199
|
+
function createMutationToken() {
|
|
2200
|
+
return randomBytes(32).toString("hex");
|
|
2201
|
+
}
|
|
2202
|
+
function hasValidMutationToken(provided, expected) {
|
|
2203
|
+
if (typeof provided !== "string" || provided.length === 0) {
|
|
2204
|
+
return false;
|
|
2205
|
+
}
|
|
2206
|
+
if (provided.length !== expected.length) {
|
|
2207
|
+
return false;
|
|
2208
|
+
}
|
|
2209
|
+
return timingSafeEqual(Buffer.from(provided, "utf8"), Buffer.from(expected, "utf8"));
|
|
2210
|
+
}
|
|
2211
|
+
|
|
2212
|
+
// src/server/terminal.ts
|
|
2213
|
+
function isAllowedTerminalCommand(command) {
|
|
2214
|
+
return /^[A-Za-z0-9._+-]+$/.test(command);
|
|
2215
|
+
}
|
|
2216
|
+
|
|
1275
2217
|
// src/server/routes/api.ts
|
|
1276
|
-
function
|
|
1277
|
-
const relative =
|
|
1278
|
-
return relative === "" || !relative.startsWith("..") && !
|
|
2218
|
+
function isWithinRoot7(root, target) {
|
|
2219
|
+
const relative = path12.relative(path12.resolve(root), path12.resolve(target));
|
|
2220
|
+
return relative === "" || !relative.startsWith("..") && !path12.isAbsolute(relative);
|
|
1279
2221
|
}
|
|
1280
2222
|
function isAllowedMutationOrigin(requestUrl, origin) {
|
|
1281
2223
|
if (origin === null) {
|
|
@@ -1290,35 +2232,35 @@ function hasMutationIntent(intent) {
|
|
|
1290
2232
|
return intent === "dashboard";
|
|
1291
2233
|
}
|
|
1292
2234
|
async function realPathWithinRoot(root, target) {
|
|
1293
|
-
if (!
|
|
2235
|
+
if (!isWithinRoot7(root, target)) {
|
|
1294
2236
|
return false;
|
|
1295
2237
|
}
|
|
1296
2238
|
try {
|
|
1297
|
-
const [realRoot, realTarget] = await Promise.all([
|
|
1298
|
-
return
|
|
2239
|
+
const [realRoot, realTarget] = await Promise.all([fs16.realpath(root), fs16.realpath(target)]);
|
|
2240
|
+
return isWithinRoot7(realRoot, realTarget);
|
|
1299
2241
|
} catch {
|
|
1300
2242
|
return false;
|
|
1301
2243
|
}
|
|
1302
2244
|
}
|
|
1303
2245
|
async function writableDestinationWithinRoot(root, destination) {
|
|
1304
|
-
if (!
|
|
2246
|
+
if (!isWithinRoot7(root, destination)) {
|
|
1305
2247
|
return false;
|
|
1306
2248
|
}
|
|
1307
2249
|
try {
|
|
1308
2250
|
const [realRoot, realParent] = await Promise.all([
|
|
1309
|
-
|
|
1310
|
-
|
|
2251
|
+
fs16.realpath(root),
|
|
2252
|
+
fs16.realpath(path12.dirname(destination))
|
|
1311
2253
|
]);
|
|
1312
|
-
return
|
|
2254
|
+
return isWithinRoot7(realRoot, realParent);
|
|
1313
2255
|
} catch {
|
|
1314
2256
|
return false;
|
|
1315
2257
|
}
|
|
1316
2258
|
}
|
|
1317
2259
|
async function copyFileExclusive(source, destination) {
|
|
1318
|
-
const content = await
|
|
2260
|
+
const content = await fs16.readFile(source);
|
|
1319
2261
|
let handle2 = null;
|
|
1320
2262
|
try {
|
|
1321
|
-
handle2 = await
|
|
2263
|
+
handle2 = await fs16.open(
|
|
1322
2264
|
destination,
|
|
1323
2265
|
constants2.O_CREAT | constants2.O_EXCL | constants2.O_WRONLY,
|
|
1324
2266
|
384
|
|
@@ -1345,15 +2287,15 @@ function resolveCommandPromptExecutable() {
|
|
|
1345
2287
|
return process.env.ComSpec ?? "cmd.exe";
|
|
1346
2288
|
}
|
|
1347
2289
|
function findExecutable(command) {
|
|
1348
|
-
if (
|
|
2290
|
+
if (path12.isAbsolute(command)) {
|
|
1349
2291
|
return existsSync(command) ? command : null;
|
|
1350
2292
|
}
|
|
1351
2293
|
const pathValue = process.env.PATH ?? "";
|
|
1352
|
-
for (const directory of pathValue.split(
|
|
2294
|
+
for (const directory of pathValue.split(path12.delimiter)) {
|
|
1353
2295
|
if (directory.length === 0) {
|
|
1354
2296
|
continue;
|
|
1355
2297
|
}
|
|
1356
|
-
const candidate =
|
|
2298
|
+
const candidate = path12.join(directory, command);
|
|
1357
2299
|
if (existsSync(candidate)) {
|
|
1358
2300
|
return candidate;
|
|
1359
2301
|
}
|
|
@@ -1394,8 +2336,8 @@ function openTerminalAt(root) {
|
|
|
1394
2336
|
if (process.platform === "darwin") {
|
|
1395
2337
|
return launchDetached("open", ["-a", "Terminal", root], root);
|
|
1396
2338
|
}
|
|
1397
|
-
const configuredTerminal = process.env.TERMINAL;
|
|
1398
|
-
if (configuredTerminal !== void 0 && findExecutable(configuredTerminal) !== null) {
|
|
2339
|
+
const configuredTerminal = process.env.TERMINAL?.trim();
|
|
2340
|
+
if (configuredTerminal !== void 0 && configuredTerminal.length > 0 && isAllowedTerminalCommand(configuredTerminal) && findExecutable(configuredTerminal) !== null) {
|
|
1399
2341
|
return launchDetached(configuredTerminal, [], root);
|
|
1400
2342
|
}
|
|
1401
2343
|
const linuxTerminals = [
|
|
@@ -1420,6 +2362,10 @@ function openTerminalAt(root) {
|
|
|
1420
2362
|
return launchDetached(terminal.command, terminal.args, root);
|
|
1421
2363
|
}
|
|
1422
2364
|
function registerApiRoutes(app, options) {
|
|
2365
|
+
const dockerController = options.dockerController ?? new DockerComposeController(options.projectRoot);
|
|
2366
|
+
app.get("/api/session", (context) => {
|
|
2367
|
+
return context.json({ token: options.mutationToken });
|
|
2368
|
+
});
|
|
1423
2369
|
app.use("/api/*", async (context, next) => {
|
|
1424
2370
|
const host = context.req.header("host") ?? new URL(context.req.url).host;
|
|
1425
2371
|
if (!isAllowedLocalHostHeader(host)) {
|
|
@@ -1429,7 +2375,8 @@ function registerApiRoutes(app, options) {
|
|
|
1429
2375
|
const origin = context.req.header("origin") ?? null;
|
|
1430
2376
|
const secFetchSite = context.req.header("sec-fetch-site") ?? null;
|
|
1431
2377
|
const intent = context.req.header("x-devsurface-intent") ?? null;
|
|
1432
|
-
|
|
2378
|
+
const token = context.req.header("x-devsurface-token") ?? null;
|
|
2379
|
+
if (!hasMutationIntent(intent) || !hasValidMutationToken(token, options.mutationToken) || isCrossSiteFetch(secFetchSite) || !isAllowedMutationOrigin(context.req.url, origin)) {
|
|
1433
2380
|
return context.json({ error: "Cross-origin mutation rejected." }, 403);
|
|
1434
2381
|
}
|
|
1435
2382
|
}
|
|
@@ -1444,6 +2391,60 @@ function registerApiRoutes(app, options) {
|
|
|
1444
2391
|
app.get("/api/processes", (context) => {
|
|
1445
2392
|
return context.json(options.processManager.list());
|
|
1446
2393
|
});
|
|
2394
|
+
app.get("/api/logs", (context) => {
|
|
2395
|
+
return context.json(options.processManager.listLogs());
|
|
2396
|
+
});
|
|
2397
|
+
app.get("/api/docker/:service/logs", async (context) => {
|
|
2398
|
+
const service = decodeURIComponent(context.req.param("service"));
|
|
2399
|
+
try {
|
|
2400
|
+
return context.json(await dockerController.logs(service));
|
|
2401
|
+
} catch (error) {
|
|
2402
|
+
if (error instanceof DockerOperationError) {
|
|
2403
|
+
if (error.code === "compose-not-found" || error.code === "service-not-found") {
|
|
2404
|
+
return context.json({ error: error.message, code: error.code }, 404);
|
|
2405
|
+
}
|
|
2406
|
+
if (error.code === "docker-not-installed" || error.code === "docker-not-running") {
|
|
2407
|
+
return context.json({ error: error.message, code: error.code }, 503);
|
|
2408
|
+
}
|
|
2409
|
+
return context.json({ error: error.message, code: error.code }, 502);
|
|
2410
|
+
}
|
|
2411
|
+
throw error;
|
|
2412
|
+
}
|
|
2413
|
+
});
|
|
2414
|
+
app.post("/api/docker/:service/start", async (context) => {
|
|
2415
|
+
const service = decodeURIComponent(context.req.param("service"));
|
|
2416
|
+
try {
|
|
2417
|
+
return context.json(await dockerController.start(service));
|
|
2418
|
+
} catch (error) {
|
|
2419
|
+
if (error instanceof DockerOperationError) {
|
|
2420
|
+
if (error.code === "compose-not-found" || error.code === "service-not-found") {
|
|
2421
|
+
return context.json({ error: error.message, code: error.code }, 404);
|
|
2422
|
+
}
|
|
2423
|
+
if (error.code === "docker-not-installed" || error.code === "docker-not-running") {
|
|
2424
|
+
return context.json({ error: error.message, code: error.code }, 503);
|
|
2425
|
+
}
|
|
2426
|
+
return context.json({ error: error.message, code: error.code }, 502);
|
|
2427
|
+
}
|
|
2428
|
+
throw error;
|
|
2429
|
+
}
|
|
2430
|
+
});
|
|
2431
|
+
app.post("/api/docker/:service/stop", async (context) => {
|
|
2432
|
+
const service = decodeURIComponent(context.req.param("service"));
|
|
2433
|
+
try {
|
|
2434
|
+
return context.json(await dockerController.stop(service));
|
|
2435
|
+
} catch (error) {
|
|
2436
|
+
if (error instanceof DockerOperationError) {
|
|
2437
|
+
if (error.code === "compose-not-found" || error.code === "service-not-found") {
|
|
2438
|
+
return context.json({ error: error.message, code: error.code }, 404);
|
|
2439
|
+
}
|
|
2440
|
+
if (error.code === "docker-not-installed" || error.code === "docker-not-running") {
|
|
2441
|
+
return context.json({ error: error.message, code: error.code }, 503);
|
|
2442
|
+
}
|
|
2443
|
+
return context.json({ error: error.message, code: error.code }, 502);
|
|
2444
|
+
}
|
|
2445
|
+
throw error;
|
|
2446
|
+
}
|
|
2447
|
+
});
|
|
1447
2448
|
app.post("/api/run/:script", async (context) => {
|
|
1448
2449
|
const script = decodeURIComponent(context.req.param("script"));
|
|
1449
2450
|
const scan = await scanProject(options.projectRoot);
|
|
@@ -1451,6 +2452,9 @@ function registerApiRoutes(app, options) {
|
|
|
1451
2452
|
if (packageScript === void 0) {
|
|
1452
2453
|
return context.json({ error: `Script "${script}" was not found.` }, 404);
|
|
1453
2454
|
}
|
|
2455
|
+
if (isDangerousCommand(packageScript)) {
|
|
2456
|
+
return context.json({ error: "Refusing to run dangerous script." }, 403);
|
|
2457
|
+
}
|
|
1454
2458
|
const command = await resolvePackageRunCommand({
|
|
1455
2459
|
cwd: options.projectRoot,
|
|
1456
2460
|
packageManager: scan.packageManager,
|
|
@@ -1496,13 +2500,24 @@ function registerApiRoutes(app, options) {
|
|
|
1496
2500
|
if (configuredCommand === null) {
|
|
1497
2501
|
return context.json({ error: `Configured command "${name}" was not found.` }, 404);
|
|
1498
2502
|
}
|
|
2503
|
+
if (isDangerousCommand(configuredCommand)) {
|
|
2504
|
+
return context.json({ error: "Refusing to run dangerous command." }, 403);
|
|
2505
|
+
}
|
|
2506
|
+
const resolvedCommand = await resolveConfiguredCommand(options.projectRoot, configuredCommand);
|
|
2507
|
+
if (resolvedCommand === null) {
|
|
2508
|
+
return context.json(
|
|
2509
|
+
{
|
|
2510
|
+
error: "Configured command uses unsupported shell syntax. Use a simple executable with arguments, or move complex logic into a package.json script."
|
|
2511
|
+
},
|
|
2512
|
+
400
|
|
2513
|
+
);
|
|
2514
|
+
}
|
|
1499
2515
|
const processInfo = options.processManager.start({
|
|
1500
2516
|
cwd: options.projectRoot,
|
|
1501
2517
|
script: name,
|
|
1502
|
-
command:
|
|
1503
|
-
args:
|
|
1504
|
-
displayCommand:
|
|
1505
|
-
shell: true
|
|
2518
|
+
command: resolvedCommand.command,
|
|
2519
|
+
args: resolvedCommand.args,
|
|
2520
|
+
displayCommand: resolvedCommand.displayCommand
|
|
1506
2521
|
});
|
|
1507
2522
|
return context.json({
|
|
1508
2523
|
...processInfo,
|
|
@@ -1510,15 +2525,15 @@ function registerApiRoutes(app, options) {
|
|
|
1510
2525
|
});
|
|
1511
2526
|
});
|
|
1512
2527
|
app.post("/api/open/folder", async (context) => {
|
|
1513
|
-
await
|
|
2528
|
+
await open_default(options.projectRoot);
|
|
1514
2529
|
return context.json({ opened: true, target: "folder" });
|
|
1515
2530
|
});
|
|
1516
2531
|
app.post("/api/open/package", async (context) => {
|
|
1517
|
-
const packagePath =
|
|
2532
|
+
const packagePath = path12.join(options.projectRoot, "package.json");
|
|
1518
2533
|
if (!await realPathWithinRoot(options.projectRoot, packagePath)) {
|
|
1519
2534
|
return context.json({ error: "package.json was not found inside the project root." }, 404);
|
|
1520
2535
|
}
|
|
1521
|
-
await
|
|
2536
|
+
await open_default(packagePath);
|
|
1522
2537
|
return context.json({ opened: true, target: "package" });
|
|
1523
2538
|
});
|
|
1524
2539
|
app.post("/api/open/terminal", (context) => {
|
|
@@ -1532,7 +2547,7 @@ function registerApiRoutes(app, options) {
|
|
|
1532
2547
|
if (examplePath === null) {
|
|
1533
2548
|
return context.json({ error: ".env.example was not found." }, 404);
|
|
1534
2549
|
}
|
|
1535
|
-
const destination = localPath ??
|
|
2550
|
+
const destination = localPath ?? path12.join(options.projectRoot, scan.config?.config.env?.local ?? ".env");
|
|
1536
2551
|
if (!await realPathWithinRoot(options.projectRoot, examplePath) || !await writableDestinationWithinRoot(options.projectRoot, destination)) {
|
|
1537
2552
|
return context.json({ error: "Refusing to copy env files outside the project root." }, 400);
|
|
1538
2553
|
}
|
|
@@ -1613,37 +2628,104 @@ function assertLocalHost(host) {
|
|
|
1613
2628
|
throw new Error("DevSurface must bind only to 127.0.0.1.");
|
|
1614
2629
|
}
|
|
1615
2630
|
}
|
|
1616
|
-
async function
|
|
2631
|
+
async function fileExists(filePath) {
|
|
1617
2632
|
try {
|
|
1618
|
-
await
|
|
2633
|
+
await fs17.access(filePath);
|
|
1619
2634
|
return true;
|
|
1620
2635
|
} catch {
|
|
1621
2636
|
return false;
|
|
1622
2637
|
}
|
|
1623
2638
|
}
|
|
1624
2639
|
async function findWebDistDir() {
|
|
1625
|
-
const moduleDir =
|
|
2640
|
+
const moduleDir = path13.dirname(fileURLToPath2(import.meta.url));
|
|
1626
2641
|
const candidates = [
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
2642
|
+
path13.join(moduleDir, "..", "web", "dist"),
|
|
2643
|
+
path13.join(moduleDir, "..", "..", "src", "web", "dist"),
|
|
2644
|
+
path13.join(moduleDir, "web", "dist")
|
|
1630
2645
|
];
|
|
1631
2646
|
for (const candidate of candidates) {
|
|
1632
|
-
if (await
|
|
2647
|
+
if (await fileExists(path13.join(candidate, "index.html"))) {
|
|
1633
2648
|
return candidate;
|
|
1634
2649
|
}
|
|
1635
2650
|
}
|
|
1636
2651
|
return null;
|
|
1637
2652
|
}
|
|
2653
|
+
function toListenError(error, port) {
|
|
2654
|
+
const code = error instanceof Error ? error.code : void 0;
|
|
2655
|
+
if (code === "EADDRINUSE") {
|
|
2656
|
+
return new Error(
|
|
2657
|
+
`Port ${port} is already in use on ${HOST}. Stop the other process or run DevSurface with --port ${port + 1}.`,
|
|
2658
|
+
{ cause: error }
|
|
2659
|
+
);
|
|
2660
|
+
}
|
|
2661
|
+
if (code === "EACCES") {
|
|
2662
|
+
return new Error(`DevSurface does not have permission to bind to ${HOST}:${port}.`, {
|
|
2663
|
+
cause: error
|
|
2664
|
+
});
|
|
2665
|
+
}
|
|
2666
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
2667
|
+
}
|
|
2668
|
+
async function listenOnLocalHost(server, wss, port) {
|
|
2669
|
+
await new Promise((resolve, reject) => {
|
|
2670
|
+
let settled = false;
|
|
2671
|
+
const cleanup = () => {
|
|
2672
|
+
server.off("error", onError);
|
|
2673
|
+
server.off("listening", onListening);
|
|
2674
|
+
wss.off("error", onError);
|
|
2675
|
+
};
|
|
2676
|
+
const onError = (error) => {
|
|
2677
|
+
if (settled) {
|
|
2678
|
+
return;
|
|
2679
|
+
}
|
|
2680
|
+
settled = true;
|
|
2681
|
+
cleanup();
|
|
2682
|
+
reject(toListenError(error, port));
|
|
2683
|
+
};
|
|
2684
|
+
const onListening = () => {
|
|
2685
|
+
if (settled) {
|
|
2686
|
+
return;
|
|
2687
|
+
}
|
|
2688
|
+
settled = true;
|
|
2689
|
+
cleanup();
|
|
2690
|
+
resolve();
|
|
2691
|
+
};
|
|
2692
|
+
wss.once("error", onError);
|
|
2693
|
+
server.once("error", onError);
|
|
2694
|
+
server.once("listening", onListening);
|
|
2695
|
+
server.listen(port, HOST);
|
|
2696
|
+
});
|
|
2697
|
+
}
|
|
2698
|
+
async function closeWebSocketServer(wss) {
|
|
2699
|
+
await new Promise((resolve) => {
|
|
2700
|
+
wss.close(() => resolve());
|
|
2701
|
+
});
|
|
2702
|
+
}
|
|
2703
|
+
async function closeHttpServer(server) {
|
|
2704
|
+
if (!server.listening) {
|
|
2705
|
+
return;
|
|
2706
|
+
}
|
|
2707
|
+
await new Promise((resolve, reject) => {
|
|
2708
|
+
server.close((error) => {
|
|
2709
|
+
if (error) {
|
|
2710
|
+
reject(error);
|
|
2711
|
+
} else {
|
|
2712
|
+
resolve();
|
|
2713
|
+
}
|
|
2714
|
+
});
|
|
2715
|
+
});
|
|
2716
|
+
}
|
|
1638
2717
|
async function createApp(options) {
|
|
1639
2718
|
const app = new Hono();
|
|
1640
|
-
registerApiRoutes(app,
|
|
2719
|
+
registerApiRoutes(app, {
|
|
2720
|
+
...options,
|
|
2721
|
+
mutationToken: options.mutationToken ?? createMutationToken()
|
|
2722
|
+
});
|
|
1641
2723
|
const webDistDir = await findWebDistDir();
|
|
1642
2724
|
if (webDistDir !== null) {
|
|
1643
2725
|
app.use("/assets/*", serveStatic({ root: webDistDir }));
|
|
1644
2726
|
app.get("/favicon.svg", serveStatic({ root: webDistDir }));
|
|
1645
2727
|
app.get("*", async (context) => {
|
|
1646
|
-
const html = await
|
|
2728
|
+
const html = await fs17.readFile(path13.join(webDistDir, "index.html"), "utf8");
|
|
1647
2729
|
return context.html(html);
|
|
1648
2730
|
});
|
|
1649
2731
|
} else {
|
|
@@ -1666,15 +2748,16 @@ async function startDevSurfaceServer(options) {
|
|
|
1666
2748
|
projectRoot: options.projectRoot,
|
|
1667
2749
|
processManager
|
|
1668
2750
|
});
|
|
1669
|
-
const server =
|
|
2751
|
+
const server = createAdaptorServer({
|
|
1670
2752
|
fetch: app.fetch,
|
|
1671
|
-
port,
|
|
1672
2753
|
hostname: HOST
|
|
1673
2754
|
});
|
|
1674
2755
|
const wss = setupWebSocket(server, processManager);
|
|
2756
|
+
await listenOnLocalHost(server, wss, port);
|
|
2757
|
+
processManager.attachCleanupHandlers();
|
|
1675
2758
|
const url = `http://${HOST}:${port}`;
|
|
1676
2759
|
if (options.openBrowser !== false) {
|
|
1677
|
-
await
|
|
2760
|
+
await open_default(url);
|
|
1678
2761
|
}
|
|
1679
2762
|
return {
|
|
1680
2763
|
url,
|
|
@@ -1682,26 +2765,19 @@ async function startDevSurfaceServer(options) {
|
|
|
1682
2765
|
processManager,
|
|
1683
2766
|
close: async () => {
|
|
1684
2767
|
processManager.killAll();
|
|
1685
|
-
await
|
|
1686
|
-
|
|
1687
|
-
});
|
|
1688
|
-
await new Promise((resolve, reject) => {
|
|
1689
|
-
server.close((error) => {
|
|
1690
|
-
if (error) {
|
|
1691
|
-
reject(error);
|
|
1692
|
-
} else {
|
|
1693
|
-
resolve();
|
|
1694
|
-
}
|
|
1695
|
-
});
|
|
1696
|
-
});
|
|
2768
|
+
await closeWebSocketServer(wss);
|
|
2769
|
+
await closeHttpServer(server);
|
|
1697
2770
|
}
|
|
1698
2771
|
};
|
|
1699
2772
|
}
|
|
1700
2773
|
|
|
2774
|
+
// src/version.ts
|
|
2775
|
+
var DEV_SURFACE_VERSION = "0.3.0";
|
|
2776
|
+
|
|
1701
2777
|
// src/cli/commands/start.ts
|
|
1702
2778
|
async function startCommand(options) {
|
|
1703
2779
|
const cwd = options.cwd ?? process.cwd();
|
|
1704
|
-
console.log(pc5.bold(`DevSurface
|
|
2780
|
+
console.log(pc5.bold(`DevSurface v${DEV_SURFACE_VERSION}`));
|
|
1705
2781
|
console.log("Scanning project...\n");
|
|
1706
2782
|
const scan = await scanProject(cwd);
|
|
1707
2783
|
printScanResult(scan);
|
|
@@ -1738,7 +2814,7 @@ function handle(command) {
|
|
|
1738
2814
|
process.exitCode = 1;
|
|
1739
2815
|
});
|
|
1740
2816
|
}
|
|
1741
|
-
program.name("devsurface").description("Turn any Node.js repository into a local developer control panel.").version(
|
|
2817
|
+
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) => {
|
|
1742
2818
|
handle(
|
|
1743
2819
|
startCommand({
|
|
1744
2820
|
cwd: process.cwd(),
|