boxdown 1.0.0 → 1.2.1

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.
Files changed (76) hide show
  1. package/README.md +135 -12
  2. package/assets/devcontainer/README.md +65 -21
  3. package/assets/devcontainer/devcontainer.json +27 -15
  4. package/assets/devcontainer/hooks/initialize.sh +76 -22
  5. package/assets/devcontainer/hooks/post-create.sh +70 -12
  6. package/assets/devcontainer/hooks/post-start.sh +20 -13
  7. package/assets/devcontainer/ssh-config-install.sh +12 -3
  8. package/assets/devcontainer/start.sh +721 -44
  9. package/assets/devcontainer/utils/coding-agent-cli-update.sh +267 -3
  10. package/assets/devcontainer/utils/deps-install.sh +68 -0
  11. package/assets/devcontainer/utils/git-config-bootstrap.sh +128 -0
  12. package/assets/devcontainer/utils/git-signing-bootstrap.sh +109 -0
  13. package/assets/devcontainer/utils/python-bootstrap.sh +69 -0
  14. package/assets/devcontainer/utils/secret-env-bootstrap.sh +22 -0
  15. package/assets/devcontainer/utils/ssh-agent-proxy-bootstrap.sh +27 -0
  16. package/assets/devcontainer/utils/ssh-agent-proxy.mjs +39 -0
  17. package/assets/devcontainer/utils/ssh-bootstrap.sh +9 -0
  18. package/dist/bin/cli.cjs +1 -1
  19. package/dist/bin/cli.mjs +1 -1
  20. package/dist/main-BDgyf2t5.cjs +5758 -0
  21. package/dist/main-J4_2Up3o.mjs +5718 -0
  22. package/dist/main-J4_2Up3o.mjs.map +1 -0
  23. package/dist/main.cjs +5 -2
  24. package/dist/main.d.cts +501 -4
  25. package/dist/main.d.cts.map +1 -1
  26. package/dist/main.d.mts +501 -4
  27. package/dist/main.d.mts.map +1 -1
  28. package/dist/main.mjs +2 -2
  29. package/docs/README.md +1 -0
  30. package/docs/architecture.md +32 -0
  31. package/docs/development.md +13 -6
  32. package/docs/features/README.md +2 -0
  33. package/docs/features/commit-signing.md +94 -0
  34. package/docs/features/generated-config-and-state.md +73 -5
  35. package/docs/features/github-auth-refresh.md +15 -2
  36. package/docs/features/lifecycle.md +103 -11
  37. package/docs/features/setup.md +66 -0
  38. package/docs/features/ssh-config-and-proxy.md +228 -7
  39. package/docs/features/start-and-shell.md +45 -5
  40. package/docs/superpowers/plans/2026-07-11-default-commit-signing.md +110 -0
  41. package/docs/superpowers/plans/2026-07-11-progress-ci-regression.md +125 -0
  42. package/docs/superpowers/plans/2026-07-17-node-ssh-agent-proxy-signing.md +132 -0
  43. package/docs/superpowers/plans/2026-07-17-runtime-secret-environment.md +174 -0
  44. package/docs/superpowers/specs/2026-07-11-default-commit-signing-design.md +416 -0
  45. package/docs/superpowers/specs/2026-07-11-progress-ci-regression-design.md +43 -0
  46. package/docs/superpowers/specs/2026-07-17-node-ssh-agent-proxy-design.md +63 -0
  47. package/docs/superpowers/specs/2026-07-17-runtime-secret-environment-design.md +194 -0
  48. package/docs/testing.md +35 -2
  49. package/docs/todo.md +2 -2
  50. package/package.json +1 -1
  51. package/src/claude-app-config.ts +304 -0
  52. package/src/cli-style.ts +43 -0
  53. package/src/codex-app-config.ts +656 -0
  54. package/src/config.ts +80 -10
  55. package/src/constants.ts +12 -0
  56. package/src/devcontainer.ts +511 -64
  57. package/src/doctor.ts +292 -30
  58. package/src/git-signing.ts +267 -0
  59. package/src/interactive-prompts.ts +692 -0
  60. package/src/list.ts +16 -2
  61. package/src/logging.ts +164 -0
  62. package/src/main.ts +1214 -63
  63. package/src/metadata.ts +52 -3
  64. package/src/package-info.ts +18 -0
  65. package/src/paths.ts +71 -11
  66. package/src/process.ts +80 -2
  67. package/src/progress.ts +593 -0
  68. package/src/purge.ts +216 -0
  69. package/src/shell.ts +25 -0
  70. package/src/ssh-config.ts +134 -16
  71. package/src/ssh-install-targets.ts +111 -0
  72. package/src/ssh-key.ts +53 -13
  73. package/src/status.ts +5 -0
  74. package/dist/main-BuEptwlL.cjs +0 -1707
  75. package/dist/main-ZFTrSVgt.mjs +0 -1685
  76. package/dist/main-ZFTrSVgt.mjs.map +0 -1
