devsurface 0.2.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 +15 -0
- package/README.md +48 -2
- package/action/dist/index.js +644 -0
- package/action.yml +39 -0
- package/dist/cli/index.js +1169 -224
- package/dist/cli/index.js.map +1 -1
- package/package.json +8 -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-CdzG3b92.js +0 -10
- package/src/web/dist/assets/index-l7i8vzTo.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 = {
|
|
@@ -49,6 +80,10 @@ var defaultConfig = {
|
|
|
49
80
|
|
|
50
81
|
// src/core/config/load.ts
|
|
51
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
|
+
}
|
|
52
87
|
function isRecord(value) {
|
|
53
88
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
54
89
|
}
|
|
@@ -125,6 +160,14 @@ function validateConfig(raw) {
|
|
|
125
160
|
if (raw.services !== void 0 && !isRecord(raw.services)) {
|
|
126
161
|
warnings.push("services must be an object.");
|
|
127
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
|
+
}
|
|
128
171
|
return {
|
|
129
172
|
config: {
|
|
130
173
|
name: typeof raw.name === "string" ? raw.name : void 0,
|
|
@@ -134,7 +177,7 @@ function validateConfig(raw) {
|
|
|
134
177
|
ports: toPorts(raw.ports, warnings),
|
|
135
178
|
env,
|
|
136
179
|
services,
|
|
137
|
-
docs
|
|
180
|
+
docs
|
|
138
181
|
},
|
|
139
182
|
warnings
|
|
140
183
|
};
|
|
@@ -142,10 +185,17 @@ function validateConfig(raw) {
|
|
|
142
185
|
async function loadConfig(root) {
|
|
143
186
|
const configPath = path.join(root, CONFIG_FILE_NAME);
|
|
144
187
|
try {
|
|
145
|
-
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");
|
|
146
196
|
const parsed = JSON.parse(content);
|
|
147
197
|
const { config, warnings } = validateConfig(parsed);
|
|
148
|
-
return { path:
|
|
198
|
+
return { path: realConfigPath, config, warnings };
|
|
149
199
|
} catch (error) {
|
|
150
200
|
const code = typeof error === "object" && error !== null && "code" in error ? error.code : void 0;
|
|
151
201
|
if (code === "ENOENT") {
|
|
@@ -162,7 +212,7 @@ async function loadConfig(root) {
|
|
|
162
212
|
}
|
|
163
213
|
}
|
|
164
214
|
|
|
165
|
-
// src/core/
|
|
215
|
+
// src/core/docker/compose.ts
|
|
166
216
|
import { promises as fs3 } from "fs";
|
|
167
217
|
import os from "os";
|
|
168
218
|
import path3 from "path";
|
|
@@ -173,7 +223,7 @@ import { parse as parseYaml } from "yaml";
|
|
|
173
223
|
import { constants } from "fs";
|
|
174
224
|
import { promises as fs2 } from "fs";
|
|
175
225
|
import path2 from "path";
|
|
176
|
-
function
|
|
226
|
+
function isWithinRoot2(root, target) {
|
|
177
227
|
const resolvedRoot = path2.resolve(root);
|
|
178
228
|
const resolvedTarget = path2.resolve(target);
|
|
179
229
|
const relative = path2.relative(resolvedRoot, resolvedTarget);
|
|
@@ -193,7 +243,7 @@ function executableNames(command) {
|
|
|
193
243
|
return extensions.map((extension) => `${command}${extension}`);
|
|
194
244
|
}
|
|
195
245
|
async function executableOutsideRoot(root, candidate) {
|
|
196
|
-
if (
|
|
246
|
+
if (isWithinRoot2(root, candidate)) {
|
|
197
247
|
return null;
|
|
198
248
|
}
|
|
199
249
|
try {
|
|
@@ -201,7 +251,7 @@ async function executableOutsideRoot(root, candidate) {
|
|
|
201
251
|
fs2.realpath(root),
|
|
202
252
|
fs2.realpath(candidate)
|
|
203
253
|
]);
|
|
204
|
-
if (
|
|
254
|
+
if (isWithinRoot2(realRoot, realCandidate)) {
|
|
205
255
|
return null;
|
|
206
256
|
}
|
|
207
257
|
await fs2.access(realCandidate, constants.X_OK);
|
|
@@ -216,7 +266,7 @@ async function resolveExecutableOutsideRoot(root, command) {
|
|
|
216
266
|
}
|
|
217
267
|
for (const entry of pathEntries(process.env.PATH ?? "")) {
|
|
218
268
|
const directory = path2.resolve(entry);
|
|
219
|
-
if (
|
|
269
|
+
if (isWithinRoot2(root, directory)) {
|
|
220
270
|
continue;
|
|
221
271
|
}
|
|
222
272
|
for (const executableName of executableNames(command)) {
|
|
@@ -229,50 +279,38 @@ async function resolveExecutableOutsideRoot(root, command) {
|
|
|
229
279
|
return null;
|
|
230
280
|
}
|
|
231
281
|
|
|
232
|
-
// src/core/
|
|
282
|
+
// src/core/docker/compose.ts
|
|
233
283
|
var COMPOSE_FILES = ["docker-compose.yml", "docker-compose.yaml", "compose.yml", "compose.yaml"];
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
return await fileExists(filePath) ? filePath : null;
|
|
247
|
-
})
|
|
248
|
-
);
|
|
249
|
-
return checks.filter((filePath) => filePath !== null);
|
|
250
|
-
}
|
|
251
|
-
async function extractServices(composePath) {
|
|
252
|
-
try {
|
|
253
|
-
const content = await fs3.readFile(composePath, "utf8");
|
|
254
|
-
const parsed = parseYaml(content);
|
|
255
|
-
if (typeof parsed === "object" && parsed !== null && "services" in parsed && typeof parsed.services === "object" && parsed.services !== null) {
|
|
256
|
-
return Object.keys(parsed.services);
|
|
257
|
-
}
|
|
258
|
-
} catch {
|
|
259
|
-
return [];
|
|
260
|
-
}
|
|
261
|
-
return [];
|
|
262
|
-
}
|
|
263
|
-
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) {
|
|
264
296
|
const relative = path3.relative(path3.resolve(root), path3.resolve(target));
|
|
265
297
|
return relative === "" || !relative.startsWith("..") && !path3.isAbsolute(relative);
|
|
266
298
|
}
|
|
267
299
|
function dockerCommandCwd(root) {
|
|
268
300
|
const candidates = [os.homedir(), os.tmpdir(), path3.parse(path3.resolve(root)).root];
|
|
269
|
-
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);
|
|
270
306
|
}
|
|
271
|
-
async
|
|
307
|
+
var runDockerCommand = async (root, args, options = {}) => {
|
|
272
308
|
const dockerExecutable = await resolveExecutableOutsideRoot(root, "docker");
|
|
273
309
|
if (dockerExecutable === null) {
|
|
274
|
-
return { code: null, stdout: "", stderr: "" };
|
|
310
|
+
return { code: null, stdout: "", stderr: "", error: "not-found" };
|
|
275
311
|
}
|
|
312
|
+
const timeoutMs = options.timeoutMs ?? 5e3;
|
|
313
|
+
const outputLimit = options.outputLimit ?? COMMAND_OUTPUT_LIMIT;
|
|
276
314
|
return await new Promise((resolve) => {
|
|
277
315
|
const child = spawn(dockerExecutable, args, {
|
|
278
316
|
cwd: dockerCommandCwd(root),
|
|
@@ -281,36 +319,152 @@ async function runDockerCommand(root, args, timeoutMs = 2500) {
|
|
|
281
319
|
let settled = false;
|
|
282
320
|
let stdout = "";
|
|
283
321
|
let stderr = "";
|
|
284
|
-
const
|
|
322
|
+
const finish = (result) => {
|
|
323
|
+
if (settled) {
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
285
326
|
settled = true;
|
|
327
|
+
clearTimeout(timeout);
|
|
328
|
+
resolve(result);
|
|
329
|
+
};
|
|
330
|
+
const timeout = setTimeout(() => {
|
|
286
331
|
child.kill();
|
|
287
|
-
|
|
332
|
+
finish({ code: null, stdout, stderr, error: "timeout" });
|
|
288
333
|
}, timeoutMs);
|
|
289
334
|
child.stdout?.on("data", (chunk) => {
|
|
290
|
-
stdout
|
|
335
|
+
stdout = appendBounded(stdout, chunk.toString(), outputLimit);
|
|
291
336
|
});
|
|
292
337
|
child.stderr?.on("data", (chunk) => {
|
|
293
|
-
stderr
|
|
338
|
+
stderr = appendBounded(stderr, chunk.toString(), outputLimit);
|
|
294
339
|
});
|
|
295
340
|
child.on("error", () => {
|
|
296
|
-
|
|
297
|
-
if (!settled) {
|
|
298
|
-
settled = true;
|
|
299
|
-
resolve({ code: null, stdout, stderr });
|
|
300
|
-
}
|
|
341
|
+
finish({ code: null, stdout, stderr, error: "spawn" });
|
|
301
342
|
});
|
|
302
343
|
child.on("close", (code) => {
|
|
303
|
-
|
|
304
|
-
if (!settled) {
|
|
305
|
-
settled = true;
|
|
306
|
-
resolve({ code, stdout, stderr });
|
|
307
|
-
}
|
|
344
|
+
finish({ code, stdout, stderr, error: null });
|
|
308
345
|
});
|
|
309
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;
|
|
310
362
|
}
|
|
311
|
-
async function
|
|
312
|
-
const
|
|
313
|
-
|
|
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);
|
|
392
|
+
}
|
|
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
|
+
});
|
|
314
468
|
}
|
|
315
469
|
function parseComposePs(output) {
|
|
316
470
|
const statuses = /* @__PURE__ */ new Map();
|
|
@@ -320,82 +474,181 @@ function parseComposePs(output) {
|
|
|
320
474
|
}
|
|
321
475
|
try {
|
|
322
476
|
const parsed = JSON.parse(compactOutput);
|
|
323
|
-
const
|
|
324
|
-
for (const row of rows2) {
|
|
477
|
+
for (const row of Array.isArray(parsed) ? parsed : [parsed]) {
|
|
325
478
|
addComposeStatusRow(statuses, row);
|
|
326
479
|
}
|
|
327
480
|
return statuses;
|
|
328
481
|
} catch {
|
|
329
482
|
}
|
|
330
|
-
const
|
|
331
|
-
for (const row of rows) {
|
|
483
|
+
for (const line of compactOutput.split(/\r?\n/)) {
|
|
332
484
|
try {
|
|
333
|
-
|
|
334
|
-
addComposeStatusRow(statuses, parsed);
|
|
485
|
+
addComposeStatusRow(statuses, JSON.parse(line));
|
|
335
486
|
} catch {
|
|
336
|
-
return
|
|
487
|
+
return /* @__PURE__ */ new Map();
|
|
337
488
|
}
|
|
338
489
|
}
|
|
339
490
|
return statuses;
|
|
340
491
|
}
|
|
341
|
-
function
|
|
342
|
-
|
|
343
|
-
|
|
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
|
+
};
|
|
344
522
|
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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;
|
|
348
535
|
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
status
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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
|
+
}
|
|
359
548
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
})
|
|
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
|
+
};
|
|
366
599
|
}
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
name: service,
|
|
379
|
-
status: "stopped",
|
|
380
|
-
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));
|
|
381
611
|
}
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
return null;
|
|
612
|
+
return {
|
|
613
|
+
service,
|
|
614
|
+
action,
|
|
615
|
+
output: cleanMessage(result.stdout || result.stderr)
|
|
616
|
+
};
|
|
388
617
|
}
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
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();
|
|
399
652
|
}
|
|
400
653
|
|
|
401
654
|
// src/core/scanner/env.ts
|
|
@@ -529,7 +782,7 @@ function detectFramework(packageJson) {
|
|
|
529
782
|
// src/core/scanner/git.ts
|
|
530
783
|
import { promises as fs5 } from "fs";
|
|
531
784
|
import path5 from "path";
|
|
532
|
-
function
|
|
785
|
+
function isWithinRoot4(root, target) {
|
|
533
786
|
const resolvedRoot = path5.resolve(root);
|
|
534
787
|
const resolvedTarget = path5.resolve(target);
|
|
535
788
|
const relative = path5.relative(resolvedRoot, resolvedTarget);
|
|
@@ -548,14 +801,14 @@ async function resolveGitDirectory(root) {
|
|
|
548
801
|
if (match) {
|
|
549
802
|
const gitDir = match[1].trim();
|
|
550
803
|
const resolvedGitDir = path5.isAbsolute(gitDir) ? path5.resolve(gitDir) : path5.resolve(root, gitDir);
|
|
551
|
-
if (!
|
|
804
|
+
if (!isWithinRoot4(root, resolvedGitDir)) {
|
|
552
805
|
return null;
|
|
553
806
|
}
|
|
554
807
|
const [realRoot, realGitDir] = await Promise.all([
|
|
555
808
|
fs5.realpath(root),
|
|
556
809
|
fs5.realpath(resolvedGitDir)
|
|
557
810
|
]);
|
|
558
|
-
return
|
|
811
|
+
return isWithinRoot4(realRoot, realGitDir) ? resolvedGitDir : null;
|
|
559
812
|
}
|
|
560
813
|
}
|
|
561
814
|
} catch {
|
|
@@ -613,7 +866,7 @@ async function detectPackageManager(root) {
|
|
|
613
866
|
// src/core/scanner/packageJson.ts
|
|
614
867
|
import { promises as fs7 } from "fs";
|
|
615
868
|
import path7 from "path";
|
|
616
|
-
function
|
|
869
|
+
function isWithinRoot5(root, target) {
|
|
617
870
|
const relative = path7.relative(root, target);
|
|
618
871
|
return relative === "" || !relative.startsWith("..") && !path7.isAbsolute(relative);
|
|
619
872
|
}
|
|
@@ -624,7 +877,7 @@ async function readPackageJson(root) {
|
|
|
624
877
|
fs7.realpath(root),
|
|
625
878
|
fs7.realpath(packageJsonPath)
|
|
626
879
|
]);
|
|
627
|
-
if (!
|
|
880
|
+
if (!isWithinRoot5(realRoot, realPackageJsonPath)) {
|
|
628
881
|
return null;
|
|
629
882
|
}
|
|
630
883
|
const content = await fs7.readFile(realPackageJsonPath, "utf8");
|
|
@@ -727,13 +980,18 @@ function extractScripts(packageJson) {
|
|
|
727
980
|
}
|
|
728
981
|
|
|
729
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
|
+
}
|
|
730
987
|
async function findFirstFile(root, candidates) {
|
|
988
|
+
const resolvedRoot = await fs8.realpath(root).catch(() => path8.resolve(root));
|
|
731
989
|
for (const candidate of candidates) {
|
|
732
990
|
const filePath = path8.join(root, candidate);
|
|
733
991
|
try {
|
|
734
|
-
const stat = await fs8.stat(filePath);
|
|
735
|
-
if (stat.isFile()) {
|
|
736
|
-
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 };
|
|
737
995
|
}
|
|
738
996
|
} catch {
|
|
739
997
|
}
|
|
@@ -744,8 +1002,9 @@ function configuredPorts(configPorts) {
|
|
|
744
1002
|
return Array.isArray(configPorts) ? configPorts : [];
|
|
745
1003
|
}
|
|
746
1004
|
async function scanProject(root = process.cwd()) {
|
|
747
|
-
const
|
|
748
|
-
const
|
|
1005
|
+
const resolvedRoot = await fs8.realpath(root).catch(() => path8.resolve(root));
|
|
1006
|
+
const config = await loadConfig(resolvedRoot);
|
|
1007
|
+
const packageJson = await readPackageJson(resolvedRoot);
|
|
749
1008
|
const scripts = extractScripts(packageJson) ?? {};
|
|
750
1009
|
const framework = detectFramework(packageJson);
|
|
751
1010
|
const portsToProbe = [
|
|
@@ -754,17 +1013,17 @@ async function scanProject(root = process.cwd()) {
|
|
|
754
1013
|
...defaultPortsForFramework(framework)
|
|
755
1014
|
];
|
|
756
1015
|
const [packageManager, env, docker, git, ports, readme, license] = await Promise.all([
|
|
757
|
-
detectPackageManager(
|
|
758
|
-
detectEnv(
|
|
759
|
-
detectDocker(
|
|
760
|
-
detectGit(
|
|
1016
|
+
detectPackageManager(resolvedRoot),
|
|
1017
|
+
detectEnv(resolvedRoot, config?.config),
|
|
1018
|
+
detectDocker(resolvedRoot),
|
|
1019
|
+
detectGit(resolvedRoot),
|
|
761
1020
|
detectPorts(portsToProbe),
|
|
762
|
-
findFirstFile(
|
|
763
|
-
findFirstFile(
|
|
1021
|
+
findFirstFile(resolvedRoot, ["README.md", "README"]),
|
|
1022
|
+
findFirstFile(resolvedRoot, ["LICENSE", "LICENSE.md", "COPYING"])
|
|
764
1023
|
]);
|
|
765
1024
|
return {
|
|
766
|
-
root,
|
|
767
|
-
projectName: config?.config.name ?? packageJson?.data.name ?? path8.basename(
|
|
1025
|
+
root: resolvedRoot,
|
|
1026
|
+
projectName: config?.config.name ?? packageJson?.data.name ?? path8.basename(resolvedRoot),
|
|
768
1027
|
packageJson,
|
|
769
1028
|
packageManager: packageManager ?? (packageJson ? "npm" : null),
|
|
770
1029
|
scripts,
|
|
@@ -798,25 +1057,6 @@ async function readIfPresent2(filePath) {
|
|
|
798
1057
|
return null;
|
|
799
1058
|
}
|
|
800
1059
|
}
|
|
801
|
-
function extractReadmeScriptReferences(readmeContent) {
|
|
802
|
-
const references = /* @__PURE__ */ new Set();
|
|
803
|
-
const commandRegexes = [
|
|
804
|
-
/\bnpm\s+run\s+([A-Za-z0-9:_-]+)/g,
|
|
805
|
-
/\bpnpm\s+run\s+([A-Za-z0-9:_-]+)/g,
|
|
806
|
-
/\bbun\s+run\s+([A-Za-z0-9:_-]+)/g,
|
|
807
|
-
/\byarn\s+run\s+([A-Za-z0-9:_-]+)/g,
|
|
808
|
-
/\bnpm\s+(test|start|build)\b/g,
|
|
809
|
-
/\bpnpm\s+(test|start|build)\b/g,
|
|
810
|
-
/\byarn\s+(test|start|build)\b/g,
|
|
811
|
-
/\bbun\s+(test|start|build)\b/g
|
|
812
|
-
];
|
|
813
|
-
for (const regex of commandRegexes) {
|
|
814
|
-
for (const match of readmeContent.matchAll(regex)) {
|
|
815
|
-
references.add(match[1]);
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
|
-
return Array.from(references);
|
|
819
|
-
}
|
|
820
1060
|
function warning(id, severity, title, message, target) {
|
|
821
1061
|
return { id, severity, title, message, target };
|
|
822
1062
|
}
|
|
@@ -888,7 +1128,7 @@ async function runDoctor(root = process.cwd(), scan) {
|
|
|
888
1128
|
} else {
|
|
889
1129
|
const readme = await readIfPresent2(result.readme.path);
|
|
890
1130
|
if (readme !== null) {
|
|
891
|
-
const references =
|
|
1131
|
+
const references = extractScriptReferences(readme);
|
|
892
1132
|
const missingScripts = references.filter((script) => result.scripts[script] === void 0);
|
|
893
1133
|
if (missingScripts.length > 0) {
|
|
894
1134
|
warnings.push(
|
|
@@ -918,7 +1158,7 @@ async function runDoctor(root = process.cwd(), scan) {
|
|
|
918
1158
|
"docker-not-running",
|
|
919
1159
|
"warning",
|
|
920
1160
|
"Docker Compose found but Docker is not running",
|
|
921
|
-
"A compose file exists, but
|
|
1161
|
+
result.docker.message ?? "A compose file exists, but Docker is not available."
|
|
922
1162
|
)
|
|
923
1163
|
);
|
|
924
1164
|
}
|
|
@@ -942,18 +1182,15 @@ async function runDoctor(root = process.cwd(), scan) {
|
|
|
942
1182
|
)
|
|
943
1183
|
);
|
|
944
1184
|
}
|
|
945
|
-
if (!result.license.exists) {
|
|
946
|
-
warnings.push(warning("missing-license", "info", "No LICENSE", "No LICENSE file was found."));
|
|
947
|
-
}
|
|
948
1185
|
return warnings;
|
|
949
1186
|
}
|
|
950
1187
|
|
|
951
|
-
// src/
|
|
952
|
-
var
|
|
1188
|
+
// src/core/security/text.ts
|
|
1189
|
+
var ESC2 = String.fromCharCode(27);
|
|
953
1190
|
var BEL = String.fromCharCode(7);
|
|
954
|
-
var OSC_SEQUENCE = new RegExp(`${
|
|
955
|
-
var CSI_SEQUENCE = new RegExp(`${
|
|
956
|
-
var ESCAPE_SEQUENCE = new RegExp(`${
|
|
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");
|
|
957
1194
|
function stripControlCharacters(value) {
|
|
958
1195
|
let result = "";
|
|
959
1196
|
for (const character of value) {
|
|
@@ -964,13 +1201,13 @@ function stripControlCharacters(value) {
|
|
|
964
1201
|
}
|
|
965
1202
|
return result;
|
|
966
1203
|
}
|
|
967
|
-
function
|
|
1204
|
+
function safeDisplayText(value) {
|
|
968
1205
|
return stripControlCharacters(
|
|
969
1206
|
String(value).replace(OSC_SEQUENCE, "").replace(CSI_SEQUENCE, "").replace(ESCAPE_SEQUENCE, "")
|
|
970
1207
|
);
|
|
971
1208
|
}
|
|
972
|
-
function
|
|
973
|
-
return values.length > 0 ? values.map((value) =>
|
|
1209
|
+
function safeDisplayList(values) {
|
|
1210
|
+
return values.length > 0 ? values.map((value) => safeDisplayText(value)).join(", ") : "none";
|
|
974
1211
|
}
|
|
975
1212
|
|
|
976
1213
|
// src/cli/commands/doctor.ts
|
|
@@ -990,8 +1227,8 @@ async function doctorCommand(cwd = process.cwd()) {
|
|
|
990
1227
|
return;
|
|
991
1228
|
}
|
|
992
1229
|
for (const item of warnings) {
|
|
993
|
-
console.log(`${colorSeverity(item.severity)} ${pc.bold(
|
|
994
|
-
console.log(` ${
|
|
1230
|
+
console.log(`${colorSeverity(item.severity)} ${pc.bold(safeDisplayText(item.title))}`);
|
|
1231
|
+
console.log(` ${safeDisplayText(item.message)}`);
|
|
995
1232
|
}
|
|
996
1233
|
}
|
|
997
1234
|
|
|
@@ -1017,6 +1254,14 @@ import pc3 from "picocolors";
|
|
|
1017
1254
|
|
|
1018
1255
|
// src/core/process/runner.ts
|
|
1019
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
|
|
1020
1265
|
function getPackageRunCommand(packageManager, script) {
|
|
1021
1266
|
const manager = packageManager ?? "npm";
|
|
1022
1267
|
const args = ["run", script];
|
|
@@ -1077,6 +1322,95 @@ async function resolvePackageInstallCommand(options) {
|
|
|
1077
1322
|
command: executable
|
|
1078
1323
|
};
|
|
1079
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
|
+
}
|
|
1080
1414
|
async function runPackageScriptToTerminal(options) {
|
|
1081
1415
|
const runCommand2 = await resolvePackageRunCommand(options);
|
|
1082
1416
|
if (runCommand2 === null) {
|
|
@@ -1121,14 +1455,14 @@ async function runCommand(script, cwd = process.cwd()) {
|
|
|
1121
1455
|
// src/cli/commands/scan.ts
|
|
1122
1456
|
import pc4 from "picocolors";
|
|
1123
1457
|
function formatList(values) {
|
|
1124
|
-
return
|
|
1458
|
+
return safeDisplayList(values);
|
|
1125
1459
|
}
|
|
1126
1460
|
function printScanResult(scan) {
|
|
1127
|
-
console.log(pc4.bold(`Project: ${
|
|
1128
|
-
console.log(`Type: ${
|
|
1129
|
-
console.log(`Manager: ${
|
|
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")}`);
|
|
1130
1464
|
console.log(`Scripts: ${formatList(Object.keys(scan.scripts))}`);
|
|
1131
|
-
console.log(`Git: ${
|
|
1465
|
+
console.log(`Git: ${safeDisplayText(scan.git?.branch ?? "not detected")}`);
|
|
1132
1466
|
console.log(`README: ${scan.readme.exists ? "found" : "missing"}`);
|
|
1133
1467
|
console.log(`LICENSE: ${scan.license.exists ? "found" : "missing"}`);
|
|
1134
1468
|
if (scan.env !== null) {
|
|
@@ -1152,17 +1486,532 @@ async function scanCommand(cwd = process.cwd()) {
|
|
|
1152
1486
|
import pc5 from "picocolors";
|
|
1153
1487
|
|
|
1154
1488
|
// src/server/index.ts
|
|
1155
|
-
import { promises as
|
|
1156
|
-
import
|
|
1157
|
-
import { fileURLToPath } from "url";
|
|
1489
|
+
import { promises as fs17 } from "fs";
|
|
1490
|
+
import path13 from "path";
|
|
1491
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1158
1492
|
import { createAdaptorServer } from "@hono/node-server";
|
|
1159
1493
|
import { serveStatic } from "@hono/node-server/serve-static";
|
|
1160
1494
|
import { Hono } from "hono";
|
|
1161
|
-
|
|
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;
|
|
1162
2009
|
|
|
1163
2010
|
// src/core/process/manager.ts
|
|
1164
2011
|
import { EventEmitter } from "events";
|
|
1165
2012
|
import spawn3 from "cross-spawn";
|
|
2013
|
+
var LOG_MESSAGE_LIMIT = 16384;
|
|
2014
|
+
var LOG_ENTRY_LIMIT = 1e3;
|
|
1166
2015
|
function killChildProcessTree(child) {
|
|
1167
2016
|
if (child.pid === void 0) {
|
|
1168
2017
|
child.kill();
|
|
@@ -1187,7 +2036,7 @@ var ProcessManager = class extends EventEmitter {
|
|
|
1187
2036
|
start(options) {
|
|
1188
2037
|
const child = spawn3(options.command, options.args, {
|
|
1189
2038
|
cwd: options.cwd,
|
|
1190
|
-
shell:
|
|
2039
|
+
shell: false,
|
|
1191
2040
|
windowsHide: true
|
|
1192
2041
|
});
|
|
1193
2042
|
const pid = String(child.pid ?? `${Date.now()}-${Math.random().toString(16).slice(2)}`);
|
|
@@ -1270,16 +2119,17 @@ var ProcessManager = class extends EventEmitter {
|
|
|
1270
2119
|
});
|
|
1271
2120
|
}
|
|
1272
2121
|
emitLog(record, stream, message) {
|
|
2122
|
+
const boundedMessage = message.length <= LOG_MESSAGE_LIMIT ? message : message.slice(-LOG_MESSAGE_LIMIT);
|
|
1273
2123
|
const event = {
|
|
1274
2124
|
pid: record.pid,
|
|
1275
2125
|
script: record.script,
|
|
1276
2126
|
stream,
|
|
1277
|
-
message,
|
|
2127
|
+
message: boundedMessage,
|
|
1278
2128
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1279
2129
|
};
|
|
1280
2130
|
this.logs.push(event);
|
|
1281
|
-
if (this.logs.length >
|
|
1282
|
-
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);
|
|
1283
2133
|
}
|
|
1284
2134
|
this.emit("log", event);
|
|
1285
2135
|
}
|
|
@@ -1301,10 +2151,9 @@ var ProcessManager = class extends EventEmitter {
|
|
|
1301
2151
|
|
|
1302
2152
|
// src/server/routes/api.ts
|
|
1303
2153
|
import { constants as constants2, existsSync } from "fs";
|
|
1304
|
-
import { promises as
|
|
1305
|
-
import
|
|
2154
|
+
import { promises as fs16 } from "fs";
|
|
2155
|
+
import path12 from "path";
|
|
1306
2156
|
import spawn4 from "cross-spawn";
|
|
1307
|
-
import open from "open";
|
|
1308
2157
|
|
|
1309
2158
|
// src/server/localAccess.ts
|
|
1310
2159
|
var LOCAL_HOSTNAMES = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "::1"]);
|
|
@@ -1345,10 +2194,30 @@ function isSameOrigin(requestUrl, origin) {
|
|
|
1345
2194
|
}
|
|
1346
2195
|
}
|
|
1347
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
|
+
|
|
1348
2217
|
// src/server/routes/api.ts
|
|
1349
|
-
function
|
|
1350
|
-
const relative =
|
|
1351
|
-
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);
|
|
1352
2221
|
}
|
|
1353
2222
|
function isAllowedMutationOrigin(requestUrl, origin) {
|
|
1354
2223
|
if (origin === null) {
|
|
@@ -1363,35 +2232,35 @@ function hasMutationIntent(intent) {
|
|
|
1363
2232
|
return intent === "dashboard";
|
|
1364
2233
|
}
|
|
1365
2234
|
async function realPathWithinRoot(root, target) {
|
|
1366
|
-
if (!
|
|
2235
|
+
if (!isWithinRoot7(root, target)) {
|
|
1367
2236
|
return false;
|
|
1368
2237
|
}
|
|
1369
2238
|
try {
|
|
1370
|
-
const [realRoot, realTarget] = await Promise.all([
|
|
1371
|
-
return
|
|
2239
|
+
const [realRoot, realTarget] = await Promise.all([fs16.realpath(root), fs16.realpath(target)]);
|
|
2240
|
+
return isWithinRoot7(realRoot, realTarget);
|
|
1372
2241
|
} catch {
|
|
1373
2242
|
return false;
|
|
1374
2243
|
}
|
|
1375
2244
|
}
|
|
1376
2245
|
async function writableDestinationWithinRoot(root, destination) {
|
|
1377
|
-
if (!
|
|
2246
|
+
if (!isWithinRoot7(root, destination)) {
|
|
1378
2247
|
return false;
|
|
1379
2248
|
}
|
|
1380
2249
|
try {
|
|
1381
2250
|
const [realRoot, realParent] = await Promise.all([
|
|
1382
|
-
|
|
1383
|
-
|
|
2251
|
+
fs16.realpath(root),
|
|
2252
|
+
fs16.realpath(path12.dirname(destination))
|
|
1384
2253
|
]);
|
|
1385
|
-
return
|
|
2254
|
+
return isWithinRoot7(realRoot, realParent);
|
|
1386
2255
|
} catch {
|
|
1387
2256
|
return false;
|
|
1388
2257
|
}
|
|
1389
2258
|
}
|
|
1390
2259
|
async function copyFileExclusive(source, destination) {
|
|
1391
|
-
const content = await
|
|
2260
|
+
const content = await fs16.readFile(source);
|
|
1392
2261
|
let handle2 = null;
|
|
1393
2262
|
try {
|
|
1394
|
-
handle2 = await
|
|
2263
|
+
handle2 = await fs16.open(
|
|
1395
2264
|
destination,
|
|
1396
2265
|
constants2.O_CREAT | constants2.O_EXCL | constants2.O_WRONLY,
|
|
1397
2266
|
384
|
|
@@ -1418,15 +2287,15 @@ function resolveCommandPromptExecutable() {
|
|
|
1418
2287
|
return process.env.ComSpec ?? "cmd.exe";
|
|
1419
2288
|
}
|
|
1420
2289
|
function findExecutable(command) {
|
|
1421
|
-
if (
|
|
2290
|
+
if (path12.isAbsolute(command)) {
|
|
1422
2291
|
return existsSync(command) ? command : null;
|
|
1423
2292
|
}
|
|
1424
2293
|
const pathValue = process.env.PATH ?? "";
|
|
1425
|
-
for (const directory of pathValue.split(
|
|
2294
|
+
for (const directory of pathValue.split(path12.delimiter)) {
|
|
1426
2295
|
if (directory.length === 0) {
|
|
1427
2296
|
continue;
|
|
1428
2297
|
}
|
|
1429
|
-
const candidate =
|
|
2298
|
+
const candidate = path12.join(directory, command);
|
|
1430
2299
|
if (existsSync(candidate)) {
|
|
1431
2300
|
return candidate;
|
|
1432
2301
|
}
|
|
@@ -1467,8 +2336,8 @@ function openTerminalAt(root) {
|
|
|
1467
2336
|
if (process.platform === "darwin") {
|
|
1468
2337
|
return launchDetached("open", ["-a", "Terminal", root], root);
|
|
1469
2338
|
}
|
|
1470
|
-
const configuredTerminal = process.env.TERMINAL;
|
|
1471
|
-
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) {
|
|
1472
2341
|
return launchDetached(configuredTerminal, [], root);
|
|
1473
2342
|
}
|
|
1474
2343
|
const linuxTerminals = [
|
|
@@ -1493,6 +2362,10 @@ function openTerminalAt(root) {
|
|
|
1493
2362
|
return launchDetached(terminal.command, terminal.args, root);
|
|
1494
2363
|
}
|
|
1495
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
|
+
});
|
|
1496
2369
|
app.use("/api/*", async (context, next) => {
|
|
1497
2370
|
const host = context.req.header("host") ?? new URL(context.req.url).host;
|
|
1498
2371
|
if (!isAllowedLocalHostHeader(host)) {
|
|
@@ -1502,7 +2375,8 @@ function registerApiRoutes(app, options) {
|
|
|
1502
2375
|
const origin = context.req.header("origin") ?? null;
|
|
1503
2376
|
const secFetchSite = context.req.header("sec-fetch-site") ?? null;
|
|
1504
2377
|
const intent = context.req.header("x-devsurface-intent") ?? null;
|
|
1505
|
-
|
|
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)) {
|
|
1506
2380
|
return context.json({ error: "Cross-origin mutation rejected." }, 403);
|
|
1507
2381
|
}
|
|
1508
2382
|
}
|
|
@@ -1520,6 +2394,57 @@ function registerApiRoutes(app, options) {
|
|
|
1520
2394
|
app.get("/api/logs", (context) => {
|
|
1521
2395
|
return context.json(options.processManager.listLogs());
|
|
1522
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
|
+
});
|
|
1523
2448
|
app.post("/api/run/:script", async (context) => {
|
|
1524
2449
|
const script = decodeURIComponent(context.req.param("script"));
|
|
1525
2450
|
const scan = await scanProject(options.projectRoot);
|
|
@@ -1527,6 +2452,9 @@ function registerApiRoutes(app, options) {
|
|
|
1527
2452
|
if (packageScript === void 0) {
|
|
1528
2453
|
return context.json({ error: `Script "${script}" was not found.` }, 404);
|
|
1529
2454
|
}
|
|
2455
|
+
if (isDangerousCommand(packageScript)) {
|
|
2456
|
+
return context.json({ error: "Refusing to run dangerous script." }, 403);
|
|
2457
|
+
}
|
|
1530
2458
|
const command = await resolvePackageRunCommand({
|
|
1531
2459
|
cwd: options.projectRoot,
|
|
1532
2460
|
packageManager: scan.packageManager,
|
|
@@ -1572,13 +2500,24 @@ function registerApiRoutes(app, options) {
|
|
|
1572
2500
|
if (configuredCommand === null) {
|
|
1573
2501
|
return context.json({ error: `Configured command "${name}" was not found.` }, 404);
|
|
1574
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
|
+
}
|
|
1575
2515
|
const processInfo = options.processManager.start({
|
|
1576
2516
|
cwd: options.projectRoot,
|
|
1577
2517
|
script: name,
|
|
1578
|
-
command:
|
|
1579
|
-
args:
|
|
1580
|
-
displayCommand:
|
|
1581
|
-
shell: true
|
|
2518
|
+
command: resolvedCommand.command,
|
|
2519
|
+
args: resolvedCommand.args,
|
|
2520
|
+
displayCommand: resolvedCommand.displayCommand
|
|
1582
2521
|
});
|
|
1583
2522
|
return context.json({
|
|
1584
2523
|
...processInfo,
|
|
@@ -1586,15 +2525,15 @@ function registerApiRoutes(app, options) {
|
|
|
1586
2525
|
});
|
|
1587
2526
|
});
|
|
1588
2527
|
app.post("/api/open/folder", async (context) => {
|
|
1589
|
-
await
|
|
2528
|
+
await open_default(options.projectRoot);
|
|
1590
2529
|
return context.json({ opened: true, target: "folder" });
|
|
1591
2530
|
});
|
|
1592
2531
|
app.post("/api/open/package", async (context) => {
|
|
1593
|
-
const packagePath =
|
|
2532
|
+
const packagePath = path12.join(options.projectRoot, "package.json");
|
|
1594
2533
|
if (!await realPathWithinRoot(options.projectRoot, packagePath)) {
|
|
1595
2534
|
return context.json({ error: "package.json was not found inside the project root." }, 404);
|
|
1596
2535
|
}
|
|
1597
|
-
await
|
|
2536
|
+
await open_default(packagePath);
|
|
1598
2537
|
return context.json({ opened: true, target: "package" });
|
|
1599
2538
|
});
|
|
1600
2539
|
app.post("/api/open/terminal", (context) => {
|
|
@@ -1608,7 +2547,7 @@ function registerApiRoutes(app, options) {
|
|
|
1608
2547
|
if (examplePath === null) {
|
|
1609
2548
|
return context.json({ error: ".env.example was not found." }, 404);
|
|
1610
2549
|
}
|
|
1611
|
-
const destination = localPath ??
|
|
2550
|
+
const destination = localPath ?? path12.join(options.projectRoot, scan.config?.config.env?.local ?? ".env");
|
|
1612
2551
|
if (!await realPathWithinRoot(options.projectRoot, examplePath) || !await writableDestinationWithinRoot(options.projectRoot, destination)) {
|
|
1613
2552
|
return context.json({ error: "Refusing to copy env files outside the project root." }, 400);
|
|
1614
2553
|
}
|
|
@@ -1689,23 +2628,23 @@ function assertLocalHost(host) {
|
|
|
1689
2628
|
throw new Error("DevSurface must bind only to 127.0.0.1.");
|
|
1690
2629
|
}
|
|
1691
2630
|
}
|
|
1692
|
-
async function
|
|
2631
|
+
async function fileExists(filePath) {
|
|
1693
2632
|
try {
|
|
1694
|
-
await
|
|
2633
|
+
await fs17.access(filePath);
|
|
1695
2634
|
return true;
|
|
1696
2635
|
} catch {
|
|
1697
2636
|
return false;
|
|
1698
2637
|
}
|
|
1699
2638
|
}
|
|
1700
2639
|
async function findWebDistDir() {
|
|
1701
|
-
const moduleDir =
|
|
2640
|
+
const moduleDir = path13.dirname(fileURLToPath2(import.meta.url));
|
|
1702
2641
|
const candidates = [
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
2642
|
+
path13.join(moduleDir, "..", "web", "dist"),
|
|
2643
|
+
path13.join(moduleDir, "..", "..", "src", "web", "dist"),
|
|
2644
|
+
path13.join(moduleDir, "web", "dist")
|
|
1706
2645
|
];
|
|
1707
2646
|
for (const candidate of candidates) {
|
|
1708
|
-
if (await
|
|
2647
|
+
if (await fileExists(path13.join(candidate, "index.html"))) {
|
|
1709
2648
|
return candidate;
|
|
1710
2649
|
}
|
|
1711
2650
|
}
|
|
@@ -1777,13 +2716,16 @@ async function closeHttpServer(server) {
|
|
|
1777
2716
|
}
|
|
1778
2717
|
async function createApp(options) {
|
|
1779
2718
|
const app = new Hono();
|
|
1780
|
-
registerApiRoutes(app,
|
|
2719
|
+
registerApiRoutes(app, {
|
|
2720
|
+
...options,
|
|
2721
|
+
mutationToken: options.mutationToken ?? createMutationToken()
|
|
2722
|
+
});
|
|
1781
2723
|
const webDistDir = await findWebDistDir();
|
|
1782
2724
|
if (webDistDir !== null) {
|
|
1783
2725
|
app.use("/assets/*", serveStatic({ root: webDistDir }));
|
|
1784
2726
|
app.get("/favicon.svg", serveStatic({ root: webDistDir }));
|
|
1785
2727
|
app.get("*", async (context) => {
|
|
1786
|
-
const html = await
|
|
2728
|
+
const html = await fs17.readFile(path13.join(webDistDir, "index.html"), "utf8");
|
|
1787
2729
|
return context.html(html);
|
|
1788
2730
|
});
|
|
1789
2731
|
} else {
|
|
@@ -1815,7 +2757,7 @@ async function startDevSurfaceServer(options) {
|
|
|
1815
2757
|
processManager.attachCleanupHandlers();
|
|
1816
2758
|
const url = `http://${HOST}:${port}`;
|
|
1817
2759
|
if (options.openBrowser !== false) {
|
|
1818
|
-
await
|
|
2760
|
+
await open_default(url);
|
|
1819
2761
|
}
|
|
1820
2762
|
return {
|
|
1821
2763
|
url,
|
|
@@ -1829,10 +2771,13 @@ async function startDevSurfaceServer(options) {
|
|
|
1829
2771
|
};
|
|
1830
2772
|
}
|
|
1831
2773
|
|
|
2774
|
+
// src/version.ts
|
|
2775
|
+
var DEV_SURFACE_VERSION = "0.3.0";
|
|
2776
|
+
|
|
1832
2777
|
// src/cli/commands/start.ts
|
|
1833
2778
|
async function startCommand(options) {
|
|
1834
2779
|
const cwd = options.cwd ?? process.cwd();
|
|
1835
|
-
console.log(pc5.bold(`DevSurface
|
|
2780
|
+
console.log(pc5.bold(`DevSurface v${DEV_SURFACE_VERSION}`));
|
|
1836
2781
|
console.log("Scanning project...\n");
|
|
1837
2782
|
const scan = await scanProject(cwd);
|
|
1838
2783
|
printScanResult(scan);
|
|
@@ -1869,7 +2814,7 @@ function handle(command) {
|
|
|
1869
2814
|
process.exitCode = 1;
|
|
1870
2815
|
});
|
|
1871
2816
|
}
|
|
1872
|
-
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) => {
|
|
1873
2818
|
handle(
|
|
1874
2819
|
startCommand({
|
|
1875
2820
|
cwd: process.cwd(),
|