@exagone313/dsh-podman 0.2.0-rc.4 → 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/README.md +12 -0
- package/README.zh.md +14 -2
- package/cordis.patch.yml +58 -0
- package/dist/approval-reasons.d.ts +3 -4
- package/dist/approval-reasons.js +40 -49
- package/dist/approval-reasons.test.js +43 -15
- package/dist/approval.js +27 -10
- package/dist/approval.test.js +165 -34
- package/dist/card-route.d.ts +3 -2
- package/dist/card-route.js +103 -35
- package/dist/card-route.test.js +60 -43
- package/dist/card-test-support.js +39 -66
- package/dist/client/ContainerCard.d.ts +1 -1
- package/dist/client/ContainerCard.js +76 -70
- package/dist/client/card-protocol.d.ts +3 -3
- package/dist/client/container-card-caches.js +11 -6
- package/dist/client/container-card-controller.d.ts +13 -18
- package/dist/client/container-card-controller.js +86 -40
- package/dist/client/container-card-create-modal.d.ts +2 -2
- package/dist/client/container-card-create-modal.js +13 -5
- package/dist/client/container-card-default-env.d.ts +10 -0
- package/dist/client/container-card-default-env.js +69 -0
- package/dist/client/container-card-directory.d.ts +2 -2
- package/dist/client/container-card-directory.js +12 -4
- package/dist/client/container-card-editors.d.ts +3 -3
- package/dist/client/container-card-editors.js +104 -32
- package/dist/client/container-card-images.d.ts +4 -4
- package/dist/client/container-card-images.js +19 -6
- package/dist/client/container-card-paths.d.ts +3 -3
- package/dist/client/container-card-paths.js +18 -11
- package/dist/client/container-card-row.d.ts +10 -6
- package/dist/client/container-card-row.js +82 -13
- package/dist/client/container-card-secrets.d.ts +3 -3
- package/dist/client/container-card-secrets.js +8 -6
- package/dist/client/container-card-shared.d.ts +4 -17
- package/dist/client/container-card-shared.js +9 -25
- package/dist/client/container-card-styles.d.ts +2 -8
- package/dist/client/container-card-styles.js +14 -52
- package/dist/client/container-card-volumes.d.ts +2 -2
- package/dist/client/container-card-volumes.js +9 -5
- package/dist/client/container-card-workspace.d.ts +8 -6
- package/dist/client/container-card-workspace.js +9 -3
- package/dist/client/directory-picker.js +2 -1
- package/dist/client/index.js +13109 -2117
- package/dist/client/locales.d.ts +6 -1
- package/dist/client/locales.js +88 -38
- package/dist/client/podman-terminal-guide.d.ts +11 -0
- package/dist/client/podman-terminal-guide.js +208 -0
- package/dist/client/podman-terminal-title.d.ts +7 -0
- package/dist/client/podman-terminal-title.js +30 -0
- package/dist/client/podman-terminal.d.ts +26 -0
- package/dist/client/podman-terminal.js +412 -0
- package/dist/client/read-only-approval.d.ts +2 -2
- package/dist/client/read-only-approval.js +13 -3
- package/dist/client/slot-contract.d.ts +0 -11
- package/dist/client/terminal-preference.d.ts +26 -0
- package/dist/client/terminal-preference.js +67 -0
- package/dist/client/terminal-protocol.d.ts +85 -0
- package/dist/client/terminal-protocol.js +14 -0
- package/dist/client/terminal-tab.d.ts +60 -0
- package/dist/client/terminal-tab.js +35 -0
- package/dist/client/terminal-targets.d.ts +33 -0
- package/dist/client/terminal-targets.js +65 -0
- package/dist/client/terminal-titles.d.ts +23 -0
- package/dist/client/terminal-titles.js +52 -0
- package/dist/client/terminal-transport.d.ts +63 -0
- package/dist/client/terminal-transport.js +207 -0
- package/dist/client/tool-views.js +215 -49
- package/dist/container-env.d.ts +11 -0
- package/dist/container-env.js +96 -0
- package/dist/container-env.test.d.ts +1 -0
- package/dist/container-env.test.js +93 -0
- package/dist/containers.test.js +196 -18
- package/dist/daemons.test.js +18 -4
- package/dist/dsh-version.js +2 -1
- package/dist/env-rows.d.ts +14 -0
- package/dist/env-rows.js +52 -0
- package/dist/env-rows.test.d.ts +1 -0
- package/dist/env-rows.test.js +43 -0
- package/dist/fs-provider.d.ts +1 -0
- package/dist/fs-provider.js +26 -12
- package/dist/fs-provider.test.js +51 -10
- package/dist/generated/version.d.ts +2 -2
- package/dist/generated/version.js +2 -2
- package/dist/grpc/proto/dshctl/v1/control.proto +8 -1
- package/dist/grpc/proto/dshguest/v1/guest.proto +5 -0
- package/dist/grpc/runtime-client.d.ts +3 -1
- package/dist/grpc/runtime-client.js +90 -17
- package/dist/guest-rpc.d.ts +15 -19
- package/dist/guest-rpc.js +289 -70
- package/dist/guest-rpc.test.d.ts +1 -0
- package/dist/guest-rpc.test.js +241 -0
- package/dist/guest-terminal.d.ts +30 -0
- package/dist/guest-terminal.js +196 -0
- package/dist/images.test.js +10 -3
- package/dist/index.d.ts +11 -16
- package/dist/index.js +42 -36
- package/dist/locales.test.d.ts +1 -0
- package/dist/locales.test.js +34 -0
- package/dist/misc.test.js +38 -10
- package/dist/mount-enums.js +2 -1
- package/dist/mount-input.d.ts +2 -0
- package/dist/mount-input.js +38 -22
- package/dist/mounts.test.js +81 -11
- package/dist/naming.test.d.ts +1 -0
- package/dist/naming.test.js +28 -0
- package/dist/output-reader.js +36 -4
- package/dist/package-deps.test.d.ts +1 -0
- package/dist/package-deps.test.js +49 -0
- package/dist/paths.test.js +4 -2
- package/dist/plugin-meta.test.d.ts +1 -0
- package/dist/plugin-meta.test.js +29 -0
- package/dist/project-path.js +33 -7
- package/dist/project-path.test.js +14 -0
- package/dist/prompts.d.ts +0 -4
- package/dist/prompts.js +15 -84
- package/dist/read-only-shell.js +3 -1
- package/dist/read-only-shell.test.js +80 -25
- package/dist/runtime-client.test.d.ts +1 -0
- package/dist/runtime-client.test.js +53 -0
- package/dist/secrets.test.js +21 -6
- package/dist/settings-commands.test.js +61 -14
- package/dist/settings-create.test.js +32 -10
- package/dist/settings-default-env.test.d.ts +1 -0
- package/dist/settings-default-env.test.js +175 -0
- package/dist/settings-mounts.test.js +46 -7
- package/dist/settings-schema.d.ts +6 -11
- package/dist/settings-schema.js +4 -8
- package/dist/settings-schema.test.d.ts +1 -0
- package/dist/settings-schema.test.js +35 -0
- package/dist/settings-workspaces.test.js +36 -63
- package/dist/spill-store.d.ts +1 -0
- package/dist/spill-store.js +17 -4
- package/dist/spill-store.test.js +31 -8
- package/dist/subprocess.d.ts +4 -0
- package/dist/subprocess.js +148 -162
- package/dist/subprocess.test.js +118 -8
- package/dist/terminal-preference.test.d.ts +1 -0
- package/dist/terminal-preference.test.js +62 -0
- package/dist/terminal-route.d.ts +10 -0
- package/dist/terminal-route.js +270 -0
- package/dist/terminal-route.test.d.ts +1 -0
- package/dist/terminal-route.test.js +265 -0
- package/dist/terminal-sessions.d.ts +69 -0
- package/dist/terminal-sessions.js +252 -0
- package/dist/terminal-shells.d.ts +22 -0
- package/dist/terminal-shells.js +99 -0
- package/dist/terminal-tab.test.d.ts +1 -0
- package/dist/terminal-tab.test.js +58 -0
- package/dist/terminal-targets.test.d.ts +1 -0
- package/dist/terminal-targets.test.js +43 -0
- package/dist/terminal-titles.test.d.ts +1 -0
- package/dist/terminal-titles.test.js +36 -0
- package/dist/test-support.d.ts +35 -5
- package/dist/test-support.js +91 -29
- package/dist/tool-defs.js +19 -4
- package/dist/tool-handlers.js +80 -28
- package/dist/tool-params.js +19 -5
- package/dist/volumes.test.js +1 -1
- package/dist/workspace-binding.d.ts +18 -9
- package/dist/workspace-binding.js +244 -77
- package/dist/workspace-binding.test.js +189 -25
- package/docs/architecture.md +62 -21
- package/docs/architecture.zh.md +57 -13
- package/docs/configuration.md +61 -20
- package/docs/configuration.zh.md +53 -19
- package/docs/development-prompt.md +72 -0
- package/docs/development-prompt.zh.md +69 -0
- package/docs/development.md +141 -21
- package/docs/development.zh.md +125 -17
- package/docs/install-dsh-and-dsh-podman.md +21 -8
- package/docs/install-dsh-and-dsh-podman.zh.md +18 -8
- package/docs/uninstall.md +83 -0
- package/docs/uninstall.zh.md +79 -0
- package/docs/update.md +120 -0
- package/docs/update.zh.md +114 -0
- package/docs/usage.md +108 -61
- package/docs/usage.zh.md +76 -38
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +59 -26
- package/quadlet/dsh-podman-orchestrator.container +46 -0
- package/quadlet/dsh.container +35 -0
- package/LICENSE.pkg +0 -17103
- package/dist/preferences.d.ts +0 -6
- package/dist/preferences.js +0 -27
package/dist/card-route.test.js
CHANGED
|
@@ -5,7 +5,6 @@ import { test } from "node:test";
|
|
|
5
5
|
import { strict as assert } from "node:assert";
|
|
6
6
|
import { CARD_PATH } from "./client/card-protocol.js";
|
|
7
7
|
import { registerCardRoute } from "./card-route.js";
|
|
8
|
-
import { installContainerPreferences } from "./preferences.js";
|
|
9
8
|
function fakeResolver() {
|
|
10
9
|
const calls = [];
|
|
11
10
|
const resolver = {
|
|
@@ -19,8 +18,9 @@ function fakeResolver() {
|
|
|
19
18
|
calls.push([method, request]);
|
|
20
19
|
if (method === "getVersion")
|
|
21
20
|
return { version: "9.9.9", commit: "abc" };
|
|
22
|
-
if (method === "listContainers")
|
|
21
|
+
if (method === "listContainers") {
|
|
23
22
|
return { containers: [{ containerName: "c1" }] };
|
|
23
|
+
}
|
|
24
24
|
if (method === "listImages")
|
|
25
25
|
return { images: [{ imageId: "img1" }] };
|
|
26
26
|
if (method === "listWorkspaces")
|
|
@@ -29,8 +29,9 @@ function fakeResolver() {
|
|
|
29
29
|
return { volumes: [{ name: "data" }] };
|
|
30
30
|
if (method === "listSecrets")
|
|
31
31
|
return { secrets: [{ name: "s1" }] };
|
|
32
|
-
if (method === "listCaches")
|
|
32
|
+
if (method === "listCaches") {
|
|
33
33
|
return { caches: [{ manager: "pacman", files: 1, bytes: "1024" }] };
|
|
34
|
+
}
|
|
34
35
|
if (method === "cleanCaches")
|
|
35
36
|
return { removedFiles: 2 };
|
|
36
37
|
if (method === "removeContainer")
|
|
@@ -44,7 +45,7 @@ function fakeRouteContext() {
|
|
|
44
45
|
const routes = [];
|
|
45
46
|
const ctx = {
|
|
46
47
|
inject(deps, callback) {
|
|
47
|
-
assert.deepEqual(deps, ["connection"
|
|
48
|
+
assert.deepEqual(deps, ["connection"]);
|
|
48
49
|
callback({
|
|
49
50
|
connection: {
|
|
50
51
|
fetch: {
|
|
@@ -59,10 +60,6 @@ function fakeRouteContext() {
|
|
|
59
60
|
},
|
|
60
61
|
},
|
|
61
62
|
},
|
|
62
|
-
settings: {
|
|
63
|
-
register: () => ({ watch: () => () => { } }),
|
|
64
|
-
get: () => undefined,
|
|
65
|
-
},
|
|
66
63
|
});
|
|
67
64
|
},
|
|
68
65
|
};
|
|
@@ -94,7 +91,15 @@ test("the card route serves the live snapshot on GET", async () => {
|
|
|
94
91
|
assert.deepEqual(snapshot.volumes, [{ name: "data" }]);
|
|
95
92
|
assert.deepEqual(snapshot.secrets, [{ name: "s1" }]);
|
|
96
93
|
assert.equal(snapshot.caches[0].bytes, 1024);
|
|
97
|
-
assert.deepEqual(calls.map(([method]) => method), [
|
|
94
|
+
assert.deepEqual(calls.map(([method]) => method), [
|
|
95
|
+
"getVersion",
|
|
96
|
+
"listContainers",
|
|
97
|
+
"listImages",
|
|
98
|
+
"listWorkspaces",
|
|
99
|
+
"listVolumes",
|
|
100
|
+
"listSecrets",
|
|
101
|
+
"listCaches",
|
|
102
|
+
]);
|
|
98
103
|
assert.equal(typeof snapshot.dshVersion, "string");
|
|
99
104
|
assert.equal(snapshot.orchestratorVersion, "9.9.9");
|
|
100
105
|
assert.equal(snapshot.versionState, "major-mismatch", "a differing major is surfaced on the card");
|
|
@@ -129,9 +134,51 @@ test("the card route runs one command per POST and returns its notice", async ()
|
|
|
129
134
|
}));
|
|
130
135
|
assert.equal(remove.status, 200);
|
|
131
136
|
assert.deepEqual(await remove.json(), {});
|
|
132
|
-
|
|
137
|
+
// A command that names no container keeps the orchestrator's default; one that
|
|
138
|
+
// names a container must reach it, or a row's Remove would hit another one.
|
|
139
|
+
const removeNamed = await routes[0].fetch(cardRequest({
|
|
140
|
+
method: "POST",
|
|
141
|
+
body: JSON.stringify({
|
|
142
|
+
command: { op: "remove", workspace: "w1", container: "foo" },
|
|
143
|
+
}),
|
|
144
|
+
}));
|
|
145
|
+
assert.equal(removeNamed.status, 200);
|
|
146
|
+
const removeCalls = calls.filter(([method]) => method === "removeContainer");
|
|
147
|
+
assert.deepEqual(removeCalls[0]?.[1], { workspaceSlug: "w1" });
|
|
148
|
+
assert.deepEqual(removeCalls[1]?.[1], {
|
|
149
|
+
workspaceSlug: "w1",
|
|
150
|
+
container: "foo",
|
|
151
|
+
});
|
|
152
|
+
const recreate = await routes[0].fetch(cardRequest({
|
|
153
|
+
method: "POST",
|
|
154
|
+
body: JSON.stringify({
|
|
155
|
+
command: {
|
|
156
|
+
op: "recreate",
|
|
157
|
+
workspace: "w1",
|
|
158
|
+
container: "foo",
|
|
159
|
+
image: "img1",
|
|
160
|
+
},
|
|
161
|
+
}),
|
|
162
|
+
}));
|
|
163
|
+
assert.equal(recreate.status, 200);
|
|
164
|
+
assert.deepEqual(calls.find(([method]) => method === "recreateContainer")?.[1], {
|
|
133
165
|
workspaceSlug: "w1",
|
|
166
|
+
container: "foo",
|
|
167
|
+
imageId: "img1",
|
|
134
168
|
});
|
|
169
|
+
const detach = await routes[0].fetch(cardRequest({
|
|
170
|
+
method: "POST",
|
|
171
|
+
body: JSON.stringify({
|
|
172
|
+
command: {
|
|
173
|
+
op: "container_secret_remove",
|
|
174
|
+
workspace: "w1",
|
|
175
|
+
container: "foo",
|
|
176
|
+
secretEnvName: "TOKEN",
|
|
177
|
+
},
|
|
178
|
+
}),
|
|
179
|
+
}));
|
|
180
|
+
assert.equal(detach.status, 200);
|
|
181
|
+
assert.deepEqual(calls.find(([method]) => method === "removeContainerSecret")?.[1], { workspaceSlug: "w1", container: "foo", env: "TOKEN" });
|
|
135
182
|
const clean = await routes[0].fetch(cardRequest({
|
|
136
183
|
method: "POST",
|
|
137
184
|
body: JSON.stringify({ command: { op: "cache_clean", cacheMode: "all" } }),
|
|
@@ -170,37 +217,7 @@ test("the card route reports a failed command as an error", async () => {
|
|
|
170
217
|
body: JSON.stringify({ command: { op: "remove", workspace: "w1" } }),
|
|
171
218
|
}));
|
|
172
219
|
assert.equal(response.status, 500);
|
|
173
|
-
assert.deepEqual(await response.json(), {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
let base;
|
|
177
|
-
let watcher;
|
|
178
|
-
const ctx = {
|
|
179
|
-
inject(deps, callback) {
|
|
180
|
-
assert.deepEqual(deps, ["settings"]);
|
|
181
|
-
callback({
|
|
182
|
-
settings: {
|
|
183
|
-
register(_ns, _schema, options) {
|
|
184
|
-
base = options.base;
|
|
185
|
-
return {
|
|
186
|
-
watch(callback) {
|
|
187
|
-
watcher = callback;
|
|
188
|
-
return () => { };
|
|
189
|
-
},
|
|
190
|
-
};
|
|
191
|
-
},
|
|
192
|
-
},
|
|
193
|
-
});
|
|
194
|
-
},
|
|
195
|
-
};
|
|
196
|
-
const configs = [];
|
|
197
|
-
const resolver = {
|
|
198
|
-
getConfig: () => ({ defaultImage: "archlinux", socketsRoot: "/run/dsh-podman" }),
|
|
199
|
-
setConfig: (patch) => configs.push(patch),
|
|
200
|
-
};
|
|
201
|
-
installContainerPreferences(ctx, resolver);
|
|
202
|
-
assert.deepEqual(base, { defaultImage: "archlinux", socketsRoot: "/run/dsh-podman" });
|
|
203
|
-
assert.ok(watcher !== undefined);
|
|
204
|
-
watcher({ defaultImage: "ubuntu", socketsRoot: "/run/other" });
|
|
205
|
-
assert.deepEqual(configs, [{ defaultImage: "ubuntu", socketsRoot: "/run/other" }]);
|
|
220
|
+
assert.deepEqual(await response.json(), {
|
|
221
|
+
error: "control plane unavailable",
|
|
222
|
+
});
|
|
206
223
|
});
|
|
@@ -6,8 +6,6 @@
|
|
|
6
6
|
// `runCommand` and folds the real `cardSnapshot` back into the fake scope, so
|
|
7
7
|
// the command tests stay focused on the command-to-control mapping. The HTTP
|
|
8
8
|
// layer itself is covered by card-route.test.ts.
|
|
9
|
-
import { strict as assert } from "node:assert";
|
|
10
|
-
import { CONTAINER_NS, settingsSchema } from "./settings-schema.js";
|
|
11
9
|
import { cardSnapshot, runCommand } from "./card-route.js";
|
|
12
10
|
export function fakeScope(initial) {
|
|
13
11
|
const scope = {
|
|
@@ -30,26 +28,14 @@ export function fakeScope(initial) {
|
|
|
30
28
|
return scope;
|
|
31
29
|
}
|
|
32
30
|
export function fakeContext(scope) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
settings: {
|
|
38
|
-
register() {
|
|
39
|
-
return scope;
|
|
40
|
-
},
|
|
41
|
-
get() {
|
|
42
|
-
return undefined;
|
|
43
|
-
},
|
|
44
|
-
},
|
|
45
|
-
});
|
|
46
|
-
},
|
|
47
|
-
};
|
|
31
|
+
// The host half no longer registers a settings namespace: the card's
|
|
32
|
+
// preferences are volatile plugin-config fields, so the fake document is
|
|
33
|
+
// handed to the driver directly.
|
|
34
|
+
return { fakeScope: scope };
|
|
48
35
|
}
|
|
49
36
|
export function baseValue() {
|
|
50
37
|
return {
|
|
51
38
|
defaultImage: "archlinux",
|
|
52
|
-
socketsRoot: "/run/dsh-podman",
|
|
53
39
|
uiLocale: "",
|
|
54
40
|
notice: "",
|
|
55
41
|
command: null,
|
|
@@ -61,62 +47,49 @@ export function baseValue() {
|
|
|
61
47
|
caches: [],
|
|
62
48
|
};
|
|
63
49
|
}
|
|
64
|
-
// installCardCommandDriver wires a fake settings
|
|
50
|
+
// installCardCommandDriver wires a fake settings document to the real command
|
|
65
51
|
// runner and snapshot builder, standing in for the browser transport: it
|
|
66
52
|
// mirrors what the card does per request — run the command, then refresh the
|
|
67
53
|
// live snapshot and record the notice.
|
|
68
54
|
export function installCardCommandDriver(ctx, resolver, registry) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
55
|
+
const scope = ctx.fakeScope;
|
|
56
|
+
const refresh = async (notice = "") => {
|
|
57
|
+
let view = {};
|
|
58
|
+
try {
|
|
59
|
+
const snapshot = await cardSnapshot(resolver, registry);
|
|
60
|
+
view = {
|
|
61
|
+
workspaces: snapshot.workspaces,
|
|
62
|
+
containers: snapshot.containers,
|
|
63
|
+
images: snapshot.images,
|
|
64
|
+
volumes: snapshot.volumes,
|
|
65
|
+
secrets: snapshot.secrets,
|
|
66
|
+
caches: snapshot.caches,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// Command tests do not always stub every list call.
|
|
71
|
+
}
|
|
72
|
+
await scope.update({ ...view, notice });
|
|
73
|
+
};
|
|
74
|
+
scope.watch((next) => {
|
|
75
|
+
const command = next.command;
|
|
76
|
+
if (command === null || command === undefined)
|
|
77
|
+
return;
|
|
78
|
+
void (async () => {
|
|
79
|
+
// Clear the command before running it, so the refresh below cannot
|
|
80
|
+
// re-deliver it.
|
|
81
|
+
await scope.update({ command: null });
|
|
82
|
+
let notice = "";
|
|
79
83
|
try {
|
|
80
|
-
|
|
81
|
-
view = {
|
|
82
|
-
workspaces: snapshot.workspaces,
|
|
83
|
-
containers: snapshot.containers,
|
|
84
|
-
images: snapshot.images,
|
|
85
|
-
volumes: snapshot.volumes,
|
|
86
|
-
secrets: snapshot.secrets,
|
|
87
|
-
caches: snapshot.caches,
|
|
88
|
-
};
|
|
84
|
+
notice = await runCommand(ctx, resolver, registry, command);
|
|
89
85
|
}
|
|
90
|
-
catch {
|
|
91
|
-
|
|
86
|
+
catch (error) {
|
|
87
|
+
notice = error instanceof Error ? error.message : String(error);
|
|
92
88
|
}
|
|
93
|
-
await
|
|
94
|
-
};
|
|
95
|
-
scope.watch((next) => {
|
|
96
|
-
resolver.setConfig({
|
|
97
|
-
defaultImage: next.defaultImage,
|
|
98
|
-
socketsRoot: next.socketsRoot,
|
|
99
|
-
});
|
|
100
|
-
const command = next.command;
|
|
101
|
-
if (command === null || command === undefined)
|
|
102
|
-
return;
|
|
103
|
-
void (async () => {
|
|
104
|
-
// Clear the command before running it, so the refresh below cannot
|
|
105
|
-
// re-deliver it.
|
|
106
|
-
await scope.update({ command: null });
|
|
107
|
-
let notice = "";
|
|
108
|
-
try {
|
|
109
|
-
notice = await runCommand(sctx, resolver, registry, command);
|
|
110
|
-
}
|
|
111
|
-
catch (error) {
|
|
112
|
-
notice = error instanceof Error ? error.message : String(error);
|
|
113
|
-
}
|
|
114
|
-
await refresh(notice);
|
|
115
|
-
})();
|
|
116
|
-
});
|
|
117
|
-
installed = refresh();
|
|
89
|
+
await refresh(notice);
|
|
90
|
+
})();
|
|
118
91
|
});
|
|
119
|
-
return
|
|
92
|
+
return refresh();
|
|
120
93
|
}
|
|
121
94
|
export async function installedMountScope() {
|
|
122
95
|
const scope = fakeScope(baseValue());
|
|
@@ -2,5 +2,5 @@ import { type ContainerCardFace } from "./container-card-controller.js";
|
|
|
2
2
|
import { NS } from "./locales.js";
|
|
3
3
|
import { type InjectFace, type PropsLocale, type PropsRuntime } from "@deepseek-ai/dsh-client-ui-slots";
|
|
4
4
|
import { type ReactNode } from "react";
|
|
5
|
-
export type ContainerCardProps = PropsRuntime<"
|
|
5
|
+
export type ContainerCardProps = PropsRuntime<"plugins.bundle.config"> & PropsLocale<typeof NS> & InjectFace<ContainerCardFace>;
|
|
6
6
|
export declare function ContainerCard(props: ContainerCardProps): ReactNode;
|
|
@@ -2,22 +2,21 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
2
2
|
// SPDX-FileCopyrightText: 2026 Elouan Martinet <exa@elou.world>
|
|
3
3
|
//
|
|
4
4
|
// SPDX-License-Identifier: MIT
|
|
5
|
-
import { BaseImageRow, ImageBuildModal, ImageItem } from "./container-card-images.js";
|
|
5
|
+
import { BaseImageRow, ImageBuildModal, ImageItem, } from "./container-card-images.js";
|
|
6
6
|
import { CachesSection } from "./container-card-caches.js";
|
|
7
|
+
import { DefaultEnvironmentSection } from "./container-card-default-env.js";
|
|
7
8
|
import { SecretsSection } from "./container-card-secrets.js";
|
|
8
|
-
import {
|
|
9
|
-
import { banner,
|
|
9
|
+
import { ConfirmButton } from "./container-card-shared.js";
|
|
10
|
+
import { banner, footerRow, hint, imageSelect, sectionTitle, } from "./container-card-styles.js";
|
|
10
11
|
import { VolumesSection } from "./container-card-volumes.js";
|
|
11
12
|
import { WorkspaceSection } from "./container-card-workspace.js";
|
|
12
|
-
import { Button,
|
|
13
|
-
import { useEffect,
|
|
13
|
+
import { Button, Modal, writeClipboard, } from "@deepseek-ai/dsh-client-ui-primitives";
|
|
14
|
+
import { useEffect, useRef, useState } from "react";
|
|
14
15
|
// The labels of the version lines, emphasised against their values.
|
|
15
16
|
const versionLabel = { fontWeight: 600 };
|
|
16
17
|
export function ContainerCard(props) {
|
|
17
18
|
const { t } = props;
|
|
18
19
|
const state = props.useContainerCard((snapshot) => snapshot);
|
|
19
|
-
const [open, setOpen] = useState(false);
|
|
20
|
-
const [hovered, setHovered] = useState(false);
|
|
21
20
|
const [buildOpen, setBuildOpen] = useState(false);
|
|
22
21
|
const [imageId, setImageId] = useState("");
|
|
23
22
|
const [parent, setParent] = useState("");
|
|
@@ -26,7 +25,6 @@ export function ContainerCard(props) {
|
|
|
26
25
|
const [defaultImage, setDefaultImage] = useState(state.defaultImage);
|
|
27
26
|
const [copyState, setCopyState] = useState("idle");
|
|
28
27
|
const copyResetTimer = useRef();
|
|
29
|
-
const projectsRootId = useId();
|
|
30
28
|
// Copies the version lines as displayed, so a user can paste them into a bug
|
|
31
29
|
// report without transcribing anything.
|
|
32
30
|
const copyVersions = async () => {
|
|
@@ -36,17 +34,31 @@ export function ContainerCard(props) {
|
|
|
36
34
|
`${t("orchestrator")} ${state.orchestratorVersion || t("versionUnknown")}`,
|
|
37
35
|
];
|
|
38
36
|
setCopyState((await writeClipboard(lines.join("\n"))) ? "copied" : "failed");
|
|
39
|
-
if (copyResetTimer.current !== undefined)
|
|
37
|
+
if (copyResetTimer.current !== undefined) {
|
|
40
38
|
clearTimeout(copyResetTimer.current);
|
|
39
|
+
}
|
|
41
40
|
copyResetTimer.current = setTimeout(() => setCopyState("idle"), 1500);
|
|
42
41
|
};
|
|
43
|
-
// The
|
|
44
|
-
//
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
42
|
+
// The reset timer is cancelled on unmount, so it cannot fire against an
|
|
43
|
+
// unmounted component.
|
|
44
|
+
useEffect(() => () => {
|
|
45
|
+
if (copyResetTimer.current !== undefined) {
|
|
46
|
+
clearTimeout(copyResetTimer.current);
|
|
47
|
+
}
|
|
49
48
|
}, []);
|
|
49
|
+
// The Plugins page renders a one-liner in its list and this component again
|
|
50
|
+
// as the entry's page, so the orchestrator snapshot is only fetched for the
|
|
51
|
+
// page. Re-reading on every page view keeps a workspace created since the
|
|
52
|
+
// last visit visible without pressing Reload.
|
|
53
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: refresh per page view
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
if (props.view === "page")
|
|
56
|
+
props.reload();
|
|
57
|
+
}, [props.view]);
|
|
58
|
+
// The list shows only the one-liner; the page heading already carries the
|
|
59
|
+
// plugin's title and description.
|
|
60
|
+
if (props.view === "summary")
|
|
61
|
+
return t("cardDescription");
|
|
50
62
|
const baseImages = state.images.filter((image) => image.isBase);
|
|
51
63
|
const customImages = state.images.filter((image) => !image.isBase);
|
|
52
64
|
const defaultCandidates = [
|
|
@@ -54,62 +66,56 @@ export function ContainerCard(props) {
|
|
|
54
66
|
...customImages.map((image) => image.imageId),
|
|
55
67
|
];
|
|
56
68
|
if (!state.available) {
|
|
57
|
-
return (
|
|
69
|
+
return (_jsxs("div", { style: { padding: "8px 0", fontSize: "13px", opacity: 0.8 }, children: [_jsx("p", { style: { margin: 0 }, children: t("unavailable") }), state.notice === ""
|
|
70
|
+
? null
|
|
71
|
+
: (_jsxs("p", { style: { margin: "4px 0 0" }, role: "status", children: [t("notice"), ": ", state.notice] })), _jsx("div", { style: { marginTop: "8px" }, children: _jsx(Button, { variant: "outline", size: "sm", disabled: state.busy, onClick: props.reload, children: state.busy ? t("busy") : t("reload") }) })] }));
|
|
58
72
|
}
|
|
59
|
-
return (_jsxs("
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}, children: [_jsx("div", { style: { ...sectionTitle, flex: 1 }, children: t("imagesTitle") }), _jsx(Button, { variant: "outline", size: "sm", disabled: state.busy, onClick: () => {
|
|
73
|
-
setDefaultImage(state.defaultImage);
|
|
74
|
-
setDefaultOpen(true);
|
|
75
|
-
}, children: t("setDefaultImage") }), _jsx(Button, { variant: "outline", size: "sm", disabled: state.busy, onClick: () => {
|
|
76
|
-
setImageId("");
|
|
77
|
-
setParent(baseImages[0]?.imageId ?? customImages[0]?.imageId ?? "");
|
|
78
|
-
setPackages([]);
|
|
79
|
-
setBuildOpen(true);
|
|
80
|
-
}, children: t("buildImage") }), _jsx(ConfirmButton, { t: t, label: t("rebuildAllImages"), title: t("confirmTitle"), description: t("confirmRebuildAllImages"), disabled: state.busy, onConfirm: props.rebuildAllImages })] }), _jsx("div", { style: sectionTitle, children: t("baseImagesTitle") }), baseImages.length === 0 ? (_jsx("p", { style: hint, children: t("none") })) : (baseImages.map((image) => (_jsx(BaseImageRow, { t: t, image: image, busy: state.busy, defaultImage: state.defaultImage, onRebuild: props.rebuildBaseImage, onPull: props.pullBaseImage, onSetDefault: props.setDefaultImage }, image.imageId)))), _jsx("div", { style: sectionTitle, children: t("customImagesTitle") }), customImages.length === 0 ? (_jsx("p", { style: hint, children: t("none") })) : (customImages.map((image) => (_jsx(ImageItem, { t: t, image: image, busy: state.busy, defaultImage: state.defaultImage, onRemove: props.removeImage, onRebuild: props.rebuildImage, onSetDefault: props.setDefaultImage }, image.imageId)))), _jsx(ImageBuildModal, { t: t, images: state.images, open: buildOpen, imageId: imageId, parent: parent, packages: packages, onImageId: setImageId, onParent: setParent, onPackages: setPackages, onClose: () => setBuildOpen(false), onBuild: () => {
|
|
81
|
-
props.buildImage(imageId.trim(), parent.trim(), packages);
|
|
73
|
+
return (_jsxs("div", { children: [state.notice === "" ? null : (_jsxs("div", { style: banner, role: "status", children: [t("notice"), ": ", state.notice] })), state.versionState === "ok" ? null : (_jsx("div", { style: state.versionState === "major-mismatch"
|
|
74
|
+
? { ...banner, borderColor: "var(--dsw-alias-state-error-primary)" }
|
|
75
|
+
: banner, role: state.versionState === "major-mismatch" ? "alert" : "status", children: t(state.versionState === "major-mismatch"
|
|
76
|
+
? "versionMismatchMajor"
|
|
77
|
+
: "versionMismatchMinor") })), _jsx("div", { style: sectionTitle, children: t("workspacesTitle") }), state.workspaces.length === 0 ? _jsx("p", { style: hint, children: t("none") }) : (state.workspaces.map((workspace) => (_jsx(WorkspaceSection, { t: t, workspace: workspace, containers: state.containers.filter((container) => container.workspaceSlug === workspace.workspaceSlug), images: state.images, volumes: state.volumes, secrets: state.secrets, busy: state.busy, defaultImage: state.defaultImage, projectsRoot: state.projectsRoot, directoryPicker: state.directoryPicker, onRemove: props.remove, onRemoveWorkspace: props.removeWorkspace, onRecreate: props.recreate, onCreate: props.createContainer, onStartContainer: props.startContainer, onAddContainerMount: props.addContainerMount, onRemoveContainerMount: props.removeContainerMount, onUpdateContainerMount: props.updateContainerMount, onSetContainerPaths: props.setContainerPaths, onAddContainerSecret: props.addContainerSecret, onRemoveContainerSecret: props.removeContainerSecret, onSyncDefaults: props.syncDefaultEnv, defaultEnvCount: Object.keys(state.containerEnv).length }, workspace.workspaceSlug)))), _jsxs("div", { style: {
|
|
78
|
+
display: "flex",
|
|
79
|
+
flexWrap: "wrap",
|
|
80
|
+
alignItems: "center",
|
|
81
|
+
gap: "8px",
|
|
82
|
+
}, children: [_jsx("div", { style: { ...sectionTitle, flex: 1 }, children: t("imagesTitle") }), _jsx(Button, { variant: "outline", size: "sm", disabled: state.busy, onClick: () => {
|
|
83
|
+
setDefaultImage(state.defaultImage);
|
|
84
|
+
setDefaultOpen(true);
|
|
85
|
+
}, children: t("setDefaultImage") }), _jsx(Button, { variant: "outline", size: "sm", disabled: state.busy, onClick: () => {
|
|
82
86
|
setImageId("");
|
|
83
|
-
setParent("");
|
|
87
|
+
setParent(baseImages[0]?.imageId ?? customImages[0]?.imageId ?? "");
|
|
84
88
|
setPackages([]);
|
|
85
|
-
setBuildOpen(
|
|
86
|
-
} }), _jsx(
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
89
|
+
setBuildOpen(true);
|
|
90
|
+
}, children: t("buildImage") }), _jsx(ConfirmButton, { t: t, label: t("rebuildAllImages"), title: t("confirmTitle"), description: t("confirmRebuildAllImages"), disabled: state.busy, onConfirm: props.rebuildAllImages })] }), _jsx("div", { style: sectionTitle, children: t("baseImagesTitle") }), baseImages.length === 0 ? _jsx("p", { style: hint, children: t("none") }) : (baseImages.map((image) => (_jsx(BaseImageRow, { t: t, image: image, busy: state.busy, defaultImage: state.defaultImage, onRebuild: props.rebuildBaseImage, onPull: props.pullBaseImage, onSetDefault: props.setDefaultImage }, image.imageId)))), _jsx("div", { style: sectionTitle, children: t("customImagesTitle") }), customImages.length === 0 ? _jsx("p", { style: hint, children: t("none") }) : (customImages.map((image) => (_jsx(ImageItem, { t: t, image: image, busy: state.busy, defaultImage: state.defaultImage, onRemove: props.removeImage, onRebuild: props.rebuildImage, onSetDefault: props.setDefaultImage }, image.imageId)))), _jsx(ImageBuildModal, { t: t, images: state.images, open: buildOpen, imageId: imageId, parent: parent, packages: packages, onImageId: setImageId, onParent: setParent, onPackages: setPackages, onClose: () => setBuildOpen(false), onBuild: () => {
|
|
91
|
+
props.buildImage(imageId.trim(), parent.trim(), packages);
|
|
92
|
+
setImageId("");
|
|
93
|
+
setParent("");
|
|
94
|
+
setPackages([]);
|
|
95
|
+
setBuildOpen(false);
|
|
96
|
+
} }), _jsxs(Modal, { open: defaultOpen, onClose: () => setDefaultOpen(false), title: t("setDefaultImageTitle"), closeLabel: t("cancel"), footer: _jsxs(_Fragment, { children: [_jsx(Button, { variant: "outline", size: "sm", onClick: () => setDefaultOpen(false), children: t("cancel") }), _jsx(Button, { variant: "primary", size: "sm", disabled: defaultImage === "", onClick: () => {
|
|
97
|
+
props.setDefaultImage(defaultImage);
|
|
98
|
+
setDefaultOpen(false);
|
|
99
|
+
}, children: t("setDefaultImage") })] }), children: [_jsxs("select", { style: imageSelect, value: defaultImage, onChange: (event) => setDefaultImage(event.target.value), children: [defaultCandidates.length === 0
|
|
100
|
+
? _jsx("option", { value: "", children: t("none") })
|
|
101
|
+
: null, defaultCandidates.map((name) => (_jsx("option", { value: name, children: name }, name)))] }), _jsx("p", { style: hint, children: t("defaultImageHint") })] }), _jsx(CachesSection, { t: t, caches: state.caches, busy: state.busy, onClean: props.cleanCaches }), _jsx(VolumesSection, { t: t, volumes: state.volumes, busy: state.busy, writable: state.writable, onCreate: props.createVolume, onRemove: props.removeVolume }), _jsx(SecretsSection, { t: t, secrets: state.secrets, busy: state.busy, writable: state.writable, onCreate: props.createSecret, onRemove: props.removeSecret, onSet: props.setSecret }), _jsx(DefaultEnvironmentSection, { t: t, env: state.containerEnv, busy: state.busy, writable: state.writable, onSave: props.saveContainerEnv, onSync: props.syncDefaultEnv }), _jsxs("div", { style: {
|
|
102
|
+
display: "flex",
|
|
103
|
+
flexWrap: "wrap",
|
|
104
|
+
alignItems: "center",
|
|
105
|
+
gap: "8px",
|
|
106
|
+
}, children: [_jsx("div", { style: { ...sectionTitle, flex: 1 }, children: t("versionsTitle") }), _jsx(Button, { variant: "outline", size: "sm", onClick: () => void copyVersions(), children: copyState === "copied"
|
|
107
|
+
? t("copied")
|
|
108
|
+
: copyState === "failed"
|
|
109
|
+
? t("copyFailed")
|
|
110
|
+
: t("copyVersions") })] }), _jsxs("div", { style: {
|
|
111
|
+
...hint,
|
|
112
|
+
display: "flex",
|
|
113
|
+
flexDirection: "column",
|
|
114
|
+
gap: "2px",
|
|
115
|
+
margin: 0,
|
|
116
|
+
}, children: [_jsxs("span", { children: [_jsx("span", { style: versionLabel, children: t("dsh") }), state.dshVersion
|
|
117
|
+
? (` ${state.dshVersion}`)
|
|
118
|
+
: (_jsxs(_Fragment, { children: [" ", _jsx("span", { style: { color: "var(--dsw-alias-state-error-primary)" }, children: t("versionUnknown") })] }))] }), _jsxs("span", { children: [_jsx("span", { style: versionLabel, children: t("cardTitle") }), state.version ? ` ${state.version}` : "", state.commit ? ` · ${state.commit}` : ""] }), _jsxs("span", { children: [_jsx("span", { style: versionLabel, children: t("orchestrator") }), state.orchestratorVersion
|
|
119
|
+
? (` ${state.orchestratorVersion}`)
|
|
120
|
+
: (_jsxs(_Fragment, { children: [" ", _jsx("span", { style: { color: "var(--dsw-alias-state-error-primary)" }, children: t("versionUnknown") })] }))] })] }), _jsx("div", { style: footerRow, children: _jsx(Button, { variant: "outline", size: "sm", disabled: state.busy, onClick: props.reload, children: state.busy ? t("busy") : t("reload") }) })] }));
|
|
115
121
|
}
|
|
@@ -7,7 +7,7 @@ export interface MountInput {
|
|
|
7
7
|
volume: string;
|
|
8
8
|
secret: string;
|
|
9
9
|
}
|
|
10
|
-
export type CommandOp = "remove" | "workspace_remove" | "recreate" | "create" | "volume_create" | "volume_remove" | "image_remove" | "secret_create" | "secret_remove" | "secret_set" | "image_rebuild" | "image_rebuild_all" | "image_build" | "image_base_rebuild" | "image_base_pull" | "container_secret_add" | "container_secret_remove" | "container_mount_add" | "container_mount_remove" | "container_mount_update" | "container_path_set" | "cache_clean";
|
|
10
|
+
export type CommandOp = "remove" | "workspace_remove" | "recreate" | "create" | "volume_create" | "volume_remove" | "image_remove" | "secret_create" | "secret_remove" | "secret_set" | "image_rebuild" | "image_rebuild_all" | "image_build" | "image_base_rebuild" | "image_base_pull" | "container_secret_add" | "container_secret_remove" | "container_mount_add" | "container_mount_remove" | "container_mount_update" | "container_path_set" | "default_env_sync" | "cache_clean";
|
|
11
11
|
export interface CommandRequest {
|
|
12
12
|
op: CommandOp;
|
|
13
13
|
workspace: string;
|
|
@@ -19,11 +19,11 @@ export interface CommandRequest {
|
|
|
19
19
|
env: Record<string, string>;
|
|
20
20
|
container: string;
|
|
21
21
|
secret: string;
|
|
22
|
-
|
|
22
|
+
secretEnvName: string;
|
|
23
23
|
length: number;
|
|
24
24
|
charset: string;
|
|
25
25
|
packages: string[];
|
|
26
|
-
|
|
26
|
+
secretEnv: Record<string, string>;
|
|
27
27
|
cacheMode: string;
|
|
28
28
|
mount: MountInput | null;
|
|
29
29
|
}
|
|
@@ -22,10 +22,15 @@ function formatBytes(bytes) {
|
|
|
22
22
|
export function CachesSection(props) {
|
|
23
23
|
const { t, caches, busy, onClean } = props;
|
|
24
24
|
const disabled = busy || caches.length === 0;
|
|
25
|
-
return (_jsxs(_Fragment, { children: [_jsx("div", { style: sectionTitle, children: t("cachesTitle") }), _jsxs("div", { style: wsBody, children: [caches.length === 0
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
25
|
+
return (_jsxs(_Fragment, { children: [_jsx("div", { style: sectionTitle, children: t("cachesTitle") }), _jsxs("div", { style: wsBody, children: [caches.length === 0
|
|
26
|
+
? _jsx("p", { style: { ...hint, margin: 0 }, children: t("noCaches") })
|
|
27
|
+
: (caches.map((cache) => (_jsxs("div", { style: {
|
|
28
|
+
display: "flex",
|
|
29
|
+
flexWrap: "wrap",
|
|
30
|
+
alignItems: "center",
|
|
31
|
+
gap: "8px",
|
|
32
|
+
}, children: [_jsx("strong", { style: { color: "var(--dsw-alias-label-primary)" }, children: cache.manager }), _jsx("span", { style: hint, children: t("cacheUsage", {
|
|
33
|
+
size: formatBytes(cache.bytes),
|
|
34
|
+
n: cache.files,
|
|
35
|
+
}) })] }, cache.manager)))), _jsxs("div", { style: { display: "flex", flexWrap: "wrap", gap: "8px" }, children: [_jsx(ConfirmButton, { t: t, label: t("cacheKeepLatest"), title: t("confirmTitle"), description: t("confirmCacheKeepLatest"), disabled: disabled, onConfirm: () => onClean("keep-latest") }), _jsx(ConfirmButton, { t: t, label: t("cacheRemoveAll"), title: t("confirmTitle"), description: t("confirmCacheRemoveAll"), disabled: disabled, onConfirm: () => onClean("all") })] })] })] }));
|
|
31
36
|
}
|