codebay 0.13.0 → 0.14.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 +41 -33
- package/.mochi/svelte-client/_hydrate-SettingsView_8hmarwmf8onq-4a7ann1f.js +40 -0
- package/.mochi/svelte-compile/{Settings-h7gsjyz3.server.js → Settings-a7b11v90.server.js} +211 -134
- package/package.json +1 -1
- package/public/favicon.svg +9 -0
- package/src/components/SettingsView.svelte +74 -0
- package/src/container-injections/claude-code-credentials.ts +2 -3
- package/src/container-injections/claude-code-skills.ts +125 -0
- package/src/container-injections/claude-output-style.ts +2 -3
- package/src/container-injections/claude-statusline.ts +5 -16
- package/src/lib/container-files.server.ts +22 -0
- package/src/lib/host-claude.server.ts +31 -0
- package/src/lib/injections.server.ts +2 -0
- package/src/pages/Settings.svelte +3 -0
- package/src/routes.ts +9 -0
- package/src/shell.html +1 -0
- package/.mochi/svelte-client/_hydrate-SettingsView_8hmarwmf8onq-wedc9x9y.js +0 -39
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codebay",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Web UI for spinning up isolated devcontainer instances from any local folder, each with a browser-based VS Code (code-server) — built for parallel Claude Code workflows.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
|
2
|
+
<rect x="0" y="0" width="24" height="24" fill="#14150f" />
|
|
3
|
+
<g fill="none" stroke="#d7d8d2" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
4
|
+
<path d="M11 21.73a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73z" />
|
|
5
|
+
<path d="M12 22V12" />
|
|
6
|
+
<polyline points="3.29 7 12 12 20.71 7" />
|
|
7
|
+
<path d="m7.5 4.27 9 5.15" />
|
|
8
|
+
</g>
|
|
9
|
+
</svg>
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import ThemePicker from './ThemePicker.svelte';
|
|
11
11
|
import Layers from '@lucide/svelte/icons/layers';
|
|
12
12
|
import FolderMinus from '@lucide/svelte/icons/folder-minus';
|
|
13
|
+
import FolderCog from '@lucide/svelte/icons/folder-cog';
|
|
13
14
|
import Trash2 from '@lucide/svelte/icons/trash-2';
|
|
14
15
|
import Hammer from '@lucide/svelte/icons/hammer';
|
|
15
16
|
import KeyRound from '@lucide/svelte/icons/key-round';
|
|
@@ -69,6 +70,7 @@
|
|
|
69
70
|
disableBuildCache,
|
|
70
71
|
copyIgnorePatterns,
|
|
71
72
|
builtinCopyIgnore,
|
|
73
|
+
claudeConfigDir,
|
|
72
74
|
gitIdentityEnabled,
|
|
73
75
|
gitIdentityName,
|
|
74
76
|
gitIdentityEmail,
|
|
@@ -110,6 +112,7 @@
|
|
|
110
112
|
disableBuildCache: boolean;
|
|
111
113
|
copyIgnorePatterns: string;
|
|
112
114
|
builtinCopyIgnore: string;
|
|
115
|
+
claudeConfigDir: string;
|
|
113
116
|
gitIdentityEnabled: boolean;
|
|
114
117
|
gitIdentityName: string;
|
|
115
118
|
gitIdentityEmail: string;
|
|
@@ -372,6 +375,29 @@
|
|
|
372
375
|
copyIgnoreFormEl?.requestSubmit();
|
|
373
376
|
}
|
|
374
377
|
|
|
378
|
+
// Blank means "use ~/.claude" — the host dir the Claude config injections read from.
|
|
379
|
+
// svelte-ignore state_referenced_locally
|
|
380
|
+
let claudeDir = $state(claudeConfigDir);
|
|
381
|
+
let savingClaudeDir = $state(false);
|
|
382
|
+
let claudeDirError = $state<string | null>(null);
|
|
383
|
+
let claudeDirSaved = $state(false);
|
|
384
|
+
let claudeDirFormEl: HTMLFormElement | undefined;
|
|
385
|
+
|
|
386
|
+
const claudeDirOpts = saveOpts<{ dir: string }>({
|
|
387
|
+
setSaving: (v) => (savingClaudeDir = v),
|
|
388
|
+
setError: (v) => (claudeDirError = v),
|
|
389
|
+
setMsg: (v) => (claudeDirSaved = !!v),
|
|
390
|
+
onSuccess: (data) => {
|
|
391
|
+
claudeDir = data?.dir ?? claudeDir;
|
|
392
|
+
claudeDirSaved = true;
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
function resetClaudeDir() {
|
|
397
|
+
flushSync(() => (claudeDir = ''));
|
|
398
|
+
claudeDirFormEl?.requestSubmit();
|
|
399
|
+
}
|
|
400
|
+
|
|
375
401
|
// svelte-ignore state_referenced_locally
|
|
376
402
|
let gitIdentity = $state(gitIdentityEnabled);
|
|
377
403
|
let savingGitToggle = $state(false);
|
|
@@ -1156,6 +1182,54 @@
|
|
|
1156
1182
|
</form>
|
|
1157
1183
|
</section>
|
|
1158
1184
|
|
|
1185
|
+
<section class="card">
|
|
1186
|
+
<form
|
|
1187
|
+
class="row image-row"
|
|
1188
|
+
method="POST"
|
|
1189
|
+
action="?/claudeConfigDir"
|
|
1190
|
+
bind:this={claudeDirFormEl}
|
|
1191
|
+
{@attach enhance(claudeDirOpts)}
|
|
1192
|
+
>
|
|
1193
|
+
<div class="label">
|
|
1194
|
+
<FolderCog size={20} />
|
|
1195
|
+
<div class="text">
|
|
1196
|
+
<div class="name">Claude config directory</div>
|
|
1197
|
+
<div class="desc">
|
|
1198
|
+
Host directory the Claude Code injections read from — credentials, statusLine, output
|
|
1199
|
+
styles, and global skills / <code>CLAUDE.md</code>. Leave blank to use
|
|
1200
|
+
<code>~/.claude</code>. Takes effect for instances created from now on.
|
|
1201
|
+
</div>
|
|
1202
|
+
</div>
|
|
1203
|
+
</div>
|
|
1204
|
+
<div class="image-controls">
|
|
1205
|
+
<input
|
|
1206
|
+
type="text"
|
|
1207
|
+
name="dir"
|
|
1208
|
+
class="image-input"
|
|
1209
|
+
bind:value={claudeDir}
|
|
1210
|
+
spellcheck="false"
|
|
1211
|
+
autocapitalize="off"
|
|
1212
|
+
autocorrect="off"
|
|
1213
|
+
placeholder="~/.claude"
|
|
1214
|
+
/>
|
|
1215
|
+
<Button type="submit" disabled={savingClaudeDir}>Save</Button>
|
|
1216
|
+
<Button
|
|
1217
|
+
type="button"
|
|
1218
|
+
icon={RotateCcw}
|
|
1219
|
+
disabled={savingClaudeDir}
|
|
1220
|
+
onclick={resetClaudeDir}
|
|
1221
|
+
title="Reset to default (~/.claude)"
|
|
1222
|
+
aria-label="Reset to default Claude config directory"
|
|
1223
|
+
/>
|
|
1224
|
+
</div>
|
|
1225
|
+
{#if claudeDirError}
|
|
1226
|
+
<div class="msg error">{claudeDirError}</div>
|
|
1227
|
+
{:else if claudeDirSaved}
|
|
1228
|
+
<div class="msg ok">Saved.</div>
|
|
1229
|
+
{/if}
|
|
1230
|
+
</form>
|
|
1231
|
+
</section>
|
|
1232
|
+
|
|
1159
1233
|
<section class="card">
|
|
1160
1234
|
<form
|
|
1161
1235
|
class="row"
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { readFile } from 'node:fs/promises';
|
|
3
|
-
import {
|
|
4
|
-
import { join } from 'node:path';
|
|
3
|
+
import { hostClaudeFile } from '../lib/host-claude.server.ts';
|
|
5
4
|
import { CLAUDE_CODE_TOKEN, CLAUDE_KEYCHAIN_SERVICE } from '../lib/config.server.ts';
|
|
6
5
|
import { getOption } from '../lib/db.server.ts';
|
|
7
6
|
import { checkPresence } from '../lib/exec.server.ts';
|
|
@@ -84,7 +83,7 @@ async function locateClaudeCredentials(): Promise<{ creds: string; source: strin
|
|
|
84
83
|
}
|
|
85
84
|
}
|
|
86
85
|
|
|
87
|
-
const file =
|
|
86
|
+
const file = hostClaudeFile('.credentials.json');
|
|
88
87
|
if (existsSync(file)) {
|
|
89
88
|
const raw = (await readFile(file, 'utf8')).trim();
|
|
90
89
|
if (isValid(raw)) return { creds: raw, source: '~/.claude/.credentials.json' };
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { readdir, readFile, stat } from 'node:fs/promises';
|
|
3
|
+
import { posix } from 'node:path';
|
|
4
|
+
import { checkPresence } from '../lib/exec.server.ts';
|
|
5
|
+
import { writeContainerFileBytes } from '../lib/container-files.server.ts';
|
|
6
|
+
import { claudeConfigFile } from '../lib/claude-settings.server.ts';
|
|
7
|
+
import { hostClaudeFile } from '../lib/host-claude.server.ts';
|
|
8
|
+
import type { Injection } from '../lib/injections.server.ts';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The env-var stdin carrier caps a single write near 128 KB (Linux `MAX_ARG_STRLEN`); base64
|
|
12
|
+
* inflates ~4/3, so a raw file over this is skipped rather than failing the whole injection.
|
|
13
|
+
*/
|
|
14
|
+
const MAX_FILE_BYTES = 90_000;
|
|
15
|
+
|
|
16
|
+
interface HostFile {
|
|
17
|
+
/** Path relative to the host Claude dir, e.g. `CLAUDE.md` or `skills/foo/SKILL.md`. */
|
|
18
|
+
rel: string;
|
|
19
|
+
bytes: Buffer;
|
|
20
|
+
/** True when any execute bit is set on the host, so container scripts stay runnable. */
|
|
21
|
+
exec: boolean;
|
|
22
|
+
oversized: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Recursively lists every file under a host dir, as `skills/<subpath>` entries. */
|
|
26
|
+
async function walkSkills(dir: string, rel: string, out: HostFile[]): Promise<void> {
|
|
27
|
+
let entries;
|
|
28
|
+
try {
|
|
29
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
30
|
+
} catch {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
for (const entry of entries) {
|
|
34
|
+
const abs = posix.join(dir, entry.name);
|
|
35
|
+
const childRel = posix.join(rel, entry.name);
|
|
36
|
+
if (entry.isDirectory()) {
|
|
37
|
+
await walkSkills(abs, childRel, out);
|
|
38
|
+
} else if (entry.isFile()) {
|
|
39
|
+
const info = await stat(abs);
|
|
40
|
+
out.push({
|
|
41
|
+
rel: childRel,
|
|
42
|
+
bytes: info.size > MAX_FILE_BYTES ? Buffer.alloc(0) : await readFile(abs),
|
|
43
|
+
exec: (info.mode & 0o111) !== 0,
|
|
44
|
+
oversized: info.size > MAX_FILE_BYTES
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Cheap presence probe for the setup-UI chip — never reads file contents. */
|
|
51
|
+
export async function hasHostSkillFiles(): Promise<boolean> {
|
|
52
|
+
if (existsSync(hostClaudeFile('CLAUDE.md'))) return true;
|
|
53
|
+
try {
|
|
54
|
+
return (await readdir(hostClaudeFile('skills'))).length > 0;
|
|
55
|
+
} catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The host global CLAUDE.md plus every file under the host skills dir. */
|
|
61
|
+
export async function collectHostSkillFiles(): Promise<HostFile[]> {
|
|
62
|
+
const files: HostFile[] = [];
|
|
63
|
+
const claudeMd = hostClaudeFile('CLAUDE.md');
|
|
64
|
+
if (existsSync(claudeMd)) {
|
|
65
|
+
const info = await stat(claudeMd);
|
|
66
|
+
if (info.isFile()) {
|
|
67
|
+
files.push({
|
|
68
|
+
rel: 'CLAUDE.md',
|
|
69
|
+
bytes: info.size > MAX_FILE_BYTES ? Buffer.alloc(0) : await readFile(claudeMd),
|
|
70
|
+
exec: false,
|
|
71
|
+
oversized: info.size > MAX_FILE_BYTES
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
await walkSkills(hostClaudeFile('skills'), 'skills', files);
|
|
76
|
+
return files;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export const claudeCodeSkills: Injection = {
|
|
80
|
+
id: 'claude-code-skills',
|
|
81
|
+
label: 'skills & CLAUDE.md',
|
|
82
|
+
|
|
83
|
+
auth: {
|
|
84
|
+
hint: 'add skills or a CLAUDE.md to your host ~/.claude',
|
|
85
|
+
async status() {
|
|
86
|
+
const available = await hasHostSkillFiles();
|
|
87
|
+
return { available, source: available ? '~/.claude' : null };
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
async apply(target, log) {
|
|
92
|
+
const files = await collectHostSkillFiles();
|
|
93
|
+
if (!files.length) {
|
|
94
|
+
log('⚠ No global skills or CLAUDE.md found on host; skipped\n');
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
log(`Injecting ${files.length} global skill/CLAUDE.md file(s)…\n`);
|
|
98
|
+
let failed = 0;
|
|
99
|
+
for (const file of files) {
|
|
100
|
+
if (file.oversized) {
|
|
101
|
+
log(`⚠ Skipped ${file.rel} (larger than ${MAX_FILE_BYTES} bytes)\n`);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
const dest = claudeConfigFile(file.rel, file.exec ? '755' : '644');
|
|
105
|
+
const wrote = await writeContainerFileBytes(target, dest, file.bytes);
|
|
106
|
+
if (!wrote.ok) {
|
|
107
|
+
failed++;
|
|
108
|
+
log(`⚠ Failed to write ${file.rel}: ${wrote.error}\n`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
log(
|
|
112
|
+
failed
|
|
113
|
+
? `⚠ ${failed} skill file(s) failed to inject\n`
|
|
114
|
+
: '✓ Global skills & CLAUDE.md injected\n'
|
|
115
|
+
);
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
async check(target) {
|
|
119
|
+
return checkPresence(
|
|
120
|
+
target,
|
|
121
|
+
'h=$(eval echo ~$(id -un)); d="${CLAUDE_CONFIG_DIR:-$h/.claude}"; ' +
|
|
122
|
+
'if [ -s "$d/CLAUDE.md" ] || [ -n "$(ls -A "$d/skills" 2>/dev/null)" ]; then echo 1; else echo 0; fi'
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
};
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { readdir, readFile } from 'node:fs/promises';
|
|
2
|
-
import { homedir } from 'node:os';
|
|
3
2
|
import { join } from 'node:path';
|
|
4
3
|
import { getOption } from '../lib/db.server.ts';
|
|
5
4
|
import { writeContainerFile } from '../lib/container-files.server.ts';
|
|
@@ -8,7 +7,7 @@ import {
|
|
|
8
7
|
mergeClaudeSettings,
|
|
9
8
|
readClaudeSettings
|
|
10
9
|
} from '../lib/claude-settings.server.ts';
|
|
11
|
-
import { readHostClaudeSettings } from '
|
|
10
|
+
import { hostClaudeFile, readHostClaudeSettings } from '../lib/host-claude.server.ts';
|
|
12
11
|
import { normalizeOutputStyle } from '../types.ts';
|
|
13
12
|
import type { ClaudeOutputStyle } from '../types.ts';
|
|
14
13
|
import type { ContainerTarget, Injection } from '../lib/injections.server.ts';
|
|
@@ -29,7 +28,7 @@ export async function resolveEffectiveOutputStyle(): Promise<string | null> {
|
|
|
29
28
|
|
|
30
29
|
/** The host's custom output-style definitions, so a non-built-in name resolves inside the container. */
|
|
31
30
|
export async function readHostOutputStyleFiles(): Promise<{ name: string; content: string }[]> {
|
|
32
|
-
const dir =
|
|
31
|
+
const dir = hostClaudeFile('output-styles');
|
|
33
32
|
let names: string[];
|
|
34
33
|
try {
|
|
35
34
|
names = (await readdir(dir)).filter((n) => n.endsWith('.md'));
|
|
@@ -1,15 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { statSync } from 'node:fs';
|
|
2
2
|
import { readFile } from 'node:fs/promises';
|
|
3
|
-
import { homedir } from 'node:os';
|
|
4
|
-
import { join } from 'node:path';
|
|
5
3
|
import { writeContainerFile } from '../lib/container-files.server.ts';
|
|
6
4
|
import {
|
|
7
5
|
claudeConfigFile,
|
|
8
6
|
mergeClaudeSettings,
|
|
9
7
|
readClaudeSettings
|
|
10
8
|
} from '../lib/claude-settings.server.ts';
|
|
9
|
+
import { expandTilde, readHostClaudeSettings } from '../lib/host-claude.server.ts';
|
|
11
10
|
import type { ContainerTarget, Injection } from '../lib/injections.server.ts';
|
|
12
11
|
|
|
12
|
+
// Re-exported so the modules and tests that historically imported these from here keep working.
|
|
13
|
+
export { expandTilde, readHostClaudeSettings };
|
|
14
|
+
|
|
13
15
|
/** Left unexpanded — Claude Code invokes this long after the injection has finished. */
|
|
14
16
|
const CONTAINER_SCRIPT_PATH = '${CLAUDE_CONFIG_DIR:-$HOME/.claude}/statusline.sh';
|
|
15
17
|
|
|
@@ -19,19 +21,6 @@ interface StatusLineConfig {
|
|
|
19
21
|
script?: string;
|
|
20
22
|
}
|
|
21
23
|
|
|
22
|
-
export async function readHostClaudeSettings(): Promise<Record<string, unknown> | null> {
|
|
23
|
-
const file = join(homedir(), '.claude', 'settings.json');
|
|
24
|
-
if (!existsSync(file)) return null;
|
|
25
|
-
try {
|
|
26
|
-
return JSON.parse(await readFile(file, 'utf8')) as Record<string, unknown>;
|
|
27
|
-
} catch {
|
|
28
|
-
return null;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export const expandTilde = (p: string): string =>
|
|
33
|
-
p.startsWith('~/') ? join(homedir(), p.slice(2)) : p;
|
|
34
|
-
|
|
35
24
|
/** Only a real file counts — else jq's `//` operator and a bare `/` resolve to the root dir. */
|
|
36
25
|
const isExistingFile = (p: string): boolean => {
|
|
37
26
|
try {
|
|
@@ -54,6 +54,15 @@ export function writeFileScript(file: ContainerFile): string {
|
|
|
54
54
|
);
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
/** Like `writeFileScript`, but the stdin payload is base64 so binary bytes survive the env-var carrier. */
|
|
58
|
+
export function writeBase64FileScript(file: ContainerFile): string {
|
|
59
|
+
const mode = file.mode ?? '644';
|
|
60
|
+
return (
|
|
61
|
+
`set -e; ${HOME_PRELUDE}f="${filePath(file)}"; mkdir -p "$(dirname "$f")"; ` +
|
|
62
|
+
`printf '%s' "$CODEBAY_STDIN" | base64 -d > "$f"; chmod ${mode} "$f"`
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
57
66
|
/** Single-quotes a value for a sourced shell file so whatever it contains stays literal. */
|
|
58
67
|
export const shellSingleQuote = (value: string): string => `'${value.replaceAll("'", "'\\''")}'`;
|
|
59
68
|
|
|
@@ -98,6 +107,19 @@ export async function writeContainerFile(
|
|
|
98
107
|
return res.ok ? { ok: true } : { ok: false, error: res.error };
|
|
99
108
|
}
|
|
100
109
|
|
|
110
|
+
/** Writes raw `bytes` to a container file, ferrying them as base64 so binary content stays intact. */
|
|
111
|
+
export async function writeContainerFileBytes(
|
|
112
|
+
target: ExecTarget,
|
|
113
|
+
file: ContainerFile,
|
|
114
|
+
bytes: Buffer
|
|
115
|
+
): Promise<{ ok: boolean; error?: string }> {
|
|
116
|
+
const res = await execInContainer(target, {
|
|
117
|
+
script: writeBase64FileScript(file),
|
|
118
|
+
stdin: bytes.toString('base64')
|
|
119
|
+
});
|
|
120
|
+
return res.ok ? { ok: true } : { ok: false, error: res.error };
|
|
121
|
+
}
|
|
122
|
+
|
|
101
123
|
/** Appends each line to every file it's missing from — the shared idempotent rc/conf write. */
|
|
102
124
|
export async function appendLinesIfAbsent(
|
|
103
125
|
target: ExecTarget,
|
|
@@ -0,0 +1,31 @@
|
|
|
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 { getOption } from './db.server.ts';
|
|
6
|
+
|
|
7
|
+
/** Expands a leading `~` to the host home; other paths pass through untouched. */
|
|
8
|
+
export const expandTilde = (p: string): string =>
|
|
9
|
+
p === '~' ? homedir() : p.startsWith('~/') ? join(homedir(), p.slice(2)) : p;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Host directory the Claude config injections read from: the `claude_config_dir` setting when set,
|
|
13
|
+
* else `~/.claude`. One resolver so credentials, statusline, output styles, and skills never diverge.
|
|
14
|
+
*/
|
|
15
|
+
export function hostClaudeDir(): string {
|
|
16
|
+
const custom = getOption('claude_config_dir')?.trim();
|
|
17
|
+
return custom ? expandTilde(custom) : join(homedir(), '.claude');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** A path under the configured host Claude dir, e.g. `hostClaudeFile('CLAUDE.md')`. */
|
|
21
|
+
export const hostClaudeFile = (...parts: string[]): string => join(hostClaudeDir(), ...parts);
|
|
22
|
+
|
|
23
|
+
export async function readHostClaudeSettings(): Promise<Record<string, unknown> | null> {
|
|
24
|
+
const file = hostClaudeFile('settings.json');
|
|
25
|
+
if (!existsSync(file)) return null;
|
|
26
|
+
try {
|
|
27
|
+
return JSON.parse(await readFile(file, 'utf8')) as Record<string, unknown>;
|
|
28
|
+
} catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -7,6 +7,7 @@ import { ttyd } from '../container-injections/ttyd.ts';
|
|
|
7
7
|
import { gitIdentity } from '../container-injections/git-identity.ts';
|
|
8
8
|
import { claudeCodeCredentials } from '../container-injections/claude-code-credentials.ts';
|
|
9
9
|
import { claudeCodeCustom } from '../container-injections/claude-code-custom.ts';
|
|
10
|
+
import { claudeCodeSkills } from '../container-injections/claude-code-skills.ts';
|
|
10
11
|
import { claudeCodeIdeExtension } from '../container-injections/claude-code-ide-extension.ts';
|
|
11
12
|
import { codeServerDark } from '../container-injections/code-server-dark.ts';
|
|
12
13
|
import { claudeCodeInstall } from '../container-injections/claude-code-install.ts';
|
|
@@ -67,6 +68,7 @@ function buildStages(claudeInjection: Injection): Injection[][] {
|
|
|
67
68
|
tmux,
|
|
68
69
|
claudeCodeInstall,
|
|
69
70
|
claudeInjection,
|
|
71
|
+
claudeCodeSkills,
|
|
70
72
|
claudeCodeIdeExtension,
|
|
71
73
|
claudeEffortLevel
|
|
72
74
|
],
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
disableBuildCache,
|
|
22
22
|
copyIgnorePatterns,
|
|
23
23
|
builtinCopyIgnore,
|
|
24
|
+
claudeConfigDir,
|
|
24
25
|
gitIdentityEnabled,
|
|
25
26
|
gitIdentityName,
|
|
26
27
|
gitIdentityEmail,
|
|
@@ -62,6 +63,7 @@
|
|
|
62
63
|
disableBuildCache: boolean;
|
|
63
64
|
copyIgnorePatterns: string;
|
|
64
65
|
builtinCopyIgnore: string;
|
|
66
|
+
claudeConfigDir: string;
|
|
65
67
|
gitIdentityEnabled: boolean;
|
|
66
68
|
gitIdentityName: string;
|
|
67
69
|
gitIdentityEmail: string;
|
|
@@ -106,6 +108,7 @@
|
|
|
106
108
|
{disableBuildCache}
|
|
107
109
|
{copyIgnorePatterns}
|
|
108
110
|
{builtinCopyIgnore}
|
|
111
|
+
{claudeConfigDir}
|
|
109
112
|
{gitIdentityEnabled}
|
|
110
113
|
{gitIdentityName}
|
|
111
114
|
{gitIdentityEmail}
|
package/src/routes.ts
CHANGED
|
@@ -214,6 +214,8 @@ export const routes: Record<string, MochiRouteValue> = {
|
|
|
214
214
|
// An explicit empty string (as opposed to unset) means "copy everything".
|
|
215
215
|
copyIgnorePatterns: getOption('copy_ignore_patterns') ?? DEFAULT_COPY_IGNORE,
|
|
216
216
|
builtinCopyIgnore: DEFAULT_COPY_IGNORE,
|
|
217
|
+
// Blank means "use ~/.claude"; the host dir credentials/skills/statusLine read from.
|
|
218
|
+
claudeConfigDir: getOption('claude_config_dir') ?? '',
|
|
217
219
|
// Not secrets, so the actual values (not just a "set" flag) go to the client.
|
|
218
220
|
// Blank means "no override" — fall back to the host's git config.
|
|
219
221
|
gitIdentityEnabled: gitIdentityEnabled(),
|
|
@@ -334,6 +336,13 @@ export const routes: Record<string, MochiRouteValue> = {
|
|
|
334
336
|
return success({ patterns });
|
|
335
337
|
},
|
|
336
338
|
|
|
339
|
+
// The host directory Claude config injections read from; blank falls back to ~/.claude.
|
|
340
|
+
claudeConfigDir: ({ formData }) => {
|
|
341
|
+
const dir = str(formData, 'dir').trim();
|
|
342
|
+
setOption('claude_config_dir', dir);
|
|
343
|
+
return success({ dir });
|
|
344
|
+
},
|
|
345
|
+
|
|
337
346
|
gitIdentityToggle: ({ formData }) => {
|
|
338
347
|
const enabled = onChecked(formData, 'enabled');
|
|
339
348
|
setOption('git_identity_enabled', enabled ? '1' : '0');
|
package/src/shell.html
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
6
7
|
<title>Codebay</title>
|
|
7
8
|
<style>
|
|
8
9
|
/* Each themed variable is defined once as light-dark(light, dark). With no
|