@miosa/cli 1.0.37 → 1.0.38
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/bin/miosa.js +0 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +5 -2
- package/dist/client.js.map +1 -1
- package/dist/commands/cp.d.ts.map +1 -1
- package/dist/commands/cp.js +9 -0
- package/dist/commands/cp.js.map +1 -1
- package/dist/commands/enterprise-util.d.ts.map +1 -1
- package/dist/commands/enterprise-util.js +12 -1
- package/dist/commands/enterprise-util.js.map +1 -1
- package/dist/commands/ls.d.ts.map +1 -1
- package/dist/commands/ls.js +5 -0
- package/dist/commands/ls.js.map +1 -1
- package/dist/commands/sandbox.d.ts.map +1 -1
- package/dist/commands/sandbox.js +240 -20
- package/dist/commands/sandbox.js.map +1 -1
- package/package.json +1 -1
- package/dist/commands/machines.d.ts +0 -3
- package/dist/commands/machines.d.ts.map +0 -1
- package/dist/commands/machines.js +0 -29
- package/dist/commands/machines.js.map +0 -1
- package/dist/miosa-linux-x64 +0 -0
package/dist/commands/sandbox.js
CHANGED
|
@@ -372,12 +372,16 @@ export function register(program) {
|
|
|
372
372
|
// exec — positional command arg; --data body overrides when supplied
|
|
373
373
|
addDataOption(sandbox
|
|
374
374
|
.command("exec <sandbox-id> [command...]")
|
|
375
|
-
.description(
|
|
375
|
+
.description('Run a command inside a Sandbox (positional args joined as shell command). Use `--` before the command to pass flags, e.g. `sandbox exec <id> -- bash -c "cd x && y"`.')
|
|
376
|
+
.allowUnknownOption()
|
|
377
|
+
.allowExcessArguments()
|
|
376
378
|
.option("--cwd <path>", "Working directory inside the Sandbox")
|
|
377
379
|
.option("--workdir <path>", "Alias for --cwd")
|
|
378
380
|
.option("--env <pair>", "Environment variable KEY=VALUE. Repeatable.", collectOption, [])
|
|
379
381
|
.option("--background", "Start the command in the background and return immediately")
|
|
380
382
|
.option("--detached", "Create a durable backend command and return command_id immediately")
|
|
383
|
+
.option("--follow", "Stream command output until it exits (alias --stream)")
|
|
384
|
+
.option("--stream", "Alias for --follow")
|
|
381
385
|
.option("--user <user>", "Run command as user")
|
|
382
386
|
.option("--sudo", "Run command through sudo")
|
|
383
387
|
.option("--tty", "Request TTY metadata for command resource")
|
|
@@ -385,11 +389,12 @@ export function register(program) {
|
|
|
385
389
|
.option("--timeout <sec>", "Exec timeout in seconds", parseIntegerOption))
|
|
386
390
|
.option("--json", "Output as JSON")
|
|
387
391
|
.action((id, words, opts) => runAction(async () => {
|
|
392
|
+
opts.follow = opts.follow || opts.stream;
|
|
388
393
|
if (opts.data) {
|
|
389
394
|
await postAndPrint(`/sandboxes/${enc(id)}/exec`, opts, {});
|
|
390
395
|
return;
|
|
391
396
|
}
|
|
392
|
-
const cmd = words
|
|
397
|
+
const cmd = joinCommandWords(words);
|
|
393
398
|
const effectiveCommand = opts.background
|
|
394
399
|
? backgroundCommand(cmd)
|
|
395
400
|
: cmd;
|
|
@@ -403,7 +408,7 @@ export function register(program) {
|
|
|
403
408
|
}
|
|
404
409
|
const env = parseEnvPairs(opts.env ?? []);
|
|
405
410
|
if (opts.detached) {
|
|
406
|
-
const result = await createSandboxCommand(id,
|
|
411
|
+
const result = await createSandboxCommand(id, words.join(" "), {
|
|
407
412
|
cwd,
|
|
408
413
|
env,
|
|
409
414
|
user: opts.user,
|
|
@@ -419,6 +424,10 @@ export function register(program) {
|
|
|
419
424
|
console.log(String(result["id"] ?? result["command_id"] ?? ""));
|
|
420
425
|
return;
|
|
421
426
|
}
|
|
427
|
+
if (opts.follow) {
|
|
428
|
+
await runFollowExec(id, cmd, cwd, env, opts.timeout, opts);
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
422
431
|
if (Object.keys(env).length > 0)
|
|
423
432
|
body["env"] = env;
|
|
424
433
|
if (opts.timeout != null)
|
|
@@ -428,12 +437,16 @@ export function register(program) {
|
|
|
428
437
|
// run — alias for exec with identical semantics
|
|
429
438
|
addDataOption(sandbox
|
|
430
439
|
.command("run <sandbox-id> [command...]")
|
|
431
|
-
.description("Run a command inside a Sandbox (alias for exec)")
|
|
440
|
+
.description("Run a command inside a Sandbox (alias for exec). Use `--` before the command to pass flags.")
|
|
441
|
+
.allowUnknownOption()
|
|
442
|
+
.allowExcessArguments()
|
|
432
443
|
.option("--cwd <path>", "Working directory inside the Sandbox")
|
|
433
444
|
.option("--workdir <path>", "Alias for --cwd")
|
|
434
445
|
.option("--env <pair>", "Environment variable KEY=VALUE. Repeatable.", collectOption, [])
|
|
435
446
|
.option("--background", "Start the command in the background and return immediately")
|
|
436
447
|
.option("--detached", "Create a durable backend command and return command_id immediately")
|
|
448
|
+
.option("--follow", "Stream command output until it exits (alias --stream)")
|
|
449
|
+
.option("--stream", "Alias for --follow")
|
|
437
450
|
.option("--user <user>", "Run command as user")
|
|
438
451
|
.option("--sudo", "Run command through sudo")
|
|
439
452
|
.option("--tty", "Request TTY metadata for command resource")
|
|
@@ -441,11 +454,12 @@ export function register(program) {
|
|
|
441
454
|
.option("--timeout <sec>", "Exec timeout in seconds", parseIntegerOption))
|
|
442
455
|
.option("--json", "Output as JSON")
|
|
443
456
|
.action((id, words, opts) => runAction(async () => {
|
|
457
|
+
opts.follow = opts.follow || opts.stream;
|
|
444
458
|
if (opts.data) {
|
|
445
459
|
await postAndPrint(`/sandboxes/${enc(id)}/exec`, opts, {});
|
|
446
460
|
return;
|
|
447
461
|
}
|
|
448
|
-
const cmd = words
|
|
462
|
+
const cmd = joinCommandWords(words);
|
|
449
463
|
const effectiveCommand = opts.background
|
|
450
464
|
? backgroundCommand(cmd)
|
|
451
465
|
: cmd;
|
|
@@ -459,7 +473,7 @@ export function register(program) {
|
|
|
459
473
|
}
|
|
460
474
|
const env = parseEnvPairs(opts.env ?? []);
|
|
461
475
|
if (opts.detached) {
|
|
462
|
-
const result = await createSandboxCommand(id,
|
|
476
|
+
const result = await createSandboxCommand(id, words.join(" "), {
|
|
463
477
|
cwd,
|
|
464
478
|
env,
|
|
465
479
|
user: opts.user,
|
|
@@ -475,12 +489,46 @@ export function register(program) {
|
|
|
475
489
|
console.log(String(result["id"] ?? result["command_id"] ?? ""));
|
|
476
490
|
return;
|
|
477
491
|
}
|
|
492
|
+
if (opts.follow) {
|
|
493
|
+
await runFollowExec(id, cmd, cwd, env, opts.timeout, opts);
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
478
496
|
if (Object.keys(env).length > 0)
|
|
479
497
|
body["env"] = env;
|
|
480
498
|
if (opts.timeout != null)
|
|
481
499
|
body["timeout"] = opts.timeout;
|
|
482
500
|
await postAndPrint(`/sandboxes/${enc(id)}/exec`, opts, body);
|
|
483
501
|
}));
|
|
502
|
+
sandbox
|
|
503
|
+
.command("ports <sandbox-id>")
|
|
504
|
+
.description("List listening TCP ports inside a Sandbox")
|
|
505
|
+
.option("--json", "Output as JSON")
|
|
506
|
+
.action((id, opts) => runAction(async () => {
|
|
507
|
+
const c = client();
|
|
508
|
+
const result = await execSandboxRaw(c, id, "ss -ltnH 2>/dev/null || cat /proc/net/tcp /proc/net/tcp6 2>/dev/null");
|
|
509
|
+
const stdout = String(result["stdout"] ?? "");
|
|
510
|
+
const ports = parseListeningPorts(stdout);
|
|
511
|
+
if (isJsonMode(opts)) {
|
|
512
|
+
console.log(JSON.stringify(ports, null, 2));
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
if (ports.length === 0) {
|
|
516
|
+
console.log(chalk.dim("(no listening ports)"));
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
renderTable(ports, [
|
|
520
|
+
{
|
|
521
|
+
header: "PORT",
|
|
522
|
+
key: "port",
|
|
523
|
+
width: 8,
|
|
524
|
+
},
|
|
525
|
+
{
|
|
526
|
+
header: "ADDRESS",
|
|
527
|
+
key: "address",
|
|
528
|
+
width: 24,
|
|
529
|
+
},
|
|
530
|
+
]);
|
|
531
|
+
}));
|
|
484
532
|
sandbox
|
|
485
533
|
.command("deploy [local-dir]")
|
|
486
534
|
.description("Upload an app directory, start it in a sandbox, expose a preview URL, and wait for readiness")
|
|
@@ -595,7 +643,8 @@ export function register(program) {
|
|
|
595
643
|
.option("--database <mode>", "none, create:postgres, postgres, or existing:<db-id>")
|
|
596
644
|
.option("--port <port>", "Runtime port for dynamic deployments", parseIntegerOption)
|
|
597
645
|
.option("--wait", "Wait for the production URL to answer")
|
|
598
|
-
.option("--
|
|
646
|
+
.option("--no-wait", "Return immediately without waiting (this is the default)")
|
|
647
|
+
.option("--timeout <duration>", "Wait timeout", parseDurationSec, 600)
|
|
599
648
|
.option("--json", "Output as JSON")
|
|
600
649
|
.action((id, opts) => runAction(async () => {
|
|
601
650
|
const result = await publishSandbox(id, opts);
|
|
@@ -740,15 +789,29 @@ export function register(program) {
|
|
|
740
789
|
console.log(JSON.stringify(result, null, 2));
|
|
741
790
|
return;
|
|
742
791
|
}
|
|
743
|
-
const rows = Array.isArray(result)
|
|
792
|
+
const rows = Array.isArray(result)
|
|
793
|
+
? result
|
|
794
|
+
: [];
|
|
744
795
|
if (rows.length === 0) {
|
|
745
796
|
console.log(chalk.dim("No sandbox env vars."));
|
|
746
797
|
return;
|
|
747
798
|
}
|
|
748
799
|
renderTable(rows, [
|
|
749
|
-
{
|
|
750
|
-
|
|
751
|
-
|
|
800
|
+
{
|
|
801
|
+
header: "NAME",
|
|
802
|
+
key: "name",
|
|
803
|
+
width: 32,
|
|
804
|
+
},
|
|
805
|
+
{
|
|
806
|
+
header: "VALUE",
|
|
807
|
+
key: "preview",
|
|
808
|
+
width: 24,
|
|
809
|
+
},
|
|
810
|
+
{
|
|
811
|
+
header: "UPDATED",
|
|
812
|
+
key: "updated_at",
|
|
813
|
+
width: 28,
|
|
814
|
+
},
|
|
752
815
|
]);
|
|
753
816
|
}));
|
|
754
817
|
env
|
|
@@ -1554,13 +1617,20 @@ function validateServiceName(name) {
|
|
|
1554
1617
|
async function deploySandbox(localDir, opts) {
|
|
1555
1618
|
const sourceDir = path.resolve(localDir);
|
|
1556
1619
|
const sourceBacked = !!opts.source;
|
|
1557
|
-
if (!sourceBacked &&
|
|
1620
|
+
if (!sourceBacked &&
|
|
1621
|
+
(!fs.existsSync(sourceDir) || !fs.statSync(sourceDir).isDirectory())) {
|
|
1558
1622
|
throw new UserError(`Local directory not found: ${sourceDir}`);
|
|
1559
1623
|
}
|
|
1560
1624
|
const c = client();
|
|
1561
|
-
let appManifest = sourceBacked
|
|
1625
|
+
let appManifest = sourceBacked
|
|
1626
|
+
? null
|
|
1627
|
+
: (loadAppManifest(sourceDir)?.manifest ?? null);
|
|
1562
1628
|
const detection = sourceBacked ? null : detectFramework(sourceDir);
|
|
1563
|
-
const port = opts.port ??
|
|
1629
|
+
const port = opts.port ??
|
|
1630
|
+
opts.publishPort ??
|
|
1631
|
+
manifestPort(appManifest) ??
|
|
1632
|
+
detection?.port ??
|
|
1633
|
+
5173;
|
|
1564
1634
|
const probePath = opts.probePath ?? manifestProbePath(appManifest) ?? "/";
|
|
1565
1635
|
let remoteWorkdir = normalizeRemoteWorkdir(appManifest?.workdir ?? "/workspace");
|
|
1566
1636
|
const start = opts.start ??
|
|
@@ -1595,9 +1665,7 @@ async function deploySandbox(localDir, opts) {
|
|
|
1595
1665
|
}
|
|
1596
1666
|
const resolvedPort = opts.port ?? opts.publishPort ?? manifestPort(appManifest) ?? port;
|
|
1597
1667
|
const resolvedProbePath = opts.probePath ?? manifestProbePath(appManifest) ?? probePath;
|
|
1598
|
-
const resolvedStart = opts.start ??
|
|
1599
|
-
manifestStartCommand(appManifest) ??
|
|
1600
|
-
start;
|
|
1668
|
+
const resolvedStart = opts.start ?? manifestStartCommand(appManifest) ?? start;
|
|
1601
1669
|
const installCommand = opts.install === false
|
|
1602
1670
|
? null
|
|
1603
1671
|
: (opts.installCommand ??
|
|
@@ -1632,6 +1700,41 @@ async function deploySandbox(localDir, opts) {
|
|
|
1632
1700
|
latency_ms: edge.latency_ms ?? null,
|
|
1633
1701
|
};
|
|
1634
1702
|
}
|
|
1703
|
+
const NON_TERMINAL_DEPLOY_STATES = new Set([
|
|
1704
|
+
"building",
|
|
1705
|
+
"pending",
|
|
1706
|
+
"queued",
|
|
1707
|
+
"deploying",
|
|
1708
|
+
]);
|
|
1709
|
+
// Bug 9: look for an existing non-terminal deployment with a matching name so
|
|
1710
|
+
// retries attach a new release instead of creating a duplicate app. Defensive:
|
|
1711
|
+
// returns null (fall back to create) if the list call fails or finds nothing.
|
|
1712
|
+
async function findExistingDeploymentByName(c, name) {
|
|
1713
|
+
try {
|
|
1714
|
+
const raw = unwrap(await c.apiGet(apiPath("/deployments")));
|
|
1715
|
+
const items = Array.isArray(raw)
|
|
1716
|
+
? raw
|
|
1717
|
+
: Array.isArray((asRecord(raw) ?? {})["data"])
|
|
1718
|
+
? (asRecord(raw) ?? {})["data"]
|
|
1719
|
+
: [];
|
|
1720
|
+
for (const item of items) {
|
|
1721
|
+
const rec = asRecord(item);
|
|
1722
|
+
if (!rec)
|
|
1723
|
+
continue;
|
|
1724
|
+
if (stringField(rec, "name") !== name)
|
|
1725
|
+
continue;
|
|
1726
|
+
const state = (stringField(rec, "state") ?? "").toLowerCase();
|
|
1727
|
+
const id = stringField(rec, "id");
|
|
1728
|
+
if (id && NON_TERMINAL_DEPLOY_STATES.has(state)) {
|
|
1729
|
+
return { id, state };
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
return null;
|
|
1733
|
+
}
|
|
1734
|
+
catch {
|
|
1735
|
+
return null;
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1635
1738
|
async function publishSandbox(sandboxId, opts) {
|
|
1636
1739
|
const c = client();
|
|
1637
1740
|
const body = {
|
|
@@ -1642,6 +1745,16 @@ async function publishSandbox(sandboxId, opts) {
|
|
|
1642
1745
|
};
|
|
1643
1746
|
if (opts.app)
|
|
1644
1747
|
body["deployment_id"] = opts.app;
|
|
1748
|
+
// Bug 9: avoid creating a duplicate deployment on retry. When publishing by
|
|
1749
|
+
// name (no explicit app id), reuse an existing non-terminal deployment with
|
|
1750
|
+
// the same name by attaching a new release to it.
|
|
1751
|
+
if (opts.name && !opts.app) {
|
|
1752
|
+
const existing = await findExistingDeploymentByName(c, opts.name);
|
|
1753
|
+
if (existing) {
|
|
1754
|
+
body["deployment_id"] = existing.id;
|
|
1755
|
+
deployStep(opts, chalk.dim(`Attaching to existing deployment ${existing.id} (${existing.state})`));
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1645
1758
|
if (opts.name)
|
|
1646
1759
|
body["name"] = opts.name;
|
|
1647
1760
|
if (opts.slug)
|
|
@@ -1724,7 +1837,8 @@ async function waitForDeploymentReady(c, deploymentId, timeoutSec) {
|
|
|
1724
1837
|
}
|
|
1725
1838
|
await sleep(2000);
|
|
1726
1839
|
}
|
|
1727
|
-
|
|
1840
|
+
const lastState = last ? String(last["state"] ?? "unknown") : "unknown";
|
|
1841
|
+
throw new UserError(`Deployment still building after ${timeoutSec}s — it may still finish. Re-check with \`miosa sandbox show ${deploymentId}\` or \`miosa deploy logs\`.`, `Last state: ${lastState}`);
|
|
1728
1842
|
}
|
|
1729
1843
|
function parsePublishDatabase(value) {
|
|
1730
1844
|
if (!value)
|
|
@@ -1820,7 +1934,11 @@ async function createSandboxForDeploy(c, template, name, source) {
|
|
|
1820
1934
|
async function readRemoteAppManifest(c, sandboxId, timeoutSec) {
|
|
1821
1935
|
const deadline = Date.now() + Math.min(timeoutSec, 120) * 1000;
|
|
1822
1936
|
while (Date.now() < deadline) {
|
|
1823
|
-
for (const filename of [
|
|
1937
|
+
for (const filename of [
|
|
1938
|
+
"miosa.app.yml",
|
|
1939
|
+
"miosa.app.yaml",
|
|
1940
|
+
"miosa.app.json",
|
|
1941
|
+
]) {
|
|
1824
1942
|
const result = await c
|
|
1825
1943
|
.apiPost(apiPath(`/sandboxes/${enc(sandboxId)}/exec`), {
|
|
1826
1944
|
command: `test -f /workspace/${filename} && cat /workspace/${filename}`,
|
|
@@ -1830,7 +1948,8 @@ async function readRemoteAppManifest(c, sandboxId, timeoutSec) {
|
|
|
1830
1948
|
.then(unwrap)
|
|
1831
1949
|
.catch(() => null);
|
|
1832
1950
|
const row = asRecord(result);
|
|
1833
|
-
if (Number(row?.["exit_code"] ?? 1) === 0 &&
|
|
1951
|
+
if (Number(row?.["exit_code"] ?? 1) === 0 &&
|
|
1952
|
+
typeof row?.["stdout"] === "string") {
|
|
1834
1953
|
return parseAppManifest(filename, row["stdout"]);
|
|
1835
1954
|
}
|
|
1836
1955
|
}
|
|
@@ -1926,6 +2045,98 @@ async function execSandbox(c, sandboxId, command, cwd, timeout) {
|
|
|
1926
2045
|
}
|
|
1927
2046
|
return result;
|
|
1928
2047
|
}
|
|
2048
|
+
// Like execSandbox but does NOT throw on a non-zero exit code.
|
|
2049
|
+
async function execSandboxRaw(c, sandboxId, command, cwd, timeout) {
|
|
2050
|
+
const body = { command: commandInCwd(command, cwd) };
|
|
2051
|
+
if (cwd) {
|
|
2052
|
+
body["cwd"] = cwd;
|
|
2053
|
+
body["dir"] = cwd;
|
|
2054
|
+
}
|
|
2055
|
+
if (timeout != null)
|
|
2056
|
+
body["timeout"] = timeout;
|
|
2057
|
+
return unwrap(await c.apiPost(apiPath(`/sandboxes/${enc(sandboxId)}/exec`), body));
|
|
2058
|
+
}
|
|
2059
|
+
function parseListeningPorts(stdout) {
|
|
2060
|
+
const lines = stdout.split("\n").map((l) => l.trim());
|
|
2061
|
+
const seen = new Set();
|
|
2062
|
+
const out = [];
|
|
2063
|
+
// ss -ltnH columns: State Recv-Q Send-Q Local-Address:Port Peer-Address:Port
|
|
2064
|
+
const ssLine = /^LISTEN\s+\d+\s+\d+\s+(\S+):(\d+)\s+/;
|
|
2065
|
+
// /proc/net/tcp: sl local_address rem_address st ... (hex)
|
|
2066
|
+
const procLine = /^\d+:\s+([0-9A-Fa-f]+):([0-9A-Fa-f]+)\s+\S+\s+([0-9A-Fa-f]+)/;
|
|
2067
|
+
for (const line of lines) {
|
|
2068
|
+
if (!line)
|
|
2069
|
+
continue;
|
|
2070
|
+
const ss = ssLine.exec(line);
|
|
2071
|
+
if (ss && ss[2]) {
|
|
2072
|
+
const port = Number(ss[2]);
|
|
2073
|
+
if (!seen.has(port)) {
|
|
2074
|
+
seen.add(port);
|
|
2075
|
+
out.push({ port, address: ss[1] ?? "*" });
|
|
2076
|
+
}
|
|
2077
|
+
continue;
|
|
2078
|
+
}
|
|
2079
|
+
const proc = procLine.exec(line);
|
|
2080
|
+
if (proc && proc[3] === "0A" && proc[2]) {
|
|
2081
|
+
const port = parseInt(proc[2], 16);
|
|
2082
|
+
if (!Number.isNaN(port) && !seen.has(port)) {
|
|
2083
|
+
seen.add(port);
|
|
2084
|
+
out.push({ port, address: "*" });
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
out.sort((a, b) => a.port - b.port);
|
|
2089
|
+
return out;
|
|
2090
|
+
}
|
|
2091
|
+
// Stream exec output: run in background to a log file, poll-read new bytes
|
|
2092
|
+
// until the process exits, then print the final exit code.
|
|
2093
|
+
async function runFollowExec(sandboxId, cmd, cwd, env, timeoutSec, opts) {
|
|
2094
|
+
if (!cmd) {
|
|
2095
|
+
throw new UserError("No command given to follow.");
|
|
2096
|
+
}
|
|
2097
|
+
const c = client();
|
|
2098
|
+
const logPath = `/tmp/miosa-follow-${Date.now()}.log`;
|
|
2099
|
+
const envPrefix = Object.entries(env)
|
|
2100
|
+
.map(([k, v]) => `${k}=${shellQuote(v)} `)
|
|
2101
|
+
.join("");
|
|
2102
|
+
// Run command, capture exit code into a sentinel file, in the background.
|
|
2103
|
+
const exitPath = `${logPath}.exit`;
|
|
2104
|
+
const inner = `${envPrefix}${cmd}`;
|
|
2105
|
+
const launch = `nohup sh -lc ${shellQuote(`( ${inner} ) > ${shellQuote(logPath)} 2>&1; echo $? > ${shellQuote(exitPath)}`)} >/dev/null 2>&1 & echo $!`;
|
|
2106
|
+
const started = await execSandboxRaw(c, sandboxId, launch, cwd, timeoutSec);
|
|
2107
|
+
const pid = String(started["stdout"] ?? "").trim();
|
|
2108
|
+
if (!pid) {
|
|
2109
|
+
throw new UserError("Could not start background command for --follow.");
|
|
2110
|
+
}
|
|
2111
|
+
const deadline = timeoutSec ? Date.now() + timeoutSec * 1000 : Infinity;
|
|
2112
|
+
let offset = 0;
|
|
2113
|
+
for (;;) {
|
|
2114
|
+
const read = await execSandboxRaw(c, sandboxId, `tail -c +${offset + 1} ${shellQuote(logPath)} 2>/dev/null`);
|
|
2115
|
+
const chunk = String(read["stdout"] ?? "");
|
|
2116
|
+
if (chunk.length > 0) {
|
|
2117
|
+
process.stdout.write(chunk);
|
|
2118
|
+
offset += Buffer.byteLength(chunk);
|
|
2119
|
+
}
|
|
2120
|
+
const alive = await execSandboxRaw(c, sandboxId, `kill -0 ${shellQuote(pid)} 2>/dev/null && echo alive || echo done`);
|
|
2121
|
+
if (String(alive["stdout"] ?? "").trim() === "done")
|
|
2122
|
+
break;
|
|
2123
|
+
if (Date.now() > deadline) {
|
|
2124
|
+
console.error(chalk.yellow(`\nTimed out after ${timeoutSec}s — command may still be running (pid ${pid}).`));
|
|
2125
|
+
return;
|
|
2126
|
+
}
|
|
2127
|
+
await sleep(1000);
|
|
2128
|
+
}
|
|
2129
|
+
// Drain any trailing output.
|
|
2130
|
+
const tail = await execSandboxRaw(c, sandboxId, `tail -c +${offset + 1} ${shellQuote(logPath)} 2>/dev/null`);
|
|
2131
|
+
const tailChunk = String(tail["stdout"] ?? "");
|
|
2132
|
+
if (tailChunk.length > 0)
|
|
2133
|
+
process.stdout.write(tailChunk);
|
|
2134
|
+
const exitRead = await execSandboxRaw(c, sandboxId, `cat ${shellQuote(exitPath)} 2>/dev/null`);
|
|
2135
|
+
const exitCode = Number(String(exitRead["stdout"] ?? "0").trim() || "0");
|
|
2136
|
+
if (!isJsonMode(opts) && exitCode !== 0) {
|
|
2137
|
+
console.error(chalk.red(`\nexit code: ${exitCode}`));
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
1929
2140
|
async function waitForInternalHttp(c, sandboxId, port, probePath, timeoutSec) {
|
|
1930
2141
|
const deadline = Date.now() + timeoutSec * 1000;
|
|
1931
2142
|
let last = { ok: false, status: null };
|
|
@@ -2237,6 +2448,15 @@ function buildNetworkPolicy(opts) {
|
|
|
2237
2448
|
function shellQuote(value) {
|
|
2238
2449
|
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
2239
2450
|
}
|
|
2451
|
+
// Join argv words back into a shell command, quoting only words that contain
|
|
2452
|
+
// whitespace or shell-significant characters. Simple tokens stay bare so
|
|
2453
|
+
// `exec <id> ls -la` reads naturally, while `bash -c "cd x && y"` survives.
|
|
2454
|
+
function joinCommandWords(words) {
|
|
2455
|
+
const SAFE = /^[A-Za-z0-9_./:=@%+-]+$/;
|
|
2456
|
+
return words
|
|
2457
|
+
.map((word) => word.length > 0 && SAFE.test(word) ? word : shellQuote(word))
|
|
2458
|
+
.join(" ");
|
|
2459
|
+
}
|
|
2240
2460
|
function joinUrlPath(basePath, probePath) {
|
|
2241
2461
|
const probe = probePath.startsWith("/") ? probePath : `/${probePath}`;
|
|
2242
2462
|
if (!basePath || basePath === "/")
|