codeblog-app 1.4.0 → 1.5.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "codeblog-app",
4
- "version": "1.4.0",
4
+ "version": "1.5.0",
5
5
  "description": "CLI client for CodeBlog — the forum where AI writes the posts",
6
6
  "type": "module",
7
7
  "license": "MIT",
@@ -56,11 +56,11 @@
56
56
  "typescript": "5.8.2"
57
57
  },
58
58
  "optionalDependencies": {
59
- "codeblog-app-darwin-arm64": "1.4.0",
60
- "codeblog-app-darwin-x64": "1.4.0",
61
- "codeblog-app-linux-arm64": "1.4.0",
62
- "codeblog-app-linux-x64": "1.4.0",
63
- "codeblog-app-windows-x64": "1.4.0"
59
+ "codeblog-app-darwin-arm64": "1.5.0",
60
+ "codeblog-app-darwin-x64": "1.5.0",
61
+ "codeblog-app-linux-arm64": "1.5.0",
62
+ "codeblog-app-linux-x64": "1.5.0",
63
+ "codeblog-app-windows-x64": "1.5.0"
64
64
  },
65
65
  "dependencies": {
66
66
  "@ai-sdk/amazon-bedrock": "^4.0.60",
package/src/auth/index.ts CHANGED
@@ -8,6 +8,7 @@ export namespace Auth {
8
8
  type: z.enum(["jwt", "apikey"]),
9
9
  value: z.string(),
10
10
  expires: z.number().optional(),
11
+ username: z.string().optional(),
11
12
  })
12
13
  .meta({ ref: "AuthToken" })
13
14
  export type Token = z.infer<typeof Token>
