@exagone313/dsh-podman 0.2.0-rc.3 → 0.2.0-rc.4
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 +2 -2
- package/dist/card-route.js +52 -8
- package/dist/card-route.test.js +26 -1
- package/dist/client/ContainerCard.js +40 -4
- package/dist/client/card-protocol.d.ts +3 -0
- package/dist/client/container-card-controller.d.ts +3 -0
- package/dist/client/container-card-controller.js +6 -0
- package/dist/client/index.js +118 -5
- package/dist/client/locales.d.ts +1 -1
- package/dist/client/locales.js +18 -0
- package/dist/dsh-version.d.ts +1 -0
- package/dist/dsh-version.js +45 -0
- package/dist/generated/version.d.ts +2 -2
- package/dist/generated/version.js +2 -2
- package/dist/grpc/proto/dshctl/v1/control.proto +3 -0
- package/dist/settings-workspaces.test.js +3 -1
- package/dist/version-compat.d.ts +4 -0
- package/dist/version-compat.js +29 -0
- package/dist/version-compat.test.d.ts +1 -0
- package/dist/version-compat.test.js +27 -0
- package/dist/workspace-binding.d.ts +1 -1
- package/dist/workspace-binding.js +10 -3
- package/dist/workspace-binding.test.js +13 -2
- package/docs/architecture.md +10 -0
- package/docs/architecture.zh.md +6 -0
- package/docs/development.md +47 -12
- package/docs/development.zh.md +43 -12
- package/docs/install-dsh-and-dsh-podman.md +3 -13
- package/docs/install-dsh-and-dsh-podman.zh.md +2 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -32,9 +32,9 @@ per-project Podman containers.
|
|
|
32
32
|
|
|
33
33
|
This repository was written in major parts using AI models:
|
|
34
34
|
|
|
35
|
+
- DeepSeek Flash
|
|
35
36
|
- early commits using OpenAI GPT-5.6 Luna
|
|
36
|
-
-
|
|
37
|
-
- source code weakness fixes by Anthropic Claude Opus 5
|
|
37
|
+
- a few fixes using Anthropic Claude Opus 5
|
|
38
38
|
|
|
39
39
|
## License
|
|
40
40
|
|
package/dist/card-route.js
CHANGED
|
@@ -11,6 +11,19 @@ import { defaultMountMode, mountKindToProto, mountModeToProto } from "./mount-en
|
|
|
11
11
|
import { unaryGuest } from "./guest-rpc.js";
|
|
12
12
|
import { workspaceSlug } from "./workspace-binding.js";
|
|
13
13
|
import { GIT_COMMIT, VERSION } from "./generated/version.js";
|
|
14
|
+
import { versionState as pluginVersionState } from "./version-compat.js";
|
|
15
|
+
import { dshVersion } from "./dsh-version.js";
|
|
16
|
+
// readOrchestratorVersion asks the orchestrator for its version, or returns ""
|
|
17
|
+
// when the call fails (an orchestrator predating the handshake).
|
|
18
|
+
async function readOrchestratorVersion(resolver) {
|
|
19
|
+
try {
|
|
20
|
+
const response = await resolver.control("getVersion", {});
|
|
21
|
+
return String(response?.version ?? "");
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return "";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
14
27
|
// The cache clean modes the settings card can request, mapped to the control
|
|
15
28
|
// plane's enum names.
|
|
16
29
|
const CACHE_CLEAN_MODES = {
|
|
@@ -99,17 +112,48 @@ function dshWorkspaceCwd(registry, slug) {
|
|
|
99
112
|
}
|
|
100
113
|
/** Build the live snapshot the card renders. */
|
|
101
114
|
export async function cardSnapshot(resolver, workspaceRegistry) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
115
|
+
// The handshake RPC is exempt from the orchestrator's version check, so it
|
|
116
|
+
// succeeds even when the rest is refused; an older orchestrator that has no
|
|
117
|
+
// GetVersion yet leaves the version unknown.
|
|
118
|
+
const orchestratorVersion = await readOrchestratorVersion(resolver);
|
|
119
|
+
let listings;
|
|
120
|
+
try {
|
|
121
|
+
listings = await Promise.all([
|
|
122
|
+
resolver.control("listContainers", {}),
|
|
123
|
+
resolver.control("listImages", {}),
|
|
124
|
+
resolver.control("listWorkspaces", {}),
|
|
125
|
+
resolver.control("listVolumes", {}),
|
|
126
|
+
resolver.control("listSecrets", {}),
|
|
127
|
+
resolver.control("listCaches", {}),
|
|
128
|
+
]);
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
// An incompatible plugin is refused before any handler runs, so the card
|
|
132
|
+
// reports the mismatch instead of an unexplained empty snapshot.
|
|
133
|
+
if (error?.code !== "FailedPrecondition")
|
|
134
|
+
throw error;
|
|
135
|
+
return {
|
|
136
|
+
version: VERSION,
|
|
137
|
+
commit: GIT_COMMIT,
|
|
138
|
+
dshVersion: dshVersion(),
|
|
139
|
+
orchestratorVersion,
|
|
140
|
+
versionState: "major-mismatch",
|
|
141
|
+
projectsRoot: resolver.getConfig().projectsRoot,
|
|
142
|
+
workspaces: [],
|
|
143
|
+
containers: [],
|
|
144
|
+
images: [],
|
|
145
|
+
volumes: [],
|
|
146
|
+
secrets: [],
|
|
147
|
+
caches: [],
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
const [containers, images, workspaces, volumes, secrets, caches] = listings;
|
|
110
151
|
return {
|
|
111
152
|
version: VERSION,
|
|
112
153
|
commit: GIT_COMMIT,
|
|
154
|
+
dshVersion: dshVersion(),
|
|
155
|
+
orchestratorVersion,
|
|
156
|
+
versionState: pluginVersionState(VERSION, orchestratorVersion),
|
|
113
157
|
projectsRoot: resolver.getConfig().projectsRoot,
|
|
114
158
|
workspaces: mergeWorkspaceViews(dshWorkspaceViews(workspaceRegistry, resolver.getConfig().projectsRoot), orchestratorWorkspaceViews(workspaces.workspaces)),
|
|
115
159
|
containers: (containers.containers ?? []).map((container) => ({
|
package/dist/card-route.test.js
CHANGED
|
@@ -17,6 +17,8 @@ function fakeResolver() {
|
|
|
17
17
|
setConfig: () => { },
|
|
18
18
|
async control(method, request) {
|
|
19
19
|
calls.push([method, request]);
|
|
20
|
+
if (method === "getVersion")
|
|
21
|
+
return { version: "9.9.9", commit: "abc" };
|
|
20
22
|
if (method === "listContainers")
|
|
21
23
|
return { containers: [{ containerName: "c1" }] };
|
|
22
24
|
if (method === "listImages")
|
|
@@ -92,7 +94,30 @@ test("the card route serves the live snapshot on GET", async () => {
|
|
|
92
94
|
assert.deepEqual(snapshot.volumes, [{ name: "data" }]);
|
|
93
95
|
assert.deepEqual(snapshot.secrets, [{ name: "s1" }]);
|
|
94
96
|
assert.equal(snapshot.caches[0].bytes, 1024);
|
|
95
|
-
assert.deepEqual(calls.map(([method]) => method), ["listContainers", "listImages", "listWorkspaces", "listVolumes", "listSecrets", "listCaches"]);
|
|
97
|
+
assert.deepEqual(calls.map(([method]) => method), ["getVersion", "listContainers", "listImages", "listWorkspaces", "listVolumes", "listSecrets", "listCaches"]);
|
|
98
|
+
assert.equal(typeof snapshot.dshVersion, "string");
|
|
99
|
+
assert.equal(snapshot.orchestratorVersion, "9.9.9");
|
|
100
|
+
assert.equal(snapshot.versionState, "major-mismatch", "a differing major is surfaced on the card");
|
|
101
|
+
});
|
|
102
|
+
test("a refused control call reports the version mismatch instead of an empty snapshot", async () => {
|
|
103
|
+
const { resolver } = fakeResolver();
|
|
104
|
+
const refusal = new Error("plugin is incompatible");
|
|
105
|
+
refusal.code = "FailedPrecondition";
|
|
106
|
+
resolver.control = async (method) => {
|
|
107
|
+
if (method === "getVersion")
|
|
108
|
+
return { version: "9.9.9", commit: "abc" };
|
|
109
|
+
throw refusal;
|
|
110
|
+
};
|
|
111
|
+
const { ctx, routes } = fakeRouteContext();
|
|
112
|
+
registerCardRoute(ctx, resolver);
|
|
113
|
+
const response = await routes[0].fetch(cardRequest());
|
|
114
|
+
assert.equal(response.status, 200);
|
|
115
|
+
const snapshot = (await response.json());
|
|
116
|
+
assert.equal(typeof snapshot.dshVersion, "string");
|
|
117
|
+
assert.equal(snapshot.orchestratorVersion, "9.9.9");
|
|
118
|
+
assert.equal(snapshot.versionState, "major-mismatch");
|
|
119
|
+
assert.deepEqual(snapshot.containers, []);
|
|
120
|
+
assert.deepEqual(snapshot.workspaces, []);
|
|
96
121
|
});
|
|
97
122
|
test("the card route runs one command per POST and returns its notice", async () => {
|
|
98
123
|
const { resolver, calls } = fakeResolver();
|
|
@@ -9,8 +9,10 @@ import { CardChevron, ConfigField, ConfirmButton } from "./container-card-shared
|
|
|
9
9
|
import { banner, cardBodyStyle, cardDescriptionStyle, cardHeadTextStyle, cardHeaderStyle, cardHoverStyle, cardNameStyle, cardOpenStyle, cardStyle, footerRow, hint, imageSelect, sectionTitle, } from "./container-card-styles.js";
|
|
10
10
|
import { VolumesSection } from "./container-card-volumes.js";
|
|
11
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";
|
|
12
|
+
import { Button, Input, Modal, writeClipboard, } from "@deepseek-ai/dsh-client-ui-primitives";
|
|
13
|
+
import { useEffect, useId, useRef, useState, } from "react";
|
|
14
|
+
// The labels of the version lines, emphasised against their values.
|
|
15
|
+
const versionLabel = { fontWeight: 600 };
|
|
14
16
|
export function ContainerCard(props) {
|
|
15
17
|
const { t } = props;
|
|
16
18
|
const state = props.useContainerCard((snapshot) => snapshot);
|
|
@@ -22,7 +24,22 @@ export function ContainerCard(props) {
|
|
|
22
24
|
const [packages, setPackages] = useState([]);
|
|
23
25
|
const [defaultOpen, setDefaultOpen] = useState(false);
|
|
24
26
|
const [defaultImage, setDefaultImage] = useState(state.defaultImage);
|
|
27
|
+
const [copyState, setCopyState] = useState("idle");
|
|
28
|
+
const copyResetTimer = useRef();
|
|
25
29
|
const projectsRootId = useId();
|
|
30
|
+
// Copies the version lines as displayed, so a user can paste them into a bug
|
|
31
|
+
// report without transcribing anything.
|
|
32
|
+
const copyVersions = async () => {
|
|
33
|
+
const lines = [
|
|
34
|
+
`${t("dsh")} ${state.dshVersion || t("versionUnknown")}`,
|
|
35
|
+
`${t("cardTitle")} ${state.version}${state.commit ? ` · ${state.commit}` : ""}`,
|
|
36
|
+
`${t("orchestrator")} ${state.orchestratorVersion || t("versionUnknown")}`,
|
|
37
|
+
];
|
|
38
|
+
setCopyState((await writeClipboard(lines.join("\n"))) ? "copied" : "failed");
|
|
39
|
+
if (copyResetTimer.current !== undefined)
|
|
40
|
+
clearTimeout(copyResetTimer.current);
|
|
41
|
+
copyResetTimer.current = setTimeout(() => setCopyState("idle"), 1500);
|
|
42
|
+
};
|
|
26
43
|
// The card mounts with the settings panel, so this re-reads the live state
|
|
27
44
|
// every time the Plugins section is opened: a workspace created since the
|
|
28
45
|
// last read shows up without pressing Reload this view.
|
|
@@ -43,7 +60,11 @@ export function ContainerCard(props) {
|
|
|
43
60
|
...cardStyle,
|
|
44
61
|
...(open ? cardOpenStyle : {}),
|
|
45
62
|
...(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: [
|
|
63
|
+
}, 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: [_jsx("span", { style: cardNameStyle, children: t("cardTitle") }), _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] })), state.versionState === "ok" ? null : (_jsx("div", { style: state.versionState === "major-mismatch"
|
|
64
|
+
? { ...banner, borderColor: "var(--dsw-alias-state-error-primary)" }
|
|
65
|
+
: banner, role: state.versionState === "major-mismatch" ? "alert" : "status", children: t(state.versionState === "major-mismatch"
|
|
66
|
+
? "versionMismatchMajor"
|
|
67
|
+
: "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 }, workspace.workspaceSlug)))), _jsxs("div", { style: {
|
|
47
68
|
display: "flex",
|
|
48
69
|
flexWrap: "wrap",
|
|
49
70
|
alignItems: "center",
|
|
@@ -75,5 +96,20 @@ export function ContainerCard(props) {
|
|
|
75
96
|
fontSize: "13px",
|
|
76
97
|
color: "var(--dsw-alias-label-secondary)",
|
|
77
98
|
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 }),
|
|
99
|
+
}, 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 }), _jsxs("div", { style: {
|
|
100
|
+
display: "flex",
|
|
101
|
+
flexWrap: "wrap",
|
|
102
|
+
alignItems: "center",
|
|
103
|
+
gap: "8px",
|
|
104
|
+
}, children: [_jsx("div", { style: { ...sectionTitle, flex: 1 }, children: t("versionsTitle") }), _jsx(Button, { variant: "outline", size: "sm", onClick: () => void copyVersions(), children: copyState === "copied"
|
|
105
|
+
? t("copied")
|
|
106
|
+
: copyState === "failed"
|
|
107
|
+
? t("copyFailed")
|
|
108
|
+
: t("copyVersions") })] }), _jsxs("div", { style: {
|
|
109
|
+
...hint,
|
|
110
|
+
display: "flex",
|
|
111
|
+
flexDirection: "column",
|
|
112
|
+
gap: "2px",
|
|
113
|
+
margin: 0,
|
|
114
|
+
}, children: [_jsxs("span", { children: [_jsx("span", { style: versionLabel, children: t("dsh") }), state.dshVersion ? (` ${state.dshVersion}`) : (_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 ? (` ${state.orchestratorVersion}`) : (_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") }) })] })) : null] }));
|
|
79
115
|
}
|
|
@@ -85,6 +85,9 @@ export interface WorkspaceView {
|
|
|
85
85
|
export interface CardSnapshot {
|
|
86
86
|
version: string;
|
|
87
87
|
commit: string;
|
|
88
|
+
dshVersion: string;
|
|
89
|
+
orchestratorVersion: string;
|
|
90
|
+
versionState: "ok" | "minor-mismatch" | "major-mismatch";
|
|
88
91
|
projectsRoot: string;
|
|
89
92
|
workspaces: readonly WorkspaceView[];
|
|
90
93
|
containers: readonly ContainerView[];
|
|
@@ -32,6 +32,9 @@ export interface CardState {
|
|
|
32
32
|
notice: string;
|
|
33
33
|
version: string;
|
|
34
34
|
commit: string;
|
|
35
|
+
dshVersion: string;
|
|
36
|
+
orchestratorVersion: string;
|
|
37
|
+
versionState: "ok" | "minor-mismatch" | "major-mismatch";
|
|
35
38
|
defaultImage: string;
|
|
36
39
|
defaultImageDraft: string;
|
|
37
40
|
socketsRoot: string;
|
|
@@ -7,6 +7,9 @@ export const CONTAINER_NS = "podman";
|
|
|
7
7
|
const EMPTY_SNAPSHOT = {
|
|
8
8
|
version: "",
|
|
9
9
|
commit: "",
|
|
10
|
+
dshVersion: "",
|
|
11
|
+
orchestratorVersion: "",
|
|
12
|
+
versionState: "ok",
|
|
10
13
|
projectsRoot: "",
|
|
11
14
|
workspaces: [],
|
|
12
15
|
containers: [],
|
|
@@ -49,6 +52,9 @@ export class ContainerCardController {
|
|
|
49
52
|
notice: this.notice,
|
|
50
53
|
version: this.snapshot.version,
|
|
51
54
|
commit: this.snapshot.commit,
|
|
55
|
+
dshVersion: this.snapshot.dshVersion,
|
|
56
|
+
orchestratorVersion: this.snapshot.orchestratorVersion,
|
|
57
|
+
versionState: this.snapshot.versionState,
|
|
52
58
|
defaultImage: value?.defaultImage ?? "",
|
|
53
59
|
defaultImageDraft: this.draft("defaultImage", value?.defaultImage ?? ""),
|
|
54
60
|
socketsRoot: value?.socketsRoot ?? "",
|
package/dist/client/index.js
CHANGED
|
@@ -3010,6 +3010,7 @@ function WorkspaceSection(props) {
|
|
|
3010
3010
|
var import_dsh_client_ui_primitives11 = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
3011
3011
|
var import_react11 = require("react");
|
|
3012
3012
|
var import_jsx_runtime12 = require("react/jsx-runtime");
|
|
3013
|
+
var versionLabel = { fontWeight: 600 };
|
|
3013
3014
|
function ContainerCard(props) {
|
|
3014
3015
|
const { t } = props;
|
|
3015
3016
|
const state = props.useContainerCard((snapshot) => snapshot);
|
|
@@ -3021,7 +3022,21 @@ function ContainerCard(props) {
|
|
|
3021
3022
|
const [packages, setPackages] = (0, import_react11.useState)([]);
|
|
3022
3023
|
const [defaultOpen, setDefaultOpen] = (0, import_react11.useState)(false);
|
|
3023
3024
|
const [defaultImage, setDefaultImage] = (0, import_react11.useState)(state.defaultImage);
|
|
3025
|
+
const [copyState, setCopyState] = (0, import_react11.useState)(
|
|
3026
|
+
"idle"
|
|
3027
|
+
);
|
|
3028
|
+
const copyResetTimer = (0, import_react11.useRef)();
|
|
3024
3029
|
const projectsRootId = (0, import_react11.useId)();
|
|
3030
|
+
const copyVersions = async () => {
|
|
3031
|
+
const lines = [
|
|
3032
|
+
`${t("dsh")} ${state.dshVersion || t("versionUnknown")}`,
|
|
3033
|
+
`${t("cardTitle")} ${state.version}${state.commit ? ` \xB7 ${state.commit}` : ""}`,
|
|
3034
|
+
`${t("orchestrator")} ${state.orchestratorVersion || t("versionUnknown")}`
|
|
3035
|
+
];
|
|
3036
|
+
setCopyState(await (0, import_dsh_client_ui_primitives11.writeClipboard)(lines.join("\n")) ? "copied" : "failed");
|
|
3037
|
+
if (copyResetTimer.current !== void 0) clearTimeout(copyResetTimer.current);
|
|
3038
|
+
copyResetTimer.current = setTimeout(() => setCopyState("idle"), 1500);
|
|
3039
|
+
};
|
|
3025
3040
|
(0, import_react11.useEffect)(() => {
|
|
3026
3041
|
props.reload();
|
|
3027
3042
|
}, []);
|
|
@@ -3054,11 +3069,7 @@ function ContainerCard(props) {
|
|
|
3054
3069
|
onClick: () => setOpen(!open),
|
|
3055
3070
|
children: [
|
|
3056
3071
|
/* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("span", { style: cardHeadTextStyle, children: [
|
|
3057
|
-
/* @__PURE__ */ (0, import_jsx_runtime12.
|
|
3058
|
-
t("cardTitle"),
|
|
3059
|
-
state.version ? ` ${state.version}` : "",
|
|
3060
|
-
state.commit ? ` \xB7 ${state.commit}` : ""
|
|
3061
|
-
] }),
|
|
3072
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { style: cardNameStyle, children: t("cardTitle") }),
|
|
3062
3073
|
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { style: cardDescriptionStyle, children: t("cardDescription") })
|
|
3063
3074
|
] }),
|
|
3064
3075
|
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(CardChevron, { open })
|
|
@@ -3071,6 +3082,16 @@ function ContainerCard(props) {
|
|
|
3071
3082
|
": ",
|
|
3072
3083
|
state.notice
|
|
3073
3084
|
] }),
|
|
3085
|
+
state.versionState === "ok" ? null : /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
|
|
3086
|
+
"div",
|
|
3087
|
+
{
|
|
3088
|
+
style: state.versionState === "major-mismatch" ? { ...banner, borderColor: "var(--dsw-alias-state-error-primary)" } : banner,
|
|
3089
|
+
role: state.versionState === "major-mismatch" ? "alert" : "status",
|
|
3090
|
+
children: t(
|
|
3091
|
+
state.versionState === "major-mismatch" ? "versionMismatchMajor" : "versionMismatchMinor"
|
|
3092
|
+
)
|
|
3093
|
+
}
|
|
3094
|
+
),
|
|
3074
3095
|
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { style: sectionTitle, children: t("workspacesTitle") }),
|
|
3075
3096
|
state.workspaces.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("p", { style: hint, children: t("none") }) : state.workspaces.map((workspace) => /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
|
|
3076
3097
|
WorkspaceSection,
|
|
@@ -3330,6 +3351,74 @@ function ContainerCard(props) {
|
|
|
3330
3351
|
onDiscard: props.discardSocketsRoot
|
|
3331
3352
|
}
|
|
3332
3353
|
),
|
|
3354
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
|
|
3355
|
+
"div",
|
|
3356
|
+
{
|
|
3357
|
+
style: {
|
|
3358
|
+
display: "flex",
|
|
3359
|
+
flexWrap: "wrap",
|
|
3360
|
+
alignItems: "center",
|
|
3361
|
+
gap: "8px"
|
|
3362
|
+
},
|
|
3363
|
+
children: [
|
|
3364
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { style: { ...sectionTitle, flex: 1 }, children: t("versionsTitle") }),
|
|
3365
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
|
|
3366
|
+
import_dsh_client_ui_primitives11.Button,
|
|
3367
|
+
{
|
|
3368
|
+
variant: "outline",
|
|
3369
|
+
size: "sm",
|
|
3370
|
+
onClick: () => void copyVersions(),
|
|
3371
|
+
children: copyState === "copied" ? t("copied") : copyState === "failed" ? t("copyFailed") : t("copyVersions")
|
|
3372
|
+
}
|
|
3373
|
+
)
|
|
3374
|
+
]
|
|
3375
|
+
}
|
|
3376
|
+
),
|
|
3377
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
|
|
3378
|
+
"div",
|
|
3379
|
+
{
|
|
3380
|
+
style: {
|
|
3381
|
+
...hint,
|
|
3382
|
+
display: "flex",
|
|
3383
|
+
flexDirection: "column",
|
|
3384
|
+
gap: "2px",
|
|
3385
|
+
margin: 0
|
|
3386
|
+
},
|
|
3387
|
+
children: [
|
|
3388
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("span", { children: [
|
|
3389
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { style: versionLabel, children: t("dsh") }),
|
|
3390
|
+
state.dshVersion ? ` ${state.dshVersion}` : /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_jsx_runtime12.Fragment, { children: [
|
|
3391
|
+
" ",
|
|
3392
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
|
|
3393
|
+
"span",
|
|
3394
|
+
{
|
|
3395
|
+
style: { color: "var(--dsw-alias-state-error-primary)" },
|
|
3396
|
+
children: t("versionUnknown")
|
|
3397
|
+
}
|
|
3398
|
+
)
|
|
3399
|
+
] })
|
|
3400
|
+
] }),
|
|
3401
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("span", { children: [
|
|
3402
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { style: versionLabel, children: t("cardTitle") }),
|
|
3403
|
+
state.version ? ` ${state.version}` : "",
|
|
3404
|
+
state.commit ? ` \xB7 ${state.commit}` : ""
|
|
3405
|
+
] }),
|
|
3406
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("span", { children: [
|
|
3407
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { style: versionLabel, children: t("orchestrator") }),
|
|
3408
|
+
state.orchestratorVersion ? ` ${state.orchestratorVersion}` : /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_jsx_runtime12.Fragment, { children: [
|
|
3409
|
+
" ",
|
|
3410
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
|
|
3411
|
+
"span",
|
|
3412
|
+
{
|
|
3413
|
+
style: { color: "var(--dsw-alias-state-error-primary)" },
|
|
3414
|
+
children: t("versionUnknown")
|
|
3415
|
+
}
|
|
3416
|
+
)
|
|
3417
|
+
] })
|
|
3418
|
+
] })
|
|
3419
|
+
]
|
|
3420
|
+
}
|
|
3421
|
+
),
|
|
3333
3422
|
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { style: footerRow, children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
|
|
3334
3423
|
import_dsh_client_ui_primitives11.Button,
|
|
3335
3424
|
{
|
|
@@ -3781,6 +3870,9 @@ var CONTAINER_NS = "podman";
|
|
|
3781
3870
|
var EMPTY_SNAPSHOT = {
|
|
3782
3871
|
version: "",
|
|
3783
3872
|
commit: "",
|
|
3873
|
+
dshVersion: "",
|
|
3874
|
+
orchestratorVersion: "",
|
|
3875
|
+
versionState: "ok",
|
|
3784
3876
|
projectsRoot: "",
|
|
3785
3877
|
workspaces: [],
|
|
3786
3878
|
containers: [],
|
|
@@ -3819,6 +3911,9 @@ var ContainerCardController = class {
|
|
|
3819
3911
|
notice: this.notice,
|
|
3820
3912
|
version: this.snapshot.version,
|
|
3821
3913
|
commit: this.snapshot.commit,
|
|
3914
|
+
dshVersion: this.snapshot.dshVersion,
|
|
3915
|
+
orchestratorVersion: this.snapshot.orchestratorVersion,
|
|
3916
|
+
versionState: this.snapshot.versionState,
|
|
3822
3917
|
defaultImage: value?.defaultImage ?? "",
|
|
3823
3918
|
defaultImageDraft: this.draft("defaultImage", value?.defaultImage ?? ""),
|
|
3824
3919
|
socketsRoot: value?.socketsRoot ?? "",
|
|
@@ -3983,6 +4078,15 @@ var NS = "podman";
|
|
|
3983
4078
|
var en = {
|
|
3984
4079
|
cardTitle: "dsh-podman",
|
|
3985
4080
|
cardDescription: "Manage the guest containers and their images.",
|
|
4081
|
+
versionMismatchMajor: "This plugin's major version differs from the orchestrator's; update the dsh image or the orchestrator image so both match.",
|
|
4082
|
+
versionMismatchMinor: "This plugin and the orchestrator have compatible but different versions; consider updating both images.",
|
|
4083
|
+
versionsTitle: "Versions",
|
|
4084
|
+
orchestrator: "dsh-podman-orchestrator",
|
|
4085
|
+
versionUnknown: "unknown",
|
|
4086
|
+
dsh: "dsh",
|
|
4087
|
+
copyVersions: "Copy versions",
|
|
4088
|
+
copied: "Copied",
|
|
4089
|
+
copyFailed: "Copy failed",
|
|
3986
4090
|
reload: "Reload this view",
|
|
3987
4091
|
reloading: "Reloading\u2026",
|
|
3988
4092
|
notice: "Notice",
|
|
@@ -4166,6 +4270,15 @@ var en = {
|
|
|
4166
4270
|
var zh = {
|
|
4167
4271
|
cardTitle: "dsh-podman",
|
|
4168
4272
|
cardDescription: "\u7BA1\u7406\u5DE5\u4F5C\u533A\u5BB9\u5668\u53CA\u5176\u955C\u50CF\u3002",
|
|
4273
|
+
versionMismatchMajor: "\u6B64\u63D2\u4EF6\u4E0E orchestrator \u7684\u4E3B\u7248\u672C\u4E0D\u540C\uFF1B\u8BF7\u66F4\u65B0 dsh \u955C\u50CF\u6216 orchestrator \u955C\u50CF\uFF0C\u4F7F\u4E24\u8005\u4E00\u81F4\u3002",
|
|
4274
|
+
versionMismatchMinor: "\u6B64\u63D2\u4EF6\u4E0E orchestrator \u7684\u7248\u672C\u4E0D\u540C\u4F46\u517C\u5BB9\uFF1B\u5EFA\u8BAE\u540C\u65F6\u66F4\u65B0\u4E24\u4E2A\u955C\u50CF\u3002",
|
|
4275
|
+
versionsTitle: "\u7248\u672C",
|
|
4276
|
+
orchestrator: "dsh-podman-orchestrator",
|
|
4277
|
+
versionUnknown: "\u672A\u77E5",
|
|
4278
|
+
dsh: "dsh",
|
|
4279
|
+
copyVersions: "\u590D\u5236\u7248\u672C",
|
|
4280
|
+
copied: "\u5DF2\u590D\u5236",
|
|
4281
|
+
copyFailed: "\u590D\u5236\u5931\u8D25",
|
|
4169
4282
|
reload: "\u5237\u65B0\u6B64\u89C6\u56FE",
|
|
4170
4283
|
reloading: "\u5237\u65B0\u4E2D\u2026",
|
|
4171
4284
|
notice: "\u63D0\u793A",
|
package/dist/client/locales.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export declare const NS = "podman";
|
|
2
|
-
export type ContainerPluginKey = "cardTitle" | "cardDescription" | "reload" | "reloading" | "notice" | "configTitle" | "defaultImage" | "socketsRoot" | "projectsRoot" | "defaultImageHint" | "save" | "discard" | "workspacesTitle" | "noContainers" | "createContainer" | "containersTitle" | "containerName" | "workspace" | "image" | "status" | "created" | "projects" | "remove" | "removePod" | "confirmRemovePod" | "recreate" | "recreateWithImage" | "imagesTitle" | "imageId" | "baseImage" | "imageTag" | "builtAt" | "packages" | "volumesTitle" | "createVolume" | "removeVolume" | "volumeName" | "secretsTitle" | "createSecret" | "secretName" | "secretCharset" | "selectImage" | "setSecret" | "removeSecret" | "removeImage" | "rebuildImage" | "rebuildAllImages" | "buildImage" | "build" | "buildImageTitle" | "pullImage" | "setDefaultImage" | "setDefaultImageTitle" | "baseImagesTitle" | "customImagesTitle" | "primitive" | "packageManager" | "parent" | "cancel" | "removeTag" | "env" | "secretEnv" | "envTitle" | "envKey" | "envValue" | "addEnv" | "removeEnv" | "secretLength" | "containerSecretsTitle" | "attachSecret" | "secretEnvName" | "detachSecret" | "none" | "unavailable" | "busy" | "confirmTitle" | "confirm" | "confirmRemoveContainer" | "confirmRecreate" | "confirmRemoveImage" | "confirmRebuildImage" | "confirmRebuildAllImages" | "confirmRemoveVolume" | "confirmSetSecret" | "confirmRemoveSecret" | "confirmDetachSecret" | "mountsTitle" | "addMount" | "mountKind" | "mountProject" | "mountProjectPath" | "browse" | "browseTitle" | "browseSelect" | "browseLoading" | "browseError" | "browseUnavailable" | "browseHidden" | "browseTruncated" | "browseOutsideRoot" | "mountDestination" | "pathsTitle" | "noPaths" | "editPaths" | "addPath" | "add" | "moveUp" | "moveDown" | "apply" | "dragToReorder" | "remountWaiting" | "remountApprove" | "remountReject" | "mountVolume" | "mountSecret" | "mountMode" | "readOnly" | "readWrite" | "remountReadOnly" | "remountReadWrite" | "confirmRemountReadOnly" | "confirmRemountReadWrite" | "confirmRemoveMount" | "cachesTitle" | "noCaches" | "cacheUsage" | "cacheKeepLatest" | "confirmCacheKeepLatest" | "cacheRemoveAll" | "confirmCacheRemoveAll" | "addContainer" | "invalidContainerName" | "containerNameTaken" | "invalidName" | "invalidImageId" | "terminalSignal" | "terminalExitCode" | "terminalRunning" | "terminalFailed" | "terminalDone" | "terminalNoOutput" | "terminalCollapseAria" | "terminalExpandAria" | "terminalExpandRest" | "toolTitle_container_bash" | "toolTitle_container_exec" | "toolTitle_container_read" | "toolTitle_container_write" | "toolTitle_container_edit" | "toolTitle_container_glob" | "toolTitle_container_grep" | "toolTitle_container_list" | "toolTitle_container_start" | "toolTitle_container_recreate" | "toolTitle_container_remove" | "toolTitle_container_mount_list" | "toolTitle_container_mount_add" | "toolTitle_container_mount_remove" | "toolTitle_container_mount_update" | "toolTitle_container_path_set" | "toolTitle_container_path_add" | "toolTitle_container_path_remove" | "toolTitle_container_secret_add" | "toolTitle_container_secret_remove" | "toolTitle_image_list" | "toolTitle_image_get" | "toolTitle_image_build" | "toolTitle_image_rebuild" | "toolTitle_image_rebuild_all" | "toolTitle_image_remove" | "toolTitle_volume_list" | "toolTitle_volume_create" | "toolTitle_volume_remove" | "toolTitle_secret_list" | "toolTitle_secret_create" | "toolTitle_secret_remove" | "toolTitle_daemon_start" | "toolTitle_daemon_list" | "toolTitle_daemon_logs" | "toolTitle_daemon_restart" | "toolTitle_daemon_stop";
|
|
2
|
+
export type ContainerPluginKey = "cardTitle" | "cardDescription" | "versionMismatchMajor" | "versionMismatchMinor" | "versionsTitle" | "orchestrator" | "versionUnknown" | "dsh" | "copyVersions" | "copied" | "copyFailed" | "reload" | "reloading" | "notice" | "configTitle" | "defaultImage" | "socketsRoot" | "projectsRoot" | "defaultImageHint" | "save" | "discard" | "workspacesTitle" | "noContainers" | "createContainer" | "containersTitle" | "containerName" | "workspace" | "image" | "status" | "created" | "projects" | "remove" | "removePod" | "confirmRemovePod" | "recreate" | "recreateWithImage" | "imagesTitle" | "imageId" | "baseImage" | "imageTag" | "builtAt" | "packages" | "volumesTitle" | "createVolume" | "removeVolume" | "volumeName" | "secretsTitle" | "createSecret" | "secretName" | "secretCharset" | "selectImage" | "setSecret" | "removeSecret" | "removeImage" | "rebuildImage" | "rebuildAllImages" | "buildImage" | "build" | "buildImageTitle" | "pullImage" | "setDefaultImage" | "setDefaultImageTitle" | "baseImagesTitle" | "customImagesTitle" | "primitive" | "packageManager" | "parent" | "cancel" | "removeTag" | "env" | "secretEnv" | "envTitle" | "envKey" | "envValue" | "addEnv" | "removeEnv" | "secretLength" | "containerSecretsTitle" | "attachSecret" | "secretEnvName" | "detachSecret" | "none" | "unavailable" | "busy" | "confirmTitle" | "confirm" | "confirmRemoveContainer" | "confirmRecreate" | "confirmRemoveImage" | "confirmRebuildImage" | "confirmRebuildAllImages" | "confirmRemoveVolume" | "confirmSetSecret" | "confirmRemoveSecret" | "confirmDetachSecret" | "mountsTitle" | "addMount" | "mountKind" | "mountProject" | "mountProjectPath" | "browse" | "browseTitle" | "browseSelect" | "browseLoading" | "browseError" | "browseUnavailable" | "browseHidden" | "browseTruncated" | "browseOutsideRoot" | "mountDestination" | "pathsTitle" | "noPaths" | "editPaths" | "addPath" | "add" | "moveUp" | "moveDown" | "apply" | "dragToReorder" | "remountWaiting" | "remountApprove" | "remountReject" | "mountVolume" | "mountSecret" | "mountMode" | "readOnly" | "readWrite" | "remountReadOnly" | "remountReadWrite" | "confirmRemountReadOnly" | "confirmRemountReadWrite" | "confirmRemoveMount" | "cachesTitle" | "noCaches" | "cacheUsage" | "cacheKeepLatest" | "confirmCacheKeepLatest" | "cacheRemoveAll" | "confirmCacheRemoveAll" | "addContainer" | "invalidContainerName" | "containerNameTaken" | "invalidName" | "invalidImageId" | "terminalSignal" | "terminalExitCode" | "terminalRunning" | "terminalFailed" | "terminalDone" | "terminalNoOutput" | "terminalCollapseAria" | "terminalExpandAria" | "terminalExpandRest" | "toolTitle_container_bash" | "toolTitle_container_exec" | "toolTitle_container_read" | "toolTitle_container_write" | "toolTitle_container_edit" | "toolTitle_container_glob" | "toolTitle_container_grep" | "toolTitle_container_list" | "toolTitle_container_start" | "toolTitle_container_recreate" | "toolTitle_container_remove" | "toolTitle_container_mount_list" | "toolTitle_container_mount_add" | "toolTitle_container_mount_remove" | "toolTitle_container_mount_update" | "toolTitle_container_path_set" | "toolTitle_container_path_add" | "toolTitle_container_path_remove" | "toolTitle_container_secret_add" | "toolTitle_container_secret_remove" | "toolTitle_image_list" | "toolTitle_image_get" | "toolTitle_image_build" | "toolTitle_image_rebuild" | "toolTitle_image_rebuild_all" | "toolTitle_image_remove" | "toolTitle_volume_list" | "toolTitle_volume_create" | "toolTitle_volume_remove" | "toolTitle_secret_list" | "toolTitle_secret_create" | "toolTitle_secret_remove" | "toolTitle_daemon_start" | "toolTitle_daemon_list" | "toolTitle_daemon_logs" | "toolTitle_daemon_restart" | "toolTitle_daemon_stop";
|
|
3
3
|
export declare const en: Record<ContainerPluginKey, string>;
|
|
4
4
|
export declare const zh: Record<ContainerPluginKey, string>;
|
|
5
5
|
declare module "@deepseek-ai/dsh-client-ui-slots" {
|
package/dist/client/locales.js
CHANGED
|
@@ -5,6 +5,15 @@ export const NS = "podman";
|
|
|
5
5
|
export const en = {
|
|
6
6
|
cardTitle: "dsh-podman",
|
|
7
7
|
cardDescription: "Manage the guest containers and their images.",
|
|
8
|
+
versionMismatchMajor: "This plugin's major version differs from the orchestrator's; update the dsh image or the orchestrator image so both match.",
|
|
9
|
+
versionMismatchMinor: "This plugin and the orchestrator have compatible but different versions; consider updating both images.",
|
|
10
|
+
versionsTitle: "Versions",
|
|
11
|
+
orchestrator: "dsh-podman-orchestrator",
|
|
12
|
+
versionUnknown: "unknown",
|
|
13
|
+
dsh: "dsh",
|
|
14
|
+
copyVersions: "Copy versions",
|
|
15
|
+
copied: "Copied",
|
|
16
|
+
copyFailed: "Copy failed",
|
|
8
17
|
reload: "Reload this view",
|
|
9
18
|
reloading: "Reloading…",
|
|
10
19
|
notice: "Notice",
|
|
@@ -188,6 +197,15 @@ export const en = {
|
|
|
188
197
|
export const zh = {
|
|
189
198
|
cardTitle: "dsh-podman",
|
|
190
199
|
cardDescription: "管理工作区容器及其镜像。",
|
|
200
|
+
versionMismatchMajor: "此插件与 orchestrator 的主版本不同;请更新 dsh 镜像或 orchestrator 镜像,使两者一致。",
|
|
201
|
+
versionMismatchMinor: "此插件与 orchestrator 的版本不同但兼容;建议同时更新两个镜像。",
|
|
202
|
+
versionsTitle: "版本",
|
|
203
|
+
orchestrator: "dsh-podman-orchestrator",
|
|
204
|
+
versionUnknown: "未知",
|
|
205
|
+
dsh: "dsh",
|
|
206
|
+
copyVersions: "复制版本",
|
|
207
|
+
copied: "已复制",
|
|
208
|
+
copyFailed: "复制失败",
|
|
191
209
|
reload: "刷新此视图",
|
|
192
210
|
reloading: "刷新中…",
|
|
193
211
|
notice: "提示",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function dshVersion(): string;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Elouan Martinet <exa@elou.world>
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: MIT
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
// The harness version this plugin runs inside. Every `@deepseek-ai/dsh-*`
|
|
8
|
+
// package carries the release version, and the dsh app package is linked into
|
|
9
|
+
// the profile's healed node_modules, so it is resolvable from the plugin even
|
|
10
|
+
// though it is not a dependency of it. "" when it cannot be resolved, which the
|
|
11
|
+
// card renders as unknown.
|
|
12
|
+
let cached;
|
|
13
|
+
export function dshVersion() {
|
|
14
|
+
cached ??= resolveDshVersion() ?? "";
|
|
15
|
+
return cached;
|
|
16
|
+
}
|
|
17
|
+
// resolveDshVersion walks the module search paths the way the harness resolves
|
|
18
|
+
// its own bundles, so a package that does not export ./package.json still
|
|
19
|
+
// resolves.
|
|
20
|
+
function resolveDshVersion() {
|
|
21
|
+
let searchPaths;
|
|
22
|
+
try {
|
|
23
|
+
searchPaths = createRequire(import.meta.url).resolve.paths("@deepseek-ai/dsh") ?? [];
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
for (const searchPath of searchPaths) {
|
|
29
|
+
if (searchPath === null)
|
|
30
|
+
continue;
|
|
31
|
+
const manifest = join(searchPath, "@deepseek-ai/dsh", "package.json");
|
|
32
|
+
if (!existsSync(manifest))
|
|
33
|
+
continue;
|
|
34
|
+
try {
|
|
35
|
+
const parsed = JSON.parse(readFileSync(manifest, "utf8"));
|
|
36
|
+
if (typeof parsed.version === "string" && parsed.version !== "") {
|
|
37
|
+
return parsed.version;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// A malformed manifest is not a version source; keep looking.
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const VERSION = "0.2.0-rc.
|
|
2
|
-
export declare const GIT_COMMIT = "
|
|
1
|
+
export declare const VERSION = "0.2.0-rc.4";
|
|
2
|
+
export declare const GIT_COMMIT = "fce200f";
|
|
@@ -7,6 +7,7 @@ package dshctl.v1;
|
|
|
7
7
|
option go_package = "github.com/Exagone313/dsh-podman/internal/genproto/dshctl/v1;dshctlv1";
|
|
8
8
|
|
|
9
9
|
service OrchestratorControl {
|
|
10
|
+
rpc GetVersion(GetVersionRequest) returns (GetVersionResponse);
|
|
10
11
|
rpc ListProjects(ListProjectsRequest) returns (ListProjectsResponse);
|
|
11
12
|
rpc CreateWorkspace(CreateWorkspaceRequest) returns (Workspace);
|
|
12
13
|
rpc DescribeWorkspace(DescribeWorkspaceRequest) returns (Workspace);
|
|
@@ -41,6 +42,8 @@ service OrchestratorControl {
|
|
|
41
42
|
rpc RemoveContainerSecret(RemoveContainerSecretRequest) returns (Container);
|
|
42
43
|
}
|
|
43
44
|
|
|
45
|
+
message GetVersionRequest {}
|
|
46
|
+
message GetVersionResponse { string version = 1; string commit = 2; }
|
|
44
47
|
message ListProjectsRequest {}
|
|
45
48
|
message ListProjectsResponse { repeated Project projects = 1; }
|
|
46
49
|
message Project { string name = 1; string host_path = 2; }
|
|
@@ -18,6 +18,8 @@ test("refresh on install publishes containers, images and workspaces", async ()
|
|
|
18
18
|
setConfig: () => { },
|
|
19
19
|
async control(method, request) {
|
|
20
20
|
calls.push([method, request]);
|
|
21
|
+
if (method === "getVersion")
|
|
22
|
+
return { version: "0.2.0", commit: "abc" };
|
|
21
23
|
if (method === "listContainers") {
|
|
22
24
|
return { containers: [{ containerName: "c1", workspaceSlug: "w1" }] };
|
|
23
25
|
}
|
|
@@ -38,7 +40,7 @@ test("refresh on install publishes containers, images and workspaces", async ()
|
|
|
38
40
|
};
|
|
39
41
|
await installCardCommandDriver(fakeContext(scope), resolver);
|
|
40
42
|
await scope.update({}); // settle the queued async refresh
|
|
41
|
-
assert.deepEqual(calls.map(([method]) => method), ["listContainers", "listImages", "listWorkspaces", "listVolumes", "listSecrets", "listCaches"]);
|
|
43
|
+
assert.deepEqual(calls.map(([method]) => method), ["getVersion", "listContainers", "listImages", "listWorkspaces", "listVolumes", "listSecrets", "listCaches"]);
|
|
42
44
|
assert.equal(scope.value.containers.length, 1);
|
|
43
45
|
assert.equal(scope.value.images.length, 1);
|
|
44
46
|
const workspaces = scope.value.workspaces;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Elouan Martinet <exa@elou.world>
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: MIT
|
|
4
|
+
// core returns the leading x.y.z of a version string, or "" when there is
|
|
5
|
+
// none. A release build is exactly the tag; a build from a later commit carries
|
|
6
|
+
// a git-describe suffix (`1.2.3-4-gabc123`), and a local build may be `dev`.
|
|
7
|
+
export function core(value) {
|
|
8
|
+
const match = /^(\d+)\.(\d+)\.(\d+)/.exec(String(value ?? ""));
|
|
9
|
+
return match === null ? "" : `${match[1]}.${match[2]}.${match[3]}`;
|
|
10
|
+
}
|
|
11
|
+
// major returns the major component of a version's core, or -1 when the value
|
|
12
|
+
// has no parseable core.
|
|
13
|
+
export function major(value) {
|
|
14
|
+
const parsed = core(value);
|
|
15
|
+
return parsed === "" ? -1 : Number(parsed.slice(0, parsed.indexOf(".")));
|
|
16
|
+
}
|
|
17
|
+
// versionState mirrors the orchestrator's own compatibility rule, so the card
|
|
18
|
+
// reports exactly what the control plane enforces.
|
|
19
|
+
export function versionState(plugin, orchestrator) {
|
|
20
|
+
const orchestratorCore = core(orchestrator);
|
|
21
|
+
if (orchestratorCore === "")
|
|
22
|
+
return "ok";
|
|
23
|
+
const pluginCore = core(plugin);
|
|
24
|
+
if (pluginCore === "")
|
|
25
|
+
return "major-mismatch";
|
|
26
|
+
if (major(plugin) !== major(orchestrator))
|
|
27
|
+
return "major-mismatch";
|
|
28
|
+
return pluginCore === orchestratorCore ? "ok" : "minor-mismatch";
|
|
29
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Elouan Martinet <exa@elou.world>
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: MIT
|
|
4
|
+
import test from "node:test";
|
|
5
|
+
import assert from "node:assert/strict";
|
|
6
|
+
import { core, major, versionState } from "./version-compat.js";
|
|
7
|
+
test("core keeps the leading x.y.z", () => {
|
|
8
|
+
assert.equal(core("1.2.3"), "1.2.3");
|
|
9
|
+
assert.equal(core("0.2.0-rc.3"), "0.2.0");
|
|
10
|
+
assert.equal(core("1.2.3-4-gabc123"), "1.2.3");
|
|
11
|
+
assert.equal(core("dev"), "");
|
|
12
|
+
assert.equal(core(""), "");
|
|
13
|
+
});
|
|
14
|
+
test("major reports the leading component", () => {
|
|
15
|
+
assert.equal(major("1.2.3"), 1);
|
|
16
|
+
assert.equal(major("0.2.0-rc.3"), 0);
|
|
17
|
+
assert.equal(major("dev"), -1);
|
|
18
|
+
});
|
|
19
|
+
test("versionState mirrors the orchestrator's compatibility rule", () => {
|
|
20
|
+
assert.equal(versionState("1.2.3", "1.2.3"), "ok");
|
|
21
|
+
assert.equal(versionState("1.2.3-4-gabc", "1.2.3"), "ok");
|
|
22
|
+
assert.equal(versionState("1.3.0", "1.2.3"), "minor-mismatch");
|
|
23
|
+
assert.equal(versionState("1.2.4", "1.2.3"), "minor-mismatch");
|
|
24
|
+
assert.equal(versionState("2.0.0", "1.9.0"), "major-mismatch");
|
|
25
|
+
assert.equal(versionState("1.9.0", "2.0.0"), "major-mismatch");
|
|
26
|
+
assert.equal(versionState("1.2.3", "dev"), "ok", "an orchestrator with no comparable version accepts any plugin");
|
|
27
|
+
});
|
|
@@ -37,4 +37,4 @@ export declare function containerNotFound(container: string, slug?: string): Err
|
|
|
37
37
|
export declare function normalizeToolError(error: unknown): Error;
|
|
38
38
|
export declare function workspaceSlug(id: unknown): string;
|
|
39
39
|
export declare function containerRowFor(containers: any[], slug: string, container: string): any | undefined;
|
|
40
|
-
export declare function metadata(token: string): grpc.Metadata;
|
|
40
|
+
export declare function metadata(token: string, pluginVersion?: string): grpc.Metadata;
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// SPDX-License-Identifier: MIT
|
|
4
4
|
import { guestClient, controlClient, grpc, unary, } from "./grpc/runtime-client.js";
|
|
5
5
|
import { isAbsolute, join } from "node:path";
|
|
6
|
+
import { VERSION } from "./generated/version.js";
|
|
6
7
|
export class WorkspaceResolver {
|
|
7
8
|
config;
|
|
8
9
|
registry;
|
|
@@ -162,7 +163,7 @@ export class WorkspaceResolver {
|
|
|
162
163
|
}
|
|
163
164
|
async create(slug, projectName) {
|
|
164
165
|
const control = controlClient(join(this.config.socketsRoot, "orchestrator.sock"));
|
|
165
|
-
const controlMetadata = metadata(this.config.controlToken);
|
|
166
|
+
const controlMetadata = metadata(this.config.controlToken, VERSION);
|
|
166
167
|
let workspace;
|
|
167
168
|
try {
|
|
168
169
|
workspace = await unary(control, "describeWorkspace", {
|
|
@@ -187,7 +188,7 @@ export class WorkspaceResolver {
|
|
|
187
188
|
};
|
|
188
189
|
}
|
|
189
190
|
async control(method, request) {
|
|
190
|
-
return unary(controlClient(join(this.config.socketsRoot, "orchestrator.sock")), method, request, metadata(this.config.controlToken));
|
|
191
|
+
return unary(controlClient(join(this.config.socketsRoot, "orchestrator.sock")), method, request, metadata(this.config.controlToken, VERSION));
|
|
191
192
|
}
|
|
192
193
|
}
|
|
193
194
|
// notFoundError builds the harness's missing-path error (`FsErrorCode`
|
|
@@ -244,11 +245,17 @@ const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{
|
|
|
244
245
|
export function containerRowFor(containers, slug, container) {
|
|
245
246
|
return containers.find((row) => row.workspaceSlug === slug && row.containerName === container);
|
|
246
247
|
}
|
|
247
|
-
|
|
248
|
+
// metadata builds the call metadata. The plugin version is sent on control
|
|
249
|
+
// calls so the orchestrator can refuse an incompatible plugin; guest calls omit
|
|
250
|
+
// it, since the guest agent does not check it.
|
|
251
|
+
export function metadata(token, pluginVersion) {
|
|
248
252
|
const result = new grpc.Metadata();
|
|
249
253
|
if (token !== "") {
|
|
250
254
|
result.set("authorization", `bearer ${token}`);
|
|
251
255
|
}
|
|
256
|
+
if (pluginVersion !== undefined && pluginVersion !== "") {
|
|
257
|
+
result.set("x-dsh-podman-plugin-version", pluginVersion);
|
|
258
|
+
}
|
|
252
259
|
return result;
|
|
253
260
|
}
|
|
254
261
|
// defaultCwdOf returns the session working directory as a usable container
|
|
@@ -11,6 +11,7 @@ import { tmpdir } from "node:os";
|
|
|
11
11
|
import { mkdirSync } from "node:fs";
|
|
12
12
|
import { controlClient, guestClient } from "./grpc/runtime-client.js";
|
|
13
13
|
import { workspaceSlug, metadata, WorkspaceResolver, containerRowFor, containerNotFound, normalizeToolError, } from "./workspace-binding.js";
|
|
14
|
+
import { VERSION } from "./generated/version.js";
|
|
14
15
|
const SLUG = "2c573001-4171-4900-904b-12a5cc02737a";
|
|
15
16
|
const SLUG_SUB = "3d684112-5282-4a11-a15c-23b6dd13848b";
|
|
16
17
|
test("workspaceSlug accepts a UUID workspace id", () => {
|
|
@@ -44,6 +45,12 @@ test("metadata omits the header when the token is empty", () => {
|
|
|
44
45
|
assert.ok(result instanceof grpc.Metadata);
|
|
45
46
|
assert.equal(result.get("authorization").length, 0);
|
|
46
47
|
});
|
|
48
|
+
test("metadata carries the plugin version only when one is given", () => {
|
|
49
|
+
const withVersion = metadata("token-1", "1.2.3");
|
|
50
|
+
assert.equal(withVersion.get("x-dsh-podman-plugin-version")[0], "1.2.3");
|
|
51
|
+
// Guest calls pass no version, and the guest agent does not check it.
|
|
52
|
+
assert.equal(metadata("token-1").get("x-dsh-podman-plugin-version").length, 0);
|
|
53
|
+
});
|
|
47
54
|
test("normalizeToolError strips the grpc prefix and names the code", () => {
|
|
48
55
|
const prefixed = Object.assign(new Error("5 NOT_FOUND: container not found"), { code: 5 });
|
|
49
56
|
const normalized = normalizeToolError(prefixed);
|
|
@@ -95,11 +102,13 @@ async function startControlServer(containers = [], options = {}) {
|
|
|
95
102
|
const loaded = grpc.loadPackageDefinition(definition);
|
|
96
103
|
const server = new grpc.Server();
|
|
97
104
|
const received = [];
|
|
105
|
+
const versions = [];
|
|
98
106
|
const createRequests = [];
|
|
99
107
|
const recreateRequests = [];
|
|
100
108
|
server.addService(loaded.dshctl.v1.OrchestratorControl.service, {
|
|
101
109
|
listWorkspaces: (call, callback) => {
|
|
102
110
|
received.push(call.metadata.get("authorization")[0]);
|
|
111
|
+
versions.push(call.metadata.get("x-dsh-podman-plugin-version")[0]);
|
|
103
112
|
callback(null, { workspaces: [] });
|
|
104
113
|
},
|
|
105
114
|
describeWorkspace: (_call, callback) => {
|
|
@@ -139,13 +148,14 @@ async function startControlServer(containers = [], options = {}) {
|
|
|
139
148
|
return {
|
|
140
149
|
socketsRoot,
|
|
141
150
|
received,
|
|
151
|
+
versions,
|
|
142
152
|
createRequests,
|
|
143
153
|
recreateRequests,
|
|
144
154
|
stop: () => server.forceShutdown(),
|
|
145
155
|
};
|
|
146
156
|
}
|
|
147
157
|
test("control calls carry the bearer token", async () => {
|
|
148
|
-
const { socketsRoot, received, stop } = await startControlServer();
|
|
158
|
+
const { socketsRoot, received, versions, stop } = await startControlServer();
|
|
149
159
|
try {
|
|
150
160
|
const resolver = new WorkspaceResolver({
|
|
151
161
|
socketsRoot,
|
|
@@ -155,13 +165,14 @@ test("control calls carry the bearer token", async () => {
|
|
|
155
165
|
}, undefined);
|
|
156
166
|
await resolver.control("listWorkspaces", {});
|
|
157
167
|
assert.equal(received[0], "bearer tok-1");
|
|
168
|
+
assert.equal(versions[0], VERSION, "control calls report the plugin version");
|
|
158
169
|
}
|
|
159
170
|
finally {
|
|
160
171
|
stop();
|
|
161
172
|
}
|
|
162
173
|
});
|
|
163
174
|
test("control calls omit the token when unset", async () => {
|
|
164
|
-
const { socketsRoot, received, stop } = await startControlServer();
|
|
175
|
+
const { socketsRoot, received, versions, stop } = await startControlServer();
|
|
165
176
|
try {
|
|
166
177
|
const resolver = new WorkspaceResolver({
|
|
167
178
|
socketsRoot,
|
package/docs/architecture.md
CHANGED
|
@@ -24,6 +24,16 @@ The plugin connects to the orchestrator over a Unix socket at
|
|
|
24
24
|
`<socketsRoot>/orchestrator.sock` and talks gRPC. Requests carry the shared
|
|
25
25
|
token (`DSH_PODMAN_ORCHESTRATOR_TOKEN`) as an `authorization: bearer` header.
|
|
26
26
|
|
|
27
|
+
Requests also carry the plugin's version in an `x-dsh-podman-plugin-version`
|
|
28
|
+
header. The plugin and the orchestrator are deployed separately (an npm package
|
|
29
|
+
inside the dsh image, and this binary), so the orchestrator refuses a control
|
|
30
|
+
call from a plugin whose **major** version differs, naming both versions and
|
|
31
|
+
which side is behind — an out-of-sync deployment fails loudly instead of being
|
|
32
|
+
misread. A missing or unparseable version is refused as well. A minor or patch
|
|
33
|
+
difference is compatible: the call proceeds, the orchestrator logs it once per
|
|
34
|
+
plugin version, and the settings card shows it. `GetVersion` is exempt from the
|
|
35
|
+
check, so a plugin can always learn the orchestrator's version.
|
|
36
|
+
|
|
27
37
|
The orchestrator service exposes these gRPC methods (backing both the UI and the
|
|
28
38
|
tools): `ListContainers` (returns only the guest containers the orchestrator
|
|
29
39
|
created — containers it does not own are never exposed),
|
package/docs/architecture.zh.md
CHANGED
|
@@ -23,6 +23,12 @@ dsh-podman 由三个组件组成:
|
|
|
23
23
|
orchestrator(编排器)并通信 gRPC。请求携带共享
|
|
24
24
|
token(`DSH_PODMAN_ORCHESTRATOR_TOKEN`)作为 `authorization: bearer` 标头。
|
|
25
25
|
|
|
26
|
+
请求还会在 `x-dsh-podman-plugin-version` 标头中携带插件版本。插件与 orchestrator
|
|
27
|
+
分别部署(前者是 dsh 镜像中的 npm 包,后者是此二进制文件),因此 orchestrator
|
|
28
|
+
会拒绝来自**主版本**不同的插件的控制调用,并指出两者的版本以及哪一方更旧——不同步的部署会明确失败,而不会被误读。缺失或无法解析的版本同样会被拒绝。次版本或修订版本不同是兼容的:调用照常进行,orchestrator
|
|
29
|
+
会按插件版本记录一次警告,设置卡片也会显示。`GetVersion`
|
|
30
|
+
不受该检查约束,因此插件始终可以获知 orchestrator 的版本。
|
|
31
|
+
|
|
26
32
|
orchestrator 服务公开以下 gRPC 方法(同时支撑 UI
|
|
27
33
|
和工具):`ListContainers`(仅返回 orchestrator 创建的 guest
|
|
28
34
|
容器——它不拥有的容器永远不会被暴露)、`StartContainer{workspace_slug, container, image_id, mounts, env, secret_env, paths}`(在工作区的
|
package/docs/development.md
CHANGED
|
@@ -81,27 +81,54 @@ make image # build images
|
|
|
81
81
|
systemctl --user restart dsh dsh-podman-orchestrator
|
|
82
82
|
```
|
|
83
83
|
|
|
84
|
-
###
|
|
84
|
+
### Install a local plugin build
|
|
85
|
+
|
|
86
|
+
The dsh image installs the plugin itself at container start, so a development
|
|
87
|
+
build is served by pointing that install at a bind-mounted package. Build the
|
|
88
|
+
plugin and pack it:
|
|
85
89
|
|
|
86
90
|
```bash
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
91
|
+
pnpm build
|
|
92
|
+
npm pack # writes exagone313-dsh-podman-<version>.tgz
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Then add one of these to the dsh container unit and restart it:
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
# the repository directory, which must contain the packed archive
|
|
99
|
+
Volume=/path/to/repo:/mnt/dsh-podman:ro
|
|
100
|
+
Environment=DSH_PODMAN_PLUGIN_SOURCE=/mnt/dsh-podman
|
|
93
101
|
```
|
|
94
102
|
|
|
103
|
+
```
|
|
104
|
+
# or the archive itself
|
|
105
|
+
Volume=/path/to/exagone313-dsh-podman-0.2.0-rc.3.tgz:/mnt/dsh-podman.tgz:ro
|
|
106
|
+
Environment=DSH_PODMAN_PLUGIN_SOURCE=/mnt/dsh-podman.tgz
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
The entrypoint installs that package on every start and never falls back to the
|
|
110
|
+
registry — a missing package is an error. Re-run `pnpm build && npm pack` and
|
|
111
|
+
restart dsh to pick up changes.
|
|
112
|
+
|
|
113
|
+
Without `DSH_PODMAN_PLUGIN_SOURCE`, the entrypoint installs
|
|
114
|
+
`@exagone313/dsh-podman@$DSH_PODMAN_PLUGIN_VERSION` (the version baked into the
|
|
115
|
+
image) exactly, upgrading or downgrading the profile's copy to match.
|
|
116
|
+
|
|
95
117
|
## Continuous integration
|
|
96
118
|
|
|
97
|
-
CI
|
|
119
|
+
CI runs on every **branch** push and pull request, split across
|
|
120
|
+
`.github/workflows/ci-common.yml` (REUSE lint and zizmor), `ci-code.yml` (Go,
|
|
121
|
+
JS, images, Trivy), `ci-docs.yml` (Deno fmt) and `ci-dsh-image.yml`. A **tag**
|
|
122
|
+
push runs only `release.yml`, which repeats the build, vet and tests itself:
|
|
98
123
|
|
|
99
124
|
- **actions-lint** — zizmor scans the workflows for insecure practices.
|
|
125
|
+
- **reuse** — REUSE license compliance.
|
|
100
126
|
- **go** — build, vet, tests, and govulncheck (Go vulnerabilities). Unfixed
|
|
101
127
|
findings don't fail the job; fixable ones do.
|
|
102
128
|
- **js** — install, typecheck, build, tests, and `pnpm audit`.
|
|
129
|
+
- **docs** — `deno fmt --check` on the Markdown.
|
|
103
130
|
- **images** — builds the orchestrator and guest-agent images (runs only after
|
|
104
|
-
the test jobs pass).
|
|
131
|
+
the test jobs pass); **dsh image** builds `Containerfile.dsh`.
|
|
105
132
|
- **trivy** — filesystem vulnerability scan (unfixed ignored) and container
|
|
106
133
|
misconfiguration scan (DS-0002 excluded via `.trivyignore.yaml`).
|
|
107
134
|
|
|
@@ -115,9 +142,12 @@ pre-releases like `1.0.0-rc.1` also work). The release workflow
|
|
|
115
142
|
|
|
116
143
|
1. Runs the tests, then builds both binaries for `linux/amd64` and
|
|
117
144
|
`linux/arm64`.
|
|
118
|
-
2. Pushes the **orchestrator** and **guest-agent** images to GHCR
|
|
119
|
-
(`ghcr.io/exagone313/dsh-podman/{orchestrator,guest-agent}`)
|
|
120
|
-
|
|
145
|
+
2. Pushes the **dsh**, **orchestrator** and **guest-agent** images to GHCR
|
|
146
|
+
(`ghcr.io/exagone313/dsh-podman/{dsh,orchestrator,guest-agent}`). Every
|
|
147
|
+
release is tagged with its version; a **stable** release — `1.0.0` or above
|
|
148
|
+
with no pre-release suffix — is also tagged with its major version (`1`) and
|
|
149
|
+
`latest`. A `0.x` release and any hyphenated tag are pre-releases: they get
|
|
150
|
+
the version tag only.
|
|
121
151
|
3. Publishes the plugin to **npm** (`@exagone313/dsh-podman`) with provenance;
|
|
122
152
|
pre-releases are published under the `next` dist-tag.
|
|
123
153
|
4. Creates a **GitHub release** with auto-generated notes and attaches the
|
|
@@ -139,4 +169,9 @@ built plugin and binaries report, since `scripts/generate-version.mjs` derives
|
|
|
139
169
|
the embedded version from `git describe --tags`. Pre-releases (`1.0.0-rc.1`) are
|
|
140
170
|
bumped the same way.
|
|
141
171
|
|
|
172
|
+
A release can also be triggered, or a failed one re-run, from the Actions tab
|
|
173
|
+
with `workflow_dispatch`, which takes the version as input. Already-published
|
|
174
|
+
steps are skipped (npm skips a version it already has, and an existing GitHub
|
|
175
|
+
release is left alone), so a retry never republishes.
|
|
176
|
+
|
|
142
177
|
The `NPM_TOKEN` secret must be configured on the repository for the npm step.
|
package/docs/development.zh.md
CHANGED
|
@@ -79,26 +79,52 @@ make image # build images
|
|
|
79
79
|
systemctl --user restart dsh dsh-podman-orchestrator
|
|
80
80
|
```
|
|
81
81
|
|
|
82
|
-
###
|
|
82
|
+
### 安装本地插件构建
|
|
83
|
+
|
|
84
|
+
dsh 镜像会在容器启动时自行安装插件,因此本地开发构建通过将安装源指向 bind mount
|
|
85
|
+
的包来使用。先构建并打包插件:
|
|
83
86
|
|
|
84
87
|
```bash
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
88
|
+
pnpm build
|
|
89
|
+
npm pack # 生成 exagone313-dsh-podman-<version>.tgz
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
然后在 dsh 容器单元中加入以下之一并重启:
|
|
93
|
+
|
|
94
|
+
```
|
|
95
|
+
# 仓库目录,其中必须包含已打包的归档
|
|
96
|
+
Volume=/path/to/repo:/mnt/dsh-podman:ro
|
|
97
|
+
Environment=DSH_PODMAN_PLUGIN_SOURCE=/mnt/dsh-podman
|
|
91
98
|
```
|
|
92
99
|
|
|
100
|
+
```
|
|
101
|
+
# 或者归档本身
|
|
102
|
+
Volume=/path/to/exagone313-dsh-podman-0.2.0-rc.3.tgz:/mnt/dsh-podman.tgz:ro
|
|
103
|
+
Environment=DSH_PODMAN_PLUGIN_SOURCE=/mnt/dsh-podman.tgz
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
入口脚本会在每次启动时安装该包,且绝不会回退到注册表——找不到包即为错误。重新运行
|
|
107
|
+
`pnpm build && npm pack` 并重启 dsh 即可生效。
|
|
108
|
+
|
|
109
|
+
未设置 `DSH_PODMAN_PLUGIN_SOURCE` 时,入口脚本会精确安装
|
|
110
|
+
`@exagone313/dsh-podman@$DSH_PODMAN_PLUGIN_VERSION`(镜像构建时写入的版本),并相应升级或降级配置中的副本。
|
|
111
|
+
|
|
93
112
|
## 持续集成
|
|
94
113
|
|
|
95
|
-
CI
|
|
114
|
+
CI 在每次**分支**推送和拉取请求时运行,分散在
|
|
115
|
+
`.github/workflows/ci-common.yml`(REUSE lint 与
|
|
116
|
+
zizmor)、`ci-code.yml`(Go、JS、镜像、Trivy)、`ci-docs.yml`(Deno fmt)和
|
|
117
|
+
`ci-dsh-image.yml` 中。推送**标签**时只运行 `release.yml`,它自身会重复构建、vet
|
|
118
|
+
和测试:
|
|
96
119
|
|
|
97
120
|
- **actions-lint** — zizmor 扫描工作流是否存在不安全实践。
|
|
121
|
+
- **reuse** — REUSE 许可证合规检查。
|
|
98
122
|
- **go** — 构建、vet、测试和 govulncheck(Go 漏洞)。未修复的
|
|
99
123
|
发现不会导致任务失败;可修复的会导致失败。
|
|
100
124
|
- **js** — 安装、类型检查、构建、测试和 `pnpm audit`。
|
|
101
|
-
- **
|
|
125
|
+
- **docs** — 对 Markdown 运行 `deno fmt --check`。
|
|
126
|
+
- **images** — 构建 orchestrator 和 guest-agent
|
|
127
|
+
镜像(仅在测试任务通过后运行);**dsh image** 构建 `Containerfile.dsh`。
|
|
102
128
|
- **trivy** — 文件系统漏洞扫描(未修复的被忽略)和容器 错误配置扫描(DS-0002
|
|
103
129
|
通过 `.trivyignore.yaml` 排除)。
|
|
104
130
|
|
|
@@ -110,9 +136,10 @@ CI(`.github/workflows/ci.yml`)在每次分支推送和拉取请求时运行
|
|
|
110
136
|
这样的预发布同样有效)触发。发布工作流 (`.github/workflows/release.yml`):
|
|
111
137
|
|
|
112
138
|
1. 运行测试,然后为 `linux/amd64` 和 `linux/arm64` 构建两个二进制文件。
|
|
113
|
-
2. 将 **orchestrator** 和 **guest-agent** 镜像推送到 GHCR
|
|
114
|
-
(`ghcr.io/exagone313/dsh-podman/{orchestrator,guest-agent}
|
|
115
|
-
|
|
139
|
+
2. 将 **dsh**、**orchestrator** 和 **guest-agent** 镜像推送到 GHCR
|
|
140
|
+
(`ghcr.io/exagone313/dsh-podman/{dsh,orchestrator,guest-agent}`)。每个发布都打上其版本标签;**稳定**发布——`1.0.0`
|
|
141
|
+
及以上且不带预发布后缀——还会打上其主版本号(`1`)和 `latest`。`0.x`
|
|
142
|
+
发布以及任何带连字符的标签都属于预发布:只打版本标签。
|
|
116
143
|
3. 将插件发布到 **npm**(`@exagone313/dsh-podman`),附带来源证明; 预发布在
|
|
117
144
|
`next` dist-tag 下发布。
|
|
118
145
|
4. 创建带有自动生成说明的 **GitHub release**,并附上 二进制文件和 npm tarball。
|
|
@@ -132,4 +159,8 @@ git push origin master 0.1.1
|
|
|
132
159
|
`scripts/generate-version.mjs` 从 `git describe --tags`
|
|
133
160
|
推导嵌入的版本。预发布版(`1.0.0-rc.1`)也用同样的方式递增。
|
|
134
161
|
|
|
162
|
+
也可以在 Actions 页面通过 `workflow_dispatch`
|
|
163
|
+
触发发布,或重新运行失败的发布,版本作为输入传入。已完成的步骤会被跳过(npm
|
|
164
|
+
会跳过已存在的版本,已有的 GitHub release 不会被改动),因此重试不会重复发布。
|
|
165
|
+
|
|
135
166
|
npm 步骤要求仓库配置 `NPM_TOKEN` 密钥。
|
|
@@ -95,12 +95,13 @@ could be added in the future.
|
|
|
95
95
|
|
|
96
96
|
## Installation
|
|
97
97
|
|
|
98
|
-
### dsh setup
|
|
99
|
-
|
|
100
98
|
This installation uses
|
|
101
99
|
[Podman Quadlet](https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html),
|
|
102
100
|
which adds Podman integration into systemd.
|
|
103
101
|
|
|
102
|
+
Note that the dsh-podman plugin will be installed at dsh container startup,
|
|
103
|
+
which requires online access.
|
|
104
|
+
|
|
104
105
|
1. Create the Quadlet directory for your user:
|
|
105
106
|
```bash
|
|
106
107
|
mkdir -p ~/.config/containers/systemd
|
|
@@ -137,17 +138,6 @@ which adds Podman integration into systemd.
|
|
|
137
138
|
9. Once your web browser has saved this token, you'll be able to access dsh with
|
|
138
139
|
the URL [http://127.0.0.1:3080/](http://127.0.0.1:3080/).
|
|
139
140
|
|
|
140
|
-
### dsh-podman plugin installation
|
|
141
|
-
|
|
142
|
-
1. Run this command to install the dsh-podman plugin:
|
|
143
|
-
```bash
|
|
144
|
-
podman exec dsh dsh plugin --profile web add @exagone313/dsh-podman --allow-build=protobufjs
|
|
145
|
-
```
|
|
146
|
-
2. Restart dsh to complete the installation:
|
|
147
|
-
```bash
|
|
148
|
-
systemctl --user restart dsh
|
|
149
|
-
```
|
|
150
|
-
|
|
151
141
|
## Verification
|
|
152
142
|
|
|
153
143
|
- Open **Settings → Plugins → dsh-podman**: the card should list the base images
|
|
@@ -77,12 +77,12 @@ SPDX-License-Identifier: MIT
|
|
|
77
77
|
|
|
78
78
|
## 安装
|
|
79
79
|
|
|
80
|
-
### dsh 配置
|
|
81
|
-
|
|
82
80
|
本次安装使用
|
|
83
81
|
[Podman Quadlet](https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html),
|
|
84
82
|
它为 systemd 增加了 Podman 集成。
|
|
85
83
|
|
|
84
|
+
注意,dsh-podman 插件会在 dsh 容器启动时安装,这需要联网。
|
|
85
|
+
|
|
86
86
|
1. 为你的用户创建 Quadlet 目录:
|
|
87
87
|
```bash
|
|
88
88
|
mkdir -p ~/.config/containers/systemd
|
|
@@ -118,17 +118,6 @@ SPDX-License-Identifier: MIT
|
|
|
118
118
|
9. 浏览器保存该令牌后,即可通过 [http://127.0.0.1:3080/](http://127.0.0.1:3080/)
|
|
119
119
|
访问 dsh。
|
|
120
120
|
|
|
121
|
-
### dsh-podman 插件安装
|
|
122
|
-
|
|
123
|
-
1. 运行以下命令安装 dsh-podman 插件:
|
|
124
|
-
```bash
|
|
125
|
-
podman exec dsh dsh plugin --profile web add @exagone313/dsh-podman --allow-build=protobufjs
|
|
126
|
-
```
|
|
127
|
-
2. 重启 dsh 以完成安装:
|
|
128
|
-
```bash
|
|
129
|
-
systemctl --user restart dsh
|
|
130
|
-
```
|
|
131
|
-
|
|
132
121
|
## 验证
|
|
133
122
|
|
|
134
123
|
- 打开 **Settings → Plugins →
|