codebay 0.9.0 → 0.10.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/.mochi/manifest.json +216 -149
- package/.mochi/svelte-client/HydratableIsland-aebn1fa1.js +1 -0
- package/.mochi/svelte-client/_hydrate-AppShell_2rtnxn6pgp2rh-rx6arrv5.js +2 -0
- package/.mochi/svelte-client/{_hydrate-AvatarsView_257s9yq1wwr4f-92hepzby.js → _hydrate-AvatarsView_257s9yq1wwr4f-jpjdmgpe.js} +1 -1
- package/.mochi/svelte-client/_hydrate-InstanceView_2ma45l9bybfhl-c9y47dat.js +1 -0
- package/.mochi/svelte-client/_hydrate-SettingsView_8hmarwmf8onq-d61feq89.js +38 -0
- package/.mochi/svelte-client/{chunk-2j95sv83.js → chunk-0fjx3z3r.js} +1 -1
- package/.mochi/svelte-client/chunk-8xvr02wh.js +2 -0
- package/.mochi/svelte-client/{chunk-m497q88s.js → chunk-9143wy3j.js} +2 -2
- package/.mochi/svelte-client/{chunk-v65n8th6.js → chunk-bn731rda.js} +1 -1
- package/.mochi/svelte-client/{chunk-a2psjmrm.js → chunk-j72r8pkk.js} +2 -2
- package/.mochi/svelte-client/chunk-k37wj1jm.js +2 -0
- package/.mochi/svelte-client/{chunk-mpv3qx28.js → chunk-p20tkpde.js} +1 -1
- package/.mochi/svelte-client/chunk-pc0fm37c.js +2 -0
- package/.mochi/svelte-client/{chunk-6rbs0mhx.js → chunk-t0ap7q89.js} +1 -1
- package/.mochi/svelte-client/{chunk-rs41by6w.js → chunk-wvtkwfsh.js} +1 -1
- package/.mochi/svelte-compile/{App-7vjcp38v.server.js → App-59cdj86f.server.js} +167 -103
- package/.mochi/svelte-compile/{Avatars-e2cfc95t.server.js → Avatars-fmagfyj2.server.js} +1 -1
- package/.mochi/svelte-compile/{Instance-h78n2dsv.server.js → Instance-s1gqwzmx.server.js} +16 -10
- package/.mochi/svelte-compile/{Settings-tav2asn7.server.js → Settings-k447mt0x.server.js} +540 -88
- package/.mochi/svelte-compile/{chunk-brjv9pyd.js → chunk-3t7v4hk0.js} +1 -7
- package/.mochi/svelte-compile/chunk-gkjgybb2.js +15 -0
- package/.mochi/svelte-compile/{chunk-m8ygpyhr.js → chunk-th2dbqvc.js} +20 -32
- package/.mochi/svelte-css/Button-oharop2xz6am.css +1 -0
- package/.mochi/svelte-css/IdeBar-3bgr3m5z0lpeg.css +1 -0
- package/.mochi/svelte-css/{InstanceView-3vzow3ttxzlh3.css → InstanceView-1pd7j16bd2b16.css} +1 -1
- package/.mochi/svelte-css/SettingsView-n3oy75uonzp2.css +1 -0
- package/.mochi/svelte-css/{TopBar-2q87li6yt8ids.css → TopBar-2y76zxwehrfos.css} +1 -1
- package/migrations/0007_instance_avatar.sql +1 -0
- package/package.json +2 -2
- package/src/avatars/diamond.ts +5 -6
- package/src/avatars/index.ts +1 -16
- package/src/avatars/pick.server.ts +25 -0
- package/src/avatars/whale.ts +7 -8
- package/src/components/AppShell.svelte +29 -4
- package/src/components/Avatar.svelte +8 -8
- package/src/components/Button.svelte +22 -1
- package/src/components/DashboardView.svelte +14 -2
- package/src/components/FolderBrowser.svelte +6 -2
- package/src/components/IdeBar.svelte +56 -13
- package/src/components/IdeLoader.svelte +15 -3
- package/src/components/InstanceCard.svelte +6 -1
- package/src/components/InstanceView.svelte +33 -15
- package/src/components/SettingsView.svelte +548 -1
- package/src/components/TopBar.svelte +37 -10
- package/src/components/UiShowcase.svelte +5 -10
- package/src/container-injections/claude-aliases.ts +1 -1
- package/src/container-injections/claude-code-custom.ts +2 -1
- package/src/container-injections/claude-code-ide-extension.ts +5 -0
- package/src/container-injections/claude-code-install.ts +67 -0
- package/src/container-injections/claude-code-models.ts +2 -1
- package/src/container-injections/claude-code-update.ts +62 -1
- package/src/container-injections/claude-effort-level.ts +32 -0
- package/src/container-injections/claude-permission-mode.ts +40 -0
- package/src/container-injections/custom-env-vars.ts +82 -0
- package/src/container-injections/host-env-vars.ts +2 -1
- package/src/lib/config.server.ts +3 -0
- package/src/lib/db.server.ts +8 -3
- package/src/lib/devcontainer.server.ts +182 -47
- package/src/lib/health.server.ts +55 -11
- package/src/lib/injections.server.ts +46 -36
- package/src/lib/instances.server.ts +185 -19
- package/src/lib/log-capture.server.ts +262 -0
- package/src/lib/ports.server.ts +31 -0
- package/src/lib/proxy.server.ts +21 -76
- package/src/pages/Settings.svelte +22 -1
- package/src/routes.ts +98 -2
- package/src/types.ts +36 -0
- package/.mochi/svelte-client/HydratableIsland-k6q8esj4.js +0 -1
- package/.mochi/svelte-client/_hydrate-AppShell_2rtnxn6pgp2rh-sg96rp17.js +0 -2
- package/.mochi/svelte-client/_hydrate-InstanceView_2ma45l9bybfhl-p3f649gk.js +0 -1
- package/.mochi/svelte-client/_hydrate-SettingsView_8hmarwmf8onq-dzmgmv78.js +0 -29
- package/.mochi/svelte-client/chunk-7wkvn2vx.js +0 -2
- package/.mochi/svelte-client/chunk-m4fzsd5b.js +0 -2
- package/.mochi/svelte-css/Button-2vtbrzb5kgu25.css +0 -1
- package/.mochi/svelte-css/IdeBar-10xodudcx57qr.css +0 -1
- package/.mochi/svelte-css/SettingsView-2qfauz68axro1.css +0 -1
- package/src/container-injections/claude-skip-permissions.ts +0 -28
|
@@ -4,7 +4,11 @@ import { basename, dirname, join, relative } from 'node:path';
|
|
|
4
4
|
import { homedir } from 'node:os';
|
|
5
5
|
import { INSTALL_SCRIPT as TMUX_INSTALL_SCRIPT } from '../container-injections/tmux.ts';
|
|
6
6
|
import { INSTALL_SCRIPT as TTYD_INSTALL_SCRIPT } from '../container-injections/ttyd.ts';
|
|
7
|
+
import { INSTALL_SCRIPT as CLAUDE_INSTALL_SCRIPT } from '../container-injections/claude-code-install.ts';
|
|
7
8
|
import { EXTENSION_ID as CLAUDE_CODE_EXTENSION_ID } from '../container-injections/claude-code-ide-extension.ts';
|
|
9
|
+
import { ENV_FILE_NAME as CUSTOM_ENV_FILE } from '../container-injections/claude-code-custom.ts';
|
|
10
|
+
import { ENV_FILE_NAME as MODELS_ENV_FILE } from '../container-injections/claude-code-models.ts';
|
|
11
|
+
import { ENV_FILE_NAME as HOST_ENV_FILE } from '../container-injections/host-env-vars.ts';
|
|
8
12
|
import {
|
|
9
13
|
CODE_SERVER_PORT,
|
|
10
14
|
DEFAULT_IMAGE,
|
|
@@ -14,12 +18,16 @@ import {
|
|
|
14
18
|
dockerEnv
|
|
15
19
|
} from './config.server.ts';
|
|
16
20
|
import { spawnCapture } from './spawn.server.ts';
|
|
17
|
-
import type
|
|
18
|
-
import type
|
|
21
|
+
import { getOption, type InstanceMode } from './db.server.ts';
|
|
22
|
+
import { claudePermissionFlags, type ClaudePermissionMode, type PortForward } from '../types.ts';
|
|
19
23
|
|
|
20
24
|
const CODE_SERVER_FEATURE = 'ghcr.io/coder/devcontainer-features/code-server:1';
|
|
21
25
|
|
|
22
|
-
/**
|
|
26
|
+
/**
|
|
27
|
+
* Installs the Claude Code CLI into the default image. Needs Node — supplied by NODE_FEATURE.
|
|
28
|
+
* Default-image only: NODE_FEATURE installs via nvm, which refuses to run when the image sets
|
|
29
|
+
* NPM_CONFIG_PREFIX, so a project-supplied image gets the sniffing CLAUDE_FEATURE_DIR instead.
|
|
30
|
+
*/
|
|
23
31
|
const CLAUDE_CODE_FEATURE = 'ghcr.io/anthropics/devcontainer-features/claude-code:1.0';
|
|
24
32
|
|
|
25
33
|
/** Node.js — required by the Claude Code feature, which no longer bundles it. */
|
|
@@ -71,12 +79,32 @@ const TTYD_FEATURE_INSTALL =
|
|
|
71
79
|
') || echo "codebay-ttyd: install failed (non-fatal); the manager retries after the container starts"\n' +
|
|
72
80
|
'exit 0\n';
|
|
73
81
|
|
|
82
|
+
/** Relative to .devcontainer/. Only written for terminal-mode instances on a project-supplied config. */
|
|
83
|
+
const CLAUDE_FEATURE_DIR = 'codebay-claude';
|
|
84
|
+
|
|
85
|
+
const CLAUDE_FEATURE_METADATA = {
|
|
86
|
+
id: 'codebay-claude',
|
|
87
|
+
version: '1.0.0',
|
|
88
|
+
name: 'Claude Code (Codebay, best-effort)',
|
|
89
|
+
description:
|
|
90
|
+
"Installs Claude Code at image build time when the project's own image doesn't already ship it. Never fails the build."
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/** Best-effort like tmux/ttyd: build time is the only reliable moment to fetch in an egress-firewalled container. */
|
|
94
|
+
const CLAUDE_FEATURE_INSTALL =
|
|
95
|
+
'#!/bin/sh\n' +
|
|
96
|
+
'(\n' +
|
|
97
|
+
`${CLAUDE_INSTALL_SCRIPT}\n` +
|
|
98
|
+
') || echo "codebay-claude: install failed (non-fatal); the manager retries after the container starts"\n' +
|
|
99
|
+
'exit 0\n';
|
|
100
|
+
|
|
74
101
|
/** Manager-dropped files the project's own .gitignore won't cover, kept out of git status. */
|
|
75
102
|
const MANAGER_GIT_EXCLUDES = [
|
|
76
103
|
'/.devcontainer/code-server-settings.json',
|
|
77
104
|
'/.devcontainer/devcontainer-lock.json',
|
|
78
105
|
'/.devcontainer/codebay-tmux/',
|
|
79
106
|
'/.devcontainer/codebay-ttyd/',
|
|
107
|
+
'/.devcontainer/codebay-claude/',
|
|
80
108
|
'/.devcontainer/codebay-terminal.sh',
|
|
81
109
|
'/.vscode/tasks.json'
|
|
82
110
|
];
|
|
@@ -116,6 +144,12 @@ const TMUX_SHELL_SESSION = 'codebay-shell';
|
|
|
116
144
|
/** Written to `$HOME` when the post-up injection sequence finishes; the terminal launcher waits on it. */
|
|
117
145
|
export const INJECTIONS_DONE_FILE = '.codebay-injections-done';
|
|
118
146
|
|
|
147
|
+
/**
|
|
148
|
+
* IDE mode's folderOpen task touches this to stay run-once across workspace reloads, but it
|
|
149
|
+
* outlives a container restart — so a relaunch must clear it or the terminal never reopens.
|
|
150
|
+
*/
|
|
151
|
+
export const TERMINAL_LAUNCHED_MARKER = '.codebay-terminal-launched';
|
|
152
|
+
|
|
119
153
|
/**
|
|
120
154
|
* An auto-launched `claude` that starts mid-injection reads `~/.claude.json` before the trust
|
|
121
155
|
* keys land and clobbers them on its next rewrite, so hold it until the sentinel appears.
|
|
@@ -135,45 +169,81 @@ const WAIT_FOR_INJECTIONS =
|
|
|
135
169
|
*/
|
|
136
170
|
const WAIT_FOR_IDE_BRIDGE = `i=0; until ls "$HOME/.claude/ide/" 2>/dev/null | grep -q "\\.lock$" || [ "$i" -ge 30 ]; do sleep 1; i=$((i + 1)); done; `;
|
|
137
171
|
|
|
172
|
+
/**
|
|
173
|
+
* The injections deliver `ANTHROPIC_*` (and host) env vars through `~/.bashrc`/`~/.zshrc`, which no
|
|
174
|
+
* auto-launch path reads — ttyd runs `bash <script>` and tmux runs its command via `$SHELL -c`, both
|
|
175
|
+
* non-interactive. LiteLLM first, so it wins over the manual override if both files somehow exist.
|
|
176
|
+
* An explicit list, never a glob: tmux runs this under the user's shell, and zsh would print
|
|
177
|
+
* "no matches found" when a file is absent.
|
|
178
|
+
*/
|
|
179
|
+
const SOURCE_INJECTED_ENV = `for f in ${[CUSTOM_ENV_FILE, MODELS_ENV_FILE, HOST_ENV_FILE]
|
|
180
|
+
.map((name) => `"$HOME/${name}"`)
|
|
181
|
+
.join(' ')}; do [ -r "$f" ] && . "$f"; done; `;
|
|
182
|
+
|
|
183
|
+
/** The label/`runOn` pair that identifies our task in a project's own tasks.json. */
|
|
184
|
+
const TERMINAL_TASK_LABEL = 'Terminal';
|
|
185
|
+
|
|
138
186
|
/**
|
|
139
187
|
* Runs under tmux so Claude survives the browser closing — code-server reaps the
|
|
140
188
|
* detached PTY, which only kills the tmux client. `-A` doubles as the run-once gate.
|
|
141
189
|
*/
|
|
142
|
-
const
|
|
143
|
-
label:
|
|
190
|
+
const terminalTask = (permissionMode: ClaudePermissionMode) => ({
|
|
191
|
+
label: TERMINAL_TASK_LABEL,
|
|
144
192
|
type: 'shell',
|
|
145
193
|
command:
|
|
146
194
|
// `"$SHELL"` must reach tmux unexpanded; VS Code would substitute a `${…}` form first.
|
|
147
|
-
`if command -v tmux >/dev/null 2>&1; then exec tmux new-session -A -s ${TMUX_SESSION} '${WAIT_FOR_INJECTIONS}${WAIT_FOR_IDE_BRIDGE}claude
|
|
195
|
+
`if command -v tmux >/dev/null 2>&1; then exec tmux new-session -A -s ${TMUX_SESSION} '${WAIT_FOR_INJECTIONS}${WAIT_FOR_IDE_BRIDGE}${SOURCE_INJECTED_ENV}claude ${claudePermissionFlags(permissionMode)}; exec "$SHELL" -l'; fi; ` +
|
|
148
196
|
// Without tmux there's no run-once gate, and folderOpen re-fires on every workspace load.
|
|
149
|
-
'MARK="$HOME
|
|
197
|
+
'MARK="$HOME/' +
|
|
198
|
+
TERMINAL_LAUNCHED_MARKER +
|
|
199
|
+
'"; [ -e "$MARK" ] && exit 0; touch "$MARK"; ' +
|
|
150
200
|
WAIT_FOR_INJECTIONS +
|
|
151
201
|
WAIT_FOR_IDE_BRIDGE +
|
|
152
|
-
|
|
202
|
+
SOURCE_INJECTED_ENV +
|
|
203
|
+
`claude ${claudePermissionFlags(permissionMode)}; exec \${env:SHELL} -l`,
|
|
153
204
|
presentation: { reveal: 'always', panel: 'shared', focus: true },
|
|
154
205
|
runOptions: { runOn: 'folderOpen' },
|
|
155
206
|
problemMatcher: []
|
|
156
|
-
};
|
|
207
|
+
});
|
|
157
208
|
|
|
158
209
|
const CODE_SERVER_APPLY_SETTINGS =
|
|
159
210
|
`mkdir -p ~/.local/share/code-server/User && ` +
|
|
160
211
|
`cp -f \\"$PWD/.devcontainer/${CODE_SERVER_SETTINGS_FILE}\\" ` +
|
|
161
212
|
`~/.local/share/code-server/User/settings.json 2>/dev/null;`;
|
|
162
213
|
|
|
163
|
-
//
|
|
164
|
-
//
|
|
214
|
+
// Fully detached (redirects + </dev/null + &) because `devcontainer up` waits for postStart's
|
|
215
|
+
// pipes to close — an attached download would keep the whole boot on the Open VSX critical path.
|
|
165
216
|
const CODE_SERVER_INSTALL_EXT =
|
|
166
|
-
`ls -d ~/.local/share/code-server/extensions/${CLAUDE_CODE_EXTENSION_ID}-* >/dev/null 2>&1 || ` +
|
|
167
|
-
`code-server --install-extension ${CLAUDE_CODE_EXTENSION_ID}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
`
|
|
174
|
-
`
|
|
175
|
-
|
|
176
|
-
|
|
217
|
+
`( ls -d ~/.local/share/code-server/extensions/${CLAUDE_CODE_EXTENSION_ID}-* >/dev/null 2>&1 || ` +
|
|
218
|
+
`code-server --install-extension ${CLAUDE_CODE_EXTENSION_ID} ) ` +
|
|
219
|
+
`>/tmp/code-server-ext.log 2>&1 </dev/null &`;
|
|
220
|
+
|
|
221
|
+
// Foreground variant for the advanced escape hatch: slower boot, but the extension is guaranteed
|
|
222
|
+
// active on the very first window.
|
|
223
|
+
const CODE_SERVER_INSTALL_EXT_BLOCKING =
|
|
224
|
+
`( ls -d ~/.local/share/code-server/extensions/${CLAUDE_CODE_EXTENSION_ID}-* >/dev/null 2>&1 || ` +
|
|
225
|
+
`code-server --install-extension ${CLAUDE_CODE_EXTENSION_ID} ) ` +
|
|
226
|
+
`>/tmp/code-server-ext.log 2>&1; `;
|
|
227
|
+
|
|
228
|
+
// By default the extension installs in the background after launch (the first window may need a
|
|
229
|
+
// reload to activate it; the claude-code-ide-extension injection waits out this install as the fallback).
|
|
230
|
+
function codeServerLaunch(blockingExtInstall: boolean): string {
|
|
231
|
+
return (
|
|
232
|
+
`bash -c "${CODE_SERVER_APPLY_SETTINGS} ` +
|
|
233
|
+
(blockingExtInstall ? CODE_SERVER_INSTALL_EXT_BLOCKING : '') +
|
|
234
|
+
// The bare default image may not export SHELL, which the Terminal task needs.
|
|
235
|
+
`export SHELL=\\"\${SHELL:-/bin/bash}\\"; ` +
|
|
236
|
+
// Probes the port rather than the process list: every `pgrep -f` pattern that matches a
|
|
237
|
+
// code-server daemon also matches this very shell (its own argv carries the nohup line
|
|
238
|
+
// below), so a cmdline guard would skip the launch every time. Bracketing can't escape it
|
|
239
|
+
// either — the payload text itself matches.
|
|
240
|
+
`(exec 3<>/dev/tcp/127.0.0.1/${CODE_SERVER_PORT}) 2>/dev/null || ` +
|
|
241
|
+
`nohup code-server --bind-addr 0.0.0.0:${CODE_SERVER_PORT} --auth none ` +
|
|
242
|
+
`--disable-workspace-trust \\"$PWD\\" >/tmp/code-server.log 2>&1 &` +
|
|
243
|
+
(blockingExtInstall ? '' : ` ${CODE_SERVER_INSTALL_EXT}`) +
|
|
244
|
+
`"`
|
|
245
|
+
);
|
|
246
|
+
}
|
|
177
247
|
|
|
178
248
|
/** Staged next to the config; ttyd runs it as its command so the nested quoting stays in a real file. */
|
|
179
249
|
const TTYD_LAUNCH_SCRIPT_FILE = 'codebay-terminal.sh';
|
|
@@ -191,7 +261,7 @@ export const TTYD_SHELL_ARG = 'shell';
|
|
|
191
261
|
* compare and never an eval. The shell branch sits above the injections wait so the split view's
|
|
192
262
|
* scratch shell opens immediately instead of blocking on Claude's boot sentinel.
|
|
193
263
|
*/
|
|
194
|
-
const
|
|
264
|
+
const ttydLaunchScript = (permissionMode: ClaudePermissionMode): string =>
|
|
195
265
|
'#!/usr/bin/env bash\n' +
|
|
196
266
|
'export SHELL="${SHELL:-/bin/bash}"\n' +
|
|
197
267
|
`if [ "$1" = "${TTYD_SHELL_ARG}" ]; then\n` +
|
|
@@ -203,9 +273,11 @@ const TTYD_LAUNCH_SCRIPT =
|
|
|
203
273
|
WAIT_FOR_INJECTIONS +
|
|
204
274
|
'\n' +
|
|
205
275
|
`if command -v tmux >/dev/null 2>&1; then\n` +
|
|
206
|
-
` exec tmux new-session -A -s ${TMUX_SESSION} 'claude
|
|
276
|
+
` exec tmux new-session -A -s ${TMUX_SESSION} '${SOURCE_INJECTED_ENV}claude ${claudePermissionFlags(permissionMode)}; exec "$SHELL" -l'\n` +
|
|
207
277
|
'fi\n' +
|
|
208
|
-
|
|
278
|
+
SOURCE_INJECTED_ENV +
|
|
279
|
+
'\n' +
|
|
280
|
+
`claude ${claudePermissionFlags(permissionMode)}\n` +
|
|
209
281
|
'exec "$SHELL" -l\n';
|
|
210
282
|
|
|
211
283
|
// ttyd defaults to read-only, so --writable is required for keyboard input. Guarded by `pgrep -x`
|
|
@@ -221,6 +293,16 @@ const TTYD_LAUNCH =
|
|
|
221
293
|
`nohup ttyd --port ${TTYD_PORT} --writable --url-arg ` +
|
|
222
294
|
`bash \\"$PWD/.devcontainer/${TTYD_LAUNCH_SCRIPT_FILE}\\" >/tmp/ttyd.log 2>&1 &"`;
|
|
223
295
|
|
|
296
|
+
/**
|
|
297
|
+
* The launcher `postStartCommand` runs, exposed so a host-side relaunch can reuse the exact
|
|
298
|
+
* string rather than growing a second, driftable copy of "how the served surface is started".
|
|
299
|
+
*/
|
|
300
|
+
export function launchCommandFor(mode: InstanceMode): string {
|
|
301
|
+
return mode === 'terminal'
|
|
302
|
+
? TTYD_LAUNCH
|
|
303
|
+
: codeServerLaunch(getOption('advanced_blocking_ext_install') === '1');
|
|
304
|
+
}
|
|
305
|
+
|
|
224
306
|
export async function devcontainerCliAvailable(): Promise<boolean> {
|
|
225
307
|
return (await spawnCapture([devcontainerBin(), '--version'])) !== null;
|
|
226
308
|
}
|
|
@@ -332,6 +414,7 @@ type DevcontainerConfig = {
|
|
|
332
414
|
forwardPorts?: (number | string)[];
|
|
333
415
|
postStartCommand?: unknown;
|
|
334
416
|
runArgs?: string[];
|
|
417
|
+
containerEnv?: Record<string, string>;
|
|
335
418
|
[key: string]: unknown;
|
|
336
419
|
};
|
|
337
420
|
|
|
@@ -385,7 +468,9 @@ export async function writeOverrideConfig(
|
|
|
385
468
|
hostPort: number,
|
|
386
469
|
forwards: PortForward[] = [],
|
|
387
470
|
defaultImage: string = DEFAULT_IMAGE,
|
|
388
|
-
mode: InstanceMode = 'ide'
|
|
471
|
+
mode: InstanceMode = 'ide',
|
|
472
|
+
permissionMode: ClaudePermissionMode = 'default',
|
|
473
|
+
envVars: { name: string; value: string }[] = []
|
|
389
474
|
): Promise<{ imageSource: string }> {
|
|
390
475
|
const isTerminal = mode === 'terminal';
|
|
391
476
|
const target = configPath(workspaceDir);
|
|
@@ -413,18 +498,26 @@ export async function writeOverrideConfig(
|
|
|
413
498
|
`./${relative(dirname(target), join(workspaceDir, '.devcontainer', dir))}`;
|
|
414
499
|
const tmuxFeatureKey = featureKey(TMUX_FEATURE_DIR);
|
|
415
500
|
const ttydFeatureKey = featureKey(TTYD_FEATURE_DIR);
|
|
501
|
+
const claudeFeatureKey = featureKey(CLAUDE_FEATURE_DIR);
|
|
416
502
|
|
|
417
503
|
// Terminal mode swaps code-server for ttyd; otherwise (tmux, tooling) the two are identical.
|
|
418
|
-
//
|
|
504
|
+
// Claude Code: the default image needs the upstream features, while terminal mode on a
|
|
505
|
+
// project-supplied image gets the local one, which installs only what that image lacks — the
|
|
506
|
+
// upstream Node feature would run nvm, which any image setting NPM_CONFIG_PREFIX breaks on.
|
|
507
|
+
// IDE mode on a project config stays hands-off: the project owns its tooling.
|
|
508
|
+
const needsClaude = isTerminal || !hadConfig;
|
|
419
509
|
config.features = {
|
|
420
510
|
...(config.features ?? {}),
|
|
421
511
|
...(isTerminal
|
|
422
512
|
? { [ttydFeatureKey]: {} }
|
|
423
513
|
: { [CODE_SERVER_FEATURE]: { host: '0.0.0.0', port: CODE_SERVER_PORT, auth: 'none' } }),
|
|
424
514
|
[tmuxFeatureKey]: {},
|
|
425
|
-
...(
|
|
426
|
-
|
|
427
|
-
: {}
|
|
515
|
+
...(needsClaude ? { [GITHUB_CLI_FEATURE]: {} } : {}),
|
|
516
|
+
...(!hadConfig
|
|
517
|
+
? { [NODE_FEATURE]: {}, [CLAUDE_CODE_FEATURE]: {} }
|
|
518
|
+
: isTerminal
|
|
519
|
+
? { [claudeFeatureKey]: {} }
|
|
520
|
+
: {})
|
|
428
521
|
};
|
|
429
522
|
|
|
430
523
|
const servedPort = isTerminal ? TTYD_PORT : CODE_SERVER_PORT;
|
|
@@ -441,30 +534,41 @@ export async function writeOverrideConfig(
|
|
|
441
534
|
runArgs.add(HOST_GATEWAY_ARG);
|
|
442
535
|
config.runArgs = [...runArgs];
|
|
443
536
|
|
|
444
|
-
const launch =
|
|
537
|
+
const launch = launchCommandFor(mode);
|
|
445
538
|
const existing = config.postStartCommand;
|
|
446
539
|
config.postStartCommand =
|
|
447
540
|
typeof existing === 'string' && existing.trim() ? `${existing} && ${launch}` : launch;
|
|
448
541
|
|
|
542
|
+
// Baked onto the container at creation, so every process inherits it (broader than remoteEnv).
|
|
543
|
+
// The user's configured names win; a project's own containerEnv keys are otherwise preserved.
|
|
544
|
+
if (envVars.length) {
|
|
545
|
+
config.containerEnv = {
|
|
546
|
+
...(config.containerEnv ?? {}),
|
|
547
|
+
...Object.fromEntries(envVars.map((v) => [v.name, v.value]))
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
|
|
449
551
|
await mkdir(join(workspaceDir, '.devcontainer'), { recursive: true }).catch(() => {});
|
|
450
552
|
await writeFile(target, JSON.stringify(config, null, 2) + '\n', 'utf8');
|
|
451
553
|
|
|
452
554
|
if (isTerminal) {
|
|
453
555
|
await writeTtydFeature(workspaceDir);
|
|
454
|
-
await writeTerminalLaunchScript(workspaceDir);
|
|
556
|
+
await writeTerminalLaunchScript(workspaceDir, permissionMode);
|
|
455
557
|
} else {
|
|
456
|
-
// Staged next to the config;
|
|
558
|
+
// Staged next to the config; codeServerLaunch copies it into the user-data dir on first start.
|
|
457
559
|
await writeFile(
|
|
458
560
|
join(workspaceDir, '.devcontainer', CODE_SERVER_SETTINGS_FILE),
|
|
459
561
|
JSON.stringify(CODE_SERVER_SETTINGS, null, 2) + '\n',
|
|
460
562
|
'utf8'
|
|
461
563
|
);
|
|
462
564
|
// The VS Code Terminal task is code-server-only; ttyd runs its own launcher script instead.
|
|
463
|
-
await writeTerminalTask(workspaceDir);
|
|
565
|
+
await writeTerminalTask(workspaceDir, permissionMode);
|
|
464
566
|
}
|
|
465
567
|
|
|
466
568
|
await writeTmuxFeature(workspaceDir);
|
|
467
569
|
|
|
570
|
+
if (isTerminal && hadConfig) await writeClaudeFeature(workspaceDir);
|
|
571
|
+
|
|
468
572
|
await writeLocalGitExclude(workspaceDir);
|
|
469
573
|
|
|
470
574
|
return { imageSource };
|
|
@@ -554,6 +658,19 @@ async function writeTmuxFeature(workspaceDir: string): Promise<void> {
|
|
|
554
658
|
await chmod(installPath, 0o755);
|
|
555
659
|
}
|
|
556
660
|
|
|
661
|
+
async function writeClaudeFeature(workspaceDir: string): Promise<void> {
|
|
662
|
+
const dir = join(workspaceDir, '.devcontainer', CLAUDE_FEATURE_DIR);
|
|
663
|
+
await mkdir(dir, { recursive: true }).catch(() => {});
|
|
664
|
+
await writeFile(
|
|
665
|
+
join(dir, 'devcontainer-feature.json'),
|
|
666
|
+
JSON.stringify(CLAUDE_FEATURE_METADATA, null, 2) + '\n',
|
|
667
|
+
'utf8'
|
|
668
|
+
);
|
|
669
|
+
const installPath = join(dir, 'install.sh');
|
|
670
|
+
await writeFile(installPath, CLAUDE_FEATURE_INSTALL, 'utf8');
|
|
671
|
+
await chmod(installPath, 0o755);
|
|
672
|
+
}
|
|
673
|
+
|
|
557
674
|
async function writeTtydFeature(workspaceDir: string): Promise<void> {
|
|
558
675
|
const dir = join(workspaceDir, '.devcontainer', TTYD_FEATURE_DIR);
|
|
559
676
|
await mkdir(dir, { recursive: true }).catch(() => {});
|
|
@@ -568,14 +685,20 @@ async function writeTtydFeature(workspaceDir: string): Promise<void> {
|
|
|
568
685
|
}
|
|
569
686
|
|
|
570
687
|
/** Staged next to the config; TTYD_LAUNCH runs it as ttyd's command. */
|
|
571
|
-
async function writeTerminalLaunchScript(
|
|
688
|
+
async function writeTerminalLaunchScript(
|
|
689
|
+
workspaceDir: string,
|
|
690
|
+
permissionMode: ClaudePermissionMode
|
|
691
|
+
): Promise<void> {
|
|
572
692
|
const path = join(workspaceDir, '.devcontainer', TTYD_LAUNCH_SCRIPT_FILE);
|
|
573
|
-
await writeFile(path,
|
|
693
|
+
await writeFile(path, ttydLaunchScript(permissionMode), 'utf8');
|
|
574
694
|
await chmod(path, 0o755);
|
|
575
695
|
}
|
|
576
696
|
|
|
577
697
|
/** Replaces the managed task rather than skipping it, so a rebuild picks up command changes. */
|
|
578
|
-
async function writeTerminalTask(
|
|
698
|
+
async function writeTerminalTask(
|
|
699
|
+
workspaceDir: string,
|
|
700
|
+
permissionMode: ClaudePermissionMode
|
|
701
|
+
): Promise<void> {
|
|
579
702
|
const tasksPath = join(workspaceDir, '.vscode', 'tasks.json');
|
|
580
703
|
let config: { version?: string; tasks?: unknown[] } = {};
|
|
581
704
|
|
|
@@ -593,9 +716,9 @@ async function writeTerminalTask(workspaceDir: string): Promise<void> {
|
|
|
593
716
|
const isManagedTask = (t: unknown) =>
|
|
594
717
|
typeof t === 'object' &&
|
|
595
718
|
t !== null &&
|
|
596
|
-
(t as Record<string, unknown>).label ===
|
|
719
|
+
(t as Record<string, unknown>).label === TERMINAL_TASK_LABEL &&
|
|
597
720
|
((t as Record<string, { runOn?: string }>).runOptions?.runOn ?? '') === 'folderOpen';
|
|
598
|
-
config.tasks = [...tasks.filter((t) => !isManagedTask(t)),
|
|
721
|
+
config.tasks = [...tasks.filter((t) => !isManagedTask(t)), terminalTask(permissionMode)];
|
|
599
722
|
|
|
600
723
|
await mkdir(join(workspaceDir, '.vscode'), { recursive: true }).catch(() => {});
|
|
601
724
|
await writeFile(tasksPath, JSON.stringify(config, null, 2) + '\n', 'utf8');
|
|
@@ -610,12 +733,7 @@ export interface UpResult {
|
|
|
610
733
|
description?: string;
|
|
611
734
|
}
|
|
612
735
|
|
|
613
|
-
|
|
614
|
-
export async function devcontainerUp(
|
|
615
|
-
workspaceDir: string,
|
|
616
|
-
onLog: (chunk: string) => void,
|
|
617
|
-
opts: { noCache?: boolean } = {}
|
|
618
|
-
): Promise<UpResult> {
|
|
736
|
+
export function devcontainerUpArgs(workspaceDir: string, opts: { noCache?: boolean } = {}) {
|
|
619
737
|
const args = [
|
|
620
738
|
devcontainerBin(),
|
|
621
739
|
'up',
|
|
@@ -625,11 +743,28 @@ export async function devcontainerUp(
|
|
|
625
743
|
];
|
|
626
744
|
// Only takes effect because --remove-existing-container drops the container before the build.
|
|
627
745
|
if (opts.noCache) args.push('--build-no-cache');
|
|
628
|
-
|
|
746
|
+
return args;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/** Defaults only — the spread lets an explicit user env win. BuildKit is standard on modern
|
|
750
|
+
* daemons; forcing it here just keeps feature-layer caching on older ones. */
|
|
751
|
+
export function devcontainerUpEnv(): Record<string, string | undefined> {
|
|
752
|
+
// Escape hatch for daemons where forcing BuildKit breaks builds (old builders, classic compose).
|
|
753
|
+
if (getOption('advanced_no_buildkit') === '1') return dockerEnv();
|
|
754
|
+
return { DOCKER_BUILDKIT: '1', COMPOSE_DOCKER_CLI_BUILD: '1', ...dockerEnv() };
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/** Streams all output to `onLog` and returns the parsed final result line. */
|
|
758
|
+
export async function devcontainerUp(
|
|
759
|
+
workspaceDir: string,
|
|
760
|
+
onLog: (chunk: string) => void,
|
|
761
|
+
opts: { noCache?: boolean } = {}
|
|
762
|
+
): Promise<UpResult> {
|
|
763
|
+
const proc = Bun.spawn(devcontainerUpArgs(workspaceDir, opts), {
|
|
629
764
|
cwd: workspaceDir,
|
|
630
765
|
stdout: 'pipe',
|
|
631
766
|
stderr: 'pipe',
|
|
632
|
-
env:
|
|
767
|
+
env: devcontainerUpEnv()
|
|
633
768
|
});
|
|
634
769
|
|
|
635
770
|
let stdoutText = '';
|
package/src/lib/health.server.ts
CHANGED
|
@@ -1,21 +1,26 @@
|
|
|
1
1
|
import { type InstanceRow } from './db.server.ts';
|
|
2
2
|
import { isRunning, publishedContainerPorts } from './docker.server.ts';
|
|
3
|
-
import { broadcastHealth } from './instances.server.ts';
|
|
3
|
+
import { broadcastHealth, relaunchSurface } from './instances.server.ts';
|
|
4
4
|
import { resolveInjections } from './injections.server.ts';
|
|
5
5
|
import type { InstanceHealth } from '../types.ts';
|
|
6
6
|
|
|
7
7
|
const REFRESH_MS = 5000;
|
|
8
|
+
const FAST_REFRESH_MS = 1000;
|
|
9
|
+
const FAST_WINDOW_MS = 120_000;
|
|
8
10
|
|
|
9
11
|
interface Monitor {
|
|
10
12
|
snapshot: InstanceHealth | null;
|
|
11
|
-
timer: ReturnType<typeof
|
|
13
|
+
timer: ReturnType<typeof setTimeout>;
|
|
14
|
+
startedAt: number;
|
|
15
|
+
everAccessible: boolean;
|
|
12
16
|
}
|
|
13
17
|
|
|
14
18
|
// Pin to globalThis so dev-mode hot reload doesn't orphan the interval timers.
|
|
15
19
|
const globalForHealth = globalThis as unknown as { __codebayHealth?: Map<string, Monitor> };
|
|
16
20
|
const monitors: Map<string, Monitor> = (globalForHealth.__codebayHealth ??= new Map());
|
|
17
21
|
|
|
18
|
-
|
|
22
|
+
/** Named for what it measures — the served surface is ttyd in terminal mode, code-server in IDE. */
|
|
23
|
+
export async function surfaceAccessible(port: number): Promise<boolean> {
|
|
19
24
|
try {
|
|
20
25
|
// Any HTTP response at all (200/302/401/…) means the server is listening.
|
|
21
26
|
await fetch(`http://127.0.0.1:${port}/`, { signal: AbortSignal.timeout(2000) });
|
|
@@ -38,7 +43,7 @@ async function check(row: InstanceRow): Promise<InstanceHealth> {
|
|
|
38
43
|
// Probes as the recorded remote user, so `$HOME` resolves to the one the injection wrote to.
|
|
39
44
|
const target = { containerId: row.container_id, remoteUser: row.remote_user, instance: row };
|
|
40
45
|
const [accessible, openPorts, injectionResults] = await Promise.all([
|
|
41
|
-
|
|
46
|
+
surfaceAccessible(row.host_port),
|
|
42
47
|
publishedContainerPorts(row.container_id),
|
|
43
48
|
Promise.all(
|
|
44
49
|
resolveInjections(row.mode)
|
|
@@ -59,27 +64,66 @@ async function check(row: InstanceRow): Promise<InstanceHealth> {
|
|
|
59
64
|
};
|
|
60
65
|
}
|
|
61
66
|
|
|
62
|
-
|
|
67
|
+
/** Probes at 1s until code-server first answers, so the IDE iframe can mount without waiting out
|
|
68
|
+
* the 5s cadence; the window cap keeps a permanently-dead code-server off the fast path. */
|
|
69
|
+
export function nextProbeDelay(
|
|
70
|
+
health: InstanceHealth,
|
|
71
|
+
everAccessible: boolean,
|
|
72
|
+
elapsedMs: number
|
|
73
|
+
): number {
|
|
74
|
+
const waitingForFirstResponse =
|
|
75
|
+
health.containerRunning && !health.codeServerAccessible && !everAccessible;
|
|
76
|
+
return waitingForFirstResponse && elapsedMs < FAST_WINDOW_MS ? FAST_REFRESH_MS : REFRESH_MS;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function tick(row: InstanceRow, mon: Monitor): Promise<void> {
|
|
63
80
|
const snapshot = await check(row);
|
|
64
|
-
const mon = monitors.get(row.id);
|
|
65
|
-
if (mon) mon.snapshot = snapshot;
|
|
66
81
|
broadcastHealth(row.id, snapshot);
|
|
67
|
-
|
|
82
|
+
// Replaced or stopped while the probe was in flight — rescheduling would leak a second chain.
|
|
83
|
+
if (monitors.get(row.id) !== mon) return;
|
|
84
|
+
if (!snapshot.containerRunning) {
|
|
85
|
+
stopHealthMonitor(row.id);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
mon.snapshot = snapshot;
|
|
89
|
+
mon.everAccessible ||= snapshot.codeServerAccessible;
|
|
90
|
+
const delay = nextProbeDelay(snapshot, mon.everAccessible, Date.now() - mon.startedAt);
|
|
91
|
+
mon.timer = setTimeout(() => void tick(row, mon), delay);
|
|
68
92
|
}
|
|
69
93
|
|
|
70
94
|
function startHealthMonitor(row: InstanceRow): Monitor {
|
|
71
95
|
const existing = monitors.get(row.id);
|
|
72
96
|
if (existing) return existing;
|
|
73
|
-
const mon: Monitor = {
|
|
97
|
+
const mon: Monitor = {
|
|
98
|
+
snapshot: null,
|
|
99
|
+
// Placeholder until the seed tick completes; it self-reschedules from then on.
|
|
100
|
+
timer: setTimeout(() => undefined, 0),
|
|
101
|
+
startedAt: Date.now(),
|
|
102
|
+
everAccessible: false
|
|
103
|
+
};
|
|
74
104
|
monitors.set(row.id, mon);
|
|
75
|
-
|
|
105
|
+
// Seed a snapshot rather than making the UI wait a full interval (the tick self-reschedules
|
|
106
|
+
// the fast/slow probe chain from here), then use the seed to decide whether to relaunch.
|
|
107
|
+
void tick(row, mon).then(async () => {
|
|
108
|
+
if (monitors.get(row.id) !== mon) return;
|
|
109
|
+
const seeded = mon.snapshot;
|
|
110
|
+
if (!seeded?.containerRunning || seeded.codeServerAccessible) return;
|
|
111
|
+
// A live container with a dead port means postStartCommand never re-ran — it only fires
|
|
112
|
+
// under `devcontainer up`. Covers starts codebay didn't perform (a daemon restart, a bare
|
|
113
|
+
// `docker start`); once per monitor, so a genuinely wedged container isn't exec'd in a loop.
|
|
114
|
+
await relaunchSurface(row).catch(() => undefined);
|
|
115
|
+
if (monitors.get(row.id) !== mon) return;
|
|
116
|
+
// Drop the seed tick's already-scheduled probe and re-probe now, so a single chain survives.
|
|
117
|
+
clearTimeout(mon.timer);
|
|
118
|
+
void tick(row, mon);
|
|
119
|
+
});
|
|
76
120
|
return mon;
|
|
77
121
|
}
|
|
78
122
|
|
|
79
123
|
export function stopHealthMonitor(id: string): void {
|
|
80
124
|
const mon = monitors.get(id);
|
|
81
125
|
if (!mon) return;
|
|
82
|
-
|
|
126
|
+
clearTimeout(mon.timer);
|
|
83
127
|
monitors.delete(id);
|
|
84
128
|
}
|
|
85
129
|
|
|
@@ -8,17 +8,20 @@ import { gitIdentity } from '../container-injections/git-identity.ts';
|
|
|
8
8
|
import { claudeCodeCredentials } from '../container-injections/claude-code-credentials.ts';
|
|
9
9
|
import { claudeCodeCustom } from '../container-injections/claude-code-custom.ts';
|
|
10
10
|
import { claudeCodeIdeExtension } from '../container-injections/claude-code-ide-extension.ts';
|
|
11
|
+
import { claudeCodeInstall } from '../container-injections/claude-code-install.ts';
|
|
11
12
|
import { claudeCodeUpdate } from '../container-injections/claude-code-update.ts';
|
|
12
13
|
import { claudeCodeModels } from '../container-injections/claude-code-models.ts';
|
|
13
14
|
import { githubCredentials } from '../container-injections/github-credentials.ts';
|
|
14
15
|
import { attentionHooks } from '../container-injections/attention-hooks.ts';
|
|
15
16
|
import { claudeStatusline } from '../container-injections/claude-statusline.ts';
|
|
16
17
|
import { claudeModel } from '../container-injections/claude-model.ts';
|
|
17
|
-
import {
|
|
18
|
+
import { claudeEffortLevel } from '../container-injections/claude-effort-level.ts';
|
|
19
|
+
import { claudePermissionMode } from '../container-injections/claude-permission-mode.ts';
|
|
18
20
|
import { claudeTrust } from '../container-injections/claude-trust.ts';
|
|
19
21
|
import { claudeAliases } from '../container-injections/claude-aliases.ts';
|
|
20
22
|
import { claudeNoCoauthor } from '../container-injections/claude-no-coauthor.ts';
|
|
21
23
|
import { hostEnvVars } from '../container-injections/host-env-vars.ts';
|
|
24
|
+
import { customEnvVars } from '../container-injections/custom-env-vars.ts';
|
|
22
25
|
|
|
23
26
|
/** Extends `ExecTarget` so exec-user semantics have a single source of truth. */
|
|
24
27
|
export interface ContainerTarget extends ExecTarget {
|
|
@@ -46,49 +49,56 @@ export interface Injection {
|
|
|
46
49
|
check?(target: ContainerTarget): Promise<boolean>;
|
|
47
50
|
}
|
|
48
51
|
|
|
49
|
-
/**
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
]
|
|
52
|
+
/**
|
|
53
|
+
* Injections within a stage apply in parallel, so no two of them may write the same container
|
|
54
|
+
* resource; the stage boundaries serialize each shared resource in the same relative order the
|
|
55
|
+
* old flat list ran it (gitconfig, ~/.claude/settings.json, rc files, ~/.claude.json, apt/dpkg).
|
|
56
|
+
*/
|
|
57
|
+
function buildStages(claudeInjection: Injection): Injection[][] {
|
|
58
|
+
return [
|
|
59
|
+
// Disjoint resources — and the slow network installs (apt tmux, npm/standalone claude,
|
|
60
|
+
// Open VSX extension) start immediately instead of queueing behind one another.
|
|
61
|
+
// claude-code-install (terminal-only) must precede claude-code-update: both are the sole
|
|
62
|
+
// npm-global writer of their stage, and the update no-ops until a binary exists.
|
|
63
|
+
[
|
|
64
|
+
gitSafeDirectory,
|
|
65
|
+
tmux,
|
|
66
|
+
claudeCodeInstall,
|
|
67
|
+
claudeInjection,
|
|
68
|
+
claudeCodeIdeExtension,
|
|
69
|
+
claudeEffortLevel
|
|
70
|
+
],
|
|
71
|
+
// git-identity needs stage 1's safe.directory; ttyd shares the apt/dpkg lock with tmux and
|
|
72
|
+
// the /usr/local/bin symlink with claude-code-install, so it trails both.
|
|
73
|
+
[gitIdentity, attentionHooks, claudeCodeModels, ttyd, claudeCodeUpdate],
|
|
74
|
+
[githubCredentials, claudeStatusline, claudePermissionMode],
|
|
75
|
+
[claudeModel, claudeAliases],
|
|
76
|
+
// claude-trust edits ~/.claude.json, which the stage-1 Claude slot also writes.
|
|
77
|
+
// custom-env-vars writes nothing (values ride containerEnv) — it only verifies + health-checks.
|
|
78
|
+
[claudeTrust, hostEnvVars, customEnvVars],
|
|
79
|
+
[claudeNoCoauthor]
|
|
80
|
+
];
|
|
81
|
+
}
|
|
76
82
|
|
|
77
83
|
/**
|
|
78
84
|
* Resolved per call so toggling the custom-endpoint setting takes effect without a restart.
|
|
79
85
|
* `mode` drops injections that don't apply to it (e.g. ttyd off IDE instances, the IDE
|
|
80
86
|
* extension off terminal ones); omit it in mode-agnostic contexts to keep every injection.
|
|
81
87
|
*/
|
|
82
|
-
export function
|
|
88
|
+
export function resolveInjectionStages(mode?: InstanceMode): Injection[][] {
|
|
83
89
|
const claudeInjection =
|
|
84
90
|
getOption('custom_endpoint_enabled') === '1' ? claudeCodeCustom : claudeCodeCredentials;
|
|
85
|
-
const
|
|
86
|
-
|
|
91
|
+
const stages = buildStages(claudeInjection)
|
|
92
|
+
.map((stage) => (mode ? stage.filter((i) => !i.modes || i.modes.includes(mode)) : stage))
|
|
93
|
+
.filter((stage) => stage.length > 0);
|
|
94
|
+
// One injection per stage restores the fully serial boot for diagnosing injection interference.
|
|
95
|
+
return getOption('advanced_serial_injections') === '1' ? stages.flat().map((i) => [i]) : stages;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Flat view of `resolveInjectionStages()` for order-insensitive consumers (health, auth chips). */
|
|
99
|
+
export function resolveInjections(mode?: InstanceMode): Injection[] {
|
|
100
|
+
return resolveInjectionStages(mode).flat();
|
|
87
101
|
}
|
|
88
102
|
|
|
89
103
|
/** @deprecated Use `resolveInjections()`; this ignores settings/mode and is kept for the tests. */
|
|
90
|
-
export const injections: Injection[] =
|
|
91
|
-
...BASE_INJECTIONS_HEAD,
|
|
92
|
-
claudeCodeCredentials,
|
|
93
|
-
...BASE_INJECTIONS_TAIL
|
|
94
|
-
];
|
|
104
|
+
export const injections: Injection[] = buildStages(claudeCodeCredentials).flat();
|