codebay 0.9.1 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.mochi/manifest.json +214 -147
- package/.mochi/svelte-client/HydratableIsland-aebn1fa1.js +1 -0
- package/.mochi/svelte-client/_hydrate-AppShell_2rtnxn6pgp2rh-rx6arrv5.js +2 -0
- package/.mochi/svelte-client/{_hydrate-AvatarsView_257s9yq1wwr4f-92hepzby.js → _hydrate-AvatarsView_257s9yq1wwr4f-jpjdmgpe.js} +1 -1
- package/.mochi/svelte-client/_hydrate-InstanceView_2ma45l9bybfhl-c9y47dat.js +1 -0
- package/.mochi/svelte-client/_hydrate-SettingsView_8hmarwmf8onq-d61feq89.js +38 -0
- package/.mochi/svelte-client/{chunk-2j95sv83.js → chunk-0fjx3z3r.js} +1 -1
- package/.mochi/svelte-client/chunk-8xvr02wh.js +2 -0
- package/.mochi/svelte-client/{chunk-m497q88s.js → chunk-9143wy3j.js} +2 -2
- package/.mochi/svelte-client/{chunk-v65n8th6.js → chunk-bn731rda.js} +1 -1
- package/.mochi/svelte-client/{chunk-a2psjmrm.js → chunk-j72r8pkk.js} +2 -2
- package/.mochi/svelte-client/chunk-k37wj1jm.js +2 -0
- package/.mochi/svelte-client/{chunk-mpv3qx28.js → chunk-p20tkpde.js} +1 -1
- package/.mochi/svelte-client/chunk-pc0fm37c.js +2 -0
- package/.mochi/svelte-client/{chunk-6rbs0mhx.js → chunk-t0ap7q89.js} +1 -1
- package/.mochi/svelte-client/{chunk-rs41by6w.js → chunk-wvtkwfsh.js} +1 -1
- package/.mochi/svelte-compile/{App-7vjcp38v.server.js → App-59cdj86f.server.js} +167 -103
- package/.mochi/svelte-compile/{Avatars-e2cfc95t.server.js → Avatars-fmagfyj2.server.js} +1 -1
- package/.mochi/svelte-compile/{Instance-h78n2dsv.server.js → Instance-s1gqwzmx.server.js} +16 -10
- package/.mochi/svelte-compile/{Settings-tav2asn7.server.js → Settings-k447mt0x.server.js} +540 -88
- package/.mochi/svelte-compile/{chunk-brjv9pyd.js → chunk-3t7v4hk0.js} +1 -7
- package/.mochi/svelte-compile/chunk-gkjgybb2.js +15 -0
- package/.mochi/svelte-compile/{chunk-m8ygpyhr.js → chunk-th2dbqvc.js} +20 -32
- package/.mochi/svelte-css/Button-oharop2xz6am.css +1 -0
- package/.mochi/svelte-css/IdeBar-3bgr3m5z0lpeg.css +1 -0
- package/.mochi/svelte-css/{InstanceView-3vzow3ttxzlh3.css → InstanceView-1pd7j16bd2b16.css} +1 -1
- package/.mochi/svelte-css/SettingsView-n3oy75uonzp2.css +1 -0
- package/.mochi/svelte-css/{TopBar-2q87li6yt8ids.css → TopBar-2y76zxwehrfos.css} +1 -1
- package/migrations/0007_instance_avatar.sql +1 -0
- package/package.json +2 -2
- package/src/avatars/diamond.ts +5 -6
- package/src/avatars/index.ts +1 -16
- package/src/avatars/pick.server.ts +25 -0
- package/src/avatars/whale.ts +7 -8
- package/src/components/AppShell.svelte +29 -4
- package/src/components/Avatar.svelte +8 -8
- package/src/components/Button.svelte +22 -1
- package/src/components/DashboardView.svelte +14 -2
- package/src/components/FolderBrowser.svelte +6 -2
- package/src/components/IdeBar.svelte +56 -13
- package/src/components/IdeLoader.svelte +15 -3
- package/src/components/InstanceCard.svelte +6 -1
- package/src/components/InstanceView.svelte +33 -15
- package/src/components/SettingsView.svelte +548 -1
- package/src/components/TopBar.svelte +37 -10
- package/src/components/UiShowcase.svelte +5 -10
- package/src/container-injections/claude-aliases.ts +1 -1
- package/src/container-injections/claude-code-custom.ts +2 -1
- package/src/container-injections/claude-code-ide-extension.ts +5 -0
- package/src/container-injections/claude-code-models.ts +2 -1
- package/src/container-injections/claude-code-update.ts +62 -1
- package/src/container-injections/claude-effort-level.ts +32 -0
- package/src/container-injections/claude-permission-mode.ts +40 -0
- package/src/container-injections/custom-env-vars.ts +82 -0
- package/src/container-injections/host-env-vars.ts +2 -1
- package/src/lib/config.server.ts +3 -0
- package/src/lib/db.server.ts +8 -3
- package/src/lib/devcontainer.server.ts +155 -43
- package/src/lib/health.server.ts +55 -11
- package/src/lib/injections.server.ts +45 -38
- package/src/lib/instances.server.ts +246 -26
- package/src/lib/log-capture.server.ts +262 -0
- package/src/lib/ports.server.ts +31 -0
- package/src/lib/proxy.server.ts +2 -2
- package/src/pages/Settings.svelte +22 -1
- package/src/routes.ts +98 -2
- package/src/types.ts +36 -0
- package/.mochi/svelte-client/HydratableIsland-k6q8esj4.js +0 -1
- package/.mochi/svelte-client/_hydrate-AppShell_2rtnxn6pgp2rh-sg96rp17.js +0 -2
- package/.mochi/svelte-client/_hydrate-InstanceView_2ma45l9bybfhl-p3f649gk.js +0 -1
- package/.mochi/svelte-client/_hydrate-SettingsView_8hmarwmf8onq-dzmgmv78.js +0 -29
- package/.mochi/svelte-client/chunk-7wkvn2vx.js +0 -2
- package/.mochi/svelte-client/chunk-m4fzsd5b.js +0 -2
- package/.mochi/svelte-css/Button-2vtbrzb5kgu25.css +0 -1
- package/.mochi/svelte-css/IdeBar-10xodudcx57qr.css +0 -1
- package/.mochi/svelte-css/SettingsView-2qfauz68axro1.css +0 -1
- package/src/container-injections/claude-skip-permissions.ts +0 -28
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import Plus from '@lucide/svelte/icons/plus';
|
|
3
3
|
import Component from '@lucide/svelte/icons/component';
|
|
4
|
+
import Terminal from '@lucide/svelte/icons/terminal';
|
|
5
|
+
import LayoutTemplate from '@lucide/svelte/icons/layout-template';
|
|
4
6
|
import { isDev } from 'mochi-framework';
|
|
5
|
-
import type { AuthProvider, InstanceFilter } from '../types.ts';
|
|
7
|
+
import type { AuthProvider, InstanceFilter, InstanceMode } from '../types.ts';
|
|
6
8
|
import type { AvatarArt } from '../avatars/index.ts';
|
|
7
9
|
import Brand from './Brand.svelte';
|
|
8
10
|
import InstanceFilterControl from './InstanceFilter.svelte';
|
|
@@ -18,6 +20,7 @@
|
|
|
18
20
|
ready = true,
|
|
19
21
|
creating = false,
|
|
20
22
|
filter,
|
|
23
|
+
defaultMode = 'ide',
|
|
21
24
|
onFilter,
|
|
22
25
|
onNew,
|
|
23
26
|
onDeleteAll
|
|
@@ -28,10 +31,14 @@
|
|
|
28
31
|
ready?: boolean;
|
|
29
32
|
creating?: boolean;
|
|
30
33
|
filter?: InstanceFilter;
|
|
34
|
+
/** Drives the shortcut button, which always offers the mode the main button won't create. */
|
|
35
|
+
defaultMode?: InstanceMode;
|
|
31
36
|
onFilter?: (v: InstanceFilter) => void;
|
|
32
|
-
onNew?: () => void;
|
|
37
|
+
onNew?: (mode?: InstanceMode) => void;
|
|
33
38
|
onDeleteAll?: () => void;
|
|
34
39
|
} = $props();
|
|
40
|
+
|
|
41
|
+
const altMode = $derived<InstanceMode>(defaultMode === 'terminal' ? 'ide' : 'terminal');
|
|
35
42
|
</script>
|
|
36
43
|
|
|
37
44
|
<header class="topbar">
|
|
@@ -48,14 +55,28 @@
|
|
|
48
55
|
<Button variant="danger" size="sm" onclick={onDeleteAll} disabled={!canDelete}>
|
|
49
56
|
Delete all
|
|
50
57
|
</Button>
|
|
51
|
-
<
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
58
|
+
<div class="new-group">
|
|
59
|
+
<Button
|
|
60
|
+
variant="primary"
|
|
61
|
+
size="sm"
|
|
62
|
+
icon={creating ? undefined : Plus}
|
|
63
|
+
onclick={() => onNew?.()}
|
|
64
|
+
disabled={!ready || creating}
|
|
65
|
+
>
|
|
66
|
+
{creating ? 'Creating…' : 'New instance'}
|
|
67
|
+
</Button>
|
|
68
|
+
<Button
|
|
69
|
+
variant="primary"
|
|
70
|
+
size="sm"
|
|
71
|
+
square
|
|
72
|
+
icon={altMode === 'terminal' ? Terminal : LayoutTemplate}
|
|
73
|
+
iconSize={15}
|
|
74
|
+
onclick={() => onNew?.(altMode)}
|
|
75
|
+
disabled={!ready || creating}
|
|
76
|
+
title={altMode === 'terminal' ? 'New terminal instance' : 'New full IDE instance'}
|
|
77
|
+
aria-label={altMode === 'terminal' ? 'New terminal instance' : 'New full IDE instance'}
|
|
78
|
+
/>
|
|
79
|
+
</div>
|
|
59
80
|
</div>
|
|
60
81
|
</header>
|
|
61
82
|
|
|
@@ -72,4 +93,10 @@
|
|
|
72
93
|
align-items: center;
|
|
73
94
|
gap: 14px;
|
|
74
95
|
}
|
|
96
|
+
/* The mode shortcut reads as part of the New instance control, not a separate action. */
|
|
97
|
+
.new-group {
|
|
98
|
+
display: inline-flex;
|
|
99
|
+
align-items: center;
|
|
100
|
+
gap: 6px;
|
|
101
|
+
}
|
|
75
102
|
</style>
|
|
@@ -71,9 +71,8 @@
|
|
|
71
71
|
|
|
72
72
|
let avatarScale = $state(8);
|
|
73
73
|
let avatarName = $state('demo-instance');
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
let avatarArtName = $state('');
|
|
74
|
+
// '' renders an empty panel; otherwise the value is an avatar name from the catalog.
|
|
75
|
+
let avatarArtName = $state(avatars[0]!.name);
|
|
77
76
|
const avatarArt = $derived(avatars.find((a) => a.name === avatarArtName));
|
|
78
77
|
|
|
79
78
|
let btnVariant = $state<'default' | 'primary' | 'danger'>('primary');
|
|
@@ -149,7 +148,7 @@
|
|
|
149
148
|
canDelete={true}
|
|
150
149
|
ready={true}
|
|
151
150
|
creating={false}
|
|
152
|
-
onNew={() => toast(
|
|
151
|
+
onNew={(mode) => toast(`New ${mode ?? 'default'} instance (demo)`)}
|
|
153
152
|
onDeleteAll={() => toast('Delete all (demo)')}
|
|
154
153
|
/>
|
|
155
154
|
|
|
@@ -187,7 +186,7 @@
|
|
|
187
186
|
</details>
|
|
188
187
|
|
|
189
188
|
<ComponentDemo title="Avatar">
|
|
190
|
-
<Avatar
|
|
189
|
+
<Avatar name={avatarName} scale={avatarScale} art={avatarArt} />
|
|
191
190
|
{#snippet controls()}
|
|
192
191
|
<label>
|
|
193
192
|
<span>scale ({avatarScale})</span>
|
|
@@ -197,14 +196,10 @@
|
|
|
197
196
|
<span>name</span>
|
|
198
197
|
<input type="text" bind:value={avatarName} />
|
|
199
198
|
</label>
|
|
200
|
-
<label>
|
|
201
|
-
<span>id</span>
|
|
202
|
-
<input type="text" bind:value={avatarId} />
|
|
203
|
-
</label>
|
|
204
199
|
<label>
|
|
205
200
|
<span>art</span>
|
|
206
201
|
<select bind:value={avatarArtName}>
|
|
207
|
-
<option value="">(
|
|
202
|
+
<option value="">(none)</option>
|
|
208
203
|
{#each avatars as art (art.name)}
|
|
209
204
|
<option value={art.name}>{art.name}</option>
|
|
210
205
|
{/each}
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
} from '../lib/container-files.server.ts';
|
|
6
6
|
import type { Injection } from '../lib/injections.server.ts';
|
|
7
7
|
|
|
8
|
-
/** Both resolve `claude`, which `claude-
|
|
8
|
+
/** Both resolve `claude`, which `claude-permission-mode` also aliases, so the two compose. */
|
|
9
9
|
const ALIAS_LINES = [
|
|
10
10
|
"alias c200='CLAUDE_CODE_DISABLE_1M_CONTEXT=1 claude'",
|
|
11
11
|
"alias cs='claude --model sonnet'"
|
|
@@ -43,7 +43,8 @@ export function customEndpointConfig(): {
|
|
|
43
43
|
};
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
/** Exported so the terminal/IDE launchers can source it explicitly — see `SOURCE_INJECTED_ENV`. */
|
|
47
|
+
export const ENV_FILE_NAME = '.codebay-claude-env';
|
|
47
48
|
|
|
48
49
|
/** The whole file (token included) rides `stdin` inside `installShellEnvFile`, so nothing hits argv. */
|
|
49
50
|
function envFileContent(config: NonNullable<ReturnType<typeof customEndpointConfig>>): string {
|
|
@@ -7,9 +7,14 @@ export const EXTENSION_ID = 'anthropic.claude-code';
|
|
|
7
7
|
const EXT_GLOB = '~/.local/share/code-server/extensions/anthropic.claude-code-*';
|
|
8
8
|
|
|
9
9
|
/** Idempotent (skips if already present); best-effort — needs Open VSX egress. */
|
|
10
|
+
// The bracketed `install-extensio[n]` keeps pgrep from matching this script's own `bash -lc` argv,
|
|
11
|
+
// so it only sees the launch line's real background install (which it waits out before falling back).
|
|
10
12
|
export const INSTALL_SCRIPT =
|
|
11
13
|
`if ls -d ${EXT_GLOB} >/dev/null 2>&1; then exit 0; fi; ` +
|
|
12
14
|
`command -v code-server >/dev/null 2>&1 || { echo "code-server not found" >&2; exit 1; }; ` +
|
|
15
|
+
`for _ in {1..90}; do ` +
|
|
16
|
+
`pgrep -f 'install-extensio[n] ${EXTENSION_ID}' >/dev/null 2>&1 || break; sleep 1; done; ` +
|
|
17
|
+
`if ls -d ${EXT_GLOB} >/dev/null 2>&1; then exit 0; fi; ` +
|
|
13
18
|
`code-server --install-extension ${EXTENSION_ID}`;
|
|
14
19
|
|
|
15
20
|
export const CHECK_SCRIPT = `ls -d ${EXT_GLOB} >/dev/null 2>&1 && echo 1 || echo 0`;
|
|
@@ -26,7 +26,8 @@ export function manualModelConfig(): Record<string, string> | null {
|
|
|
26
26
|
return Object.keys(filled).length ? filled : null;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
/** Exported so the terminal/IDE launchers can source it explicitly — see `SOURCE_INJECTED_ENV`. */
|
|
30
|
+
export const ENV_FILE_NAME = '.codebay-claude-models-env';
|
|
30
31
|
|
|
31
32
|
function injectModels(
|
|
32
33
|
target: ContainerTarget,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { execInContainer } from '../lib/exec.server.ts';
|
|
2
|
+
import { getOption, setOption } from '../lib/db.server.ts';
|
|
2
3
|
import type { Injection } from '../lib/injections.server.ts';
|
|
3
4
|
|
|
4
5
|
/**
|
|
@@ -18,6 +19,63 @@ export const UPDATE_SCRIPT =
|
|
|
18
19
|
'npm install -g @anthropic-ai/claude-code@latest >/dev/null 2>&1 || exit 1; ' +
|
|
19
20
|
'echo "updated $installed -> $latest"';
|
|
20
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Same check with the latest version supplied by the host as `$0` (exec args, never interpolated
|
|
24
|
+
* into the script), skipping the in-container `npm view` registry round-trip.
|
|
25
|
+
*/
|
|
26
|
+
export const PINNED_UPDATE_SCRIPT =
|
|
27
|
+
'latest="$0"; ' +
|
|
28
|
+
'command -v claude >/dev/null 2>&1 || exit 0; ' +
|
|
29
|
+
'command -v npm >/dev/null 2>&1 || exit 0; ' +
|
|
30
|
+
"installed=$(claude --version 2>/dev/null | grep -oE '[0-9]+\\.[0-9]+\\.[0-9]+' | head -n1); " +
|
|
31
|
+
'if [ "$installed" = "$latest" ]; then echo "current $latest"; exit 0; fi; ' +
|
|
32
|
+
'npm install -g @anthropic-ai/claude-code@latest >/dev/null 2>&1 || exit 1; ' +
|
|
33
|
+
'echo "updated $installed -> $latest"';
|
|
34
|
+
|
|
35
|
+
/** Anything looser could smuggle shell metacharacters out of a corrupted DB row into a container. */
|
|
36
|
+
export const VERSION_RE = /^\d+\.\d+\.\d+$/;
|
|
37
|
+
|
|
38
|
+
export const CACHE_TTL_MS = 60 * 60 * 1000;
|
|
39
|
+
|
|
40
|
+
export const LATEST_VERSION_KEY = 'claude_code_latest_version';
|
|
41
|
+
export const LATEST_CHECKED_AT_KEY = 'claude_code_latest_checked_at';
|
|
42
|
+
|
|
43
|
+
export function cacheIsFresh(checkedAt: string | null, now: number, ttlMs = CACHE_TTL_MS): boolean {
|
|
44
|
+
const t = Number(checkedAt);
|
|
45
|
+
return Number.isFinite(t) && t > 0 && now - t < ttlMs;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Best-effort: any failure (offline, timeout, odd payload) returns null and the caller falls back. */
|
|
49
|
+
export async function fetchLatestVersion(): Promise<string | null> {
|
|
50
|
+
try {
|
|
51
|
+
const res = await fetch('https://registry.npmjs.org/@anthropic-ai/claude-code/latest', {
|
|
52
|
+
signal: AbortSignal.timeout(3000)
|
|
53
|
+
});
|
|
54
|
+
if (!res.ok) return null;
|
|
55
|
+
const version = ((await res.json()) as { version?: unknown }).version;
|
|
56
|
+
return typeof version === 'string' && VERSION_RE.test(version) ? version : null;
|
|
57
|
+
} catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Fresh cached version, a newly fetched one (cached for next time), or null → use UPDATE_SCRIPT. */
|
|
63
|
+
async function resolveLatestVersion(): Promise<string | null> {
|
|
64
|
+
const cached = getOption(LATEST_VERSION_KEY);
|
|
65
|
+
if (
|
|
66
|
+
cached &&
|
|
67
|
+
VERSION_RE.test(cached) &&
|
|
68
|
+
cacheIsFresh(getOption(LATEST_CHECKED_AT_KEY), Date.now())
|
|
69
|
+
) {
|
|
70
|
+
return cached;
|
|
71
|
+
}
|
|
72
|
+
const version = await fetchLatestVersion();
|
|
73
|
+
if (!version) return null;
|
|
74
|
+
setOption(LATEST_VERSION_KEY, version);
|
|
75
|
+
setOption(LATEST_CHECKED_AT_KEY, String(Date.now()));
|
|
76
|
+
return version;
|
|
77
|
+
}
|
|
78
|
+
|
|
21
79
|
/** Best-effort, non-fatal — a failed update leaves the cached version in place. */
|
|
22
80
|
export const claudeCodeUpdate: Injection = {
|
|
23
81
|
id: 'claude-code-update',
|
|
@@ -25,10 +83,13 @@ export const claudeCodeUpdate: Injection = {
|
|
|
25
83
|
|
|
26
84
|
async apply(target, log) {
|
|
27
85
|
log('Checking Claude Code is up to date…\n');
|
|
86
|
+
const latest = await resolveLatestVersion();
|
|
28
87
|
// Omitting remoteUser runs as root, which the global npm reinstall needs.
|
|
29
88
|
const res = await execInContainer(
|
|
30
89
|
{ containerId: target.containerId },
|
|
31
|
-
|
|
90
|
+
latest
|
|
91
|
+
? { script: PINNED_UPDATE_SCRIPT, args: [latest], capture: true }
|
|
92
|
+
: { script: UPDATE_SCRIPT, capture: true }
|
|
32
93
|
);
|
|
33
94
|
if (!res.ok) {
|
|
34
95
|
log(`⚠ Claude Code update check failed: ${res.error} — keeping the installed version\n`);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { getOption } from '../lib/db.server.ts';
|
|
2
|
+
import { mergeClaudeSettings, readClaudeSettings } from '../lib/claude-settings.server.ts';
|
|
3
|
+
import { normalizeEffortLevel } from '../types.ts';
|
|
4
|
+
import type { ClaudeEffortLevel } from '../types.ts';
|
|
5
|
+
import type { Injection } from '../lib/injections.server.ts';
|
|
6
|
+
|
|
7
|
+
/** Also read by the settings-page `serverProps` to seed the UI. */
|
|
8
|
+
export function getClaudeEffortLevel(): ClaudeEffortLevel {
|
|
9
|
+
return normalizeEffortLevel(getOption('claude_effort_level'));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Writes Claude Code's `effortLevel` default into the container so new sessions start at the chosen effort. */
|
|
13
|
+
export const claudeEffortLevel: Injection = {
|
|
14
|
+
id: 'claude-effort-level',
|
|
15
|
+
label: 'effort level',
|
|
16
|
+
|
|
17
|
+
async apply(target, log) {
|
|
18
|
+
const level = getClaudeEffortLevel();
|
|
19
|
+
log(`Setting Claude effort level (${level})…\n`);
|
|
20
|
+
const result = await mergeClaudeSettings(target, { effortLevel: level });
|
|
21
|
+
log(
|
|
22
|
+
result.ok
|
|
23
|
+
? '✓ Claude effort level configured in container\n'
|
|
24
|
+
: `⚠ Claude effort level injection failed: ${result.error}\n`
|
|
25
|
+
);
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
async check(target) {
|
|
29
|
+
const settings = await readClaudeSettings(target);
|
|
30
|
+
return settings?.effortLevel === getClaudeEffortLevel();
|
|
31
|
+
}
|
|
32
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import {
|
|
2
|
+
appendLinesIfAbsent,
|
|
3
|
+
linesPresent,
|
|
4
|
+
SHELL_RC_FILES
|
|
5
|
+
} from '../lib/container-files.server.ts';
|
|
6
|
+
import { getOption } from '../lib/db.server.ts';
|
|
7
|
+
import { claudePermissionFlags, normalizePermissionMode } from '../types.ts';
|
|
8
|
+
import type { ClaudePermissionMode } from '../types.ts';
|
|
9
|
+
import type { Injection } from '../lib/injections.server.ts';
|
|
10
|
+
|
|
11
|
+
/** Also read by `provision()`, which bakes the same flags into the container's launcher. */
|
|
12
|
+
export function getClaudePermissionMode(): ClaudePermissionMode {
|
|
13
|
+
return normalizePermissionMode(getOption('claude_permission_mode'));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const aliasLine = () => `alias claude='claude ${claudePermissionFlags(getClaudePermissionMode())}'`;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Keeps a hand-typed `claude` on the same permission mode the auto-launched session gets.
|
|
20
|
+
* The `default` mode's bypass is safe here only because instances are throwaway sandboxes.
|
|
21
|
+
*/
|
|
22
|
+
export const claudePermissionMode: Injection = {
|
|
23
|
+
id: 'claude-permission-mode',
|
|
24
|
+
label: 'claude permission mode',
|
|
25
|
+
|
|
26
|
+
async apply(target, log) {
|
|
27
|
+
const line = aliasLine();
|
|
28
|
+
log(`Installing ${line}…\n`);
|
|
29
|
+
const res = await appendLinesIfAbsent(target, SHELL_RC_FILES, [line]);
|
|
30
|
+
log(
|
|
31
|
+
res.ok
|
|
32
|
+
? '✓ claude permission-mode alias installed\n'
|
|
33
|
+
: `⚠ claude alias setup failed: ${res.error}\n`
|
|
34
|
+
);
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
async check(target) {
|
|
38
|
+
return linesPresent(target, SHELL_RC_FILES, [aliasLine()]);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { getOption } from '../lib/db.server.ts';
|
|
2
|
+
import { checkPresence } from '../lib/exec.server.ts';
|
|
3
|
+
import type { Injection } from '../lib/injections.server.ts';
|
|
4
|
+
|
|
5
|
+
export interface CustomEnvVar {
|
|
6
|
+
name: string;
|
|
7
|
+
value: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Same rule the host-env-vars name list and the settings action validate against. */
|
|
11
|
+
const NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
12
|
+
|
|
13
|
+
/** Exported so `/settings` and the log-redaction step parse the stored list the same way. */
|
|
14
|
+
export function parseCustomEnvVars(raw: string | null): CustomEnvVar[] {
|
|
15
|
+
let parsed: unknown;
|
|
16
|
+
try {
|
|
17
|
+
parsed = JSON.parse(raw ?? '[]');
|
|
18
|
+
} catch {
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
if (!Array.isArray(parsed)) return [];
|
|
22
|
+
const out: CustomEnvVar[] = [];
|
|
23
|
+
for (const entry of parsed) {
|
|
24
|
+
const name = (entry as CustomEnvVar)?.name;
|
|
25
|
+
const value = (entry as CustomEnvVar)?.value;
|
|
26
|
+
if (typeof name === 'string' && typeof value === 'string' && NAME_RE.test(name)) {
|
|
27
|
+
out.push({ name, value });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Null means "skip" — neither `containerEnv` nor the injection render anything. */
|
|
34
|
+
export function customEnvVarsConfig(): { vars: CustomEnvVar[] } | null {
|
|
35
|
+
if (getOption('custom_env_vars_enabled') !== '1') return null;
|
|
36
|
+
const vars = parseCustomEnvVars(getOption('custom_env_vars')).filter((v) => v.value !== '');
|
|
37
|
+
return vars.length ? { vars } : null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The secret values eligible for log redaction — only while the feature is enabled. */
|
|
41
|
+
export function customEnvVarValues(): string[] {
|
|
42
|
+
return customEnvVarsConfig()?.vars.map((v) => v.value) ?? [];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Names ride argv (never a value), and `printenv` output is discarded so no value is ever echoed. */
|
|
46
|
+
const PRESENCE_SCRIPT =
|
|
47
|
+
'for n in "$@"; do printenv "$n" >/dev/null 2>&1 || { echo 0; exit 0; }; done; echo 1';
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The values themselves are injected via devcontainer.json `containerEnv` at container-create time
|
|
51
|
+
* (a post-up injection can't set container-wide env); this module verifies they landed and draws
|
|
52
|
+
* the health row. No `auth` block: values come from Settings, not a discovered host dependency.
|
|
53
|
+
*/
|
|
54
|
+
export const customEnvVars: Injection = {
|
|
55
|
+
id: 'custom-env-vars',
|
|
56
|
+
label: 'custom env vars',
|
|
57
|
+
|
|
58
|
+
async apply(target, log) {
|
|
59
|
+
const config = customEnvVarsConfig();
|
|
60
|
+
if (!config) {
|
|
61
|
+
log('⚠ Custom env vars not configured (disabled or none set); skipped\n');
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const names = config.vars.map((v) => v.name);
|
|
65
|
+
log(`Verifying ${names.length} custom env var(s) via containerEnv: ${names.join(', ')}…\n`);
|
|
66
|
+
const present = await checkPresence(target, PRESENCE_SCRIPT, ['custom-env-vars', ...names]);
|
|
67
|
+
log(
|
|
68
|
+
present
|
|
69
|
+
? '✓ custom env vars present in the container\n'
|
|
70
|
+
: '⚠ custom env vars not visible (containerEnv may not apply to this base image)\n'
|
|
71
|
+
);
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
async check(target) {
|
|
75
|
+
const config = customEnvVarsConfig();
|
|
76
|
+
if (!config) return false;
|
|
77
|
+
return checkPresence(target, PRESENCE_SCRIPT, [
|
|
78
|
+
'custom-env-vars',
|
|
79
|
+
...config.vars.map((v) => v.name)
|
|
80
|
+
]);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
@@ -7,7 +7,8 @@ import {
|
|
|
7
7
|
} from '../lib/container-files.server.ts';
|
|
8
8
|
import type { ContainerTarget, Injection } from '../lib/injections.server.ts';
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
/** Exported so the terminal/IDE launchers can source it explicitly — see `SOURCE_INJECTED_ENV`. */
|
|
11
|
+
export const ENV_FILE_NAME = '.codebay-host-env';
|
|
11
12
|
|
|
12
13
|
interface ResolvedVar {
|
|
13
14
|
name: string;
|
package/src/lib/config.server.ts
CHANGED
|
@@ -51,6 +51,9 @@ export const APP_VERSION: string = (() => {
|
|
|
51
51
|
/** Per-instance working copies live here: <INSTANCES_DIR>/<id>/workspace. */
|
|
52
52
|
export const INSTANCES_DIR = join(DATA_DIR, 'instances');
|
|
53
53
|
|
|
54
|
+
/** Extracted Claude Code logs, in one flat folder that outlives the instances they came from. */
|
|
55
|
+
export const LOGS_DIR = join(DATA_DIR, 'logs');
|
|
56
|
+
|
|
54
57
|
export const DB_PATH = join(DATA_DIR, 'app.sqlite');
|
|
55
58
|
|
|
56
59
|
export const PORT_BASE = 8001;
|
package/src/lib/db.server.ts
CHANGED
|
@@ -30,6 +30,8 @@ export interface InstanceRow {
|
|
|
30
30
|
remote_user: string | null;
|
|
31
31
|
/** `'local'` when the folder shipped its own config, else the injected image; null pre-dates this. */
|
|
32
32
|
image_source: string | null;
|
|
33
|
+
/** Sprite name chosen collision-free at creation; null pre-dates this and falls back to the id hash. */
|
|
34
|
+
avatar: string | null;
|
|
33
35
|
/** Fixed at creation: which editor surface this instance provisions and serves. */
|
|
34
36
|
mode: InstanceMode;
|
|
35
37
|
/** Terminal mode only: 1 when the scratch-shell pane was left open, so a reload restores it. */
|
|
@@ -64,8 +66,8 @@ export function closeDb(): void {
|
|
|
64
66
|
export function insertInstance(row: InstanceRow): void {
|
|
65
67
|
db.query(
|
|
66
68
|
`INSERT INTO instances
|
|
67
|
-
(id, name, source_path, workspace_path, host_port, container_id, remote_workspace_folder, status, error, created_at, bridge_token, remote_user, image_source, mode, terminal_split)
|
|
68
|
-
VALUES ($id, $name, $source_path, $workspace_path, $host_port, $container_id, $remote_workspace_folder, $status, $error, $created_at, $bridge_token, $remote_user, $image_source, $mode, $terminal_split)`
|
|
69
|
+
(id, name, source_path, workspace_path, host_port, container_id, remote_workspace_folder, status, error, created_at, bridge_token, remote_user, image_source, avatar, mode, terminal_split)
|
|
70
|
+
VALUES ($id, $name, $source_path, $workspace_path, $host_port, $container_id, $remote_workspace_folder, $status, $error, $created_at, $bridge_token, $remote_user, $image_source, $avatar, $mode, $terminal_split)`
|
|
69
71
|
).run({
|
|
70
72
|
$id: row.id,
|
|
71
73
|
$name: row.name,
|
|
@@ -80,6 +82,7 @@ export function insertInstance(row: InstanceRow): void {
|
|
|
80
82
|
$bridge_token: row.bridge_token,
|
|
81
83
|
$remote_user: row.remote_user,
|
|
82
84
|
$image_source: row.image_source,
|
|
85
|
+
$avatar: row.avatar,
|
|
83
86
|
$mode: row.mode,
|
|
84
87
|
$terminal_split: row.terminal_split
|
|
85
88
|
});
|
|
@@ -143,7 +146,9 @@ const UPDATABLE_COLUMNS = [
|
|
|
143
146
|
'error',
|
|
144
147
|
'remote_user',
|
|
145
148
|
'image_source',
|
|
146
|
-
'terminal_split'
|
|
149
|
+
'terminal_split',
|
|
150
|
+
// Reassigned when a rebuild finds the recorded port taken over on the host.
|
|
151
|
+
'host_port'
|
|
147
152
|
] as const;
|
|
148
153
|
|
|
149
154
|
type UpdatableColumn = (typeof UPDATABLE_COLUMNS)[number];
|