@@ -1,1707 +0,0 @@
1
- let node_fs = require("node:fs");
2
- let node_module = require("node:module");
3
- let node_path = require("node:path");
4
- let node_child_process = require("node:child_process");
5
- let node_crypto = require("node:crypto");
6
- let node_os = require("node:os");
7
- let node_url = require("node:url");
8
-
9
- //#region src/coding-agents.ts
10
- const CODING_AGENT_ALIASES = {
11
- codex: "codex",
12
- opencode: "opencode",
13
- claude: "claude",
14
- cc: "claude",
15
- antigravity: "antigravity"
16
- };
17
- function codingAgentFromCommand(command) {
18
- return CODING_AGENT_ALIASES[command];
19
- }
20
- function codingAgentBinary(agent) {
21
- if (agent === "antigravity") {
22
- return "agy";
23
- }
24
- return agent;
25
- }
26
-
27
- //#endregion
28
- //#region src/constants.ts
29
- const DEVCONTAINER_CLI_VERSION = "0.84.1";
30
- const BOXDOWN_CONTAINER_DEVCONTAINER_DIR = "/opt/boxdown/devcontainer";
31
- const BOXDOWN_CONTAINER_STATE_DIR = "/opt/boxdown/state";
32
- const BOXDOWN_CONTAINER_SSH_DIR = `${BOXDOWN_CONTAINER_STATE_DIR}/ssh`;
33
- const BOXDOWN_CONTAINER_SSH_PUBLIC_KEY_PATH = `${BOXDOWN_CONTAINER_SSH_DIR}/id_ed25519.pub`;
34
- const PACKAGE_NAME = "boxdown";
35
-
36
- //#endregion
37
- //#region src/devcontainer-cli.ts
38
- function devcontainerBinFromPackageJson(packageJson) {
39
- return typeof packageJson.bin === "string" ? packageJson.bin : packageJson.bin?.devcontainer;
40
- }
41
- function resolveDevcontainerCli(context) {
42
- const requireFromBoxdown = (0, node_module.createRequire)((0, node_path.join)(context.packageRoot, "package.json"));
43
- let packageJsonPath;
44
- try {
45
- packageJsonPath = requireFromBoxdown.resolve("@devcontainers/cli/package.json");
46
- } catch (error) {
47
- throw new Error(`Boxdown's packaged @devcontainers/cli dependency is missing. Reinstall boxdown so @devcontainers/cli@${DEVCONTAINER_CLI_VERSION} is available.`, { cause: error });
48
- }
49
- const packageJson = JSON.parse((0, node_fs.readFileSync)(packageJsonPath, "utf8"));
50
- const bin = devcontainerBinFromPackageJson(packageJson);
51
- if (packageJson.version !== "0.84.1") {
52
- throw new Error(`Boxdown expected @devcontainers/cli@${DEVCONTAINER_CLI_VERSION} but resolved ${packageJson.version ?? "an unknown version"}.`);
53
- }
54
- if (bin === undefined) {
55
- throw new Error("Boxdown resolved @devcontainers/cli, but the package does not expose a devcontainer binary.");
56
- }
57
- const cliPath = (0, node_path.resolve)((0, node_path.dirname)(packageJsonPath), bin);
58
- if (!(0, node_fs.existsSync)(cliPath)) {
59
- throw new Error(`Boxdown resolved @devcontainers/cli, but its devcontainer binary is missing: ${cliPath}`);
60
- }
61
- return {
62
- command: process.execPath,
63
- argsPrefix: [cliPath],
64
- path: cliPath,
65
- version: packageJson.version
66
- };
67
- }
68
-
69
- //#endregion
70
- //#region src/process.ts
71
- function mergedEnv(env) {
72
- return {
73
- ...process.env,
74
- ...env ?? {}
75
- };
76
- }
77
- function writeChunk(target, chunk) {
78
- if (target === "stdout") {
79
- process.stdout.write(chunk);
80
- } else if (target === "stderr") {
81
- process.stderr.write(chunk);
82
- }
83
- }
84
- function runBuffered(command, args, options = {}) {
85
- return new Promise((resolve) => {
86
- const child = (0, node_child_process.spawn)(command, args, {
87
- cwd: options.cwd,
88
- env: mergedEnv(options.env),
89
- stdio: [
90
- "pipe",
91
- "pipe",
92
- "pipe"
93
- ]
94
- });
95
- const stdoutChunks = [];
96
- const stderrChunks = [];
97
- child.stdout.on("data", (chunk) => {
98
- stdoutChunks.push(chunk);
99
- writeChunk(options.mirrorStdout ?? "stdout", chunk);
100
- });
101
- child.stderr.on("data", (chunk) => {
102
- stderrChunks.push(chunk);
103
- writeChunk(options.mirrorStderr ?? "stderr", chunk);
104
- });
105
- child.on("error", (error) => {
106
- resolve({
107
- code: 127,
108
- stdout: Buffer.concat(stdoutChunks).toString("utf8"),
109
- stderr: `${Buffer.concat(stderrChunks).toString("utf8")}${error.message}\n`
110
- });
111
- });
112
- child.on("close", (code) => {
113
- resolve({
114
- code: code ?? 1,
115
- stdout: Buffer.concat(stdoutChunks).toString("utf8"),
116
- stderr: Buffer.concat(stderrChunks).toString("utf8")
117
- });
118
- });
119
- if (options.input !== undefined) {
120
- child.stdin.end(options.input);
121
- } else {
122
- child.stdin.end();
123
- }
124
- });
125
- }
126
- function runInteractive(command, args, options = {}) {
127
- return new Promise((resolve) => {
128
- const child = (0, node_child_process.spawn)(command, args, {
129
- cwd: options.cwd,
130
- env: mergedEnv(options.env),
131
- stdio: "inherit"
132
- });
133
- child.on("error", (error) => {
134
- process.stderr.write(`${error.message}\n`);
135
- resolve(127);
136
- });
137
- child.on("close", (code) => {
138
- resolve(code ?? 1);
139
- });
140
- });
141
- }
142
-
143
- //#endregion
144
- //#region src/doctor.ts
145
- function nodeVersionPasses(version) {
146
- const major = Number.parseInt(version.split(".")[0] ?? "", 10);
147
- return Number.isInteger(major) && major >= 24;
148
- }
149
- function check(name, pass, okMessage, failMessage) {
150
- return {
151
- name,
152
- level: pass ? "ok" : "fail",
153
- message: pass ? okMessage : failMessage
154
- };
155
- }
156
- async function commandWorks(command, args) {
157
- const result = await runBuffered(command, args, {
158
- mirrorStdout: false,
159
- mirrorStderr: false
160
- });
161
- return result.code === 0;
162
- }
163
- async function commandExists(command, args) {
164
- const result = await runBuffered(command, args, {
165
- mirrorStdout: false,
166
- mirrorStderr: false
167
- });
168
- return result.code !== 127;
169
- }
170
- async function runDoctorChecks(context) {
171
- const checks = [];
172
- const nodeVersion = process.versions.node;
173
- checks.push(check("node", nodeVersionPasses(nodeVersion), `Node ${nodeVersion}`, `Node ${nodeVersion}; expected >=24.0.0`));
174
- checks.push(check("devcontainers-cli", await packagedDevcontainerCliWorks(context), "Packaged @devcontainers/cli is available", "Packaged @devcontainers/cli is required but was not available"));
175
- checks.push(check("docker-cli", await commandWorks("docker", ["--version"]), "Docker CLI is available", "Docker CLI is required but was not available"));
176
- checks.push(check("docker-daemon", await commandWorks("docker", ["info"]), "Docker daemon is reachable", "Docker daemon is required but was not reachable"));
177
- checks.push(check("ssh", await commandExists("ssh", ["-V"]), "ssh is available", "ssh is required but was not available"));
178
- checks.push(check("ssh-keygen", await commandExists("ssh-keygen", ["-?"]), "ssh-keygen is available", "ssh-keygen is required but was not available"));
179
- checks.push(check("assets", (0, node_fs.existsSync)(context.assetsDevcontainerDir), `Devcontainer assets found at ${context.assetsDevcontainerDir}`, `Missing Boxdown devcontainer assets: ${context.assetsDevcontainerDir}`));
180
- if (await commandWorks("gh", ["--version"])) {
181
- const ghAuth = await commandWorks("gh", [
182
- "auth",
183
- "status",
184
- "--hostname",
185
- "github.com"
186
- ]);
187
- checks.push({
188
- name: "gh-auth",
189
- level: ghAuth ? "ok" : "warn",
190
- message: ghAuth ? "GitHub CLI auth is available" : "GitHub CLI is available but not authenticated"
191
- });
192
- } else {
193
- checks.push({
194
- name: "gh",
195
- level: "warn",
196
- message: "GitHub CLI is optional and was not available"
197
- });
198
- }
199
- return checks;
200
- }
201
- async function packagedDevcontainerCliWorks(context) {
202
- try {
203
- const cli = resolveDevcontainerCli(context);
204
- return await commandWorks(cli.command, [...cli.argsPrefix, "--version"]);
205
- } catch {
206
- return false;
207
- }
208
- }
209
- function doctorHasFailures(checks) {
210
- return checks.some((item) => item.level === "fail");
211
- }
212
- function formatDoctorText(checks) {
213
- const lines = ["Boxdown doctor", ""];
214
- for (const item of checks) {
215
- lines.push(`[${item.level}] ${item.name}: ${item.message}`);
216
- }
217
- lines.push("", doctorHasFailures(checks) ? "Result: failed" : "Result: ok");
218
- return `${lines.join("\n")}\n`;
219
- }
220
-
221
- //#endregion
222
- //#region src/jsonc.ts
223
- function stripJsonComments(input) {
224
- let output = "";
225
- let inString = false;
226
- let escaped = false;
227
- for (let index = 0; index < input.length; index += 1) {
228
- const char = input[index];
229
- const next = input[index + 1];
230
- if (char === undefined) {
231
- break;
232
- }
233
- if (inString) {
234
- output += char;
235
- if (escaped) {
236
- escaped = false;
237
- } else if (char === "\\") {
238
- escaped = true;
239
- } else if (char === "\"") {
240
- inString = false;
241
- }
242
- continue;
243
- }
244
- if (char === "\"") {
245
- inString = true;
246
- output += char;
247
- continue;
248
- }
249
- if (char === "/" && next === "/") {
250
- while (index < input.length && input[index] !== "\n") {
251
- index += 1;
252
- }
253
- if (input[index] === "\n") {
254
- output += "\n";
255
- }
256
- continue;
257
- }
258
- if (char === "/" && next === "*") {
259
- index += 2;
260
- while (index < input.length) {
261
- if (input[index] === "\n") {
262
- output += "\n";
263
- }
264
- if (input[index] === "*" && input[index + 1] === "/") {
265
- index += 1;
266
- break;
267
- }
268
- index += 1;
269
- }
270
- continue;
271
- }
272
- output += char;
273
- }
274
- return output;
275
- }
276
- function stripTrailingCommas(input) {
277
- let output = "";
278
- let inString = false;
279
- let escaped = false;
280
- for (let index = 0; index < input.length; index += 1) {
281
- const char = input[index];
282
- if (char === undefined) {
283
- break;
284
- }
285
- if (inString) {
286
- output += char;
287
- if (escaped) {
288
- escaped = false;
289
- } else if (char === "\\") {
290
- escaped = true;
291
- } else if (char === "\"") {
292
- inString = false;
293
- }
294
- continue;
295
- }
296
- if (char === "\"") {
297
- inString = true;
298
- output += char;
299
- continue;
300
- }
301
- if (char === ",") {
302
- let nextIndex = index + 1;
303
- while (/\s/.test(input[nextIndex] ?? "")) {
304
- nextIndex += 1;
305
- }
306
- if (input[nextIndex] === "}" || input[nextIndex] === "]") {
307
- continue;
308
- }
309
- }
310
- output += char;
311
- }
312
- return output;
313
- }
314
- function parseJsonc(input) {
315
- return JSON.parse(stripTrailingCommas(stripJsonComments(input)));
316
- }
317
-
318
- //#endregion
319
- //#region src/shell.ts
320
- function shellQuote(value) {
321
- if (value.length === 0) {
322
- return "''";
323
- }
324
- return `'${value.replaceAll("'", "'\\''")}'`;
325
- }
326
- function sshConfigQuote(value) {
327
- return `"${value.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"")}"`;
328
- }
329
- const DEFAULT_TTY_MAX_COLUMNS = 120;
330
- function interactiveShellEnvArgs(env = process.env) {
331
- return [
332
- `TERM=${env.TERM ?? "xterm-256color"}`,
333
- "COLORTERM=truecolor",
334
- `BOXDOWN_TTY_NORMALIZE=${env.BOXDOWN_TTY_NORMALIZE ?? "1"}`,
335
- `BOXDOWN_TTY_MAX_COLUMNS=${env.BOXDOWN_TTY_MAX_COLUMNS ?? String(120)}`
336
- ];
337
- }
338
- function interactiveTtySetupScript() {
339
- return [
340
- "if [ -t 0 ]; then",
341
- " case \"${BOXDOWN_TTY_NORMALIZE:-1}\" in",
342
- " 0|false|FALSE|no|NO|off|OFF) ;;",
343
- " *)",
344
- " max_columns=\"${BOXDOWN_TTY_MAX_COLUMNS:-120}\"",
345
- " if [ \"$max_columns\" -gt 0 ] 2>/dev/null; then",
346
- " set -- $(stty size 2>/dev/null || true)",
347
- " rows=\"${1:-}\"",
348
- " columns=\"${2:-}\"",
349
- " if [ -n \"$columns\" ] && [ \"$columns\" -gt \"$max_columns\" ] 2>/dev/null; then",
350
- " stty cols \"$max_columns\" 2>/dev/null || true",
351
- " export COLUMNS=\"$max_columns\"",
352
- " if [ -n \"$rows\" ]; then export LINES=\"$rows\"; fi",
353
- " printf \"Boxdown: terminal width clamped to %s columns (was %s). Set BOXDOWN_TTY_NORMALIZE=0 to disable.\\n\" \"$max_columns\" \"$columns\" >&2",
354
- " fi",
355
- " fi",
356
- " ;;",
357
- " esac",
358
- "fi"
359
- ].join("\n");
360
- }
361
- function interactiveShellScript() {
362
- return [interactiveTtySetupScript(), "exec bash -i"].join("\n");
363
- }
364
- function interactiveCommandScript() {
365
- return [interactiveTtySetupScript(), "exec \"$@\""].join("\n");
366
- }
367
-
368
- //#endregion
369
- //#region src/config.ts
370
- function readBaseDevcontainerConfig(assetsDevcontainerDir) {
371
- const configPath = (0, node_path.join)(assetsDevcontainerDir, "devcontainer.json");
372
- return parseJsonc((0, node_fs.readFileSync)(configPath, "utf8"));
373
- }
374
- function buildGeneratedDevcontainerConfig(context) {
375
- const baseConfig = readBaseDevcontainerConfig(context.assetsDevcontainerDir);
376
- const mounts = Array.isArray(baseConfig.mounts) ? baseConfig.mounts.filter((mount) => typeof mount === "string") : [];
377
- const boxdownMounts = [`type=bind,source=${context.assetsDevcontainerDir},target=${BOXDOWN_CONTAINER_DEVCONTAINER_DIR},readonly`, `type=bind,source=${context.sshPublicKeyRuntimeDir},target=${BOXDOWN_CONTAINER_SSH_DIR},readonly`];
378
- return {
379
- ...baseConfig,
380
- name: `Boxdown: ${context.workspaceBasename}`,
381
- mounts: [...mounts, ...boxdownMounts],
382
- initializeCommand: `BOXDOWN_WORKSPACE_FOLDER=${shellQuote(context.workspaceFolder)} bash ${shellQuote((0, node_path.join)(context.assetsDevcontainerDir, "hooks", "initialize.sh"))}`,
383
- postCreateCommand: `bash ${shellQuote(`${BOXDOWN_CONTAINER_DEVCONTAINER_DIR}/hooks/post-create.sh`)}`,
384
- postStartCommand: `bash ${shellQuote(`${BOXDOWN_CONTAINER_DEVCONTAINER_DIR}/hooks/post-start.sh`)}`,
385
- containerEnv: {
386
- ...baseConfig.containerEnv ?? {},
387
- BOXDOWN_CONTAINER_WORKSPACE_FOLDER: "/workspaces/${localWorkspaceFolderBasename}",
388
- BOXDOWN_WORKSPACE_BASENAME: "${localWorkspaceFolderBasename}",
389
- DEVCONTAINER_SSH_PUBLIC_KEY_FILE: BOXDOWN_CONTAINER_SSH_PUBLIC_KEY_PATH
390
- }
391
- };
392
- }
393
- function writeGeneratedDevcontainerConfig(context) {
394
- const config = buildGeneratedDevcontainerConfig(context);
395
- (0, node_fs.mkdirSync)(context.workspaceCacheDir, { recursive: true });
396
- (0, node_fs.writeFileSync)(context.generatedConfigPath, `${JSON.stringify(config, null, 2)}\n`);
397
- return config;
398
- }
399
- function publishContainerPortFromConfig(config) {
400
- return config.runArgs?.find((arg) => /^[0-9.]+::[0-9]+$/.test(arg))?.split("::")[1];
401
- }
402
-
403
- //#endregion
404
- //#region src/github-git-auth.ts
405
- function canonicalGithubRemoteUrl(remoteUrl) {
406
- const trimmed = remoteUrl.trim();
407
- const scpLike = /^git@github\.com:([^/]+)\/(.+)$/i.exec(trimmed);
408
- if (scpLike !== null) {
409
- return canonicalGithubUrlFromParts(scpLike[1], scpLike[2]);
410
- }
411
- try {
412
- const parsed = new URL(trimmed);
413
- const protocol = parsed.protocol.toLowerCase();
414
- const host = parsed.hostname.toLowerCase();
415
- if (host !== "github.com") {
416
- return undefined;
417
- }
418
- if (protocol === "ssh:" && parsed.username !== "git") {
419
- return undefined;
420
- }
421
- if (protocol !== "https:" && protocol !== "ssh:") {
422
- return undefined;
423
- }
424
- const parts = parsed.pathname.split("/").filter((part) => part.length > 0);
425
- if (parts.length !== 2) {
426
- return undefined;
427
- }
428
- const [owner, repo] = parts;
429
- return canonicalGithubUrlFromParts(owner, repo);
430
- } catch {
431
- return undefined;
432
- }
433
- }
434
- function canonicalGithubUrlFromParts(owner, repo) {
435
- if (owner === undefined || repo === undefined) {
436
- return undefined;
437
- }
438
- const normalizedOwner = owner.trim();
439
- const normalizedRepo = repo.trim().replace(/\/+$/, "").replace(/\.git$/i, "");
440
- if (normalizedOwner.length === 0 || normalizedRepo.length === 0 || normalizedOwner.includes("/") || normalizedRepo.includes("/")) {
441
- return undefined;
442
- }
443
- return `https://github.com/${normalizedOwner}/${normalizedRepo}.git`;
444
- }
445
- async function configureWorkspaceGithubGitAuth(workspaceFolder) {
446
- const insideWorkTree = await runWorkspaceGit(workspaceFolder, ["rev-parse", "--is-inside-work-tree"]);
447
- if (insideWorkTree.code !== 0 || insideWorkTree.stdout.trim() !== "true") {
448
- return false;
449
- }
450
- const remoteConfig = await runWorkspaceGit(workspaceFolder, [
451
- "config",
452
- "--local",
453
- "--get-regexp",
454
- "^remote\\..*\\.url$"
455
- ]);
456
- if (remoteConfig.code !== 0) {
457
- return false;
458
- }
459
- const githubRemotes = parseGithubRemoteConfig(remoteConfig.stdout);
460
- if (githubRemotes.length === 0) {
461
- return false;
462
- }
463
- for (const remote of githubRemotes) {
464
- const fetchUrl = await runWorkspaceGit(workspaceFolder, [
465
- "remote",
466
- "set-url",
467
- remote.name,
468
- remote.url
469
- ]);
470
- if (fetchUrl.code !== 0) {
471
- return false;
472
- }
473
- const pushUrl = await runWorkspaceGit(workspaceFolder, [
474
- "remote",
475
- "set-url",
476
- "--push",
477
- remote.name,
478
- remote.url
479
- ]);
480
- if (pushUrl.code !== 0) {
481
- return false;
482
- }
483
- }
484
- await runWorkspaceGit(workspaceFolder, [
485
- "config",
486
- "--local",
487
- "--unset-all",
488
- "credential.https://github.com.helper"
489
- ]);
490
- const resetHelper = await runWorkspaceGit(workspaceFolder, [
491
- "config",
492
- "--local",
493
- "--add",
494
- "credential.https://github.com.helper",
495
- ""
496
- ]);
497
- if (resetHelper.code !== 0) {
498
- return false;
499
- }
500
- const ghHelper = await runWorkspaceGit(workspaceFolder, [
501
- "config",
502
- "--local",
503
- "--add",
504
- "credential.https://github.com.helper",
505
- "!gh auth git-credential"
506
- ]);
507
- if (ghHelper.code !== 0) {
508
- return false;
509
- }
510
- for (const remoteUrl of new Set(githubRemotes.map((remote) => remote.url))) {
511
- const rewrite = await runWorkspaceGit(workspaceFolder, [
512
- "config",
513
- "--local",
514
- "--replace-all",
515
- `url.${remoteUrl}.insteadOf`,
516
- remoteUrl
517
- ]);
518
- if (rewrite.code !== 0) {
519
- return false;
520
- }
521
- }
522
- return true;
523
- }
524
- function parseGithubRemoteConfig(configOutput) {
525
- const remotes = [];
526
- for (const line of configOutput.split(/\r?\n/)) {
527
- const match = /^remote\.(.+)\.url\s+(.+)$/.exec(line);
528
- if (match === null) {
529
- continue;
530
- }
531
- const name = match[1];
532
- const rawUrl = match[2];
533
- if (name === undefined || rawUrl === undefined) {
534
- continue;
535
- }
536
- const url = canonicalGithubRemoteUrl(rawUrl);
537
- if (url !== undefined) {
538
- remotes.push({
539
- name,
540
- url
541
- });
542
- }
543
- }
544
- return remotes;
545
- }
546
- async function runWorkspaceGit(workspaceFolder, args) {
547
- return runBuffered("git", args, {
548
- cwd: workspaceFolder,
549
- mirrorStdout: false,
550
- mirrorStderr: false
551
- });
552
- }
553
-
554
- //#endregion
555
- //#region src/ssh-key.ts
556
- async function ensureHostSshKey(context, quiet = false) {
557
- (0, node_fs.mkdirSync)(context.sshKeyDir, {
558
- recursive: true,
559
- mode: 448
560
- });
561
- if (!(0, node_fs.existsSync)(context.sshKeyPath)) {
562
- if (!quiet) {
563
- process.stderr.write(`Generating Boxdown SSH identity: ${context.sshKeyPath}\n`);
564
- }
565
- const result = await runBuffered("ssh-keygen", [
566
- "-t",
567
- "ed25519",
568
- "-f",
569
- context.sshKeyPath,
570
- "-N",
571
- "",
572
- "-C",
573
- `${context.workspaceBasename}-devcontainer`
574
- ], {
575
- mirrorStdout: quiet ? false : "stderr",
576
- mirrorStderr: "stderr"
577
- });
578
- if (result.code !== 0) {
579
- throw new Error(`ssh-keygen failed while creating ${context.sshKeyPath}`);
580
- }
581
- }
582
- if (!(0, node_fs.existsSync)(context.sshPublicKeyPath)) {
583
- const result = await runBuffered("ssh-keygen", [
584
- "-y",
585
- "-f",
586
- context.sshKeyPath
587
- ], {
588
- mirrorStdout: false,
589
- mirrorStderr: "stderr"
590
- });
591
- if (result.code !== 0) {
592
- throw new Error(`ssh-keygen failed while deriving ${context.sshPublicKeyPath}`);
593
- }
594
- (0, node_fs.writeFileSync)(context.sshPublicKeyPath, result.stdout);
595
- }
596
- (0, node_fs.mkdirSync)(context.sshPublicKeyRuntimeDir, {
597
- recursive: true,
598
- mode: 493
599
- });
600
- (0, node_fs.writeFileSync)(context.sshPublicKeyRuntimePath, (0, node_fs.readFileSync)(context.sshPublicKeyPath, "utf8"));
601
- (0, node_fs.chmodSync)(context.sshKeyPath, 384);
602
- (0, node_fs.chmodSync)(context.sshPublicKeyPath, 420);
603
- (0, node_fs.chmodSync)(context.sshPublicKeyRuntimePath, 420);
604
- }
605
-
606
- //#endregion
607
- //#region src/ssh-config.ts
608
- function defaultSshAlias(workspaceBasename) {
609
- return `${workspaceBasename}-devcontainer`;
610
- }
611
- function validateSshAlias(alias) {
612
- if (!/^[A-Za-z0-9_.-]+$/.test(alias)) {
613
- throw new Error(`SSH alias contains unsupported characters: ${alias}`);
614
- }
615
- }
616
- function defaultSshConfigPath(env = process.env) {
617
- return env.BOXDOWN_SSH_CONFIG ?? env.DEVCONTAINER_SSH_CONFIG ?? (0, node_path.join)(env.HOME ?? "", ".ssh", "config");
618
- }
619
- function buildProxyCommand(context, alias) {
620
- const cliPath = (0, node_path.join)(context.packageRoot, "dist", "bin", "cli.cjs");
621
- return `${shellQuote(process.execPath)} ${shellQuote(cliPath)} ssh-proxy --workspace ${shellQuote(context.workspaceFolder)} --alias ${shellQuote(alias)}`;
622
- }
623
- function buildSshConfigBlock(context, alias) {
624
- validateSshAlias(alias);
625
- return [
626
- `# BEGIN ${alias} boxdown devcontainer ssh`,
627
- `Host ${alias}`,
628
- ` HostName ${alias}`,
629
- " User node",
630
- " IdentityFile none",
631
- ` IdentityFile ${sshConfigQuote(context.sshKeyPath)}`,
632
- " IdentitiesOnly yes",
633
- ` ProxyCommand ${buildProxyCommand(context, alias)}`,
634
- " StrictHostKeyChecking no",
635
- " UserKnownHostsFile /dev/null",
636
- " LogLevel ERROR",
637
- `# END ${alias} boxdown devcontainer ssh`,
638
- ""
639
- ].join("\n");
640
- }
641
- function replaceSshConfigBlock(existingConfig, alias, block) {
642
- const begin = `# BEGIN ${alias} boxdown devcontainer ssh`;
643
- const end = `# END ${alias} boxdown devcontainer ssh`;
644
- const lines = existingConfig.split(/\r?\n/);
645
- const nextLines = [];
646
- let skipping = false;
647
- for (const line of lines) {
648
- if (line === begin) {
649
- skipping = true;
650
- continue;
651
- }
652
- if (line === end) {
653
- skipping = false;
654
- continue;
655
- }
656
- if (!skipping) {
657
- nextLines.push(line);
658
- }
659
- }
660
- while (nextLines.length > 0 && nextLines[nextLines.length - 1] === "") {
661
- nextLines.pop();
662
- }
663
- return `${nextLines.join("\n")}${nextLines.length > 0 ? "\n\n" : ""}${block}`;
664
- }
665
- async function installSshConfig(context, alias, options = {}) {
666
- validateSshAlias(alias);
667
- await ensureHostSshKey(context, options.quiet ?? false);
668
- const sshConfigPath = options.configPath ?? defaultSshConfigPath();
669
- const sshConfigDir = (0, node_path.dirname)(sshConfigPath);
670
- (0, node_fs.mkdirSync)(sshConfigDir, {
671
- recursive: true,
672
- mode: 448
673
- });
674
- if (!(0, node_fs.existsSync)(sshConfigPath)) {
675
- (0, node_fs.writeFileSync)(sshConfigPath, "");
676
- }
677
- (0, node_fs.chmodSync)(sshConfigDir, 448);
678
- (0, node_fs.chmodSync)(sshConfigPath, 384);
679
- const existingConfig = (0, node_fs.readFileSync)(sshConfigPath, "utf8");
680
- const block = buildSshConfigBlock(context, alias);
681
- const nextConfig = replaceSshConfigBlock(existingConfig, alias, block);
682
- if (nextConfig !== existingConfig) {
683
- (0, node_fs.writeFileSync)(sshConfigPath, nextConfig);
684
- if (!options.quiet) {
685
- process.stdout.write(`Installed SSH alias: ${alias}\n`);
686
- }
687
- } else if (!options.quiet) {
688
- process.stdout.write(`SSH alias already up to date: ${alias}\n`);
689
- }
690
- (0, node_fs.chmodSync)(sshConfigPath, 384);
691
- if (!options.quiet) {
692
- process.stdout.write(`SSH config: ${sshConfigPath}\n`);
693
- process.stdout.write(`Identity file: ${context.sshKeyPath}\n\n`);
694
- process.stdout.write(`Validate with:\n ssh ${alias} 'whoami && pwd'\n`);
695
- }
696
- }
697
-
698
- //#endregion
699
- //#region src/status.ts
700
- function dockerLabelsFromString(labels) {
701
- return Object.fromEntries(labels.split(",").map((label) => label.trim()).filter((label) => label.length > 0).map((label) => {
702
- const separator = label.indexOf("=");
703
- return separator === -1 ? [label, ""] : [label.slice(0, separator), label.slice(separator + 1)];
704
- }));
705
- }
706
- function parseDockerPsJsonLines(output) {
707
- const lines = output.split(/\r?\n/).filter((line) => line.trim().length > 0);
708
- return lines.map((line) => {
709
- let parsed;
710
- try {
711
- parsed = JSON.parse(line);
712
- } catch {
713
- throw new Error(`Could not parse docker ps output: ${line}`);
714
- }
715
- if (typeof parsed.ID !== "string" || parsed.ID.length === 0) {
716
- throw new Error(`Docker ps output is missing container ID: ${line}`);
717
- }
718
- return {
719
- id: parsed.ID,
720
- name: typeof parsed.Names === "string" && parsed.Names.length > 0 ? parsed.Names : undefined,
721
- state: typeof parsed.State === "string" && parsed.State.length > 0 ? parsed.State : undefined,
722
- status: typeof parsed.Status === "string" && parsed.Status.length > 0 ? parsed.Status : undefined,
723
- localFolder: typeof parsed.Labels === "string" ? dockerLabelsFromString(parsed.Labels)["devcontainer.local_folder"] : undefined
724
- };
725
- });
726
- }
727
- function readFileUtf8(path) {
728
- return (0, node_fs.readFileSync)(path, "utf8");
729
- }
730
- function managedSshBlockMarkers(alias) {
731
- return {
732
- begin: `# BEGIN ${alias} boxdown devcontainer ssh`,
733
- end: `# END ${alias} boxdown devcontainer ssh`
734
- };
735
- }
736
- function findManagedSshConfigBlock(config, alias) {
737
- const { begin, end } = managedSshBlockMarkers(alias);
738
- const beginIndex = config.indexOf(begin);
739
- if (beginIndex === -1) {
740
- return undefined;
741
- }
742
- const endIndex = config.indexOf(end, beginIndex);
743
- if (endIndex === -1) {
744
- return "";
745
- }
746
- const afterEndMarkerIndex = endIndex + end.length;
747
- const afterEndLineIndex = config[afterEndMarkerIndex] === "\n" ? afterEndMarkerIndex + 1 : afterEndMarkerIndex;
748
- return config.slice(beginIndex, afterEndLineIndex);
749
- }
750
- function inspectSshConfigStatus(context, alias, configPath, exists, readFile = readFileUtf8) {
751
- const configExists = exists(configPath);
752
- if (!configExists) {
753
- return {
754
- configPath,
755
- configExists,
756
- managedBlockState: "missing"
757
- };
758
- }
759
- const config = readFile(configPath);
760
- const managedBlock = findManagedSshConfigBlock(config, alias);
761
- if (managedBlock === undefined) {
762
- return {
763
- configPath,
764
- configExists,
765
- managedBlockState: "missing"
766
- };
767
- }
768
- return {
769
- configPath,
770
- configExists,
771
- managedBlockState: managedBlock === buildSshConfigBlock(context, alias) ? "installed" : "outdated"
772
- };
773
- }
774
- function createStatusInfo(context, alias, container, exists, options = {}) {
775
- const state = container?.state?.toLowerCase();
776
- const sshConfig = inspectSshConfigStatus(context, alias, options.sshConfigPath ?? defaultSshConfigPath(), exists, options.readFile);
777
- return {
778
- workspace: {
779
- folder: context.workspaceFolder,
780
- basename: context.workspaceBasename,
781
- id: context.workspaceId
782
- },
783
- ssh: {
784
- alias,
785
- aliasSource: options.aliasSource ?? "provided",
786
- configPath: sshConfig.configPath,
787
- configExists: sshConfig.configExists,
788
- managedBlockState: sshConfig.managedBlockState,
789
- keyPath: context.sshKeyPath,
790
- keyExists: exists(context.sshKeyPath),
791
- publicKeyPath: context.sshPublicKeyPath,
792
- publicKeyExists: exists(context.sshPublicKeyPath),
793
- publicKeyRuntimePath: context.sshPublicKeyRuntimePath,
794
- publicKeyRuntimeExists: exists(context.sshPublicKeyRuntimePath)
795
- },
796
- paths: {
797
- cacheRoot: context.cacheRoot,
798
- dataRoot: context.dataRoot,
799
- workspaceCacheDir: context.workspaceCacheDir,
800
- workspaceDataDir: context.workspaceDataDir,
801
- generatedConfigPath: context.generatedConfigPath,
802
- generatedConfigExists: exists(context.generatedConfigPath),
803
- assetsDevcontainerDir: context.assetsDevcontainerDir,
804
- assetsDevcontainerExists: exists(context.assetsDevcontainerDir)
805
- },
806
- container: {
807
- found: container !== undefined,
808
- running: state === "running",
809
- id: container?.id,
810
- name: container?.name,
811
- state: container?.state,
812
- status: container?.status
813
- }
814
- };
815
- }
816
- function statusIsHealthy(status) {
817
- return status.paths.generatedConfigExists && status.paths.assetsDevcontainerExists && status.ssh.keyExists && status.ssh.publicKeyExists && status.ssh.publicKeyRuntimeExists && status.container.found && status.container.running;
818
- }
819
- const color = {
820
- green: "\x1B[32m",
821
- red: "\x1B[31m",
822
- reset: "\x1B[0m"
823
- };
824
- function colorize(value, colorName, enabled) {
825
- if (!enabled) {
826
- return value;
827
- }
828
- return `${color[colorName]}${value}${color.reset}`;
829
- }
830
- function existenceText(value, colorEnabled) {
831
- return colorize(value ? "exists" : "missing", value ? "green" : "red", colorEnabled);
832
- }
833
- function runningText(value, colorEnabled) {
834
- return colorize(value ? "yes" : "no", value ? "green" : "red", colorEnabled);
835
- }
836
- function managedBlockText(state, colorEnabled) {
837
- return colorize(state, state === "installed" ? "green" : "red", colorEnabled);
838
- }
839
- function aliasSourceText(source) {
840
- return source === "default" ? "computed default" : "provided";
841
- }
842
- function installedText(state) {
843
- return state === "installed" ? "installed" : "not installed";
844
- }
845
- function stateText(state, healthy, colorEnabled) {
846
- return colorize(state, healthy ? "green" : "red", colorEnabled);
847
- }
848
- function formatStatusText(status, options = {}) {
849
- const colorEnabled = options.color ?? false;
850
- const containerState = status.container.found ? status.container.state ?? "unknown" : "absent";
851
- const healthy = statusIsHealthy(status);
852
- const lines = [
853
- "Boxdown status",
854
- "",
855
- "Workspace:",
856
- ` Path: ${status.workspace.folder}`,
857
- ` Name: ${status.workspace.basename}`,
858
- ` ID: ${status.workspace.id}`,
859
- ` SSH alias: ${status.ssh.alias} (${aliasSourceText(status.ssh.aliasSource)}; ${installedText(status.ssh.managedBlockState)})`,
860
- "",
861
- "Paths:",
862
- ` Cache root: ${status.paths.cacheRoot}`,
863
- ` Data root: ${status.paths.dataRoot}`,
864
- ` Workspace cache: ${status.paths.workspaceCacheDir}`,
865
- ` Workspace data: ${status.paths.workspaceDataDir}`,
866
- ` Generated config: ${status.paths.generatedConfigPath} (${existenceText(status.paths.generatedConfigExists, colorEnabled)})`,
867
- ` Devcontainer assets: ${status.paths.assetsDevcontainerDir} (${existenceText(status.paths.assetsDevcontainerExists, colorEnabled)})`,
868
- "",
869
- "SSH:",
870
- ` SSH config: ${status.ssh.configPath} (${existenceText(status.ssh.configExists, colorEnabled)})`,
871
- ` Boxdown SSH block: ${managedBlockText(status.ssh.managedBlockState, colorEnabled)}`,
872
- ` Private key: ${status.ssh.keyPath} (${existenceText(status.ssh.keyExists, colorEnabled)})`,
873
- ` Public key: ${status.ssh.publicKeyPath} (${existenceText(status.ssh.publicKeyExists, colorEnabled)})`,
874
- ` Runtime public key: ${status.ssh.publicKeyRuntimePath} (${existenceText(status.ssh.publicKeyRuntimeExists, colorEnabled)})`,
875
- "",
876
- "Container:",
877
- ` State: ${stateText(containerState, healthy, colorEnabled)}`,
878
- ` Running: ${runningText(status.container.running, colorEnabled)}`
879
- ];
880
- if (status.container.id !== undefined) {
881
- lines.push(` ID: ${status.container.id}`);
882
- }
883
- if (status.container.name !== undefined) {
884
- lines.push(` Name: ${status.container.name}`);
885
- }
886
- if (status.container.status !== undefined) {
887
- lines.push(` Docker status: ${status.container.status}`);
888
- }
889
- return `${lines.join("\n")}\n`;
890
- }
891
-
892
- //#endregion
893
- //#region src/devcontainer.ts
894
- function devcontainerWorkspaceArgs(context) {
895
- return [
896
- "--workspace-folder",
897
- context.workspaceFolder,
898
- "--override-config",
899
- context.generatedConfigPath
900
- ];
901
- }
902
- function log(message, proxyMode = false) {
903
- if (proxyMode) {
904
- process.stderr.write(`${message}\n`);
905
- } else {
906
- process.stdout.write(`${message}\n`);
907
- }
908
- }
909
- function parseContainerIdFromUpOutput(output) {
910
- return /"containerId"\s*:\s*"([^"]+)"/.exec(output)?.[1];
911
- }
912
- async function findWorkspaceContainer(context) {
913
- const result = await runBuffered("docker", [
914
- "ps",
915
- "-a",
916
- "--filter",
917
- `label=devcontainer.local_folder=${context.workspaceFolder}`,
918
- "--format",
919
- "{{json .}}"
920
- ], {
921
- mirrorStdout: false,
922
- mirrorStderr: false
923
- });
924
- if (result.code !== 0) {
925
- throw new Error(`Could not inspect devcontainer for ${context.workspaceFolder}`);
926
- }
927
- return parseDockerPsJsonLines(result.stdout)[0];
928
- }
929
- async function listWorkspaceContainers() {
930
- const result = await runBuffered("docker", [
931
- "ps",
932
- "-a",
933
- "--filter",
934
- "label=devcontainer.local_folder",
935
- "--format",
936
- "{{json .}}"
937
- ], {
938
- mirrorStdout: false,
939
- mirrorStderr: false
940
- });
941
- if (result.code !== 0) {
942
- return undefined;
943
- }
944
- return parseDockerPsJsonLines(result.stdout);
945
- }
946
- async function findRunningContainerId(context) {
947
- const result = await runBuffered("docker", [
948
- "ps",
949
- "--filter",
950
- `label=devcontainer.local_folder=${context.workspaceFolder}`,
951
- "--format",
952
- "{{.ID}}"
953
- ], {
954
- mirrorStdout: false,
955
- mirrorStderr: false
956
- });
957
- if (result.code !== 0) {
958
- return undefined;
959
- }
960
- return result.stdout.split(/\r?\n/).find((line) => line.length > 0);
961
- }
962
- async function stopWorkspaceContainer(context) {
963
- const container = await findWorkspaceContainer(context);
964
- if (container === undefined) {
965
- process.stdout.write(`No devcontainer found for: ${context.workspaceFolder}\n`);
966
- return;
967
- }
968
- if (container.state?.toLowerCase() !== "running") {
969
- process.stdout.write(`Devcontainer is not running for: ${context.workspaceFolder}\n`);
970
- return;
971
- }
972
- const result = await runBuffered("docker", ["stop", container.id], {
973
- mirrorStdout: false,
974
- mirrorStderr: false
975
- });
976
- if (result.code !== 0) {
977
- throw new Error(`Could not stop devcontainer ${container.id}`);
978
- }
979
- process.stdout.write(`Stopped devcontainer: ${container.id}\n`);
980
- }
981
- async function removeWorkspaceContainer(context) {
982
- const container = await findWorkspaceContainer(context);
983
- if (container === undefined) {
984
- process.stdout.write(`No devcontainer found for: ${context.workspaceFolder}\n`);
985
- return;
986
- }
987
- const result = await runBuffered("docker", [
988
- "rm",
989
- "-f",
990
- container.id
991
- ], {
992
- mirrorStdout: false,
993
- mirrorStderr: false
994
- });
995
- if (result.code !== 0) {
996
- throw new Error(`Could not remove devcontainer ${container.id}`);
997
- }
998
- process.stdout.write(`Removed devcontainer: ${container.id}\n`);
999
- }
1000
- async function startDevcontainer(context, options = {}) {
1001
- await ensureHostSshKey(context, options.proxyMode ?? false);
1002
- writeGeneratedDevcontainerConfig(context);
1003
- if (options.reuseRunning === true && options.recreate !== true) {
1004
- const runningContainerId = await findRunningContainerId(context);
1005
- if (runningContainerId !== undefined) {
1006
- log(`Using running devcontainer for: ${context.workspaceFolder}`, options.proxyMode);
1007
- return runningContainerId;
1008
- }
1009
- }
1010
- const cli = resolveDevcontainerCli(context);
1011
- log(`Starting devcontainer for: ${context.workspaceFolder}`, options.proxyMode);
1012
- const args = ["up", ...devcontainerWorkspaceArgs(context)];
1013
- if (options.recreate === true) {
1014
- args.push("--remove-existing-container");
1015
- log("Removing existing dev container so create-time settings apply.", options.proxyMode);
1016
- }
1017
- const result = await runBuffered(cli.command, [...cli.argsPrefix, ...args], {
1018
- mirrorStdout: options.proxyMode === true ? "stderr" : "stdout",
1019
- mirrorStderr: "stderr"
1020
- });
1021
- if (result.code !== 0) {
1022
- throw new Error(`devcontainer up failed for ${context.workspaceFolder}`);
1023
- }
1024
- const containerId = parseContainerIdFromUpOutput(`${result.stdout}\n${result.stderr}`) ?? await findRunningContainerId(context);
1025
- if (containerId === undefined) {
1026
- throw new Error(`Could not resolve devcontainer ID for ${context.workspaceFolder}`);
1027
- }
1028
- return containerId;
1029
- }
1030
- async function printPortHint(context, containerId) {
1031
- const config = buildGeneratedDevcontainerConfig(context);
1032
- const containerPort = publishContainerPortFromConfig(config);
1033
- if (containerPort === undefined) {
1034
- process.stderr.write("Warning: could not find a runArgs publish port.\n");
1035
- return;
1036
- }
1037
- const result = await runBuffered("docker", [
1038
- "port",
1039
- containerId,
1040
- `${containerPort}/tcp`
1041
- ], {
1042
- mirrorStdout: false,
1043
- mirrorStderr: false
1044
- });
1045
- const hostBinding = result.stdout.split(/\r?\n/).find((line) => line.length > 0);
1046
- if (result.code === 0 && hostBinding !== undefined) {
1047
- process.stdout.write(`\nDev server available at: http://${hostBinding}\n\n`);
1048
- } else {
1049
- process.stderr.write(`Warning: container is running but port ${containerPort}/tcp is not mapped.\n`);
1050
- }
1051
- }
1052
- async function openShell(context) {
1053
- const cli = resolveDevcontainerCli(context);
1054
- process.stdout.write("Dropping into container shell...\n");
1055
- return runInteractive(cli.command, [
1056
- ...cli.argsPrefix,
1057
- "exec",
1058
- ...devcontainerWorkspaceArgs(context),
1059
- "--",
1060
- "env",
1061
- ...interactiveShellEnvArgs(),
1062
- "bash",
1063
- "-c",
1064
- interactiveShellScript()
1065
- ]);
1066
- }
1067
- function codingAgentDevcontainerExecArgs(context, agent, agentArgs = []) {
1068
- return [
1069
- "exec",
1070
- ...devcontainerWorkspaceArgs(context),
1071
- "--",
1072
- "env",
1073
- ...interactiveShellEnvArgs(),
1074
- "bash",
1075
- "-c",
1076
- interactiveCommandScript(),
1077
- "boxdown-agent",
1078
- codingAgentBinary(agent),
1079
- ...agentArgs
1080
- ];
1081
- }
1082
- async function openCodingAgentCli(context, agent, agentArgs = []) {
1083
- const cli = resolveDevcontainerCli(context);
1084
- process.stdout.write(`Dropping into ${codingAgentBinary(agent)} inside the devcontainer...\n`);
1085
- return runInteractive(cli.command, [...cli.argsPrefix, ...codingAgentDevcontainerExecArgs(context, agent, agentArgs)]);
1086
- }
1087
- async function ensureContainerSshRuntime(context) {
1088
- const cli = resolveDevcontainerCli(context);
1089
- const result = await runBuffered(cli.command, [
1090
- ...cli.argsPrefix,
1091
- "exec",
1092
- ...devcontainerWorkspaceArgs(context),
1093
- "--",
1094
- "bash",
1095
- `${BOXDOWN_CONTAINER_DEVCONTAINER_DIR}/utils/ssh-bootstrap.sh`,
1096
- "runtime"
1097
- ], {
1098
- mirrorStdout: "stderr",
1099
- mirrorStderr: "stderr"
1100
- });
1101
- if (result.code !== 0) {
1102
- throw new Error("Failed to prepare devcontainer SSH runtime");
1103
- }
1104
- }
1105
- async function refreshContainerCodingAgentClis(context, proxyMode = false, agents = []) {
1106
- const cli = resolveDevcontainerCli(context);
1107
- const result = await runBuffered(cli.command, [
1108
- ...cli.argsPrefix,
1109
- "exec",
1110
- ...devcontainerWorkspaceArgs(context),
1111
- "--",
1112
- "bash",
1113
- `${BOXDOWN_CONTAINER_DEVCONTAINER_DIR}/utils/coding-agent-cli-update.sh`,
1114
- "maybe-update",
1115
- ...agents
1116
- ], {
1117
- mirrorStdout: proxyMode ? "stderr" : "stdout",
1118
- mirrorStderr: "stderr"
1119
- });
1120
- if (result.code !== 0) {
1121
- process.stderr.write("Warning: could not refresh one or more coding-agent CLIs inside the devcontainer.\n");
1122
- }
1123
- }
1124
- async function runSshdProxy(containerId) {
1125
- return runInteractive("docker", [
1126
- "exec",
1127
- "-i",
1128
- containerId,
1129
- "/usr/sbin/sshd",
1130
- "-i",
1131
- "-o",
1132
- "LogLevel=QUIET",
1133
- "-o",
1134
- "PubkeyAuthentication=yes",
1135
- "-o",
1136
- "PasswordAuthentication=no",
1137
- "-o",
1138
- "KbdInteractiveAuthentication=no",
1139
- "-o",
1140
- "AllowTcpForwarding=yes",
1141
- "-o",
1142
- "AllowStreamLocalForwarding=yes",
1143
- "-o",
1144
- "PermitTTY=yes"
1145
- ]);
1146
- }
1147
- async function hostGhTokenOrEmpty() {
1148
- const result = await runBuffered("gh", ["auth", "token"], {
1149
- mirrorStdout: false,
1150
- mirrorStderr: false
1151
- });
1152
- if (result.code !== 0) {
1153
- return "";
1154
- }
1155
- return result.stdout.trim();
1156
- }
1157
- async function refreshContainerGhAuth(context) {
1158
- await ensureHostSshKey(context, true);
1159
- writeGeneratedDevcontainerConfig(context);
1160
- const token = await hostGhTokenOrEmpty();
1161
- if (token.length === 0) {
1162
- return;
1163
- }
1164
- const cli = resolveDevcontainerCli(context);
1165
- const login = await runBuffered(cli.command, [
1166
- ...cli.argsPrefix,
1167
- "exec",
1168
- ...devcontainerWorkspaceArgs(context),
1169
- "--",
1170
- "gh",
1171
- "auth",
1172
- "login",
1173
- "--hostname",
1174
- "github.com",
1175
- "--git-protocol",
1176
- "https",
1177
- "--with-token",
1178
- "--insecure-storage"
1179
- ], {
1180
- input: `${token}\n`,
1181
- mirrorStdout: false,
1182
- mirrorStderr: false
1183
- });
1184
- if (login.code !== 0) {
1185
- process.stderr.write("Warning: could not refresh GitHub CLI auth inside the devcontainer.\n");
1186
- return;
1187
- }
1188
- const gitAuthConfigured = await configureWorkspaceGithubGitAuth(context.workspaceFolder);
1189
- if (!gitAuthConfigured) {
1190
- process.stderr.write("Warning: GitHub CLI auth refreshed, but GitHub Git auth was not configured for this workspace.\n");
1191
- }
1192
- const verify = await runBuffered(cli.command, [
1193
- ...cli.argsPrefix,
1194
- "exec",
1195
- ...devcontainerWorkspaceArgs(context),
1196
- "--",
1197
- "gh",
1198
- "auth",
1199
- "status",
1200
- "--hostname",
1201
- "github.com"
1202
- ], {
1203
- mirrorStdout: false,
1204
- mirrorStderr: false
1205
- });
1206
- if (verify.code === 0) {
1207
- process.stdout.write("GitHub CLI auth refreshed inside the devcontainer.\n");
1208
- } else {
1209
- process.stderr.write("Warning: GitHub CLI auth refresh completed, but verification failed.\n");
1210
- }
1211
- }
1212
-
1213
- //#endregion
1214
- //#region src/list.ts
1215
- function containerState(container, dockerAvailable) {
1216
- if (!dockerAvailable) {
1217
- return "unknown";
1218
- }
1219
- return container?.state?.toLowerCase() ?? "absent";
1220
- }
1221
- function displayState(repoExists, container, dockerAvailable) {
1222
- if (!repoExists) {
1223
- return "missing";
1224
- }
1225
- return containerState(container, dockerAvailable);
1226
- }
1227
- function createWorkspaceListEntries(metadata, containers, exists) {
1228
- const dockerAvailable = containers !== undefined;
1229
- const containersByWorkspace = new Map((containers ?? []).filter((container) => container.localFolder !== undefined).map((container) => [container.localFolder, container]));
1230
- return metadata.map((item) => {
1231
- const container = containersByWorkspace.get(item.workspaceFolder);
1232
- const repoExists = exists(item.workspaceFolder);
1233
- const state = displayState(repoExists, container, dockerAvailable);
1234
- return {
1235
- ...item,
1236
- repoExists,
1237
- state,
1238
- container: {
1239
- found: container !== undefined,
1240
- running: container?.state?.toLowerCase() === "running",
1241
- id: container?.id,
1242
- name: container?.name,
1243
- state: container?.state ?? (dockerAvailable ? undefined : "unknown"),
1244
- status: container?.status
1245
- }
1246
- };
1247
- }).sort((a, b) => a.workspaceBasename.localeCompare(b.workspaceBasename) || a.workspaceFolder.localeCompare(b.workspaceFolder));
1248
- }
1249
- function pad(value, width) {
1250
- return value.padEnd(width, " ");
1251
- }
1252
- function containerLabel(entry) {
1253
- if (!entry.container.found) {
1254
- return "-";
1255
- }
1256
- return entry.container.name ?? entry.container.id ?? "-";
1257
- }
1258
- function formatWorkspaceListText(entries) {
1259
- if (entries.length === 0) {
1260
- return "Boxdown list\n\nNo Boxdown workspaces found.\n";
1261
- }
1262
- const rows = entries.map((entry) => [
1263
- entry.state,
1264
- entry.workspaceBasename,
1265
- entry.workspaceFolder,
1266
- entry.sshAlias,
1267
- containerLabel(entry)
1268
- ]);
1269
- const headers = [
1270
- "STATE",
1271
- "REPO",
1272
- "PATH",
1273
- "SSH ALIAS",
1274
- "CONTAINER"
1275
- ];
1276
- const widths = headers.map((header, index) => Math.max(header.length, ...rows.map((row) => row[index]?.length ?? 0)));
1277
- const lines = [
1278
- "Boxdown list",
1279
- "",
1280
- headers.map((header, index) => pad(header, widths[index] ?? header.length)).join(" "),
1281
- rows.map((row) => row.map((value, index) => pad(value, widths[index] ?? value.length)).join(" ")).join("\n")
1282
- ];
1283
- return `${lines.join("\n")}\n`;
1284
- }
1285
-
1286
- //#endregion
1287
- //#region src/metadata.ts
1288
- const WORKSPACE_METADATA_VERSION = 1;
1289
- function isWorkspaceMetadata(value) {
1290
- if (value === null || typeof value !== "object") {
1291
- return false;
1292
- }
1293
- const candidate = value;
1294
- return candidate.version === 1 && typeof candidate.workspaceId === "string" && typeof candidate.workspaceFolder === "string" && typeof candidate.workspaceBasename === "string" && typeof candidate.sshAlias === "string" && typeof candidate.firstSeenAt === "string" && typeof candidate.lastSeenAt === "string";
1295
- }
1296
- function workspaceMetadataPath(context) {
1297
- return (0, node_path.join)(context.workspaceDataDir, "metadata.json");
1298
- }
1299
- function readWorkspaceMetadataFile(path) {
1300
- const parsed = JSON.parse((0, node_fs.readFileSync)(path, "utf8"));
1301
- if (!isWorkspaceMetadata(parsed)) {
1302
- throw new Error(`Invalid Boxdown workspace metadata: ${path}`);
1303
- }
1304
- return parsed;
1305
- }
1306
- function writeWorkspaceMetadata(context, sshAlias, now = new Date()) {
1307
- const metadataPath = workspaceMetadataPath(context);
1308
- const timestamp = now.toISOString();
1309
- let firstSeenAt = timestamp;
1310
- if ((0, node_fs.existsSync)(metadataPath)) {
1311
- firstSeenAt = readWorkspaceMetadataFile(metadataPath).firstSeenAt;
1312
- }
1313
- const metadata = {
1314
- version: 1,
1315
- workspaceId: context.workspaceId,
1316
- workspaceFolder: context.workspaceFolder,
1317
- workspaceBasename: context.workspaceBasename,
1318
- sshAlias,
1319
- firstSeenAt,
1320
- lastSeenAt: timestamp
1321
- };
1322
- (0, node_fs.mkdirSync)(context.workspaceDataDir, { recursive: true });
1323
- (0, node_fs.writeFileSync)(metadataPath, `${JSON.stringify(metadata, null, 2)}\n`);
1324
- return metadata;
1325
- }
1326
- function listWorkspaceMetadata(dataRoot) {
1327
- const workspacesDir = (0, node_path.join)(dataRoot, "workspaces");
1328
- if (!(0, node_fs.existsSync)(workspacesDir)) {
1329
- return [];
1330
- }
1331
- return (0, node_fs.readdirSync)(workspacesDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => (0, node_path.join)(workspacesDir, entry.name, "metadata.json")).filter((metadataPath) => (0, node_fs.existsSync)(metadataPath)).map((metadataPath) => readWorkspaceMetadataFile(metadataPath));
1332
- }
1333
-
1334
- //#endregion
1335
- //#region src/paths.ts
1336
- function packageRootFromImportMeta(importMetaUrl = require("url").pathToFileURL(__filename).href) {
1337
- return (0, node_path.dirname)((0, node_path.dirname)((0, node_url.fileURLToPath)(importMetaUrl)));
1338
- }
1339
- function workspaceIdFor(workspaceFolder) {
1340
- return (0, node_crypto.createHash)("sha256").update(workspaceFolder).digest("hex").slice(0, 16);
1341
- }
1342
- function resolveWorkspaceFolder(workspace, cwd = process.cwd()) {
1343
- const candidate = (0, node_path.resolve)(cwd, workspace ?? ".");
1344
- if (!(0, node_fs.existsSync)(candidate)) {
1345
- throw new Error(`Workspace does not exist: ${candidate}`);
1346
- }
1347
- if (!(0, node_fs.statSync)(candidate).isDirectory()) {
1348
- throw new Error(`Workspace is not a directory: ${candidate}`);
1349
- }
1350
- return (0, node_fs.realpathSync)(candidate);
1351
- }
1352
- function defaultCacheRoot(env = process.env) {
1353
- if (env.BOXDOWN_CACHE_HOME) {
1354
- return env.BOXDOWN_CACHE_HOME;
1355
- }
1356
- if (env.XDG_CACHE_HOME) {
1357
- return (0, node_path.join)(env.XDG_CACHE_HOME, PACKAGE_NAME);
1358
- }
1359
- return (0, node_path.join)((0, node_os.homedir)(), ".cache", PACKAGE_NAME);
1360
- }
1361
- function defaultDataRoot(env = process.env) {
1362
- if (env.BOXDOWN_DATA_HOME) {
1363
- return env.BOXDOWN_DATA_HOME;
1364
- }
1365
- if (env.XDG_DATA_HOME) {
1366
- return (0, node_path.join)(env.XDG_DATA_HOME, PACKAGE_NAME);
1367
- }
1368
- return (0, node_path.join)((0, node_os.homedir)(), ".local", "share", PACKAGE_NAME);
1369
- }
1370
- function createWorkspaceContext(options = {}) {
1371
- const env = options.env ?? process.env;
1372
- const workspaceFolder = resolveWorkspaceFolder(options.workspace, options.cwd);
1373
- const workspaceBasename = (0, node_path.basename)(workspaceFolder);
1374
- const workspaceId = workspaceIdFor(workspaceFolder);
1375
- const packageRoot = options.packageRoot ?? packageRootFromImportMeta();
1376
- const assetsDevcontainerDir = options.assetsDevcontainerDir ?? env.BOXDOWN_DEVCONTAINER_ASSETS_DIR ?? (0, node_path.join)(packageRoot, "assets", "devcontainer");
1377
- const cacheRoot = defaultCacheRoot(env);
1378
- const dataRoot = defaultDataRoot(env);
1379
- const workspaceCacheDir = (0, node_path.join)(cacheRoot, "workspaces", workspaceId);
1380
- const workspaceDataDir = (0, node_path.join)(dataRoot, "workspaces", workspaceId);
1381
- return {
1382
- workspaceFolder,
1383
- workspaceBasename,
1384
- workspaceId,
1385
- packageRoot,
1386
- assetsDevcontainerDir,
1387
- cacheRoot,
1388
- dataRoot,
1389
- workspaceCacheDir,
1390
- workspaceDataDir,
1391
- generatedConfigPath: (0, node_path.join)(workspaceCacheDir, "devcontainer.json"),
1392
- sshKeyDir: (0, node_path.join)(workspaceDataDir, "ssh"),
1393
- sshKeyPath: (0, node_path.join)(workspaceDataDir, "ssh", "id_ed25519"),
1394
- sshPublicKeyPath: (0, node_path.join)(workspaceDataDir, "ssh", "id_ed25519.pub"),
1395
- sshPublicKeyRuntimeDir: (0, node_path.join)(workspaceDataDir, "ssh-public"),
1396
- sshPublicKeyRuntimePath: (0, node_path.join)(workspaceDataDir, "ssh-public", "id_ed25519.pub")
1397
- };
1398
- }
1399
-
1400
- //#endregion
1401
- //#region src/main.ts
1402
- const USAGE = `Usage:
1403
- boxdown start [--workspace <path>] [--recreate]
1404
- boxdown codex [--workspace <path>] [--recreate] [-- <codex args...>]
1405
- boxdown claude [--workspace <path>] [--recreate] [-- <claude args...>]
1406
- boxdown cc [--workspace <path>] [--recreate] [-- <claude args...>]
1407
- boxdown opencode [--workspace <path>] [--recreate] [-- <opencode args...>]
1408
- boxdown antigravity [--workspace <path>] [--recreate] [-- <agy args...>]
1409
- boxdown list [--json]
1410
- boxdown status [--workspace <path>] [--alias <name>] [--json]
1411
- boxdown stop [--workspace <path>]
1412
- boxdown down [--workspace <path>]
1413
- boxdown doctor [--workspace <path>]
1414
- boxdown ssh-config install [--workspace <path>] [--alias <name>]
1415
- boxdown ssh-proxy [--workspace <path>] [--alias <name>]
1416
- boxdown refresh-gh-token [--workspace <path>]
1417
- boxdown refresh-gh-token-running [--workspace <path>]
1418
-
1419
- Commands:
1420
- start Start or reuse the workspace devcontainer, then open
1421
- an interactive shell inside it. Alias: shell.
1422
- codex Start or reuse the devcontainer, then launch Codex.
1423
- claude Start or reuse the devcontainer, then launch Claude
1424
- Code. Alias: cc.
1425
- opencode Start or reuse the devcontainer, then launch
1426
- OpenCode.
1427
- antigravity Start or reuse the devcontainer, then launch
1428
- Antigravity CLI (agy).
1429
- list List Boxdown-known devcontainer workspaces from any
1430
- directory.
1431
- status Show workspace state, generated paths, SSH key paths,
1432
- and the matching devcontainer state.
1433
- stop Stop the workspace devcontainer if it is running.
1434
- down Remove the workspace devcontainer. Keeps Boxdown
1435
- cache, generated config, data, and SSH keys.
1436
- doctor Check required host tools and Boxdown assets.
1437
- ssh-config install Install or update an SSH host alias for the workspace
1438
- devcontainer.
1439
- ssh-proxy Internal command used by the generated SSH
1440
- ProxyCommand. Starts or reuses the devcontainer and
1441
- bridges SSH over docker exec.
1442
- refresh-gh-token Start or reuse the devcontainer, then copy host
1443
- GitHub CLI auth into the container when available.
1444
- refresh-gh-token-running Refresh GitHub CLI auth only if the workspace
1445
- devcontainer is already running.
1446
-
1447
- Options:
1448
- --workspace <path> Target project directory. Defaults to the current directory.
1449
- --alias <name> SSH host alias. Defaults to <repo-name>-devcontainer.
1450
- --recreate Remove the existing devcontainer before starting.
1451
- --json Print JSON output. Supported by status and list.
1452
- --help, -h Show help.
1453
- `;
1454
- function commandWritesWorkspaceMetadata(command) {
1455
- return [
1456
- "start",
1457
- "ssh-config-install",
1458
- "ssh-proxy",
1459
- "refresh-gh-token",
1460
- "refresh-gh-token-running",
1461
- "coding-agent"
1462
- ].includes(command);
1463
- }
1464
- function parseCliArgs(argv) {
1465
- const args = [...argv];
1466
- let workspace;
1467
- let alias;
1468
- let recreate = false;
1469
- let json = false;
1470
- let passthroughArgs;
1471
- const positional = [];
1472
- function parsed(command) {
1473
- if (json && command !== "status" && command !== "list") {
1474
- throw new Error("--json is only supported with status and list");
1475
- }
1476
- if (passthroughArgs !== undefined) {
1477
- throw new Error("-- passthrough is only supported with coding-agent commands");
1478
- }
1479
- return {
1480
- command,
1481
- workspace,
1482
- alias,
1483
- recreate,
1484
- json
1485
- };
1486
- }
1487
- function parsedCodingAgent(agent) {
1488
- if (json) {
1489
- throw new Error("--json is only supported with status and list");
1490
- }
1491
- return {
1492
- command: "coding-agent",
1493
- agent,
1494
- agentArgs: passthroughArgs ?? [],
1495
- workspace,
1496
- alias,
1497
- recreate,
1498
- json
1499
- };
1500
- }
1501
- while (args.length > 0) {
1502
- const arg = args.shift();
1503
- if (arg === undefined) {
1504
- break;
1505
- }
1506
- if (arg === "--") {
1507
- passthroughArgs = args.splice(0);
1508
- break;
1509
- }
1510
- if (arg === "--help" || arg === "-h") {
1511
- return parsed("help");
1512
- }
1513
- if (arg === "--workspace") {
1514
- const value = args.shift();
1515
- if (value === undefined) {
1516
- throw new Error("--workspace requires a value");
1517
- }
1518
- workspace = value;
1519
- continue;
1520
- }
1521
- if (arg === "--alias") {
1522
- const value = args.shift();
1523
- if (value === undefined) {
1524
- throw new Error("--alias requires a value");
1525
- }
1526
- alias = value;
1527
- continue;
1528
- }
1529
- if (arg === "--recreate") {
1530
- recreate = true;
1531
- continue;
1532
- }
1533
- if (arg === "--json") {
1534
- json = true;
1535
- continue;
1536
- }
1537
- if (arg.startsWith("-")) {
1538
- throw new Error(`Unknown option: ${arg}`);
1539
- }
1540
- positional.push(arg);
1541
- }
1542
- if (positional.length === 0) {
1543
- return parsed("help");
1544
- }
1545
- if (positional[0] === "start" || positional[0] === "shell") {
1546
- return parsed("start");
1547
- }
1548
- const codingAgent = codingAgentFromCommand(positional[0] ?? "");
1549
- if (codingAgent !== undefined) {
1550
- if (positional.length > 1) {
1551
- throw new Error("Coding-agent CLI arguments must come after --");
1552
- }
1553
- return parsedCodingAgent(codingAgent);
1554
- }
1555
- if (positional[0] === "list" && positional.length === 1) {
1556
- return parsed("list");
1557
- }
1558
- if (positional[0] === "status" && positional.length === 1) {
1559
- return parsed("status");
1560
- }
1561
- if (positional[0] === "stop" && positional.length === 1) {
1562
- return parsed("stop");
1563
- }
1564
- if (positional[0] === "down" && positional.length === 1) {
1565
- return parsed("down");
1566
- }
1567
- if (positional[0] === "doctor" && positional.length === 1) {
1568
- return parsed("doctor");
1569
- }
1570
- if (positional[0] === "ssh-config") {
1571
- if (positional.length === 1 || positional[1] === "install" && positional.length === 2) {
1572
- return parsed("ssh-config-install");
1573
- }
1574
- throw new Error(`Unknown ssh-config command: ${positional.slice(1).join(" ")}. Usage: boxdown ssh-config [install] [--workspace <path>] [--alias <name>]`);
1575
- }
1576
- if (positional[0] === "ssh-proxy" && positional.length === 1) {
1577
- return parsed("ssh-proxy");
1578
- }
1579
- if (positional[0] === "refresh-gh-token" && positional.length === 1) {
1580
- return parsed("refresh-gh-token");
1581
- }
1582
- if (positional[0] === "refresh-gh-token-running" && positional.length === 1) {
1583
- return parsed("refresh-gh-token-running");
1584
- }
1585
- throw new Error(`Unknown command: ${positional.join(" ")}`);
1586
- }
1587
- async function runCli(argv = process.argv.slice(2)) {
1588
- try {
1589
- const parsed = parseCliArgs(argv);
1590
- if (parsed.command === "help") {
1591
- process.stdout.write(USAGE);
1592
- return 0;
1593
- }
1594
- if (parsed.command === "list") {
1595
- const metadata = listWorkspaceMetadata(defaultDataRoot());
1596
- const containers = await listWorkspaceContainers();
1597
- const entries = createWorkspaceListEntries(metadata, containers, node_fs.existsSync);
1598
- if (parsed.json) {
1599
- process.stdout.write(`${JSON.stringify(entries, null, 2)}\n`);
1600
- } else {
1601
- process.stdout.write(formatWorkspaceListText(entries));
1602
- }
1603
- return 0;
1604
- }
1605
- const context = createWorkspaceContext({ workspace: parsed.workspace });
1606
- const alias = parsed.alias ?? defaultSshAlias(context.workspaceBasename);
1607
- const aliasSource = parsed.alias === undefined ? "default" : "provided";
1608
- if (commandWritesWorkspaceMetadata(parsed.command)) {
1609
- writeWorkspaceMetadata(context, alias);
1610
- }
1611
- if (parsed.command === "ssh-config-install") {
1612
- await installSshConfig(context, alias);
1613
- return 0;
1614
- }
1615
- if (parsed.command === "status") {
1616
- const container = await findWorkspaceContainer(context);
1617
- const status = createStatusInfo(context, alias, container, node_fs.existsSync, { aliasSource });
1618
- if (parsed.json) {
1619
- process.stdout.write(`${JSON.stringify(status, null, 2)}\n`);
1620
- } else {
1621
- process.stdout.write(formatStatusText(status, { color: true }));
1622
- }
1623
- return statusIsHealthy(status) ? 0 : 1;
1624
- }
1625
- if (parsed.command === "stop") {
1626
- await stopWorkspaceContainer(context);
1627
- return 0;
1628
- }
1629
- if (parsed.command === "down") {
1630
- await removeWorkspaceContainer(context);
1631
- return 0;
1632
- }
1633
- if (parsed.command === "doctor") {
1634
- const checks = await runDoctorChecks(context);
1635
- process.stdout.write(formatDoctorText(checks));
1636
- return doctorHasFailures(checks) ? 1 : 0;
1637
- }
1638
- if (!(0, node_fs.existsSync)(context.assetsDevcontainerDir)) {
1639
- throw new Error(`Missing Boxdown devcontainer assets: ${context.assetsDevcontainerDir}`);
1640
- }
1641
- if (parsed.command === "ssh-proxy") {
1642
- await installSshConfig(context, alias, { quiet: true });
1643
- const containerId = await startDevcontainer(context, {
1644
- recreate: parsed.recreate,
1645
- proxyMode: true,
1646
- reuseRunning: true
1647
- });
1648
- await refreshContainerCodingAgentClis(context, true);
1649
- await ensureContainerSshRuntime(context);
1650
- return runSshdProxy(containerId);
1651
- }
1652
- if (parsed.command === "refresh-gh-token-running") {
1653
- const containerId = await findRunningContainerId(context);
1654
- if (containerId === undefined) {
1655
- throw new Error(`No running devcontainer found for: ${context.workspaceFolder}`);
1656
- }
1657
- await refreshContainerGhAuth(context);
1658
- return 0;
1659
- }
1660
- if (parsed.command === "refresh-gh-token") {
1661
- await startDevcontainer(context);
1662
- await refreshContainerGhAuth(context);
1663
- return 0;
1664
- }
1665
- if (parsed.command === "coding-agent") {
1666
- const agent = parsed.agent;
1667
- if (agent === undefined) {
1668
- throw new Error("Missing coding-agent command");
1669
- }
1670
- await startDevcontainer(context, { recreate: parsed.recreate });
1671
- await refreshContainerCodingAgentClis(context, false, [agent]);
1672
- return openCodingAgentCli(context, agent, parsed.agentArgs ?? []);
1673
- }
1674
- const containerId = await startDevcontainer(context, { recreate: parsed.recreate });
1675
- await printPortHint(context, containerId);
1676
- return openShell(context);
1677
- } catch (error) {
1678
- process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
1679
- return 1;
1680
- }
1681
- }
1682
-
1683
- //#endregion
1684
- Object.defineProperty(exports, 'USAGE', {
1685
- enumerable: true,
1686
- get: function () {
1687
- return USAGE;
1688
- }
1689
- });
1690
- Object.defineProperty(exports, 'commandWritesWorkspaceMetadata', {
1691
- enumerable: true,
1692
- get: function () {
1693
- return commandWritesWorkspaceMetadata;
1694
- }
1695
- });
1696
- Object.defineProperty(exports, 'parseCliArgs', {
1697
- enumerable: true,
1698
- get: function () {
1699
- return parseCliArgs;
1700
- }
1701
- });
1702
- Object.defineProperty(exports, 'runCli', {
1703
- enumerable: true,
1704
- get: function () {
1705
- return runCli;
1706
- }
1707
- });