codebay 0.12.0 → 0.13.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.
Files changed (57) hide show
  1. package/.mochi/manifest.json +198 -190
  2. package/.mochi/svelte-client/HydratableIsland-zrdsrsmg.js +1 -0
  3. package/.mochi/svelte-client/{_hydrate-AppShell_2rtnxn6pgp2rh-aqm4k957.js → _hydrate-AppShell_2rtnxn6pgp2rh-v7y7dh53.js} +2 -2
  4. package/.mochi/svelte-client/{_hydrate-AvatarsView_257s9yq1wwr4f-m7h7xdxn.js → _hydrate-AvatarsView_257s9yq1wwr4f-tmwd6fpb.js} +1 -1
  5. package/.mochi/svelte-client/{_hydrate-InstanceView_2ma45l9bybfhl-d256bw54.js → _hydrate-InstanceView_2ma45l9bybfhl-m7t64dbh.js} +1 -1
  6. package/.mochi/svelte-client/_hydrate-SettingsView_8hmarwmf8onq-wedc9x9y.js +39 -0
  7. package/.mochi/svelte-client/{chunk-2eaagsfc.js → chunk-0mc09cyb.js} +1 -1
  8. package/.mochi/svelte-client/{chunk-a5e8vytv.js → chunk-5jwfs4p9.js} +2 -2
  9. package/.mochi/svelte-client/chunk-6qg4q98a.js +2 -0
  10. package/.mochi/svelte-client/{chunk-d8rdrqsh.js → chunk-ccfh8hxz.js} +1 -1
  11. package/.mochi/svelte-client/{chunk-d9bszcp8.js → chunk-phf7v5jj.js} +1 -1
  12. package/.mochi/svelte-client/{chunk-y1xjps59.js → chunk-spk1b2r4.js} +1 -1
  13. package/.mochi/svelte-client/chunk-tmeh70wg.js +2 -0
  14. package/.mochi/svelte-client/{chunk-6v5d7dex.js → chunk-vs1yjbqt.js} +1 -1
  15. package/.mochi/svelte-client/{chunk-1ttqgrz7.js → chunk-x0m3k0rn.js} +1 -1
  16. package/.mochi/svelte-client/{chunk-s70rry6z.js → chunk-y7f4jbdm.js} +1 -1
  17. package/.mochi/svelte-compile/{App-bhak71kt.server.js → App-94xsdn0w.server.js} +2 -2
  18. package/.mochi/svelte-compile/{Avatars-c0k5jx8q.server.js → Avatars-fnccw47h.server.js} +1 -1
  19. package/.mochi/svelte-compile/{Instance-hj76qnha.server.js → Instance-knnkcn2p.server.js} +1 -1
  20. package/.mochi/svelte-compile/{Settings-j3aygrjb.server.js → Settings-h7gsjyz3.server.js} +169 -114
  21. package/.mochi/svelte-compile/{chunk-gz6hv1h5.js → chunk-apdte4nh.js} +121 -143
  22. package/.mochi/svelte-compile/{chunk-zd49axkf.js → chunk-w0hca7va.js} +2 -1
  23. package/package.json +1 -1
  24. package/src/avatars/anchor.ts +1 -2
  25. package/src/avatars/bear.ts +2 -3
  26. package/src/avatars/bee.ts +7 -8
  27. package/src/avatars/cat.ts +4 -5
  28. package/src/avatars/cherry.ts +7 -8
  29. package/src/avatars/crab.ts +6 -7
  30. package/src/avatars/crown.ts +2 -3
  31. package/src/avatars/dog.ts +8 -9
  32. package/src/avatars/fish.ts +8 -9
  33. package/src/avatars/flower.ts +6 -7
  34. package/src/avatars/fox.ts +8 -9
  35. package/src/avatars/frog.ts +6 -7
  36. package/src/avatars/key.ts +2 -3
  37. package/src/avatars/mushroom.ts +4 -5
  38. package/src/avatars/octopus.ts +7 -8
  39. package/src/avatars/owl.ts +7 -8
  40. package/src/avatars/penguin.ts +8 -9
  41. package/src/avatars/planet.ts +8 -9
  42. package/src/avatars/rabbit.ts +3 -4
  43. package/src/avatars/robot.ts +6 -7
  44. package/src/avatars/snail.ts +6 -7
  45. package/src/avatars/star.ts +5 -6
  46. package/src/components/SettingsView.svelte +74 -0
  47. package/src/container-injections/claude-code-install.ts +12 -7
  48. package/src/container-injections/claude-code-update.ts +45 -10
  49. package/src/container-injections/claude-output-style.ts +84 -0
  50. package/src/lib/injections.server.ts +5 -1
  51. package/src/pages/Settings.svelte +9 -1
  52. package/src/routes.ts +10 -0
  53. package/src/types.ts +16 -0
  54. package/.mochi/svelte-client/HydratableIsland-4j3bazeq.js +0 -1
  55. package/.mochi/svelte-client/_hydrate-SettingsView_8hmarwmf8onq-pwbtv8ma.js +0 -39
  56. package/.mochi/svelte-client/chunk-krevp8yb.js +0 -2
  57. package/.mochi/svelte-client/chunk-wcfbjj3x.js +0 -2
