codeblog-app 2.3.0 → 2.3.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.
Files changed (83) hide show
  1. package/drizzle/0000_init.sql +34 -0
  2. package/drizzle/meta/_journal.json +13 -0
  3. package/drizzle.config.ts +10 -0
  4. package/package.json +73 -8
  5. package/src/ai/__tests__/chat.test.ts +188 -0
  6. package/src/ai/__tests__/compat.test.ts +46 -0
  7. package/src/ai/__tests__/home.ai-stream.integration.test.ts +77 -0
  8. package/src/ai/__tests__/provider-registry.test.ts +61 -0
  9. package/src/ai/__tests__/provider.test.ts +238 -0
  10. package/src/ai/__tests__/stream-events.test.ts +152 -0
  11. package/src/ai/__tests__/tools.test.ts +93 -0
  12. package/src/ai/chat.ts +336 -0
  13. package/src/ai/configure.ts +143 -0
  14. package/src/ai/models.ts +26 -0
  15. package/src/ai/provider-registry.ts +150 -0
  16. package/src/ai/provider.ts +264 -0
  17. package/src/ai/stream-events.ts +64 -0
  18. package/src/ai/tools.ts +118 -0
  19. package/src/ai/types.ts +105 -0
  20. package/src/auth/index.ts +49 -0
  21. package/src/auth/oauth.ts +123 -0
  22. package/src/cli/__tests__/commands.test.ts +229 -0
  23. package/src/cli/cmd/agent.ts +97 -0
  24. package/src/cli/cmd/ai.ts +10 -0
  25. package/src/cli/cmd/chat.ts +190 -0
  26. package/src/cli/cmd/comment.ts +67 -0
  27. package/src/cli/cmd/config.ts +153 -0
  28. package/src/cli/cmd/feed.ts +53 -0
  29. package/src/cli/cmd/forum.ts +106 -0
  30. package/src/cli/cmd/login.ts +45 -0
  31. package/src/cli/cmd/logout.ts +12 -0
  32. package/src/cli/cmd/me.ts +188 -0
  33. package/src/cli/cmd/post.ts +25 -0
  34. package/src/cli/cmd/publish.ts +64 -0
  35. package/src/cli/cmd/scan.ts +78 -0
  36. package/src/cli/cmd/search.ts +35 -0
  37. package/src/cli/cmd/setup.ts +622 -0
  38. package/src/cli/cmd/tui.ts +20 -0
  39. package/src/cli/cmd/uninstall.ts +281 -0
  40. package/src/cli/cmd/update.ts +123 -0
  41. package/src/cli/cmd/vote.ts +50 -0
  42. package/src/cli/cmd/whoami.ts +18 -0
  43. package/src/cli/mcp-print.ts +6 -0
  44. package/src/cli/ui.ts +357 -0
  45. package/src/config/index.ts +92 -0
  46. package/src/flag/index.ts +23 -0
  47. package/src/global/index.ts +38 -0
  48. package/src/id/index.ts +20 -0
  49. package/src/index.ts +203 -0
  50. package/src/mcp/__tests__/client.test.ts +149 -0
  51. package/src/mcp/__tests__/e2e.ts +331 -0
  52. package/src/mcp/__tests__/integration.ts +148 -0
  53. package/src/mcp/client.ts +118 -0
  54. package/src/server/index.ts +48 -0
  55. package/src/storage/chat.ts +73 -0
  56. package/src/storage/db.ts +85 -0
  57. package/src/storage/schema.sql.ts +39 -0
  58. package/src/storage/schema.ts +1 -0
  59. package/src/tui/__tests__/input-intent.test.ts +27 -0
  60. package/src/tui/__tests__/stream-assembler.test.ts +33 -0
  61. package/src/tui/ai-stream.ts +28 -0
  62. package/src/tui/app.tsx +210 -0
  63. package/src/tui/commands.ts +220 -0
  64. package/src/tui/context/exit.tsx +15 -0
  65. package/src/tui/context/helper.tsx +25 -0
  66. package/src/tui/context/route.tsx +24 -0
  67. package/src/tui/context/theme.tsx +471 -0
  68. package/src/tui/input-intent.ts +26 -0
  69. package/src/tui/routes/home.tsx +1060 -0
  70. package/src/tui/routes/model.tsx +210 -0
  71. package/src/tui/routes/notifications.tsx +87 -0
  72. package/src/tui/routes/post.tsx +102 -0
  73. package/src/tui/routes/search.tsx +105 -0
  74. package/src/tui/routes/setup.tsx +267 -0
  75. package/src/tui/routes/trending.tsx +107 -0
  76. package/src/tui/stream-assembler.ts +49 -0
  77. package/src/util/__tests__/context.test.ts +31 -0
  78. package/src/util/__tests__/lazy.test.ts +37 -0
  79. package/src/util/context.ts +23 -0
  80. package/src/util/error.ts +46 -0
  81. package/src/util/lazy.ts +18 -0
  82. package/src/util/log.ts +144 -0
  83. package/tsconfig.json +11 -0
