codebay 0.5.0 → 0.7.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 +286 -227
- package/.mochi/svelte-client/HydratableIsland-qyjx1d98.js +1 -0
- package/.mochi/svelte-client/_hydrate-AppShell_2rtnxn6pgp2rh-fkna7bj2.js +2 -0
- package/.mochi/svelte-client/_hydrate-AvatarsView_257s9yq1wwr4f-89wtkb86.js +13 -0
- package/.mochi/svelte-client/_hydrate-InstanceView_2ma45l9bybfhl-eamnbyqk.js +1 -0
- package/.mochi/svelte-client/{_hydrate-SettingsView_8hmarwmf8onq-r9vvc7rx.js → _hydrate-SettingsView_8hmarwmf8onq-kdra4mcb.js} +3 -3
- package/.mochi/svelte-client/chunk-330mrspw.js +2 -0
- package/.mochi/svelte-client/{chunk-dz1019cy.js → chunk-4jgkak13.js} +1 -1
- package/.mochi/svelte-client/chunk-fqg2qzj8.js +2 -0
- package/.mochi/svelte-client/chunk-k5m4kt20.js +2 -0
- package/.mochi/svelte-client/{chunk-nmf4dq86.js → chunk-qj7zy2d9.js} +1 -1
- package/.mochi/svelte-client/{chunk-ssa0ad2f.js → chunk-rpdphch7.js} +1 -1
- package/.mochi/svelte-compile/{App-9qjy14bc.server.js → App-6rcf0d74.server.js} +113 -46
- package/.mochi/svelte-compile/{Avatars-sq8mjn6k.server.js → Avatars-pzyz0tv3.server.js} +75 -15
- package/.mochi/svelte-compile/{Instance-13j59daw.server.js → Instance-49ksrp11.server.js} +15 -3
- package/.mochi/svelte-compile/{Settings-p0s2awg1.server.js → Settings-sz5d6qj7.server.js} +3 -3
- package/.mochi/svelte-compile/{chunk-dfe2km6a.js → chunk-q8rbesaw.js} +324 -123
- package/.mochi/svelte-css/Avatar-3pi30db6ozvf0.css +1 -0
- package/.mochi/svelte-css/AvatarEditor-2ewwtejaml4e2.css +1 -0
- package/.mochi/svelte-css/{AvatarsView-3hq37u4dgtljn.css → AvatarsView-2ughbzc26luyb.css} +1 -1
- package/.mochi/svelte-css/InstanceCard-3vnltxnbxkvay.css +1 -0
- package/.mochi/svelte-css/InstanceFilter-3q6x0jt0hcgpz.css +1 -0
- package/.mochi/svelte-css/{InstanceView-wq85bxtusont.css → InstanceView-3vzow3ttxzlh3.css} +1 -1
- package/README.md +2 -0
- package/package.json +2 -2
- package/src/avatars/anchor.ts +1 -0
- package/src/avatars/ant-head.ts +17 -0
- package/src/avatars/apple.ts +17 -0
- package/src/avatars/bear.ts +1 -0
- package/src/avatars/bee.ts +1 -0
- package/src/avatars/bodybuilder.ts +17 -0
- package/src/avatars/cat.ts +1 -0
- package/src/avatars/cherry.ts +1 -0
- package/src/avatars/crab.ts +1 -0
- package/src/avatars/crown.ts +1 -0
- package/src/avatars/diamond.ts +1 -0
- package/src/avatars/dog.ts +1 -0
- package/src/avatars/editor.ts +17 -0
- package/src/avatars/fish.ts +1 -0
- package/src/avatars/flower.ts +1 -0
- package/src/avatars/fox.ts +1 -0
- package/src/avatars/frog.ts +1 -0
- package/src/avatars/ghost.ts +1 -0
- package/src/avatars/heart.ts +8 -8
- package/src/avatars/horse.ts +17 -0
- package/src/avatars/index.ts +20 -0
- package/src/avatars/invader.ts +1 -0
- package/src/avatars/key.ts +1 -0
- package/src/avatars/lightning.ts +1 -0
- package/src/avatars/mushroom.ts +1 -0
- package/src/avatars/octopus.ts +1 -0
- package/src/avatars/owl.ts +1 -0
- package/src/avatars/penguin.ts +1 -0
- package/src/avatars/planet.ts +1 -0
- package/src/avatars/praying-mantis.ts +17 -0
- package/src/avatars/rabbit.ts +1 -0
- package/src/avatars/robot.ts +1 -0
- package/src/avatars/rocket.ts +1 -0
- package/src/avatars/serialize.ts +4 -3
- package/src/avatars/skull.ts +6 -6
- package/src/avatars/snail.ts +1 -0
- package/src/avatars/snowflake.ts +17 -0
- package/src/avatars/star.ts +1 -0
- package/src/avatars/strawberry.ts +17 -0
- package/src/avatars/target.ts +1 -0
- package/src/avatars/tree.ts +1 -0
- package/src/avatars/types.ts +6 -3
- package/src/avatars/vangogh-almond-blossoms.ts +17 -0
- package/src/avatars/wasp.ts +17 -0
- package/src/avatars/whale.ts +1 -0
- package/src/avatars/windmill.ts +17 -0
- package/src/components/AppShell.svelte +13 -0
- package/src/components/Avatar.svelte +4 -1
- package/src/components/AvatarEditor.svelte +83 -5
- package/src/components/AvatarsView.svelte +14 -1
- package/src/components/DashboardView.svelte +39 -4
- package/src/components/InstanceCard.svelte +55 -6
- package/src/components/InstanceFilter.svelte +68 -0
- package/src/components/InstanceView.svelte +41 -8
- package/src/components/SettingsView.svelte +2 -2
- package/src/components/TopBar.svelte +9 -1
- package/src/container-injections/attention-hooks.ts +19 -38
- package/src/container-injections/claude-aliases.ts +7 -23
- package/src/container-injections/claude-code-credentials.ts +12 -8
- package/src/container-injections/claude-code-custom.ts +35 -48
- package/src/container-injections/claude-code-ide-extension.ts +39 -0
- package/src/container-injections/claude-code-models.ts +8 -24
- package/src/container-injections/claude-model.ts +5 -28
- package/src/container-injections/claude-no-coauthor.ts +5 -17
- package/src/container-injections/claude-skip-permissions.ts +7 -17
- package/src/container-injections/claude-statusline.ts +16 -18
- package/src/container-injections/claude-trust.ts +73 -0
- package/src/container-injections/host-env-vars.ts +13 -24
- package/src/container-injections/tmux.ts +2 -11
- package/src/index.ts +2 -0
- package/src/lib/claude-settings.server.ts +35 -0
- package/src/lib/container-files.server.ts +219 -0
- package/src/lib/devcontainer.server.ts +38 -1
- package/src/lib/exec.server.ts +2 -7
- package/src/lib/injections.server.ts +4 -0
- package/src/lib/instances.server.ts +14 -0
- package/src/lib/theme.server.ts +7 -3
- package/src/shell.html +2 -0
- package/src/types.ts +3 -0
- package/.mochi/svelte-client/HydratableIsland-79z9xq4b.js +0 -1
- package/.mochi/svelte-client/_hydrate-AppShell_2rtnxn6pgp2rh-f5rzyjm8.js +0 -2
- package/.mochi/svelte-client/_hydrate-AvatarsView_257s9yq1wwr4f-fxyd5qx4.js +0 -13
- package/.mochi/svelte-client/_hydrate-InstanceView_2ma45l9bybfhl-nv31ptdp.js +0 -1
- package/.mochi/svelte-client/chunk-6eexkr6p.js +0 -2
- package/.mochi/svelte-client/chunk-7znwaf7t.js +0 -2
- package/.mochi/svelte-client/chunk-zmmb1c4b.js +0 -2
- package/.mochi/svelte-css/Avatar-24th63awm9mna.css +0 -1
- package/.mochi/svelte-css/AvatarEditor-1ryrs1md5g36y.css +0 -1
- package/.mochi/svelte-css/InstanceCard-16f77aysm7lgp.css +0 -1
|
@@ -2,8 +2,12 @@ import { existsSync, statSync } from 'node:fs';
|
|
|
2
2
|
import { readFile } from 'node:fs/promises';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { writeContainerFile } from '../lib/container-files.server.ts';
|
|
6
|
+
import {
|
|
7
|
+
claudeConfigFile,
|
|
8
|
+
mergeClaudeSettings,
|
|
9
|
+
readClaudeSettings
|
|
10
|
+
} from '../lib/claude-settings.server.ts';
|
|
7
11
|
import type { ContainerTarget, Injection } from '../lib/injections.server.ts';
|
|
8
12
|
|
|
9
13
|
/** Left unexpanded — Claude Code invokes this long after the injection has finished. */
|
|
@@ -65,23 +69,20 @@ export async function readStatusLineConfig(): Promise<StatusLineConfig | null> {
|
|
|
65
69
|
return { statusLine: { ...statusLine, command }, script };
|
|
66
70
|
}
|
|
67
71
|
|
|
68
|
-
/**
|
|
72
|
+
/** A referenced script is staged first, then the statusLine block is merged into settings.json. */
|
|
69
73
|
async function injectStatusLine(
|
|
70
74
|
target: ContainerTarget,
|
|
71
75
|
config: StatusLineConfig
|
|
72
76
|
): Promise<{ ok: boolean; error?: string }> {
|
|
73
77
|
if (config.script) {
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
'
|
|
77
|
-
|
|
78
|
-
|
|
78
|
+
const wrote = await writeContainerFile(
|
|
79
|
+
target,
|
|
80
|
+
claudeConfigFile('statusline.sh', '755'),
|
|
81
|
+
config.script
|
|
82
|
+
);
|
|
83
|
+
if (!wrote.ok) return wrote;
|
|
79
84
|
}
|
|
80
|
-
|
|
81
|
-
script: mergeClaudeSettingsScript(),
|
|
82
|
-
stdin: JSON.stringify({ statusLine: config.statusLine })
|
|
83
|
-
});
|
|
84
|
-
return res.ok ? { ok: true } : { ok: false, error: res.error };
|
|
85
|
+
return mergeClaudeSettings(target, { statusLine: config.statusLine });
|
|
85
86
|
}
|
|
86
87
|
|
|
87
88
|
export const claudeStatusline: Injection = {
|
|
@@ -115,10 +116,7 @@ export const claudeStatusline: Injection = {
|
|
|
115
116
|
},
|
|
116
117
|
|
|
117
118
|
async check(target) {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
'h=$(eval echo ~$(id -un)); d="${CLAUDE_CONFIG_DIR:-$h/.claude}"; ' +
|
|
121
|
-
'[ -s "$d/settings.json" ] && grep -q statusLine "$d/settings.json" && echo 1 || echo 0'
|
|
122
|
-
);
|
|
119
|
+
const settings = await readClaudeSettings(target);
|
|
120
|
+
return !!settings?.statusLine;
|
|
123
121
|
}
|
|
124
122
|
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { deepMerge, editJsonFile, readJsonFile } from '../lib/container-files.server.ts';
|
|
2
|
+
import {
|
|
3
|
+
CLAUDE_JSON_FILE,
|
|
4
|
+
mergeClaudeSettings,
|
|
5
|
+
readClaudeSettings
|
|
6
|
+
} from '../lib/claude-settings.server.ts';
|
|
7
|
+
import type { Injection } from '../lib/injections.server.ts';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Pre-accepts the three prompts `claude` shows on first launch, so a throwaway instance
|
|
11
|
+
* opens straight into a session: the folder-trust dialog and the MCP-server approval
|
|
12
|
+
* (both per-project, keyed by the path `claude` runs in), and the one-time Bypass
|
|
13
|
+
* Permissions acknowledgement (`--dangerously-skip-permissions` still trips it without this).
|
|
14
|
+
* Key names verified against the shipped `claude` binary.
|
|
15
|
+
*/
|
|
16
|
+
export function claudeTrustConfig(workspacePath: string | null): Record<string, unknown> {
|
|
17
|
+
const cfg: Record<string, unknown> = {
|
|
18
|
+
// Idempotent with the credentials injection; keeps this self-sufficient when auth is skipped.
|
|
19
|
+
hasCompletedOnboarding: true,
|
|
20
|
+
bypassPermissionsModeAccepted: true
|
|
21
|
+
};
|
|
22
|
+
if (workspacePath) {
|
|
23
|
+
cfg.projects = {
|
|
24
|
+
[workspacePath]: {
|
|
25
|
+
hasTrustDialogAccepted: true,
|
|
26
|
+
hasCompletedProjectOnboarding: true,
|
|
27
|
+
enableAllProjectMcpServers: true
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
return cfg;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** claude ≥2.1.220 keeps the bypass acknowledgement here; the `.claude.json` key stays for older versions. */
|
|
35
|
+
export const CLAUDE_TRUST_SETTINGS: Record<string, unknown> = {
|
|
36
|
+
skipDangerousModePermissionPrompt: true
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/** Safe here only because instances are throwaway, single-tenant sandboxes. */
|
|
40
|
+
export const claudeTrust: Injection = {
|
|
41
|
+
id: 'claude-trust',
|
|
42
|
+
label: 'Claude trust & MCP auto-accept',
|
|
43
|
+
|
|
44
|
+
async apply(target, log) {
|
|
45
|
+
log('Pre-accepting Claude trust, MCP, and bypass-permissions prompts…\n');
|
|
46
|
+
// deepMerge recurses into `projects`, so the per-project keys layer onto any existing entries.
|
|
47
|
+
const res = await editJsonFile(target, CLAUDE_JSON_FILE, (cur) =>
|
|
48
|
+
deepMerge(cur, claudeTrustConfig(target.instance.remote_workspace_folder))
|
|
49
|
+
);
|
|
50
|
+
if (!res.ok) {
|
|
51
|
+
log(`⚠ Claude trust injection failed: ${res.error}\n`);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const settings = await mergeClaudeSettings(target, CLAUDE_TRUST_SETTINGS);
|
|
55
|
+
log(
|
|
56
|
+
settings.ok
|
|
57
|
+
? '✓ Claude startup prompts pre-accepted\n'
|
|
58
|
+
: `⚠ Claude trust injection failed: ${settings.error}\n`
|
|
59
|
+
);
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
// Not keyed on `.claude.json`'s `bypassPermissionsModeAccepted`: claude ≥2.1.220 drops
|
|
63
|
+
// that key (and `enableAllProjectMcpServers`) when it rewrites the file after startup.
|
|
64
|
+
async check(target) {
|
|
65
|
+
const settings = await readClaudeSettings(target);
|
|
66
|
+
if (settings?.skipDangerousModePermissionPrompt !== true) return false;
|
|
67
|
+
const workspace = target.instance.remote_workspace_folder;
|
|
68
|
+
if (!workspace) return true;
|
|
69
|
+
const cfg = await readJsonFile(target, CLAUDE_JSON_FILE);
|
|
70
|
+
const projects = cfg?.projects as Record<string, Record<string, unknown>> | undefined;
|
|
71
|
+
return projects?.[workspace]?.hasTrustDialogAccepted === true;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { getOption } from '../lib/db.server.ts';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
containerFileExists,
|
|
4
|
+
shellEnvFile,
|
|
5
|
+
installShellEnvFile,
|
|
6
|
+
shellSingleQuote
|
|
7
|
+
} from '../lib/container-files.server.ts';
|
|
3
8
|
import type { ContainerTarget, Injection } from '../lib/injections.server.ts';
|
|
4
9
|
|
|
5
|
-
const
|
|
10
|
+
const ENV_FILE_NAME = '.codebay-host-env';
|
|
6
11
|
|
|
7
12
|
interface ResolvedVar {
|
|
8
13
|
name: string;
|
|
@@ -47,31 +52,15 @@ export function hostEnvVarsConfig(): {
|
|
|
47
52
|
}
|
|
48
53
|
|
|
49
54
|
/** Values ride `stdin` as one payload, so they reach neither argv nor `docker inspect`. */
|
|
50
|
-
|
|
55
|
+
function injectHostEnvVars(
|
|
51
56
|
target: ContainerTarget,
|
|
52
57
|
resolved: ResolvedVar[]
|
|
53
58
|
): Promise<{ ok: boolean; error?: string }> {
|
|
54
59
|
// Every interactive shell sources this file, so values must be single-quoted to stay literal.
|
|
55
|
-
const
|
|
56
|
-
.map(({ name, value }) => `export ${name}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
'set -e; f=$(eval echo "' +
|
|
60
|
-
ENV_FILE +
|
|
61
|
-
'"); ' +
|
|
62
|
-
'printf \'%s\\n\' "$CODEBAY_STDIN" > "$f"; chmod 600 "$f"; ' +
|
|
63
|
-
// The grep guard keeps a re-apply from stacking duplicate source lines.
|
|
64
|
-
'h=$(eval echo ~$(id -un)); src="[ -f \\"$f\\" ] && . \\"$f\\""; ' +
|
|
65
|
-
'for rc in "$h/.bashrc" "$h/.zshrc"; do ' +
|
|
66
|
-
'grep -qF "$src" "$rc" 2>/dev/null || printf \'%s\\n\' "$src" >> "$rc"; ' +
|
|
67
|
-
'done';
|
|
68
|
-
|
|
69
|
-
const res = await execInContainer(target, {
|
|
70
|
-
script,
|
|
71
|
-
stdin: payload,
|
|
72
|
-
args: ['host-env-vars']
|
|
73
|
-
});
|
|
74
|
-
return res.ok ? { ok: true } : { ok: false, error: res.error };
|
|
60
|
+
const content =
|
|
61
|
+
resolved.map(({ name, value }) => `export ${name}=${shellSingleQuote(value)}`).join('\n') +
|
|
62
|
+
'\n';
|
|
63
|
+
return installShellEnvFile(target, ENV_FILE_NAME, content);
|
|
75
64
|
}
|
|
76
65
|
|
|
77
66
|
/**
|
|
@@ -103,6 +92,6 @@ export const hostEnvVars: Injection = {
|
|
|
103
92
|
},
|
|
104
93
|
|
|
105
94
|
async check(target) {
|
|
106
|
-
return
|
|
95
|
+
return containerFileExists(target, shellEnvFile(ENV_FILE_NAME));
|
|
107
96
|
}
|
|
108
97
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { checkPresence, execInContainer } from '../lib/exec.server.ts';
|
|
2
|
+
import { appendLinesIfAbsent } from '../lib/container-files.server.ts';
|
|
2
3
|
import type { Injection } from '../lib/injections.server.ts';
|
|
3
4
|
|
|
4
5
|
/** apt-get is the real path (the default image is Debian); the rest cover project images. */
|
|
@@ -26,13 +27,6 @@ export const TMUX_CONF_LINES = [
|
|
|
26
27
|
'bind m set -g mouse \\; display-message "mouse: #{?mouse,on,off}"'
|
|
27
28
|
];
|
|
28
29
|
|
|
29
|
-
/** Lines arrive as `$@` so no conf text is interpolated into the loop body. */
|
|
30
|
-
const CONF_SCRIPT =
|
|
31
|
-
'h=$(eval echo ~$(id -un)); f="$h/.tmux.conf"; ' +
|
|
32
|
-
'for line in "$@"; do ' +
|
|
33
|
-
'grep -qF "$line" "$f" 2>/dev/null || printf \'%s\\n\' "$line" >> "$f"; ' +
|
|
34
|
-
'done';
|
|
35
|
-
|
|
36
30
|
const CHECK_SCRIPT = 'command -v tmux >/dev/null 2>&1 && echo 1 || echo 0';
|
|
37
31
|
|
|
38
32
|
/**
|
|
@@ -54,10 +48,7 @@ export const tmux: Injection = {
|
|
|
54
48
|
log(`⚠ tmux install failed: ${install.error} — terminal falls back to non-persistent mode\n`);
|
|
55
49
|
return;
|
|
56
50
|
}
|
|
57
|
-
const conf = await
|
|
58
|
-
script: CONF_SCRIPT,
|
|
59
|
-
args: ['tmux-conf', ...TMUX_CONF_LINES]
|
|
60
|
-
});
|
|
51
|
+
const conf = await appendLinesIfAbsent(target, [{ name: '.tmux.conf' }], TMUX_CONF_LINES);
|
|
61
52
|
log(conf.ok ? '✓ tmux installed\n' : `⚠ tmux conf setup failed: ${conf.error}\n`);
|
|
62
53
|
},
|
|
63
54
|
|
package/src/index.ts
CHANGED
|
@@ -27,6 +27,8 @@ ensureMochiKey();
|
|
|
27
27
|
await Mochi.serve({
|
|
28
28
|
port: PORT,
|
|
29
29
|
hostname: HOST,
|
|
30
|
+
// Bun defaults to 10s and aborts slower form POSTs mid-flight; passed through to Bun.serve.
|
|
31
|
+
idleTimeout: 120,
|
|
30
32
|
development: process.env.MODE === 'development',
|
|
31
33
|
htmlShell: './src/shell.html',
|
|
32
34
|
handle: sequence(basicAuth, themeHandle),
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ExecTarget } from './exec.server.ts';
|
|
2
|
+
import {
|
|
3
|
+
deepMerge,
|
|
4
|
+
editJsonFile,
|
|
5
|
+
readJsonFile,
|
|
6
|
+
type ContainerFile
|
|
7
|
+
} from './container-files.server.ts';
|
|
8
|
+
|
|
9
|
+
/** A file in `~/.claude` (or wherever `CLAUDE_CONFIG_DIR` points), e.g. settings.json or .bridge-header. */
|
|
10
|
+
export const claudeConfigFile = (name: string, mode?: string): ContainerFile => ({
|
|
11
|
+
dir: '${CLAUDE_CONFIG_DIR:-$h/.claude}',
|
|
12
|
+
name,
|
|
13
|
+
mode
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
/** `~/.claude/settings.json`, honoring a `CLAUDE_CONFIG_DIR` override. */
|
|
17
|
+
export const CLAUDE_SETTINGS_FILE: ContainerFile = claudeConfigFile('settings.json');
|
|
18
|
+
|
|
19
|
+
/** `~/.claude.json` sits beside the config dir, not inside it — hence a different dir expr. */
|
|
20
|
+
export const CLAUDE_JSON_FILE: ContainerFile = {
|
|
21
|
+
dir: '${CLAUDE_CONFIG_DIR:-$h}',
|
|
22
|
+
name: '.claude.json'
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/** Deep-merges a patch into settings.json, letting the settings.json injections compose in any order. */
|
|
26
|
+
export function mergeClaudeSettings(
|
|
27
|
+
target: ExecTarget,
|
|
28
|
+
patch: Record<string, unknown>
|
|
29
|
+
): Promise<{ ok: boolean; error?: string }> {
|
|
30
|
+
return editJsonFile(target, CLAUDE_SETTINGS_FILE, (current) => deepMerge(current, patch));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function readClaudeSettings(target: ExecTarget): Promise<Record<string, unknown> | null> {
|
|
34
|
+
return readJsonFile<Record<string, unknown>>(target, CLAUDE_SETTINGS_FILE);
|
|
35
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { checkPresence, execInContainer, type ExecTarget } from './exec.server.ts';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A file inside a container, addressed by a shell expr for its dir (`$h` = the exec user's home) + name.
|
|
5
|
+
* Every field is interpolated into a bash script, so callers must pass literal, trusted values only.
|
|
6
|
+
*/
|
|
7
|
+
export interface ContainerFile {
|
|
8
|
+
/** Shell expr for the containing dir; `$h` is bound to the resolved home. Defaults to `$h`. */
|
|
9
|
+
dir?: string;
|
|
10
|
+
name: string;
|
|
11
|
+
/** chmod mode applied on write. */
|
|
12
|
+
mode?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Resolves the exec user's home the same way every injection historically has. */
|
|
16
|
+
const HOME_PRELUDE = 'h=$(eval echo ~$(id -un)); ';
|
|
17
|
+
|
|
18
|
+
const filePath = (file: ContainerFile): string => `${file.dir ?? '$h'}/${file.name}`;
|
|
19
|
+
|
|
20
|
+
/** The two rc files either shell may open, so an interactive-shell tweak must reach both. */
|
|
21
|
+
export const SHELL_RC_FILES: ContainerFile[] = [{ name: '.bashrc' }, { name: '.zshrc' }];
|
|
22
|
+
|
|
23
|
+
/** `bash -lc` runs profile scripts whose stdout precedes ours, so reads mark where real output starts. */
|
|
24
|
+
const READ_MARKER = '__CODEBAY_READ__';
|
|
25
|
+
|
|
26
|
+
export function readFileScript(file: ContainerFile): string {
|
|
27
|
+
// A status char follows the marker: A(bsent), O(k)+content, E (exists but unreadable).
|
|
28
|
+
return (
|
|
29
|
+
`${HOME_PRELUDE}f="${filePath(file)}"; ` +
|
|
30
|
+
`if [ ! -e "$f" ]; then printf '%sA' '${READ_MARKER}'; ` +
|
|
31
|
+
`elif c=$(cat "$f" 2>/dev/null); then printf '%sO%s' '${READ_MARKER}' "$c"; ` +
|
|
32
|
+
`else printf '%sE' '${READ_MARKER}'; fi`
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
type ContainerRead = { ok: true; content: string | null } | { ok: false; error: string };
|
|
37
|
+
|
|
38
|
+
/** Slices `readFileScript` output at the marker, so profile noise can't masquerade as file content. */
|
|
39
|
+
export function parseReadScriptOutput(stdout: string, name: string): ContainerRead {
|
|
40
|
+
const at = stdout.indexOf(READ_MARKER);
|
|
41
|
+
if (at === -1) return { ok: false, error: `read of ${name} produced no marker` };
|
|
42
|
+
const payload = stdout.slice(at + READ_MARKER.length);
|
|
43
|
+
if (payload[0] === 'A') return { ok: true, content: null };
|
|
44
|
+
if (payload[0] === 'O') return { ok: true, content: payload.slice(1) };
|
|
45
|
+
return { ok: false, error: `${name} exists but is unreadable` };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function writeFileScript(file: ContainerFile): string {
|
|
49
|
+
const mode = file.mode ?? '644';
|
|
50
|
+
// `set -e` so a partial write can't exit 0 and get reported as a success.
|
|
51
|
+
return (
|
|
52
|
+
`set -e; ${HOME_PRELUDE}f="${filePath(file)}"; mkdir -p "$(dirname "$f")"; ` +
|
|
53
|
+
`printf '%s' "$CODEBAY_STDIN" > "$f"; chmod ${mode} "$f"`
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Single-quotes a value for a sourced shell file so whatever it contains stays literal. */
|
|
58
|
+
export const shellSingleQuote = (value: string): string => `'${value.replaceAll("'", "'\\''")}'`;
|
|
59
|
+
|
|
60
|
+
/** Lines arrive as `$@` so none is interpolated into the loop body; each file gets a `grep -qF` guard. */
|
|
61
|
+
export function appendLinesScript(files: ContainerFile[]): string {
|
|
62
|
+
const paths = files.map((f) => `"${filePath(f)}"`).join(' ');
|
|
63
|
+
// `set -e` so a failed append (readonly fs, perms) can't exit 0 and get reported as a success.
|
|
64
|
+
return (
|
|
65
|
+
`set -e; ${HOME_PRELUDE}for line in "$@"; do ` +
|
|
66
|
+
`for f in ${paths}; do ` +
|
|
67
|
+
`mkdir -p "$(dirname "$f")"; ` +
|
|
68
|
+
`grep -qF "$line" "$f" 2>/dev/null || printf '%s\\n' "$line" >> "$f"; ` +
|
|
69
|
+
`done; done`
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Read that distinguishes "absent" (`content: null`) from "couldn't read" (`ok: false`). */
|
|
74
|
+
export async function readContainerFileResult(
|
|
75
|
+
target: ExecTarget,
|
|
76
|
+
file: ContainerFile
|
|
77
|
+
): Promise<ContainerRead> {
|
|
78
|
+
const res = await execInContainer(target, { script: readFileScript(file), capture: true });
|
|
79
|
+
if (!res.ok) return { ok: false, error: res.error ?? `read of ${file.name} failed` };
|
|
80
|
+
return parseReadScriptOutput(res.stdout, file.name);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Returns the file's contents, or null when it's absent, empty, or unreadable. Note: output capture trims trailing whitespace (incl. trailing newlines), so a whitespace-only file reads as null — fine for JSON callers, but don't use this where exact bytes matter. */
|
|
84
|
+
export async function readContainerFile(
|
|
85
|
+
target: ExecTarget,
|
|
86
|
+
file: ContainerFile
|
|
87
|
+
): Promise<string | null> {
|
|
88
|
+
const res = await readContainerFileResult(target, file);
|
|
89
|
+
return res.ok && res.content ? res.content : null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function writeContainerFile(
|
|
93
|
+
target: ExecTarget,
|
|
94
|
+
file: ContainerFile,
|
|
95
|
+
content: string
|
|
96
|
+
): Promise<{ ok: boolean; error?: string }> {
|
|
97
|
+
const res = await execInContainer(target, { script: writeFileScript(file), stdin: content });
|
|
98
|
+
return res.ok ? { ok: true } : { ok: false, error: res.error };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Appends each line to every file it's missing from — the shared idempotent rc/conf write. */
|
|
102
|
+
export async function appendLinesIfAbsent(
|
|
103
|
+
target: ExecTarget,
|
|
104
|
+
files: ContainerFile[],
|
|
105
|
+
lines: string[]
|
|
106
|
+
): Promise<{ ok: boolean; error?: string }> {
|
|
107
|
+
if (!lines.length) return { ok: true };
|
|
108
|
+
const res = await execInContainer(target, {
|
|
109
|
+
script: appendLinesScript(files),
|
|
110
|
+
args: ['append-lines', ...lines]
|
|
111
|
+
});
|
|
112
|
+
return res.ok ? { ok: true } : { ok: false, error: res.error };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** A line counts as present when any listed file carries it — the either-rc-file check the injections have always used. */
|
|
116
|
+
export function linesPresentScript(files: ContainerFile[]): string {
|
|
117
|
+
const greps = files.map((f) => `grep -qF "$line" "${filePath(f)}" 2>/dev/null`).join(' || ');
|
|
118
|
+
return `${HOME_PRELUDE}for line in "$@"; do ${greps} || { echo 0; exit 0; }; done; echo 1`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** The `check()` counterpart of `appendLinesIfAbsent`. */
|
|
122
|
+
export function linesPresent(
|
|
123
|
+
target: ExecTarget,
|
|
124
|
+
files: ContainerFile[],
|
|
125
|
+
lines: string[]
|
|
126
|
+
): Promise<boolean> {
|
|
127
|
+
return checkPresence(target, linesPresentScript(files), ['lines-present', ...lines]);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** True when the file exists and is non-empty — a content-free probe for `check()`. */
|
|
131
|
+
export function containerFileExists(target: ExecTarget, file: ContainerFile): Promise<boolean> {
|
|
132
|
+
return checkPresence(
|
|
133
|
+
target,
|
|
134
|
+
`${HOME_PRELUDE}f="${filePath(file)}"; [ -s "$f" ] && echo 1 || echo 0`
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** `$HOME`-addressed so the write, the `check()` probe, and the sourced rc line all resolve one path. */
|
|
139
|
+
export const shellEnvFile = (name: string): ContainerFile => ({ dir: '$HOME', name, mode: '600' });
|
|
140
|
+
|
|
141
|
+
/** Writes a mode-600 env file in the home dir and guards a source line into both rc files. */
|
|
142
|
+
export async function installShellEnvFile(
|
|
143
|
+
target: ExecTarget,
|
|
144
|
+
name: string,
|
|
145
|
+
content: string
|
|
146
|
+
): Promise<{ ok: boolean; error?: string }> {
|
|
147
|
+
const wrote = await writeContainerFile(target, shellEnvFile(name), content);
|
|
148
|
+
if (!wrote.ok) return wrote;
|
|
149
|
+
const path = `$HOME/${name}`;
|
|
150
|
+
return appendLinesIfAbsent(target, SHELL_RC_FILES, [`[ -f "${path}" ] && . "${path}"`]);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function isPlainObject(v: unknown): v is Record<string, unknown> {
|
|
154
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Mirrors jq's `.[0] * .[1]`: recurse into plain objects, replace arrays and scalars with the patch. */
|
|
158
|
+
export function deepMerge<T extends Record<string, unknown> = Record<string, unknown>>(
|
|
159
|
+
base: Record<string, unknown>,
|
|
160
|
+
patch: Record<string, unknown>
|
|
161
|
+
): T {
|
|
162
|
+
const out: Record<string, unknown> = { ...base };
|
|
163
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
164
|
+
// A `__proto__` key in parsed JSON would pollute Object.prototype via plain assignment.
|
|
165
|
+
if (key === '__proto__') continue;
|
|
166
|
+
const current = out[key];
|
|
167
|
+
out[key] = isPlainObject(value) && isPlainObject(current) ? deepMerge(current, value) : value;
|
|
168
|
+
}
|
|
169
|
+
return out as T;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
type JsonRead = { ok: true; data: Record<string, unknown> | null } | { ok: false; error: string };
|
|
173
|
+
|
|
174
|
+
/** Absent/empty means "start fresh"; anything else must parse to an object, so callers can refuse destructive fallbacks. */
|
|
175
|
+
export function parseJsonRead(raw: string | null, name: string): JsonRead {
|
|
176
|
+
if (raw === null || raw === '') return { ok: true, data: null };
|
|
177
|
+
try {
|
|
178
|
+
const data: unknown = JSON.parse(raw);
|
|
179
|
+
if (!isPlainObject(data)) return { ok: false, error: `${name} is not a JSON object` };
|
|
180
|
+
return { ok: true, data };
|
|
181
|
+
} catch {
|
|
182
|
+
return { ok: false, error: `${name} exists but is not valid JSON` };
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Parses the container file, treating an absent or invalid file as "no JSON" (null). */
|
|
187
|
+
export async function readJsonFile<T = Record<string, unknown>>(
|
|
188
|
+
target: ExecTarget,
|
|
189
|
+
file: ContainerFile
|
|
190
|
+
): Promise<T | null> {
|
|
191
|
+
const parsed = parseJsonRead(await readContainerFile(target, file), file.name);
|
|
192
|
+
return parsed.ok ? (parsed.data as T | null) : null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export async function writeJsonFile(
|
|
196
|
+
target: ExecTarget,
|
|
197
|
+
file: ContainerFile,
|
|
198
|
+
data: unknown
|
|
199
|
+
): Promise<{ ok: boolean; error?: string }> {
|
|
200
|
+
return writeContainerFile(target, file, JSON.stringify(data, null, 2));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Read-modify-write: an absent/empty file starts from `{}`, but a failed read or unparseable
|
|
205
|
+
* existing content aborts the edit — overwriting what we couldn't read would silently destroy it.
|
|
206
|
+
*/
|
|
207
|
+
export async function editJsonFile(
|
|
208
|
+
target: ExecTarget,
|
|
209
|
+
file: ContainerFile,
|
|
210
|
+
edit: (current: Record<string, unknown>) => Record<string, unknown> | void
|
|
211
|
+
): Promise<{ ok: boolean; error?: string }> {
|
|
212
|
+
const read = await readContainerFileResult(target, file);
|
|
213
|
+
if (!read.ok) return { ok: false, error: `read before edit failed: ${read.error}` };
|
|
214
|
+
const parsed = parseJsonRead(read.content, file.name);
|
|
215
|
+
if (!parsed.ok) return { ok: false, error: parsed.error };
|
|
216
|
+
const current = parsed.data ?? {};
|
|
217
|
+
const next = edit(current) ?? current;
|
|
218
|
+
return writeJsonFile(target, file, next);
|
|
219
|
+
}
|
|
@@ -3,6 +3,7 @@ import { existsSync, statSync } from 'node:fs';
|
|
|
3
3
|
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
|
+
import { EXTENSION_ID as CLAUDE_CODE_EXTENSION_ID } from '../container-injections/claude-code-ide-extension.ts';
|
|
6
7
|
import {
|
|
7
8
|
CODE_SERVER_PORT,
|
|
8
9
|
DEFAULT_IMAGE,
|
|
@@ -62,6 +63,13 @@ const EXCLUDE_MARKER_END = '# <<< codebay <<<';
|
|
|
62
63
|
|
|
63
64
|
const CODE_SERVER_SETTINGS = {
|
|
64
65
|
'workbench.colorTheme': 'Default Dark Modern',
|
|
66
|
+
// Stop VS Code re-resolving the theme from the OS/browser color scheme at runtime —
|
|
67
|
+
// that re-resolution (fired on focus/tab-switch) is what randomly flips the editor to
|
|
68
|
+
// light. Pinning both preferred themes to dark keeps either branch dark even if it does.
|
|
69
|
+
'window.autoDetectColorScheme': false,
|
|
70
|
+
'window.autoDetectHighContrast': false,
|
|
71
|
+
'workbench.preferredDarkColorTheme': 'Default Dark Modern',
|
|
72
|
+
'workbench.preferredLightColorTheme': 'Default Dark Modern',
|
|
65
73
|
'workbench.secondarySideBar.defaultVisibility': 'hidden',
|
|
66
74
|
'chat.commandCenter.enabled': false,
|
|
67
75
|
// Instances are throwaway sandboxes — never nag to install recommended extensions.
|
|
@@ -78,6 +86,26 @@ const CODE_SERVER_SETTINGS = {
|
|
|
78
86
|
|
|
79
87
|
const TMUX_SESSION = 'codebay';
|
|
80
88
|
|
|
89
|
+
/** Written to `$HOME` when the post-up injection sequence finishes; the terminal launcher waits on it. */
|
|
90
|
+
export const INJECTIONS_DONE_FILE = '.codebay-injections-done';
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* An auto-launched `claude` that starts mid-injection reads `~/.claude.json` before the trust
|
|
94
|
+
* keys land and clobbers them on its next rewrite, so hold it until the sentinel appears.
|
|
95
|
+
* Bounded so a failed boot (sentinel never written) still yields a usable terminal.
|
|
96
|
+
*/
|
|
97
|
+
const WAIT_FOR_INJECTIONS =
|
|
98
|
+
`[ -e "$HOME/${INJECTIONS_DONE_FILE}" ] || echo "Waiting for codebay setup to finish…"; ` +
|
|
99
|
+
`i=0; until [ -e "$HOME/${INJECTIONS_DONE_FILE}" ] || [ "$i" -ge 60 ]; do sleep 1; i=$((i + 1)); done; `;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* claude scans `~/.claude/ide/*.lock` once at startup and never retries, so it must not race
|
|
103
|
+
* the code-server extension host writing that lock — hold it briefly until the bridge appears.
|
|
104
|
+
* Bounded so an offline/uninstalled instance (lock never written) still yields a usable terminal.
|
|
105
|
+
* No single quotes: this is spliced into the single-quoted tmux command string below.
|
|
106
|
+
*/
|
|
107
|
+
const WAIT_FOR_IDE_BRIDGE = `i=0; until ls "$HOME/.claude/ide/"*.lock >/dev/null 2>&1 || [ "$i" -ge 30 ]; do sleep 1; i=$((i + 1)); done; `;
|
|
108
|
+
|
|
81
109
|
/**
|
|
82
110
|
* Runs under tmux so Claude survives the browser closing — code-server reaps the
|
|
83
111
|
* detached PTY, which only kills the tmux client. `-A` doubles as the run-once gate.
|
|
@@ -87,9 +115,11 @@ const TERMINAL_TASK = {
|
|
|
87
115
|
type: 'shell',
|
|
88
116
|
command:
|
|
89
117
|
// `"$SHELL"` must reach tmux unexpanded; VS Code would substitute a `${…}` form first.
|
|
90
|
-
`if command -v tmux >/dev/null 2>&1; then exec tmux new-session -A -s ${TMUX_SESSION} 'claude --dangerously-skip-permissions; exec "$SHELL" -l'; fi; ` +
|
|
118
|
+
`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 --dangerously-skip-permissions; exec "$SHELL" -l'; fi; ` +
|
|
91
119
|
// Without tmux there's no run-once gate, and folderOpen re-fires on every workspace load.
|
|
92
120
|
'MARK="$HOME/.codebay-terminal-launched"; [ -e "$MARK" ] && exit 0; touch "$MARK"; ' +
|
|
121
|
+
WAIT_FOR_INJECTIONS +
|
|
122
|
+
WAIT_FOR_IDE_BRIDGE +
|
|
93
123
|
'claude --dangerously-skip-permissions; exec ${env:SHELL} -l',
|
|
94
124
|
presentation: { reveal: 'always', panel: 'shared', focus: true },
|
|
95
125
|
runOptions: { runOn: 'folderOpen' },
|
|
@@ -101,8 +131,15 @@ const CODE_SERVER_APPLY_SETTINGS =
|
|
|
101
131
|
`cp -f \\"$PWD/.devcontainer/${CODE_SERVER_SETTINGS_FILE}\\" ` +
|
|
102
132
|
`~/.local/share/code-server/User/settings.json 2>/dev/null;`;
|
|
103
133
|
|
|
134
|
+
// Runs before code-server first launches so the extension host activates it on the first window
|
|
135
|
+
// (the /ide bridge, in-container over localhost); best-effort, offline-tolerant, skipped if present.
|
|
136
|
+
const CODE_SERVER_INSTALL_EXT =
|
|
137
|
+
`ls -d ~/.local/share/code-server/extensions/${CLAUDE_CODE_EXTENSION_ID}-* >/dev/null 2>&1 || ` +
|
|
138
|
+
`code-server --install-extension ${CLAUDE_CODE_EXTENSION_ID} >/tmp/code-server-ext.log 2>&1 || true; `;
|
|
139
|
+
|
|
104
140
|
const CODE_SERVER_LAUNCH =
|
|
105
141
|
`bash -c "${CODE_SERVER_APPLY_SETTINGS} ` +
|
|
142
|
+
`${CODE_SERVER_INSTALL_EXT} ` +
|
|
106
143
|
// The bare default image may not export SHELL, which the Terminal task needs.
|
|
107
144
|
`export SHELL=\\"\${SHELL:-/bin/bash}\\"; ` +
|
|
108
145
|
`pgrep -f 'code-server.*${CODE_SERVER_PORT}' >/dev/null 2>&1 || ` +
|
package/src/lib/exec.server.ts
CHANGED
|
@@ -82,11 +82,6 @@ export async function checkPresence(
|
|
|
82
82
|
args?: string[]
|
|
83
83
|
): Promise<boolean> {
|
|
84
84
|
const res = await execInContainer(target, { script, args, capture: true });
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
/** Only for secret-only files; injections mixing secret and non-secret content roll their own. */
|
|
89
|
-
export function writeSecretFileScript(dirExpr: string, filename: string, mode = '600'): string {
|
|
90
|
-
const path = `${dirExpr}/${filename}`;
|
|
91
|
-
return `mkdir -p "${dirExpr}"; printf '%s' "$CODEBAY_STDIN" > "${path}"; chmod ${mode} "${path}";`;
|
|
85
|
+
// The login shell's profile noise precedes the probe's echo, so only the last line counts.
|
|
86
|
+
return res.ok && res.stdout.split('\n').at(-1)?.trim() === '1';
|
|
92
87
|
}
|
|
@@ -6,12 +6,14 @@ import { tmux } from '../container-injections/tmux.ts';
|
|
|
6
6
|
import { gitIdentity } from '../container-injections/git-identity.ts';
|
|
7
7
|
import { claudeCodeCredentials } from '../container-injections/claude-code-credentials.ts';
|
|
8
8
|
import { claudeCodeCustom } from '../container-injections/claude-code-custom.ts';
|
|
9
|
+
import { claudeCodeIdeExtension } from '../container-injections/claude-code-ide-extension.ts';
|
|
9
10
|
import { claudeCodeModels } from '../container-injections/claude-code-models.ts';
|
|
10
11
|
import { githubCredentials } from '../container-injections/github-credentials.ts';
|
|
11
12
|
import { attentionHooks } from '../container-injections/attention-hooks.ts';
|
|
12
13
|
import { claudeStatusline } from '../container-injections/claude-statusline.ts';
|
|
13
14
|
import { claudeModel } from '../container-injections/claude-model.ts';
|
|
14
15
|
import { claudeSkipPermissions } from '../container-injections/claude-skip-permissions.ts';
|
|
16
|
+
import { claudeTrust } from '../container-injections/claude-trust.ts';
|
|
15
17
|
import { claudeAliases } from '../container-injections/claude-aliases.ts';
|
|
16
18
|
import { claudeNoCoauthor } from '../container-injections/claude-no-coauthor.ts';
|
|
17
19
|
import { hostEnvVars } from '../container-injections/host-env-vars.ts';
|
|
@@ -52,11 +54,13 @@ const BASE_INJECTIONS_HEAD: Injection[] = [
|
|
|
52
54
|
const BASE_INJECTIONS_TAIL: Injection[] = [
|
|
53
55
|
// Self-skips unless manual override is on and LiteLLM off, so it's safe in the always-run tail.
|
|
54
56
|
claudeCodeModels,
|
|
57
|
+
claudeCodeIdeExtension,
|
|
55
58
|
githubCredentials,
|
|
56
59
|
attentionHooks,
|
|
57
60
|
claudeStatusline,
|
|
58
61
|
claudeModel,
|
|
59
62
|
claudeSkipPermissions,
|
|
63
|
+
claudeTrust,
|
|
60
64
|
claudeAliases,
|
|
61
65
|
claudeNoCoauthor,
|
|
62
66
|
hostEnvVars
|
|
@@ -38,9 +38,11 @@ import {
|
|
|
38
38
|
copyWorkspace,
|
|
39
39
|
devcontainerCliAvailable,
|
|
40
40
|
devcontainerUp,
|
|
41
|
+
INJECTIONS_DONE_FILE,
|
|
41
42
|
readDeclaredContainerPorts,
|
|
42
43
|
writeOverrideConfig
|
|
43
44
|
} from './devcontainer.server.ts';
|
|
45
|
+
import { writeContainerFile } from './container-files.server.ts';
|
|
44
46
|
import { clearAttention, getAttention } from './bridge.server.ts';
|
|
45
47
|
import { proxyPathFor } from './proxy.server.ts';
|
|
46
48
|
import { resolveInjections } from './injections.server.ts';
|
|
@@ -321,6 +323,13 @@ async function provision(row: InstanceRow, opts: { noCache?: boolean } = {}): Pr
|
|
|
321
323
|
error: null
|
|
322
324
|
});
|
|
323
325
|
|
|
326
|
+
// updateInstance writes the DB, not `row`; mirror the fields so `target.instance`
|
|
327
|
+
// (e.g. remote_workspace_folder, which claude-trust keys on) isn't the stale pre-boot row.
|
|
328
|
+
row.container_id = result.containerId;
|
|
329
|
+
row.remote_workspace_folder = result.remoteWorkspaceFolder ?? null;
|
|
330
|
+
row.remote_user = result.remoteUser ?? null;
|
|
331
|
+
row.status = 'running';
|
|
332
|
+
|
|
324
333
|
const target = {
|
|
325
334
|
containerId: result.containerId,
|
|
326
335
|
remoteUser: result.remoteUser,
|
|
@@ -335,6 +344,11 @@ async function provision(row: InstanceRow, opts: { noCache?: boolean } = {}): Pr
|
|
|
335
344
|
}
|
|
336
345
|
}
|
|
337
346
|
|
|
347
|
+
// Unblocks the terminal launcher's wait; written even after ⚠s — its timeout is the fallback.
|
|
348
|
+
await writeContainerFile(target, { dir: '$HOME', name: INJECTIONS_DONE_FILE }, 'done\n').catch(
|
|
349
|
+
() => undefined
|
|
350
|
+
);
|
|
351
|
+
|
|
338
352
|
appendLog(row.id, `\n✓ Instance running — open it via the proxy at ${proxyPathFor(row.id)}\n`);
|
|
339
353
|
triggerReconcile();
|
|
340
354
|
} catch (err) {
|