@@ -2,15 +2,14 @@ import type { AvatarArt } from './types.ts';
2
2
 
3
3
  const art: AvatarArt = {
4
4
  name: 'flower',
5
- robot: true,
6
5
  pixels: [
7
- '..#..#..',
8
- '.###.##.',
9
- '########',
10
- '.######.',
11
- '..####..',
12
6
  '...##...',
13
- '..##.##.',
7
+ '..#+##..',
8
+ '.#.##.#.',
9
+ '###++#+#',
10
+ '#+#++###',
11
+ '.#.##.#.',
12
+ '..##+#..',
14
13
  '...##...'
15
14
  ]
16
15
  };
@@ -2,16 +2,15 @@ import type { AvatarArt } from './types.ts';
2
2
 
3
3
  const art: AvatarArt = {
4
4
  name: 'fox',
5
- robot: true,
6
5
  pixels: [
7
- '##....##',
8
- '##....##',
9
- '########',
10
- '###..###',
11
- '########',
12
- '########',
13
- '.######.',
14
- '..####..'
6
+ '#...+...',
7
+ '##.++...',
8
+ '###++...',
9
+ '#####..#',
10
+ '##.#####',
11
+ '#######.',
12
+ '######..',
13
+ '#####...'
15
14
  ]
16
15
  };
17
16
 
@@ -2,16 +2,15 @@ import type { AvatarArt } from './types.ts';
2
2
 
3
3
  const art: AvatarArt = {
4
4
  name: 'frog',
5
- robot: true,
6
5
  pixels: [
7
- '.#....#.',
8
- '###..###',
6
+ '........',
7
+ '........',
8
+ '.##..##.',
9
+ '#..##..#',
10
+ '#..##..#',
9
11
  '########',
10
12
  '########',
11
- '########',
12
- '########',
13
- '.######.',
14
- '..####..'
13
+ '.######.'
15
14
  ]
16
15
  };
17
16
 
@@ -2,16 +2,15 @@ import type { AvatarArt } from './types.ts';
2
2
 
3
3
  const art: AvatarArt = {
4
4
  name: 'key',
5
- robot: true,
6
5
  pixels: [
7
6
  '.####...',
8
7
  '##..##..',
9
8
  '##..##..',
10
- '##..##..',
11
9
  '.####...',
12
10
  '..##....',
13
11
  '..####..',
14
- '..##.##.'
12
+ '..##....',
13
+ '..####..'
15
14
  ]
16
15
  };
17
16
 
@@ -2,16 +2,15 @@ import type { AvatarArt } from './types.ts';
2
2
 
3
3
  const art: AvatarArt = {
4
4
  name: 'mushroom',
5
- robot: true,
6
5
  pixels: [
7
6
  '..####..',
8
- '.######.',
9
- '########',
10
- '########',
7
+ '.##.###.',
8
+ '#.###.##',
11
9
  '########',
12
10
  '..####..',
13
11
  '..####..',
14
- '.######.'
12
+ '..####..',
13
+ '...##...'
15
14
  ]
16
15
  };
17
16
 
