chamba 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +53 -14
- package/README.md +28 -8
- package/dist/commands/advanced.js +7 -278
- package/dist/commands/dev.js +11 -612
- package/dist/commands/doctor.js +1 -29
- package/dist/commands/menu.js +1 -80
- package/dist/commands/onboard.js +6 -230
- package/dist/commands/settings.js +15 -349
- package/dist/lib/agent-commands.js +2 -0
- package/dist/lib/agent-context.js +6 -210
- package/dist/lib/browser.js +1 -40
- package/dist/lib/chamba-yaml.js +10 -191
- package/dist/lib/constants.js +1 -125
- package/dist/lib/dockerfile-builder.js +41 -261
- package/dist/lib/env.js +2 -78
- package/dist/lib/git-env.js +1 -21
- package/dist/lib/global-config.js +4 -66
- package/dist/lib/pnpm-store.js +1 -19
- package/dist/lib/ports.js +1 -210
- package/dist/lib/safe-rm.js +1 -36
- package/dist/lib/sessions.js +2 -34
- package/dist/lib/shadows.js +1 -176
- package/dist/lib/skills.js +2 -0
- package/dist/lib/templates.js +1 -0
- package/dist/lib/webterm.js +1 -305
- package/dist/lib/workspace-identity.js +4 -260
- package/package.json +4 -4
- package/templates/Dockerfile +12 -19
- package/templates/claude-statusline.sh +5 -7
- package/templates/context/baseline.md +2 -0
- package/templates/pane-apps/client/assets/c-BNa-DeUl.json +3552 -0
- package/templates/pane-apps/client/assets/cpp-B-T4NRoH.json +20239 -0
- package/templates/pane-apps/client/assets/csharp-BbxHChhA.json +6010 -0
- package/templates/pane-apps/client/assets/css-CWCxuwmz.json +1860 -0
- package/templates/pane-apps/client/assets/diff-B-2N89g8.json +154 -0
- package/templates/pane-apps/client/assets/docker-DWTvYW26.json +103 -0
- package/templates/pane-apps/client/assets/files-D0nJgFAA.js +12 -0
- package/templates/pane-apps/client/assets/files-h0K1vZyQ.css +1 -0
- package/templates/pane-apps/client/assets/go-B2hyYeNg.json +3156 -0
- package/templates/pane-apps/client/assets/graphql-Z6Id2aNO.json +1292 -0
- package/templates/pane-apps/client/assets/html-BtzvlIfu.json +2627 -0
- package/templates/pane-apps/client/assets/ini-Bn3Twap1.json +108 -0
- package/templates/pane-apps/client/assets/java-C7t2dwLO.json +1885 -0
- package/templates/pane-apps/client/assets/javascript-DuC2v0C0.json +5996 -0
- package/templates/pane-apps/client/assets/json-CwdkWeV5.json +206 -0
- package/templates/pane-apps/client/assets/jsonc-MYWDbTU3.json +206 -0
- package/templates/pane-apps/client/assets/jsx-Cn8EZuIC.json +5996 -0
- package/templates/pane-apps/client/assets/kotlin-CFNOz1gw.json +580 -0
- package/templates/pane-apps/client/assets/make-BVnlFCcl.json +629 -0
- package/templates/pane-apps/client/assets/markdown-B4Xk2WM_.json +3263 -0
- package/templates/pane-apps/client/assets/php-CAQzQmmI.json +4188 -0
- package/templates/pane-apps/client/assets/python-CYQee8m4.json +4174 -0
- package/templates/pane-apps/client/assets/reviews-BsoGZ35r.css +1 -0
- package/templates/pane-apps/client/assets/reviews-iW0Mbn4e.js +153 -0
- package/templates/pane-apps/client/assets/ruby-D7R58tus.json +3114 -0
- package/templates/pane-apps/client/assets/rust-BFBbFiei.json +1103 -0
- package/templates/pane-apps/client/assets/scss-TQRhBkPG.json +1874 -0
- package/templates/pane-apps/client/assets/shellscript-DieAkC4W.json +2375 -0
- package/templates/pane-apps/client/assets/specs-D9z3neR-.js +13 -0
- package/templates/pane-apps/client/assets/sql-Bxd030LV.json +634 -0
- package/templates/pane-apps/client/assets/toml-ChrKGzsV.json +430 -0
- package/templates/pane-apps/client/assets/tsx-D2e90MFw.json +5996 -0
- package/templates/pane-apps/client/assets/typescript-DonXQ_n1.json +5747 -0
- package/templates/pane-apps/client/assets/xml-B17r2RXF.json +382 -0
- package/templates/pane-apps/client/assets/yaml-CKn4h1zy.json +627 -0
- package/templates/pane-apps/client/reviews/index.html +15 -0
- package/templates/pane-apps/client/specs/index.html +3 -2
- package/templates/pane-apps/server/reviews.mjs +20 -0
- package/templates/pane-apps/server/specs.mjs +5 -1588
- package/templates/skills/chamba-statusline/SKILL.md +7 -19
- package/templates/skills/dx-review/SKILL.md +123 -0
- package/templates/skills/dx-review/references/acts.md +162 -0
- package/templates/skills/dx-spec/SKILL.md +223 -27
- package/templates/skills/dx-spec/references/principles-template.md +2 -0
- package/templates/skills/dx-spec/references/review-guide.md +4 -50
- package/templates/skills/dx-spec/references/spec-guide.md +24 -4
- package/templates/skills/dx-spec/references/stages.md +143 -58
- package/templates/skills/dx-spec-execute/SKILL.md +148 -35
- package/templates/startup.mjs +10 -4
- package/templates/tool-helper.sh +166 -0
- package/templates/webterm/README.md +60 -26
- package/templates/webterm/color.js +61 -0
- package/templates/webterm/config.js +35 -4
- package/templates/webterm/context/claude.md +0 -1
- package/templates/webterm/public/app/alerts.js +4 -4
- package/templates/webterm/public/app/anchored.js +81 -0
- package/templates/webterm/public/app/connection.js +9 -0
- package/templates/webterm/public/app/dom.js +2 -0
- package/templates/webterm/public/app/frames.js +51 -17
- package/templates/webterm/public/app/main.js +22 -8
- package/templates/webterm/public/app/new-session.js +13 -41
- package/templates/webterm/public/app/palette.js +58 -0
- package/templates/webterm/public/app/pane-shape.js +167 -0
- package/templates/webterm/public/app/pane-shell.js +199 -134
- package/templates/webterm/public/app/pane.js +33 -24
- package/templates/webterm/public/app/reviews-host.js +15 -0
- package/templates/webterm/public/app/specs-host.js +12 -218
- package/templates/webterm/public/app/state.js +5 -0
- package/templates/webterm/public/app/tabs.js +6 -2
- package/templates/webterm/public/app/theme.js +75 -27
- package/templates/webterm/public/app/tool-host.js +302 -0
- package/templates/webterm/public/app/workspace-color.js +192 -0
- package/templates/webterm/public/index.html +17 -3
- package/templates/webterm/public/styles.css +143 -21
- package/templates/webterm/server.js +198 -150
- package/templates/webterm/tools/commands.js +61 -0
- package/templates/webterm/tools/index.js +25 -0
- package/templates/webterm/tools/mount.js +337 -0
- package/templates/webterm/tools/paths.js +34 -0
- package/templates/webterm/tools/reviews.js +133 -0
- package/templates/webterm/tools/specs.js +154 -0
- package/templates/webterm/typed-line.js +70 -7
- package/templates/context/context-usage.md +0 -1
- package/templates/context-usage.sh +0 -266
- package/templates/pane-apps/client/assets/specs-B1970L17.css +0 -1
- package/templates/pane-apps/client/assets/specs-cEee_SPn.js +0 -23
- package/templates/skills/dx-spec-config/SKILL.md +0 -313
- package/templates/skills/dx-spec-config/references/principles-template.md +0 -12
- package/templates/specs.sh +0 -106
- package/templates/webterm/specs.js +0 -358
|
@@ -1,268 +1,48 @@
|
|
|
1
|
-
|
|
2
|
-
// src/lib/dockerfile-builder.ts - In-memory Dockerfile assembly and temp-file build
|
|
3
|
-
// Combines base template + profile hook + USER instruction at build time
|
|
4
|
-
// =========================================================================================================================================
|
|
5
|
-
import { spawn, spawnSync } from "node:child_process";
|
|
6
|
-
import { createHash, randomBytes } from "node:crypto";
|
|
7
|
-
import { existsSync, readdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
8
|
-
import { tmpdir } from "node:os";
|
|
9
|
-
import { join } from "node:path";
|
|
10
|
-
import { spinner } from "@clack/prompts";
|
|
11
|
-
import { CONTAINER_HOME, CONTAINER_NAME_PREFIX, CONTAINER_STARTUP, CONTAINER_USER, LABEL_BUILD_HASH, LABEL_MANAGED, WEB_KEY_FILE_PATH, } from "./constants.js";
|
|
12
|
-
// --- User shell config appended after USER instruction -----------------------------------------------------------------------------------
|
|
13
|
-
const USER_SHELL_CONFIG = `
|
|
1
|
+
import{spawn as k,spawnSync as w}from"node:child_process";import{createHash as B,randomBytes as M}from"node:crypto";import{existsSync as N,readdirSync as C,readFileSync as d,unlinkSync as I,writeFileSync as P}from"node:fs";import{tmpdir as x}from"node:os";import{join as u}from"node:path";import{spinner as U}from"@clack/prompts";import{CONTAINER_HOME as s,CONTAINER_NAME_PREFIX as H,CONTAINER_STARTUP as O,CONTAINER_USER as D,LABEL_BUILD_HASH as g,LABEL_MANAGED as W,WEB_KEY_FILE_PATH as E}from"./constants.js";const F=`
|
|
14
2
|
# ---------------------------------------------------------------------------
|
|
15
3
|
# User shell config (PATH, prompt, welcome message, status alias)
|
|
16
4
|
# ---------------------------------------------------------------------------
|
|
17
|
-
ENV PATH="${
|
|
5
|
+
ENV PATH="${s}/.cargo/bin:${s}/.bun/bin:${s}/.local/bin:/usr/local/go/bin:\${PATH}"
|
|
18
6
|
# Prompt helper: print the working directory relative to the workspace root, so /workspace shows as
|
|
19
7
|
# "/" and /workspace/src shows as "/src". Paths outside the workspace fall back to their full path.
|
|
20
|
-
RUN echo '__chamba_pwd() { local p="\${PWD#/workspace}"; printf "/%s" "\${p#/}"; }' >> ${
|
|
21
|
-
echo '# The web interface URL, key and all. The key is minted by the webterm server at every start and' >> ${
|
|
22
|
-
echo '# published to the key file, so the greeting reads it back instead of holding a URL that goes stale.' >> ${
|
|
23
|
-
echo '__chamba_web_url() { if [ -r ${
|
|
24
|
-
>> ${
|
|
25
|
-
echo 'export PS1="\\[\\033[01;32m\\][chamba@\${CHAMBA_WORKSPACE}]\\[\\033[00m\\] \\[\\033[01;34m\\]\\$(__chamba_pwd)\\[\\033[00m\\] \\[\\033[01;32m\\]
|
|
26
|
-
>> ${
|
|
27
|
-
echo 'echo ""' >> ${
|
|
28
|
-
echo 'echo -e "\\033[32m
|
|
29
|
-
echo 'echo ""' >> ${
|
|
30
|
-
echo 'echo -e " \\033[90m
|
|
31
|
-
echo 'echo -e " \\033[90m
|
|
32
|
-
echo 'echo -e " \\033[90m
|
|
33
|
-
echo 'echo ""' >> ${
|
|
34
|
-
echo '# The web interface, where the agents usually run. CHAMBA_WEB_URL is injected by docker run whenever' >> ${
|
|
35
|
-
echo '# a host port could be published for it, so its absence means this session has no web address.' >> ${
|
|
36
|
-
echo '# The URL gets its own line: on a host that could not open a browser, this banner is how it is found.' >> ${
|
|
37
|
-
echo 'if [ -n "$CHAMBA_WEB_URL" ]; then' >> ${
|
|
38
|
-
echo ' echo -e "\\033[32m
|
|
39
|
-
echo ' echo ""' >> ${
|
|
40
|
-
echo ' echo -e " \\033[97m$(__chamba_web_url)\\033[0m"' >> ${
|
|
41
|
-
echo ' echo ""' >> ${
|
|
42
|
-
echo 'fi' >> ${
|
|
43
|
-
echo 'alias status="node ${
|
|
8
|
+
RUN echo '__chamba_pwd() { local p="\${PWD#/workspace}"; printf "/%s" "\${p#/}"; }' >> ${s}/.bashrc && \\
|
|
9
|
+
echo '# The web interface URL, key and all. The key is minted by the webterm server at every start and' >> ${s}/.bashrc && \\
|
|
10
|
+
echo '# published to the key file, so the greeting reads it back instead of holding a URL that goes stale.' >> ${s}/.bashrc && \\
|
|
11
|
+
echo '__chamba_web_url() { if [ -r ${E} ]; then echo "\${CHAMBA_WEB_URL}/?k=$(head -n 1 ${E})"; else echo "\${CHAMBA_WEB_URL}"; fi; }' \\
|
|
12
|
+
>> ${s}/.bashrc && \\
|
|
13
|
+
echo 'export PS1="\\[\\033[01;32m\\][chamba@\${CHAMBA_WORKSPACE}]\\[\\033[00m\\] \\[\\033[01;34m\\]\\$(__chamba_pwd)\\[\\033[00m\\] \\[\\033[01;32m\\]\u276F\\[\\033[00m\\] "' \\
|
|
14
|
+
>> ${s}/.bashrc && \\
|
|
15
|
+
echo 'echo ""' >> ${s}/.bashrc && \\
|
|
16
|
+
echo 'echo -e "\\033[32m\u25CF\\033[0m \\033[1mYou'"'"'re now in a chamba sandbox\\033[0m \\033[90m\xB7\\033[0m \\033[1m\${CHAMBA_WORKSPACE}\\033[0m"' >> ${s}/.bashrc && \\
|
|
17
|
+
echo 'echo ""' >> ${s}/.bashrc && \\
|
|
18
|
+
echo 'echo -e " \\033[90m\u25B8 Run \\033[38;5;208mclaude\\033[90m, \\033[38;5;208mopencode\\033[90m, or \\033[38;5;208mcodex\\033[90m to start an agent.\\033[0m"' >> ${s}/.bashrc && \\
|
|
19
|
+
echo 'echo -e " \\033[90m\u25B8 Run \\033[97mstatus\\033[90m to see container details & installed versions.\\033[0m"' >> ${s}/.bashrc && \\
|
|
20
|
+
echo 'echo -e " \\033[90m\u25B8 Run \\033[97mexit\\033[90m to end the session and return to the host.\\033[0m"' >> ${s}/.bashrc && \\
|
|
21
|
+
echo 'echo ""' >> ${s}/.bashrc && \\
|
|
22
|
+
echo '# The web interface, where the agents usually run. CHAMBA_WEB_URL is injected by docker run whenever' >> ${s}/.bashrc && \\
|
|
23
|
+
echo '# a host port could be published for it, so its absence means this session has no web address.' >> ${s}/.bashrc && \\
|
|
24
|
+
echo '# The URL gets its own line: on a host that could not open a browser, this banner is how it is found.' >> ${s}/.bashrc && \\
|
|
25
|
+
echo 'if [ -n "$CHAMBA_WEB_URL" ]; then' >> ${s}/.bashrc && \\
|
|
26
|
+
echo ' echo -e "\\033[32m\u25CF\\033[0m \\033[90mThe web interface for this workspace:\\033[0m"' >> ${s}/.bashrc && \\
|
|
27
|
+
echo ' echo ""' >> ${s}/.bashrc && \\
|
|
28
|
+
echo ' echo -e " \\033[97m$(__chamba_web_url)\\033[0m"' >> ${s}/.bashrc && \\
|
|
29
|
+
echo ' echo ""' >> ${s}/.bashrc && \\
|
|
30
|
+
echo 'fi' >> ${s}/.bashrc && \\
|
|
31
|
+
echo 'alias status="node ${O}"' >> ${s}/.bashrc
|
|
44
32
|
|
|
45
33
|
CMD ["/bin/bash"]
|
|
46
|
-
`;
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
// USER must come after profile hook and before shell config
|
|
62
|
-
content += `\nUSER ${CONTAINER_USER}\n`;
|
|
63
|
-
content += USER_SHELL_CONFIG;
|
|
64
|
-
return content;
|
|
65
|
-
}
|
|
66
|
-
// --- Build hash for image staleness detection --------------------------------------------------------------------------------------------
|
|
67
|
-
// Filenames (relative to the templates dir) of every artifact that the Dockerfile bakes into the image
|
|
68
|
-
// via COPY. Edits to any of these change the build hash and trigger a rebuild prompt at session start.
|
|
69
|
-
// Kept in sync with templates/Dockerfile by the bidirectional test in tests/dockerfile-builder.test.ts.
|
|
70
|
-
export const BAKED_TEMPLATE_FILES = [
|
|
71
|
-
"claude-statusline.sh",
|
|
72
|
-
"context-usage.sh",
|
|
73
|
-
"git-readonly-wrapper.mjs",
|
|
74
|
-
"npmrc",
|
|
75
|
-
"pnpm-config.yaml",
|
|
76
|
-
"runtime-constants.mjs",
|
|
77
|
-
"specs.sh",
|
|
78
|
-
"startup-git-mode.mjs",
|
|
79
|
-
"startup.mjs",
|
|
80
|
-
"webpane.sh",
|
|
81
|
-
"webterm.sh",
|
|
82
|
-
];
|
|
83
|
-
// Directories (relative to the templates dir) baked into the image via a directory COPY. Hashed
|
|
84
|
-
// recursively by computeBuildHash so editing any file inside triggers a rebuild prompt, with
|
|
85
|
-
// node_modules skipped - a local install there must never flip the hash (it is .dockerignore'd too).
|
|
86
|
-
export const BAKED_TEMPLATE_DIRS = ["pane-apps", "webterm"];
|
|
87
|
-
// Relative paths of every regular file under dir (sorted, node_modules subtrees skipped). Missing
|
|
88
|
-
// dirs yield [] for the same reason computeBuildHash tolerates missing files: minimal test contexts.
|
|
89
|
-
function walkTemplateDir(root, prefix = "") {
|
|
90
|
-
const files = [];
|
|
91
|
-
let entries;
|
|
92
|
-
try {
|
|
93
|
-
entries = readdirSync(root, { withFileTypes: true });
|
|
94
|
-
}
|
|
95
|
-
catch {
|
|
96
|
-
return files;
|
|
97
|
-
}
|
|
98
|
-
for (const entry of entries) {
|
|
99
|
-
if (entry.name === "node_modules")
|
|
100
|
-
continue;
|
|
101
|
-
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
102
|
-
if (entry.isDirectory())
|
|
103
|
-
files.push(...walkTemplateDir(join(root, entry.name), rel));
|
|
104
|
-
else if (entry.isFile())
|
|
105
|
-
files.push(rel);
|
|
106
|
-
}
|
|
107
|
-
return files.sort();
|
|
108
|
-
}
|
|
109
|
-
/**
|
|
110
|
-
* Fingerprint everything the package contributes to the image: the assembled Dockerfile content
|
|
111
|
-
* plus every file in BAKED_TEMPLATE_FILES, hashed in deterministic order.
|
|
112
|
-
*
|
|
113
|
-
* Tolerates missing files in buildContextDir. In production (real package install) all baked files
|
|
114
|
-
* exist, so the existsSync branch never fires. In tests that build minimal images from a temp
|
|
115
|
-
* contextDir, missing files naturally produce a different hash than production - exactly the
|
|
116
|
-
* contract the staleness tests rely on.
|
|
117
|
-
*/
|
|
118
|
-
export function computeBuildHash(dockerfileContent, buildContextDir) {
|
|
119
|
-
const h = createHash("sha256");
|
|
120
|
-
h.update("dockerfile:\n");
|
|
121
|
-
h.update(dockerfileContent);
|
|
122
|
-
for (const name of [...BAKED_TEMPLATE_FILES].sort()) {
|
|
123
|
-
h.update(`\nfile:${name}\n`);
|
|
124
|
-
const path = join(buildContextDir, name);
|
|
125
|
-
if (existsSync(path)) {
|
|
126
|
-
h.update(readFileSync(path));
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
for (const dir of [...BAKED_TEMPLATE_DIRS].sort()) {
|
|
130
|
-
for (const rel of walkTemplateDir(join(buildContextDir, dir))) {
|
|
131
|
-
h.update(`\nfile:${dir}/${rel}\n`);
|
|
132
|
-
h.update(readFileSync(join(buildContextDir, dir, rel)));
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
return h.digest("hex");
|
|
136
|
-
}
|
|
137
|
-
// --- Image staleness detection -----------------------------------------------------------------------------------------------------------
|
|
138
|
-
//
|
|
139
|
-
// Called at session start; returns true if the running container's image is older than the current
|
|
140
|
-
// package and needs a rebuild prompt.
|
|
141
|
-
//
|
|
142
|
-
// Mechanism: at build time, buildImageWithTempfile below stamps every image with a chamba.build-hash
|
|
143
|
-
// label carrying computeBuildHash's fingerprint of the assembled Dockerfile + every baked template
|
|
144
|
-
// file. At session start we recompute the expected hash from current package sources and compare.
|
|
145
|
-
// Any change to templates/Dockerfile, the active profile hook, or any baked template file produces a
|
|
146
|
-
// different hash -> rebuild prompt fires.
|
|
147
|
-
//
|
|
148
|
-
// When shipping a new bake-time artifact:
|
|
149
|
-
// - Editing an existing template file or the Dockerfile -> auto-detected. No action.
|
|
150
|
-
// - Adding a NEW templated COPY -> add the filename to BAKED_TEMPLATE_FILES above. The unit test in
|
|
151
|
-
// tests/dockerfile-builder.test.ts will fail until you do.
|
|
152
|
-
//
|
|
153
|
-
// Cosmetic Dockerfile edits (comment-only, whitespace) DO trigger a rebuild for users on prior
|
|
154
|
-
// images. This is intentional: the Dockerfile and template files are rarely edited, so any change
|
|
155
|
-
// is treated as meaningful. A cosmetic edit here costs every user a rebuild, so it is worth asking
|
|
156
|
-
// whether the change is worth making at all.
|
|
157
|
-
/** Returns true if the container's stamped chamba.build-hash label does not match the expected hash. */
|
|
158
|
-
export function isImageStale(containerName, expectedBuildHash) {
|
|
159
|
-
const result = spawnSync("docker", ["inspect", "--format", `{{ index .Config.Labels "${LABEL_BUILD_HASH}" }}`, containerName], {
|
|
160
|
-
encoding: "utf8",
|
|
161
|
-
stdio: "pipe",
|
|
162
|
-
});
|
|
163
|
-
if (result.status !== 0)
|
|
164
|
-
return true;
|
|
165
|
-
return result.stdout.trim() !== expectedBuildHash;
|
|
166
|
-
}
|
|
167
|
-
// --- Build image with temp file ----------------------------------------------------------------------------------------------------------
|
|
168
|
-
// Grey ANSI for the inline build percentage on the spinner line.
|
|
169
|
-
const grey = (s) => `\x1b[90m${s}\x1b[0m`;
|
|
170
|
-
/**
|
|
171
|
-
* Run docker build behind a single clack spinner for interactive sessions.
|
|
172
|
-
* All raw buildx output is captured, never streamed. BuildKit "[ n/N ]" step markers (from
|
|
173
|
-
* --progress=plain) are parsed into a grey percentage on the spinner line when available; if nothing
|
|
174
|
-
* parses, the spinner just keeps animating with the plain text. The captured log is written to stderr
|
|
175
|
-
* only on failure, so a broken build stays diagnosable.
|
|
176
|
-
*/
|
|
177
|
-
function runBuildWithSpinner(buildArgs) {
|
|
178
|
-
return new Promise((resolve) => {
|
|
179
|
-
const s = spinner();
|
|
180
|
-
const baseMessage = "Docker rebuilding container..";
|
|
181
|
-
s.start(baseMessage);
|
|
182
|
-
let captured = "";
|
|
183
|
-
let lastStep = 0;
|
|
184
|
-
let totalSteps = 0;
|
|
185
|
-
const onData = (chunk) => {
|
|
186
|
-
const text = chunk.toString();
|
|
187
|
-
captured += text;
|
|
188
|
-
try {
|
|
189
|
-
for (const m of text.matchAll(/\[\s*(\d+)\/(\d+)\]/g)) {
|
|
190
|
-
const cur = Number(m[1]);
|
|
191
|
-
const total = Number(m[2]);
|
|
192
|
-
if (total > 0)
|
|
193
|
-
totalSteps = total;
|
|
194
|
-
if (cur > lastStep)
|
|
195
|
-
lastStep = cur;
|
|
196
|
-
}
|
|
197
|
-
if (totalSteps > 0) {
|
|
198
|
-
const pct = Math.min(100, Math.round((lastStep / totalSteps) * 100));
|
|
199
|
-
s.message(`${baseMessage} ${grey(`${pct}%`)}`);
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
catch {
|
|
203
|
-
// Parsing must never affect the build; fall back to the plain animated spinner.
|
|
204
|
-
}
|
|
205
|
-
};
|
|
206
|
-
const child = spawn("docker", buildArgs, { stdio: ["ignore", "pipe", "pipe"] });
|
|
207
|
-
child.stdout?.on("data", onData);
|
|
208
|
-
child.stderr?.on("data", onData);
|
|
209
|
-
child.on("error", (err) => {
|
|
210
|
-
s.stop("Docker build failed");
|
|
211
|
-
process.stderr.write(`${err.message}\n`);
|
|
212
|
-
resolve({ status: 1 });
|
|
213
|
-
});
|
|
214
|
-
child.on("close", (code) => {
|
|
215
|
-
const status = code ?? 1;
|
|
216
|
-
if (status === 0) {
|
|
217
|
-
s.stop("Container image ready");
|
|
218
|
-
}
|
|
219
|
-
else {
|
|
220
|
-
s.stop("Docker build failed");
|
|
221
|
-
process.stderr.write(captured);
|
|
222
|
-
}
|
|
223
|
-
resolve({ status });
|
|
224
|
-
});
|
|
225
|
-
});
|
|
226
|
-
}
|
|
227
|
-
/**
|
|
228
|
-
* Write Dockerfile content to a temp file, run docker build, then clean up.
|
|
229
|
-
* Build context is always the templates directory so COPY instructions resolve correctly.
|
|
230
|
-
* Quiet mode (tests) runs synchronously with captured output; interactive mode shows a spinner.
|
|
231
|
-
*/
|
|
232
|
-
export async function buildImageWithTempfile(dockerfileContent, buildContextDir, imageName, noCache = false, quiet = false) {
|
|
233
|
-
const tmpFile = join(tmpdir(), `${CONTAINER_NAME_PREFIX}Dockerfile-${randomBytes(8).toString("hex")}`);
|
|
234
|
-
try {
|
|
235
|
-
writeFileSync(tmpFile, dockerfileContent);
|
|
236
|
-
const buildHash = computeBuildHash(dockerfileContent, buildContextDir);
|
|
237
|
-
const buildArgs = [
|
|
238
|
-
"build",
|
|
239
|
-
"--label",
|
|
240
|
-
`${LABEL_MANAGED}=true`,
|
|
241
|
-
"--label",
|
|
242
|
-
`${LABEL_BUILD_HASH}=${buildHash}`,
|
|
243
|
-
"-f",
|
|
244
|
-
tmpFile,
|
|
245
|
-
"-t",
|
|
246
|
-
imageName,
|
|
247
|
-
];
|
|
248
|
-
if (noCache)
|
|
249
|
-
buildArgs.push("--no-cache");
|
|
250
|
-
// Quiet mode (tests): synchronous, output captured, no spinner.
|
|
251
|
-
if (quiet) {
|
|
252
|
-
buildArgs.push(buildContextDir);
|
|
253
|
-
const result = spawnSync("docker", buildArgs, { stdio: "pipe" });
|
|
254
|
-
return { status: result.status ?? 1 };
|
|
255
|
-
}
|
|
256
|
-
// Interactive mode: line-oriented progress so step markers are parseable, hidden behind a spinner.
|
|
257
|
-
buildArgs.push("--progress=plain", buildContextDir);
|
|
258
|
-
return await runBuildWithSpinner(buildArgs);
|
|
259
|
-
}
|
|
260
|
-
finally {
|
|
261
|
-
try {
|
|
262
|
-
unlinkSync(tmpFile);
|
|
263
|
-
}
|
|
264
|
-
catch {
|
|
265
|
-
// temp file cleanup is best-effort
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
}
|
|
34
|
+
`;function ne(o,t){let e=d(o,"utf8");return t?.trim()&&(e+=`
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
`,e+=`# Profile hook (from chamba.yaml)
|
|
37
|
+
`,e+=`# ---------------------------------------------------------------------------
|
|
38
|
+
`,e+=t.endsWith(`
|
|
39
|
+
`)?t:`${t}
|
|
40
|
+
`),e+=`
|
|
41
|
+
USER ${D}
|
|
42
|
+
`,e+=F,e}const j=["claude-statusline.sh","git-readonly-wrapper.mjs","npmrc","pnpm-config.yaml","runtime-constants.mjs","startup-git-mode.mjs","startup.mjs","tool-helper.sh","webpane.sh","webterm.sh"],K=["pane-apps","webterm"];function y(o,t=""){const e=[];let r;try{r=C(o,{withFileTypes:!0})}catch{return e}for(const n of r){if(n.name==="node_modules")continue;const a=t?`${t}/${n.name}`:n.name;n.isDirectory()?e.push(...y(u(o,n.name),a)):n.isFile()&&e.push(a)}return e.sort()}function v(o,t){const e=B("sha256");e.update(`dockerfile:
|
|
43
|
+
`),e.update(o);for(const r of[...j].sort()){e.update(`
|
|
44
|
+
file:${r}
|
|
45
|
+
`);const n=u(t,r);N(n)&&e.update(d(n))}for(const r of[...K].sort())for(const n of y(u(t,r)))e.update(`
|
|
46
|
+
file:${r}/${n}
|
|
47
|
+
`),e.update(d(u(t,r,n)));return e.digest("hex")}function re(o,t){const e=w("docker",["inspect","--format",`{{ index .Config.Labels "${g}" }}`,o],{encoding:"utf8",stdio:"pipe"});return e.status!==0?!0:e.stdout.trim()!==t}const G=new RegExp("\x1B\\[[0-9;]*m","g"),R=o=>`\x1B[90m${o}\x1B[0m`,T=60,X=8,Y=14;function L(o,t){return t<3?"":o.length>t?`${o.slice(0,t-2)}..`:o}function V(o,t){const e=o.replace(G,"").split(/[\r\n]/).map(r=>r.replace(/^#\d+\s+(\d+\.\d+\s+)?/,"").replace(/[^\x20-\x7e]+/g," ").replace(/\s+/g," ").trim()).filter(Boolean);return L(e.at(-1)??"",t)}function q(o,t,e,r){const n=r-1-Y;let a=o,i=o.length;t!==""&&i+2+t.length<=n&&(a+=` ${R(t)}`,i+=2+t.length);const c=Math.min(n-i-2,T);if(e!==""&&c>=X){const h=L(e,c);h!==""&&(a+=` ${R(h)}`)}return a}function z(o){return new Promise(t=>{const e=U({indicator:"timer"}),r="Docker rebuilding container",n=process.stdout.columns??80;e.start(r);let a="",i=0,c=0,h="";const f=m=>{const l=m.toString();a+=l;try{for(const $ of l.matchAll(/\[\s*(\d+)\/(\d+)\]/g)){const _=Number($[1]),A=Number($[2]);A>0&&(c=A),_>i&&(i=_)}const b=c>0?`${Math.min(100,Math.round(i/c*100))}%`:"";if(h=V(l,T)||h,b===""&&h==="")return;const S=Math.min(n,process.stdout.columns??80);e.message(q(r,b,h,S))}catch{}},p=k("docker",o,{stdio:["ignore","pipe","pipe"]});p.stdout?.on("data",f),p.stderr?.on("data",f),p.on("error",m=>{e.stop("Docker build failed"),process.stderr.write(`${m.message}
|
|
48
|
+
`),t({status:1})}),p.on("close",m=>{const l=m??1;l===0?e.stop("Container image ready"):(e.stop("Docker build failed"),process.stderr.write(a)),t({status:l})})})}async function ae(o,t,e,r=!1,n=!1){const a=u(x(),`${H}Dockerfile-${M(8).toString("hex")}`);try{P(a,o);const i=v(o,t),c=["build","--label",`${W}=true`,"--label",`${g}=${i}`,"-f",a,"-t",e];return r&&c.push("--no-cache"),n?(c.push(t),{status:w("docker",c,{stdio:"pipe"}).status??1}):(c.push("--progress=plain",t),await z(c))}finally{try{I(a)}catch{}}}export{K as BAKED_TEMPLATE_DIRS,j as BAKED_TEMPLATE_FILES,ne as buildDockerfile,ae as buildImageWithTempfile,V as buildTailLine,v as computeBuildHash,re as isImageStale,q as spinnerLine};
|
package/dist/lib/env.js
CHANGED
|
@@ -1,78 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
// An `env` entry is either an env-file path (no '=') loaded via --env-file, or an inline KEY=VALUE injected via -e.
|
|
4
|
-
// Config is the source of truth: a hashed label over inline vars + resolved file contents recreates the container on change.
|
|
5
|
-
// =========================================================================================================================================
|
|
6
|
-
import { createHash } from "node:crypto";
|
|
7
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
8
|
-
import { join } from "node:path";
|
|
9
|
-
// --- Constants ---------------------------------------------------------------------------------------------------------------------------
|
|
10
|
-
// Inline var keys must be valid shell env identifiers - the same pattern the `ports` env field uses.
|
|
11
|
-
const ENV_KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
12
|
-
// --- Config validation and normalization -------------------------------------------------------------------------------------------------
|
|
13
|
-
/**
|
|
14
|
-
* Validate and normalize the raw `env` field into inline vars and file entries. This is the single source of
|
|
15
|
-
* truth for env semantics (unit-testable without the JSON Schema). A scalar or undefined is normalized to an
|
|
16
|
-
* array; entries are trimmed and empties dropped. Classification is by presence of '=': an entry with '=' is
|
|
17
|
-
* an inline KEY=VALUE var; an entry without '=' is an env-file path resolved relative to the workspace dir.
|
|
18
|
-
* Throws on the first malformed inline entry with a clear message. Missing files never throw - they are
|
|
19
|
-
* flagged exists:false and warned about at startup rather than failing the session.
|
|
20
|
-
*/
|
|
21
|
-
export function validateEnvConfig(raw, workspaceDir) {
|
|
22
|
-
const entries = (Array.isArray(raw) ? raw : raw === undefined ? [] : [raw]).map((e) => e.trim()).filter((e) => e.length > 0);
|
|
23
|
-
const inlineVars = [];
|
|
24
|
-
const files = [];
|
|
25
|
-
for (const entry of entries) {
|
|
26
|
-
const eq = entry.indexOf("=");
|
|
27
|
-
if (eq === -1) {
|
|
28
|
-
// No '=' -> file path, resolved relative to the workspace dir.
|
|
29
|
-
const path = join(workspaceDir, entry);
|
|
30
|
-
files.push({ path, exists: existsSync(path) });
|
|
31
|
-
}
|
|
32
|
-
else {
|
|
33
|
-
// Has '=' -> inline KEY=VALUE. The key must be a valid env identifier; the value may be empty or contain further '='.
|
|
34
|
-
const key = entry.slice(0, eq);
|
|
35
|
-
if (!ENV_KEY_PATTERN.test(key)) {
|
|
36
|
-
throw new Error(`env: invalid variable "${entry}" - the key must match ${ENV_KEY_PATTERN.source} (e.g. FOO=bar).`);
|
|
37
|
-
}
|
|
38
|
-
inlineVars.push(entry);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
return { inlineVars, files };
|
|
42
|
-
}
|
|
43
|
-
// --- Docker argument builders (pure) -----------------------------------------------------------------------------------------------------
|
|
44
|
-
/**
|
|
45
|
-
* Docker run args for env injection: --env-file for each existing file (missing files omitted), then
|
|
46
|
-
* -e KEY=VALUE per inline var. Files come first so an inline var wins a duplicate key - docker applies later
|
|
47
|
-
* -e over an earlier --env-file - which lets a chamba.yaml inline var override a value from an env file.
|
|
48
|
-
*/
|
|
49
|
-
export function envRunArgs(cfg) {
|
|
50
|
-
const fileArgs = cfg.files.filter((f) => f.exists).flatMap((f) => ["--env-file", f.path]);
|
|
51
|
-
const inlineArgs = cfg.inlineVars.flatMap((v) => ["-e", v]);
|
|
52
|
-
return [...fileArgs, ...inlineArgs];
|
|
53
|
-
}
|
|
54
|
-
/** One warning line per missing env file. A missing file is skipped, never fatal. */
|
|
55
|
-
export function envWarnings(cfg) {
|
|
56
|
-
return cfg.files.filter((f) => !f.exists).map((f) => `env "${f.path}" not found - skipping`);
|
|
57
|
-
}
|
|
58
|
-
// --- Fingerprint -------------------------------------------------------------------------------------------------------------------------
|
|
59
|
-
/**
|
|
60
|
-
* Deterministic fingerprint over the resolved env config, used as the container LABEL_ENV. Covers each inline
|
|
61
|
-
* KEY=VALUE and, for each existing file, its path plus contents - so editing an inline var, editing a file's
|
|
62
|
-
* contents, or repointing at a different file recreates the container on the next session. An empty config
|
|
63
|
-
* (no inline vars and no existing files) fingerprints to "" so an env-less workspace never recreates on
|
|
64
|
-
* account of this label. Hashed so no secret values leak into docker labels (which are visible via inspect).
|
|
65
|
-
*/
|
|
66
|
-
export function envLabel(cfg) {
|
|
67
|
-
if (cfg.inlineVars.length === 0 && cfg.files.every((f) => !f.exists))
|
|
68
|
-
return "";
|
|
69
|
-
const parts = [];
|
|
70
|
-
for (const v of cfg.inlineVars)
|
|
71
|
-
parts.push(`e:${v}`);
|
|
72
|
-
for (const f of cfg.files) {
|
|
73
|
-
if (!f.exists)
|
|
74
|
-
continue;
|
|
75
|
-
parts.push(`f:${f.path}\n${readFileSync(f.path, "utf8")}`);
|
|
76
|
-
}
|
|
77
|
-
return createHash("sha256").update(parts.join("\0")).digest("hex").slice(0, 12);
|
|
78
|
-
}
|
|
1
|
+
import{createHash as l}from"node:crypto";import{existsSync as p,readFileSync as c}from"node:fs";import{join as u}from"node:path";const a=/^[A-Za-z_][A-Za-z0-9_]*$/;function m(e,n){const t=(Array.isArray(e)?e:e===void 0?[]:[e]).map(i=>i.trim()).filter(i=>i.length>0),s=[],o=[];for(const i of t){const f=i.indexOf("=");if(f===-1){const r=u(n,i);o.push({path:r,exists:p(r)})}else{const r=i.slice(0,f);if(!a.test(r))throw new Error(`env: invalid variable "${i}" - the key must match ${a.source} (e.g. FOO=bar).`);s.push(i)}}return{inlineVars:s,files:o}}function d(e){const n=e.files.filter(s=>s.exists).flatMap(s=>["--env-file",s.path]),t=e.inlineVars.flatMap(s=>["-e",s]);return[...n,...t]}function y(e){return e.files.filter(n=>!n.exists).map(n=>`env "${n.path}" not found - skipping`)}function A(e){if(e.inlineVars.length===0&&e.files.every(t=>!t.exists))return"";const n=[];for(const t of e.inlineVars)n.push(`e:${t}`);for(const t of e.files)t.exists&&n.push(`f:${t.path}
|
|
2
|
+
${c(t.path,"utf8")}`);return l("sha256").update(n.join("\0")).digest("hex").slice(0,12)}export{A as envLabel,d as envRunArgs,y as envWarnings,m as validateEnvConfig};
|
package/dist/lib/git-env.js
CHANGED
|
@@ -1,21 +1 @@
|
|
|
1
|
-
|
|
2
|
-
// src/lib/git-env.ts - The environment every git call in chamba runs in
|
|
3
|
-
// git takes its repo from GIT_DIR, GIT_WORK_TREE and GIT_INDEX_FILE before it takes it from -C or the working
|
|
4
|
-
// directory, so a call made from inside a git hook would answer about that hook's repo instead of this one.
|
|
5
|
-
// =========================================================================================================================================
|
|
6
|
-
/**
|
|
7
|
-
* The environment to run git in when the repo is chosen by path.
|
|
8
|
-
*
|
|
9
|
-
* git takes its repo from the environment as well as from the command line, and GIT_DIR, GIT_WORK_TREE and
|
|
10
|
-
* GIT_INDEX_FILE win over `-C` and over the working directory. Anything chamba starts inherits whatever the
|
|
11
|
-
* caller had set - a git hook sets all three - so a question asked about the workspace could be answered about
|
|
12
|
-
* another repo entirely. Every git call here names its repo by path, so none of those variables is ever wanted.
|
|
13
|
-
*/
|
|
14
|
-
export function gitEnv() {
|
|
15
|
-
const env = { ...process.env };
|
|
16
|
-
for (const key of Object.keys(env)) {
|
|
17
|
-
if (key.startsWith("GIT_"))
|
|
18
|
-
delete env[key];
|
|
19
|
-
}
|
|
20
|
-
return env;
|
|
21
|
-
}
|
|
1
|
+
function n(){const e={...process.env};for(const t of Object.keys(e))t.startsWith("GIT_")&&delete e[t];return e}export{n as gitEnv};
|
|
@@ -1,66 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
// =========================================================================================================================================
|
|
6
|
-
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
|
-
import { homedir } from "node:os";
|
|
8
|
-
import { join } from "node:path";
|
|
9
|
-
import { CHAMBA_DIR, GLOBAL_CONFIG_FILE, GLOBAL_DIR, WEB_RANGE_DEFAULT } from "./constants.js";
|
|
10
|
-
import { formatWebRange, parseWebRange } from "./ports.js";
|
|
11
|
-
// --- Keys --------------------------------------------------------------------------------------------------------------------------------
|
|
12
|
-
/** Field names mapped to the keys written in the global config file. */
|
|
13
|
-
export const GLOBAL_CONFIG_KEYS = {
|
|
14
|
-
webRange: "web_range",
|
|
15
|
-
};
|
|
16
|
-
// --- Path --------------------------------------------------------------------------------------------------------------------------------
|
|
17
|
-
/** Absolute path to the global config file - ~/.chamba/global/config */
|
|
18
|
-
export function globalConfigPath() {
|
|
19
|
-
return join(homedir(), CHAMBA_DIR, GLOBAL_DIR, GLOBAL_CONFIG_FILE);
|
|
20
|
-
}
|
|
21
|
-
// --- Parse / write -----------------------------------------------------------------------------------------------------------------------
|
|
22
|
-
// Parse the config into an ordered map of raw key=value pairs. Returns an empty map when the file is
|
|
23
|
-
// missing or unreadable - absence is not an error, it just means defaults apply. Unknown keys are kept
|
|
24
|
-
// so a newer chamba's settings survive a write by an older one.
|
|
25
|
-
function parseGlobalConfig() {
|
|
26
|
-
const config = new Map();
|
|
27
|
-
try {
|
|
28
|
-
const lines = readFileSync(globalConfigPath(), "utf8")
|
|
29
|
-
.trimEnd()
|
|
30
|
-
.split("\n")
|
|
31
|
-
.map((l) => l.trim())
|
|
32
|
-
.filter(Boolean);
|
|
33
|
-
for (const line of lines) {
|
|
34
|
-
const eq = line.indexOf("=");
|
|
35
|
-
if (eq === -1)
|
|
36
|
-
continue;
|
|
37
|
-
config.set(line.slice(0, eq), line.slice(eq + 1));
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
catch {
|
|
41
|
-
// Missing or unreadable - treat as empty.
|
|
42
|
-
}
|
|
43
|
-
return config;
|
|
44
|
-
}
|
|
45
|
-
// Write the raw key=value map back to ~/.chamba/global/config, creating ~/.chamba/global/ on demand.
|
|
46
|
-
// Unlike the per-workspace lock (which no-ops when missing), the global config has no init step, so it
|
|
47
|
-
// is created lazily on the first write.
|
|
48
|
-
function writeGlobalConfig(config) {
|
|
49
|
-
mkdirSync(join(homedir(), CHAMBA_DIR, GLOBAL_DIR), { recursive: true });
|
|
50
|
-
const content = `${[...config].map(([key, value]) => `${key}=${value}`).join("\n")}\n`;
|
|
51
|
-
writeFileSync(globalConfigPath(), content);
|
|
52
|
-
}
|
|
53
|
-
// --- Web agent interface -----------------------------------------------------------------------------------------------------------------
|
|
54
|
-
/** Read the web interface host-port range. Falls back to the default when unset, missing, or invalid. */
|
|
55
|
-
export function readWebRange() {
|
|
56
|
-
const value = parseGlobalConfig().get(GLOBAL_CONFIG_KEYS.webRange);
|
|
57
|
-
const parsed = value !== undefined ? parseWebRange(value) : null;
|
|
58
|
-
// The default is a constant that always parses; the assertion just narrows the type.
|
|
59
|
-
return parsed ?? parseWebRange(WEB_RANGE_DEFAULT);
|
|
60
|
-
}
|
|
61
|
-
/** Write the web interface host-port range. Creates the config file on demand and preserves all other keys. */
|
|
62
|
-
export function writeWebRange(range) {
|
|
63
|
-
const config = parseGlobalConfig();
|
|
64
|
-
config.set(GLOBAL_CONFIG_KEYS.webRange, formatWebRange(range));
|
|
65
|
-
writeGlobalConfig(config);
|
|
66
|
-
}
|
|
1
|
+
import{mkdirSync as p,readFileSync as g,writeFileSync as u}from"node:fs";import{homedir as r}from"node:os";import{join as i}from"node:path";import{CHAMBA_DIR as c,GLOBAL_CONFIG_FILE as b,GLOBAL_DIR as a,WEB_RANGE_DEFAULT as d}from"./constants.js";import{formatWebRange as R,parseWebRange as f}from"./ports.js";const s={webRange:"web_range"};function l(){return i(r(),c,a,b)}function m(){const n=new Map;try{const e=g(l(),"utf8").trimEnd().split(`
|
|
2
|
+
`).map(o=>o.trim()).filter(Boolean);for(const o of e){const t=o.indexOf("=");t!==-1&&n.set(o.slice(0,t),o.slice(t+1))}}catch{}return n}function _(n){p(i(r(),c,a),{recursive:!0});const e=`${[...n].map(([o,t])=>`${o}=${t}`).join(`
|
|
3
|
+
`)}
|
|
4
|
+
`;u(l(),e)}function C(){const n=m().get(s.webRange);return(n!==void 0?f(n):null)??f(d)}function E(n){const e=m();e.set(s.webRange,R(n)),_(e)}export{s as GLOBAL_CONFIG_KEYS,l as globalConfigPath,C as readWebRange,E as writeWebRange};
|
package/dist/lib/pnpm-store.js
CHANGED
|
@@ -1,19 +1 @@
|
|
|
1
|
-
|
|
2
|
-
// src/lib/pnpm-store.ts - Per-workspace pnpm global store mount
|
|
3
|
-
// pnpm hardlinks from its global store into node_modules. Hardlinks cannot cross filesystems, so when the global store sits on the
|
|
4
|
-
// container overlay FS while /workspace is a host bind mount, pnpm falls back to creating a per-project .pnpm-store inside the project -
|
|
5
|
-
// which then ends up on the host repo. Mounting a host-side cache dir onto pnpm's default global store path puts the store on the same
|
|
6
|
-
// device as the workspace and node_modules shadow, eliminating the fallback.
|
|
7
|
-
// =========================================================================================================================================
|
|
8
|
-
import { mkdirSync } from "node:fs";
|
|
9
|
-
import { join } from "node:path";
|
|
10
|
-
import { CONTAINER_PNPM_STORE, PNPM_STORE_DIR } from "./constants.js";
|
|
11
|
-
/**
|
|
12
|
-
* Lazily creates the host-side pnpm store directory under the workspace cache dir
|
|
13
|
-
* and returns -v args mounting it onto pnpm's default global store path in the container.
|
|
14
|
-
*/
|
|
15
|
-
export function buildPnpmStoreMountArgs(workspaceCacheDir) {
|
|
16
|
-
const hostStore = join(workspaceCacheDir, PNPM_STORE_DIR);
|
|
17
|
-
mkdirSync(hostStore, { recursive: true });
|
|
18
|
-
return ["-v", `${hostStore}:${CONTAINER_PNPM_STORE}`];
|
|
19
|
-
}
|
|
1
|
+
import{mkdirSync as t}from"node:fs";import{join as i}from"node:path";import{CONTAINER_PNPM_STORE as m,PNPM_STORE_DIR as n}from"./constants.js";function P(o){const r=i(o,n);return t(r,{recursive:!0}),["-v",`${r}:${m}`]}export{P as buildPnpmStoreMountArgs};
|