@molecule/api-code-sandbox-docker 1.0.2 → 1.1.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/provider.d.ts.map +1 -1
- package/dist/provider.js +72 -6
- package/package.json +3 -3
package/dist/provider.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAQH,OAAO,KAAK,EAcV,eAAe,
|
|
1
|
+
{"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAQH,OAAO,KAAK,EAcV,eAAe,EAOhB,MAAM,4BAA4B,CAAA;AAoBnC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AA6J9C;;;;;;;;;;;;GAYG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAQ9D;AAkCD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAsB,kBAAkB,CAAC,CAAC,EACxC,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACpB,IAAI,EAAE;IACJ,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,OAAO,CAAC,EAAE,MAAM,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAAA;IACjC,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,CAAA;IACrC,GAAG,CAAC,EAAE;QAAE,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,KAAK,IAAI,CAAA;KAAE,CAAA;CAC1D,GACA,OAAO,CAAC,CAAC,CAAC,CA2BZ;AA21CD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,MAAM,CAAC,EAAE,YAAY,GAAG,eAAe,CAErE"}
|
package/dist/provider.js
CHANGED
|
@@ -508,6 +508,15 @@ class DockerSandboxProvider {
|
|
|
508
508
|
labels[`${this.labelPrefix}.volumeName`] = config.volumeName;
|
|
509
509
|
hostConfig.Binds = [`${config.volumeName}:/workspace`];
|
|
510
510
|
}
|
|
511
|
+
// What the container does when its main process exits. Docker's default is
|
|
512
|
+
// `no`, matching this provider's historical behaviour: a sandbox whose
|
|
513
|
+
// process exits stays stopped for the caller to inspect. A caller running
|
|
514
|
+
// something long-lived (a production instance) asks for `always`/`on-failure`
|
|
515
|
+
// so the workload comes back after a crash or a daemon restart without a
|
|
516
|
+
// control plane in the loop.
|
|
517
|
+
if (config.restartPolicy && config.restartPolicy !== 'no') {
|
|
518
|
+
hostConfig.RestartPolicy = { Name: config.restartPolicy };
|
|
519
|
+
}
|
|
511
520
|
const body = {
|
|
512
521
|
Image: image,
|
|
513
522
|
Env: env,
|
|
@@ -515,6 +524,13 @@ class DockerSandboxProvider {
|
|
|
515
524
|
HostConfig: hostConfig,
|
|
516
525
|
ExposedPorts: Object.fromEntries(publishPorts.map((port) => [`${port}/tcp`, {}])),
|
|
517
526
|
};
|
|
527
|
+
// The caller's own main process, replacing the image's CMD. Without it a
|
|
528
|
+
// container that Docker restarts comes back running the image's idle command
|
|
529
|
+
// and serving nothing, while `status` still reports it as running — which it
|
|
530
|
+
// is, and empty. `Cmd` (not `Entrypoint`) so the image's entrypoint — tini,
|
|
531
|
+
// enabled above via `Init` — still wraps it and reaps its children.
|
|
532
|
+
if (config.command?.length)
|
|
533
|
+
body.Cmd = [...config.command];
|
|
518
534
|
// Retry a transient create timeout (daemon momentarily overwhelmed under
|
|
519
535
|
// concurrent boots) instead of failing the whole sandbox boot. The onRetry
|
|
520
536
|
// guard adopts a container a timed-out create may have already made — keyed on
|
|
@@ -823,17 +839,32 @@ class DockerSandboxProvider {
|
|
|
823
839
|
// (e.g. a per-sandbox subdomain `sb-{port}.preview.localhost:{port}`).
|
|
824
840
|
return provider.previewUrlTemplate.replace(/\{port\}/g, String(port ?? 5173));
|
|
825
841
|
},
|
|
842
|
+
/**
|
|
843
|
+
* Spawn a long-running process with streaming I/O, optionally on a PTY.
|
|
844
|
+
*
|
|
845
|
+
* `opts.pty` asks Docker for `Tty: true`, which gives the process a real
|
|
846
|
+
* controlling terminal: Ctrl-C (`0x03`) becomes SIGINT for the foreground
|
|
847
|
+
* job and the width can be renegotiated through `handle.resize()`. A TTY
|
|
848
|
+
* exec stream is NOT multiplexed, so the frame parser is bypassed and
|
|
849
|
+
* everything (stderr included, as on a real terminal) arrives on stdout.
|
|
850
|
+
*
|
|
851
|
+
* @param command - The command to run.
|
|
852
|
+
* @param opts - Working directory, env, and optional PTY size.
|
|
853
|
+
* @returns A handle with streaming I/O, stdin, kill, and (on a PTY) resize.
|
|
854
|
+
*/
|
|
826
855
|
async spawn(command, opts) {
|
|
856
|
+
const tty = Boolean(opts?.pty);
|
|
827
857
|
const execCreate = (await provider.dockerApi(`/containers/${containerId}/exec`, 'POST', {
|
|
828
858
|
Cmd: ['sh', '-c', command],
|
|
829
859
|
AttachStdin: true,
|
|
830
860
|
AttachStdout: true,
|
|
831
861
|
AttachStderr: true,
|
|
832
|
-
Tty:
|
|
862
|
+
Tty: tty,
|
|
863
|
+
...(opts?.pty ? { ConsoleSize: [opts.pty.rows, opts.pty.cols] } : {}),
|
|
833
864
|
WorkingDir: opts?.cwd ?? '/workspace',
|
|
834
865
|
Env: opts?.env ? Object.entries(opts.env).map(([k, v]) => `${k}=${v}`) : undefined,
|
|
835
866
|
}));
|
|
836
|
-
const socket = await provider.dockerExecUpgrade(execCreate.Id);
|
|
867
|
+
const socket = await provider.dockerExecUpgrade(execCreate.Id, tty);
|
|
837
868
|
const parser = new DockerMuxParser();
|
|
838
869
|
let closeCb = null;
|
|
839
870
|
// Kill idle spawn sockets after 10 minutes to prevent descriptor leaks
|
|
@@ -841,7 +872,17 @@ class DockerSandboxProvider {
|
|
|
841
872
|
socket.on('timeout', () => {
|
|
842
873
|
socket.destroy();
|
|
843
874
|
});
|
|
875
|
+
// A TTY exec stream carries raw terminal bytes — there are no 8-byte
|
|
876
|
+
// frame headers to strip, and feeding them to the mux parser would read
|
|
877
|
+
// the first byte of the shell's output as a stream descriptor.
|
|
878
|
+
const ttyListeners = [];
|
|
844
879
|
socket.on('data', (chunk) => {
|
|
880
|
+
if (tty) {
|
|
881
|
+
const text = chunk.toString('utf8');
|
|
882
|
+
for (const cb of ttyListeners)
|
|
883
|
+
cb(text);
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
845
886
|
try {
|
|
846
887
|
parser.feed(chunk);
|
|
847
888
|
}
|
|
@@ -867,10 +908,16 @@ class DockerSandboxProvider {
|
|
|
867
908
|
socket.write(data);
|
|
868
909
|
},
|
|
869
910
|
onStdout(cb) {
|
|
870
|
-
|
|
911
|
+
if (tty)
|
|
912
|
+
ttyListeners.push(cb);
|
|
913
|
+
else
|
|
914
|
+
parser.onStdout(cb);
|
|
871
915
|
},
|
|
872
916
|
onStderr(cb) {
|
|
873
|
-
|
|
917
|
+
// On a TTY both streams share one descriptor, so nothing arrives
|
|
918
|
+
// here — registering is harmless and keeps the handle uniform.
|
|
919
|
+
if (!tty)
|
|
920
|
+
parser.onStderr(cb);
|
|
874
921
|
},
|
|
875
922
|
onClose(cb) {
|
|
876
923
|
closeCb = cb;
|
|
@@ -879,6 +926,22 @@ class DockerSandboxProvider {
|
|
|
879
926
|
if (!socket.destroyed)
|
|
880
927
|
socket.destroy();
|
|
881
928
|
},
|
|
929
|
+
...(tty
|
|
930
|
+
? {
|
|
931
|
+
resize(size) {
|
|
932
|
+
void provider
|
|
933
|
+
.dockerApi(`/exec/${execCreate.Id}/resize?h=${Math.max(1, Math.floor(size.rows))}&w=${Math.max(1, Math.floor(size.cols))}`, 'POST')
|
|
934
|
+
.catch((error) => {
|
|
935
|
+
// Best-effort: a resize races the exec exiting, and a
|
|
936
|
+
// failed one only leaves the remote width stale — never a
|
|
937
|
+
// reason to tear down a live session.
|
|
938
|
+
logger.debug('Spawn resize failed', {
|
|
939
|
+
error: error instanceof Error ? error.message : error,
|
|
940
|
+
});
|
|
941
|
+
});
|
|
942
|
+
},
|
|
943
|
+
}
|
|
944
|
+
: {}),
|
|
882
945
|
};
|
|
883
946
|
},
|
|
884
947
|
async hibernate() {
|
|
@@ -1311,11 +1374,14 @@ class DockerSandboxProvider {
|
|
|
1311
1374
|
* bidirectional socket. Docker hijacks the connection (101 Switching Protocols)
|
|
1312
1375
|
* when stdin is attached, giving us a raw TCP socket for streaming I/O.
|
|
1313
1376
|
* @param execId - The exec instance ID from the create step.
|
|
1377
|
+
* @param tty - Whether the exec was created with a TTY; the start call must
|
|
1378
|
+
* agree, or Docker streams multiplexed frames to a terminal expecting raw
|
|
1379
|
+
* bytes.
|
|
1314
1380
|
* @returns The raw socket for stdin writes and multiplexed stdout/stderr reads.
|
|
1315
1381
|
*/
|
|
1316
|
-
dockerExecUpgrade(execId) {
|
|
1382
|
+
dockerExecUpgrade(execId, tty = false) {
|
|
1317
1383
|
return new Promise((resolve, reject) => {
|
|
1318
|
-
const body = JSON.stringify({ Detach: false, Tty:
|
|
1384
|
+
const body = JSON.stringify({ Detach: false, Tty: tty });
|
|
1319
1385
|
const req = http.request({
|
|
1320
1386
|
...this.endpoint,
|
|
1321
1387
|
path: `/v1.44/exec/${execId}/start`,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@molecule/api-code-sandbox-docker",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Docker-based code sandbox container provider",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -30,14 +30,14 @@
|
|
|
30
30
|
"license": "Apache-2.0",
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@molecule/api-bond": "1.0.1",
|
|
33
|
-
"@molecule/api-code-sandbox": "1.0
|
|
33
|
+
"@molecule/api-code-sandbox": "1.2.0",
|
|
34
34
|
"@types/node": "26.1.2",
|
|
35
35
|
"typescript": "6.0.3",
|
|
36
36
|
"vitest": "4.1.10"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
39
|
"@molecule/api-bond": "^1.0.1",
|
|
40
|
-
"@molecule/api-code-sandbox": "^1.0
|
|
40
|
+
"@molecule/api-code-sandbox": "^1.2.0",
|
|
41
41
|
"@molecule/api-i18n": "^1.0.1"
|
|
42
42
|
},
|
|
43
43
|
"peerDependenciesMeta": {
|