codebay 0.0.1 → 0.2.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/import-css/index-0nwv3654.css +18 -0
- package/.mochi/import-css/index-xndx4980.css +54 -0
- package/.mochi/manifest.json +1608 -0
- package/.mochi/server-island.js +1 -0
- package/.mochi/svelte-client/HydratableIsland-ps09gjy2.js +1 -0
- package/.mochi/svelte-client/_hydrate-AppShell_2rtnxn6pgp2rh-62w5zwww.js +2 -0
- package/.mochi/svelte-client/_hydrate-AvatarsView_257s9yq1wwr4f-jmm501zf.js +13 -0
- package/.mochi/svelte-client/_hydrate-InstanceView_2ma45l9bybfhl-ewk4n2c0.js +1 -0
- package/.mochi/svelte-client/_hydrate-SettingsView_8hmarwmf8onq-e7e89md0.js +23 -0
- package/.mochi/svelte-client/chunk-jx5cekhh.js +2 -0
- package/.mochi/svelte-client/chunk-k3my1snp.js +2 -0
- package/.mochi/svelte-client/chunk-kgn1b2t1.js +4 -0
- package/.mochi/svelte-client/chunk-ry7q5zk5.js +1 -0
- package/.mochi/svelte-client/chunk-x843txjc.js +2 -0
- package/.mochi/svelte-client/chunk-zjcg8kw5.js +2 -0
- package/.mochi/svelte-compile/App-8k1gyxdc.server.js +1167 -0
- package/.mochi/svelte-compile/Avatars-f7c6rp96.server.js +351 -0
- package/.mochi/svelte-compile/ClientStats-z3tn9ad6.server.js +99 -0
- package/.mochi/svelte-compile/DefaultError-wh77vebf.server.js +52 -0
- package/.mochi/svelte-compile/Instance-yvps12zg.server.js +355 -0
- package/.mochi/svelte-compile/Settings-jwg3httb.server.js +1169 -0
- package/.mochi/svelte-compile/chunk-4kwy539a.js +14 -0
- package/.mochi/svelte-compile/chunk-9bjk7yx6.js +81 -0
- package/.mochi/svelte-compile/chunk-dqgstte4.js +52 -0
- package/.mochi/svelte-compile/chunk-sxqys3nw.js +1668 -0
- package/.mochi/svelte-compile/chunk-v6796ga4.js +3295 -0
- package/.mochi/svelte-compile/chunk-we0ggtws.js +34 -0
- package/.mochi/svelte-css/AppBar-3bxinv4pabv8l.css +1 -0
- package/.mochi/svelte-css/AppShell-36mpg5smnbmuw.css +1 -0
- package/.mochi/svelte-css/Avatar-24th63awm9mna.css +1 -0
- package/.mochi/svelte-css/AvatarEditor-1ryrs1md5g36y.css +1 -0
- package/.mochi/svelte-css/AvatarsView-3hq37u4dgtljn.css +1 -0
- package/.mochi/svelte-css/BackLink-2wlu0eovz9j86.css +1 -0
- package/.mochi/svelte-css/BranchBox-1yqu1q2mgnjwz.css +1 -0
- package/.mochi/svelte-css/Brand-1la7zw5uhcbl4.css +1 -0
- package/.mochi/svelte-css/Button-2vtbrzb5kgu25.css +1 -0
- package/.mochi/svelte-css/CheckmarkIcon-3qlazj2oc50jc.css +1 -0
- package/.mochi/svelte-css/ClientStats-19qzckq40p2q9.css +1 -0
- package/.mochi/svelte-css/CoinButton-1rej3sh3tkjud.css +1 -0
- package/.mochi/svelte-css/CredMenu-9hwgeptp7crb.css +1 -0
- package/.mochi/svelte-css/DashboardView-1cy4xpqhja9sx.css +1 -0
- package/.mochi/svelte-css/DefaultError-2b0daamydxgb2.css +1 -0
- package/.mochi/svelte-css/ErrorIcon-2x0tzsqxskw28.css +1 -0
- package/.mochi/svelte-css/FolderBrowser-1x428zb3f2kti.css +1 -0
- package/.mochi/svelte-css/HealthBox-er2her0jukwl.css +1 -0
- package/.mochi/svelte-css/IdeBar-2qoqi2ya9zdn3.css +1 -0
- package/.mochi/svelte-css/IdeLoader-26rlko8e00j3g.css +1 -0
- package/.mochi/svelte-css/InstanceCard-16f77aysm7lgp.css +1 -0
- package/.mochi/svelte-css/InstanceView-wq85bxtusont.css +1 -0
- package/.mochi/svelte-css/LoaderIcon-soq5wt3p05ye.css +1 -0
- package/.mochi/svelte-css/PortsBox-1t25b5yg7jva3.css +1 -0
- package/.mochi/svelte-css/SettingsCog-jftyadvo64qo.css +1 -0
- package/.mochi/svelte-css/SettingsView-3bbkzsvnrth3v.css +1 -0
- package/.mochi/svelte-css/Skeleton-8k69usiju43h.css +1 -0
- package/.mochi/svelte-css/StatusBadge-3mq0j7z6aq4j.css +1 -0
- package/.mochi/svelte-css/ThemePicker-1ir8jyocm8mvi.css +1 -0
- package/.mochi/svelte-css/ToastBar-3havgkmtofntq.css +1 -0
- package/.mochi/svelte-css/ToastIcon-2z5wrkc95khg0.css +1 -0
- package/.mochi/svelte-css/ToastMessage-2jfwb2og2lfa8.css +1 -0
- package/.mochi/svelte-css/ToastWrapper-1fbnym10rm2nv.css +1 -0
- package/.mochi/svelte-css/Toaster-kk60j193854n.css +1 -0
- package/.mochi/svelte-css/TopBar-2q87li6yt8ids.css +1 -0
- package/LICENSE +21 -0
- package/README.md +33 -3
- package/bin/codebay.ts +22 -0
- package/migrations/0001_init.sql +20 -0
- package/migrations/0002_port_forwards.sql +9 -0
- package/migrations/0003_options.sql +4 -0
- package/migrations/0004_instance_image.sql +1 -0
- package/package.json +87 -9
- package/public/sounds/done.wav +0 -0
- package/public/sounds/waiting.wav +0 -0
- package/src/__fixtures__/HelloWorld.svelte +4 -0
- package/src/additional-svelte-typings.d.ts +1 -0
- package/src/api.ts +61 -0
- package/src/avatars/anchor.ts +17 -0
- package/src/avatars/bear.ts +17 -0
- package/src/avatars/bee.ts +17 -0
- package/src/avatars/bob-marley.ts +17 -0
- package/src/avatars/car.ts +17 -0
- package/src/avatars/carpathians.ts +17 -0
- package/src/avatars/cat-body.ts +17 -0
- package/src/avatars/cat-sits.ts +17 -0
- package/src/avatars/cat.ts +17 -0
- package/src/avatars/celtic-cross.ts +17 -0
- package/src/avatars/cherry.ts +17 -0
- package/src/avatars/cotton.ts +17 -0
- package/src/avatars/crab.ts +17 -0
- package/src/avatars/crown.ts +17 -0
- package/src/avatars/diamond.ts +17 -0
- package/src/avatars/dog.ts +17 -0
- package/src/avatars/elephant.ts +17 -0
- package/src/avatars/fish.ts +17 -0
- package/src/avatars/fleur-de-lis.ts +17 -0
- package/src/avatars/flower.ts +17 -0
- package/src/avatars/fox.ts +17 -0
- package/src/avatars/frog.ts +17 -0
- package/src/avatars/ghost.ts +17 -0
- package/src/avatars/giraffe.ts +17 -0
- package/src/avatars/glasses.ts +17 -0
- package/src/avatars/heart.ts +17 -0
- package/src/avatars/index.ts +128 -0
- package/src/avatars/invader.ts +17 -0
- package/src/avatars/kangaroo.ts +17 -0
- package/src/avatars/key.ts +17 -0
- package/src/avatars/lightning.ts +17 -0
- package/src/avatars/mushroom.ts +17 -0
- package/src/avatars/octopus.ts +17 -0
- package/src/avatars/owl.ts +17 -0
- package/src/avatars/penguin.ts +17 -0
- package/src/avatars/planet.ts +17 -0
- package/src/avatars/rabbit.ts +17 -0
- package/src/avatars/rat.ts +17 -0
- package/src/avatars/robot.ts +17 -0
- package/src/avatars/rocket.ts +17 -0
- package/src/avatars/sea.ts +17 -0
- package/src/avatars/serialize.ts +65 -0
- package/src/avatars/skull.ts +17 -0
- package/src/avatars/snail.ts +17 -0
- package/src/avatars/snake.ts +17 -0
- package/src/avatars/snakegame.ts +17 -0
- package/src/avatars/star.ts +17 -0
- package/src/avatars/svelte.ts +17 -0
- package/src/avatars/target.ts +17 -0
- package/src/avatars/tree.ts +17 -0
- package/src/avatars/types.ts +24 -0
- package/src/avatars/ukraine.ts +17 -0
- package/src/avatars/whale.ts +17 -0
- package/src/avatars/wheat.ts +17 -0
- package/src/avatars/yinyan.ts +17 -0
- package/src/components/AppBar.svelte +35 -0
- package/src/components/AppShell.svelte +302 -0
- package/src/components/Avatar.svelte +235 -0
- package/src/components/AvatarEditor.svelte +321 -0
- package/src/components/AvatarsView.svelte +113 -0
- package/src/components/BranchBox.svelte +32 -0
- package/src/components/Brand.svelte +41 -0
- package/src/components/Button.svelte +161 -0
- package/src/components/CoinButton.svelte +121 -0
- package/src/components/CredMenu.svelte +171 -0
- package/src/components/DashboardView.svelte +198 -0
- package/src/components/FolderBrowser.svelte +635 -0
- package/src/components/HealthBox.svelte +161 -0
- package/src/components/IdeBar.svelte +186 -0
- package/src/components/IdeLoader.svelte +135 -0
- package/src/components/InstanceCard.svelte +220 -0
- package/src/components/InstanceView.svelte +565 -0
- package/src/components/PortsBox.svelte +73 -0
- package/src/components/SettingsCog.svelte +22 -0
- package/src/components/SettingsView.svelte +1484 -0
- package/src/components/Skeleton.svelte +43 -0
- package/src/components/StatusBadge.svelte +54 -0
- package/src/components/ThemePicker.svelte +77 -0
- package/src/components/TopBar.svelte +64 -0
- package/src/components/UiShowcase.svelte +578 -0
- package/src/components/ui-showcase/ComponentDemo.svelte +85 -0
- package/src/container-injections/attention-hooks.ts +117 -0
- package/src/container-injections/claude-aliases.ts +43 -0
- package/src/container-injections/claude-code-credentials.ts +145 -0
- package/src/container-injections/claude-code-custom.ts +125 -0
- package/src/container-injections/claude-no-coauthor.ts +35 -0
- package/src/container-injections/claude-skip-permissions.ts +38 -0
- package/src/container-injections/claude-statusline.ts +109 -0
- package/src/container-injections/git-identity.ts +63 -0
- package/src/container-injections/git-safe-directory.ts +18 -0
- package/src/container-injections/github-credentials.ts +162 -0
- package/src/container-injections/host-env-vars.ts +108 -0
- package/src/container-injections/tmux.ts +67 -0
- package/src/fontsource.d.ts +3 -0
- package/src/index.ts +81 -0
- package/src/lib/auth.server.ts +95 -0
- package/src/lib/bridge.server.ts +34 -0
- package/src/lib/config.server.ts +122 -0
- package/src/lib/crypto.server.ts +12 -0
- package/src/lib/db.server.ts +196 -0
- package/src/lib/devcontainer.server.ts +503 -0
- package/src/lib/docker-client.server.ts +61 -0
- package/src/lib/docker.server.ts +139 -0
- package/src/lib/exec.server.ts +92 -0
- package/src/lib/git.server.ts +68 -0
- package/src/lib/health.server.ts +105 -0
- package/src/lib/injections.server.ts +72 -0
- package/src/lib/instances.server.ts +552 -0
- package/src/lib/picker.server.ts +46 -0
- package/src/lib/ports.server.ts +9 -0
- package/src/lib/proxy.server.ts +170 -0
- package/src/lib/repo-url.ts +60 -0
- package/src/lib/spawn.server.ts +14 -0
- package/src/lib/theme.server.ts +22 -0
- package/src/live.ts +50 -0
- package/src/pages/App.svelte +14 -0
- package/src/pages/Avatars.svelte +7 -0
- package/src/pages/Instance.svelte +9 -0
- package/src/pages/Settings.svelte +73 -0
- package/src/pages/UI.svelte +10 -0
- package/src/routes.ts +440 -0
- package/src/settings.ts +14 -0
- package/src/shell.html +100 -0
- package/src/sound.ts +69 -0
- package/src/theme.ts +24 -0
- package/src/types.ts +79 -0
- package/svelte.config.js +5 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { Writable } from 'node:stream';
|
|
2
|
+
import { getDocker } from './docker-client.server.ts';
|
|
3
|
+
|
|
4
|
+
export interface ExecTarget {
|
|
5
|
+
containerId: string;
|
|
6
|
+
remoteUser?: string | null;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function collector(): { stream: Writable; text: () => string } {
|
|
10
|
+
const chunks: Buffer[] = [];
|
|
11
|
+
return {
|
|
12
|
+
stream: new Writable({
|
|
13
|
+
write(chunk, _enc, cb) {
|
|
14
|
+
chunks.push(Buffer.from(chunk));
|
|
15
|
+
cb();
|
|
16
|
+
}
|
|
17
|
+
}),
|
|
18
|
+
text: () => Buffer.concat(chunks).toString('utf8')
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const STDIN_ENV = '__CODEBAY_EXEC_STDIN';
|
|
23
|
+
const STDIN_VAR = 'CODEBAY_STDIN';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Real exec stdin is unusable on Bun — dockerode's hijack path needs an `upgrade` event
|
|
27
|
+
* Bun never emits, and `openStdin` needs full-duplex client HTTP Bun also lacks.
|
|
28
|
+
*/
|
|
29
|
+
function wrapWithStdin(script: string): string {
|
|
30
|
+
// The carrier is an env var (never argv, so never in `ps`) and is unset so children don't inherit it.
|
|
31
|
+
return `${STDIN_VAR}="$${STDIN_ENV}"; unset ${STDIN_ENV}\n${script}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The one place the `bash -lc` container-exec pattern lives; every injection goes through it.
|
|
36
|
+
* `stdin` reaches the script as `$CODEBAY_STDIN`, and `args` as `$0`, `$1`, ….
|
|
37
|
+
*/
|
|
38
|
+
export async function execInContainer(
|
|
39
|
+
target: ExecTarget,
|
|
40
|
+
opts: { script: string; stdin?: string; args?: string[]; capture?: boolean }
|
|
41
|
+
): Promise<{ ok: boolean; stdout: string; error?: string }> {
|
|
42
|
+
const user = target.remoteUser?.trim() || 'root';
|
|
43
|
+
const hasStdin = opts.stdin !== undefined;
|
|
44
|
+
try {
|
|
45
|
+
const exec = await (await getDocker()).getContainer(target.containerId).exec({
|
|
46
|
+
Cmd: [
|
|
47
|
+
'bash',
|
|
48
|
+
'-lc',
|
|
49
|
+
hasStdin ? wrapWithStdin(opts.script) : opts.script,
|
|
50
|
+
...(opts.args ?? [])
|
|
51
|
+
],
|
|
52
|
+
User: user,
|
|
53
|
+
Env: hasStdin ? [`${STDIN_ENV}=${opts.stdin}`] : undefined,
|
|
54
|
+
AttachStdout: true,
|
|
55
|
+
AttachStderr: true,
|
|
56
|
+
Tty: false // multiplexes stdout/stderr so the demux below can separate them
|
|
57
|
+
});
|
|
58
|
+
const stream = await exec.start({});
|
|
59
|
+
|
|
60
|
+
const out = collector();
|
|
61
|
+
const err = collector();
|
|
62
|
+
exec.modem.demuxStream(stream, out.stream, err.stream);
|
|
63
|
+
await new Promise<void>((resolve, reject) => {
|
|
64
|
+
stream.on('end', resolve);
|
|
65
|
+
stream.on('error', reject);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const code = (await exec.inspect()).ExitCode ?? 1;
|
|
69
|
+
const stdout = opts.capture ? out.text().trim() : '';
|
|
70
|
+
return code === 0
|
|
71
|
+
? { ok: true, stdout }
|
|
72
|
+
: { ok: false, stdout, error: err.text().trim() || `exit ${code}` };
|
|
73
|
+
} catch (e) {
|
|
74
|
+
return { ok: false, stdout: '', error: (e as Error).message };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** The shape every injection's `check()` uses: a script that echoes `1` or `0`. */
|
|
79
|
+
export async function checkPresence(
|
|
80
|
+
target: ExecTarget,
|
|
81
|
+
script: string,
|
|
82
|
+
args?: string[]
|
|
83
|
+
): Promise<boolean> {
|
|
84
|
+
const res = await execInContainer(target, { script, args, capture: true });
|
|
85
|
+
return res.ok && res.stdout === '1';
|
|
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}";`;
|
|
92
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { readGhToken } from '../container-injections/github-credentials.ts';
|
|
4
|
+
import { parseRepoUrl } from './repo-url.ts';
|
|
5
|
+
|
|
6
|
+
/** Reads the bind-mounted host copy, so polling the container's branch costs no `docker exec`. */
|
|
7
|
+
export async function readGitBranch(workspacePath: string): Promise<string | null> {
|
|
8
|
+
try {
|
|
9
|
+
const head = (await readFile(join(workspacePath, '.git', 'HEAD'), 'utf8')).trim();
|
|
10
|
+
const ref = head.match(/^ref:\s*refs\/heads\/(.+)$/);
|
|
11
|
+
if (ref) return ref[1] ?? null;
|
|
12
|
+
// Detached HEAD: the file holds a raw commit SHA instead of a ref.
|
|
13
|
+
return head ? head.slice(0, 7) : null;
|
|
14
|
+
} catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The token goes in env-scoped git config, so it lands in neither argv nor the clone's
|
|
21
|
+
* persisted `.git/config`; `GIT_TERMINAL_PROMPT=0` fails fast instead of blocking on a prompt.
|
|
22
|
+
*/
|
|
23
|
+
export async function cloneRepo(
|
|
24
|
+
source: string,
|
|
25
|
+
dest: string,
|
|
26
|
+
onLog: (chunk: string) => void,
|
|
27
|
+
opts: { branch?: string } = {}
|
|
28
|
+
): Promise<void> {
|
|
29
|
+
const parsed = parseRepoUrl(source);
|
|
30
|
+
if (!parsed) throw new Error(`Invalid repository URL: ${source}`);
|
|
31
|
+
|
|
32
|
+
const env: Record<string, string | undefined> = {
|
|
33
|
+
...process.env,
|
|
34
|
+
GIT_TERMINAL_PROMPT: '0'
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Keyed by host so GitHub Enterprise Server clones authenticate too, not just github.com.
|
|
38
|
+
const found = await readGhToken(parsed.host);
|
|
39
|
+
if (found) {
|
|
40
|
+
const basic = Buffer.from(`x-access-token:${found.token}`).toString('base64');
|
|
41
|
+
env.GIT_CONFIG_COUNT = '1';
|
|
42
|
+
env.GIT_CONFIG_KEY_0 = `http.https://${parsed.host}/.extraheader`;
|
|
43
|
+
env.GIT_CONFIG_VALUE_0 = `Authorization: Basic ${basic}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const args = ['git', 'clone', '--progress'];
|
|
47
|
+
if (opts.branch?.trim()) args.push('--branch', opts.branch.trim());
|
|
48
|
+
args.push(parsed.cloneUrl, dest);
|
|
49
|
+
|
|
50
|
+
const proc = Bun.spawn(args, { stdout: 'pipe', stderr: 'pipe', env });
|
|
51
|
+
|
|
52
|
+
let stderrTail = '';
|
|
53
|
+
const decoder = new TextDecoder();
|
|
54
|
+
const pump = async (stream: ReadableStream<Uint8Array>, capture: boolean) => {
|
|
55
|
+
for await (const bytes of stream) {
|
|
56
|
+
const text = decoder.decode(bytes, { stream: true });
|
|
57
|
+
if (capture) stderrTail = (stderrTail + text).slice(-2000);
|
|
58
|
+
onLog(text);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
await Promise.all([pump(proc.stdout, false), pump(proc.stderr, true)]);
|
|
63
|
+
const code = await proc.exited;
|
|
64
|
+
if (code !== 0) {
|
|
65
|
+
const tail = stderrTail.trim().split('\n').slice(-3).join(' ');
|
|
66
|
+
throw new Error(`git clone failed (exit ${code})${tail ? `: ${tail}` : ''}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { type InstanceRow } from './db.server.ts';
|
|
2
|
+
import { isRunning, publishedContainerPorts } from './docker.server.ts';
|
|
3
|
+
import { broadcastHealth } from './instances.server.ts';
|
|
4
|
+
import { resolveInjections } from './injections.server.ts';
|
|
5
|
+
import type { InstanceHealth } from '../types.ts';
|
|
6
|
+
|
|
7
|
+
const REFRESH_MS = 5000;
|
|
8
|
+
|
|
9
|
+
interface Monitor {
|
|
10
|
+
snapshot: InstanceHealth | null;
|
|
11
|
+
timer: ReturnType<typeof setInterval>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Pin to globalThis so dev-mode hot reload doesn't orphan the interval timers.
|
|
15
|
+
const globalForHealth = globalThis as unknown as { __codebayHealth?: Map<string, Monitor> };
|
|
16
|
+
const monitors: Map<string, Monitor> = (globalForHealth.__codebayHealth ??= new Map());
|
|
17
|
+
|
|
18
|
+
async function codeServerAccessible(port: number): Promise<boolean> {
|
|
19
|
+
try {
|
|
20
|
+
// Any HTTP response at all (200/302/401/…) means the server is listening.
|
|
21
|
+
await fetch(`http://127.0.0.1:${port}/`, { signal: AbortSignal.timeout(2000) });
|
|
22
|
+
return true;
|
|
23
|
+
} catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function check(row: InstanceRow): Promise<InstanceHealth> {
|
|
29
|
+
const down: InstanceHealth = {
|
|
30
|
+
containerRunning: false,
|
|
31
|
+
codeServerAccessible: false,
|
|
32
|
+
injections: [],
|
|
33
|
+
openPorts: [],
|
|
34
|
+
checkedAt: Date.now()
|
|
35
|
+
};
|
|
36
|
+
if (!row.container_id || !(await isRunning(row.container_id))) return down;
|
|
37
|
+
|
|
38
|
+
// Probes as the recorded remote user, so `$HOME` resolves to the one the injection wrote to.
|
|
39
|
+
const target = { containerId: row.container_id, remoteUser: row.remote_user, instance: row };
|
|
40
|
+
const [accessible, openPorts, injectionResults] = await Promise.all([
|
|
41
|
+
codeServerAccessible(row.host_port),
|
|
42
|
+
publishedContainerPorts(row.container_id),
|
|
43
|
+
Promise.all(
|
|
44
|
+
resolveInjections()
|
|
45
|
+
.filter((i) => i.check)
|
|
46
|
+
.map(async (i) => ({
|
|
47
|
+
id: i.id,
|
|
48
|
+
label: i.label,
|
|
49
|
+
ok: await i.check!(target).catch(() => false)
|
|
50
|
+
}))
|
|
51
|
+
)
|
|
52
|
+
]);
|
|
53
|
+
return {
|
|
54
|
+
containerRunning: true,
|
|
55
|
+
codeServerAccessible: accessible,
|
|
56
|
+
injections: injectionResults,
|
|
57
|
+
openPorts,
|
|
58
|
+
checkedAt: Date.now()
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function tick(row: InstanceRow): Promise<void> {
|
|
63
|
+
const snapshot = await check(row);
|
|
64
|
+
const mon = monitors.get(row.id);
|
|
65
|
+
if (mon) mon.snapshot = snapshot;
|
|
66
|
+
broadcastHealth(row.id, snapshot);
|
|
67
|
+
if (!snapshot.containerRunning) stopHealthMonitor(row.id);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function startHealthMonitor(row: InstanceRow): Monitor {
|
|
71
|
+
const existing = monitors.get(row.id);
|
|
72
|
+
if (existing) return existing;
|
|
73
|
+
const mon: Monitor = { snapshot: null, timer: setInterval(() => void tick(row), REFRESH_MS) };
|
|
74
|
+
monitors.set(row.id, mon);
|
|
75
|
+
void tick(row); // seed the first snapshot rather than making the UI wait a full interval
|
|
76
|
+
return mon;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function stopHealthMonitor(id: string): void {
|
|
80
|
+
const mon = monitors.get(id);
|
|
81
|
+
if (!mon) return;
|
|
82
|
+
clearInterval(mon.timer);
|
|
83
|
+
monitors.delete(id);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Driven from the reconcile loop, so monitors track the container lifecycle automatically. */
|
|
87
|
+
export function syncHealthMonitors(rows: InstanceRow[]): void {
|
|
88
|
+
const running = new Set(
|
|
89
|
+
rows.filter((r) => r.status === 'running' && r.container_id).map((r) => r.id)
|
|
90
|
+
);
|
|
91
|
+
for (const row of rows) {
|
|
92
|
+
if (running.has(row.id) && !monitors.has(row.id)) startHealthMonitor(row);
|
|
93
|
+
}
|
|
94
|
+
for (const id of [...monitors.keys()]) {
|
|
95
|
+
if (!running.has(id)) stopHealthMonitor(id);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function currentHealthSnapshots(): { id: string; health: InstanceHealth }[] {
|
|
100
|
+
const out: { id: string; health: InstanceHealth }[] = [];
|
|
101
|
+
for (const [id, mon] of monitors) {
|
|
102
|
+
if (mon.snapshot) out.push({ id, health: mon.snapshot });
|
|
103
|
+
}
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { InstanceRow } from './db.server.ts';
|
|
2
|
+
import type { ExecTarget } from './exec.server.ts';
|
|
3
|
+
import { getOption } from './db.server.ts';
|
|
4
|
+
import { gitSafeDirectory } from '../container-injections/git-safe-directory.ts';
|
|
5
|
+
import { tmux } from '../container-injections/tmux.ts';
|
|
6
|
+
import { gitIdentity } from '../container-injections/git-identity.ts';
|
|
7
|
+
import { claudeCodeCredentials } from '../container-injections/claude-code-credentials.ts';
|
|
8
|
+
import { claudeCodeCustom } from '../container-injections/claude-code-custom.ts';
|
|
9
|
+
import { githubCredentials } from '../container-injections/github-credentials.ts';
|
|
10
|
+
import { attentionHooks } from '../container-injections/attention-hooks.ts';
|
|
11
|
+
import { claudeStatusline } from '../container-injections/claude-statusline.ts';
|
|
12
|
+
import { claudeSkipPermissions } from '../container-injections/claude-skip-permissions.ts';
|
|
13
|
+
import { claudeAliases } from '../container-injections/claude-aliases.ts';
|
|
14
|
+
import { claudeNoCoauthor } from '../container-injections/claude-no-coauthor.ts';
|
|
15
|
+
import { hostEnvVars } from '../container-injections/host-env-vars.ts';
|
|
16
|
+
|
|
17
|
+
/** Extends `ExecTarget` so exec-user semantics have a single source of truth. */
|
|
18
|
+
export interface ContainerTarget extends ExecTarget {
|
|
19
|
+
instance: InstanceRow;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Bundles host-side discovery, container `apply()`, and a live `check()` in one module,
|
|
24
|
+
* so what gets injected and what gets health-probed can never drift apart.
|
|
25
|
+
*/
|
|
26
|
+
export interface Injection {
|
|
27
|
+
/** Doubles as the health-row key. */
|
|
28
|
+
id: string;
|
|
29
|
+
label: string;
|
|
30
|
+
/** Omit for injections with no host dependency — presence is what draws the setup-UI chip. */
|
|
31
|
+
auth?: {
|
|
32
|
+
/** Short instruction shown when unavailable, e.g. "run `gh auth login`". */
|
|
33
|
+
hint: string;
|
|
34
|
+
status(): Promise<{ available: boolean; source: string | null }>;
|
|
35
|
+
};
|
|
36
|
+
apply(target: ContainerTarget, log: (msg: string) => void): Promise<void>;
|
|
37
|
+
/** Omit to keep the injection out of the health list. */
|
|
38
|
+
check?(target: ContainerTarget): Promise<boolean>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Split around the Claude slot, which `resolveInjections()` fills at call time. */
|
|
42
|
+
const BASE_INJECTIONS_HEAD: Injection[] = [
|
|
43
|
+
// Must stay first — every later git-touching injection depends on safe.directory.
|
|
44
|
+
gitSafeDirectory,
|
|
45
|
+
// The package install is the slowest injection, so start it before a folderOpen can find tmux missing.
|
|
46
|
+
tmux,
|
|
47
|
+
gitIdentity
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
const BASE_INJECTIONS_TAIL: Injection[] = [
|
|
51
|
+
githubCredentials,
|
|
52
|
+
attentionHooks,
|
|
53
|
+
claudeStatusline,
|
|
54
|
+
claudeSkipPermissions,
|
|
55
|
+
claudeAliases,
|
|
56
|
+
claudeNoCoauthor,
|
|
57
|
+
hostEnvVars
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
/** Resolved per call so toggling the custom-endpoint setting takes effect without a restart. */
|
|
61
|
+
export function resolveInjections(): Injection[] {
|
|
62
|
+
const claudeInjection =
|
|
63
|
+
getOption('custom_endpoint_enabled') === '1' ? claudeCodeCustom : claudeCodeCredentials;
|
|
64
|
+
return [...BASE_INJECTIONS_HEAD, claudeInjection, ...BASE_INJECTIONS_TAIL];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** @deprecated Use `resolveInjections()`; this ignores settings and is kept for the tests. */
|
|
68
|
+
export const injections: Injection[] = [
|
|
69
|
+
...BASE_INJECTIONS_HEAD,
|
|
70
|
+
claudeCodeCredentials,
|
|
71
|
+
...BASE_INJECTIONS_TAIL
|
|
72
|
+
];
|