@@ -2,16 +2,15 @@ import type { AvatarArt } from './types.ts';
2
2
 
3
3
  const art: AvatarArt = {
4
4
  name: 'octopus',
5
- robot: true,
6
5
  pixels: [
7
- '..####..',
6
+ '.#..###.',
7
+ '#..####.',
8
+ '#..#.#..',
9
+ '#..###..',
8
10
  '.######.',
9
- '###..###',
10
- '########',
11
- '########',
12
- '########',
13
- '.######.',
14
- '#.#.#.#.'
11
+ '...#####',
12
+ '##.##.#.',
13
+ '.###..##'
15
14
  ]
16
15
  };
17
16
 
@@ -2,16 +2,15 @@ import type { AvatarArt } from './types.ts';
2
2
 
3
3
  const art: AvatarArt = {
4
4
  name: 'owl',
5
- robot: true,
6
5
  pixels: [
6
+ '.#....#.',
7
7
  '#......#',
8
- '##....##',
9
- '.######.',
10
- '.#.##.#.',
11
- '.#.##.#.',
12
- '.######.',
13
- '.######.',
14
- '..#..#..'
8
+ '########',
9
+ '#..##..#',
10
+ '#..##..#',
11
+ '########',
12
+ '###..###',
13
+ '.###.##.'
15
14
  ]
16
15
  };
17
16
 
@@ -2,16 +2,15 @@ import type { AvatarArt } from './types.ts';
2
2
 
3
3
  const art: AvatarArt = {
4
4
  name: 'penguin',
5
- robot: true,
6
5
  pixels: [
7
- '..####..',
8
- '.######.',
9
- '.##..##.',
10
- '.#.##.#.',
11
- '##...##.',
12
- '##...##.',
13
- '#######.',
14
- '.#....#.'
6
+ '..++++..',
7
+ '.++++++.',
8
+ '.+.++.+.',
9
+ '.++..++.',
10
+ '++##.#++',
11
+ '+######+',
12
+ '+######+',
13
+ '.######.'
15
14
  ]
16
15
  };
17
16
 
@@ -2,16 +2,15 @@ import type { AvatarArt } from './types.ts';
2
2
 
3
3
  const art: AvatarArt = {
4
4
  name: 'planet',
5
- robot: true,
6
5
  pixels: [
7
- '...###..',
8
- '..#####.',
9
- '#.#####.',
10
- '.#######',
11
- '..#####.',
12
- '..####..',
13
- '.###....',
14
- '........'
6
+ '.....+++',
7
+ '...###.+',
8
+ '..####+.',
9
+ '.####+#.',
10
+ '.###+##.',
11
+ '+##+###.',
12
+ '+.+###..',
13
+ '++......'
15
14
  ]
16
15
  };
17
16
 
@@ -2,16 +2,15 @@ import type { AvatarArt } from './types.ts';
2
2
 
3
3
  const art: AvatarArt = {
4
4
  name: 'rabbit',
5
- robot: true,
6
5
  pixels: [
7
6
  '.#....#.',
8
7
  '.##..##.',
9
8
  '.##..##.',
10
9
  '..####..',
11
- '.######.',
10
+ '.#.##.#.',
11
+ '########',
12
12
  '###..###',
13
- '#..##..#',
14
- '.######.'
13
+ '.#.##.#.'
15
14
  ]
16
15
  };
17
16
 
@@ -2,16 +2,15 @@ import type { AvatarArt } from './types.ts';
2
2
 
3
3
  const art: AvatarArt = {
4
4
  name: 'robot',
5
- robot: true,
6
5
  pixels: [
7
- '...##...',
8
- '...#....',
9
6
  '.######.',
7
+ '...##...',
8
+ '########',
9
+ '########',
10
10
  '#.####.#',
11
- '#..##..#',
12
- '.######.',
13
- '.#####..',
14
- '.######.'
11
+ '#.####.#',
12
+ '###..###',
13
+ '########'
15
14
  ]
16
15
  };
17
16
 
@@ -2,15 +2,14 @@ import type { AvatarArt } from './types.ts';
2
2
 
