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,117 @@
|
|
|
1
|
+
import { PORT } from '../lib/config.server.ts';
|
|
2
|
+
import { checkPresence, execInContainer } from '../lib/exec.server.ts';
|
|
3
|
+
import type { ContainerTarget, Injection } from '../lib/injections.server.ts';
|
|
4
|
+
|
|
5
|
+
function bridgeUrl(): string {
|
|
6
|
+
return `http://host.docker.internal:${PORT}/api/bridge/attention`;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Read by curl as `-H @<file>` so the token never reaches argv, where `ps` would show it. */
|
|
10
|
+
const HEADER_FILE = '${CLAUDE_CONFIG_DIR:-$HOME/.claude}/.bridge-header';
|
|
11
|
+
|
|
12
|
+
/** Claude discards hook stdout/stderr, so a silently-failing ping would leave no other trace. */
|
|
13
|
+
const HOOK_LOG = '${CLAUDE_CONFIG_DIR:-$HOME/.claude}/.bridge-hook.log';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* `-f` is deliberately absent so a 4xx still yields an HTTP status to log rather than
|
|
17
|
+
* curl bailing early; `curl_exit` then separates "unreachable" from "answered with a status".
|
|
18
|
+
*/
|
|
19
|
+
function hookFor(id: string, state: 'done' | 'waiting' | 'busy') {
|
|
20
|
+
const url = `${bridgeUrl()}?id=${encodeURIComponent(id)}&state=${state}`;
|
|
21
|
+
const command =
|
|
22
|
+
`log="${HOOK_LOG}"; ` +
|
|
23
|
+
// The Content-Type is for Mochi's CSRF guard, which reads a bodiless POST as a form submit.
|
|
24
|
+
`http=$(curl -sS -m 5 -o /dev/null -w '%{http_code}' -X POST -H 'Content-Type: application/json' -H @"${HEADER_FILE}" '${url}' 2>>"$log"); ` +
|
|
25
|
+
`rc=$?; ` +
|
|
26
|
+
`printf '[%s] state=${state} http=%s curl_exit=%s\\n' "$(date -Is 2>/dev/null || date)" "$http" "$rc" >> "$log" 2>/dev/null; ` +
|
|
27
|
+
// Keep the log bounded without a race window that could lose the file.
|
|
28
|
+
`t=$(tail -n 500 "$log" 2>/dev/null) && printf '%s\\n' "$t" > "$log" 2>/dev/null; ` +
|
|
29
|
+
`true`;
|
|
30
|
+
return [
|
|
31
|
+
{
|
|
32
|
+
hooks: [
|
|
33
|
+
{
|
|
34
|
+
type: 'command',
|
|
35
|
+
command
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
}
|
|
39
|
+
];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Carries no token — that lives in the mode-600 header file `writeBridgeHeader` stages. */
|
|
43
|
+
export function attentionHookSettings(id: string): string {
|
|
44
|
+
const settings = {
|
|
45
|
+
hooks: {
|
|
46
|
+
Stop: hookFor(id, 'done'),
|
|
47
|
+
Notification: hookFor(id, 'waiting'),
|
|
48
|
+
UserPromptSubmit: hookFor(id, 'busy')
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
return JSON.stringify(settings, null, 2);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Stored as a whole header line so curl can consume the file directly with `-H @`. */
|
|
55
|
+
async function writeBridgeHeader(
|
|
56
|
+
target: ContainerTarget,
|
|
57
|
+
token: string
|
|
58
|
+
): Promise<{ ok: boolean; error?: string }> {
|
|
59
|
+
const script =
|
|
60
|
+
'h=$(eval echo ~$(id -un)); d="${CLAUDE_CONFIG_DIR:-$h/.claude}"; mkdir -p "$d"; ' +
|
|
61
|
+
'f="$d/.bridge-header"; printf \'X-Bridge-Token: %s\\n\' "$CODEBAY_STDIN" > "$f"; chmod 600 "$f"';
|
|
62
|
+
const res = await execInContainer(target, { script, stdin: token });
|
|
63
|
+
return res.ok ? { ok: true } : { ok: false, error: res.error };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Merging rather than overwriting is what lets the settings.json injections compose in any order. */
|
|
67
|
+
export function mergeClaudeSettingsScript(): string {
|
|
68
|
+
return (
|
|
69
|
+
'h=$(eval echo ~$(id -un)); d="${CLAUDE_CONFIG_DIR:-$h/.claude}"; mkdir -p "$d"; ' +
|
|
70
|
+
'f="$d/settings.json"; new="$CODEBAY_STDIN"; ' +
|
|
71
|
+
'if command -v jq >/dev/null 2>&1 && [ -s "$f" ] && ' +
|
|
72
|
+
'merged=$(printf \'%s\' "$new" | jq -s \'.[0] * .[1]\' "$f" - 2>/dev/null); then ' +
|
|
73
|
+
'printf \'%s\' "$merged" > "$f"; else printf \'%s\' "$new" > "$f"; fi; ' +
|
|
74
|
+
'chmod 644 "$f"'
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function injectClaudeHooks(
|
|
79
|
+
target: ContainerTarget,
|
|
80
|
+
settingsJson: string
|
|
81
|
+
): Promise<{ ok: boolean; error?: string }> {
|
|
82
|
+
const res = await execInContainer(target, {
|
|
83
|
+
script: mergeClaudeSettingsScript(),
|
|
84
|
+
stdin: settingsJson
|
|
85
|
+
});
|
|
86
|
+
return res.ok ? { ok: true } : { ok: false, error: res.error };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export const attentionHooks: Injection = {
|
|
90
|
+
id: 'attention-hooks',
|
|
91
|
+
label: 'Claude hooks',
|
|
92
|
+
|
|
93
|
+
async apply(target, log) {
|
|
94
|
+
log('Injecting Claude attention hooks…\n');
|
|
95
|
+
const header = await writeBridgeHeader(target, target.instance.bridge_token);
|
|
96
|
+
if (!header.ok) {
|
|
97
|
+
log(`⚠ Claude hook injection failed: ${header.error}\n`);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const settings = attentionHookSettings(target.instance.id);
|
|
101
|
+
const hooks = await injectClaudeHooks(target, settings);
|
|
102
|
+
log(
|
|
103
|
+
hooks.ok
|
|
104
|
+
? '✓ Claude attention hooks installed\n'
|
|
105
|
+
: `⚠ Claude hook injection failed: ${hooks.error}\n`
|
|
106
|
+
);
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
async check(target) {
|
|
110
|
+
return checkPresence(
|
|
111
|
+
target,
|
|
112
|
+
'h=$(eval echo ~$(id -un)); d="${CLAUDE_CONFIG_DIR:-$h/.claude}"; ' +
|
|
113
|
+
'[ -s "$d/settings.json" ] && grep -q "$1" "$d/settings.json" && echo 1 || echo 0',
|
|
114
|
+
['attention-check', target.instance.id]
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { checkPresence, execInContainer } from '../lib/exec.server.ts';
|
|
2
|
+
import type { Injection } from '../lib/injections.server.ts';
|
|
3
|
+
|
|
4
|
+
/** Both resolve `claude`, which `claude-skip-permissions` also aliases, so the two compose. */
|
|
5
|
+
const ALIAS_LINES = [
|
|
6
|
+
"alias c200='CLAUDE_CODE_DISABLE_1M_CONTEXT=1 claude'",
|
|
7
|
+
"alias cs='claude --model sonnet'"
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
/** Lines arrive as `$@` so no alias text is interpolated into the loop body. */
|
|
11
|
+
const APPLY_SCRIPT =
|
|
12
|
+
'h=$(eval echo ~$(id -un)); ' +
|
|
13
|
+
'for line in "$@"; do ' +
|
|
14
|
+
'for f in "$h/.bashrc" "$h/.zshrc"; do ' +
|
|
15
|
+
'grep -qF "$line" "$f" 2>/dev/null || printf \'%s\\n\' "$line" >> "$f"; ' +
|
|
16
|
+
'done; ' +
|
|
17
|
+
'done';
|
|
18
|
+
|
|
19
|
+
const CHECK_SCRIPT =
|
|
20
|
+
'h=$(eval echo ~$(id -un)); ' +
|
|
21
|
+
'for line in "$@"; do ' +
|
|
22
|
+
'grep -qF "$line" "$h/.bashrc" 2>/dev/null || grep -qF "$line" "$h/.zshrc" 2>/dev/null || ' +
|
|
23
|
+
'{ echo 0; exit 0; }; ' +
|
|
24
|
+
'done; ' +
|
|
25
|
+
'echo 1';
|
|
26
|
+
|
|
27
|
+
export const claudeAliases: Injection = {
|
|
28
|
+
id: 'claude-aliases',
|
|
29
|
+
label: 'claude aliases',
|
|
30
|
+
|
|
31
|
+
async apply(target, log) {
|
|
32
|
+
log('Installing c200 / cs claude aliases…\n');
|
|
33
|
+
const res = await execInContainer(target, {
|
|
34
|
+
script: APPLY_SCRIPT,
|
|
35
|
+
args: ['claude-aliases', ...ALIAS_LINES]
|
|
36
|
+
});
|
|
37
|
+
log(res.ok ? '✓ claude aliases installed\n' : `⚠ claude aliases setup failed: ${res.error}\n`);
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
async check(target) {
|
|
41
|
+
return checkPresence(target, CHECK_SCRIPT, ['claude-aliases', ...ALIAS_LINES]);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { CLAUDE_CODE_TOKEN, CLAUDE_KEYCHAIN_SERVICE } from '../lib/config.server.ts';
|
|
6
|
+
import { getOption } from '../lib/db.server.ts';
|
|
7
|
+
import { checkPresence, execInContainer, writeSecretFileScript } from '../lib/exec.server.ts';
|
|
8
|
+
import { spawnCapture } from '../lib/spawn.server.ts';
|
|
9
|
+
import type { ContainerTarget, Injection } from '../lib/injections.server.ts';
|
|
10
|
+
|
|
11
|
+
/** A blank field falls through to the env var, so a user can set just one provider. */
|
|
12
|
+
function manualClaudeToken(): string | null {
|
|
13
|
+
if (getOption('manual_tokens_enabled') !== '1') return null;
|
|
14
|
+
return getOption('manual_claude_code_token')?.trim() || null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* `claude` reports "Not logged in" without these, however valid the access token —
|
|
19
|
+
* `scopes` is what flips it (checked against claude 2.1.215; `expiresAt` doesn't matter).
|
|
20
|
+
*/
|
|
21
|
+
const TOKEN_SCOPES = [
|
|
22
|
+
'user:file_upload',
|
|
23
|
+
'user:inference',
|
|
24
|
+
'user:mcp_servers',
|
|
25
|
+
'user:profile',
|
|
26
|
+
'user:sessions:claude_code'
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
export function tokenCredentials(accessToken: string): string {
|
|
30
|
+
return JSON.stringify({ claudeAiOauth: { accessToken, scopes: TOKEN_SCOPES } });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* A stale access token is still worth injecting — `claude` refreshes it in-container —
|
|
35
|
+
* so expiry is judged by the refresh token whenever its expiry is known.
|
|
36
|
+
*/
|
|
37
|
+
export function isValid(json: string): boolean {
|
|
38
|
+
try {
|
|
39
|
+
const data = JSON.parse(json) as {
|
|
40
|
+
claudeAiOauth?: { accessToken?: string; expiresAt?: number; refreshTokenExpiresAt?: number };
|
|
41
|
+
};
|
|
42
|
+
const oauth = data.claudeAiOauth;
|
|
43
|
+
if (!oauth?.accessToken) return false;
|
|
44
|
+
const expiry = oauth.refreshTokenExpiresAt ?? oauth.expiresAt;
|
|
45
|
+
if (typeof expiry === 'number' && expiry <= Date.now()) return false;
|
|
46
|
+
return true;
|
|
47
|
+
} catch {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* `[ -s ]` isn't enough: on a rejected refresh `claude` blanks the tokens in place
|
|
54
|
+
* rather than deleting the file. Whitespace is stripped so compact and pretty JSON both match.
|
|
55
|
+
*/
|
|
56
|
+
export const LIVE_CREDENTIALS_TEST =
|
|
57
|
+
'[ -s "$f" ] && tr -d \'[:space:]\' < "$f" | grep -q \'"accessToken":"[^"]\'';
|
|
58
|
+
|
|
59
|
+
/** macOS keeps these in the login Keychain; everything else uses ~/.claude/.credentials.json. */
|
|
60
|
+
async function locateClaudeCredentials(): Promise<{ creds: string; source: string } | null> {
|
|
61
|
+
const manual = manualClaudeToken();
|
|
62
|
+
if (manual) {
|
|
63
|
+
return { creds: tokenCredentials(manual), source: 'Settings — manual token' };
|
|
64
|
+
}
|
|
65
|
+
if (CLAUDE_CODE_TOKEN) {
|
|
66
|
+
return {
|
|
67
|
+
creds: tokenCredentials(CLAUDE_CODE_TOKEN),
|
|
68
|
+
source: 'CODEBAY_CLAUDE_CODE_TOKEN env var'
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (process.platform === 'darwin') {
|
|
73
|
+
const out = await spawnCapture([
|
|
74
|
+
'security',
|
|
75
|
+
'find-generic-password',
|
|
76
|
+
'-s',
|
|
77
|
+
CLAUDE_KEYCHAIN_SERVICE,
|
|
78
|
+
'-w'
|
|
79
|
+
]);
|
|
80
|
+
if (out && isValid(out)) {
|
|
81
|
+
return { creds: out, source: `macOS Keychain — "${CLAUDE_KEYCHAIN_SERVICE}"` };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const file = join(homedir(), '.claude', '.credentials.json');
|
|
86
|
+
if (existsSync(file)) {
|
|
87
|
+
const raw = (await readFile(file, 'utf8')).trim();
|
|
88
|
+
if (isValid(raw)) return { creds: raw, source: '~/.claude/.credentials.json' };
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** `hasCompletedOnboarding` is what stops `claude` re-running its first-run setup wizard. */
|
|
94
|
+
async function injectClaudeCredentials(
|
|
95
|
+
target: ContainerTarget,
|
|
96
|
+
creds: string
|
|
97
|
+
): Promise<{ ok: boolean; error?: string }> {
|
|
98
|
+
const script =
|
|
99
|
+
'h=$(eval echo ~$(id -un)); d="${CLAUDE_CONFIG_DIR:-$h/.claude}"; ' +
|
|
100
|
+
writeSecretFileScript('$d', '.credentials.json', '600') +
|
|
101
|
+
' cfg="${CLAUDE_CONFIG_DIR:+$CLAUDE_CONFIG_DIR/.claude.json}"; cfg="${cfg:-$h/.claude.json}"; ' +
|
|
102
|
+
'printf \'%s\' \'{"hasCompletedOnboarding":true}\' > "$cfg"; chmod 644 "$cfg"';
|
|
103
|
+
const res = await execInContainer(target, { script, stdin: creds });
|
|
104
|
+
return res.ok ? { ok: true } : { ok: false, error: res.error };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Caveat: refresh tokens rotate, so snapshotting a live login hands one lineage to two
|
|
109
|
+
* `claude` installs and whichever refreshes first logs the other out. Only `setup-token` fixes it.
|
|
110
|
+
*/
|
|
111
|
+
export const claudeCodeCredentials: Injection = {
|
|
112
|
+
id: 'claude-code-credentials',
|
|
113
|
+
label: 'Claude Code',
|
|
114
|
+
|
|
115
|
+
auth: {
|
|
116
|
+
hint: 'run `claude` and sign in',
|
|
117
|
+
async status() {
|
|
118
|
+
const found = await locateClaudeCredentials();
|
|
119
|
+
return { available: found !== null, source: found?.source ?? null };
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
async apply(target, log) {
|
|
124
|
+
const found = await locateClaudeCredentials();
|
|
125
|
+
if (!found) {
|
|
126
|
+
log('⚠ No Claude Code credentials found on host; skipped auth injection\n');
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
log('Injecting Claude Code credentials…\n');
|
|
130
|
+
const injected = await injectClaudeCredentials(target, found.creds);
|
|
131
|
+
log(
|
|
132
|
+
injected.ok
|
|
133
|
+
? '✓ Claude Code authorized in container\n'
|
|
134
|
+
: `⚠ Claude auth injection failed: ${injected.error}\n`
|
|
135
|
+
);
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
async check(target) {
|
|
139
|
+
return checkPresence(
|
|
140
|
+
target,
|
|
141
|
+
'h=$(eval echo ~$(id -un)); d="${CLAUDE_CONFIG_DIR:-$h/.claude}"; f="$d/.credentials.json"; ' +
|
|
142
|
+
`if ${LIVE_CREDENTIALS_TEST}; then echo 1; else echo 0; fi`
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
};
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { getOption } from '../lib/db.server.ts';
|
|
2
|
+
import { checkPresence, execInContainer } from '../lib/exec.server.ts';
|
|
3
|
+
import type { ContainerTarget, Injection } from '../lib/injections.server.ts';
|
|
4
|
+
|
|
5
|
+
/** Mirrors the reference launcher script (claude-code.sh); each is overridable in Settings. */
|
|
6
|
+
export const DEFAULT_OPUS_MODEL = 'eu.anthropic.claude-opus-4-8';
|
|
7
|
+
export const DEFAULT_SONNET_MODEL = 'eu.anthropic.claude-sonnet-4-6';
|
|
8
|
+
export const DEFAULT_HAIKU_MODEL = 'eu.anthropic.claude-haiku-4-5-20251001-v1:0';
|
|
9
|
+
export const DEFAULT_SMALL_FAST_MODEL = 'eu.anthropic.claude-haiku-4-5-20251001-v1:0';
|
|
10
|
+
export const DEFAULT_MODEL = 'opusplan';
|
|
11
|
+
|
|
12
|
+
/** Null means "skip the injection" — disabled, or a blank base URL or token. */
|
|
13
|
+
export function customEndpointConfig(): {
|
|
14
|
+
baseUrl: string;
|
|
15
|
+
token: string;
|
|
16
|
+
opusModel: string;
|
|
17
|
+
sonnetModel: string;
|
|
18
|
+
haikuModel: string;
|
|
19
|
+
smallFastModel: string;
|
|
20
|
+
defaultModel: string;
|
|
21
|
+
} | null {
|
|
22
|
+
if (getOption('custom_endpoint_enabled') !== '1') return null;
|
|
23
|
+
const baseUrl = getOption('custom_endpoint_base_url')?.trim() || '';
|
|
24
|
+
const token = getOption('custom_endpoint_token')?.trim() || '';
|
|
25
|
+
if (!baseUrl || !token) return null;
|
|
26
|
+
return {
|
|
27
|
+
baseUrl,
|
|
28
|
+
token,
|
|
29
|
+
opusModel: getOption('custom_endpoint_opus_model')?.trim() || DEFAULT_OPUS_MODEL,
|
|
30
|
+
sonnetModel: getOption('custom_endpoint_sonnet_model')?.trim() || DEFAULT_SONNET_MODEL,
|
|
31
|
+
haikuModel: getOption('custom_endpoint_haiku_model')?.trim() || DEFAULT_HAIKU_MODEL,
|
|
32
|
+
smallFastModel:
|
|
33
|
+
getOption('custom_endpoint_small_fast_model')?.trim() || DEFAULT_SMALL_FAST_MODEL,
|
|
34
|
+
defaultModel: getOption('custom_endpoint_model')?.trim() || DEFAULT_MODEL
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const ENV_FILE = '~/.codebay-claude-env';
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Non-secret values ride `args`, so nothing needs hand-quoting; only the token uses `stdin`.
|
|
42
|
+
* The file mixes secret and non-secret content, hence no `writeSecretFileScript`.
|
|
43
|
+
*/
|
|
44
|
+
async function injectCustomEndpoint(
|
|
45
|
+
target: ContainerTarget,
|
|
46
|
+
config: NonNullable<ReturnType<typeof customEndpointConfig>>
|
|
47
|
+
): Promise<{ ok: boolean; error?: string }> {
|
|
48
|
+
const script =
|
|
49
|
+
'set -e; f=$(eval echo "' +
|
|
50
|
+
ENV_FILE +
|
|
51
|
+
'"); ' +
|
|
52
|
+
'{ ' +
|
|
53
|
+
"printf '%s\\n' " +
|
|
54
|
+
"'export DISABLE_AUTOUPDATER=1' " +
|
|
55
|
+
"'export CLAUDE_CODE_USE_BEDROCK=1' " +
|
|
56
|
+
"'export CLAUDE_CODE_SKIP_BEDROCK_AUTH=1'; " +
|
|
57
|
+
'printf \'export ANTHROPIC_BEDROCK_BASE_URL=%s\\n\' "$1"; ' +
|
|
58
|
+
'printf \'export ANTHROPIC_AUTH_TOKEN=%s\\n\' "$CODEBAY_STDIN"; ' +
|
|
59
|
+
'printf \'export ANTHROPIC_DEFAULT_OPUS_MODEL=%s\\n\' "$2"; ' +
|
|
60
|
+
'printf \'export ANTHROPIC_DEFAULT_SONNET_MODEL=%s\\n\' "$3"; ' +
|
|
61
|
+
'printf \'export ANTHROPIC_DEFAULT_HAIKU_MODEL=%s\\n\' "$4"; ' +
|
|
62
|
+
'printf \'export ANTHROPIC_SMALL_FAST_MODEL=%s\\n\' "$5"; ' +
|
|
63
|
+
'printf \'export ANTHROPIC_MODEL=%s\\n\' "$6"; ' +
|
|
64
|
+
'} > "$f"; chmod 600 "$f"; ' +
|
|
65
|
+
// The grep guard keeps a re-apply from stacking duplicate source lines.
|
|
66
|
+
'h=$(eval echo ~$(id -un)); src="[ -f \\"$f\\" ] && . \\"$f\\""; ' +
|
|
67
|
+
'for rc in "$h/.bashrc" "$h/.zshrc"; do ' +
|
|
68
|
+
'grep -qF "$src" "$rc" 2>/dev/null || printf \'%s\\n\' "$src" >> "$rc"; ' +
|
|
69
|
+
'done; ' +
|
|
70
|
+
// `hasCompletedOnboarding` is what suppresses `claude`'s first-run wizard.
|
|
71
|
+
'h=$(eval echo ~$(id -un)); ' +
|
|
72
|
+
'cfg="${CLAUDE_CONFIG_DIR:+$CLAUDE_CONFIG_DIR/.claude.json}"; cfg="${cfg:-$h/.claude.json}"; ' +
|
|
73
|
+
'printf \'%s\' \'{"hasCompletedOnboarding":true}\' > "$cfg"; chmod 644 "$cfg"';
|
|
74
|
+
|
|
75
|
+
const res = await execInContainer(target, {
|
|
76
|
+
script,
|
|
77
|
+
stdin: config.token,
|
|
78
|
+
args: [
|
|
79
|
+
'claude-custom',
|
|
80
|
+
config.baseUrl,
|
|
81
|
+
config.opusModel,
|
|
82
|
+
config.sonnetModel,
|
|
83
|
+
config.haikuModel,
|
|
84
|
+
config.smallFastModel,
|
|
85
|
+
config.defaultModel
|
|
86
|
+
]
|
|
87
|
+
});
|
|
88
|
+
return res.ok ? { ok: true } : { ok: false, error: res.error };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Replaces `claude-code-credentials` in the registry when the custom-endpoint setting is on. */
|
|
92
|
+
export const claudeCodeCustom: Injection = {
|
|
93
|
+
id: 'claude-code-custom',
|
|
94
|
+
label: 'LiteLLM + Bedrock',
|
|
95
|
+
|
|
96
|
+
auth: {
|
|
97
|
+
hint: 'set the LiteLLM URL + token in Settings',
|
|
98
|
+
async status() {
|
|
99
|
+
const config = customEndpointConfig();
|
|
100
|
+
return {
|
|
101
|
+
available: config !== null,
|
|
102
|
+
source: config ? config.baseUrl : null
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
async apply(target, log) {
|
|
108
|
+
const config = customEndpointConfig();
|
|
109
|
+
if (!config) {
|
|
110
|
+
log('⚠ LiteLLM + Bedrock not configured (base URL or token missing); skipped\n');
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
log(`Injecting LiteLLM + Bedrock endpoint (${config.baseUrl})…\n`);
|
|
114
|
+
const result = await injectCustomEndpoint(target, config);
|
|
115
|
+
log(
|
|
116
|
+
result.ok
|
|
117
|
+
? '✓ LiteLLM + Bedrock endpoint configured in container\n'
|
|
118
|
+
: `⚠ LiteLLM + Bedrock injection failed: ${result.error}\n`
|
|
119
|
+
);
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
async check(target) {
|
|
123
|
+
return checkPresence(target, `f=$(eval echo "${ENV_FILE}"); [ -s "$f" ] && echo 1 || echo 0`);
|
|
124
|
+
}
|
|
125
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { checkPresence, execInContainer } from '../lib/exec.server.ts';
|
|
2
|
+
import { mergeClaudeSettingsScript } from './attention-hooks.ts';
|
|
3
|
+
import type { ContainerTarget, Injection } from '../lib/injections.server.ts';
|
|
4
|
+
|
|
5
|
+
async function injectNoCoauthor(target: ContainerTarget): Promise<{ ok: boolean; error?: string }> {
|
|
6
|
+
const res = await execInContainer(target, {
|
|
7
|
+
script: mergeClaudeSettingsScript(),
|
|
8
|
+
stdin: JSON.stringify({ includeCoAuthoredBy: false })
|
|
9
|
+
});
|
|
10
|
+
return res.ok ? { ok: true } : { ok: false, error: res.error };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** A Codebay-wide default rather than an opt-in setting, hence no `auth` block. */
|
|
14
|
+
export const claudeNoCoauthor: Injection = {
|
|
15
|
+
id: 'claude-no-coauthor',
|
|
16
|
+
label: 'no co-author byline',
|
|
17
|
+
|
|
18
|
+
async apply(target, log) {
|
|
19
|
+
log('Disabling Claude co-author commit byline…\n');
|
|
20
|
+
const result = await injectNoCoauthor(target);
|
|
21
|
+
log(
|
|
22
|
+
result.ok
|
|
23
|
+
? '✓ Claude co-author byline disabled\n'
|
|
24
|
+
: `⚠ Claude co-author byline injection failed: ${result.error}\n`
|
|
25
|
+
);
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
async check(target) {
|
|
29
|
+
return checkPresence(
|
|
30
|
+
target,
|
|
31
|
+
'h=$(eval echo ~$(id -un)); d="${CLAUDE_CONFIG_DIR:-$h/.claude}"; ' +
|
|
32
|
+
'[ -s "$d/settings.json" ] && grep -q includeCoAuthoredBy "$d/settings.json" && echo 1 || echo 0'
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { checkPresence, execInContainer } from '../lib/exec.server.ts';
|
|
2
|
+
import type { Injection } from '../lib/injections.server.ts';
|
|
3
|
+
|
|
4
|
+
const ALIAS_LINE = "alias claude='claude --dangerously-skip-permissions'";
|
|
5
|
+
|
|
6
|
+
/** Both rc files, since either shell may be the one code-server opens. */
|
|
7
|
+
const APPLY_SCRIPT =
|
|
8
|
+
'h=$(eval echo ~$(id -un)); ' +
|
|
9
|
+
`line="${ALIAS_LINE}"; ` +
|
|
10
|
+
'for f in "$h/.bashrc" "$h/.zshrc"; do ' +
|
|
11
|
+
'grep -qF "$line" "$f" 2>/dev/null || printf \'%s\\n\' "$line" >> "$f"; ' +
|
|
12
|
+
'done';
|
|
13
|
+
|
|
14
|
+
const CHECK_SCRIPT =
|
|
15
|
+
'h=$(eval echo ~$(id -un)); ' +
|
|
16
|
+
`line="${ALIAS_LINE}"; ` +
|
|
17
|
+
'if grep -qF "$line" "$h/.bashrc" 2>/dev/null || grep -qF "$line" "$h/.zshrc" 2>/dev/null; ' +
|
|
18
|
+
'then echo 1; else echo 0; fi';
|
|
19
|
+
|
|
20
|
+
/** Safe here only because instances are throwaway, single-tenant sandboxes. */
|
|
21
|
+
export const claudeSkipPermissions: Injection = {
|
|
22
|
+
id: 'claude-skip-permissions',
|
|
23
|
+
label: 'claude skip-permissions alias',
|
|
24
|
+
|
|
25
|
+
async apply(target, log) {
|
|
26
|
+
log('Installing claude --dangerously-skip-permissions alias…\n');
|
|
27
|
+
const res = await execInContainer(target, { script: APPLY_SCRIPT });
|
|
28
|
+
log(
|
|
29
|
+
res.ok
|
|
30
|
+
? '✓ claude skip-permissions alias installed\n'
|
|
31
|
+
: `⚠ claude alias setup failed: ${res.error}\n`
|
|
32
|
+
);
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
async check(target) {
|
|
36
|
+
return checkPresence(target, CHECK_SCRIPT);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { checkPresence, execInContainer } from '../lib/exec.server.ts';
|
|
6
|
+
import { mergeClaudeSettingsScript } from './attention-hooks.ts';
|
|
7
|
+
import type { ContainerTarget, Injection } from '../lib/injections.server.ts';
|
|
8
|
+
|
|
9
|
+
/** Left unexpanded — Claude Code invokes this long after the injection has finished. */
|
|
10
|
+
const CONTAINER_SCRIPT_PATH = '${CLAUDE_CONFIG_DIR:-$HOME/.claude}/statusline.sh';
|
|
11
|
+
|
|
12
|
+
interface StatusLineConfig {
|
|
13
|
+
statusLine: Record<string, unknown>;
|
|
14
|
+
/** Only set when `command` references a script file. */
|
|
15
|
+
script?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function readHostClaudeSettings(): Promise<Record<string, unknown> | null> {
|
|
19
|
+
const file = join(homedir(), '.claude', 'settings.json');
|
|
20
|
+
if (!existsSync(file)) return null;
|
|
21
|
+
try {
|
|
22
|
+
return JSON.parse(await readFile(file, 'utf8')) as Record<string, unknown>;
|
|
23
|
+
} catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** A heuristic: a statusLine command is either a script path or a package runner with no file. */
|
|
29
|
+
export function extractScriptPath(command: string): string | null {
|
|
30
|
+
const token = command.split(/\s+/).find((t) => t.startsWith('/') && existsSync(t));
|
|
31
|
+
return token ?? null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** A referenced script is read and its command rewritten, since the host path won't exist inside. */
|
|
35
|
+
export async function readStatusLineConfig(): Promise<StatusLineConfig | null> {
|
|
36
|
+
const settings = await readHostClaudeSettings();
|
|
37
|
+
const statusLine = settings?.statusLine as { type?: string; command?: string } | undefined;
|
|
38
|
+
if (!statusLine || statusLine.type !== 'command' || !statusLine.command) return null;
|
|
39
|
+
|
|
40
|
+
const hostPath = extractScriptPath(statusLine.command);
|
|
41
|
+
if (!hostPath) return { statusLine };
|
|
42
|
+
|
|
43
|
+
let script: string;
|
|
44
|
+
try {
|
|
45
|
+
script = await readFile(hostPath, 'utf8');
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
const command = statusLine.command.replaceAll(hostPath, CONTAINER_SCRIPT_PATH);
|
|
50
|
+
return { statusLine: { ...statusLine, command }, script };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Two execs rather than one, because `execInContainer` carries a single `stdin` payload per call. */
|
|
54
|
+
async function injectStatusLine(
|
|
55
|
+
target: ContainerTarget,
|
|
56
|
+
config: StatusLineConfig
|
|
57
|
+
): Promise<{ ok: boolean; error?: string }> {
|
|
58
|
+
if (config.script) {
|
|
59
|
+
const writeScript =
|
|
60
|
+
'h=$(eval echo ~$(id -un)); d="${CLAUDE_CONFIG_DIR:-$h/.claude}"; mkdir -p "$d"; ' +
|
|
61
|
+
'printf \'%s\' "$CODEBAY_STDIN" > "$d/statusline.sh"; chmod 755 "$d/statusline.sh"';
|
|
62
|
+
const res = await execInContainer(target, { script: writeScript, stdin: config.script });
|
|
63
|
+
if (!res.ok) return { ok: false, error: res.error };
|
|
64
|
+
}
|
|
65
|
+
const res = await execInContainer(target, {
|
|
66
|
+
script: mergeClaudeSettingsScript(),
|
|
67
|
+
stdin: JSON.stringify({ statusLine: config.statusLine })
|
|
68
|
+
});
|
|
69
|
+
return res.ok ? { ok: true } : { ok: false, error: res.error };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export const claudeStatusline: Injection = {
|
|
73
|
+
id: 'claude-statusline',
|
|
74
|
+
label: 'statusLine',
|
|
75
|
+
|
|
76
|
+
auth: {
|
|
77
|
+
hint: 'configure `statusLine` in ~/.claude/settings.json',
|
|
78
|
+
async status() {
|
|
79
|
+
const config = await readStatusLineConfig();
|
|
80
|
+
return {
|
|
81
|
+
available: config !== null,
|
|
82
|
+
source: config ? '~/.claude/settings.json' : null
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
async apply(target, log) {
|
|
88
|
+
const config = await readStatusLineConfig();
|
|
89
|
+
if (!config) {
|
|
90
|
+
log('⚠ No statusLine configured on host; skipped\n');
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
log('Injecting Claude Code statusLine…\n');
|
|
94
|
+
const result = await injectStatusLine(target, config);
|
|
95
|
+
log(
|
|
96
|
+
result.ok
|
|
97
|
+
? '✓ statusLine installed in container\n'
|
|
98
|
+
: `⚠ statusLine injection failed: ${result.error}\n`
|
|
99
|
+
);
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
async check(target) {
|
|
103
|
+
return checkPresence(
|
|
104
|
+
target,
|
|
105
|
+
'h=$(eval echo ~$(id -un)); d="${CLAUDE_CONFIG_DIR:-$h/.claude}"; ' +
|
|
106
|
+
'[ -s "$d/settings.json" ] && grep -q statusLine "$d/settings.json" && echo 1 || echo 0'
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { checkPresence, execInContainer } from '../lib/exec.server.ts';
|
|
2
|
+
import { spawnCapture } from '../lib/spawn.server.ts';
|
|
3
|
+
import type { Injection } from '../lib/injections.server.ts';
|
|
4
|
+
|
|
5
|
+
interface GitIdentity {
|
|
6
|
+
name: string;
|
|
7
|
+
email: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function readGitConfig(key: string): Promise<string | null> {
|
|
11
|
+
return spawnCapture(['git', 'config', '--global', '--get', key]);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** `--global` so this reads the host user's identity, not the manager checkout's override. */
|
|
15
|
+
async function readGitIdentity(): Promise<GitIdentity | null> {
|
|
16
|
+
const [name, email] = await Promise.all([
|
|
17
|
+
readGitConfig('user.name'),
|
|
18
|
+
readGitConfig('user.email')
|
|
19
|
+
]);
|
|
20
|
+
return name && email ? { name, email } : null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Without this, `git commit` in the container fails outright or commits as `root@<container>`. */
|
|
24
|
+
export const gitIdentity: Injection = {
|
|
25
|
+
id: 'git-identity',
|
|
26
|
+
label: 'git identity',
|
|
27
|
+
|
|
28
|
+
auth: {
|
|
29
|
+
hint: 'run `git config --global user.name/.email`',
|
|
30
|
+
async status() {
|
|
31
|
+
const identity = await readGitIdentity();
|
|
32
|
+
return identity
|
|
33
|
+
? { available: true, source: `git config — ${identity.name}` }
|
|
34
|
+
: { available: false, source: null };
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
async apply(target, log) {
|
|
39
|
+
const identity = await readGitIdentity();
|
|
40
|
+
if (!identity) {
|
|
41
|
+
log('⚠ No global git identity found on host; skipped git identity injection\n');
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
log('Injecting git identity…\n');
|
|
45
|
+
// Passed as args so a name with spaces or quotes needs no escaping.
|
|
46
|
+
const res = await execInContainer(target, {
|
|
47
|
+
script: 'git config --global user.name "$1"; git config --global user.email "$2"',
|
|
48
|
+
args: ['git-identity', identity.name, identity.email]
|
|
49
|
+
});
|
|
50
|
+
log(
|
|
51
|
+
res.ok
|
|
52
|
+
? `✓ git identity set to ${identity.name} <${identity.email}>\n`
|
|
53
|
+
: `⚠ git identity injection failed: ${res.error}\n`
|
|
54
|
+
);
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
async check(target) {
|
|
58
|
+
return checkPresence(
|
|
59
|
+
target,
|
|
60
|
+
'[ -n "$(git config --global user.name)" ] && [ -n "$(git config --global user.email)" ] && echo 1 || echo 0'
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
};
|