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/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: typeof raw.docs === "string" ? raw.docs : void 0
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 content = await fs.readFile(configPath, "utf8");
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: configPath, config, warnings };
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/scanner/docker.ts
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 isWithinRoot(root, target) {
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 (isWithinRoot(root, candidate)) {
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 (isWithinRoot(realRoot, realCandidate)) {
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 (isWithinRoot(root, directory)) {
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/scanner/docker.ts
282
+ // src/core/docker/compose.ts
233
283
  var COMPOSE_FILES = ["docker-compose.yml", "docker-compose.yaml", "compose.yml", "compose.yaml"];
234
- async function fileExists(filePath) {
235
- try {
236
- await fs3.access(filePath);
237
- return true;
238
- } catch {
239
- return false;
240
- }
241
- }
242
- async function getComposeFiles(root) {
243
- const checks = await Promise.all(
244
- COMPOSE_FILES.map(async (file) => {
245
- const filePath = path3.join(root, file);
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) => !isWithinRoot2(root, candidate)) ?? os.homedir();
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 function runDockerCommand(root, args, timeoutMs = 2500) {
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 timeout = setTimeout(() => {
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
- resolve({ code: null, stdout, stderr });
332
+ finish({ code: null, stdout, stderr, error: "timeout" });
288
333
  }, timeoutMs);
289
334
  child.stdout?.on("data", (chunk) => {
290
- stdout += chunk.toString();
335
+ stdout = appendBounded(stdout, chunk.toString(), outputLimit);
291
336
  });
292
337
  child.stderr?.on("data", (chunk) => {
293
- stderr += chunk.toString();
338
+ stderr = appendBounded(stderr, chunk.toString(), outputLimit);
294
339
  });
295
340
  child.on("error", () => {
296
- clearTimeout(timeout);
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
- clearTimeout(timeout);
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 isDockerRunning(root) {
312
- const result = await runDockerCommand(root, ["info"]);
313
- return result.code === 0;
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 rows2 = Array.isArray(parsed) ? parsed : [parsed];
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 rows = compactOutput.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
331
- for (const row of rows) {
483
+ for (const line of compactOutput.split(/\r?\n/)) {
332
484
  try {
333
- const parsed = JSON.parse(row);
334
- addComposeStatusRow(statuses, parsed);
485
+ addComposeStatusRow(statuses, JSON.parse(line));
335
486
  } catch {
336
- return statuses;
487
+ return /* @__PURE__ */ new Map();
337
488
  }
338
489
  }
339
490
  return statuses;
340
491
  }
341
- function addComposeStatusRow(statuses, row) {
342
- if (typeof row !== "object" || row === null) {
343
- return;
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
- const record = row;
346
- if (typeof record.Service !== "string") {
347
- return;
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
- const state = typeof record.State === "string" ? record.State.toLowerCase() : "";
350
- statuses.set(record.Service, {
351
- name: record.Service,
352
- status: state === "running" ? "running" : state ? "stopped" : "unknown",
353
- containerId: typeof record.ID === "string" && record.ID.length > 0 ? record.ID : null
354
- });
355
- }
356
- async function getServiceStatuses(root, serviceNames, dockerRunning) {
357
- if (serviceNames.length === 0) {
358
- return [];
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
- if (!dockerRunning) {
361
- return serviceNames.map((service) => ({
362
- name: service,
363
- status: "unknown",
364
- containerId: null
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
- const ps = await runDockerCommand(root, [
368
- "compose",
369
- "--project-directory",
370
- root,
371
- "ps",
372
- "--format",
373
- "json"
374
- ]);
375
- const statuses = ps.code === 0 ? parseComposePs(ps.stdout) : /* @__PURE__ */ new Map();
376
- return serviceNames.map(
377
- (service) => statuses.get(service) ?? {
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
- async function detectDocker(root) {
385
- const composeFiles = await getComposeFiles(root);
386
- if (composeFiles.length === 0) {
387
- return null;
612
+ return {
613
+ service,
614
+ action,
615
+ output: cleanMessage(result.stdout || result.stderr)
616
+ };
388
617
  }
389
- const serviceLists = await Promise.all(
390
- composeFiles.map((composeFile) => extractServices(composeFile))
391
- );
392
- const serviceNames = Array.from(new Set(serviceLists.flat()));
393
- const dockerRunning = await isDockerRunning(root);
394
- return {
395
- composeFiles,
396
- services: await getServiceStatuses(root, serviceNames, dockerRunning),
397
- dockerRunning
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 isWithinRoot3(root, target) {
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 (!isWithinRoot3(root, resolvedGitDir)) {
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 isWithinRoot3(realRoot, realGitDir) ? resolvedGitDir : null;
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 isWithinRoot4(root, target) {
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 (!isWithinRoot4(realRoot, realPackageJsonPath)) {
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: filePath, exists: true };
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 config = await loadConfig(root);
748
- const packageJson = await readPackageJson(root);
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(root),
758
- detectEnv(root, config?.config),
759
- detectDocker(root),
760
- detectGit(root),
1016
+ detectPackageManager(resolvedRoot),
1017
+ detectEnv(resolvedRoot, config?.config),
1018
+ detectDocker(resolvedRoot),
1019
+ detectGit(resolvedRoot),
761
1020
  detectPorts(portsToProbe),
762
- findFirstFile(root, ["README.md", "README"]),
763
- findFirstFile(root, ["LICENSE", "LICENSE.md", "COPYING"])
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(root),
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 = extractReadmeScriptReferences(readme);
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 the Docker daemon did not answer docker info."
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/cli/terminal.ts
952
- var ESC = String.fromCharCode(27);
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(`${ESC}\\][\\s\\S]*?(?:${BEL}|${ESC}\\\\)`, "g");
955
- var CSI_SEQUENCE = new RegExp(`${ESC}\\[[0-?]*[ -/]*[@-~]`, "g");
956
- var ESCAPE_SEQUENCE = new RegExp(`${ESC}[@-Z\\\\-_]`, "g");
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 safeTerminalText(value) {
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 safeTerminalList(values) {
973
- return values.length > 0 ? values.map((value) => safeTerminalText(value)).join(", ") : "none";
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(safeTerminalText(item.title))}`);
994
- console.log(` ${safeTerminalText(item.message)}`);
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 safeTerminalList(values);
1458
+ return safeDisplayList(values);
1125
1459
  }
1126
1460
  function printScanResult(scan) {
1127
- console.log(pc4.bold(`Project: ${safeTerminalText(scan.projectName)}`));
1128
- console.log(`Type: ${safeTerminalText(scan.framework?.type ?? "Unknown")}`);
1129
- console.log(`Manager: ${safeTerminalText(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")}`);
1130
1464
  console.log(`Scripts: ${formatList(Object.keys(scan.scripts))}`);
1131
- console.log(`Git: ${safeTerminalText(scan.git?.branch ?? "not detected")}`);
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 fs12 } from "fs";
1156
- import path12 from "path";
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
- import open2 from "open";
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: options.shell ?? false,
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 > 1e3) {
1282
- this.logs.splice(0, this.logs.length - 1e3);
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 fs11 } from "fs";
1305
- import path11 from "path";
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 isWithinRoot5(root, target) {
1350
- const relative = path11.relative(path11.resolve(root), path11.resolve(target));
1351
- return relative === "" || !relative.startsWith("..") && !path11.isAbsolute(relative);
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 (!isWithinRoot5(root, target)) {
2235
+ if (!isWithinRoot7(root, target)) {
1367
2236
  return false;
1368
2237
  }
1369
2238
  try {
1370
- const [realRoot, realTarget] = await Promise.all([fs11.realpath(root), fs11.realpath(target)]);
1371
- return isWithinRoot5(realRoot, realTarget);
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 (!isWithinRoot5(root, destination)) {
2246
+ if (!isWithinRoot7(root, destination)) {
1378
2247
  return false;
1379
2248
  }
1380
2249
  try {
1381
2250
  const [realRoot, realParent] = await Promise.all([
1382
- fs11.realpath(root),
1383
- fs11.realpath(path11.dirname(destination))
2251
+ fs16.realpath(root),
2252
+ fs16.realpath(path12.dirname(destination))
1384
2253
  ]);
1385
- return isWithinRoot5(realRoot, realParent);
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 fs11.readFile(source);
2260
+ const content = await fs16.readFile(source);
1392
2261
  let handle2 = null;
1393
2262
  try {
1394
- handle2 = await fs11.open(
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 (path11.isAbsolute(command)) {
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(path11.delimiter)) {
2294
+ for (const directory of pathValue.split(path12.delimiter)) {
1426
2295
  if (directory.length === 0) {
1427
2296
  continue;
1428
2297
  }
1429
- const candidate = path11.join(directory, command);
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
- if (!hasMutationIntent(intent) || isCrossSiteFetch(secFetchSite) || !isAllowedMutationOrigin(context.req.url, origin)) {
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: configuredCommand,
1579
- args: [],
1580
- displayCommand: configuredCommand,
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 open(options.projectRoot);
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 = path11.join(options.projectRoot, "package.json");
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 open(packagePath);
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 ?? path11.join(options.projectRoot, scan.config?.config.env?.local ?? ".env");
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 fileExists2(filePath) {
2631
+ async function fileExists(filePath) {
1693
2632
  try {
1694
- await fs12.access(filePath);
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 = path12.dirname(fileURLToPath(import.meta.url));
2640
+ const moduleDir = path13.dirname(fileURLToPath2(import.meta.url));
1702
2641
  const candidates = [
1703
- path12.join(moduleDir, "..", "web", "dist"),
1704
- path12.join(moduleDir, "..", "..", "src", "web", "dist"),
1705
- path12.join(moduleDir, "web", "dist")
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 fileExists2(path12.join(candidate, "index.html"))) {
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, options);
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 fs12.readFile(path12.join(webDistDir, "index.html"), "utf8");
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 open2(url);
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 v0.2.0`));
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("0.2.0").option("-p, --port <port>", "dashboard port", toPort, 4567).option("--no-open", "do not open the browser automatically").action((options) => {
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(),