3
3
  const art: AvatarArt = {
4
4
  name: 'snail',
5
- robot: true,
6
5
  pixels: [
7
- '.####..#',
8
- '#....#.#',
6
+ '........',
7
+ '.....#.#',
8
+ '.##..#.#',
9
9
  '#.##.#.#',
10
- '#.#..#.#',
11
- '#....##.',
12
- '#...#.#.',
13
- '######.#',
10
+ '##.#.#.#',
11
+ '#..##.#.',
12
+ '########',
14
13
  '.######.'
15
14
  ]
16
15
  };
@@ -2,16 +2,15 @@ import type { AvatarArt } from './types.ts';
2
2
 
3
3
  const art: AvatarArt = {
4
4
  name: 'star',
5
- robot: true,
6
5
  pixels: [
6
+ '...#....',
7
7
  '...##...',
8
8
  '...##...',
9
- '########',
9
+ '..####..',
10
10
  '.######.',
11
- '.######.',
12
- '.##..##.',
13
- '##....##',
14
- '#......#'
11
+ '..####..',
12
+ '...##...',
13
+ '....#...'
15
14
  ]
16
15
  };
17
16
 
@@ -42,14 +42,17 @@
42
42
  import {
43
43
  CLAUDE_PERMISSION_MODES,
44
44
  CLAUDE_EFFORT_LEVELS,
45
+ CLAUDE_OUTPUT_STYLES,
45
46
  type ClaudePermissionMode,
46
47
  type ClaudeEffortLevel,
48
+ type ClaudeOutputStyle,
47
49
  type InstanceMode
48
50
  } from '../types.ts';
49
51
  import Terminal from '@lucide/svelte/icons/terminal';
50
52
  import LayoutTemplate from '@lucide/svelte/icons/layout-template';
51
53
  import ShieldCheck from '@lucide/svelte/icons/shield-check';
52
54
  import Gauge from '@lucide/svelte/icons/gauge';
55
+ import MessageSquare from '@lucide/svelte/icons/message-square';
53
56
  import { installPopupBackTrap } from '../lib/popup-nav.ts';
54
57
 
55
58
  /** Every settings form action fails with the same `{ error }` shape. */
@@ -60,6 +63,7 @@
60
63
  defaultMode,
61
64
  claudePermissionMode,
62
65
  claudeEffortLevel,
66
+ claudeOutputStyle,
63
67
  defaultImage,
64
68
  builtinImage,
65
69
  disableBuildCache,
@@ -100,6 +104,7 @@
100
104
  defaultMode: InstanceMode;
101
105
  claudePermissionMode: ClaudePermissionMode;
102
106
  claudeEffortLevel: ClaudeEffortLevel;
107
+ claudeOutputStyle: ClaudeOutputStyle;
103
108
  defaultImage: string;
104
109
  builtinImage: string;
105
110
  disableBuildCache: boolean;
@@ -292,6 +297,33 @@
292
297
  effortFormEl?.requestSubmit();
293
298
  }
294
299
 
300
+ // svelte-ignore state_referenced_locally
301
+ let outputStyleChoice = $state<ClaudeOutputStyle>(claudeOutputStyle);
302
+ let savingOutputStyle = $state(false);
303
+ let outputStyleError = $state<string | null>(null);
304
+ let outputStyleFormEl: HTMLFormElement | undefined;
305
+
306
+ const outputStyleOpts = saveOpts<{ style: ClaudeOutputStyle }>({
307
+ setSaving: (v) => (savingOutputStyle = v),
308
+ setError: (v) => (outputStyleError = v),
309
+ setMsg: () => {},
310
+ onSuccess: (data) => {
311
+ if (data?.style) outputStyleChoice = data.style;
312
+ }
313
+ });
314
+
315
+ const OUTPUT_STYLE_LABELS: Record<ClaudeOutputStyle, string> = {
316
+ default: 'Default',
317
+ none: 'None',
318
+ Concise: 'Concise'
319
+ };
320
+
321
+ function chooseOutputStyle(next: ClaudeOutputStyle) {
322
+ if (next === outputStyleChoice) return;
323
+ flushSync(() => (outputStyleChoice = next));
324
+ outputStyleFormEl?.requestSubmit();
325
+ }
326
+
295
327
  // svelte-ignore state_referenced_locally
