@exagone313/dsh-podman 0.2.0-rc.2
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/LICENSE +21 -0
- package/README.md +41 -0
- package/README.zh.md +41 -0
- package/cordis.patch.yml +17 -0
- package/dist/approval-reasons.d.ts +129 -0
- package/dist/approval-reasons.js +253 -0
- package/dist/approval-reasons.test.d.ts +1 -0
- package/dist/approval-reasons.test.js +115 -0
- package/dist/approval.d.ts +29 -0
- package/dist/approval.js +412 -0
- package/dist/approval.test.d.ts +1 -0
- package/dist/approval.test.js +462 -0
- package/dist/card-route.d.ts +15 -0
- package/dist/card-route.js +478 -0
- package/dist/card-route.test.d.ts +1 -0
- package/dist/card-route.test.js +181 -0
- package/dist/card-test-support.d.ts +15 -0
- package/dist/card-test-support.js +160 -0
- package/dist/client/ContainerCard.d.ts +6 -0
- package/dist/client/ContainerCard.js +79 -0
- package/dist/client/card-client.d.ts +6 -0
- package/dist/client/card-client.js +47 -0
- package/dist/client/card-protocol.d.ts +101 -0
- package/dist/client/card-protocol.js +10 -0
- package/dist/client/container-card-caches.d.ts +10 -0
- package/dist/client/container-card-caches.js +31 -0
- package/dist/client/container-card-controller.d.ts +107 -0
- package/dist/client/container-card-controller.js +207 -0
- package/dist/client/container-card-create-modal.d.ts +24 -0
- package/dist/client/container-card-create-modal.js +109 -0
- package/dist/client/container-card-directory-styles.d.ts +5 -0
- package/dist/client/container-card-directory-styles.js +246 -0
- package/dist/client/container-card-directory.d.ts +12 -0
- package/dist/client/container-card-directory.js +144 -0
- package/dist/client/container-card-editors.d.ts +30 -0
- package/dist/client/container-card-editors.js +116 -0
- package/dist/client/container-card-images.d.ts +37 -0
- package/dist/client/container-card-images.js +35 -0
- package/dist/client/container-card-paths.d.ts +20 -0
- package/dist/client/container-card-paths.js +88 -0
- package/dist/client/container-card-row.d.ts +30 -0
- package/dist/client/container-card-row.js +54 -0
- package/dist/client/container-card-secrets.d.ts +21 -0
- package/dist/client/container-card-secrets.js +71 -0
- package/dist/client/container-card-shared.d.ts +55 -0
- package/dist/client/container-card-shared.js +152 -0
- package/dist/client/container-card-styles.d.ts +21 -0
- package/dist/client/container-card-styles.js +143 -0
- package/dist/client/container-card-volumes.d.ts +12 -0
- package/dist/client/container-card-volumes.js +37 -0
- package/dist/client/container-card-workspace.d.ts +31 -0
- package/dist/client/container-card-workspace.js +24 -0
- package/dist/client/directory-picker.d.ts +12 -0
- package/dist/client/directory-picker.js +30 -0
- package/dist/client/index.d.ts +5 -0
- package/dist/client/index.js +4419 -0
- package/dist/client/locales.d.ts +9 -0
- package/dist/client/locales.js +370 -0
- package/dist/client/read-only-approval.d.ts +8 -0
- package/dist/client/read-only-approval.js +63 -0
- package/dist/client/slot-contract.d.ts +12 -0
- package/dist/client/slot-contract.js +4 -0
- package/dist/client/terminal-styles.d.ts +5 -0
- package/dist/client/terminal-styles.js +31 -0
- package/dist/client/tool-views.d.ts +8 -0
- package/dist/client/tool-views.js +241 -0
- package/dist/containers.test.d.ts +1 -0
- package/dist/containers.test.js +443 -0
- package/dist/daemons.test.d.ts +1 -0
- package/dist/daemons.test.js +153 -0
- package/dist/fs-provider.d.ts +23 -0
- package/dist/fs-provider.js +184 -0
- package/dist/fs-provider.test.d.ts +1 -0
- package/dist/fs-provider.test.js +260 -0
- package/dist/generated/version.d.ts +2 -0
- package/dist/generated/version.js +6 -0
- package/dist/grpc/proto/dshctl/v1/control.proto +133 -0
- package/dist/grpc/proto/dshguest/v1/guest.proto +79 -0
- package/dist/grpc/runtime-client.d.ts +5 -0
- package/dist/grpc/runtime-client.js +43 -0
- package/dist/guest-rpc.d.ts +62 -0
- package/dist/guest-rpc.js +391 -0
- package/dist/images.test.d.ts +1 -0
- package/dist/images.test.js +79 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +140 -0
- package/dist/misc.test.d.ts +1 -0
- package/dist/misc.test.js +141 -0
- package/dist/mount-enums.d.ts +22 -0
- package/dist/mount-enums.js +81 -0
- package/dist/mount-input.d.ts +14 -0
- package/dist/mount-input.js +64 -0
- package/dist/mounts.test.d.ts +1 -0
- package/dist/mounts.test.js +408 -0
- package/dist/output-reader.d.ts +34 -0
- package/dist/output-reader.js +87 -0
- package/dist/paths.test.d.ts +1 -0
- package/dist/paths.test.js +74 -0
- package/dist/preferences.d.ts +6 -0
- package/dist/preferences.js +27 -0
- package/dist/project-path.d.ts +6 -0
- package/dist/project-path.js +73 -0
- package/dist/project-path.test.d.ts +1 -0
- package/dist/project-path.test.js +52 -0
- package/dist/prompts.d.ts +13 -0
- package/dist/prompts.js +149 -0
- package/dist/public.d.ts +4 -0
- package/dist/public.js +63 -0
- package/dist/read-only-shell.d.ts +14 -0
- package/dist/read-only-shell.js +150 -0
- package/dist/read-only-shell.test.d.ts +1 -0
- package/dist/read-only-shell.test.js +127 -0
- package/dist/secrets.test.d.ts +1 -0
- package/dist/secrets.test.js +181 -0
- package/dist/settings-commands.test.d.ts +1 -0
- package/dist/settings-commands.test.js +549 -0
- package/dist/settings-create.test.d.ts +1 -0
- package/dist/settings-create.test.js +394 -0
- package/dist/settings-mounts.test.d.ts +1 -0
- package/dist/settings-mounts.test.js +467 -0
- package/dist/settings-paths.test.d.ts +1 -0
- package/dist/settings-paths.test.js +65 -0
- package/dist/settings-schema.d.ts +14 -0
- package/dist/settings-schema.js +14 -0
- package/dist/settings-workspaces.test.d.ts +1 -0
- package/dist/settings-workspaces.test.js +265 -0
- package/dist/spill-store.d.ts +17 -0
- package/dist/spill-store.js +54 -0
- package/dist/spill-store.test.d.ts +1 -0
- package/dist/spill-store.test.js +59 -0
- package/dist/subprocess.d.ts +9 -0
- package/dist/subprocess.js +369 -0
- package/dist/subprocess.test.d.ts +1 -0
- package/dist/subprocess.test.js +234 -0
- package/dist/test-support.d.ts +328 -0
- package/dist/test-support.js +437 -0
- package/dist/tool-defs.d.ts +26 -0
- package/dist/tool-defs.js +188 -0
- package/dist/tool-handlers.d.ts +2 -0
- package/dist/tool-handlers.js +546 -0
- package/dist/tool-params.d.ts +803 -0
- package/dist/tool-params.js +494 -0
- package/dist/tool-schemas.d.ts +2 -0
- package/dist/tool-schemas.js +5 -0
- package/dist/tool-views.d.ts +2 -0
- package/dist/tool-views.js +56 -0
- package/dist/views.test.d.ts +1 -0
- package/dist/views.test.js +23 -0
- package/dist/volumes.test.d.ts +1 -0
- package/dist/volumes.test.js +40 -0
- package/dist/workspace-binding.d.ts +40 -0
- package/dist/workspace-binding.js +275 -0
- package/dist/workspace-binding.test.d.ts +1 -0
- package/dist/workspace-binding.test.js +337 -0
- package/docs/architecture.md +173 -0
- package/docs/architecture.zh.md +129 -0
- package/docs/configuration.md +160 -0
- package/docs/configuration.zh.md +147 -0
- package/docs/development.md +142 -0
- package/docs/development.zh.md +135 -0
- package/docs/install-dsh-and-dsh-podman.md +165 -0
- package/docs/install-dsh-and-dsh-podman.zh.md +145 -0
- package/docs/usage.md +393 -0
- package/docs/usage.zh.md +315 -0
- package/package.json +119 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Elouan Martinet <exa@elou.world>
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: MIT
|
|
4
|
+
// Test doubles for the settings card. `installCardCommandDriver` stands in for
|
|
5
|
+
// the browser transport: it hands each committed command to the real
|
|
6
|
+
// `runCommand` and folds the real `cardSnapshot` back into the fake scope, so
|
|
7
|
+
// the command tests stay focused on the command-to-control mapping. The HTTP
|
|
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
|
+
import { cardSnapshot, runCommand } from "./card-route.js";
|
|
12
|
+
export function fakeScope(initial) {
|
|
13
|
+
const scope = {
|
|
14
|
+
value: { ...initial },
|
|
15
|
+
listeners: [],
|
|
16
|
+
async update(patch) {
|
|
17
|
+
const prev = scope.value;
|
|
18
|
+
scope.value = { ...scope.value, ...patch };
|
|
19
|
+
for (const listener of [...scope.listeners]) {
|
|
20
|
+
await listener(scope.value, prev);
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
watch(callback) {
|
|
24
|
+
scope.listeners.push(callback);
|
|
25
|
+
return () => {
|
|
26
|
+
scope.listeners = scope.listeners.filter((listener) => listener !== callback);
|
|
27
|
+
};
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
return scope;
|
|
31
|
+
}
|
|
32
|
+
export function fakeContext(scope) {
|
|
33
|
+
return {
|
|
34
|
+
inject(deps, callback) {
|
|
35
|
+
assert.deepEqual(deps, ["settings"]);
|
|
36
|
+
callback({
|
|
37
|
+
settings: {
|
|
38
|
+
register() {
|
|
39
|
+
return scope;
|
|
40
|
+
},
|
|
41
|
+
get() {
|
|
42
|
+
return undefined;
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export function baseValue() {
|
|
50
|
+
return {
|
|
51
|
+
defaultImage: "archlinux",
|
|
52
|
+
socketsRoot: "/run/dsh-podman",
|
|
53
|
+
uiLocale: "",
|
|
54
|
+
notice: "",
|
|
55
|
+
command: null,
|
|
56
|
+
workspaces: [],
|
|
57
|
+
containers: [],
|
|
58
|
+
images: [],
|
|
59
|
+
volumes: [],
|
|
60
|
+
secrets: [],
|
|
61
|
+
caches: [],
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
// installCardCommandDriver wires a fake settings scope to the real command
|
|
65
|
+
// runner and snapshot builder, standing in for the browser transport: it
|
|
66
|
+
// mirrors what the card does per request — run the command, then refresh the
|
|
67
|
+
// live snapshot and record the notice.
|
|
68
|
+
export function installCardCommandDriver(ctx, resolver, registry) {
|
|
69
|
+
let installed = Promise.resolve();
|
|
70
|
+
ctx.inject(["settings"], (sctx) => {
|
|
71
|
+
const scope = sctx.settings.register(CONTAINER_NS, settingsSchema, {
|
|
72
|
+
base: {
|
|
73
|
+
defaultImage: resolver.getConfig().defaultImage,
|
|
74
|
+
socketsRoot: resolver.getConfig().socketsRoot,
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
const refresh = async (notice = "") => {
|
|
78
|
+
let view = {};
|
|
79
|
+
try {
|
|
80
|
+
const snapshot = await cardSnapshot(resolver, registry);
|
|
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
|
+
};
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
// Command tests do not always stub every list call.
|
|
92
|
+
}
|
|
93
|
+
await scope.update({ ...view, notice });
|
|
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();
|
|
118
|
+
});
|
|
119
|
+
return installed;
|
|
120
|
+
}
|
|
121
|
+
export async function installedMountScope() {
|
|
122
|
+
const scope = fakeScope(baseValue());
|
|
123
|
+
const calls = [];
|
|
124
|
+
const resolver = {
|
|
125
|
+
getConfig: () => ({}),
|
|
126
|
+
setConfig: () => { },
|
|
127
|
+
async control(method, request) {
|
|
128
|
+
calls.push([method, request]);
|
|
129
|
+
if (method === "listContainers")
|
|
130
|
+
return { containers: [] };
|
|
131
|
+
if (method === "listImages")
|
|
132
|
+
return { images: [] };
|
|
133
|
+
if (method === "listWorkspaces")
|
|
134
|
+
return { workspaces: [] };
|
|
135
|
+
return {};
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
await installCardCommandDriver(fakeContext(scope), resolver);
|
|
139
|
+
await scope.update({}); // settle the queued async refresh
|
|
140
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
141
|
+
calls.length = 0;
|
|
142
|
+
return { scope, calls };
|
|
143
|
+
}
|
|
144
|
+
export function mountCommand(op, mount) {
|
|
145
|
+
return {
|
|
146
|
+
op,
|
|
147
|
+
workspace: "w1",
|
|
148
|
+
container: "web",
|
|
149
|
+
image: "",
|
|
150
|
+
mount: {
|
|
151
|
+
kind: "",
|
|
152
|
+
project: "",
|
|
153
|
+
destination: "",
|
|
154
|
+
mode: "",
|
|
155
|
+
volume: "",
|
|
156
|
+
secret: "",
|
|
157
|
+
...mount,
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type ContainerCardFace } from "./container-card-controller.js";
|
|
2
|
+
import { NS } from "./locales.js";
|
|
3
|
+
import { type InjectFace, type PropsLocale, type PropsRuntime } from "@deepseek-ai/dsh-client-ui-slots";
|
|
4
|
+
import { type ReactNode } from "react";
|
|
5
|
+
export type ContainerCardProps = PropsRuntime<"settings.plugin.item"> & PropsLocale<typeof NS> & InjectFace<ContainerCardFace>;
|
|
6
|
+
export declare function ContainerCard(props: ContainerCardProps): ReactNode;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// SPDX-FileCopyrightText: 2026 Elouan Martinet <exa@elou.world>
|
|
3
|
+
//
|
|
4
|
+
// SPDX-License-Identifier: MIT
|
|
5
|
+
import { BaseImageRow, ImageBuildModal, ImageItem } from "./container-card-images.js";
|
|
6
|
+
import { CachesSection } from "./container-card-caches.js";
|
|
7
|
+
import { SecretsSection } from "./container-card-secrets.js";
|
|
8
|
+
import { CardChevron, ConfigField, ConfirmButton } from "./container-card-shared.js";
|
|
9
|
+
import { banner, cardBodyStyle, cardDescriptionStyle, cardHeadTextStyle, cardHeaderStyle, cardHoverStyle, cardNameStyle, cardOpenStyle, cardStyle, footerRow, hint, imageSelect, sectionTitle, } from "./container-card-styles.js";
|
|
10
|
+
import { VolumesSection } from "./container-card-volumes.js";
|
|
11
|
+
import { WorkspaceSection } from "./container-card-workspace.js";
|
|
12
|
+
import { Button, Input, Modal } from "@deepseek-ai/dsh-client-ui-primitives";
|
|
13
|
+
import { useEffect, useId, useState } from "react";
|
|
14
|
+
export function ContainerCard(props) {
|
|
15
|
+
const { t } = props;
|
|
16
|
+
const state = props.useContainerCard((snapshot) => snapshot);
|
|
17
|
+
const [open, setOpen] = useState(false);
|
|
18
|
+
const [hovered, setHovered] = useState(false);
|
|
19
|
+
const [buildOpen, setBuildOpen] = useState(false);
|
|
20
|
+
const [imageId, setImageId] = useState("");
|
|
21
|
+
const [parent, setParent] = useState("");
|
|
22
|
+
const [packages, setPackages] = useState([]);
|
|
23
|
+
const [defaultOpen, setDefaultOpen] = useState(false);
|
|
24
|
+
const [defaultImage, setDefaultImage] = useState(state.defaultImage);
|
|
25
|
+
const projectsRootId = useId();
|
|
26
|
+
// The card mounts with the settings panel, so this re-reads the live state
|
|
27
|
+
// every time the Plugins section is opened: a workspace created since the
|
|
28
|
+
// last read shows up without pressing Reload this view.
|
|
29
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: mount-only refresh
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
props.reload();
|
|
32
|
+
}, []);
|
|
33
|
+
const baseImages = state.images.filter((image) => image.isBase);
|
|
34
|
+
const customImages = state.images.filter((image) => !image.isBase);
|
|
35
|
+
const defaultCandidates = [
|
|
36
|
+
...baseImages.map((image) => image.imageId),
|
|
37
|
+
...customImages.map((image) => image.imageId),
|
|
38
|
+
];
|
|
39
|
+
if (!state.available) {
|
|
40
|
+
return (_jsx("p", { style: { padding: "8px 0", fontSize: "13px", opacity: 0.8 }, children: t("unavailable") }));
|
|
41
|
+
}
|
|
42
|
+
return (_jsxs("li", { style: {
|
|
43
|
+
...cardStyle,
|
|
44
|
+
...(open ? cardOpenStyle : {}),
|
|
45
|
+
...(hovered && !open ? cardHoverStyle : {}),
|
|
46
|
+
}, onPointerEnter: () => setHovered(true), onPointerLeave: () => setHovered(false), children: [_jsxs("button", { type: "button", style: cardHeaderStyle, "aria-expanded": open, onClick: () => setOpen(!open), children: [_jsxs("span", { style: cardHeadTextStyle, children: [_jsxs("span", { style: cardNameStyle, children: [t("cardTitle"), state.version ? ` ${state.version}` : "", state.commit ? ` · ${state.commit}` : ""] }), _jsx("span", { style: cardDescriptionStyle, children: t("cardDescription") })] }), _jsx(CardChevron, { open: open })] }), open ? (_jsxs("div", { style: cardBodyStyle, children: [state.notice === "" ? null : (_jsxs("div", { style: banner, role: "status", children: [t("notice"), ": ", state.notice] })), _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 }, workspace.workspaceSlug)))), _jsxs("div", { style: {
|
|
47
|
+
display: "flex",
|
|
48
|
+
flexWrap: "wrap",
|
|
49
|
+
alignItems: "center",
|
|
50
|
+
gap: "8px",
|
|
51
|
+
}, children: [_jsx("div", { style: { ...sectionTitle, flex: 1 }, children: t("imagesTitle") }), _jsx(Button, { variant: "outline", size: "sm", disabled: state.busy, onClick: () => {
|
|
52
|
+
setDefaultImage(state.defaultImage);
|
|
53
|
+
setDefaultOpen(true);
|
|
54
|
+
}, children: t("setDefaultImage") }), _jsx(Button, { variant: "outline", size: "sm", disabled: state.busy, onClick: () => {
|
|
55
|
+
setImageId("");
|
|
56
|
+
setParent(baseImages[0]?.imageId ?? customImages[0]?.imageId ?? "");
|
|
57
|
+
setPackages([]);
|
|
58
|
+
setBuildOpen(true);
|
|
59
|
+
}, 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: () => {
|
|
60
|
+
props.buildImage(imageId.trim(), parent.trim(), packages);
|
|
61
|
+
setImageId("");
|
|
62
|
+
setParent("");
|
|
63
|
+
setPackages([]);
|
|
64
|
+
setBuildOpen(false);
|
|
65
|
+
} }), _jsx(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: () => {
|
|
66
|
+
props.setDefaultImage(defaultImage);
|
|
67
|
+
setDefaultOpen(false);
|
|
68
|
+
}, children: t("setDefaultImage") })] }), children: _jsxs("select", { style: imageSelect, value: defaultImage, onChange: (event) => setDefaultImage(event.target.value), children: [defaultCandidates.length === 0 ? (_jsx("option", { value: "", children: t("none") })) : null, defaultCandidates.map((name) => (_jsx("option", { value: name, children: name }, name)))] }) }), _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("div", { style: sectionTitle, children: t("configTitle") }), _jsxs("div", { style: {
|
|
69
|
+
display: "flex",
|
|
70
|
+
flexWrap: "wrap",
|
|
71
|
+
alignItems: "center",
|
|
72
|
+
gap: "8px",
|
|
73
|
+
padding: "8px 0",
|
|
74
|
+
}, children: [_jsx("label", { htmlFor: projectsRootId, style: {
|
|
75
|
+
fontSize: "13px",
|
|
76
|
+
color: "var(--dsw-alias-label-secondary)",
|
|
77
|
+
minWidth: "110px",
|
|
78
|
+
}, children: t("projectsRoot") }), _jsx(Input, { id: projectsRootId, value: state.projectsRoot, disabled: true, style: { width: "200px" } })] }), _jsx(ConfigField, { t: t, label: t("socketsRoot"), value: state.socketsRootDraft, current: state.socketsRoot, writable: state.writable, onChange: props.editSocketsRoot, onSave: props.saveSocketsRoot, onDiscard: props.discardSocketsRoot }), _jsx("div", { style: footerRow, children: _jsx(Button, { variant: "outline", size: "sm", disabled: state.busy, onClick: props.reload, children: state.busy ? t("busy") : t("reload") }) })] })) : null] }));
|
|
79
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type CardCommandResult, type CardSnapshot, type CommandRequest } from "./card-protocol.js";
|
|
2
|
+
export interface CardClient {
|
|
3
|
+
snapshot(): Promise<CardSnapshot>;
|
|
4
|
+
command(request: CommandRequest): Promise<CardCommandResult>;
|
|
5
|
+
}
|
|
6
|
+
export declare function createCardClient(): CardClient;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Elouan Martinet <exa@elou.world>
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: MIT
|
|
4
|
+
// The card's browser half of the host route. The route is served on the
|
|
5
|
+
// harness API channel, so a same-origin fetch carries the browser session the
|
|
6
|
+
// harness issued for it.
|
|
7
|
+
import { CARD_PATH, } from "./card-protocol.js";
|
|
8
|
+
// The route path already carries the harness API prefix, and a same-origin
|
|
9
|
+
// fetch carries the browser session the harness issued for it.
|
|
10
|
+
const CARD_URL = CARD_PATH;
|
|
11
|
+
async function failure(response) {
|
|
12
|
+
try {
|
|
13
|
+
const body = (await response.json());
|
|
14
|
+
if (typeof body.error === "string" && body.error !== "") {
|
|
15
|
+
return new Error(body.error);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
// A non-JSON body falls through to the status text.
|
|
20
|
+
}
|
|
21
|
+
return new Error(`card request failed (${response.status})`);
|
|
22
|
+
}
|
|
23
|
+
export function createCardClient() {
|
|
24
|
+
return {
|
|
25
|
+
async snapshot() {
|
|
26
|
+
const response = await fetch(CARD_URL, {
|
|
27
|
+
headers: { accept: "application/json" },
|
|
28
|
+
});
|
|
29
|
+
if (!response.ok)
|
|
30
|
+
throw await failure(response);
|
|
31
|
+
return (await response.json());
|
|
32
|
+
},
|
|
33
|
+
async command(request) {
|
|
34
|
+
const response = await fetch(CARD_URL, {
|
|
35
|
+
method: "POST",
|
|
36
|
+
headers: {
|
|
37
|
+
accept: "application/json",
|
|
38
|
+
"content-type": "application/json",
|
|
39
|
+
},
|
|
40
|
+
body: JSON.stringify({ command: request }),
|
|
41
|
+
});
|
|
42
|
+
if (!response.ok)
|
|
43
|
+
throw await failure(response);
|
|
44
|
+
return (await response.json());
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
export declare const CARD_PATH = "/api/podman/card";
|
|
2
|
+
export interface MountInput {
|
|
3
|
+
kind: string;
|
|
4
|
+
project: string;
|
|
5
|
+
destination: string;
|
|
6
|
+
mode: string;
|
|
7
|
+
volume: string;
|
|
8
|
+
secret: string;
|
|
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";
|
|
11
|
+
export interface CommandRequest {
|
|
12
|
+
op: CommandOp;
|
|
13
|
+
workspace: string;
|
|
14
|
+
projectName: string;
|
|
15
|
+
image: string;
|
|
16
|
+
mounts: readonly MountInput[];
|
|
17
|
+
paths: readonly string[];
|
|
18
|
+
value: string;
|
|
19
|
+
env: Record<string, string>;
|
|
20
|
+
container: string;
|
|
21
|
+
secret: string;
|
|
22
|
+
secretEnv: string;
|
|
23
|
+
length: number;
|
|
24
|
+
charset: string;
|
|
25
|
+
packages: string[];
|
|
26
|
+
secretEnvMap: Record<string, string>;
|
|
27
|
+
cacheMode: string;
|
|
28
|
+
mount: MountInput | null;
|
|
29
|
+
}
|
|
30
|
+
export interface ContainerView {
|
|
31
|
+
containerName: string;
|
|
32
|
+
workspaceSlug: string;
|
|
33
|
+
imageId: string;
|
|
34
|
+
status: string;
|
|
35
|
+
createdAt: string;
|
|
36
|
+
mounts: readonly {
|
|
37
|
+
projectName: string;
|
|
38
|
+
destination: string;
|
|
39
|
+
kind: string;
|
|
40
|
+
mode: string;
|
|
41
|
+
volume: string;
|
|
42
|
+
secret: string;
|
|
43
|
+
}[];
|
|
44
|
+
paths: readonly string[];
|
|
45
|
+
env: Record<string, string>;
|
|
46
|
+
secretEnv: Record<string, string>;
|
|
47
|
+
}
|
|
48
|
+
export interface ImageView {
|
|
49
|
+
imageId: string;
|
|
50
|
+
parent: string;
|
|
51
|
+
packages: readonly string[];
|
|
52
|
+
imageTag: string;
|
|
53
|
+
builtAt: string;
|
|
54
|
+
isBase: boolean;
|
|
55
|
+
status: string;
|
|
56
|
+
primitive: string;
|
|
57
|
+
packageManager: string;
|
|
58
|
+
basePublic: boolean;
|
|
59
|
+
}
|
|
60
|
+
export interface VolumeView {
|
|
61
|
+
name: string;
|
|
62
|
+
}
|
|
63
|
+
export interface SecretView {
|
|
64
|
+
name: string;
|
|
65
|
+
}
|
|
66
|
+
export interface CacheView {
|
|
67
|
+
manager: string;
|
|
68
|
+
path: string;
|
|
69
|
+
files: number;
|
|
70
|
+
bytes: number;
|
|
71
|
+
}
|
|
72
|
+
export interface WorkspaceView {
|
|
73
|
+
workspaceSlug: string;
|
|
74
|
+
projectName: string;
|
|
75
|
+
containerName: string;
|
|
76
|
+
imageId: string;
|
|
77
|
+
status: string;
|
|
78
|
+
createdAt: string;
|
|
79
|
+
mounts: readonly {
|
|
80
|
+
projectName: string;
|
|
81
|
+
mode: string;
|
|
82
|
+
}[];
|
|
83
|
+
}
|
|
84
|
+
/** The live orchestrator state the card renders. Nothing here is persisted. */
|
|
85
|
+
export interface CardSnapshot {
|
|
86
|
+
version: string;
|
|
87
|
+
commit: string;
|
|
88
|
+
projectsRoot: string;
|
|
89
|
+
workspaces: readonly WorkspaceView[];
|
|
90
|
+
containers: readonly ContainerView[];
|
|
91
|
+
images: readonly ImageView[];
|
|
92
|
+
volumes: readonly VolumeView[];
|
|
93
|
+
secrets: readonly SecretView[];
|
|
94
|
+
caches: readonly CacheView[];
|
|
95
|
+
}
|
|
96
|
+
export interface CardCommandResult {
|
|
97
|
+
notice?: string;
|
|
98
|
+
}
|
|
99
|
+
export interface CardError {
|
|
100
|
+
error: string;
|
|
101
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Elouan Martinet <exa@elou.world>
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: MIT
|
|
4
|
+
// The settings card talks to the host over one authenticated route on the
|
|
5
|
+
// harness's connection service. These shapes are shared by both halves, and
|
|
6
|
+
// neither the snapshot nor the commands touch the persisted settings document.
|
|
7
|
+
// The full route path, harness API prefix included: the connection service
|
|
8
|
+
// registers and matches the whole pathname (a path outside `/api/` is refused
|
|
9
|
+
// at registration).
|
|
10
|
+
export const CARD_PATH = "/api/podman/card";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type CacheView } from "./container-card-controller.js";
|
|
2
|
+
import { NS } from "./locales.js";
|
|
3
|
+
import { type TranslateNS } from "@deepseek-ai/dsh-client-ui-slots";
|
|
4
|
+
import { type ReactNode } from "react";
|
|
5
|
+
export declare function CachesSection(props: {
|
|
6
|
+
t: TranslateNS<typeof NS>;
|
|
7
|
+
caches: readonly CacheView[];
|
|
8
|
+
busy: boolean;
|
|
9
|
+
onClean: (mode: string) => void;
|
|
10
|
+
}): ReactNode;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// SPDX-FileCopyrightText: 2026 Elouan Martinet <exa@elou.world>
|
|
3
|
+
//
|
|
4
|
+
// SPDX-License-Identifier: MIT
|
|
5
|
+
import { ConfirmButton } from "./container-card-shared.js";
|
|
6
|
+
import { hint, sectionTitle, wsBody } from "./container-card-styles.js";
|
|
7
|
+
// Format a byte count with binary units, e.g. "1.2 GiB".
|
|
8
|
+
function formatBytes(bytes) {
|
|
9
|
+
if (!Number.isFinite(bytes) || bytes <= 0)
|
|
10
|
+
return "0 B";
|
|
11
|
+
const units = ["B", "KiB", "MiB", "GiB", "TiB"];
|
|
12
|
+
let value = bytes;
|
|
13
|
+
let unit = 0;
|
|
14
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
15
|
+
value /= 1024;
|
|
16
|
+
unit += 1;
|
|
17
|
+
}
|
|
18
|
+
return `${unit === 0 ? String(value) : value.toFixed(1)} ${units[unit]}`;
|
|
19
|
+
}
|
|
20
|
+
// The package-manager build caches: their current size and the two cleanup
|
|
21
|
+
// actions. The caches are re-downloadable, so neither action is destructive.
|
|
22
|
+
export function CachesSection(props) {
|
|
23
|
+
const { t, caches, busy, onClean } = props;
|
|
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 ? (_jsx("p", { style: { ...hint, margin: 0 }, children: t("noCaches") })) : (caches.map((cache) => (_jsxs("div", { style: {
|
|
26
|
+
display: "flex",
|
|
27
|
+
flexWrap: "wrap",
|
|
28
|
+
alignItems: "center",
|
|
29
|
+
gap: "8px",
|
|
30
|
+
}, children: [_jsx("strong", { style: { color: "var(--dsw-alias-label-primary)" }, children: cache.manager }), _jsx("span", { style: hint, children: t("cacheUsage", { size: formatBytes(cache.bytes), n: cache.files }) })] }, 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
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { type SnapshotStore } from "@deepseek-ai/dsh-client-store";
|
|
2
|
+
import type { SettingsScope, SettingsScopeSnapshot } from "@deepseek-ai/dsh-client-ui-settings/client";
|
|
3
|
+
import type { CacheView, ContainerView, ImageView, MountInput, SecretView, VolumeView, WorkspaceView } from "./card-protocol.js";
|
|
4
|
+
import { type CardClient } from "./card-client.js";
|
|
5
|
+
import { type DirectoryPickerFace } from "./directory-picker.js";
|
|
6
|
+
export declare const CONTAINER_NS = "podman";
|
|
7
|
+
export interface ContainerSettings {
|
|
8
|
+
defaultImage: string;
|
|
9
|
+
socketsRoot: string;
|
|
10
|
+
uiLocale?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface ProjectMountView {
|
|
13
|
+
projectName: string;
|
|
14
|
+
destination: string;
|
|
15
|
+
kind: string;
|
|
16
|
+
mode: string;
|
|
17
|
+
volume: string;
|
|
18
|
+
secret: string;
|
|
19
|
+
}
|
|
20
|
+
export interface ContainerCreateConfig {
|
|
21
|
+
image?: string;
|
|
22
|
+
env?: Record<string, string>;
|
|
23
|
+
mounts?: readonly MountInput[];
|
|
24
|
+
paths?: readonly string[];
|
|
25
|
+
secretEnv?: Record<string, string>;
|
|
26
|
+
}
|
|
27
|
+
export type { CacheView, ContainerView, ImageView, MountInput, SecretView, VolumeView, WorkspaceView, };
|
|
28
|
+
export interface CardState {
|
|
29
|
+
available: boolean;
|
|
30
|
+
writable: boolean;
|
|
31
|
+
busy: boolean;
|
|
32
|
+
notice: string;
|
|
33
|
+
version: string;
|
|
34
|
+
commit: string;
|
|
35
|
+
defaultImage: string;
|
|
36
|
+
defaultImageDraft: string;
|
|
37
|
+
socketsRoot: string;
|
|
38
|
+
socketsRootDraft: string;
|
|
39
|
+
projectsRoot: string;
|
|
40
|
+
directoryPicker?: DirectoryPickerFace;
|
|
41
|
+
workspaces: readonly WorkspaceView[];
|
|
42
|
+
containers: readonly ContainerView[];
|
|
43
|
+
images: readonly ImageView[];
|
|
44
|
+
volumes: readonly VolumeView[];
|
|
45
|
+
secrets: readonly SecretView[];
|
|
46
|
+
caches: readonly CacheView[];
|
|
47
|
+
}
|
|
48
|
+
export interface ContainerCardFace {
|
|
49
|
+
hooks: {
|
|
50
|
+
containerCard: SnapshotStore<CardState>;
|
|
51
|
+
};
|
|
52
|
+
reload: () => void;
|
|
53
|
+
remove: (workspace: string) => void;
|
|
54
|
+
cleanCaches: (mode: string) => void;
|
|
55
|
+
removeWorkspace: (workspace: string) => void;
|
|
56
|
+
recreate: (workspace: string, image: string, env?: Record<string, string>) => void;
|
|
57
|
+
createContainer: (workspace: WorkspaceView, config?: ContainerCreateConfig) => void;
|
|
58
|
+
startContainer: (workspace: WorkspaceView, container: string, config?: ContainerCreateConfig) => void;
|
|
59
|
+
addContainerMount: (workspace: string, container: string, mount: MountInput) => void;
|
|
60
|
+
removeContainerMount: (workspace: string, container: string, mount: MountInput) => void;
|
|
61
|
+
updateContainerMount: (workspace: string, container: string, mount: MountInput) => void;
|
|
62
|
+
setContainerPaths: (workspace: string, container: string, paths: readonly string[]) => void;
|
|
63
|
+
createVolume: (name: string) => void;
|
|
64
|
+
removeVolume: (name: string) => void;
|
|
65
|
+
removeImage: (imageId: string) => void;
|
|
66
|
+
rebuildImage: (imageId: string) => void;
|
|
67
|
+
rebuildAllImages: () => void;
|
|
68
|
+
buildImage: (imageId: string, parent: string, packages: string[]) => void;
|
|
69
|
+
rebuildBaseImage: (name: string) => void;
|
|
70
|
+
pullBaseImage: (name: string) => void;
|
|
71
|
+
setDefaultImage: (name: string) => void;
|
|
72
|
+
createSecret: (name: string, length?: number, charset?: string) => void;
|
|
73
|
+
removeSecret: (name: string) => void;
|
|
74
|
+
setSecret: (name: string, value: string) => void;
|
|
75
|
+
addContainerSecret: (workspace: string, envVar: string, secret: string) => void;
|
|
76
|
+
removeContainerSecret: (workspace: string, envVar: string) => void;
|
|
77
|
+
editDefaultImage: (text: string) => void;
|
|
78
|
+
saveDefaultImage: () => void;
|
|
79
|
+
discardDefaultImage: () => void;
|
|
80
|
+
editSocketsRoot: (text: string) => void;
|
|
81
|
+
saveSocketsRoot: () => void;
|
|
82
|
+
discardSocketsRoot: () => void;
|
|
83
|
+
}
|
|
84
|
+
export declare class ContainerCardController {
|
|
85
|
+
private readonly scope;
|
|
86
|
+
private readonly store;
|
|
87
|
+
private readonly drafts;
|
|
88
|
+
private readonly client;
|
|
89
|
+
private snapshot;
|
|
90
|
+
private busy;
|
|
91
|
+
private notice;
|
|
92
|
+
private directoryPicker;
|
|
93
|
+
constructor(scope: SettingsScope<ContainerSettings>, client?: CardClient);
|
|
94
|
+
private draft;
|
|
95
|
+
private project;
|
|
96
|
+
private publish;
|
|
97
|
+
setDirectoryPicker(picker: DirectoryPickerFace | undefined): void;
|
|
98
|
+
private command;
|
|
99
|
+
private dispatch;
|
|
100
|
+
private reload;
|
|
101
|
+
private edit;
|
|
102
|
+
private save;
|
|
103
|
+
private discard;
|
|
104
|
+
inject(): ContainerCardFace;
|
|
105
|
+
private defaultImage;
|
|
106
|
+
}
|
|
107
|
+
export type { SettingsScopeSnapshot };
|