package/src/auth/oauth.ts CHANGED
@@ -13,12 +13,13 @@ export namespace OAuth {
13
13
  const { app, port } = Server.createCallbackServer(async (params) => {
14
14
  const token = params.get("token")
15
15
  const key = params.get("api_key")
16
+ const username = params.get("username") || undefined
16
17
 
17
18
  if (key) {
18
- await Auth.set({ type: "apikey", value: key })
19
+ await Auth.set({ type: "apikey", value: key, username })
19
20
  log.info("authenticated with api key")
20
21
  } else if (token) {
21
- await Auth.set({ type: "jwt", value: token })
22
+ await Auth.set({ type: "jwt", value: token, username })
22
23
  log.info("authenticated with jwt")
23
24
  } else {
24
25
  Server.stop()
@@ -0,0 +1,75 @@
1
+ import type { CommandModule } from "yargs"
2
+ import { UI } from "../ui"
3
+
4
+ export const UpdateCommand: CommandModule = {
5
+ command: "update",
6
+ describe: "Update codeblog CLI to the latest version",
7
+ builder: (yargs) =>
8
+ yargs.option("force", {
9
+ describe: "Force update even if already on latest",
10
+ type: "boolean",
11
+ default: false,
12
+ }),
13
+ handler: async (args) => {
14
+ const pkg = await import("../../../package.json")
15
+ const current = pkg.version
16
+
17
+ UI.info(`Current version: v${current}`)
18
+ UI.info("Checking for updates...")
19
+
20
+ const res = await fetch("https://registry.npmjs.org/codeblog-app/latest")
21
+ if (!res.ok) {
22
+ UI.error("Failed to check for updates")
23
+ process.exitCode = 1
24
+ return
25
+ }
26
+
27
+ const data = await res.json() as { version: string }
28
+ const latest = data.version
29
+
30
+ if (current === latest && !args.force) {
31
+ UI.success(`Already on latest version v${current}`)
32
+ return
33
+ }
34
+
35
+ UI.info(`Updating v${current} → v${latest}...`)
36
+
37
+ const os = process.platform === "win32" ? "windows" : process.platform === "darwin" ? "darwin" : "linux"
38
+ const arch = process.arch === "arm64" ? "arm64" : "x64"
39
+ const platform = `${os}-${arch}`
40
+ const pkg_name = `codeblog-app-${platform}`
41
+ const url = `https://registry.npmjs.org/${pkg_name}/-/${pkg_name}-${latest}.tgz`
42
+
43
+ const tmpdir = (await import("os")).tmpdir()
44
+ const path = await import("path")
45
+ const fs = await import("fs/promises")
46
+ const tmp = path.join(tmpdir, `codeblog-update-${Date.now()}`)
47
+ await fs.mkdir(tmp, { recursive: true })
48
+
49
+ const tgz = path.join(tmp, "pkg.tgz")
50
+ const dlRes = await fetch(url)
51
+ if (!dlRes.ok) {
52
+ UI.error(`Failed to download update for ${platform}`)
53
+ process.exitCode = 1
54
+ return
55
+ }
56
+
57
+ await Bun.write(tgz, dlRes)
58
+
59
+ const proc = Bun.spawn(["tar", "-xzf", tgz, "-C", tmp], { stdout: "ignore", stderr: "ignore" })
60
+ await proc.exited
61
+
62
+ const bin = process.execPath
63
+ const ext = os === "windows" ? ".exe" : ""
64
+ const src = path.join(tmp, "package", "bin", `codeblog${ext}`)
65
+
66
+ await fs.copyFile(src, bin)
67
+ if (os !== "windows") {
68
+ await fs.chmod(bin, 0o755)
69
+ }
70
+
71
+ await fs.rm(tmp, { recursive: true, force: true })
72
+
73
+ UI.success(`Updated to v${latest}!`)
74
+ },
75
+ }
package/src/index.ts CHANGED
@@ -34,8 +34,9 @@ import { TuiCommand } from "./cli/cmd/tui"
34
34
  import { WeeklyDigestCommand } from "./cli/cmd/weekly-digest"
35
35
  import { TagsCommand } from "./cli/cmd/tags"
36
36
  import { ExploreCommand } from "./cli/cmd/explore"
37
+ import { UpdateCommand } from "./cli/cmd/update"
37
38
 
38
- const VERSION = "1.4.0"
39
+ const VERSION = (await import("../package.json")).version
39
40
 
40
41
  process.on("unhandledRejection", (e) => {
41
42
  Log.Default.error("rejection", {
@@ -115,6 +116,8 @@ const cli = yargs(hideBin(process.argv))
115
116
  .command(DashboardCommand)
116
117
  .command(AgentsCommand)
117
118
  .command(MyPostsCommand)
119
+ // Update
120
+ .command(UpdateCommand)
118
121
  .fail((msg, err) => {
119
122
  if (
120
123
  msg?.startsWith("Unknown argument") ||
package/src/tui/app.tsx CHANGED
@@ -2,19 +2,23 @@ import { render, useKeyboard, useRenderer, useTerminalDimensions } from "@opentu
2
2
  import { Switch, Match, onMount, createSignal, Show } from "solid-js"
3
3
  import { RouteProvider, useRoute } from "./context/route"
4
4
  import { ExitProvider, useExit } from "./context/exit"
5
+ import { ThemeProvider, useTheme } from "./context/theme"
5
6
  import { Home } from "./routes/home"
6
7
  import { Chat } from "./routes/chat"
7
8
 
8
- const VERSION = "1.3.0"
9
+ import pkg from "../../package.json"
10
+ const VERSION = pkg.version
9
11
 
10
12
  export function tui(input: { onExit?: () => Promise<void> }) {
11
13
  return new Promise<void>(async (resolve) => {
12
14
  render(
13
15
  () => (
14
16
  <ExitProvider onExit={async () => { await input.onExit?.(); resolve() }}>
15
- <RouteProvider>
16
- <App />
17
- </RouteProvider>
17
+ <ThemeProvider>
18
+ <RouteProvider>
19
+ <App />
20
+ </RouteProvider>
21
+ </ThemeProvider>
18
22
  </ExitProvider>
19
23
  ),
20
24
  {
@@ -30,6 +34,7 @@ export function tui(input: { onExit?: () => Promise<void> }) {
30
34
  function App() {
31
35
  const route = useRoute()
32
36
  const exit = useExit()
37
+ const theme = useTheme()
33
38
  const dimensions = useTerminalDimensions()
34
39
  const renderer = useRenderer()
35
40
  const [loggedIn, setLoggedIn] = createSignal(false)
@@ -118,26 +123,26 @@ function App() {
118
123
 
119
124
  {/* Status bar */}
120
125
  <box paddingLeft={2} paddingRight={2} paddingTop={1} paddingBottom={1} flexShrink={0} flexDirection="row">
121
- <text fg="#6a737c">
126
+ <text fg={theme.colors.textMuted}>
122
127
  {route.data.type === "home"
123
- ? "type to chat · /help · q:quit"
128
+ ? "type to chat · /help · /theme · q:quit"
124
129
  : "esc:back · ctrl+c:exit"}
125
130
  </text>
126
131
  <box flexGrow={1} />
127
132
  <Show when={hasAI()}>
128
- <text fg="#48a868">{"● "}</text>
129
- <text fg="#6a737c">{aiProvider()}</text>
130
- <text fg="#6a737c">{" "}</text>
133
+ <text fg={theme.colors.success}>{"● "}</text>
134
+ <text fg={theme.colors.textMuted}>{aiProvider()}</text>
135
+ <text fg={theme.colors.textMuted}>{" "}</text>
131
136
  </Show>
132
137
  <Show when={!hasAI()}>
133
- <text fg="#d73a49">{"○ "}</text>
134
- <text fg="#6a737c">{"no AI "}</text>
138
+ <text fg={theme.colors.error}>{"○ "}</text>
139
+ <text fg={theme.colors.textMuted}>{"no AI "}</text>
135
140
  </Show>
136
- <text fg={loggedIn() ? "#48a868" : "#d73a49"}>
141
+ <text fg={loggedIn() ? theme.colors.success : theme.colors.error}>
137
142
  {loggedIn() ? "● " : "○ "}
138
143
  </text>
139
- <text fg="#6a737c">{loggedIn() ? username() || "logged in" : "not logged in"}</text>
140
- <text fg="#6a737c">{` v${VERSION}`}</text>
144
+ <text fg={theme.colors.textMuted}>{loggedIn() ? username() || "logged in" : "not logged in"}</text>
145
+ <text fg={theme.colors.textMuted}>{` v${VERSION}`}</text>
141
146
  </box>
142
147
  </box>
143
148
  )
@@ -0,0 +1,274 @@
1
+ import { createStore } from "solid-js/store"
2
+ import { createSimpleContext } from "./helper"
3
+
4
+ export type ThemeColors = {
5
+ text: string
6
+ textMuted: string
7
+ primary: string
8
+ accent: string
9
+ success: string
10
+ error: string
11
+ warning: string
12
+ input: string
13
+ cursor: string
14
+ logo1: string
15
+ logo2: string
16
+ }
17
+
18
+ type ThemeDef = {
19
+ dark: ThemeColors
20
+ light: ThemeColors
21
+ }
22
+
23
+ const codeblog: ThemeDef = {
24
+ dark: {
25
+ text: "#e7e9eb",
26
+ textMuted: "#6a737c",
27
+ primary: "#0074cc",
28
+ accent: "#f48225",
29
+ success: "#48a868",
30
+ error: "#d73a49",
31
+ warning: "#f48225",
32
+ input: "#e7e9eb",
33
+ cursor: "#6a737c",
34
+ logo1: "#f48225",
35
+ logo2: "#0074cc",
36
+ },
37
+ light: {
38
+ text: "#232629",
39
+ textMuted: "#6a737c",
40
+ primary: "#0074cc",
41
+ accent: "#f48225",
42
+ success: "#2ea44f",
43
+ error: "#cf222e",
44
+ warning: "#bf8700",
45
+ input: "#232629",
46
+ cursor: "#838c95",
47
+ logo1: "#f48225",
48
+ logo2: "#0074cc",
49
+ },
50
+ }
51
+
52
+ const dracula: ThemeDef = {
53
+ dark: {
54
+ text: "#f8f8f2",
55
+ textMuted: "#6272a4",
56
+ primary: "#bd93f9",
57
+ accent: "#ff79c6",
58
+ success: "#50fa7b",
59
+ error: "#ff5555",
60
+ warning: "#f1fa8c",
61
+ input: "#f8f8f2",
62
+ cursor: "#6272a4",
63
+ logo1: "#ff79c6",
64
+ logo2: "#bd93f9",
65
+ },
66
+ light: {
67
+ text: "#282a36",
68
+ textMuted: "#6272a4",
69
+ primary: "#7c3aed",
70
+ accent: "#db2777",
71
+ success: "#16a34a",
72
+ error: "#dc2626",
73
+ warning: "#ca8a04",
74
+ input: "#282a36",
75
+ cursor: "#6272a4",
76
+ logo1: "#db2777",
77
+ logo2: "#7c3aed",
78
+ },
79
+ }
80
+
81
+ const nord: ThemeDef = {
82
+ dark: {
83
+ text: "#eceff4",
84
+ textMuted: "#4c566a",
85
+ primary: "#88c0d0",
86
+ accent: "#81a1c1",
87
+ success: "#a3be8c",
88
+ error: "#bf616a",
89
+ warning: "#ebcb8b",
90
+ input: "#eceff4",
91
+ cursor: "#4c566a",
92
+ logo1: "#88c0d0",
93
+ logo2: "#81a1c1",
94
+ },
95
+ light: {
96
+ text: "#2e3440",
97
+ textMuted: "#4c566a",
98
+ primary: "#5e81ac",
99
+ accent: "#81a1c1",
100
+ success: "#a3be8c",
101
+ error: "#bf616a",
102
+ warning: "#d08770",
103
+ input: "#2e3440",
104
+ cursor: "#4c566a",
105
+ logo1: "#5e81ac",
106
+ logo2: "#81a1c1",
107
+ },
108
+ }
109
+
110
+ const tokyonight: ThemeDef = {
111
+ dark: {
112
+ text: "#c0caf5",
113
+ textMuted: "#565f89",
114
+ primary: "#7aa2f7",
115
+ accent: "#bb9af7",
116
+ success: "#9ece6a",
117
+ error: "#f7768e",
118
+ warning: "#e0af68",
119
+ input: "#c0caf5",
120
+ cursor: "#565f89",
121
+ logo1: "#bb9af7",
122
+ logo2: "#7aa2f7",
123
+ },
124
+ light: {
125
+ text: "#343b58",
126
+ textMuted: "#6172b0",
127
+ primary: "#2e7de9",
128
+ accent: "#9854f1",
129
+ success: "#587539",
130
+ error: "#f52a65",
131
+ warning: "#8c6c3e",
132
+ input: "#343b58",
133
+ cursor: "#6172b0",
134
+ logo1: "#9854f1",
135
+ logo2: "#2e7de9",
136
+ },
137
+ }
138
+
139
+ const monokai: ThemeDef = {
140
+ dark: {
141
+ text: "#f8f8f2",
142
+ textMuted: "#75715e",
143
+ primary: "#66d9ef",
144
+ accent: "#f92672",
145
+ success: "#a6e22e",
146
+ error: "#f92672",
147
+ warning: "#e6db74",
148
+ input: "#f8f8f2",
149
+ cursor: "#75715e",
150
+ logo1: "#f92672",
151
+ logo2: "#66d9ef",
152
+ },
153
+ light: {
154
+ text: "#272822",
155
+ textMuted: "#75715e",
156
+ primary: "#0089b3",
157
+ accent: "#c4265e",
158
+ success: "#718c00",
159
+ error: "#c4265e",
160
+ warning: "#c99e00",
161
+ input: "#272822",
162
+ cursor: "#75715e",
163
+ logo1: "#c4265e",
164
+ logo2: "#0089b3",
165
+ },
166
+ }
167
+
168
+ const github: ThemeDef = {
169
+ dark: {
170
+ text: "#c9d1d9",
171
+ textMuted: "#8b949e",
172
+ primary: "#58a6ff",
173
+ accent: "#bc8cff",
174
+ success: "#3fb950",
175
+ error: "#f85149",
176
+ warning: "#d29922",
177
+ input: "#c9d1d9",
178
+ cursor: "#8b949e",
179
+ logo1: "#58a6ff",
180
+ logo2: "#bc8cff",
181
+ },
182
+ light: {
183
+ text: "#24292f",
184
+ textMuted: "#57606a",
185
+ primary: "#0969da",
186
+ accent: "#8250df",
187
+ success: "#1a7f37",
188
+ error: "#cf222e",
189
+ warning: "#9a6700",
190
+ input: "#24292f",
191
+ cursor: "#57606a",
192
+ logo1: "#0969da",
193
+ logo2: "#8250df",
194
+ },
195
+ }
196
+
197
+ const solarized: ThemeDef = {
198
+ dark: {
199
+ text: "#839496",
200
+ textMuted: "#586e75",
201
+ primary: "#268bd2",
202
+ accent: "#d33682",
203
+ success: "#859900",
204
+ error: "#dc322f",
205
+ warning: "#b58900",
206
+ input: "#93a1a1",
207
+ cursor: "#586e75",
208
+ logo1: "#cb4b16",
209
+ logo2: "#268bd2",
210
+ },
211
+ light: {
212
+ text: "#657b83",
213
+ textMuted: "#93a1a1",
214
+ primary: "#268bd2",
215
+ accent: "#d33682",
216
+ success: "#859900",
217
+ error: "#dc322f",
218
+ warning: "#b58900",
219
+ input: "#586e75",
220
+ cursor: "#93a1a1",
221
+ logo1: "#cb4b16",
222
+ logo2: "#268bd2",
223
+ },
224
+ }
225
+
226
+ export const THEMES: Record<string, ThemeDef> = {
227
+ codeblog,
228
+ dracula,
229
+ nord,
230
+ tokyonight,
231
+ monokai,
232
+ github,
233
+ solarized,
234
+ }
235
+
236
+ export const THEME_NAMES = Object.keys(THEMES)
237
+
238
+ function detect(): "dark" | "light" {
239
+ const env = process.env.COLORFGBG
240
+ if (env) {
241
+ const parts = env.split(";")
242
+ const bg = parseInt(parts[parts.length - 1] || "0", 10)
243
+ if (bg > 6 && bg !== 8) return "light"
244
+ }
245
+ return "dark"
246
+ }
247
+
248
+ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
249
+ name: "Theme",
250
+ init: () => {
251
+ const [store, setStore] = createStore({
252
+ name: "codeblog" as string,
253
+ mode: detect() as "dark" | "light",
254
+ })
255
+
256
+ return {
257
+ get colors(): ThemeColors {
258
+ const def = THEMES[store.name] || THEMES.codeblog
259
+ return def[store.mode]
260
+ },
261
+ get name() { return store.name },
262
+ get mode() { return store.mode },
263
+ set(name: string) {
264
+ if (THEMES[name]) setStore("name", name)
265
+ },
266
+ toggle() {
267
+ setStore("mode", store.mode === "dark" ? "light" : "dark")
268
+ },
269
+ setMode(mode: "dark" | "light") {
270
+ setStore("mode", mode)
271
+ },
272
+ }
273
+ },
274
+ })
@@ -1,6 +1,7 @@
1
1
  import { createSignal, For, Show, onMount } from "solid-js"
2
2
  import { useKeyboard } from "@opentui/solid"
3
3
  import { useRoute } from "../context/route"
4
+ import { useTheme } from "../context/theme"
4
5
 
5
6
  interface Message {
6
7
  role: "user" | "assistant"
@@ -9,6 +10,7 @@ interface Message {
9
10
 
10
11
  export function Chat() {
11
12
  const route = useRoute()
13
+ const theme = useTheme()
12
14
  const [messages, setMessages] = createSignal<Message[]>([])
13
15
  const [streaming, setStreaming] = createSignal(false)
14
16
  const [streamText, setStreamText] = createSignal("")
@@ -164,12 +166,12 @@ export function Chat() {
164
166
  <box flexDirection="column" flexGrow={1}>
165
167
  {/* Header */}
166
168
  <box paddingLeft={2} paddingRight={2} paddingTop={1} flexShrink={0} flexDirection="row" gap={1}>
167
- <text fg="#0074cc">
169
+ <text fg={theme.colors.primary}>
168
170
  <span style={{ bold: true }}>AI Chat</span>
169
171
  </text>
170
- <text fg="#6a737c">{modelName()}</text>
172
+ <text fg={theme.colors.textMuted}>{modelName()}</text>
171
173
  <box flexGrow={1} />
172
- <text fg="#6a737c">esc:back · /help · /model · /clear</text>
174
+ <text fg={theme.colors.textMuted}>esc:back · /help · /model · /clear</text>
173
175
  </box>
174
176
 
175
177
  {/* Messages */}
@@ -177,31 +179,31 @@ export function Chat() {
177
179
  <For each={messages()}>
178
180
  {(msg) => (
179
181
  <box flexDirection="row" paddingBottom={1}>
180
- <text fg={msg.role === "user" ? "#0074cc" : "#48a868"}>
182
+ <text fg={msg.role === "user" ? theme.colors.primary : theme.colors.success}>
181
183
  <span style={{ bold: true }}>{msg.role === "user" ? "❯ " : "◆ "}</span>
182
184
  </text>
183
- <text fg="#e7e9eb">{msg.content}</text>
185
+ <text fg={theme.colors.text}>{msg.content}</text>
184
186
  </box>
185
187
  )}
186
188
  </For>
187
189
 
188
190
  <Show when={streaming()}>
189
191
  <box flexDirection="row" paddingBottom={1}>
190
- <text fg="#48a868">
192
+ <text fg={theme.colors.success}>
191
193
  <span style={{ bold: true }}>{"◆ "}</span>
192
194
  </text>
193
- <text fg="#a0a0a0">{streamText() || "thinking..."}</text>
195
+ <text fg={theme.colors.textMuted}>{streamText() || "thinking..."}</text>
194
196
  </box>
195
197
  </Show>
196
198
  </box>
197
199
 
198
200
  {/* Input */}
199
201
  <box paddingLeft={2} paddingRight={2} paddingBottom={1} flexShrink={0} flexDirection="row">
200
- <text fg="#0074cc">
202
+ <text fg={theme.colors.primary}>
201
203
  <span style={{ bold: true }}>{"❯ "}</span>
202
204
  </text>
203
- <text fg="#e7e9eb">{inputBuf()}</text>
204
- <text fg="#6a737c">{"█"}</text>
205
+ <text fg={theme.colors.input}>{inputBuf()}</text>
206
+ <text fg={theme.colors.cursor}>{"█"}</text>
205
207
  </box>
206
208
  </box>
207
209
  )
@@ -2,6 +2,7 @@ import { createSignal, Show } from "solid-js"
2
2
  import { useKeyboard } from "@opentui/solid"
3
3
  import { useRoute } from "../context/route"
4
4
  import { useExit } from "../context/exit"
5
+ import { useTheme, THEME_NAMES } from "../context/theme"
5
6
 
6
7
  const LOGO = [
7
8
  " ██████╗ ██████╗ ██████╗ ███████╗██████╗ ██╗ ██████╗ ██████╗ ",
@@ -26,6 +27,8 @@ const HELP_TEXT = [
26
27
  " /notifications View notifications",
27
28
  " /dashboard Your stats",
28
29
  " /models List available AI models",
30
+ " /theme [name] Switch theme (codeblog, dracula, nord, tokyonight, monokai, github, solarized)",
31
+ " /dark | /light Toggle dark/light mode",
29
32
  " /help Show this help",
30
33
  " /exit Exit",
31
34
  "",
@@ -41,6 +44,7 @@ export function Home(props: {
41
44
  }) {
42
45
  const route = useRoute()
43
46
  const exit = useExit()
47
+ const theme = useTheme()
44
48
  const [input, setInput] = createSignal("")
45
49
  const [message, setMessage] = createSignal("")
46
50
  const [messageColor, setMessageColor] = createSignal("#6a737c")
@@ -72,15 +76,42 @@ export function Home(props: {
72
76
  }
73
77
 
74
78
  if (cmd === "/login") {
75
- showMsg("Opening browser for login...", "#0074cc")
79
+ showMsg("Opening browser for login...", theme.colors.primary)
76
80
  await props.onLogin()
77
- showMsg("Logged in!", "#48a868")
81
+ showMsg("Logged in!", theme.colors.success)
82
+ return
83
+ }
84
+
85
+ if (cmd === "/theme") {
86
+ const name = parts[1]
87
+ if (!name) {
88
+ showMsg(`Theme: ${theme.name} (${theme.mode}) | Available: ${THEME_NAMES.join(", ")}`, theme.colors.text)
89
+ return
90
+ }
91
+ if (THEME_NAMES.includes(name)) {
92
+ theme.set(name)
93
+ showMsg(`Theme set to ${name}`, theme.colors.success)
94
+ } else {
95
+ showMsg(`Unknown theme: ${name}. Available: ${THEME_NAMES.join(", ")}`, theme.colors.error)
96
+ }
97
+ return
98
+ }
99
+
100
+ if (cmd === "/dark") {
101
+ theme.setMode("dark")
102
+ showMsg("Switched to dark mode", theme.colors.success)
103
+ return
104
+ }
105
+
106
+ if (cmd === "/light") {
107
+ theme.setMode("light")
108
+ showMsg("Switched to light mode", theme.colors.success)
78
109
  return
79
110
  }
80
111
 
81
112
  if (cmd === "/config") {
82
113
  if (parts[1] === "ai") {
83
- showMsg("Use CLI: codeblog config --provider anthropic --api-key sk-...", "#f48225")
114
+ showMsg("Use CLI: codeblog config --provider anthropic --api-key sk-...", theme.colors.warning)
84
115
  return
85
116
  }
86
117
  try {
@@ -89,73 +120,72 @@ export function Home(props: {
89
120
  const providers = cfg.providers || {}
90
121
  const keys = Object.keys(providers)
91
122
  const model = cfg.model || "claude-sonnet-4-20250514"
92
- showMsg(`Model: ${model} | Providers: ${keys.length > 0 ? keys.join(", ") : "none"} | URL: ${cfg.api_url || "https://codeblog.ai"}`, "#e7e9eb")
123
+ showMsg(`Model: ${model} | Providers: ${keys.length > 0 ? keys.join(", ") : "none"} | URL: ${cfg.api_url || "https://codeblog.ai"}`, theme.colors.text)
93
124
  } catch {
94
- showMsg("Failed to load config", "#d73a49")
125
+ showMsg("Failed to load config", theme.colors.error)
95
126
  }
96
127
  return
97
128
  }
98
129
 
99
130
  if (cmd === "/scan") {
100
- showMsg("Scanning IDE sessions...", "#0074cc")
131
+ showMsg("Scanning IDE sessions...", theme.colors.primary)
101
132
  try {
102
133
  const { registerAllScanners, scanAll } = await import("../../scanner")
103
134
  registerAllScanners()
104
135
  const sessions = scanAll(10)
105
136
  if (sessions.length === 0) {
106
- showMsg("No IDE sessions found.", "#f48225")
137
+ showMsg("No IDE sessions found.", theme.colors.warning)
107
138
  } else {
108
139
  const summary = sessions.slice(0, 3).map((s) => `[${s.source}] ${s.project}`).join(" | ")
109
- showMsg(`Found ${sessions.length} sessions: ${summary}`, "#48a868")
140
+ showMsg(`Found ${sessions.length} sessions: ${summary}`, theme.colors.success)
110
141
  }
111
142
  } catch (err) {
112
- showMsg(`Scan failed: ${err instanceof Error ? err.message : String(err)}`, "#d73a49")
143
+ showMsg(`Scan failed: ${err instanceof Error ? err.message : String(err)}`, theme.colors.error)
113
144
  }
114
145
  return
115
146
  }
116
147
 
117
148
  if (cmd === "/publish") {
118
- showMsg("Publishing sessions...", "#0074cc")
149
+ showMsg("Publishing sessions...", theme.colors.primary)
119
150
  try {
120
151
  const { Publisher } = await import("../../publisher")
121
152
  const results = await Publisher.scanAndPublish({ limit: 1 })
122
153
  const ok = results.filter((r) => r.postId)
123
154
  if (ok.length > 0) {
124
- showMsg(`Published ${ok.length} post(s)!`, "#48a868")
155
+ showMsg(`Published ${ok.length} post(s)!`, theme.colors.success)
125
156
  } else {
126
- showMsg("No sessions to publish.", "#f48225")
157
+ showMsg("No sessions to publish.", theme.colors.warning)
127
158
  }
128
159
  } catch (err) {
129
- showMsg(`Publish failed: ${err instanceof Error ? err.message : String(err)}`, "#d73a49")
160
+ showMsg(`Publish failed: ${err instanceof Error ? err.message : String(err)}`, theme.colors.error)
130
161
  }
131
162
  return
132
163
  }
133
164
 
134
165
  if (cmd === "/ai-publish") {
135
166
  if (!props.hasAI) {
136
- showMsg("No AI configured. Use: /config ai", "#d73a49")
167
+ showMsg("No AI configured. Use: /config ai", theme.colors.error)
137
168
  return
138
169
  }
139
- showMsg("AI is writing a post from your session...", "#0074cc")
140
- // Delegate to CLI command for now
141
- showMsg("Use CLI: codeblog ai-publish", "#f48225")
170
+ showMsg("AI is writing a post from your session...", theme.colors.primary)
171
+ showMsg("Use CLI: codeblog ai-publish", theme.colors.warning)
142
172
  return
143
173
  }
144
174
 
145
175
  if (cmd === "/feed") {
146
- showMsg("Loading feed...", "#0074cc")
176
+ showMsg("Loading feed...", theme.colors.primary)
147
177
  try {
148
178
  const { Feed } = await import("../../api/feed")
149
179
  const result = await Feed.list()
150
180
  const posts = (result as any).posts || []
151
181
  if (posts.length === 0) {
152
- showMsg("No posts yet.", "#f48225")
182
+ showMsg("No posts yet.", theme.colors.warning)
153
183
  } else {
154
184
  const summary = posts.slice(0, 3).map((p: any) => p.title?.slice(0, 40)).join(" | ")
155
- showMsg(`${posts.length} posts: ${summary}`, "#e7e9eb")
185
+ showMsg(`${posts.length} posts: ${summary}`, theme.colors.text)
156
186
  }
157
187
  } catch (err) {
158
- showMsg(`Feed failed: ${err instanceof Error ? err.message : String(err)}`, "#d73a49")
188
+ showMsg(`Feed failed: ${err instanceof Error ? err.message : String(err)}`, theme.colors.error)
159
189
  }
160
190
  return
161
191
  }
@@ -166,9 +196,9 @@ export function Home(props: {
166
196
  const models = await AIProvider.available()
167
197
  const configured = models.filter((m) => m.hasKey)
168
198
  const names = configured.map((m) => m.model.name).join(", ")
169
- showMsg(configured.length > 0 ? `Available: ${names}` : "No models configured. Use: codeblog config --provider anthropic --api-key sk-...", configured.length > 0 ? "#48a868" : "#f48225")
199
+ showMsg(configured.length > 0 ? `Available: ${names}` : "No models configured. Use: codeblog config --provider anthropic --api-key sk-...", configured.length > 0 ? theme.colors.success : theme.colors.warning)
170
200
  } catch (err) {
171
- showMsg(`Failed: ${err instanceof Error ? err.message : String(err)}`, "#d73a49")
201
+ showMsg(`Failed: ${err instanceof Error ? err.message : String(err)}`, theme.colors.error)
172
202
  }
173
203
  return
174
204
  }
@@ -176,32 +206,32 @@ export function Home(props: {
176
206
  if (cmd === "/search") {
177
207
  const query = parts.slice(1).join(" ")
178
208
  if (!query) {
179
- showMsg("Usage: /search <query>", "#f48225")
209
+ showMsg("Usage: /search <query>", theme.colors.warning)
180
210
  return
181
211
  }
182
212
  try {
183
213
  const { Posts } = await import("../../api/posts")
184
214
  const result = await Posts.search(query)
185
215
  const posts = (result as any).posts || []
186
- showMsg(posts.length > 0 ? `${posts.length} results for "${query}"` : `No results for "${query}"`, posts.length > 0 ? "#48a868" : "#f48225")
216
+ showMsg(posts.length > 0 ? `${posts.length} results for "${query}"` : `No results for "${query}"`, posts.length > 0 ? theme.colors.success : theme.colors.warning)
187
217
  } catch (err) {
188
- showMsg(`Search failed: ${err instanceof Error ? err.message : String(err)}`, "#d73a49")
218
+ showMsg(`Search failed: ${err instanceof Error ? err.message : String(err)}`, theme.colors.error)
189
219
  }
190
220
  return
191
221
  }
192
222
 
193
223
  if (cmd === "/trending" || cmd === "/notifications" || cmd === "/dashboard") {
194
- showMsg(`Use CLI: codeblog ${cmd.slice(1)}`, "#f48225")
224
+ showMsg(`Use CLI: codeblog ${cmd.slice(1)}`, theme.colors.warning)
195
225
  return
196
226
  }
197
227
 
198
- showMsg(`Unknown command: ${cmd}. Type /help`, "#d73a49")
228
+ showMsg(`Unknown command: ${cmd}. Type /help`, theme.colors.error)
199
229
  return
200
230
  }
201
231
 
202
232
  // Regular text → start AI chat
203
233
  if (!props.hasAI) {
204
- showMsg("No AI provider configured. Run: /config ai", "#d73a49")
234
+ showMsg("No AI provider configured. Run: /config ai", theme.colors.error)
205
235
  return
206
236
  }
207
237
 
@@ -242,28 +272,28 @@ export function Home(props: {
242
272
  {/* Logo */}
243
273
  <box flexShrink={0} flexDirection="column">
244
274
  {LOGO.map((line, i) => (
245
- <text fg={i < 4 ? "#f48225" : "#0074cc"}>{line}</text>
275
+ <text fg={i < 4 ? theme.colors.logo1 : theme.colors.logo2}>{line}</text>
246
276
  ))}
247
277
  </box>
248
278
 
249
279
  <box height={1} flexShrink={0}>
250
- <text fg="#6a737c">The AI-powered coding forum</text>
280
+ <text fg={theme.colors.textMuted}>The AI-powered coding forum</text>
251
281
  </box>
252
282
 
253
283
  {/* Status indicators */}
254
284
  <box height={2} flexShrink={0} flexDirection="column" paddingTop={1}>
255
285
  <box flexDirection="row" gap={2}>
256
286
  <Show when={!props.loggedIn}>
257
- <text fg="#d73a49">○ Not logged in — type /login</text>
287
+ <text fg={theme.colors.error}>○ Not logged in — type /login</text>
258
288
  </Show>
259
289
  <Show when={props.loggedIn}>
260
- <text fg="#48a868">● {props.username || "Logged in"}</text>
290
+ <text fg={theme.colors.success}>● {props.username || "Logged in"}</text>
261
291
  </Show>
262
292
  <Show when={!props.hasAI}>
263
- <text fg="#d73a49">○ No AI — type /config ai</text>
293
+ <text fg={theme.colors.error}>○ No AI — type /config ai</text>
264
294
  </Show>
265
295
  <Show when={props.hasAI}>
266
- <text fg="#48a868">● {props.aiProvider}</text>
296
+ <text fg={theme.colors.success}>● {props.aiProvider}</text>
267
297
  </Show>
268
298
  </box>
269
299
  </box>
@@ -271,11 +301,11 @@ export function Home(props: {
271
301
  {/* Input prompt */}
272
302
  <box width="100%" maxWidth={75} flexShrink={0} paddingTop={1}>
273
303
  <box flexDirection="row" width="100%">
274
- <text fg="#0074cc">
304
+ <text fg={theme.colors.primary}>
275
305
  <span style={{ bold: true }}>{"❯ "}</span>
276
306
  </text>
277
- <text fg="#e7e9eb">{input()}</text>
278
- <text fg="#6a737c">{"█"}</text>
307
+ <text fg={theme.colors.input}>{input()}</text>
308
+ <text fg={theme.colors.cursor}>{"█"}</text>
279
309
  </box>
280
310
  </box>
281
311
 
@@ -290,7 +320,7 @@ export function Home(props: {
290
320
  <Show when={showHelp()}>
291
321
  <box width="100%" maxWidth={75} paddingTop={1} flexShrink={0} flexDirection="column">
292
322
  {HELP_TEXT.map((line) => (
293
- <text fg={line.startsWith(" /") ? "#0074cc" : "#6a737c"}>{line}</text>
323
+ <text fg={line.startsWith(" /") ? theme.colors.primary : theme.colors.textMuted}>{line}</text>
294
324
  ))}
295
325
  </box>
296
326
  </Show>