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
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import { enhance } from 'mochi-framework';
|
|
12
12
|
import type { MochiEnhanceOptions } from 'mochi-framework';
|
|
13
13
|
import { onBackLinkClick, installPopupBackTrap } from '../lib/popup-nav.ts';
|
|
14
|
+
import { tick } from 'svelte';
|
|
14
15
|
|
|
15
16
|
let { id, injectionChecks = 0 }: { id: string; injectionChecks?: number } = $props();
|
|
16
17
|
|
|
@@ -18,11 +19,29 @@
|
|
|
18
19
|
let health = $state<InstanceHealth | null>(null);
|
|
19
20
|
let lastFetchedAt = $state<number | null>(null);
|
|
20
21
|
let logs = $state('');
|
|
22
|
+
let logEl = $state<HTMLDivElement | null>(null);
|
|
23
|
+
let following = $state(true);
|
|
21
24
|
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
// Pin to the newest output whenever logs grow — but only while following.
|
|
26
|
+
// tick() lets the <pre> text node grow before we scroll, so we reach the true bottom.
|
|
27
|
+
$effect(() => {
|
|
28
|
+
if (!following || logs.length === 0) return; // reading logs.length tracks appended chunks
|
|
29
|
+
tick().then(() => {
|
|
30
|
+
if (following) logEl?.scrollTo(0, logEl.scrollHeight);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// A user scrolling up pauses following; programmatic scroll-to-bottom keeps the
|
|
35
|
+
// gap ~0, so it never trips this — only re-clicking "Follow log" resumes.
|
|
36
|
+
function onLogScroll() {
|
|
37
|
+
if (!logEl) return;
|
|
38
|
+
const gap = logEl.scrollHeight - logEl.clientHeight - logEl.scrollTop;
|
|
39
|
+
if (gap > 40) following = false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function toggleFollow() {
|
|
43
|
+
following = !following;
|
|
44
|
+
if (following) logEl?.scrollTo(0, logEl.scrollHeight);
|
|
26
45
|
}
|
|
27
46
|
|
|
28
47
|
$effect(() => installPopupBackTrap());
|
|
@@ -234,11 +253,16 @@
|
|
|
234
253
|
<div class="logwrap panel">
|
|
235
254
|
<div class="log-bar panel-bar">
|
|
236
255
|
<span>Boot log</span>
|
|
237
|
-
<
|
|
238
|
-
|
|
239
|
-
|
|
256
|
+
<div class="log-actions">
|
|
257
|
+
<button class="copy" class:active={following} onclick={toggleFollow}>
|
|
258
|
+
{following ? 'Following' : 'Follow log'}
|
|
259
|
+
</button>
|
|
260
|
+
<button class="copy" onclick={copyLogs} disabled={!logs}>
|
|
261
|
+
{copied ? 'Copied!' : 'Copy logs'}
|
|
262
|
+
</button>
|
|
263
|
+
</div>
|
|
240
264
|
</div>
|
|
241
|
-
<div class="logs" {
|
|
265
|
+
<div class="logs" bind:this={logEl} onscroll={onLogScroll}>
|
|
242
266
|
<pre>{logs || 'Waiting for output…'}<span class="caret"></span></pre>
|
|
243
267
|
</div>
|
|
244
268
|
{#if instance?.status === 'error' && instance.error}
|
|
@@ -504,6 +528,11 @@
|
|
|
504
528
|
align-items: center;
|
|
505
529
|
justify-content: space-between;
|
|
506
530
|
}
|
|
531
|
+
.log-actions {
|
|
532
|
+
display: flex;
|
|
533
|
+
gap: 6px;
|
|
534
|
+
align-items: center;
|
|
535
|
+
}
|
|
507
536
|
.copy {
|
|
508
537
|
font-family: var(--font-mono);
|
|
509
538
|
font-weight: 600;
|
|
@@ -520,6 +549,10 @@
|
|
|
520
549
|
background: transparent;
|
|
521
550
|
color: var(--bg);
|
|
522
551
|
}
|
|
552
|
+
.copy.active {
|
|
553
|
+
background: transparent;
|
|
554
|
+
color: var(--bg);
|
|
555
|
+
}
|
|
523
556
|
.copy:disabled {
|
|
524
557
|
opacity: 0.5;
|
|
525
558
|
cursor: not-allowed;
|
|
@@ -878,8 +878,8 @@
|
|
|
878
878
|
<div class="text">
|
|
879
879
|
<div class="name">Rebuild running containers (no cache)</div>
|
|
880
880
|
<div class="desc">
|
|
881
|
-
|
|
882
|
-
In-container edits are kept; stopped instances are left alone.
|
|
881
|
+
Rebuild every currently-running instance from scratch, without using any cached image
|
|
882
|
+
layers. In-container edits are kept; stopped instances are left alone.
|
|
883
883
|
</div>
|
|
884
884
|
{#if rebuildError}
|
|
885
885
|
<div class="msg error">{rebuildError}</div>
|
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
import Plus from '@lucide/svelte/icons/plus';
|
|
3
3
|
import Component from '@lucide/svelte/icons/component';
|
|
4
4
|
import { isDev } from 'mochi-framework';
|
|
5
|
-
import type { AuthProvider } from '../types.ts';
|
|
5
|
+
import type { AuthProvider, InstanceFilter } from '../types.ts';
|
|
6
6
|
import type { AvatarArt } from '../avatars/index.ts';
|
|
7
7
|
import Brand from './Brand.svelte';
|
|
8
|
+
import InstanceFilterControl from './InstanceFilter.svelte';
|
|
8
9
|
import SettingsCog from './SettingsCog.svelte';
|
|
9
10
|
import CredMenu from './CredMenu.svelte';
|
|
10
11
|
import Button from './Button.svelte';
|
|
@@ -16,6 +17,8 @@
|
|
|
16
17
|
canDelete = false,
|
|
17
18
|
ready = true,
|
|
18
19
|
creating = false,
|
|
20
|
+
filter,
|
|
21
|
+
onFilter,
|
|
19
22
|
onNew,
|
|
20
23
|
onDeleteAll
|
|
21
24
|
}: {
|
|
@@ -24,6 +27,8 @@
|
|
|
24
27
|
canDelete?: boolean;
|
|
25
28
|
ready?: boolean;
|
|
26
29
|
creating?: boolean;
|
|
30
|
+
filter?: InstanceFilter;
|
|
31
|
+
onFilter?: (v: InstanceFilter) => void;
|
|
27
32
|
onNew?: () => void;
|
|
28
33
|
onDeleteAll?: () => void;
|
|
29
34
|
} = $props();
|
|
@@ -35,6 +40,9 @@
|
|
|
35
40
|
{#if isDev}
|
|
36
41
|
<Button variant="default" size="sm" icon={Component} href="/debug">Debug</Button>
|
|
37
42
|
{/if}
|
|
43
|
+
{#if filter && onFilter}
|
|
44
|
+
<InstanceFilterControl value={filter} onchange={onFilter} />
|
|
45
|
+
{/if}
|
|
38
46
|
<SettingsCog />
|
|
39
47
|
<CredMenu {auth} />
|
|
40
48
|
<Button variant="danger" size="sm" onclick={onDeleteAll} disabled={!canDelete}>
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { PORT } from '../lib/config.server.ts';
|
|
2
|
-
import {
|
|
2
|
+
import { writeContainerFile } from '../lib/container-files.server.ts';
|
|
3
|
+
import {
|
|
4
|
+
claudeConfigFile,
|
|
5
|
+
mergeClaudeSettings,
|
|
6
|
+
readClaudeSettings
|
|
7
|
+
} from '../lib/claude-settings.server.ts';
|
|
3
8
|
import type { ContainerTarget, Injection } from '../lib/injections.server.ts';
|
|
4
9
|
|
|
5
10
|
function bridgeUrl(): string {
|
|
@@ -40,50 +45,32 @@ function hookFor(id: string, state: 'done' | 'waiting' | 'busy') {
|
|
|
40
45
|
}
|
|
41
46
|
|
|
42
47
|
/** Carries no token — that lives in the mode-600 header file `writeBridgeHeader` stages. */
|
|
43
|
-
export function attentionHookSettings(id: string): string {
|
|
44
|
-
|
|
48
|
+
export function attentionHookSettings(id: string): Record<string, unknown> {
|
|
49
|
+
return {
|
|
45
50
|
hooks: {
|
|
46
51
|
Stop: hookFor(id, 'done'),
|
|
47
52
|
Notification: hookFor(id, 'waiting'),
|
|
48
53
|
UserPromptSubmit: hookFor(id, 'busy')
|
|
49
54
|
}
|
|
50
55
|
};
|
|
51
|
-
return JSON.stringify(settings, null, 2);
|
|
52
56
|
}
|
|
53
57
|
|
|
54
58
|
/** Stored as a whole header line so curl can consume the file directly with `-H @`. */
|
|
55
|
-
|
|
59
|
+
function writeBridgeHeader(
|
|
56
60
|
target: ContainerTarget,
|
|
57
61
|
token: string
|
|
58
62
|
): Promise<{ ok: boolean; error?: string }> {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
'
|
|
62
|
-
|
|
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"'
|
|
63
|
+
return writeContainerFile(
|
|
64
|
+
target,
|
|
65
|
+
claudeConfigFile('.bridge-header', '600'),
|
|
66
|
+
`X-Bridge-Token: ${token}\n`
|
|
75
67
|
);
|
|
76
68
|
}
|
|
77
69
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const res = await execInContainer(target, {
|
|
83
|
-
script: mergeClaudeSettingsScript(),
|
|
84
|
-
stdin: settingsJson
|
|
85
|
-
});
|
|
86
|
-
return res.ok ? { ok: true } : { ok: false, error: res.error };
|
|
70
|
+
/** Matches the delimited `id=<id>&state=` fragment of the hook's curl URL, so no other instance id can match by substring. */
|
|
71
|
+
export function hasAttentionHook(settings: Record<string, unknown> | null, id: string): boolean {
|
|
72
|
+
if (settings === null) return false;
|
|
73
|
+
return JSON.stringify(settings.hooks ?? null).includes(`id=${encodeURIComponent(id)}&state=`);
|
|
87
74
|
}
|
|
88
75
|
|
|
89
76
|
export const attentionHooks: Injection = {
|
|
@@ -97,8 +84,7 @@ export const attentionHooks: Injection = {
|
|
|
97
84
|
log(`⚠ Claude hook injection failed: ${header.error}\n`);
|
|
98
85
|
return;
|
|
99
86
|
}
|
|
100
|
-
const
|
|
101
|
-
const hooks = await injectClaudeHooks(target, settings);
|
|
87
|
+
const hooks = await mergeClaudeSettings(target, attentionHookSettings(target.instance.id));
|
|
102
88
|
log(
|
|
103
89
|
hooks.ok
|
|
104
90
|
? '✓ Claude attention hooks installed\n'
|
|
@@ -107,11 +93,6 @@ export const attentionHooks: Injection = {
|
|
|
107
93
|
},
|
|
108
94
|
|
|
109
95
|
async check(target) {
|
|
110
|
-
return
|
|
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
|
-
);
|
|
96
|
+
return hasAttentionHook(await readClaudeSettings(target), target.instance.id);
|
|
116
97
|
}
|
|
117
98
|
};
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
appendLinesIfAbsent,
|
|
3
|
+
linesPresent,
|
|
4
|
+
SHELL_RC_FILES
|
|
5
|
+
} from '../lib/container-files.server.ts';
|
|
2
6
|
import type { Injection } from '../lib/injections.server.ts';
|
|
3
7
|
|
|
4
8
|
/** Both resolve `claude`, which `claude-skip-permissions` also aliases, so the two compose. */
|
|
@@ -7,37 +11,17 @@ const ALIAS_LINES = [
|
|
|
7
11
|
"alias cs='claude --model sonnet'"
|
|
8
12
|
];
|
|
9
13
|
|
|
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
14
|
export const claudeAliases: Injection = {
|
|
28
15
|
id: 'claude-aliases',
|
|
29
16
|
label: 'claude aliases',
|
|
30
17
|
|
|
31
18
|
async apply(target, log) {
|
|
32
19
|
log('Installing c200 / cs claude aliases…\n');
|
|
33
|
-
const res = await
|
|
34
|
-
script: APPLY_SCRIPT,
|
|
35
|
-
args: ['claude-aliases', ...ALIAS_LINES]
|
|
36
|
-
});
|
|
20
|
+
const res = await appendLinesIfAbsent(target, SHELL_RC_FILES, ALIAS_LINES);
|
|
37
21
|
log(res.ok ? '✓ claude aliases installed\n' : `⚠ claude aliases setup failed: ${res.error}\n`);
|
|
38
22
|
},
|
|
39
23
|
|
|
40
24
|
async check(target) {
|
|
41
|
-
return
|
|
25
|
+
return linesPresent(target, SHELL_RC_FILES, ALIAS_LINES);
|
|
42
26
|
}
|
|
43
27
|
};
|
|
@@ -4,7 +4,9 @@ import { homedir } from 'node:os';
|
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { CLAUDE_CODE_TOKEN, CLAUDE_KEYCHAIN_SERVICE } from '../lib/config.server.ts';
|
|
6
6
|
import { getOption } from '../lib/db.server.ts';
|
|
7
|
-
import { checkPresence
|
|
7
|
+
import { checkPresence } from '../lib/exec.server.ts';
|
|
8
|
+
import { deepMerge, editJsonFile, writeContainerFile } from '../lib/container-files.server.ts';
|
|
9
|
+
import { CLAUDE_JSON_FILE, claudeConfigFile } from '../lib/claude-settings.server.ts';
|
|
8
10
|
import { spawnCapture } from '../lib/spawn.server.ts';
|
|
9
11
|
import type { ContainerTarget, Injection } from '../lib/injections.server.ts';
|
|
10
12
|
|
|
@@ -95,13 +97,15 @@ async function injectClaudeCredentials(
|
|
|
95
97
|
target: ContainerTarget,
|
|
96
98
|
creds: string
|
|
97
99
|
): Promise<{ ok: boolean; error?: string }> {
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
return
|
|
100
|
+
const wrote = await writeContainerFile(
|
|
101
|
+
target,
|
|
102
|
+
claudeConfigFile('.credentials.json', '600'),
|
|
103
|
+
creds
|
|
104
|
+
);
|
|
105
|
+
if (!wrote.ok) return wrote;
|
|
106
|
+
return editJsonFile(target, CLAUDE_JSON_FILE, (cur) =>
|
|
107
|
+
deepMerge(cur, { hasCompletedOnboarding: true })
|
|
108
|
+
);
|
|
105
109
|
}
|
|
106
110
|
|
|
107
111
|
/**
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { getOption } from '../lib/db.server.ts';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
containerFileExists,
|
|
4
|
+
shellEnvFile,
|
|
5
|
+
deepMerge,
|
|
6
|
+
editJsonFile,
|
|
7
|
+
installShellEnvFile,
|
|
8
|
+
shellSingleQuote
|
|
9
|
+
} from '../lib/container-files.server.ts';
|
|
10
|
+
import { CLAUDE_JSON_FILE } from '../lib/claude-settings.server.ts';
|
|
3
11
|
import type { ContainerTarget, Injection } from '../lib/injections.server.ts';
|
|
4
12
|
|
|
5
13
|
/** Mirrors the reference launcher script (claude-code.sh); each is overridable in Settings. */
|
|
@@ -35,57 +43,36 @@ export function customEndpointConfig(): {
|
|
|
35
43
|
};
|
|
36
44
|
}
|
|
37
45
|
|
|
38
|
-
const
|
|
46
|
+
const ENV_FILE_NAME = '.codebay-claude-env';
|
|
47
|
+
|
|
48
|
+
/** The whole file (token included) rides `stdin` inside `installShellEnvFile`, so nothing hits argv. */
|
|
49
|
+
function envFileContent(config: NonNullable<ReturnType<typeof customEndpointConfig>>): string {
|
|
50
|
+
return (
|
|
51
|
+
[
|
|
52
|
+
'export DISABLE_AUTOUPDATER=1',
|
|
53
|
+
'export CLAUDE_CODE_USE_BEDROCK=1',
|
|
54
|
+
'export CLAUDE_CODE_SKIP_BEDROCK_AUTH=1',
|
|
55
|
+
`export ANTHROPIC_BEDROCK_BASE_URL=${shellSingleQuote(config.baseUrl)}`,
|
|
56
|
+
`export ANTHROPIC_AUTH_TOKEN=${shellSingleQuote(config.token)}`,
|
|
57
|
+
`export ANTHROPIC_DEFAULT_OPUS_MODEL=${shellSingleQuote(config.opusModel)}`,
|
|
58
|
+
`export ANTHROPIC_DEFAULT_SONNET_MODEL=${shellSingleQuote(config.sonnetModel)}`,
|
|
59
|
+
`export ANTHROPIC_DEFAULT_HAIKU_MODEL=${shellSingleQuote(config.haikuModel)}`,
|
|
60
|
+
`export ANTHROPIC_SMALL_FAST_MODEL=${shellSingleQuote(config.smallFastModel)}`,
|
|
61
|
+
`export ANTHROPIC_MODEL=${shellSingleQuote(config.defaultModel)}`
|
|
62
|
+
].join('\n') + '\n'
|
|
63
|
+
);
|
|
64
|
+
}
|
|
39
65
|
|
|
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
66
|
async function injectCustomEndpoint(
|
|
45
67
|
target: ContainerTarget,
|
|
46
68
|
config: NonNullable<ReturnType<typeof customEndpointConfig>>
|
|
47
69
|
): Promise<{ ok: boolean; error?: string }> {
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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 };
|
|
70
|
+
const env = await installShellEnvFile(target, ENV_FILE_NAME, envFileContent(config));
|
|
71
|
+
if (!env.ok) return env;
|
|
72
|
+
// `hasCompletedOnboarding` is what suppresses `claude`'s first-run wizard.
|
|
73
|
+
return editJsonFile(target, CLAUDE_JSON_FILE, (cur) =>
|
|
74
|
+
deepMerge(cur, { hasCompletedOnboarding: true })
|
|
75
|
+
);
|
|
89
76
|
}
|
|
90
77
|
|
|
91
78
|
/** Replaces `claude-code-credentials` in the registry when the custom-endpoint setting is on. */
|
|
@@ -120,6 +107,6 @@ export const claudeCodeCustom: Injection = {
|
|
|
120
107
|
},
|
|
121
108
|
|
|
122
109
|
async check(target) {
|
|
123
|
-
return
|
|
110
|
+
return containerFileExists(target, shellEnvFile(ENV_FILE_NAME));
|
|
124
111
|
}
|
|
125
112
|
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { checkPresence, execInContainer } from '../lib/exec.server.ts';
|
|
2
|
+
import type { Injection } from '../lib/injections.server.ts';
|
|
3
|
+
|
|
4
|
+
/** On Open VSX (code-server's default registry); the Marketplace build `/ide` targets is unreachable. */
|
|
5
|
+
export const EXTENSION_ID = 'anthropic.claude-code';
|
|
6
|
+
|
|
7
|
+
const EXT_GLOB = '~/.local/share/code-server/extensions/anthropic.claude-code-*';
|
|
8
|
+
|
|
9
|
+
/** Idempotent (skips if already present); best-effort — needs Open VSX egress. */
|
|
10
|
+
export const INSTALL_SCRIPT =
|
|
11
|
+
`if ls -d ${EXT_GLOB} >/dev/null 2>&1; then exit 0; fi; ` +
|
|
12
|
+
`command -v code-server >/dev/null 2>&1 || { echo "code-server not found" >&2; exit 1; }; ` +
|
|
13
|
+
`code-server --install-extension ${EXTENSION_ID}`;
|
|
14
|
+
|
|
15
|
+
export const CHECK_SCRIPT = `ls -d ${EXT_GLOB} >/dev/null 2>&1 && echo 1 || echo 0`;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Runtime fallback to the build-time launch-line install in CODE_SERVER_LAUNCH; failure is
|
|
19
|
+
* non-fatal since the IDE integration is optional and everything else works without it.
|
|
20
|
+
*/
|
|
21
|
+
export const claudeCodeIdeExtension: Injection = {
|
|
22
|
+
id: 'claude-code-ide-extension',
|
|
23
|
+
label: 'Claude Code IDE extension',
|
|
24
|
+
|
|
25
|
+
async apply(target, log) {
|
|
26
|
+
log('Installing Claude Code IDE extension…\n');
|
|
27
|
+
// Full target (with remoteUser) so it lands in that user's code-server home, where code-server reads it.
|
|
28
|
+
const res = await execInContainer(target, { script: INSTALL_SCRIPT });
|
|
29
|
+
log(
|
|
30
|
+
res.ok
|
|
31
|
+
? '✓ Claude Code IDE extension installed\n'
|
|
32
|
+
: `⚠ Claude Code IDE extension install failed: ${res.error} — /ide integration off\n`
|
|
33
|
+
);
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
async check(target) {
|
|
37
|
+
return checkPresence(target, CHECK_SCRIPT);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getOption } from '../lib/db.server.ts';
|
|
2
|
-
import {
|
|
2
|
+
import { installShellEnvFile, shellSingleQuote } from '../lib/container-files.server.ts';
|
|
3
3
|
import type { ContainerTarget, Injection } from '../lib/injections.server.ts';
|
|
4
4
|
|
|
5
5
|
/** The option key → Claude env var mapping; order is the field order in Settings. */
|
|
@@ -26,33 +26,17 @@ export function manualModelConfig(): Record<string, string> | null {
|
|
|
26
26
|
return Object.keys(filled).length ? filled : null;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
const
|
|
29
|
+
const ENV_FILE_NAME = '.codebay-claude-models-env';
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
async function injectModels(
|
|
31
|
+
function injectModels(
|
|
33
32
|
target: ContainerTarget,
|
|
34
33
|
config: Record<string, string>
|
|
35
34
|
): Promise<{ ok: boolean; error?: string }> {
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
'"); ' +
|
|
42
|
-
'{ ' +
|
|
43
|
-
writes +
|
|
44
|
-
'} > "$f"; chmod 600 "$f"; ' +
|
|
45
|
-
// The grep guard keeps a re-apply from stacking duplicate source lines.
|
|
46
|
-
'h=$(eval echo ~$(id -un)); src="[ -f \\"$f\\" ] && . \\"$f\\""; ' +
|
|
47
|
-
'for rc in "$h/.bashrc" "$h/.zshrc"; do ' +
|
|
48
|
-
'grep -qF "$src" "$rc" 2>/dev/null || printf \'%s\\n\' "$src" >> "$rc"; ' +
|
|
49
|
-
'done';
|
|
50
|
-
|
|
51
|
-
const res = await execInContainer(target, {
|
|
52
|
-
script,
|
|
53
|
-
args: ['claude-models', ...entries.map(([, value]) => value)]
|
|
54
|
-
});
|
|
55
|
-
return res.ok ? { ok: true } : { ok: false, error: res.error };
|
|
35
|
+
const content =
|
|
36
|
+
Object.entries(config)
|
|
37
|
+
.map(([env, value]) => `export ${env}=${shellSingleQuote(value)}`)
|
|
38
|
+
.join('\n') + '\n';
|
|
39
|
+
return installShellEnvFile(target, ENV_FILE_NAME, content);
|
|
56
40
|
}
|
|
57
41
|
|
|
58
42
|
/** Sets Claude model env vars on the standard path; skipped when LiteLLM is active. */
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { getOption } from '../lib/db.server.ts';
|
|
2
|
-
import {
|
|
3
|
-
import { mergeClaudeSettingsScript } from './attention-hooks.ts';
|
|
2
|
+
import { mergeClaudeSettings, readClaudeSettings } from '../lib/claude-settings.server.ts';
|
|
4
3
|
import { readHostClaudeSettings } from './claude-statusline.ts';
|
|
5
|
-
import type {
|
|
4
|
+
import type { Injection } from '../lib/injections.server.ts';
|
|
6
5
|
|
|
7
6
|
/** The app's own model config (manual override / LiteLLM) exports ANTHROPIC_MODEL, which wins over settings.json. */
|
|
8
7
|
function appOwnsModel(): boolean {
|
|
@@ -20,35 +19,16 @@ export async function hostClaudeModel(): Promise<string | null> {
|
|
|
20
19
|
return typeof model === 'string' && model.trim() ? model.trim() : null;
|
|
21
20
|
}
|
|
22
21
|
|
|
23
|
-
async function injectModel(
|
|
24
|
-
target: ContainerTarget,
|
|
25
|
-
model: string
|
|
26
|
-
): Promise<{ ok: boolean; error?: string }> {
|
|
27
|
-
const res = await execInContainer(target, {
|
|
28
|
-
script: mergeClaudeSettingsScript(),
|
|
29
|
-
stdin: JSON.stringify({ model })
|
|
30
|
-
});
|
|
31
|
-
return res.ok ? { ok: true } : { ok: false, error: res.error };
|
|
32
|
-
}
|
|
33
|
-
|
|
34
22
|
/** Mirrors the host's default `model` into the container so subscription instances match the host. */
|
|
35
23
|
export const claudeModel: Injection = {
|
|
36
24
|
id: 'claude-model',
|
|
37
25
|
label: 'model default',
|
|
38
26
|
|
|
39
|
-
auth: {
|
|
40
|
-
hint: 'set `model` in ~/.claude/settings.json',
|
|
41
|
-
async status() {
|
|
42
|
-
const model = await hostClaudeModel();
|
|
43
|
-
return { available: model !== null, source: model ? '~/.claude/settings.json' : null };
|
|
44
|
-
}
|
|
45
|
-
},
|
|
46
|
-
|
|
47
27
|
async apply(target, log) {
|
|
48
28
|
const model = await hostClaudeModel();
|
|
49
29
|
if (!model) return;
|
|
50
30
|
log(`Injecting Claude model default (${model})…\n`);
|
|
51
|
-
const result = await
|
|
31
|
+
const result = await mergeClaudeSettings(target, { model });
|
|
52
32
|
log(
|
|
53
33
|
result.ok
|
|
54
34
|
? '✓ Claude model default configured in container\n'
|
|
@@ -57,10 +37,7 @@ export const claudeModel: Injection = {
|
|
|
57
37
|
},
|
|
58
38
|
|
|
59
39
|
async check(target) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
'h=$(eval echo ~$(id -un)); d="${CLAUDE_CONFIG_DIR:-$h/.claude}"; ' +
|
|
63
|
-
'[ -s "$d/settings.json" ] && grep -q \'"model"\' "$d/settings.json" && echo 1 || echo 0'
|
|
64
|
-
);
|
|
40
|
+
const settings = await readClaudeSettings(target);
|
|
41
|
+
return typeof settings?.model === 'string' && settings.model.trim() !== '';
|
|
65
42
|
}
|
|
66
43
|
};
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import type { ContainerTarget, Injection } from '../lib/injections.server.ts';
|
|
1
|
+
import { mergeClaudeSettings, readClaudeSettings } from '../lib/claude-settings.server.ts';
|
|
2
|
+
import type { Injection } from '../lib/injections.server.ts';
|
|
4
3
|
|
|
5
4
|
/**
|
|
6
5
|
* Both schemas on purpose: newer `claude` reads the nested `attribution` object, older builds
|
|
@@ -12,14 +11,6 @@ export const NO_COAUTHOR_SETTINGS = {
|
|
|
12
11
|
attribution: { commit: '', pr: '', includeCoAuthoredBy: false }
|
|
13
12
|
};
|
|
14
13
|
|
|
15
|
-
async function injectNoCoauthor(target: ContainerTarget): Promise<{ ok: boolean; error?: string }> {
|
|
16
|
-
const res = await execInContainer(target, {
|
|
17
|
-
script: mergeClaudeSettingsScript(),
|
|
18
|
-
stdin: JSON.stringify(NO_COAUTHOR_SETTINGS)
|
|
19
|
-
});
|
|
20
|
-
return res.ok ? { ok: true } : { ok: false, error: res.error };
|
|
21
|
-
}
|
|
22
|
-
|
|
23
14
|
/** A Codebay-wide default rather than an opt-in setting, hence no `auth` block. */
|
|
24
15
|
export const claudeNoCoauthor: Injection = {
|
|
25
16
|
id: 'claude-no-coauthor',
|
|
@@ -27,7 +18,7 @@ export const claudeNoCoauthor: Injection = {
|
|
|
27
18
|
|
|
28
19
|
async apply(target, log) {
|
|
29
20
|
log('Disabling Claude co-author commit byline…\n');
|
|
30
|
-
const result = await
|
|
21
|
+
const result = await mergeClaudeSettings(target, NO_COAUTHOR_SETTINGS);
|
|
31
22
|
log(
|
|
32
23
|
result.ok
|
|
33
24
|
? '✓ Claude co-author byline disabled\n'
|
|
@@ -36,10 +27,7 @@ export const claudeNoCoauthor: Injection = {
|
|
|
36
27
|
},
|
|
37
28
|
|
|
38
29
|
async check(target) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
'h=$(eval echo ~$(id -un)); d="${CLAUDE_CONFIG_DIR:-$h/.claude}"; ' +
|
|
42
|
-
'[ -s "$d/settings.json" ] && grep -q includeCoAuthoredBy "$d/settings.json" && echo 1 || echo 0'
|
|
43
|
-
);
|
|
30
|
+
const settings = await readClaudeSettings(target);
|
|
31
|
+
return settings !== null && 'includeCoAuthoredBy' in settings;
|
|
44
32
|
}
|
|
45
33
|
};
|
|
@@ -1,22 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
appendLinesIfAbsent,
|
|
3
|
+
linesPresent,
|
|
4
|
+
SHELL_RC_FILES
|
|
5
|
+
} from '../lib/container-files.server.ts';
|
|
2
6
|
import type { Injection } from '../lib/injections.server.ts';
|
|
3
7
|
|
|
4
8
|
const ALIAS_LINE = "alias claude='claude --dangerously-skip-permissions'";
|
|
5
9
|
|
|
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
10
|
/** Safe here only because instances are throwaway, single-tenant sandboxes. */
|
|
21
11
|
export const claudeSkipPermissions: Injection = {
|
|
22
12
|
id: 'claude-skip-permissions',
|
|
@@ -24,7 +14,7 @@ export const claudeSkipPermissions: Injection = {
|
|
|
24
14
|
|
|
25
15
|
async apply(target, log) {
|
|
26
16
|
log('Installing claude --dangerously-skip-permissions alias…\n');
|
|
27
|
-
const res = await
|
|
17
|
+
const res = await appendLinesIfAbsent(target, SHELL_RC_FILES, [ALIAS_LINE]);
|
|
28
18
|
log(
|
|
29
19
|
res.ok
|
|
30
20
|
? '✓ claude skip-permissions alias installed\n'
|
|
@@ -33,6 +23,6 @@ export const claudeSkipPermissions: Injection = {
|
|
|
33
23
|
},
|
|
34
24
|
|
|
35
25
|
async check(target) {
|
|
36
|
-
return
|
|
26
|
+
return linesPresent(target, SHELL_RC_FILES, [ALIAS_LINE]);
|
|
37
27
|
}
|
|
38
28
|
};
|