296
328
  let image = $state(defaultImage);
297
329
  let savingImage = $state(false);
@@ -973,6 +1005,48 @@
973
1005
  {/if}
974
1006
  </section>
975
1007
 
1008
+ <section class="card">
1009
+ <form
1010
+ class="row"
1011
+ method="POST"
1012
+ action="?/claudeOutputStyle"
1013
+ bind:this={outputStyleFormEl}
1014
+ {@attach enhance(outputStyleOpts)}
1015
+ >
1016
+ <div class="label">
1017
+ <MessageSquare size={20} />
1018
+ <div class="text">
1019
+ <div class="name">Claude output style</div>
1020
+ <div class="desc">
1021
+ The output style Claude Code starts new sessions in, written to
1022
+ <code>~/.claude/settings.json</code>. <strong>Default</strong> inherits your host
1023
+ <code>~/.claude/settings.json</code> (including any custom styles);
1024
+ <strong>None</strong>
1025
+ forces it off. Applies on create and rebuild, not to already-running instances.
1026
+ </div>
1027
+ </div>
1028
+ </div>
1029
+ <input type="hidden" name="style" value={outputStyleChoice} />
1030
+ <div class="mode-toggle" role="group" aria-label="Claude output style">
1031
+ {#each CLAUDE_OUTPUT_STYLES as option (option)}
1032
+ <button
1033
+ type="button"
1034
+ class="mode-btn"
1035
+ class:active={outputStyleChoice === option}
1036
+ aria-pressed={outputStyleChoice === option}
1037
+ disabled={savingOutputStyle}
1038
+ onclick={() => chooseOutputStyle(option)}
1039
+ >
1040
+ {OUTPUT_STYLE_LABELS[option]}
1041
+ </button>
1042
+ {/each}
1043
+ </div>
1044
+ </form>
1045
+ {#if outputStyleError}
1046
+ <div class="sub"><div class="msg error">{outputStyleError}</div></div>
1047
+ {/if}
1048
+ </section>
1049
+
976
1050
  <section class="card">
977
1051
  <form
978
1052
  class="row image-row"
@@ -8,20 +8,25 @@ import type { Injection } from '../lib/injections.server.ts';
8
8
  * Node, else the standalone installer, which needs no Node at all.
9
9
  *
10
10
  * `$1` is the remote user (the injection passes it; the build-time feature falls back to the
11
- * devcontainer CLI's `$_REMOTE_USER`) — the standalone installer puts the binary under that
12
- * user's `$HOME`, so running it as root would strand it in `/root`.
11
+ * devcontainer CLI's `$_REMOTE_USER`). Claude Code 2.x's native binary — installed by npm's
12
+ * postinstall as much as by the standalone installer — lands under that user's `$HOME`, so both
13
+ * branches run as that user via `run_as`; running as root would strand the binary in `/root`.
14
+ * Success is judged by `claude --version` (as the user), not `command -v claude`, so a stranded
15
+ * binary the wrapper still resolves falls through to the standalone installer instead of passing.
13
16
  */
14
17
  export const INSTALL_SCRIPT =
15
18
  'if command -v claude >/dev/null 2>&1; then exit 0; fi\n' +
16
- 'if command -v npm >/dev/null 2>&1 && npm install -g @anthropic-ai/claude-code@latest >/dev/null 2>&1; then\n' +
17
- ' p="$(npm prefix -g 2>/dev/null)"\n' +
19
+ 'u="${1:-${_REMOTE_USER:-root}}"\n' +
20
+ // Fall back to a home the remote user owns, never root's, so `su -m` can't strand the binary in /root.
21
+ 'h="$(getent passwd "$u" 2>/dev/null | cut -d: -f6)"; [ -n "$h" ] || { [ "$u" = root ] && h=/root || h="/home/$u"; }\n' +
22
+ 'run_as() { if [ "$(id -un)" = "$u" ]; then HOME="$h" sh -c "$1"; else HOME="$h" su -m "$u" -c "$1"; fi; }\n' +
23
+ "if command -v npm >/dev/null 2>&1 && run_as 'npm install -g @anthropic-ai/claude-code@latest >/dev/null 2>&1'; then\n" +
24
+ ' p="$(run_as \'npm prefix -g 2>/dev/null\')"\n' +
18
25
  // npm's global bin is off PATH whenever the image sets its own NPM_CONFIG_PREFIX, so a bare
19
26
  // `command -v` would miss an install that just succeeded — and so would the terminal launcher.
20
27
  ' [ -n "$p" ] && [ -x "$p/bin/claude" ] && ln -sf "$p/bin/claude" /usr/local/bin/claude 2>/dev/null\n' +
21
- ' if command -v claude >/dev/null 2>&1 || [ -x "$p/bin/claude" ]; then exit 0; fi\n' +
28
+ " if run_as 'claude --version >/dev/null 2>&1'; then exit 0; fi\n" +
22
29
  'fi\n' +
23
- 'u="${1:-${_REMOTE_USER:-root}}"\n' +
24
- 'h="$(getent passwd "$u" 2>/dev/null | cut -d: -f6)"; [ -n "$h" ] || h="$HOME"\n' +
25
30
  'f="$(mktemp)"\n' +
26
31
  'if command -v curl >/dev/null 2>&1; then curl -fsSL https://claude.ai/install.sh -o "$f"\n' +
27
32
  'elif command -v wget >/dev/null 2>&1; then wget -qO "$f" https://claude.ai/install.sh\n' +
@@ -8,29 +8,63 @@ import type { Injection } from '../lib/injections.server.ts';
8
8
  * was current when the layer was first baked. This re-checks at boot and only reinstalls when
9
9
  * behind — no-op if Claude Code isn't installed (project-owned images own their tooling) or
10
10
  * npm/registry is unreachable, and it echoes `updated <old> -> <new>` so `apply()` can log it.
11
+ *
12
+ * `$1` is the container's remote user. Claude Code 2.x ships a native binary that npm's
13
+ * postinstall drops into the *invoking* user's HOME, so the reinstall (and the `claude --version`
14
+ * probes) run as that user via `run_as` — a root reinstall would strand the binary in `/root`,
15
+ * exactly as the standalone installer in `claude-code-install.ts` would. Success is judged by a
16
+ * post-install `claude --version` (as the remote user), not npm's exit code, so a stranded or
17
+ * half-downloaded binary surfaces as a failure instead of a false `updated`.
11
18
  */
19
+ const RUN_AS_PRELUDE =
20
+ 'u="${1:-root}"; ' +
21
+ // Fall back to a home the remote user actually owns, never root's — otherwise `su -m` below
22
+ // would strand the native binary in /root, the exact failure this exists to prevent.
23
+ 'h="$(getent passwd "$u" 2>/dev/null | cut -d: -f6)"; [ -n "$h" ] || { [ "$u" = root ] && h=/root || h="/home/$u"; }; ' +
24
+ 'run_as() { if [ "$(id -un)" = "$u" ]; then HOME="$h" sh -c "$1"; else HOME="$h" su -m "$u" -c "$1"; fi; }; ' +
25
+ "ver() { run_as 'claude --version 2>/dev/null' | grep -oE '[0-9]+\\.[0-9]+\\.[0-9]+' | head -n1; }; ";
26
+
27
+ const NPM_INSTALL_CMD = 'npm install -g @anthropic-ai/claude-code@latest >/dev/null 2>&1';
28
+
29
+ /**
30
+ * Prefer the remote user — correct HOME and ownership on the common image whose global prefix is
31
+ * chowned to that user. Fall back to a root install with HOME pointed at the user's home when the
32
+ * user can't write a root-owned global prefix (`su` would EACCES there). Success is decided by the
33
+ * post-install `ver()`, so neither branch's exit status matters.
34
+ */
35
+ const REINSTALL =
36
+ `if run_as '${NPM_INSTALL_CMD}'; then :; ` +
37
+ `elif [ "$(id -un)" != "$u" ]; then HOME="$h" ${NPM_INSTALL_CMD} || true; fi; `;
38
+
12
39
  export const UPDATE_SCRIPT =
13
40
  'command -v claude >/dev/null 2>&1 || exit 0; ' +
14
41
  'command -v npm >/dev/null 2>&1 || exit 0; ' +
15
- "installed=$(claude --version 2>/dev/null | grep -oE '[0-9]+\\.[0-9]+\\.[0-9]+' | head -n1); " +
42
+ RUN_AS_PRELUDE +
43
+ 'installed="$(ver)"; ' +
16
44
  'latest=$(npm view @anthropic-ai/claude-code version 2>/dev/null); ' +
17
45
  'if [ -z "$latest" ]; then exit 0; fi; ' +
18
46
  'if [ "$installed" = "$latest" ]; then echo "current $latest"; exit 0; fi; ' +
19
- 'npm install -g @anthropic-ai/claude-code@latest >/dev/null 2>&1 || exit 1; ' +
20
- 'echo "updated $installed -> $latest"';
47
+ REINSTALL +
48
+ 'now="$(ver)"; ' +
49
+ 'if [ "$now" = "$latest" ]; then echo "updated ${installed:-none} -> $now"; exit 0; fi; ' +
50
+ 'exit 1';
21
51
 
22
52
  /**
23
53
  * 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.
54
+ * into the script), skipping the in-container `npm view` registry round-trip. `$1` is the remote
55
+ * user — see `UPDATE_SCRIPT` above for why the reinstall must run as that user.
25
56
  */
26
57
  export const PINNED_UPDATE_SCRIPT =
27
58
  'latest="$0"; ' +
28
59
  'command -v claude >/dev/null 2>&1 || exit 0; ' +
29
60
  '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); " +
61
+ RUN_AS_PRELUDE +
62
+ 'installed="$(ver)"; ' +
31
63
  '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"';
64
+ REINSTALL +
65
+ 'now="$(ver)"; ' +
66
+ 'if [ "$now" = "$latest" ]; then echo "updated ${installed:-none} -> $now"; exit 0; fi; ' +
67
+ 'exit 1';
34
68
 
35
69
  /** Anything looser could smuggle shell metacharacters out of a corrupted DB row into a container. */
36
70
  export const VERSION_RE = /^\d+\.\d+\.\d+$/;
@@ -84,12 +118,13 @@ export const claudeCodeUpdate: Injection = {
84
118
  async apply(target, log) {
85
119
  log('Checking Claude Code is up to date…\n');
86
120
  const latest = await resolveLatestVersion();
87
- // Omitting remoteUser runs as root, which the global npm reinstall needs.
121
+ // Runs as root (remoteUser omitted) so the script can `su -m` into the remote user for the
122
+ // reinstall, keeping Claude Code 2.x's native binary in that user's HOME (see UPDATE_SCRIPT).
88
123
  const res = await execInContainer(
89
124
  { containerId: target.containerId },
90
125
  latest
91
- ? { script: PINNED_UPDATE_SCRIPT, args: [latest], capture: true }
92
- : { script: UPDATE_SCRIPT, capture: true }
126
+ ? { script: PINNED_UPDATE_SCRIPT, args: [latest, target.remoteUser ?? ''], capture: true }
127
+ : { script: UPDATE_SCRIPT, args: ['claude-update', target.remoteUser ?? ''], capture: true }
93
128
  );
94
129
  if (!res.ok) {
95
130
  log(`⚠ Claude Code update check failed: ${res.error} — keeping the installed version\n`);
@@ -0,0 +1,84 @@
1
+ import { readdir, readFile } from 'node:fs/promises';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { getOption } from '../lib/db.server.ts';
5
+ import { writeContainerFile } from '../lib/container-files.server.ts';
6
+ import {
7
+ claudeConfigFile,
8
+ mergeClaudeSettings,
9
+ readClaudeSettings
10
+ } from '../lib/claude-settings.server.ts';
11
+ import { readHostClaudeSettings } from './claude-statusline.ts';
12
+ import { normalizeOutputStyle } from '../types.ts';
13
+ import type { ClaudeOutputStyle } from '../types.ts';
14
+ import type { ContainerTarget, Injection } from '../lib/injections.server.ts';
15
+
16
+ /** Also read by the settings-page `serverProps` to seed the UI. */
17
+ export function getClaudeOutputStyle(): ClaudeOutputStyle {
18
+ return normalizeOutputStyle(getOption('claude_output_style'));
19
+ }
20
+
21
+ /** The `outputStyle` actually applied: the setting itself, the host's value when inheriting, or null. */
22
+ export async function resolveEffectiveOutputStyle(): Promise<string | null> {
23
+ const setting = getClaudeOutputStyle();
24
+ if (setting === 'none') return null;
25
+ if (setting !== 'default') return setting;
26
+ const style = (await readHostClaudeSettings())?.outputStyle;
27
+ return typeof style === 'string' && style.trim() ? style.trim() : null;
28
+ }
29
+
30
+ /** The host's custom output-style definitions, so a non-built-in name resolves inside the container. */
31
+ export async function readHostOutputStyleFiles(): Promise<{ name: string; content: string }[]> {
32
+ const dir = join(homedir(), '.claude', 'output-styles');
33
+ let names: string[];
34
+ try {
35
+ names = (await readdir(dir)).filter((n) => n.endsWith('.md'));
36
+ } catch {
37
+ return [];
38
+ }
39
+ const files = await Promise.all(
40
+ names.map(async (name) => {
41
+ try {
42
+ return { name, content: await readFile(join(dir, name), 'utf8') };
43
+ } catch {
44
+ return null;
45
+ }
46
+ })
47
+ );
48
+ return files.filter((f): f is { name: string; content: string } => f !== null);
49
+ }
50
+
51
+ /** Writes Claude Code's `outputStyle` default into the container, mirroring host custom-style files. */
52
+ export const claudeOutputStyle: Injection = {
53
+ id: 'claude-output-style',
54
+ label: 'output style',
55
+
56
+ async apply(target: ContainerTarget, log) {
57
+ const style = await resolveEffectiveOutputStyle();
58
+ if (!style) {
59
+ log('No Claude output style to apply; skipped\n');
60
+ return;
61
+ }
62
+ for (const file of await readHostOutputStyleFiles()) {
63
+ await writeContainerFile(
64
+ target,
65
+ claudeConfigFile(`output-styles/${file.name}`, '644'),
66
+ file.content
67
+ );
68
+ }
69
+ log(`Setting Claude output style (${style})…\n`);
70
+ const result = await mergeClaudeSettings(target, { outputStyle: style });
71
+ log(
72
+ result.ok
73
+ ? '✓ Claude output style configured in container\n'
74
+ : `⚠ Claude output style injection failed: ${result.error}\n`
75
+ );
76
+ },
77
+
78
+ async check(target) {
79
+ const expected = await resolveEffectiveOutputStyle();
80
+ const settings = await readClaudeSettings(target);
81
+ const actual = settings?.outputStyle;
82
+ return expected === null ? actual == null : actual === expected;
83
+ }
84
+ };
@@ -17,6 +17,7 @@ import { attentionHooks } from '../container-injections/attention-hooks.ts';
17
17
  import { claudeStatusline } from '../container-injections/claude-statusline.ts';
18
18
  import { claudeModel } from '../container-injections/claude-model.ts';
19
19
  import { claudeEffortLevel } from '../container-injections/claude-effort-level.ts';
20
+ import { claudeOutputStyle } from '../container-injections/claude-output-style.ts';
20
21
  import { claudePermissionMode } from '../container-injections/claude-permission-mode.ts';
21
22
  import { claudeTrust } from '../container-injections/claude-trust.ts';
22
23
  import { claudeAliases } from '../container-injections/claude-aliases.ts';
@@ -78,7 +79,10 @@ function buildStages(claudeInjection: Injection): Injection[][] {
78
79
  // claude-trust edits ~/.claude.json, which the stage-1 Claude slot also writes.
79
80
  // custom-env-vars writes nothing (values ride containerEnv) — it only verifies + health-checks.
80
81
  [claudeTrust, hostEnvVars, customEnvVars],
81
- [claudeNoCoauthor]
82
+ [claudeNoCoauthor],
83
+ // A settings.json writer of its own: every other stage already has one, and same-stage
84
+ // settings.json writers race — so output style merges last, on its own.
85
+ [claudeOutputStyle]
82
86
  ];
83
87
  }
84
88