@greppleai/grip 0.0.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +93 -0
- package/README.md +13 -7
- package/dist/bun/cli.d.ts.map +1 -1
- package/dist/bun/cli.js +4 -2
- package/dist/bun/cli.js.map +1 -1
- package/dist/bun/embedded-assets.d.ts +3 -0
- package/dist/bun/embedded-assets.d.ts.map +1 -0
- package/dist/bun/embedded-assets.js +52 -0
- package/dist/bun/embedded-assets.js.map +1 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +1 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +3 -3
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +17 -2
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/remote-workspace-protocol/contract.d.ts +13 -4
- package/dist/core/remote-workspace-protocol/contract.d.ts.map +1 -1
- package/dist/core/remote-workspace-protocol/contract.js +17 -1
- package/dist/core/remote-workspace-protocol/contract.js.map +1 -1
- package/dist/core/remote-workspace-protocol/guest-shared-client.d.ts +16 -0
- package/dist/core/remote-workspace-protocol/guest-shared-client.d.ts.map +1 -0
- package/dist/core/remote-workspace-protocol/guest-shared-client.js +54 -0
- package/dist/core/remote-workspace-protocol/guest-shared-client.js.map +1 -0
- package/dist/core/remote-workspace-protocol/guest-shared-outbound-scheduler.d.ts +23 -0
- package/dist/core/remote-workspace-protocol/guest-shared-outbound-scheduler.d.ts.map +1 -0
- package/dist/core/remote-workspace-protocol/guest-shared-outbound-scheduler.js +112 -0
- package/dist/core/remote-workspace-protocol/guest-shared-outbound-scheduler.js.map +1 -0
- package/dist/core/remote-workspace-protocol/guest-shared-uplink.d.ts +30 -0
- package/dist/core/remote-workspace-protocol/guest-shared-uplink.d.ts.map +1 -0
- package/dist/core/remote-workspace-protocol/guest-shared-uplink.js +243 -0
- package/dist/core/remote-workspace-protocol/guest-shared-uplink.js.map +1 -0
- package/dist/core/remote-workspace-protocol/lima-owner-directory.d.ts +3 -0
- package/dist/core/remote-workspace-protocol/lima-owner-directory.d.ts.map +1 -0
- package/dist/core/remote-workspace-protocol/lima-owner-directory.js +31 -0
- package/dist/core/remote-workspace-protocol/lima-owner-directory.js.map +1 -0
- package/dist/core/remote-workspace-protocol/lima-owner-lock.d.ts +9 -0
- package/dist/core/remote-workspace-protocol/lima-owner-lock.d.ts.map +1 -0
- package/dist/core/remote-workspace-protocol/lima-owner-lock.js +89 -0
- package/dist/core/remote-workspace-protocol/lima-owner-lock.js.map +1 -0
- package/dist/core/remote-workspace-protocol/session.d.ts +2 -0
- package/dist/core/remote-workspace-protocol/session.d.ts.map +1 -1
- package/dist/core/remote-workspace-protocol/session.js +3 -0
- package/dist/core/remote-workspace-protocol/session.js.map +1 -1
- package/dist/core/remote-workspace-protocol/shared-transport.d.ts +71 -0
- package/dist/core/remote-workspace-protocol/shared-transport.d.ts.map +1 -0
- package/dist/core/remote-workspace-protocol/shared-transport.js +68 -0
- package/dist/core/remote-workspace-protocol/shared-transport.js.map +1 -0
- package/dist/core/sandbox/command.d.ts +2 -4
- package/dist/core/sandbox/command.d.ts.map +1 -1
- package/dist/core/sandbox/command.js +24 -46
- package/dist/core/sandbox/command.js.map +1 -1
- package/dist/core/sandbox/config.d.ts +91 -0
- package/dist/core/sandbox/config.d.ts.map +1 -0
- package/dist/core/sandbox/config.js +88 -0
- package/dist/core/sandbox/config.js.map +1 -0
- package/dist/core/sandbox/lima-guest-binary.d.ts +11 -0
- package/dist/core/sandbox/lima-guest-binary.d.ts.map +1 -0
- package/dist/core/sandbox/lima-guest-binary.js +140 -0
- package/dist/core/sandbox/lima-guest-binary.js.map +1 -0
- package/dist/core/sandbox/lima.d.ts +22 -3
- package/dist/core/sandbox/lima.d.ts.map +1 -1
- package/dist/core/sandbox/lima.js +434 -42
- package/dist/core/sandbox/lima.js.map +1 -1
- package/dist/core/sandbox/shared-lima-roots.d.ts +11 -0
- package/dist/core/sandbox/shared-lima-roots.d.ts.map +1 -0
- package/dist/core/sandbox/shared-lima-roots.js +25 -0
- package/dist/core/sandbox/shared-lima-roots.js.map +1 -0
- package/dist/core/sandbox/update-shared-guest-binary.d.ts +8 -0
- package/dist/core/sandbox/update-shared-guest-binary.d.ts.map +1 -0
- package/dist/core/sandbox/update-shared-guest-binary.js +100 -0
- package/dist/core/sandbox/update-shared-guest-binary.js.map +1 -0
- package/dist/core/settings-manager.d.ts +1 -5
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +25 -14
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/core/tools/file-mutation-queue.d.ts +2 -0
- package/dist/core/tools/file-mutation-queue.d.ts.map +1 -1
- package/dist/core/tools/file-mutation-queue.js +9 -0
- package/dist/core/tools/file-mutation-queue.js.map +1 -1
- package/dist/core/tools/operations.d.ts +13 -2
- package/dist/core/tools/operations.d.ts.map +1 -1
- package/dist/core/tools/operations.js +132 -36
- package/dist/core/tools/operations.js.map +1 -1
- package/dist/daemon/activity-budget.d.ts +10 -0
- package/dist/daemon/activity-budget.d.ts.map +1 -0
- package/dist/daemon/activity-budget.js +34 -0
- package/dist/daemon/activity-budget.js.map +1 -0
- package/dist/daemon/config.d.ts +2 -0
- package/dist/daemon/config.d.ts.map +1 -1
- package/dist/daemon/config.js +15 -0
- package/dist/daemon/config.js.map +1 -1
- package/dist/daemon/outbound-budget.d.ts +10 -0
- package/dist/daemon/outbound-budget.d.ts.map +1 -0
- package/dist/daemon/outbound-budget.js +30 -0
- package/dist/daemon/outbound-budget.js.map +1 -0
- package/dist/daemon/security.d.ts.map +1 -1
- package/dist/daemon/security.js +3 -1
- package/dist/daemon/security.js.map +1 -1
- package/dist/daemon/server.d.ts.map +1 -1
- package/dist/daemon/server.js +104 -24
- package/dist/daemon/server.js.map +1 -1
- package/dist/daemon/shared-connection.d.ts +33 -0
- package/dist/daemon/shared-connection.d.ts.map +1 -0
- package/dist/daemon/shared-connection.js +271 -0
- package/dist/daemon/shared-connection.js.map +1 -0
- package/dist/daemon/shared-revision.d.ts +5 -0
- package/dist/daemon/shared-revision.d.ts.map +1 -0
- package/dist/daemon/shared-revision.js +61 -0
- package/dist/daemon/shared-revision.js.map +1 -0
- package/dist/daemon/shared-roots.d.ts +9 -0
- package/dist/daemon/shared-roots.d.ts.map +1 -0
- package/dist/daemon/shared-roots.js +33 -0
- package/dist/daemon/shared-roots.js.map +1 -0
- package/dist/daemon/workspace-runtime.d.ts +1 -1
- package/dist/daemon/workspace-runtime.d.ts.map +1 -1
- package/dist/daemon/workspace-runtime.js +42 -10
- package/dist/daemon/workspace-runtime.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +5 -3
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/assistant-message.d.ts +1 -0
- package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/assistant-message.js +13 -3
- package/dist/modes/interactive/components/assistant-message.js.map +1 -1
- package/dist/modes/interactive/components/branch-summary-message.d.ts +4 -3
- package/dist/modes/interactive/components/branch-summary-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/branch-summary-message.js +5 -4
- package/dist/modes/interactive/components/branch-summary-message.js.map +1 -1
- package/dist/modes/interactive/components/compaction-summary-message.d.ts +4 -3
- package/dist/modes/interactive/components/compaction-summary-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/compaction-summary-message.js +5 -4
- package/dist/modes/interactive/components/compaction-summary-message.js.map +1 -1
- package/dist/modes/interactive/components/custom-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/custom-message.js +4 -3
- package/dist/modes/interactive/components/custom-message.js.map +1 -1
- package/dist/modes/interactive/components/hook-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/hook-execution.js +3 -2
- package/dist/modes/interactive/components/hook-execution.js.map +1 -1
- package/dist/modes/interactive/components/message-card.d.ts +9 -0
- package/dist/modes/interactive/components/message-card.d.ts.map +1 -0
- package/dist/modes/interactive/components/message-card.js +24 -0
- package/dist/modes/interactive/components/message-card.js.map +1 -0
- package/dist/modes/interactive/components/skill-invocation-message.d.ts +4 -3
- package/dist/modes/interactive/components/skill-invocation-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/skill-invocation-message.js +5 -4
- package/dist/modes/interactive/components/skill-invocation-message.js.map +1 -1
- package/dist/modes/interactive/components/tool-execution.d.ts +1 -1
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +27 -21
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/components/user-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/user-message.js +3 -2
- package/dist/modes/interactive/components/user-message.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +0 -3
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +55 -44
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/theme/theme.d.ts +3 -0
- package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/dist/modes/interactive/theme/theme.js +10 -0
- package/dist/modes/interactive/theme/theme.js.map +1 -1
- package/dist/utils/clipboard-native.d.ts.map +1 -1
- package/dist/utils/clipboard-native.js +1 -1
- package/dist/utils/clipboard-native.js.map +1 -1
- package/dist/utils/image-resize.d.ts.map +1 -1
- package/dist/utils/image-resize.js +5 -0
- package/dist/utils/image-resize.js.map +1 -1
- package/dist/utils/photon.d.ts.map +1 -1
- package/dist/utils/photon.js +1 -0
- package/dist/utils/photon.js.map +1 -1
- package/docs/daemon.md +6 -4
- package/docs/docs.json +1 -1
- package/docs/index.md +8 -12
- package/docs/packages.md +3 -3
- package/docs/quickstart.md +11 -19
- package/docs/sandbox.md +40 -117
- package/docs/settings.md +12 -11
- package/docs/termux.md +1 -18
- package/docs/usage.md +2 -2
- package/examples/README.md +0 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package.json +1 -1
- package/npm-shrinkwrap.json +12 -12
- package/package.json +5 -6
- package/dist/core/sandbox/docker.d.ts +0 -169
- package/dist/core/sandbox/docker.d.ts.map +0 -1
- package/dist/core/sandbox/docker.js +0 -633
- package/dist/core/sandbox/docker.js.map +0 -1
- package/examples/sandbox-docker/Dockerfile +0 -274
- package/examples/sandbox-docker/README.md +0 -88
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@greppleai/grip",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@greppleai/grip",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.1.3",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@greppleai/grip-agent-core": "^0.
|
|
13
|
-
"@greppleai/grip-ai": "^0.
|
|
14
|
-
"@greppleai/grip-tui": "^0.
|
|
12
|
+
"@greppleai/grip-agent-core": "^0.1.3",
|
|
13
|
+
"@greppleai/grip-ai": "^0.1.3",
|
|
14
|
+
"@greppleai/grip-tui": "^0.1.3",
|
|
15
15
|
"@silvia-odwyer/photon-node": "0.3.4",
|
|
16
16
|
"chalk": "5.6.2",
|
|
17
17
|
"cross-spawn": "7.0.6",
|
|
@@ -502,11 +502,11 @@
|
|
|
502
502
|
"hasInstallScript": true
|
|
503
503
|
},
|
|
504
504
|
"node_modules/@greppleai/grip-agent-core": {
|
|
505
|
-
"version": "0.
|
|
506
|
-
"resolved": "https://registry.npmjs.org/@greppleai/grip-agent-core/-/grip-agent-core-0.
|
|
505
|
+
"version": "0.1.3",
|
|
506
|
+
"resolved": "https://registry.npmjs.org/@greppleai/grip-agent-core/-/grip-agent-core-0.1.3.tgz",
|
|
507
507
|
"license": "MIT",
|
|
508
508
|
"dependencies": {
|
|
509
|
-
"@greppleai/grip-ai": "^0.
|
|
509
|
+
"@greppleai/grip-ai": "^0.1.3",
|
|
510
510
|
"ignore": "7.0.5",
|
|
511
511
|
"typebox": "1.3.7",
|
|
512
512
|
"yaml": "2.9.0"
|
|
@@ -516,8 +516,8 @@
|
|
|
516
516
|
}
|
|
517
517
|
},
|
|
518
518
|
"node_modules/@greppleai/grip-ai": {
|
|
519
|
-
"version": "0.
|
|
520
|
-
"resolved": "https://registry.npmjs.org/@greppleai/grip-ai/-/grip-ai-0.
|
|
519
|
+
"version": "0.1.3",
|
|
520
|
+
"resolved": "https://registry.npmjs.org/@greppleai/grip-ai/-/grip-ai-0.1.3.tgz",
|
|
521
521
|
"license": "MIT",
|
|
522
522
|
"dependencies": {
|
|
523
523
|
"@anthropic-ai/sdk": "0.124.0",
|
|
@@ -540,8 +540,8 @@
|
|
|
540
540
|
}
|
|
541
541
|
},
|
|
542
542
|
"node_modules/@greppleai/grip-tui": {
|
|
543
|
-
"version": "0.
|
|
544
|
-
"resolved": "https://registry.npmjs.org/@greppleai/grip-tui/-/grip-tui-0.
|
|
543
|
+
"version": "0.1.3",
|
|
544
|
+
"resolved": "https://registry.npmjs.org/@greppleai/grip-tui/-/grip-tui-0.1.3.tgz",
|
|
545
545
|
"license": "MIT",
|
|
546
546
|
"dependencies": {
|
|
547
547
|
"get-east-asian-width": "1.6.0",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@greppleai/grip",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"gripConfig": {
|
|
@@ -31,17 +31,16 @@
|
|
|
31
31
|
"scripts": {
|
|
32
32
|
"clean": "shx rm -rf dist",
|
|
33
33
|
"build": "tsgo -p tsconfig.build.json && shx chmod +x dist/cli.js && npm run copy-assets",
|
|
34
|
-
"build:binary": "npm --prefix ../tui run build && npm --prefix ../ai run build && npm --prefix ../agent run build && npm run build && bun build --
|
|
34
|
+
"build:binary": "npm --prefix ../tui run build && npm --prefix ../ai run build && npm --prefix ../agent run build && npm run build && bun build --target=bun ./dist/utils/image-resize-worker.js --outfile ./dist/bun/image-resize-worker.js && node ../../scripts/prepare-binary-assets.mjs && bun build --compile --asset=./dist/bun/grip-assets.blob ./dist/bun/cli.js --outfile dist/grip",
|
|
35
35
|
"copy-assets": "shx mkdir -p dist/modes/interactive/theme && shx cp src/modes/interactive/theme/*.json dist/modes/interactive/theme/ && shx mkdir -p dist/core/export-html/vendor && shx cp src/core/export-html/template.html src/core/export-html/template.css src/core/export-html/template.js dist/core/export-html/ && shx cp src/core/export-html/vendor/*.js dist/core/export-html/vendor/",
|
|
36
|
-
"copy-binary-assets": "shx cp package.json dist/ && shx cp README.md dist/ && shx cp CHANGELOG.md dist/ && shx mkdir -p dist/theme && shx cp src/modes/interactive/theme/*.json dist/theme/ && shx mkdir -p dist/export-html/vendor && shx cp src/core/export-html/template.html src/core/export-html/template.css src/core/export-html/template.js dist/export-html/ && shx cp src/core/export-html/vendor/*.js dist/export-html/vendor/ && shx cp -r docs dist/ && shx cp -r examples dist/ && shx cp ../../node_modules/@silvia-odwyer/photon-node/photon_rs_bg.wasm dist/",
|
|
37
36
|
"test": "vitest --run",
|
|
38
37
|
"shrinkwrap": "node ../../scripts/generate-coding-agent-shrinkwrap.mjs",
|
|
39
38
|
"prepublishOnly": "npm run clean && npm run build && npm run shrinkwrap"
|
|
40
39
|
},
|
|
41
40
|
"dependencies": {
|
|
42
|
-
"@greppleai/grip-agent-core": "^0.
|
|
43
|
-
"@greppleai/grip-ai": "^0.
|
|
44
|
-
"@greppleai/grip-tui": "^0.
|
|
41
|
+
"@greppleai/grip-agent-core": "^0.1.3",
|
|
42
|
+
"@greppleai/grip-ai": "^0.1.3",
|
|
43
|
+
"@greppleai/grip-tui": "^0.1.3",
|
|
45
44
|
"@silvia-odwyer/photon-node": "0.3.4",
|
|
46
45
|
"chalk": "5.6.2",
|
|
47
46
|
"cross-spawn": "7.0.6",
|
|
@@ -1,169 +0,0 @@
|
|
|
1
|
-
import type { SandboxSettings, SettingsManager } from "../settings-manager.ts";
|
|
2
|
-
export declare const DEFAULT_SANDBOX_IMAGE = "ghcr.io/greppleai/grip-sandbox:latest";
|
|
3
|
-
export declare const DEFAULT_SANDBOX_WORKSPACE_MOUNT = "/workspace";
|
|
4
|
-
export declare const DEFAULT_SANDBOX_DAEMON_PORT = 8787;
|
|
5
|
-
export declare const DEFAULT_SANDBOX_DOCKER_BINARY = "docker";
|
|
6
|
-
export declare const DEFAULT_SANDBOX_LIMA_BINARY = "limactl";
|
|
7
|
-
export declare const DEFAULT_SANDBOX_LIMA_TEMPLATE = "template:docker-rootful";
|
|
8
|
-
export declare const SANDBOX_LABEL_PREFIX = "ai.greppleai.grip";
|
|
9
|
-
export declare const SANDBOX_LABEL_ENABLED = "ai.greppleai.grip.sandbox";
|
|
10
|
-
export declare const SANDBOX_LABEL_WORKSPACE_HASH = "ai.greppleai.grip.workspace-hash";
|
|
11
|
-
export declare const SANDBOX_LABEL_WORKSPACE_MOUNT = "ai.greppleai.grip.workspace-mount";
|
|
12
|
-
export declare const SANDBOX_LABEL_DAEMON_PORT = "ai.greppleai.grip.daemon-port";
|
|
13
|
-
export declare const SANDBOX_LABEL_OWNER_UID = "ai.greppleai.grip.owner-uid";
|
|
14
|
-
export declare const SANDBOX_LABEL_VERSION = "ai.greppleai.grip.version";
|
|
15
|
-
export declare const SANDBOX_LABEL_SESSION = "ai.greppleai.grip.session";
|
|
16
|
-
export type SandboxCleanupBehavior = "stop" | "remove";
|
|
17
|
-
export type SandboxRuntime = "docker" | "lima";
|
|
18
|
-
export interface SandboxConfig {
|
|
19
|
-
runtime: SandboxRuntime;
|
|
20
|
-
image: string;
|
|
21
|
-
dockerBinary: string;
|
|
22
|
-
limaBinary: string;
|
|
23
|
-
limaTemplate: string;
|
|
24
|
-
limaInstanceNamePrefix: string;
|
|
25
|
-
workspaceMountPath: string;
|
|
26
|
-
containerNamePrefix: string;
|
|
27
|
-
daemonPort: number;
|
|
28
|
-
daemonHostBind: string;
|
|
29
|
-
cleanup: SandboxCleanupBehavior;
|
|
30
|
-
ownerUid: string | undefined;
|
|
31
|
-
}
|
|
32
|
-
export type SandboxConfigOverrides = SandboxSettings;
|
|
33
|
-
export interface SandboxEnvironment {
|
|
34
|
-
GRIP_SANDBOX_RUNTIME?: string;
|
|
35
|
-
GRIP_SANDBOX_IMAGE?: string;
|
|
36
|
-
GRIP_SANDBOX_DOCKER?: string;
|
|
37
|
-
GRIP_SANDBOX_LIMA?: string;
|
|
38
|
-
GRIP_SANDBOX_LIMA_TEMPLATE?: string;
|
|
39
|
-
GRIP_SANDBOX_LIMA_INSTANCE_PREFIX?: string;
|
|
40
|
-
GRIP_SANDBOX_WORKSPACE_MOUNT?: string;
|
|
41
|
-
GRIP_SANDBOX_CONTAINER_PREFIX?: string;
|
|
42
|
-
GRIP_SANDBOX_DAEMON_PORT?: string;
|
|
43
|
-
GRIP_SANDBOX_DAEMON_HOST_BIND?: string;
|
|
44
|
-
GRIP_SANDBOX_CLEANUP?: string;
|
|
45
|
-
}
|
|
46
|
-
export interface SandboxStartOptions extends SandboxConfigOverrides {
|
|
47
|
-
workspaceRoot: string;
|
|
48
|
-
sessionId?: string;
|
|
49
|
-
name?: string;
|
|
50
|
-
}
|
|
51
|
-
export interface SandboxListOptions {
|
|
52
|
-
workspaceRoot: string;
|
|
53
|
-
allWorkspaces?: boolean;
|
|
54
|
-
}
|
|
55
|
-
export interface SandboxStopOptions {
|
|
56
|
-
workspaceRoot: string;
|
|
57
|
-
target?: string;
|
|
58
|
-
currentContainerId?: string;
|
|
59
|
-
}
|
|
60
|
-
export interface DockerRunInvocation {
|
|
61
|
-
command: string;
|
|
62
|
-
args: string[];
|
|
63
|
-
env: Record<string, string>;
|
|
64
|
-
token: string;
|
|
65
|
-
containerName: string;
|
|
66
|
-
labels: Record<string, string>;
|
|
67
|
-
workspaceRoot: string;
|
|
68
|
-
workspaceHash: string;
|
|
69
|
-
}
|
|
70
|
-
export interface SandboxContainer {
|
|
71
|
-
id: string;
|
|
72
|
-
name: string;
|
|
73
|
-
image: string;
|
|
74
|
-
status: string;
|
|
75
|
-
state: string | undefined;
|
|
76
|
-
createdAt: string | undefined;
|
|
77
|
-
workspaceRoot: string | undefined;
|
|
78
|
-
workspaceMountPath: string | undefined;
|
|
79
|
-
daemonPort: number | undefined;
|
|
80
|
-
daemonEndpoint: string | undefined;
|
|
81
|
-
labels: Record<string, string>;
|
|
82
|
-
}
|
|
83
|
-
export interface SandboxStartResult {
|
|
84
|
-
containerId: string;
|
|
85
|
-
containerName: string;
|
|
86
|
-
workspaceRoot: string;
|
|
87
|
-
workspaceMountPath: string;
|
|
88
|
-
daemonUrl: string;
|
|
89
|
-
daemonUrlRedacted: string;
|
|
90
|
-
token: string;
|
|
91
|
-
labels: Record<string, string>;
|
|
92
|
-
}
|
|
93
|
-
export type SandboxStopResult = {
|
|
94
|
-
status: "stopped";
|
|
95
|
-
container: SandboxContainer;
|
|
96
|
-
} | {
|
|
97
|
-
status: "removed";
|
|
98
|
-
container: SandboxContainer;
|
|
99
|
-
} | {
|
|
100
|
-
status: "already-stopped";
|
|
101
|
-
container: SandboxContainer;
|
|
102
|
-
} | {
|
|
103
|
-
status: "not-found";
|
|
104
|
-
message: string;
|
|
105
|
-
};
|
|
106
|
-
export interface SandboxService {
|
|
107
|
-
resolveConfig(overrides?: SandboxConfigOverrides): SandboxConfig;
|
|
108
|
-
start(options: SandboxStartOptions): Promise<SandboxStartResult>;
|
|
109
|
-
list(options: SandboxListOptions): Promise<SandboxContainer[]>;
|
|
110
|
-
stop(options: SandboxStopOptions): Promise<SandboxStopResult>;
|
|
111
|
-
}
|
|
112
|
-
export interface DockerCommandResult {
|
|
113
|
-
exitCode: number | null;
|
|
114
|
-
stdout: string;
|
|
115
|
-
stderr: string;
|
|
116
|
-
}
|
|
117
|
-
export interface DockerCommandOptions {
|
|
118
|
-
env?: Record<string, string>;
|
|
119
|
-
}
|
|
120
|
-
export interface DockerRunner {
|
|
121
|
-
run(command: string, args: string[], options?: DockerCommandOptions): Promise<DockerCommandResult>;
|
|
122
|
-
}
|
|
123
|
-
export interface ManagedSandboxContainer {
|
|
124
|
-
workspaceRoot: string;
|
|
125
|
-
daemonPort: number;
|
|
126
|
-
ownerId?: string;
|
|
127
|
-
}
|
|
128
|
-
export interface DockerSandboxServiceOptions {
|
|
129
|
-
settingsManager?: Pick<SettingsManager, "getSandboxSettings">;
|
|
130
|
-
env?: SandboxEnvironment;
|
|
131
|
-
runner?: DockerRunner;
|
|
132
|
-
tokenGenerator?: () => string;
|
|
133
|
-
portAllocator?: (host: string, preferredPort: number) => Promise<number>;
|
|
134
|
-
readinessWaiter?: (host: string, port: number) => Promise<void>;
|
|
135
|
-
managedContainers?: Map<string, ManagedSandboxContainer>;
|
|
136
|
-
}
|
|
137
|
-
export declare function allocateSandboxDaemonPort(host: string, preferredPort: number): Promise<number>;
|
|
138
|
-
export declare function redactSecrets(value: string): string;
|
|
139
|
-
export declare function resolveSandboxConfig(settings: SandboxSettings | undefined, env?: SandboxEnvironment, overrides?: SandboxConfigOverrides): SandboxConfig;
|
|
140
|
-
export declare function createSandboxLabels(config: SandboxConfig, workspaceRoot: string, sessionId: string): Record<string, string>;
|
|
141
|
-
export declare function createSandboxContainerName(config: SandboxConfig, workspaceRoot: string, sessionId: string, instanceId?: string): string;
|
|
142
|
-
export declare function buildDockerRunInvocation(config: SandboxConfig, options: SandboxStartOptions, token?: string): DockerRunInvocation;
|
|
143
|
-
export declare class DockerSandboxService {
|
|
144
|
-
private readonly settingsManager;
|
|
145
|
-
private readonly env;
|
|
146
|
-
private readonly runner;
|
|
147
|
-
private readonly tokenGenerator;
|
|
148
|
-
private readonly portAllocator;
|
|
149
|
-
private readonly readinessWaiter;
|
|
150
|
-
private readonly managedContainers;
|
|
151
|
-
constructor(options?: DockerSandboxServiceOptions);
|
|
152
|
-
resolveConfig(overrides?: SandboxConfigOverrides): SandboxConfig;
|
|
153
|
-
buildStartInvocation(options: SandboxStartOptions): DockerRunInvocation;
|
|
154
|
-
checkDockerAvailable(config?: SandboxConfig): Promise<void>;
|
|
155
|
-
private reserveDaemonPort;
|
|
156
|
-
private waitForStartedSandbox;
|
|
157
|
-
private startWithReservedPort;
|
|
158
|
-
private startLocked;
|
|
159
|
-
start(options: SandboxStartOptions): Promise<SandboxStartResult>;
|
|
160
|
-
list(options: SandboxListOptions): Promise<SandboxContainer[]>;
|
|
161
|
-
private forgetManagedContainer;
|
|
162
|
-
private forgetStoppedContainer;
|
|
163
|
-
stop(options: SandboxStopOptions): Promise<SandboxStopResult>;
|
|
164
|
-
stopManagedContainers(): Promise<void>;
|
|
165
|
-
private containerFromPsRecord;
|
|
166
|
-
}
|
|
167
|
-
export declare function parseDockerPortOutput(output: string): string;
|
|
168
|
-
export declare function parseDockerInspectContainers(output: string, daemonPort?: number): SandboxContainer[];
|
|
169
|
-
//# sourceMappingURL=docker.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"docker.d.ts","sourceRoot":"","sources":["../../../src/core/sandbox/docker.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash, randomBytes } from \"node:crypto\";\nimport { open, readFile, stat, unlink } from \"node:fs/promises\";\nimport { createConnection, createServer } from \"node:net\";\nimport { tmpdir } from \"node:os\";\nimport { basename, join, resolve } from \"node:path\";\nimport { VERSION } from \"../../config.ts\";\nimport { DAEMON_WEBSOCKET_PATH } from \"../../daemon/config.ts\";\nimport { spawnProcess } from \"../../utils/child-process.ts\";\nimport type { SandboxSettings, SettingsManager } from \"../settings-manager.ts\";\n\nexport const DEFAULT_SANDBOX_IMAGE = \"ghcr.io/greppleai/grip-sandbox:latest\";\nexport const DEFAULT_SANDBOX_WORKSPACE_MOUNT = \"/workspace\";\nexport const DEFAULT_SANDBOX_DAEMON_PORT = 8787;\nexport const DEFAULT_SANDBOX_DOCKER_BINARY = \"docker\";\nexport const DEFAULT_SANDBOX_LIMA_BINARY = \"limactl\";\nexport const DEFAULT_SANDBOX_LIMA_TEMPLATE = \"template:docker-rootful\";\nexport const SANDBOX_LABEL_PREFIX = \"ai.greppleai.grip\";\nexport const SANDBOX_LABEL_ENABLED = `${SANDBOX_LABEL_PREFIX}.sandbox`;\nexport const SANDBOX_LABEL_WORKSPACE_HASH = `${SANDBOX_LABEL_PREFIX}.workspace-hash`;\nexport const SANDBOX_LABEL_WORKSPACE_MOUNT = `${SANDBOX_LABEL_PREFIX}.workspace-mount`;\nexport const SANDBOX_LABEL_DAEMON_PORT = `${SANDBOX_LABEL_PREFIX}.daemon-port`;\nexport const SANDBOX_LABEL_OWNER_UID = `${SANDBOX_LABEL_PREFIX}.owner-uid`;\nexport const SANDBOX_LABEL_VERSION = `${SANDBOX_LABEL_PREFIX}.version`;\nexport const SANDBOX_LABEL_SESSION = `${SANDBOX_LABEL_PREFIX}.session`;\n\nconst reservedSandboxDaemonPorts = new Set<number>();\nconst sandboxPortLockPath = join(tmpdir(), `greppleai-grip-sandbox-port-${process.getuid?.() ?? \"user\"}.lock`);\nlet sandboxStartQueue: Promise<void> = Promise.resolve();\n\nfunction isProcessAlive(pid: number): boolean {\n\ttry {\n\t\tprocess.kill(pid, 0);\n\t\treturn true;\n\t} catch (error) {\n\t\treturn error instanceof Error && \"code\" in error && error.code === \"EPERM\";\n\t}\n}\nasync function isSandboxPortLockStale(): Promise<boolean> {\n\ttry {\n\t\tconst owner = JSON.parse(await readFile(sandboxPortLockPath, \"utf8\")) as { pid?: unknown };\n\t\treturn typeof owner.pid === \"number\" && !isProcessAlive(owner.pid);\n\t} catch {\n\t\ttry {\n\t\t\treturn Date.now() - (await stat(sandboxPortLockPath)).mtimeMs > 10_000;\n\t\t} catch {\n\t\t\treturn false;\n\t\t}\n\t}\n}\n\nasync function acquireSandboxPortLock(): Promise<() => Promise<void>> {\n\tconst token = `${process.pid}:${randomBytes(16).toString(\"hex\")}`;\n\tconst deadline = Date.now() + 30_000;\n\twhile (Date.now() < deadline) {\n\t\ttry {\n\t\t\tconst handle = await open(sandboxPortLockPath, \"wx\", 0o600);\n\t\t\tawait handle.writeFile(JSON.stringify({ pid: process.pid, token }));\n\t\t\treturn async () => {\n\t\t\t\tawait handle.close();\n\t\t\t\ttry {\n\t\t\t\t\tconst current = JSON.parse(await readFile(sandboxPortLockPath, \"utf8\")) as { token?: unknown };\n\t\t\t\t\tif (current.token === token) await unlink(sandboxPortLockPath);\n\t\t\t\t} catch {}\n\t\t\t};\n\t\t} catch (error) {\n\t\t\tif (!(error instanceof Error && \"code\" in error && error.code === \"EEXIST\")) throw error;\n\t\t\tconst stale = await isSandboxPortLockStale();\n\t\t\tif (stale) {\n\t\t\t\tawait unlink(sandboxPortLockPath).catch(() => undefined);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tawait new Promise((resolveDelay) => setTimeout(resolveDelay, 50));\n\t\t}\n\t}\n\tthrow new Error(\"Timed out waiting for the sandbox daemon port allocation lock\");\n}\n\nasync function withSandboxStartLock<T>(operation: () => Promise<T>): Promise<T> {\n\tconst previous = sandboxStartQueue;\n\tlet release!: () => void;\n\tsandboxStartQueue = new Promise<void>((resolveQueue) => {\n\t\trelease = resolveQueue;\n\t});\n\tawait previous;\n\tlet releaseFileLock: (() => Promise<void>) | undefined;\n\ttry {\n\t\treleaseFileLock = await acquireSandboxPortLock();\n\t\treturn await operation();\n\t} finally {\n\t\ttry {\n\t\t\tawait releaseFileLock?.();\n\t\t} finally {\n\t\t\trelease();\n\t\t}\n\t}\n}\n\nexport type SandboxCleanupBehavior = \"stop\" | \"remove\";\nexport type SandboxRuntime = \"docker\" | \"lima\";\n\nexport interface SandboxConfig {\n\truntime: SandboxRuntime;\n\timage: string;\n\tdockerBinary: string;\n\tlimaBinary: string;\n\tlimaTemplate: string;\n\tlimaInstanceNamePrefix: string;\n\tworkspaceMountPath: string;\n\tcontainerNamePrefix: string;\n\tdaemonPort: number;\n\tdaemonHostBind: string;\n\tcleanup: SandboxCleanupBehavior;\n\townerUid: string | undefined;\n}\n\nexport type SandboxConfigOverrides = SandboxSettings;\n\nexport interface SandboxEnvironment {\n\tGRIP_SANDBOX_RUNTIME?: string;\n\tGRIP_SANDBOX_IMAGE?: string;\n\tGRIP_SANDBOX_DOCKER?: string;\n\tGRIP_SANDBOX_LIMA?: string;\n\tGRIP_SANDBOX_LIMA_TEMPLATE?: string;\n\tGRIP_SANDBOX_LIMA_INSTANCE_PREFIX?: string;\n\tGRIP_SANDBOX_WORKSPACE_MOUNT?: string;\n\tGRIP_SANDBOX_CONTAINER_PREFIX?: string;\n\tGRIP_SANDBOX_DAEMON_PORT?: string;\n\tGRIP_SANDBOX_DAEMON_HOST_BIND?: string;\n\tGRIP_SANDBOX_CLEANUP?: string;\n}\n\nexport interface SandboxStartOptions extends SandboxConfigOverrides {\n\tworkspaceRoot: string;\n\tsessionId?: string;\n\tname?: string;\n}\n\nexport interface SandboxListOptions {\n\tworkspaceRoot: string;\n\tallWorkspaces?: boolean;\n}\n\nexport interface SandboxStopOptions {\n\tworkspaceRoot: string;\n\ttarget?: string;\n\tcurrentContainerId?: string;\n}\n\nexport interface DockerRunInvocation {\n\tcommand: string;\n\targs: string[];\n\tenv: Record<string, string>;\n\ttoken: string;\n\tcontainerName: string;\n\tlabels: Record<string, string>;\n\tworkspaceRoot: string;\n\tworkspaceHash: string;\n}\n\nexport interface SandboxContainer {\n\tid: string;\n\tname: string;\n\timage: string;\n\tstatus: string;\n\tstate: string | undefined;\n\tcreatedAt: string | undefined;\n\tworkspaceRoot: string | undefined;\n\tworkspaceMountPath: string | undefined;\n\tdaemonPort: number | undefined;\n\tdaemonEndpoint: string | undefined;\n\tlabels: Record<string, string>;\n}\n\nexport interface SandboxStartResult {\n\tcontainerId: string;\n\tcontainerName: string;\n\tworkspaceRoot: string;\n\tworkspaceMountPath: string;\n\tdaemonUrl: string;\n\tdaemonUrlRedacted: string;\n\ttoken: string;\n\tlabels: Record<string, string>;\n}\n\nexport type SandboxStopResult =\n\t| { status: \"stopped\"; container: SandboxContainer }\n\t| { status: \"removed\"; container: SandboxContainer }\n\t| { status: \"already-stopped\"; container: SandboxContainer }\n\t| { status: \"not-found\"; message: string };\n\nexport interface SandboxService {\n\tresolveConfig(overrides?: SandboxConfigOverrides): SandboxConfig;\n\tstart(options: SandboxStartOptions): Promise<SandboxStartResult>;\n\tlist(options: SandboxListOptions): Promise<SandboxContainer[]>;\n\tstop(options: SandboxStopOptions): Promise<SandboxStopResult>;\n}\n\nexport interface DockerCommandResult {\n\texitCode: number | null;\n\tstdout: string;\n\tstderr: string;\n}\n\nexport interface DockerCommandOptions {\n\tenv?: Record<string, string>;\n}\n\nexport interface DockerRunner {\n\trun(command: string, args: string[], options?: DockerCommandOptions): Promise<DockerCommandResult>;\n}\nexport interface ManagedSandboxContainer {\n\tworkspaceRoot: string;\n\tdaemonPort: number;\n\townerId?: string;\n}\n\nexport interface DockerSandboxServiceOptions {\n\tsettingsManager?: Pick<SettingsManager, \"getSandboxSettings\">;\n\tenv?: SandboxEnvironment;\n\trunner?: DockerRunner;\n\ttokenGenerator?: () => string;\n\tportAllocator?: (host: string, preferredPort: number) => Promise<number>;\n\treadinessWaiter?: (host: string, port: number) => Promise<void>;\n\tmanagedContainers?: Map<string, ManagedSandboxContainer>;\n}\n\ninterface DockerPsRecord {\n\tID?: string;\n\tNames?: string;\n\tImage?: string;\n\tStatus?: string;\n\tState?: string;\n\tCreatedAt?: string;\n\tLabels?: string;\n\tPorts?: string;\n}\n\ninterface DockerInspectMount {\n\tType?: string;\n\tSource?: string;\n\tDestination?: string;\n}\n\ninterface DockerInspectPortBinding {\n\tHostIp?: string;\n\tHostPort?: string;\n}\n\ninterface DockerInspectConfig {\n\tImage?: string;\n\tLabels?: Record<string, string>;\n}\n\ninterface DockerInspectState {\n\tStatus?: string;\n\tRunning?: boolean;\n\tStartedAt?: string;\n}\n\ninterface DockerInspectNetworkSettings {\n\tPorts?: Record<string, DockerInspectPortBinding[] | null>;\n}\n\ninterface DockerInspectRecord {\n\tId?: string;\n\tName?: string;\n\tConfig?: DockerInspectConfig;\n\tState?: DockerInspectState;\n\tCreated?: string;\n\tMounts?: DockerInspectMount[];\n\tNetworkSettings?: DockerInspectNetworkSettings;\n}\n\nclass ProcessDockerRunner implements DockerRunner {\n\trun(command: string, args: string[], options: DockerCommandOptions = {}): Promise<DockerCommandResult> {\n\t\treturn new Promise((resolveCommand, reject) => {\n\t\t\tconst child = spawnProcess(command, args, {\n\t\t\t\tstdio: [\"ignore\", \"pipe\", \"pipe\"],\n\t\t\t\tenv: { ...process.env, ...options.env },\n\t\t\t});\n\t\t\tlet stdout = \"\";\n\t\t\tlet stderr = \"\";\n\t\t\tchild.stdout?.on(\"data\", (chunk: Buffer) => {\n\t\t\t\tstdout += chunk.toString(\"utf8\");\n\t\t\t});\n\t\t\tchild.stderr?.on(\"data\", (chunk: Buffer) => {\n\t\t\t\tstderr += chunk.toString(\"utf8\");\n\t\t\t});\n\t\t\tchild.once(\"error\", reject);\n\t\t\tchild.once(\"close\", (exitCode) => resolveCommand({ exitCode, stdout, stderr }));\n\t\t});\n\t}\n}\n\nfunction nonEmpty(value: string | undefined): string | undefined {\n\tconst trimmed = value?.trim();\n\treturn trimmed ? trimmed : undefined;\n}\n\nfunction parseRuntime(value: string | undefined, settingName: string): SandboxRuntime | undefined {\n\tif (value === undefined) return undefined;\n\tif (value === \"docker\" || value === \"lima\") return value;\n\tthrow new Error(`Invalid ${settingName}: expected docker or lima`);\n}\n\nfunction parsePositiveInteger(value: string | number | undefined, settingName: string): number | undefined {\n\tif (value === undefined) return undefined;\n\tconst text = String(value).trim();\n\tif (!/^\\d+$/.test(text)) throw new Error(`Invalid ${settingName}: expected a decimal integer`);\n\tconst parsed = Number(text);\n\tif (!Number.isSafeInteger(parsed) || parsed < 1 || parsed > 65535) {\n\t\tthrow new Error(`Invalid ${settingName}: expected a port from 1 to 65535`);\n\t}\n\treturn parsed;\n}\n\nfunction parseCleanup(value: string | undefined, settingName: string): SandboxCleanupBehavior | undefined {\n\tif (value === undefined) return undefined;\n\tif (value === \"stop\" || value === \"remove\") return value;\n\tthrow new Error(`Invalid ${settingName}: expected stop or remove`);\n}\n\nfunction parseLabels(text: string | undefined): Record<string, string> {\n\tconst labels: Record<string, string> = {};\n\tif (!text) return labels;\n\tfor (const entry of text.split(\",\")) {\n\t\tconst separator = entry.indexOf(\"=\");\n\t\tif (separator === -1) continue;\n\t\tlabels[entry.slice(0, separator)] = entry.slice(separator + 1);\n\t}\n\treturn labels;\n}\n\nfunction sanitizeNameSegment(value: string): string {\n\tconst sanitized = value\n\t\t.toLowerCase()\n\t\t.replace(/[^a-z0-9_.-]+/g, \"-\")\n\t\t.replace(/^-+|-+$/g, \"\");\n\treturn sanitized || \"workspace\";\n}\n\nfunction getWorkspaceHash(workspaceRoot: string): string {\n\treturn createHash(\"sha256\").update(resolve(workspaceRoot)).digest(\"hex\").slice(0, 16);\n}\n\nfunction getOwnerUid(): string | undefined {\n\tconst uid = process.getuid?.();\n\treturn uid === undefined ? undefined : String(uid);\n}\n\nfunction generateToken(): string {\n\treturn randomBytes(32).toString(\"base64url\");\n}\n\nasync function tryReservePort(host: string, port: number): Promise<number | undefined> {\n\treturn new Promise((resolvePort) => {\n\t\tconst server = createServer();\n\t\tserver.unref();\n\t\tserver.once(\"error\", () => resolvePort(undefined));\n\t\tserver.listen({ host, port, exclusive: true }, () => {\n\t\t\tconst address = server.address();\n\t\t\tconst selectedPort = typeof address === \"object\" && address ? address.port : undefined;\n\t\t\tserver.close(() => resolvePort(selectedPort));\n\t\t});\n\t});\n}\n\nexport async function allocateSandboxDaemonPort(host: string, preferredPort: number): Promise<number> {\n\tconst preferred = await tryReservePort(host, preferredPort);\n\tif (preferred !== undefined) return preferred;\n\tconst fallback = await tryReservePort(host, 0);\n\tif (fallback === undefined) throw new Error(`Unable to allocate a sandbox daemon port on ${host}`);\n\treturn fallback;\n}\n\nasync function canConnect(host: string, port: number): Promise<boolean> {\n\treturn new Promise((resolveConnection) => {\n\t\tconst socket = createConnection({ host, port });\n\t\tlet settled = false;\n\t\tconst finish = (connected: boolean): void => {\n\t\t\tif (settled) return;\n\t\t\tsettled = true;\n\t\t\tsocket.destroy();\n\t\t\tresolveConnection(connected);\n\t\t};\n\t\tsocket.setTimeout(250, () => finish(false));\n\t\tsocket.once(\"connect\", () => finish(true));\n\t\tsocket.once(\"error\", () => finish(false));\n\t});\n}\n\nasync function waitForSandboxDaemonPort(host: string, port: number): Promise<void> {\n\tconst connectHost = host === \"0.0.0.0\" ? \"127.0.0.1\" : host;\n\tconst deadline = Date.now() + 15_000;\n\twhile (Date.now() < deadline) {\n\t\tif (await canConnect(connectHost, port)) return;\n\t\tawait new Promise((resolveDelay) => setTimeout(resolveDelay, 100));\n\t}\n\tthrow new Error(`Sandbox daemon did not bind ${connectHost}:${port} within 15 seconds`);\n}\n\nfunction ensureSuccessful(result: DockerCommandResult, args: string[]): void {\n\tif (result.exitCode === 0) return;\n\tconst detail = result.stderr.trim() || result.stdout.trim() || `exit code ${result.exitCode ?? \"unknown\"}`;\n\tthrow new Error(redactSecrets(`Docker command failed: docker ${args.join(\" \")}: ${detail}`));\n}\n\nfunction parseJsonLines<T>(text: string): T[] {\n\treturn text\n\t\t.split(/\\r?\\n/)\n\t\t.map((line) => line.trim())\n\t\t.filter((line) => line.length > 0)\n\t\t.map((line) => JSON.parse(line) as T);\n}\n\nfunction parseJsonArray<T>(text: string): T[] {\n\tconst trimmed = text.trim();\n\tif (!trimmed) return [];\n\tconst parsed = JSON.parse(trimmed) as unknown;\n\treturn Array.isArray(parsed) ? (parsed as T[]) : [parsed as T];\n}\n\nfunction getEndpointFromPorts(\n\tports: Record<string, DockerInspectPortBinding[] | null> | undefined,\n\tdaemonPort: number,\n): string | undefined {\n\tconst bindings = ports?.[`${daemonPort}/tcp`];\n\tconst binding = bindings?.[0];\n\tif (!binding?.HostPort) return undefined;\n\tconst host = daemonConnectionHost(binding.HostIp || \"127.0.0.1\");\n\treturn `ws://${urlHost(host)}:${binding.HostPort}${DAEMON_WEBSOCKET_PATH}`;\n}\n\nexport function redactSecrets(value: string): string {\n\treturn value\n\t\t.replace(/(GRIP_DAEMON_TOKEN=)[^\\s,;]+/g, \"$1[REDACTED]\")\n\t\t.replace(/(PI_DAEMON_TOKEN=)[^\\s,;]+/g, \"$1[REDACTED]\")\n\t\t.replace(/(Authorization:\\s*Bearer\\s+)[^\\s,;]+/gi, \"$1[REDACTED]\")\n\t\t.replace(/([?&]token=)[^\\s&]+/gi, \"$1[REDACTED]\");\n}\n\nexport function resolveSandboxConfig(\n\tsettings: SandboxSettings | undefined,\n\tenv: SandboxEnvironment = {},\n\toverrides: SandboxConfigOverrides = {},\n): SandboxConfig {\n\tconst runtime =\n\t\tparseRuntime(overrides.runtime, \"sandbox.runtime\") ??\n\t\tparseRuntime(env.GRIP_SANDBOX_RUNTIME, \"GRIP_SANDBOX_RUNTIME\") ??\n\t\tparseRuntime(settings?.runtime, \"sandbox.runtime\") ??\n\t\t\"docker\";\n\tconst image =\n\t\tnonEmpty(overrides.image) ??\n\t\tnonEmpty(env.GRIP_SANDBOX_IMAGE) ??\n\t\tnonEmpty(settings?.image) ??\n\t\tDEFAULT_SANDBOX_IMAGE;\n\tconst dockerBinary =\n\t\tnonEmpty(overrides.dockerBinary) ??\n\t\tnonEmpty(env.GRIP_SANDBOX_DOCKER) ??\n\t\tnonEmpty(settings?.dockerBinary) ??\n\t\tDEFAULT_SANDBOX_DOCKER_BINARY;\n\tconst limaBinary =\n\t\tnonEmpty(overrides.limaBinary) ??\n\t\tnonEmpty(env.GRIP_SANDBOX_LIMA) ??\n\t\tnonEmpty(settings?.limaBinary) ??\n\t\tDEFAULT_SANDBOX_LIMA_BINARY;\n\tconst limaTemplate =\n\t\tnonEmpty(overrides.limaTemplate) ??\n\t\tnonEmpty(env.GRIP_SANDBOX_LIMA_TEMPLATE) ??\n\t\tnonEmpty(settings?.limaTemplate) ??\n\t\tDEFAULT_SANDBOX_LIMA_TEMPLATE;\n\tconst limaInstanceNamePrefix =\n\t\tnonEmpty(overrides.limaInstanceNamePrefix) ??\n\t\tnonEmpty(env.GRIP_SANDBOX_LIMA_INSTANCE_PREFIX) ??\n\t\tnonEmpty(settings?.limaInstanceNamePrefix) ??\n\t\t\"grip-sandbox\";\n\tconst workspaceMountPath =\n\t\tnonEmpty(overrides.workspaceMountPath) ??\n\t\tnonEmpty(env.GRIP_SANDBOX_WORKSPACE_MOUNT) ??\n\t\tnonEmpty(settings?.workspaceMountPath) ??\n\t\tDEFAULT_SANDBOX_WORKSPACE_MOUNT;\n\tconst containerNamePrefix =\n\t\tnonEmpty(overrides.containerNamePrefix) ??\n\t\tnonEmpty(env.GRIP_SANDBOX_CONTAINER_PREFIX) ??\n\t\tnonEmpty(settings?.containerNamePrefix) ??\n\t\t\"grip-sandbox\";\n\tconst daemonPort =\n\t\toverrides.daemonPort ??\n\t\tparsePositiveInteger(env.GRIP_SANDBOX_DAEMON_PORT, \"GRIP_SANDBOX_DAEMON_PORT\") ??\n\t\tparsePositiveInteger(settings?.daemonPort, \"sandbox.daemonPort\") ??\n\t\tDEFAULT_SANDBOX_DAEMON_PORT;\n\tconst daemonHostBind =\n\t\tnonEmpty(overrides.daemonHostBind) ??\n\t\tnonEmpty(env.GRIP_SANDBOX_DAEMON_HOST_BIND) ??\n\t\tnonEmpty(settings?.daemonHostBind) ??\n\t\t\"127.0.0.1\";\n\tconst cleanup =\n\t\toverrides.cleanup ??\n\t\tparseCleanup(env.GRIP_SANDBOX_CLEANUP, \"GRIP_SANDBOX_CLEANUP\") ??\n\t\tparseCleanup(settings?.cleanup, \"sandbox.cleanup\") ??\n\t\t\"stop\";\n\treturn {\n\t\truntime,\n\t\timage,\n\t\tdockerBinary,\n\t\tlimaBinary,\n\t\tlimaTemplate,\n\t\tlimaInstanceNamePrefix,\n\t\tworkspaceMountPath,\n\t\tcontainerNamePrefix,\n\t\tdaemonPort,\n\t\tdaemonHostBind,\n\t\tcleanup,\n\t\townerUid: getOwnerUid(),\n\t};\n}\n\nfunction dockerPublishHost(host: string): string {\n\treturn host.includes(\":\") && !host.startsWith(\"[\") ? `[${host}]` : host;\n}\n\nfunction daemonConnectionHost(host: string): string {\n\tif (host === \"0.0.0.0\") return \"127.0.0.1\";\n\tif (host === \"::\") return \"::1\";\n\treturn host;\n}\n\nfunction urlHost(host: string): string {\n\treturn host.includes(\":\") && !host.startsWith(\"[\") ? `[${host}]` : host;\n}\n\nexport function createSandboxLabels(\n\tconfig: SandboxConfig,\n\tworkspaceRoot: string,\n\tsessionId: string,\n): Record<string, string> {\n\tconst labels: Record<string, string> = {\n\t\t[SANDBOX_LABEL_ENABLED]: \"true\",\n\t\t[SANDBOX_LABEL_WORKSPACE_HASH]: getWorkspaceHash(workspaceRoot),\n\t\t[SANDBOX_LABEL_WORKSPACE_MOUNT]: config.workspaceMountPath,\n\t\t[SANDBOX_LABEL_DAEMON_PORT]: String(config.daemonPort),\n\t\t[SANDBOX_LABEL_VERSION]: VERSION,\n\t\t[SANDBOX_LABEL_SESSION]: sessionId,\n\t};\n\tif (config.ownerUid) labels[SANDBOX_LABEL_OWNER_UID] = config.ownerUid;\n\treturn labels;\n}\n\nexport function createSandboxContainerName(\n\tconfig: SandboxConfig,\n\tworkspaceRoot: string,\n\tsessionId: string,\n\tinstanceId?: string,\n): string {\n\tconst base = `${sanitizeNameSegment(config.containerNamePrefix)}-${sanitizeNameSegment(basename(resolve(workspaceRoot)))}-${getWorkspaceHash(workspaceRoot)}-${sanitizeNameSegment(sessionId).slice(0, 16)}`;\n\treturn instanceId ? `${base}-${sanitizeNameSegment(instanceId).slice(0, 8)}` : base;\n}\n\nexport function buildDockerRunInvocation(\n\tconfig: SandboxConfig,\n\toptions: SandboxStartOptions,\n\ttoken: string = generateToken(),\n): DockerRunInvocation {\n\tconst workspaceRoot = resolve(options.workspaceRoot);\n\tconst sessionId = options.sessionId ?? randomBytes(8).toString(\"hex\");\n\tconst labels = createSandboxLabels(config, workspaceRoot, sessionId);\n\tconst containerName = createSandboxContainerName(\n\t\tconfig,\n\t\tworkspaceRoot,\n\t\tsessionId,\n\t\tcreateHash(\"sha256\").update(token).digest(\"hex\"),\n\t);\n\tconst args = [\n\t\t\"run\",\n\t\t\"--detach\",\n\t\t\"--network\",\n\t\t\"bridge\",\n\t\t\"--publish\",\n\t\t`${dockerPublishHost(config.daemonHostBind)}:${config.daemonPort}:${config.daemonPort}`,\n\t\t\"--name\",\n\t\tcontainerName,\n\t\t\"--workdir\",\n\t\tconfig.workspaceMountPath,\n\t\t\"--mount\",\n\t\t`type=bind,source=${workspaceRoot},target=${config.workspaceMountPath}`,\n\t\t\"--env\",\n\t\t\"GRIP_DAEMON_TOKEN\",\n\t];\n\tfor (const [key, value] of Object.entries(labels)) args.push(\"--label\", `${key}=${value}`);\n\targs.push(\n\t\tconfig.image,\n\t\t\"grip\",\n\t\t\"--daemon\",\n\t\t\"--daemon-host\",\n\t\t\"0.0.0.0\",\n\t\t\"--daemon-allow-insecure-transport\",\n\t\t\"--daemon-port\",\n\t\tString(config.daemonPort),\n\t\t\"--daemon-cwd\",\n\t\tconfig.workspaceMountPath,\n\t\t\"--daemon-allow-root\",\n\t\t\"--daemon-allow-process-exec\",\n\t);\n\treturn {\n\t\tcommand: config.dockerBinary,\n\t\targs,\n\t\tenv: { GRIP_DAEMON_TOKEN: token },\n\t\ttoken,\n\t\tcontainerName,\n\t\tlabels,\n\t\tworkspaceRoot,\n\t\tworkspaceHash: labels[SANDBOX_LABEL_WORKSPACE_HASH],\n\t};\n}\n\nexport class DockerSandboxService {\n\tprivate readonly settingsManager: Pick<SettingsManager, \"getSandboxSettings\"> | undefined;\n\tprivate readonly env: SandboxEnvironment;\n\tprivate readonly runner: DockerRunner;\n\tprivate readonly tokenGenerator: () => string;\n\tprivate readonly portAllocator: (host: string, preferredPort: number) => Promise<number>;\n\tprivate readonly readinessWaiter: (host: string, port: number) => Promise<void>;\n\tprivate readonly managedContainers: Map<string, ManagedSandboxContainer>;\n\n\tconstructor(options: DockerSandboxServiceOptions = {}) {\n\t\tthis.settingsManager = options.settingsManager;\n\t\tthis.env = options.env ?? process.env;\n\t\tthis.runner = options.runner ?? new ProcessDockerRunner();\n\t\tthis.tokenGenerator = options.tokenGenerator ?? generateToken;\n\t\tthis.portAllocator = options.portAllocator ?? allocateSandboxDaemonPort;\n\t\tthis.readinessWaiter = options.readinessWaiter ?? waitForSandboxDaemonPort;\n\t\tthis.managedContainers = options.managedContainers ?? new Map();\n\t}\n\n\tresolveConfig(overrides: SandboxConfigOverrides = {}): SandboxConfig {\n\t\treturn resolveSandboxConfig(this.settingsManager?.getSandboxSettings(), this.env, overrides);\n\t}\n\n\tbuildStartInvocation(options: SandboxStartOptions): DockerRunInvocation {\n\t\treturn buildDockerRunInvocation(this.resolveConfig(options), options, this.tokenGenerator());\n\t}\n\n\tasync checkDockerAvailable(config: SandboxConfig = this.resolveConfig()): Promise<void> {\n\t\tconst args = [\"version\", \"--format\", \"{{.Server.Version}}\"];\n\t\ttry {\n\t\t\tensureSuccessful(await this.runner.run(config.dockerBinary, args), args);\n\t\t} catch (error) {\n\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\tthrow new Error(\n\t\t\t\tredactSecrets(`Docker is unavailable or not reachable via ${config.dockerBinary}: ${message}`),\n\t\t\t);\n\t\t}\n\t}\n\n\tprivate async reserveDaemonPort(config: SandboxConfig): Promise<number> {\n\t\tfor (let attempt = 0; attempt < 16; attempt++) {\n\t\t\tconst preferredPort = reservedSandboxDaemonPorts.has(config.daemonPort) ? 0 : config.daemonPort;\n\t\t\tconst candidate = await this.portAllocator(config.daemonHostBind, preferredPort);\n\t\t\tif (!reservedSandboxDaemonPorts.has(candidate)) return candidate;\n\t\t}\n\t\tthrow new Error(\"Unable to reserve a unique sandbox daemon port\");\n\t}\n\n\tprivate async waitForStartedSandbox(config: SandboxConfig, containerId: string): Promise<void> {\n\t\ttry {\n\t\t\tawait this.readinessWaiter(config.daemonHostBind, config.daemonPort);\n\t\t} catch (error) {\n\t\t\tconst cleanupArgs = [\"rm\", \"--force\", containerId];\n\t\t\tconst cleanupResult = await this.runner.run(config.dockerBinary, cleanupArgs);\n\t\t\tif (cleanupResult.exitCode !== 0) {\n\t\t\t\tconst cleanupError = new Error(\n\t\t\t\t\tcleanupResult.stderr.trim() || cleanupResult.stdout.trim() || \"Failed to remove unready sandbox\",\n\t\t\t\t);\n\t\t\t\tthrow new AggregateError([error, cleanupError], \"Sandbox readiness and cleanup both failed\");\n\t\t\t}\n\t\t\tthis.managedContainers.delete(containerId);\n\t\t\tthrow error;\n\t\t}\n\t}\n\n\tprivate async startWithReservedPort(options: SandboxStartOptions, daemonPort: number): Promise<SandboxStartResult> {\n\t\tconst effectiveOptions = { ...options, daemonPort };\n\t\tconst config = this.resolveConfig(effectiveOptions);\n\t\tconst invocation = buildDockerRunInvocation(config, effectiveOptions, this.tokenGenerator());\n\t\tawait this.checkDockerAvailable(config);\n\t\tconst runResult = await this.runner.run(invocation.command, invocation.args, { env: invocation.env });\n\t\tensureSuccessful(runResult, invocation.args);\n\t\tconst containerId = runResult.stdout.trim();\n\t\tthis.managedContainers.set(containerId, {\n\t\t\tworkspaceRoot: invocation.workspaceRoot,\n\t\t\tdaemonPort: config.daemonPort,\n\t\t\townerId: options.sessionId,\n\t\t});\n\t\tawait this.waitForStartedSandbox(config, containerId);\n\t\tconst daemonHost = daemonConnectionHost(config.daemonHostBind);\n\t\tconst daemonUrl = `ws://${urlHost(daemonHost)}:${config.daemonPort}${DAEMON_WEBSOCKET_PATH}`;\n\t\treturn {\n\t\t\tcontainerId,\n\t\t\tcontainerName: invocation.containerName,\n\t\t\tworkspaceRoot: invocation.workspaceRoot,\n\t\t\tworkspaceMountPath: config.workspaceMountPath,\n\t\t\tdaemonUrl,\n\t\t\tdaemonUrlRedacted: redactSecrets(daemonUrl),\n\t\t\ttoken: invocation.token,\n\t\t\tlabels: invocation.labels,\n\t\t};\n\t}\n\n\tprivate async startLocked(options: SandboxStartOptions): Promise<SandboxStartResult> {\n\t\tconst configured = this.resolveConfig(options);\n\t\tconst daemonPort = await this.reserveDaemonPort(configured);\n\t\treservedSandboxDaemonPorts.add(daemonPort);\n\t\ttry {\n\t\t\treturn await this.startWithReservedPort(options, daemonPort);\n\t\t} catch (error) {\n\t\t\tif (![...this.managedContainers.values()].some((container) => container.daemonPort === daemonPort)) {\n\t\t\t\treservedSandboxDaemonPorts.delete(daemonPort);\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t}\n\n\tasync start(options: SandboxStartOptions): Promise<SandboxStartResult> {\n\t\treturn withSandboxStartLock(() => this.startLocked(options));\n\t}\n\n\tasync list(options: SandboxListOptions): Promise<SandboxContainer[]> {\n\t\tconst config = this.resolveConfig();\n\t\tconst args = [\n\t\t\t\"container\",\n\t\t\t\"ls\",\n\t\t\t\"--all\",\n\t\t\t\"--filter\",\n\t\t\t`label=${SANDBOX_LABEL_ENABLED}=true`,\n\t\t\t...(config.ownerUid ? [\"--filter\", `label=${SANDBOX_LABEL_OWNER_UID}=${config.ownerUid}`] : []),\n\t\t];\n\t\tif (!options.allWorkspaces) {\n\t\t\targs.push(\"--filter\", `label=${SANDBOX_LABEL_WORKSPACE_HASH}=${getWorkspaceHash(options.workspaceRoot)}`);\n\t\t}\n\t\targs.push(\"--format\", \"{{json .}}\");\n\t\tconst result = await this.runner.run(config.dockerBinary, args);\n\t\tensureSuccessful(result, args);\n\t\tconst rows = parseJsonLines<DockerPsRecord>(result.stdout);\n\t\treturn rows.filter((row) => row.ID || row.Names).map((row) => this.containerFromPsRecord(row));\n\t}\n\tprivate forgetManagedContainer(reference: string, workspaceRoot: string): void {\n\t\tconst canonicalWorkspaceRoot = resolve(workspaceRoot);\n\t\tfor (const [containerId, container] of this.managedContainers) {\n\t\t\tif (container.workspaceRoot === canonicalWorkspaceRoot && containerIdsMatch(containerId, reference)) {\n\t\t\t\tthis.managedContainers.delete(containerId);\n\t\t\t\treservedSandboxDaemonPorts.delete(container.daemonPort);\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate forgetStoppedContainer(container: SandboxContainer, workspaceRoot: string): void {\n\t\tthis.forgetManagedContainer(container.id, workspaceRoot);\n\t\tif (container.daemonPort !== undefined) reservedSandboxDaemonPorts.delete(container.daemonPort);\n\t}\n\n\tasync stop(options: SandboxStopOptions): Promise<SandboxStopResult> {\n\t\tconst containers = await this.list({ workspaceRoot: options.workspaceRoot });\n\t\tconst selected = selectStopTarget(containers, options.target, options.currentContainerId);\n\t\tif (!selected) {\n\t\t\tconst requestedId = options.currentContainerId ?? options.target;\n\t\t\tif (requestedId) this.forgetManagedContainer(requestedId, options.workspaceRoot);\n\t\t\treturn { status: \"not-found\", message: \"No matching Grip sandbox container found\" };\n\t\t}\n\t\tconst config = this.resolveConfig();\n\t\tif (selected.state && selected.state !== \"running\" && config.cleanup === \"stop\") {\n\t\t\tthis.forgetStoppedContainer(selected, options.workspaceRoot);\n\t\t\treturn { status: \"already-stopped\", container: selected };\n\t\t}\n\t\tconst args = [\n\t\t\tconfig.cleanup === \"remove\" ? \"rm\" : \"stop\",\n\t\t\t...(config.cleanup === \"remove\" ? [\"--force\"] : []),\n\t\t\tselected.id,\n\t\t];\n\t\tconst result = await this.runner.run(config.dockerBinary, args);\n\t\tif (result.exitCode !== 0) {\n\t\t\tconst output = `${result.stderr}\\n${result.stdout}`;\n\t\t\tif (/No such container|not found/i.test(output)) {\n\t\t\t\tthis.forgetStoppedContainer(selected, options.workspaceRoot);\n\t\t\t\treturn { status: \"not-found\", message: \"Sandbox container no longer exists\" };\n\t\t\t}\n\t\t\tensureSuccessful(result, args);\n\t\t}\n\t\tthis.forgetStoppedContainer(selected, options.workspaceRoot);\n\t\treturn { status: config.cleanup === \"remove\" ? \"removed\" : \"stopped\", container: selected };\n\t}\n\n\tasync stopManagedContainers(): Promise<void> {\n\t\tawait withSandboxStartLock(async () => {\n\t\t\tconst failures: unknown[] = [];\n\t\t\tfor (const [containerId, container] of [...this.managedContainers]) {\n\t\t\t\ttry {\n\t\t\t\t\tawait this.stop({\n\t\t\t\t\t\tworkspaceRoot: container.workspaceRoot,\n\t\t\t\t\t\ttarget: containerId,\n\t\t\t\t\t\tcurrentContainerId: containerId,\n\t\t\t\t\t});\n\t\t\t\t\tthis.managedContainers.delete(containerId);\n\t\t\t\t\treservedSandboxDaemonPorts.delete(container.daemonPort);\n\t\t\t\t} catch (error) {\n\t\t\t\t\tfailures.push(error);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (failures.length > 0) throw new AggregateError(failures, \"Failed to stop managed sandbox containers\");\n\t\t});\n\t}\n\n\tprivate containerFromPsRecord(row: DockerPsRecord): SandboxContainer {\n\t\tconst labels = parseLabels(row.Labels);\n\t\tconst daemonPort = parsePositiveInteger(\n\t\t\tlabels[`${SANDBOX_LABEL_PREFIX}.daemon-port`] ?? undefined,\n\t\t\t\"daemon port\",\n\t\t);\n\t\treturn {\n\t\t\tid: row.ID ?? \"\",\n\t\t\tname: row.Names ?? \"\",\n\t\t\timage: row.Image ?? \"\",\n\t\t\tstatus: row.Status ?? \"\",\n\t\t\tstate: row.State,\n\t\t\tcreatedAt: row.CreatedAt,\n\t\t\tworkspaceRoot: undefined,\n\t\t\tworkspaceMountPath: labels[SANDBOX_LABEL_WORKSPACE_MOUNT],\n\t\t\tdaemonPort,\n\t\t\tdaemonEndpoint: row.Ports ? redactSecrets(row.Ports) : undefined,\n\t\t\tlabels,\n\t\t};\n\t}\n}\n\nexport function parseDockerPortOutput(output: string): string {\n\tconst firstLine = output.trim().split(/\\r?\\n/)[0];\n\tconst match = firstLine?.match(/^(?<host>.+):(?<port>\\d+)$/);\n\tif (!match?.groups) throw new Error(`Unable to parse Docker daemon port output: ${output.trim()}`);\n\tconst host = match.groups.host === \"0.0.0.0\" ? \"127.0.0.1\" : match.groups.host;\n\treturn `ws://${host}:${match.groups.port}${DAEMON_WEBSOCKET_PATH}`;\n}\n\nexport function parseDockerInspectContainers(\n\toutput: string,\n\tdaemonPort = DEFAULT_SANDBOX_DAEMON_PORT,\n): SandboxContainer[] {\n\treturn parseJsonArray<DockerInspectRecord>(output).map((record) => {\n\t\tconst labels = record.Config?.Labels ?? {};\n\t\tconst mountPath = labels[SANDBOX_LABEL_WORKSPACE_MOUNT] ?? DEFAULT_SANDBOX_WORKSPACE_MOUNT;\n\t\tconst mount = record.Mounts?.find((candidate) => candidate.Destination === mountPath);\n\t\tconst name = record.Name?.replace(/^\\//, \"\") ?? \"\";\n\t\treturn {\n\t\t\tid: record.Id ?? \"\",\n\t\t\tname,\n\t\t\timage: record.Config?.Image ?? \"\",\n\t\t\tstatus: record.State?.Status ?? \"\",\n\t\t\tstate: record.State?.Running ? \"running\" : record.State?.Status,\n\t\t\tcreatedAt: record.State?.StartedAt ?? record.Created,\n\t\t\tworkspaceRoot: mount?.Source,\n\t\t\tworkspaceMountPath: mount?.Destination ?? mountPath,\n\t\t\tdaemonPort,\n\t\t\tdaemonEndpoint: getEndpointFromPorts(record.NetworkSettings?.Ports, daemonPort),\n\t\t\tlabels,\n\t\t};\n\t});\n}\nfunction containerIdsMatch(left: string, right: string): boolean {\n\treturn left === right || left.startsWith(right) || right.startsWith(left);\n}\n\nfunction selectStopTarget(\n\tcontainers: SandboxContainer[],\n\ttarget: string | undefined,\n\tcurrentContainerId: string | undefined,\n): SandboxContainer | undefined {\n\tif (target) {\n\t\treturn containers.find((container) => containerIdsMatch(container.id, target) || container.name === target);\n\t}\n\tif (currentContainerId) {\n\t\tconst current = containers.find((container) => containerIdsMatch(container.id, currentContainerId));\n\t\tif (current) return current;\n\t}\n\treturn containers.length === 1 ? containers[0] : undefined;\n}\n"]}
|