codebay 0.10.0 → 0.10.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.
- package/.mochi/manifest.json +25 -25
- package/package.json +1 -1
- package/src/lib/devcontainer.server.ts +30 -5
- package/src/lib/instances.server.ts +70 -12
package/.mochi/manifest.json
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
"componentEntryUrls": {
|
|
6
6
|
"AppShell_2rtnxn6pgp2rh": "/_mochi/client/_hydrate-AppShell_2rtnxn6pgp2rh-rx6arrv5.js",
|
|
7
7
|
"InstanceView_2ma45l9bybfhl": "/_mochi/client/_hydrate-InstanceView_2ma45l9bybfhl-c9y47dat.js",
|
|
8
|
-
"
|
|
9
|
-
"
|
|
8
|
+
"AvatarsView_257s9yq1wwr4f": "/_mochi/client/_hydrate-AvatarsView_257s9yq1wwr4f-jpjdmgpe.js",
|
|
9
|
+
"SettingsView_8hmarwmf8onq": "/_mochi/client/_hydrate-SettingsView_8hmarwmf8onq-d61feq89.js"
|
|
10
10
|
},
|
|
11
11
|
"cssFileUrls": {
|
|
12
12
|
"$mochi/templates/DefaultError.svelte": "/_mochi/css/DefaultError-2b0daamydxgb2.css",
|
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
"$mochi/templates/BackLink.svelte": "/_mochi/css/BackLink-2wlu0eovz9j86.css",
|
|
15
15
|
"src/components/AppShell.svelte": "/_mochi/css/AppShell-36mpg5smnbmuw.css",
|
|
16
16
|
"src/components/InstanceView.svelte": "/_mochi/css/InstanceView-1pd7j16bd2b16.css",
|
|
17
|
-
"src/components/SettingsView.svelte": "/_mochi/css/SettingsView-n3oy75uonzp2.css",
|
|
18
17
|
"src/components/AvatarsView.svelte": "/_mochi/css/AvatarsView-2llo48w0wt0xn.css",
|
|
18
|
+
"src/components/SettingsView.svelte": "/_mochi/css/SettingsView-n3oy75uonzp2.css",
|
|
19
19
|
"src/components/TerminalSplit.svelte": "/_mochi/css/TerminalSplit-2416xfuln89a3.css",
|
|
20
20
|
"src/components/DashboardView.svelte": "/_mochi/css/DashboardView-1cy4xpqhja9sx.css",
|
|
21
21
|
"src/components/IdeBar.svelte": "/_mochi/css/IdeBar-3bgr3m5z0lpeg.css",
|
|
@@ -28,25 +28,25 @@
|
|
|
28
28
|
"node_modules/svelte-french-toast/dist/components/ToastBar.svelte": "/_mochi/css/ToastBar-3havgkmtofntq.css",
|
|
29
29
|
"src/components/Skeleton.svelte": "/_mochi/css/Skeleton-8k69usiju43h.css",
|
|
30
30
|
"src/components/StatusBadge.svelte": "/_mochi/css/StatusBadge-3mq0j7z6aq4j.css",
|
|
31
|
-
"src/components/HealthBox.svelte": "/_mochi/css/HealthBox-er2her0jukwl.css",
|
|
32
31
|
"src/components/AppBar.svelte": "/_mochi/css/AppBar-3bxinv4pabv8l.css",
|
|
32
|
+
"src/components/AvatarEditor.svelte": "/_mochi/css/AvatarEditor-2mdq8ss346drw.css",
|
|
33
33
|
"src/components/Avatar.svelte": "/_mochi/css/Avatar-3pi30db6ozvf0.css",
|
|
34
|
+
"src/components/HealthBox.svelte": "/_mochi/css/HealthBox-er2her0jukwl.css",
|
|
34
35
|
"src/components/Button.svelte": "/_mochi/css/Button-oharop2xz6am.css",
|
|
35
|
-
"src/components/CoinButton.svelte": "/_mochi/css/CoinButton-1rej3sh3tkjud.css",
|
|
36
36
|
"src/components/ThemePicker.svelte": "/_mochi/css/ThemePicker-1ir8jyocm8mvi.css",
|
|
37
|
-
"src/components/
|
|
37
|
+
"src/components/CoinButton.svelte": "/_mochi/css/CoinButton-1rej3sh3tkjud.css",
|
|
38
38
|
"src/components/TerminalPane.svelte": "/_mochi/css/TerminalPane-3bwgk6ulzq6b3.css",
|
|
39
|
-
"
|
|
40
|
-
"src/components/InstanceCard.svelte": "/_mochi/css/InstanceCard-3vnltxnbxkvay.css",
|
|
39
|
+
"node_modules/svelte-french-toast/dist/components/ToastWrapper.svelte": "/_mochi/css/ToastWrapper-1fbnym10rm2nv.css",
|
|
41
40
|
"src/components/TopBar.svelte": "/_mochi/css/TopBar-2y76zxwehrfos.css",
|
|
41
|
+
"src/components/InstanceCard.svelte": "/_mochi/css/InstanceCard-3vnltxnbxkvay.css",
|
|
42
|
+
"src/components/FolderBrowser.svelte": "/_mochi/css/FolderBrowser-2pxp9z1o7zixk.css",
|
|
42
43
|
"node_modules/svelte-french-toast/dist/components/ToastMessage.svelte": "/_mochi/css/ToastMessage-2jfwb2og2lfa8.css",
|
|
43
|
-
"node_modules/svelte-french-toast/dist/components/ToastWrapper.svelte": "/_mochi/css/ToastWrapper-1fbnym10rm2nv.css",
|
|
44
|
-
"src/components/BranchBox.svelte": "/_mochi/css/BranchBox-1yqu1q2mgnjwz.css",
|
|
45
|
-
"src/components/PortsBox.svelte": "/_mochi/css/PortsBox-1t25b5yg7jva3.css",
|
|
46
44
|
"src/components/Brand.svelte": "/_mochi/css/Brand-1bh2b800g5vrn.css",
|
|
47
|
-
"src/components/
|
|
45
|
+
"src/components/PortsBox.svelte": "/_mochi/css/PortsBox-1t25b5yg7jva3.css",
|
|
46
|
+
"src/components/BranchBox.svelte": "/_mochi/css/BranchBox-1yqu1q2mgnjwz.css",
|
|
47
|
+
"src/components/SettingsCog.svelte": "/_mochi/css/SettingsCog-jftyadvo64qo.css",
|
|
48
48
|
"src/components/CredMenu.svelte": "/_mochi/css/CredMenu-9hwgeptp7crb.css",
|
|
49
|
-
"src/components/
|
|
49
|
+
"src/components/InstanceFilter.svelte": "/_mochi/css/InstanceFilter-3q6x0jt0hcgpz.css"
|
|
50
50
|
},
|
|
51
51
|
"clientFiles": {
|
|
52
52
|
"/_mochi/css/DefaultError-2b0daamydxgb2.css": "svelte-css/DefaultError-2b0daamydxgb2.css",
|
|
@@ -54,8 +54,8 @@
|
|
|
54
54
|
"/_mochi/css/BackLink-2wlu0eovz9j86.css": "svelte-css/BackLink-2wlu0eovz9j86.css",
|
|
55
55
|
"/_mochi/css/AppShell-36mpg5smnbmuw.css": "svelte-css/AppShell-36mpg5smnbmuw.css",
|
|
56
56
|
"/_mochi/css/InstanceView-1pd7j16bd2b16.css": "svelte-css/InstanceView-1pd7j16bd2b16.css",
|
|
57
|
-
"/_mochi/css/SettingsView-n3oy75uonzp2.css": "svelte-css/SettingsView-n3oy75uonzp2.css",
|
|
58
57
|
"/_mochi/css/AvatarsView-2llo48w0wt0xn.css": "svelte-css/AvatarsView-2llo48w0wt0xn.css",
|
|
58
|
+
"/_mochi/css/SettingsView-n3oy75uonzp2.css": "svelte-css/SettingsView-n3oy75uonzp2.css",
|
|
59
59
|
"/_mochi/css/TerminalSplit-2416xfuln89a3.css": "svelte-css/TerminalSplit-2416xfuln89a3.css",
|
|
60
60
|
"/_mochi/css/DashboardView-1cy4xpqhja9sx.css": "svelte-css/DashboardView-1cy4xpqhja9sx.css",
|
|
61
61
|
"/_mochi/css/IdeBar-3bgr3m5z0lpeg.css": "svelte-css/IdeBar-3bgr3m5z0lpeg.css",
|
|
@@ -68,25 +68,25 @@
|
|
|
68
68
|
"/_mochi/css/ToastBar-3havgkmtofntq.css": "svelte-css/ToastBar-3havgkmtofntq.css",
|
|
69
69
|
"/_mochi/css/Skeleton-8k69usiju43h.css": "svelte-css/Skeleton-8k69usiju43h.css",
|
|
70
70
|
"/_mochi/css/StatusBadge-3mq0j7z6aq4j.css": "svelte-css/StatusBadge-3mq0j7z6aq4j.css",
|
|
71
|
-
"/_mochi/css/HealthBox-er2her0jukwl.css": "svelte-css/HealthBox-er2her0jukwl.css",
|
|
72
71
|
"/_mochi/css/AppBar-3bxinv4pabv8l.css": "svelte-css/AppBar-3bxinv4pabv8l.css",
|
|
72
|
+
"/_mochi/css/AvatarEditor-2mdq8ss346drw.css": "svelte-css/AvatarEditor-2mdq8ss346drw.css",
|
|
73
73
|
"/_mochi/css/Avatar-3pi30db6ozvf0.css": "svelte-css/Avatar-3pi30db6ozvf0.css",
|
|
74
|
+
"/_mochi/css/HealthBox-er2her0jukwl.css": "svelte-css/HealthBox-er2her0jukwl.css",
|
|
74
75
|
"/_mochi/css/Button-oharop2xz6am.css": "svelte-css/Button-oharop2xz6am.css",
|
|
75
|
-
"/_mochi/css/CoinButton-1rej3sh3tkjud.css": "svelte-css/CoinButton-1rej3sh3tkjud.css",
|
|
76
76
|
"/_mochi/css/ThemePicker-1ir8jyocm8mvi.css": "svelte-css/ThemePicker-1ir8jyocm8mvi.css",
|
|
77
|
-
"/_mochi/css/
|
|
77
|
+
"/_mochi/css/CoinButton-1rej3sh3tkjud.css": "svelte-css/CoinButton-1rej3sh3tkjud.css",
|
|
78
78
|
"/_mochi/css/TerminalPane-3bwgk6ulzq6b3.css": "svelte-css/TerminalPane-3bwgk6ulzq6b3.css",
|
|
79
|
-
"/_mochi/css/
|
|
80
|
-
"/_mochi/css/InstanceCard-3vnltxnbxkvay.css": "svelte-css/InstanceCard-3vnltxnbxkvay.css",
|
|
79
|
+
"/_mochi/css/ToastWrapper-1fbnym10rm2nv.css": "svelte-css/ToastWrapper-1fbnym10rm2nv.css",
|
|
81
80
|
"/_mochi/css/TopBar-2y76zxwehrfos.css": "svelte-css/TopBar-2y76zxwehrfos.css",
|
|
81
|
+
"/_mochi/css/InstanceCard-3vnltxnbxkvay.css": "svelte-css/InstanceCard-3vnltxnbxkvay.css",
|
|
82
|
+
"/_mochi/css/FolderBrowser-2pxp9z1o7zixk.css": "svelte-css/FolderBrowser-2pxp9z1o7zixk.css",
|
|
82
83
|
"/_mochi/css/ToastMessage-2jfwb2og2lfa8.css": "svelte-css/ToastMessage-2jfwb2og2lfa8.css",
|
|
83
|
-
"/_mochi/css/ToastWrapper-1fbnym10rm2nv.css": "svelte-css/ToastWrapper-1fbnym10rm2nv.css",
|
|
84
|
-
"/_mochi/css/BranchBox-1yqu1q2mgnjwz.css": "svelte-css/BranchBox-1yqu1q2mgnjwz.css",
|
|
85
|
-
"/_mochi/css/PortsBox-1t25b5yg7jva3.css": "svelte-css/PortsBox-1t25b5yg7jva3.css",
|
|
86
84
|
"/_mochi/css/Brand-1bh2b800g5vrn.css": "svelte-css/Brand-1bh2b800g5vrn.css",
|
|
87
|
-
"/_mochi/css/
|
|
88
|
-
"/_mochi/css/
|
|
85
|
+
"/_mochi/css/PortsBox-1t25b5yg7jva3.css": "svelte-css/PortsBox-1t25b5yg7jva3.css",
|
|
86
|
+
"/_mochi/css/BranchBox-1yqu1q2mgnjwz.css": "svelte-css/BranchBox-1yqu1q2mgnjwz.css",
|
|
89
87
|
"/_mochi/css/SettingsCog-jftyadvo64qo.css": "svelte-css/SettingsCog-jftyadvo64qo.css",
|
|
88
|
+
"/_mochi/css/CredMenu-9hwgeptp7crb.css": "svelte-css/CredMenu-9hwgeptp7crb.css",
|
|
89
|
+
"/_mochi/css/InstanceFilter-3q6x0jt0hcgpz.css": "svelte-css/InstanceFilter-3q6x0jt0hcgpz.css",
|
|
90
90
|
"/_mochi/import-css/index-0nwv3654.css": "import-css/index-0nwv3654.css",
|
|
91
91
|
"/_mochi/import-css/index-xndx4980.css": "import-css/index-xndx4980.css",
|
|
92
92
|
"/_mochi/import-css/xterm-1yy1hf1g.css": "import-css/xterm-1yy1hf1g.css",
|
|
@@ -101,8 +101,8 @@
|
|
|
101
101
|
"/_mochi/client/chunk-wvtkwfsh.js": "svelte-client/chunk-wvtkwfsh.js",
|
|
102
102
|
"/_mochi/client/_hydrate-AppShell_2rtnxn6pgp2rh-rx6arrv5.js": "svelte-client/_hydrate-AppShell_2rtnxn6pgp2rh-rx6arrv5.js",
|
|
103
103
|
"/_mochi/client/_hydrate-InstanceView_2ma45l9bybfhl-c9y47dat.js": "svelte-client/_hydrate-InstanceView_2ma45l9bybfhl-c9y47dat.js",
|
|
104
|
-
"/_mochi/client/_hydrate-SettingsView_8hmarwmf8onq-d61feq89.js": "svelte-client/_hydrate-SettingsView_8hmarwmf8onq-d61feq89.js",
|
|
105
104
|
"/_mochi/client/_hydrate-AvatarsView_257s9yq1wwr4f-jpjdmgpe.js": "svelte-client/_hydrate-AvatarsView_257s9yq1wwr4f-jpjdmgpe.js",
|
|
105
|
+
"/_mochi/client/_hydrate-SettingsView_8hmarwmf8onq-d61feq89.js": "svelte-client/_hydrate-SettingsView_8hmarwmf8onq-d61feq89.js",
|
|
106
106
|
"/_mochi/client/chunk-p20tkpde.js": "svelte-client/chunk-p20tkpde.js",
|
|
107
107
|
"/_mochi/client/chunk-0fjx3z3r.js": "svelte-client/chunk-0fjx3z3r.js"
|
|
108
108
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codebay",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.1",
|
|
4
4
|
"description": "Web UI for spinning up isolated devcontainer instances from any local folder, each with a browser-based VS Code (code-server) — built for parallel Claude Code workflows.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -150,6 +150,16 @@ export const INJECTIONS_DONE_FILE = '.codebay-injections-done';
|
|
|
150
150
|
*/
|
|
151
151
|
export const TERMINAL_LAUNCHED_MARKER = '.codebay-terminal-launched';
|
|
152
152
|
|
|
153
|
+
/**
|
|
154
|
+
* Far above the ~107s a real injection phase measures: at 60s this expired mid-phase and released
|
|
155
|
+
* the launcher into the window where `claude-code-update`'s `npm install -g` had unlinked the
|
|
156
|
+
* binary. `provision()` writes the sentinel on its failure path too, so a dead boot never waits here.
|
|
157
|
+
*/
|
|
158
|
+
const INJECTIONS_WAIT_SECONDS = 600;
|
|
159
|
+
|
|
160
|
+
/** Long enough to ride out an in-flight `npm install -g` of Claude Code. */
|
|
161
|
+
const CLAUDE_BINARY_WAIT_SECONDS = 60;
|
|
162
|
+
|
|
153
163
|
/**
|
|
154
164
|
* An auto-launched `claude` that starts mid-injection reads `~/.claude.json` before the trust
|
|
155
165
|
* keys land and clobbers them on its next rewrite, so hold it until the sentinel appears.
|
|
@@ -157,7 +167,20 @@ export const TERMINAL_LAUNCHED_MARKER = '.codebay-terminal-launched';
|
|
|
157
167
|
*/
|
|
158
168
|
const WAIT_FOR_INJECTIONS =
|
|
159
169
|
`[ -e "$HOME/${INJECTIONS_DONE_FILE}" ] || echo "Waiting for codebay setup to finish…"; ` +
|
|
160
|
-
`i=0; until [ -e "$HOME/${INJECTIONS_DONE_FILE}" ] || [ "$i" -ge
|
|
170
|
+
`i=0; until [ -e "$HOME/${INJECTIONS_DONE_FILE}" ] || [ "$i" -ge ${INJECTIONS_WAIT_SECONDS} ]; do sleep 1; i=$((i + 1)); done; ` +
|
|
171
|
+
`[ -e "$HOME/${INJECTIONS_DONE_FILE}" ] || echo "codebay: setup did not finish in time — starting anyway"; `;
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Never exec a binary that may not be there: the update injection unlinks `claude` while it
|
|
175
|
+
* reinstalls, and an IDE-mode project image is free to ship no Claude Code at all — both used to
|
|
176
|
+
* surface as a bare `command not found` under the "Waiting for codebay setup" line. The retry is
|
|
177
|
+
* skipped once the sentinel exists, since no injection can be mid-reinstall by then and a missing
|
|
178
|
+
* binary is permanent. No single quotes: this is spliced into the single-quoted tmux commands below.
|
|
179
|
+
*/
|
|
180
|
+
const launchClaude = (permissionMode: ClaudePermissionMode): string =>
|
|
181
|
+
`if [ ! -e "$HOME/${INJECTIONS_DONE_FILE}" ]; then i=0; until command -v claude >/dev/null 2>&1 || [ "$i" -ge ${CLAUDE_BINARY_WAIT_SECONDS} ]; do sleep 1; i=$((i + 1)); done; fi; ` +
|
|
182
|
+
`if command -v claude >/dev/null 2>&1; then claude ${claudePermissionFlags(permissionMode)}; ` +
|
|
183
|
+
`else echo "codebay: claude is not installed in this container — check the boot log, then Rebuild"; fi; `;
|
|
161
184
|
|
|
162
185
|
/**
|
|
163
186
|
* claude scans `~/.claude/ide/*.lock` once at startup and never retries, so it must not race
|
|
@@ -192,7 +215,7 @@ const terminalTask = (permissionMode: ClaudePermissionMode) => ({
|
|
|
192
215
|
type: 'shell',
|
|
193
216
|
command:
|
|
194
217
|
// `"$SHELL"` must reach tmux unexpanded; VS Code would substitute a `${…}` form first.
|
|
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}
|
|
218
|
+
`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}${launchClaude(permissionMode)}exec "$SHELL" -l'; fi; ` +
|
|
196
219
|
// Without tmux there's no run-once gate, and folderOpen re-fires on every workspace load.
|
|
197
220
|
'MARK="$HOME/' +
|
|
198
221
|
TERMINAL_LAUNCHED_MARKER +
|
|
@@ -200,7 +223,8 @@ const terminalTask = (permissionMode: ClaudePermissionMode) => ({
|
|
|
200
223
|
WAIT_FOR_INJECTIONS +
|
|
201
224
|
WAIT_FOR_IDE_BRIDGE +
|
|
202
225
|
SOURCE_INJECTED_ENV +
|
|
203
|
-
|
|
226
|
+
launchClaude(permissionMode) +
|
|
227
|
+
'exec ${env:SHELL} -l',
|
|
204
228
|
presentation: { reveal: 'always', panel: 'shared', focus: true },
|
|
205
229
|
runOptions: { runOn: 'folderOpen' },
|
|
206
230
|
problemMatcher: []
|
|
@@ -273,11 +297,12 @@ const ttydLaunchScript = (permissionMode: ClaudePermissionMode): string =>
|
|
|
273
297
|
WAIT_FOR_INJECTIONS +
|
|
274
298
|
'\n' +
|
|
275
299
|
`if command -v tmux >/dev/null 2>&1; then\n` +
|
|
276
|
-
` exec tmux new-session -A -s ${TMUX_SESSION} '${SOURCE_INJECTED_ENV}
|
|
300
|
+
` exec tmux new-session -A -s ${TMUX_SESSION} '${SOURCE_INJECTED_ENV}${launchClaude(permissionMode)}exec "$SHELL" -l'\n` +
|
|
277
301
|
'fi\n' +
|
|
278
302
|
SOURCE_INJECTED_ENV +
|
|
279
303
|
'\n' +
|
|
280
|
-
|
|
304
|
+
launchClaude(permissionMode) +
|
|
305
|
+
'\n' +
|
|
281
306
|
'exec "$SHELL" -l\n';
|
|
282
307
|
|
|
283
308
|
// ttyd defaults to read-only, so --writable is required for keyboard input. Guarded by `pgrep -x`
|
|
@@ -47,7 +47,7 @@ import {
|
|
|
47
47
|
writeOverrideConfig
|
|
48
48
|
} from './devcontainer.server.ts';
|
|
49
49
|
import { writeContainerFile } from './container-files.server.ts';
|
|
50
|
-
import { execInContainer } from './exec.server.ts';
|
|
50
|
+
import { execInContainer, type ExecTarget } from './exec.server.ts';
|
|
51
51
|
import { clearAttention, getAttention } from './bridge.server.ts';
|
|
52
52
|
import { proxyPathFor } from './proxy.server.ts';
|
|
53
53
|
import { resolveInjectionStages } from './injections.server.ts';
|
|
@@ -97,6 +97,27 @@ interface LiveState {
|
|
|
97
97
|
const globalForReg = globalThis as unknown as { __codebayRegistry?: Map<string, LiveState> };
|
|
98
98
|
const registry: Map<string, LiveState> = (globalForReg.__codebayRegistry ??= new Map());
|
|
99
99
|
|
|
100
|
+
/**
|
|
101
|
+
* The ids `provision()` is currently building. A `creating` row is only left alone while its boot
|
|
102
|
+
* is actually in flight — otherwise a server restart mid-boot would strand it there forever, since
|
|
103
|
+
* reconcile skips `creating` and `rebuildInstance` refuses to touch it.
|
|
104
|
+
*/
|
|
105
|
+
const globalForBoots = globalThis as unknown as { __codebayProvisioning?: Set<string> };
|
|
106
|
+
const inFlight: Set<string> = (globalForBoots.__codebayProvisioning ??= new Set());
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Whether reconcile may re-derive a row's status from Docker. A live boot owns its own row until it
|
|
110
|
+
* opens the instance itself; an orphaned `creating` row (the manager restarted mid-boot) must not
|
|
111
|
+
* be left there, since nothing else would ever move it and Rebuild refuses to touch a booting row.
|
|
112
|
+
*/
|
|
113
|
+
export function reconcilesFromDocker(
|
|
114
|
+
row: Pick<InstanceRow, 'id' | 'status'>,
|
|
115
|
+
building: ReadonlySet<string>
|
|
116
|
+
): boolean {
|
|
117
|
+
if (row.status === 'error') return false;
|
|
118
|
+
return row.status !== 'creating' || !building.has(row.id);
|
|
119
|
+
}
|
|
120
|
+
|
|
100
121
|
function live(id: string): LiveState {
|
|
101
122
|
let state = registry.get(id);
|
|
102
123
|
if (!state) {
|
|
@@ -333,6 +354,9 @@ function failInstance(id: string, err: unknown): void {
|
|
|
333
354
|
}
|
|
334
355
|
|
|
335
356
|
async function boot(row: InstanceRow, opts: { branch?: string } = {}): Promise<void> {
|
|
357
|
+
// Claims the row for the copy/clone phase, which runs ahead of the build; provision() re-claims
|
|
358
|
+
// it and owns the release from there.
|
|
359
|
+
inFlight.add(row.id);
|
|
336
360
|
try {
|
|
337
361
|
const sourceStart = Date.now();
|
|
338
362
|
if (isRepoUrl(row.source_path)) {
|
|
@@ -349,6 +373,7 @@ async function boot(row: InstanceRow, opts: { branch?: string } = {}): Promise<v
|
|
|
349
373
|
}
|
|
350
374
|
await seedDeclaredPorts(row);
|
|
351
375
|
} catch (err) {
|
|
376
|
+
inFlight.delete(row.id);
|
|
352
377
|
failInstance(row.id, err);
|
|
353
378
|
return;
|
|
354
379
|
}
|
|
@@ -396,6 +421,9 @@ async function rescueHijackedPort(row: InstanceRow): Promise<void> {
|
|
|
396
421
|
|
|
397
422
|
/** Never re-copies the workspace, so in-container edits survive a rebuild. */
|
|
398
423
|
async function provision(row: InstanceRow, opts: { noCache?: boolean } = {}): Promise<void> {
|
|
424
|
+
// Held so the `catch` can release the container-side launcher's wait on a boot that dies.
|
|
425
|
+
let containerId: string | null = null;
|
|
426
|
+
inFlight.add(row.id);
|
|
399
427
|
try {
|
|
400
428
|
// On a rebuild `devcontainer up --remove-existing-container` discards the old container's
|
|
401
429
|
// ~/.claude, so drain it first; a first boot has no container_id and skips straight past.
|
|
@@ -437,11 +465,13 @@ async function provision(row: InstanceRow, opts: { noCache?: boolean } = {}): Pr
|
|
|
437
465
|
);
|
|
438
466
|
}
|
|
439
467
|
|
|
468
|
+
containerId = result.containerId;
|
|
469
|
+
// The row deliberately stays `creating` until the injections below finish: `running` is what
|
|
470
|
+
// opens the IDE/terminal to the user, and a container whose setup is still landing isn't ready.
|
|
440
471
|
updateInstance(row.id, {
|
|
441
472
|
container_id: result.containerId,
|
|
442
473
|
remote_workspace_folder: result.remoteWorkspaceFolder ?? null,
|
|
443
474
|
remote_user: result.remoteUser ?? null,
|
|
444
|
-
status: 'running',
|
|
445
475
|
error: null
|
|
446
476
|
});
|
|
447
477
|
|
|
@@ -450,11 +480,6 @@ async function provision(row: InstanceRow, opts: { noCache?: boolean } = {}): Pr
|
|
|
450
480
|
row.container_id = result.containerId;
|
|
451
481
|
row.remote_workspace_folder = result.remoteWorkspaceFolder ?? null;
|
|
452
482
|
row.remote_user = result.remoteUser ?? null;
|
|
453
|
-
row.status = 'running';
|
|
454
|
-
|
|
455
|
-
// Start the health monitor now so the IDE mounts as soon as code-server answers,
|
|
456
|
-
// instead of after every injection plus a full reconcile/health poll cycle.
|
|
457
|
-
triggerReconcile();
|
|
458
483
|
|
|
459
484
|
const target = {
|
|
460
485
|
containerId: result.containerId,
|
|
@@ -478,22 +503,43 @@ async function provision(row: InstanceRow, opts: { noCache?: boolean } = {}): Pr
|
|
|
478
503
|
appendLog(row.id, `⏱ injections: ${elapsed(injectionsStart)}\n`);
|
|
479
504
|
|
|
480
505
|
// Unblocks the terminal launcher's wait; written even after ⚠s — its timeout is the fallback.
|
|
481
|
-
await
|
|
482
|
-
() => undefined
|
|
483
|
-
);
|
|
506
|
+
await writeInjectionsDone(row.id, target);
|
|
484
507
|
|
|
485
508
|
// postStartCommand ran before the injections above, and its launcher bails when the binary
|
|
486
509
|
// is missing — so a container whose build-time ttyd install failed got it too late to be
|
|
487
510
|
// started. No-ops (one accessibility probe) whenever postStart already brought the port up.
|
|
488
511
|
await relaunchSurface(row);
|
|
489
512
|
|
|
513
|
+
// Everything the instance needs is in place, so open it to the user: this is what lights up
|
|
514
|
+
// Open IDE/terminal, the proxy, and the health and log-capture monitors.
|
|
515
|
+
updateInstance(row.id, { status: 'running' });
|
|
516
|
+
row.status = 'running';
|
|
517
|
+
|
|
490
518
|
appendLog(row.id, `\n✓ Instance running — open it via the proxy at ${proxyPathFor(row.id)}\n`);
|
|
491
519
|
triggerReconcile();
|
|
492
520
|
} catch (err) {
|
|
521
|
+
// The launcher waits minutes for the sentinel, so a boot that will never write it must say so.
|
|
522
|
+
if (containerId)
|
|
523
|
+
await writeInjectionsDone(row.id, { containerId, remoteUser: row.remote_user });
|
|
493
524
|
failInstance(row.id, err);
|
|
525
|
+
} finally {
|
|
526
|
+
inFlight.delete(row.id);
|
|
494
527
|
}
|
|
495
528
|
}
|
|
496
529
|
|
|
530
|
+
/**
|
|
531
|
+
* `writeContainerFile` reports failure by return value rather than throwing, and a sentinel that
|
|
532
|
+
* never lands leaves the launcher waiting out its whole bound — so surface it in the boot log.
|
|
533
|
+
*/
|
|
534
|
+
async function writeInjectionsDone(id: string, target: ExecTarget): Promise<void> {
|
|
535
|
+
const res = await writeContainerFile(
|
|
536
|
+
target,
|
|
537
|
+
{ dir: '$HOME', name: INJECTIONS_DONE_FILE },
|
|
538
|
+
'done\n'
|
|
539
|
+
).catch((err: Error) => ({ ok: false, error: err.message }));
|
|
540
|
+
if (!res.ok) appendLog(id, `⚠ Could not mark setup complete in the container: ${res.error}\n`);
|
|
541
|
+
}
|
|
542
|
+
|
|
497
543
|
/** The first instance keeps the bare name; later collisions get a `#2`, `#3`, … suffix. */
|
|
498
544
|
function uniqueName(desired: string, excludeId?: string): string {
|
|
499
545
|
const taken = new Set(
|
|
@@ -563,10 +609,19 @@ export async function listInstances(): Promise<Instance[]> {
|
|
|
563
609
|
const branches = new Map<string, string | null>();
|
|
564
610
|
await Promise.all(
|
|
565
611
|
rows.map(async (row) => {
|
|
566
|
-
if (!row.container_id || row
|
|
612
|
+
if (!row.container_id || !reconcilesFromDocker(row, inFlight)) return;
|
|
567
613
|
const running = await isRunning(row.container_id);
|
|
568
614
|
const next: InstanceStatus = running ? 'running' : 'stopped';
|
|
569
615
|
if (next !== row.status) {
|
|
616
|
+
// Only reachable for a `creating` row whose boot died with the manager, and its
|
|
617
|
+
// injections will never finish — so stop the container-side launcher waiting on them.
|
|
618
|
+
if (row.status === 'creating') {
|
|
619
|
+
appendLog(row.id, `\n⚠ Setup was interrupted — Rebuild to run it again\n`);
|
|
620
|
+
void writeInjectionsDone(row.id, {
|
|
621
|
+
containerId: row.container_id,
|
|
622
|
+
remoteUser: row.remote_user
|
|
623
|
+
});
|
|
624
|
+
}
|
|
570
625
|
updateInstance(row.id, { status: next });
|
|
571
626
|
row.status = next;
|
|
572
627
|
}
|
|
@@ -672,7 +727,10 @@ export function removeForwardedPort(id: string, containerPort: number): Instance
|
|
|
672
727
|
export function rebuildInstance(id: string, opts: { noCache?: boolean } = {}): InstanceRow {
|
|
673
728
|
const row = getInstance(id);
|
|
674
729
|
if (!row) throw new Error('Instance not found');
|
|
675
|
-
if (
|
|
730
|
+
if (inFlight.has(id)) return row; // a build is already in flight
|
|
731
|
+
// Claimed before the broadcast below, which reconciles synchronously: an unclaimed `creating`
|
|
732
|
+
// row reads as orphaned, and reconcile would flip it straight back off `creating`.
|
|
733
|
+
inFlight.add(id);
|
|
676
734
|
updateInstance(id, { status: 'creating', error: null });
|
|
677
735
|
triggerReconcile();
|
|
678
736
|
const fresh = getInstance(id)!;
|