package/src/cli/ui.ts ADDED
@@ -0,0 +1,357 @@
1
+ import { EOL } from "os"
2
+
3
+ export namespace UI {
4
+ export const Style = {
5
+ TEXT_HIGHLIGHT: "\x1b[96m",
6
+ TEXT_HIGHLIGHT_BOLD: "\x1b[96m\x1b[1m",
7
+ TEXT_DIM: "\x1b[90m",
8
+ TEXT_DIM_BOLD: "\x1b[90m\x1b[1m",
9
+ TEXT_NORMAL: "\x1b[0m",
10
+ TEXT_NORMAL_BOLD: "\x1b[1m",
11
+ TEXT_WARNING: "\x1b[93m",
12
+ TEXT_WARNING_BOLD: "\x1b[93m\x1b[1m",
13
+ TEXT_DANGER: "\x1b[91m",
14
+ TEXT_DANGER_BOLD: "\x1b[91m\x1b[1m",
15
+ TEXT_SUCCESS: "\x1b[92m",
16
+ TEXT_SUCCESS_BOLD: "\x1b[92m\x1b[1m",
17
+ TEXT_INFO: "\x1b[94m",
18
+ TEXT_INFO_BOLD: "\x1b[94m\x1b[1m",
19
+ }
20
+
21
+ export function println(...message: string[]) {
22
+ print(...message)
23
+ Bun.stderr.write(EOL)
24
+ }
25
+
26
+ export function print(...message: string[]) {
27
+ Bun.stderr.write(message.join(" "))
28
+ }
29
+
30
+ export function logo() {
31
+ const orange = "\x1b[38;5;214m"
32
+ const cyan = "\x1b[36m"
33
+ const reset = "\x1b[0m"
34
+ return [
35
+ "",
36
+ `${orange} ██████╗ ██████╗ ██████╗ ███████╗${cyan}██████╗ ██╗ ██████╗ ██████╗ ${reset}`,
37
+ `${orange} ██╔════╝██╔═══██╗██╔══██╗██╔════╝${cyan}██╔══██╗██║ ██╔═══██╗██╔════╝ ${reset}`,
38
+ `${orange} ██║ ██║ ██║██║ ██║█████╗ ${cyan}██████╔╝██║ ██║ ██║██║ ███╗${reset}`,
39
+ `${orange} ██║ ██║ ██║██║ ██║██╔══╝ ${cyan}██╔══██╗██║ ██║ ██║██║ ██║${reset}`,
40
+ `${orange} ╚██████╗╚██████╔╝██████╔╝███████╗${cyan}██████╔╝███████╗╚██████╔╝╚██████╔╝${reset}`,
41
+ `${orange} ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝${cyan}╚═════╝ ╚══════╝ ╚═════╝ ╚═════╝ ${reset}`,
42
+ "",
43
+ ` ${Style.TEXT_DIM}The AI-powered coding forum in your terminal${Style.TEXT_NORMAL}`,
44
+ "",
45
+ ].join(EOL)
46
+ }
47
+
48
+ export function error(message: string) {
49
+ println(Style.TEXT_DANGER_BOLD + "Error: " + Style.TEXT_NORMAL + message)
50
+ }
51
+
52
+ export function success(message: string) {
53
+ println(Style.TEXT_SUCCESS_BOLD + "✓ " + Style.TEXT_NORMAL + message)
54
+ }
55
+
56
+ export function info(message: string) {
57
+ println(Style.TEXT_INFO + "ℹ " + Style.TEXT_NORMAL + message)
58
+ }
59
+
60
+ export function warn(message: string) {
61
+ println(Style.TEXT_WARNING + "⚠ " + Style.TEXT_NORMAL + message)
62
+ }
63
+
64
+ export async function input(prompt: string): Promise<string> {
65
+ const readline = require("readline")
66
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
67
+ return new Promise((resolve) => {
68
+ rl.question(prompt, (answer: string) => {
69
+ rl.close()
70
+ resolve(answer.trim())
71
+ })
72
+ })
73
+ }
74
+
75
+ /**
76
+ * Input with ESC support. Returns null if user presses Escape, otherwise the input string.
77
+ */
78
+ export async function inputWithEscape(prompt: string): Promise<string | null> {
79
+ const stdin = process.stdin
80
+ process.stderr.write(prompt)
81
+
82
+ return new Promise((resolve) => {
83
+ const wasRaw = stdin.isRaw
84
+ if (stdin.isTTY && stdin.setRawMode) stdin.setRawMode(true)
85
+
86
+ let buf = ""
87
+ let paste = false
88
+ let onData: ((ch: Buffer) => void) = () => {}
89
+
90
+ const restore = () => {
91
+ if (stdin.isTTY && stdin.setRawMode) stdin.setRawMode(wasRaw ?? false)
92
+ stdin.removeListener("data", onData)
93
+ }
94
+
95
+ const readClipboard = () => {
96
+ if (process.platform !== "darwin") return ""
97
+ try {
98
+ const out = Bun.spawnSync(["pbpaste"])
99
+ if (out.exitCode !== 0) return ""
100
+ return out.stdout.toString().replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\n/g, "")
101
+ } catch {
102
+ return ""
103
+ }
104
+ }
105
+
106
+ const append = (text: string) => {
107
+ if (!text) return
108
+ buf += text
109
+ process.stderr.write(text)
110
+ }
111
+
112
+ onData = (ch: Buffer) => {
113
+ const c = ch.toString("utf8")
114
+ if (c === "\u0003") {
115
+ // Ctrl+C
116
+ restore()
117
+ process.exit(130)
118
+ }
119
+ if (!paste && c === "\x1b") {
120
+ // Escape
121
+ restore()
122
+ process.stderr.write("\n")
123
+ resolve(null)
124
+ return
125
+ }
126
+ if (c === "\x16" || c === "\x1bv") {
127
+ const clip = readClipboard()
128
+ if (clip) append(clip)
129
+ return
130
+ }
131
+
132
+ let text = c
133
+ if (text.includes("\x1b[200~")) {
134
+ paste = true
135
+ text = text.replace(/\x1b\[200~/g, "")
136
+ }
137
+ if (text.includes("\x1b[201~")) {
138
+ paste = false
139
+ text = text.replace(/\x1b\[201~/g, "")
140
+ }
141
+ text = text.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "").replace(/\x1b./g, "")
142
+
143
+ if (!paste && (text === "\r" || text === "\n")) {
144
+ // Enter
145
+ restore()
146
+ process.stderr.write("\n")
147
+ resolve(buf)
148
+ return
149
+ }
150
+ if (!paste && (text === "\u007f" || text === "\b")) {
151
+ // Backspace
152
+ if (buf.length > 0) {
153
+ buf = buf.slice(0, -1)
154
+ process.stderr.write("\b \b")
155
+ }
156
+ return
157
+ }
158
+ // Regular character
159
+ const clean = text.replace(/[\x00-\x08\x0b-\x1f\x7f]/g, "")
160
+ append(clean)
161
+ }
162
+ stdin.on("data", onData)
163
+ })
164
+ }
165
+
166
+ export async function password(prompt: string): Promise<string> {
167
+ const readline = require("readline")
168
+ const rl = readline.createInterface({ input: process.stdin, output: process.stderr, terminal: true })
169
+ return new Promise((resolve) => {
170
+ // Disable echoing by writing the prompt manually and muting stdout
171
+ process.stderr.write(prompt)
172
+ const stdin = process.stdin
173
+ const wasRaw = stdin.isRaw
174
+ if (stdin.isTTY && stdin.setRawMode) stdin.setRawMode(true)
175
+
176
+ let buf = ""
177
+ const onData = (ch: Buffer) => {
178
+ const c = ch.toString("utf8")
179
+ if (c === "\n" || c === "\r" || c === "\u0004") {
180
+ if (stdin.isTTY && stdin.setRawMode) stdin.setRawMode(wasRaw ?? false)
181
+ stdin.removeListener("data", onData)
182
+ process.stderr.write("\n")
183
+ rl.close()
184
+ resolve(buf.trim())
185
+ } else if (c === "\u007f" || c === "\b") {
186
+ // backspace
187
+ if (buf.length > 0) buf = buf.slice(0, -1)
188
+ } else if (c === "\u0003") {
189
+ // Ctrl+C
190
+ if (stdin.isTTY && stdin.setRawMode) stdin.setRawMode(wasRaw ?? false)
191
+ stdin.removeListener("data", onData)
192
+ rl.close()
193
+ process.exit(130)
194
+ } else {
195
+ buf += c
196
+ process.stderr.write("*")
197
+ }
198
+ }
199
+ stdin.on("data", onData)
200
+ })
201
+ }
202
+
203
+ export async function select(prompt: string, options: string[]): Promise<number> {
204
+ if (options.length === 0) return 0
205
+
206
+ const stdin = process.stdin
207
+ const wasRaw = stdin.isRaw
208
+ if (stdin.isTTY && stdin.setRawMode) stdin.setRawMode(true)
209
+ process.stderr.write("\x1b[?25l")
210
+
211
+ let idx = 0
212
+ let drawnRows = 0
213
+ const maxRows = 12
214
+ let onData: ((ch: Buffer) => void) = () => {}
215
+
216
+ const stripAnsi = (text: string) => text.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "").replace(/\x1b./g, "")
217
+ const rowCount = (line: string) => {
218
+ const cols = Math.max(20, process.stderr.columns || 80)
219
+ const len = Array.from(stripAnsi(line)).length
220
+ return Math.max(1, Math.ceil((len || 1) / cols))
221
+ }
222
+
223
+ const draw = () => {
224
+ if (drawnRows > 1) process.stderr.write(`\x1b[${drawnRows - 1}F`)
225
+ process.stderr.write("\x1b[J")
226
+
227
+ const start = options.length <= maxRows ? 0 : Math.max(0, Math.min(idx - 4, options.length - maxRows))
228
+ const items = options.slice(start, start + maxRows)
229
+ const lines = [
230
+ prompt,
231
+ ...items.map((item, i) => {
232
+ const active = start + i === idx
233
+ const marker = active ? `${Style.TEXT_HIGHLIGHT}●${Style.TEXT_NORMAL}` : "○"
234
+ const text = active ? `${Style.TEXT_NORMAL_BOLD}${item}${Style.TEXT_NORMAL}` : item
235
+ return ` ${marker} ${text}`
236
+ }),
237
+ options.length > maxRows
238
+ ? ` ${Style.TEXT_DIM}${start > 0 ? "↑ more " : ""}${start + maxRows < options.length ? "↓ more" : ""}${Style.TEXT_NORMAL}`
239
+ : ` ${Style.TEXT_DIM}${Style.TEXT_NORMAL}`,
240
+ ` ${Style.TEXT_DIM}Use ↑/↓ then Enter (Esc to cancel)${Style.TEXT_NORMAL}`,
241
+ ]
242
+ process.stderr.write(lines.map((line) => `\x1b[2K\r${line}`).join("\n"))
243
+ drawnRows = lines.reduce((sum, line) => sum + rowCount(line), 0)
244
+ }
245
+
246
+ const restore = () => {
247
+ process.stderr.write("\x1b[?25h")
248
+ if (stdin.isTTY && stdin.setRawMode) stdin.setRawMode(wasRaw ?? false)
249
+ stdin.removeListener("data", onData)
250
+ process.stderr.write("\n")
251
+ }
252
+
253
+ draw()
254
+
255
+ return new Promise((resolve) => {
256
+ onData = (ch: Buffer) => {
257
+ const c = ch.toString("utf8")
258
+ if (c === "\u0003") {
259
+ restore()
260
+ process.exit(130)
261
+ }
262
+ if (c === "\r" || c === "\n") {
263
+ restore()
264
+ resolve(idx)
265
+ return
266
+ }
267
+ if (c === "\x1b") {
268
+ restore()
269
+ resolve(-1)
270
+ return
271
+ }
272
+ if (c === "k" || c.includes("\x1b[A") || c.includes("\x1bOA")) {
273
+ idx = (idx - 1 + options.length) % options.length
274
+ draw()
275
+ return
276
+ }
277
+ if (c === "j" || c.includes("\x1b[B") || c.includes("\x1bOB")) {
278
+ idx = (idx + 1) % options.length
279
+ draw()
280
+ return
281
+ }
282
+ }
283
+ stdin.on("data", onData)
284
+ })
285
+ }
286
+
287
+ export async function waitKey(prompt: string, keys: string[]): Promise<string> {
288
+ const stdin = process.stdin
289
+ process.stderr.write(` ${Style.TEXT_DIM}${prompt}${Style.TEXT_NORMAL}`)
290
+
291
+ return new Promise((resolve) => {
292
+ const wasRaw = stdin.isRaw
293
+ if (stdin.isTTY && stdin.setRawMode) stdin.setRawMode(true)
294
+
295
+ const onData = (ch: Buffer) => {
296
+ const c = ch.toString("utf8")
297
+ if (c === "\u0003") {
298
+ // Ctrl+C
299
+ if (stdin.isTTY && stdin.setRawMode) stdin.setRawMode(wasRaw ?? false)
300
+ stdin.removeListener("data", onData)
301
+ process.exit(130)
302
+ }
303
+ const key = (c === "\r" || c === "\n") ? "enter" : c === "\x1b" ? "escape" : c.toLowerCase()
304
+ if (keys.includes(key)) {
305
+ if (stdin.isTTY && stdin.setRawMode) stdin.setRawMode(wasRaw ?? false)
306
+ stdin.removeListener("data", onData)
307
+ process.stderr.write("\n")
308
+ resolve(key)
309
+ }
310
+ }
311
+ stdin.on("data", onData)
312
+ })
313
+ }
314
+
315
+ /**
316
+ * Wait for Enter key (or Esc to skip). Returns "enter" or "escape".
317
+ */
318
+ export async function waitEnter(prompt?: string): Promise<"enter" | "escape"> {
319
+ return waitKey(prompt || "Press Enter to continue...", ["enter", "escape"]) as Promise<"enter" | "escape">
320
+ }
321
+
322
+ /**
323
+ * Streaming typewriter effect — prints text character by character to stderr.
324
+ */
325
+ export async function typeText(text: string, opts?: { charDelay?: number; prefix?: string }) {
326
+ const delay = opts?.charDelay ?? 12
327
+ const prefix = opts?.prefix ?? " "
328
+ Bun.stderr.write(prefix)
329
+ for (const ch of text) {
330
+ Bun.stderr.write(ch)
331
+ if (delay > 0) await Bun.sleep(delay)
332
+ }
333
+ Bun.stderr.write(EOL)
334
+ }
335
+
336
+ /**
337
+ * Clean markdown formatting from MCP tool output for CLI display.
338
+ * Removes **bold**, *italic*, keeps structure readable.
339
+ */
340
+ export function cleanMarkdown(text: string): string {
341
+ return text
342
+ .replace(/\*\*(.+?)\*\*/g, "$1") // **bold** → bold
343
+ .replace(/\*(.+?)\*/g, "$1") // *italic* → italic
344
+ .replace(/`([^`]+)`/g, "$1") // `code` → code
345
+ .replace(/^#{1,6}\s+/gm, "") // ### heading → heading
346
+ .replace(/^---+$/gm, "──────────────────────────────────") // horizontal rule
347
+ }
348
+
349
+ /**
350
+ * Print a horizontal divider.
351
+ */
352
+ export function divider() {
353
+ println("")
354
+ println(` ${Style.TEXT_DIM}──────────────────────────────────${Style.TEXT_NORMAL}`)
355
+ println("")
356
+ }
357
+ }
@@ -0,0 +1,92 @@
1
+ import path from "path"
2
+ import { chmod, writeFile } from "fs/promises"
3
+ import { Global } from "../global"
4
+
5
+ const CONFIG_FILE = path.join(Global.Path.config, "config.json")
6
+
7
+ export namespace Config {
8
+ export type ModelApi = "anthropic" | "openai" | "google" | "openai-compatible"
9
+ export type CompatProfile = "anthropic" | "openai" | "openai-compatible" | "google"
10
+
11
+ export interface FeatureFlags {
12
+ ai_provider_registry_v2?: boolean
13
+ ai_onboarding_wizard_v2?: boolean
14
+ }
15
+
16
+ export interface ProviderConfig {
17
+ api_key: string
18
+ base_url?: string
19
+ api?: ModelApi
20
+ compat_profile?: CompatProfile
21
+ }
22
+
23
+ export interface CodeblogConfig {
24
+ api_url: string
25
+ api_key?: string
26
+ token?: string
27
+ model?: string
28
+ default_provider?: string
29
+ default_language?: string
30
+ activeAgent?: string
31
+ providers?: Record<string, ProviderConfig>
32
+ feature_flags?: FeatureFlags
33
+ }
34
+
35
+ const defaults: CodeblogConfig = {
36
+ api_url: "https://codeblog.ai",
37
+ }
38
+
39
+ export const filepath = CONFIG_FILE
40
+
41
+ const FEATURE_FLAG_ENV: Record<keyof FeatureFlags, string> = {
42
+ ai_provider_registry_v2: "CODEBLOG_AI_PROVIDER_REGISTRY_V2",
43
+ ai_onboarding_wizard_v2: "CODEBLOG_AI_ONBOARDING_WIZARD_V2",
44
+ }
45
+
46
+ export async function load(): Promise<CodeblogConfig> {
47
+ const file = Bun.file(CONFIG_FILE)
48
+ const data = await file.json().catch(() => ({}))
49
+ return { ...defaults, ...data }
50
+ }
51
+
52
+ export async function save(config: Partial<CodeblogConfig>) {
53
+ const current = await load()
54
+ const merged = { ...current, ...config }
55
+ await writeFile(CONFIG_FILE, JSON.stringify(merged, null, 2))
56
+ await chmod(CONFIG_FILE, 0o600).catch(() => {})
57
+ }
58
+
59
+ export async function url() {
60
+ return process.env.CODEBLOG_URL || (await load()).api_url || "https://codeblog.ai"
61
+ }
62
+
63
+ export async function key() {
64
+ return process.env.CODEBLOG_API_KEY || (await load()).api_key || ""
65
+ }
66
+
67
+ export async function token() {
68
+ return process.env.CODEBLOG_TOKEN || (await load()).token || ""
69
+ }
70
+
71
+ export async function language() {
72
+ return process.env.CODEBLOG_LANGUAGE || (await load()).default_language
73
+ }
74
+
75
+ function parseBool(raw: string | undefined): boolean | undefined {
76
+ if (!raw) return undefined
77
+ const v = raw.trim().toLowerCase()
78
+ if (["1", "true", "yes", "on"].includes(v)) return true
79
+ if (["0", "false", "no", "off"].includes(v)) return false
80
+ return undefined
81
+ }
82
+
83
+ export function envFlagName(flag: keyof FeatureFlags): string {
84
+ return FEATURE_FLAG_ENV[flag]
85
+ }
86
+
87
+ export async function featureEnabled(flag: keyof FeatureFlags): Promise<boolean> {
88
+ const env = parseBool(process.env[FEATURE_FLAG_ENV[flag]])
89
+ if (env !== undefined) return env
90
+ return !!(await load()).feature_flags?.[flag]
91
+ }
92
+ }
@@ -0,0 +1,23 @@
1
+ function truthy(key: string) {
2
+ const value = process.env[key]?.toLowerCase()
3
+ return value === "true" || value === "1"
4
+ }
5
+
6
+ export namespace Flag {
7
+ export const CODEBLOG_URL = process.env["CODEBLOG_URL"]
8
+ export const CODEBLOG_API_KEY = process.env["CODEBLOG_API_KEY"]
9
+ export const CODEBLOG_TOKEN = process.env["CODEBLOG_TOKEN"]
10
+ export const CODEBLOG_DEBUG = truthy("CODEBLOG_DEBUG")
11
+ export const CODEBLOG_DISABLE_AUTOUPDATE = truthy("CODEBLOG_DISABLE_AUTOUPDATE")
12
+ export const CODEBLOG_DISABLE_SCANNER_CACHE = truthy("CODEBLOG_DISABLE_SCANNER_CACHE")
13
+ export const CODEBLOG_TEST_HOME = process.env["CODEBLOG_TEST_HOME"]
14
+ export declare const CODEBLOG_CLIENT: string
15
+ }
16
+
17
+ Object.defineProperty(Flag, "CODEBLOG_CLIENT", {
18
+ get() {
19
+ return process.env["CODEBLOG_CLIENT"] ?? "cli"
20
+ },
21
+ enumerable: true,
22
+ configurable: false,
23
+ })
@@ -0,0 +1,38 @@
1
+ import fs from "fs/promises"
2
+ import { xdgData, xdgCache, xdgConfig, xdgState } from "xdg-basedir"
3
+ import path from "path"
4
+ import os from "os"
5
+
6
+ const app = "codeblog"
7
+
8
+ const home = process.env.CODEBLOG_TEST_HOME || os.homedir()
9
+ const win = os.platform() === "win32"
10
+ const appdata = process.env.APPDATA || path.join(home, "AppData", "Roaming")
11
+ const localappdata = process.env.LOCALAPPDATA || path.join(home, "AppData", "Local")
12
+
13
+ const data = win ? path.join(localappdata, app) : path.join(xdgData || path.join(home, ".local", "share"), app)
14
+ const cache = win ? path.join(localappdata, app, "cache") : path.join(xdgCache || path.join(home, ".cache"), app)
15
+ const config = win ? path.join(appdata, app) : path.join(xdgConfig || path.join(home, ".config"), app)
16
+ const state = win ? path.join(localappdata, app, "state") : path.join(xdgState || path.join(home, ".local", "state"), app)
17
+
18
+ export namespace Global {
19
+ export const Path = {
20
+ get home() {
21
+ return process.env.CODEBLOG_TEST_HOME || os.homedir()
22
+ },
23
+ data,
24
+ bin: path.join(data, "bin"),
25
+ log: path.join(data, "log"),
26
+ cache,
27
+ config,
28
+ state,
29
+ }
30
+ }
31
+
32
+ await Promise.all([
33
+ fs.mkdir(Global.Path.data, { recursive: true }),
34
+ fs.mkdir(Global.Path.config, { recursive: true }),
35
+ fs.mkdir(Global.Path.state, { recursive: true }),
36
+ fs.mkdir(Global.Path.log, { recursive: true }),
37
+ fs.mkdir(Global.Path.bin, { recursive: true }),
38
+ ])
@@ -0,0 +1,20 @@
1
+ import { randomBytes } from "crypto"
2
+
3
+ export namespace ID {
4
+ export function generate(prefix = ""): string {
5
+ const bytes = randomBytes(12).toString("hex")
6
+ return prefix ? `${prefix}_${bytes}` : bytes
7
+ }
8
+
9
+ export function short(): string {
10
+ return randomBytes(6).toString("hex")
11
+ }
12
+
13
+ export function uuid(): string {
14
+ return crypto.randomUUID()
15
+ }
16
+
17
+ export function timestamp(): string {
18
+ return `${Date.now()}-${randomBytes(4).toString("hex")}`
19
+ }
20
+ }