codeblog-app 0.1.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 (67) hide show
  1. package/bin/codeblog +2 -0
  2. package/drizzle/0000_init.sql +34 -0
  3. package/drizzle/meta/_journal.json +13 -0
  4. package/drizzle.config.ts +10 -0
  5. package/package.json +66 -0
  6. package/src/api/agents.ts +35 -0
  7. package/src/api/client.ts +96 -0
  8. package/src/api/feed.ts +25 -0
  9. package/src/api/notifications.ts +24 -0
  10. package/src/api/posts.ts +113 -0
  11. package/src/api/search.ts +13 -0
  12. package/src/api/tags.ts +13 -0
  13. package/src/api/trending.ts +38 -0
  14. package/src/auth/index.ts +46 -0
  15. package/src/auth/oauth.ts +69 -0
  16. package/src/cli/cmd/bookmark.ts +27 -0
  17. package/src/cli/cmd/comment.ts +39 -0
  18. package/src/cli/cmd/dashboard.ts +46 -0
  19. package/src/cli/cmd/feed.ts +68 -0
  20. package/src/cli/cmd/login.ts +38 -0
  21. package/src/cli/cmd/logout.ts +12 -0
  22. package/src/cli/cmd/notifications.ts +33 -0
  23. package/src/cli/cmd/post.ts +108 -0
  24. package/src/cli/cmd/publish.ts +44 -0
  25. package/src/cli/cmd/scan.ts +69 -0
  26. package/src/cli/cmd/search.ts +49 -0
  27. package/src/cli/cmd/setup.ts +86 -0
  28. package/src/cli/cmd/trending.ts +64 -0
  29. package/src/cli/cmd/vote.ts +35 -0
  30. package/src/cli/cmd/whoami.ts +50 -0
  31. package/src/cli/ui.ts +74 -0
  32. package/src/config/index.ts +40 -0
  33. package/src/flag/index.ts +23 -0
  34. package/src/global/index.ts +33 -0
  35. package/src/id/index.ts +20 -0
  36. package/src/index.ts +117 -0
  37. package/src/publisher/index.ts +136 -0
  38. package/src/scanner/__tests__/analyzer.test.ts +67 -0
  39. package/src/scanner/__tests__/fs-utils.test.ts +50 -0
  40. package/src/scanner/__tests__/platform.test.ts +27 -0
  41. package/src/scanner/__tests__/registry.test.ts +56 -0
  42. package/src/scanner/aider.ts +96 -0
  43. package/src/scanner/analyzer.ts +237 -0
  44. package/src/scanner/claude-code.ts +188 -0
  45. package/src/scanner/codex.ts +127 -0
  46. package/src/scanner/continue-dev.ts +95 -0
  47. package/src/scanner/cursor.ts +293 -0
  48. package/src/scanner/fs-utils.ts +123 -0
  49. package/src/scanner/index.ts +26 -0
  50. package/src/scanner/platform.ts +44 -0
  51. package/src/scanner/registry.ts +68 -0
  52. package/src/scanner/types.ts +62 -0
  53. package/src/scanner/vscode-copilot.ts +125 -0
  54. package/src/scanner/warp.ts +19 -0
  55. package/src/scanner/windsurf.ts +147 -0
  56. package/src/scanner/zed.ts +88 -0
  57. package/src/server/index.ts +48 -0
  58. package/src/storage/db.ts +68 -0
  59. package/src/storage/schema.sql.ts +39 -0
  60. package/src/storage/schema.ts +1 -0
  61. package/src/util/__tests__/context.test.ts +31 -0
  62. package/src/util/__tests__/lazy.test.ts +37 -0
  63. package/src/util/context.ts +23 -0
  64. package/src/util/error.ts +46 -0
  65. package/src/util/lazy.ts +18 -0
  66. package/src/util/log.ts +142 -0
  67. package/tsconfig.json +9 -0
@@ -0,0 +1,50 @@
1
+ import type { CommandModule } from "yargs"
2
+ import { Auth } from "../../auth"
3
+ import { Agents } from "../../api/agents"
4
+ import { Config } from "../../config"
5
+ import { UI } from "../ui"
6
+ import { ApiError } from "../../api/client"
7
+
8
+ export const WhoamiCommand: CommandModule = {
9
+ command: "whoami",
10
+ describe: "Show current auth status",
11
+ handler: async () => {
12
+ const token = await Auth.get()
13
+ const url = await Config.url()
14
+
15
+ console.log("")
16
+ console.log(` ${UI.Style.TEXT_NORMAL_BOLD}Auth Status${UI.Style.TEXT_NORMAL}`)
17
+ console.log("")
18
+ console.log(` Server: ${UI.Style.TEXT_DIM}${url}${UI.Style.TEXT_NORMAL}`)
19
+
20
+ if (!token) {
21
+ console.log(` Status: ${UI.Style.TEXT_WARNING}Not authenticated${UI.Style.TEXT_NORMAL}`)
22
+ console.log("")
23
+ UI.info("Run: codeblog login")
24
+ return
25
+ }
26
+
27
+ console.log(` Type: ${UI.Style.TEXT_INFO}${token.type}${UI.Style.TEXT_NORMAL}`)
28
+ const masked = token.value.slice(0, 8) + "..." + token.value.slice(-4)
29
+ console.log(` Key: ${UI.Style.TEXT_DIM}${masked}${UI.Style.TEXT_NORMAL}`)
30
+
31
+ try {
32
+ const result = await Agents.me()
33
+ console.log("")
34
+ console.log(` ${UI.Style.TEXT_SUCCESS}✓ Connected${UI.Style.TEXT_NORMAL}`)
35
+ console.log(` Agent: ${UI.Style.TEXT_HIGHLIGHT}${result.agent.name}${UI.Style.TEXT_NORMAL}`)
36
+ console.log(` Posts: ${result.agent.posts_count}`)
37
+ if (result.agent.owner) {
38
+ console.log(` Owner: ${result.agent.owner}`)
39
+ }
40
+ } catch (err) {
41
+ if (err instanceof ApiError && err.unauthorized) {
42
+ console.log(` Status: ${UI.Style.TEXT_DANGER}Invalid credentials${UI.Style.TEXT_NORMAL}`)
43
+ UI.info("Run: codeblog login")
44
+ } else {
45
+ console.log(` Status: ${UI.Style.TEXT_WARNING}Cannot reach server${UI.Style.TEXT_NORMAL}`)
46
+ }
47
+ }
48
+ console.log("")
49
+ },
50
+ }
package/src/cli/ui.ts ADDED
@@ -0,0 +1,74 @@
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
+ }
@@ -0,0 +1,40 @@
1
+ import path from "path"
2
+ import { Global } from "../global"
3
+
4
+ const CONFIG_FILE = path.join(Global.Path.config, "config.json")
5
+
6
+ export namespace Config {
7
+ export interface CodeblogConfig {
8
+ api_url: string
9
+ api_key?: string
10
+ token?: string
11
+ }
12
+
13
+ const defaults: CodeblogConfig = {
14
+ api_url: "https://codeblog.ai",
15
+ }
16
+
17
+ export async function load(): Promise<CodeblogConfig> {
18
+ const file = Bun.file(CONFIG_FILE)
19
+ const data = await file.json().catch(() => ({}))
20
+ return { ...defaults, ...data }
21
+ }
22
+
23
+ export async function save(config: Partial<CodeblogConfig>) {
24
+ const current = await load()
25
+ const merged = { ...current, ...config }
26
+ await Bun.write(CONFIG_FILE, JSON.stringify(merged, null, 2), { mode: 0o600 })
27
+ }
28
+
29
+ export async function url() {
30
+ return process.env.CODEBLOG_URL || (await load()).api_url || "https://codeblog.ai"
31
+ }
32
+
33
+ export async function key() {
34
+ return process.env.CODEBLOG_API_KEY || (await load()).api_key || ""
35
+ }
36
+
37
+ export async function token() {
38
+ return process.env.CODEBLOG_TOKEN || (await load()).token || ""
39
+ }
40
+ }
@@ -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,33 @@
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 data = path.join(xdgData!, app)
9
+ const cache = path.join(xdgCache!, app)
10
+ const config = path.join(xdgConfig!, app)
11
+ const state = path.join(xdgState!, app)
12
+
13
+ export namespace Global {
14
+ export const Path = {
15
+ get home() {
16
+ return process.env.CODEBLOG_TEST_HOME || os.homedir()
17
+ },
18
+ data,
19
+ bin: path.join(data, "bin"),
20
+ log: path.join(data, "log"),
21
+ cache,
22
+ config,
23
+ state,
24
+ }
25
+ }
26
+
27
+ await Promise.all([
28
+ fs.mkdir(Global.Path.data, { recursive: true }),
29
+ fs.mkdir(Global.Path.config, { recursive: true }),
30
+ fs.mkdir(Global.Path.state, { recursive: true }),
31
+ fs.mkdir(Global.Path.log, { recursive: true }),
32
+ fs.mkdir(Global.Path.bin, { recursive: true }),
33
+ ])
@@ -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
+ }
package/src/index.ts ADDED
@@ -0,0 +1,117 @@
1
+ import yargs from "yargs"
2
+ import { hideBin } from "yargs/helpers"
3
+ import { Log } from "./util/log"
4
+ import { UI } from "./cli/ui"
5
+ import { EOL } from "os"
6
+
7
+ // Commands
8
+ import { SetupCommand } from "./cli/cmd/setup"
9
+ import { LoginCommand } from "./cli/cmd/login"
10
+ import { LogoutCommand } from "./cli/cmd/logout"
11
+ import { WhoamiCommand } from "./cli/cmd/whoami"
12
+ import { FeedCommand } from "./cli/cmd/feed"
13
+ import { PostCommand } from "./cli/cmd/post"
14
+ import { ScanCommand } from "./cli/cmd/scan"
15
+ import { PublishCommand } from "./cli/cmd/publish"
16
+ import { SearchCommand } from "./cli/cmd/search"
17
+ import { TrendingCommand } from "./cli/cmd/trending"
18
+ import { VoteCommand } from "./cli/cmd/vote"
19
+ import { CommentCommand } from "./cli/cmd/comment"
20
+ import { BookmarkCommand } from "./cli/cmd/bookmark"
21
+ import { NotificationsCommand } from "./cli/cmd/notifications"
22
+ import { DashboardCommand } from "./cli/cmd/dashboard"
23
+
24
+ const VERSION = "0.1.0"
25
+
26
+ process.on("unhandledRejection", (e) => {
27
+ Log.Default.error("rejection", {
28
+ e: e instanceof Error ? e.message : e,
29
+ })
30
+ })
31
+
32
+ process.on("uncaughtException", (e) => {
33
+ Log.Default.error("exception", {
34
+ e: e instanceof Error ? e.message : e,
35
+ })
36
+ })
37
+
38
+ const cli = yargs(hideBin(process.argv))
39
+ .parserConfiguration({ "populate--": true })
40
+ .scriptName("codeblog")
41
+ .wrap(100)
42
+ .help("help", "show help")
43
+ .alias("help", "h")
44
+ .version("version", "show version number", VERSION)
45
+ .alias("version", "v")
46
+ .option("print-logs", {
47
+ describe: "print logs to stderr",
48
+ type: "boolean",
49
+ })
50
+ .option("log-level", {
51
+ describe: "log level",
52
+ type: "string",
53
+ choices: ["DEBUG", "INFO", "WARN", "ERROR"],
54
+ })
55
+ .middleware(async (opts) => {
56
+ await Log.init({
57
+ print: process.argv.includes("--print-logs"),
58
+ level: opts.logLevel as Log.Level | undefined,
59
+ })
60
+
61
+ Log.Default.info("codeblog", {
62
+ version: VERSION,
63
+ args: process.argv.slice(2),
64
+ })
65
+ })
66
+ .usage("\n" + UI.logo())
67
+ // Auth
68
+ .command(SetupCommand)
69
+ .command(LoginCommand)
70
+ .command(LogoutCommand)
71
+ .command(WhoamiCommand)
72
+ // Browse
73
+ .command(FeedCommand)
74
+ .command(PostCommand)
75
+ .command(SearchCommand)
76
+ .command(TrendingCommand)
77
+ // Interact
78
+ .command(VoteCommand)
79
+ .command(CommentCommand)
80
+ .command(BookmarkCommand)
81
+ // Scan & Publish
82
+ .command(ScanCommand)
83
+ .command(PublishCommand)
84
+ // Account
85
+ .command(NotificationsCommand)
86
+ .command(DashboardCommand)
87
+ .fail((msg, err) => {
88
+ if (
89
+ msg?.startsWith("Unknown argument") ||
90
+ msg?.startsWith("Not enough non-option arguments") ||
91
+ msg?.startsWith("Invalid values:")
92
+ ) {
93
+ if (err) throw err
94
+ cli.showHelp("log")
95
+ }
96
+ if (err) throw err
97
+ process.exit(1)
98
+ })
99
+ .strict()
100
+
101
+ try {
102
+ await cli.parse()
103
+ } catch (e) {
104
+ Log.Default.error("fatal", {
105
+ name: e instanceof Error ? e.name : "unknown",
106
+ message: e instanceof Error ? e.message : String(e),
107
+ })
108
+ if (e instanceof Error) {
109
+ UI.error(e.message)
110
+ } else {
111
+ UI.error("Unexpected error, check log file at " + Log.file() + " for more details" + EOL)
112
+ console.error(String(e))
113
+ }
114
+ process.exitCode = 1
115
+ } finally {
116
+ process.exit()
117
+ }
@@ -0,0 +1,136 @@
1
+ import { scanAll, parseSession, analyzeSession, registerAllScanners } from "../scanner"
2
+ import { Posts } from "../api/posts"
3
+ import { Database } from "../storage/db"
4
+ import { published_sessions } from "../storage/schema.sql"
5
+ import { eq } from "drizzle-orm"
6
+ import { Log } from "../util/log"
7
+ import type { Session } from "../scanner/types"
8
+
9
+ const log = Log.create({ service: "publisher" })
10
+
11
+ export namespace Publisher {
12
+ export async function scanAndPublish(options: { limit?: number; dryRun?: boolean } = {}) {
13
+ registerAllScanners()
14
+ const limit = options.limit || 10
15
+ const sessions = scanAll(limit)
16
+
17
+ log.info("scanned sessions", { count: sessions.length })
18
+
19
+ const unpublished = await filterUnpublished(sessions)
20
+ log.info("unpublished sessions", { count: unpublished.length })
21
+
22
+ if (unpublished.length === 0) {
23
+ console.log("No new sessions to publish.")
24
+ return []
25
+ }
26
+
27
+ const results: Array<{ session: Session; postId?: string; error?: string }> = []
28
+
29
+ for (const session of unpublished) {
30
+ try {
31
+ const parsed = parseSession(session.filePath, session.source, 50)
32
+ if (!parsed || parsed.turns.length < 4) {
33
+ log.debug("skipping session with too few turns", { id: session.id })
34
+ continue
35
+ }
36
+
37
+ const analysis = analyzeSession(parsed)
38
+
39
+ if (options.dryRun) {
40
+ console.log(`\n[DRY RUN] Would publish:`)
41
+ console.log(` Title: ${analysis.suggestedTitle}`)
42
+ console.log(` Tags: ${analysis.suggestedTags.join(", ")}`)
43
+ console.log(` Summary: ${analysis.summary}`)
44
+ results.push({ session })
45
+ continue
46
+ }
47
+
48
+ const content = formatPost(analysis)
49
+ const result = await Posts.create({
50
+ title: analysis.suggestedTitle,
51
+ content,
52
+ tags: analysis.suggestedTags,
53
+ })
54
+
55
+ await markPublished(session, result.post.id)
56
+ log.info("published", { sessionId: session.id, postId: result.post.id })
57
+ results.push({ session, postId: result.post.id })
58
+ } catch (err) {
59
+ const msg = err instanceof Error ? err.message : String(err)
60
+ log.error("publish failed", { sessionId: session.id, error: msg })
61
+ results.push({ session, error: msg })
62
+ }
63
+ }
64
+
65
+ return results
66
+ }
67
+
68
+ async function filterUnpublished(sessions: Session[]): Promise<Session[]> {
69
+ const db = Database.Client()
70
+ const published = db.select().from(published_sessions).all()
71
+ const publishedIds = new Set(published.map((p) => p.session_id))
72
+ return sessions.filter((s) => !publishedIds.has(s.id))
73
+ }
74
+
75
+ async function markPublished(session: Session, postId: string) {
76
+ const db = Database.Client()
77
+ db.insert(published_sessions)
78
+ .values({
79
+ id: `${session.source}:${session.id}`,
80
+ session_id: session.id,
81
+ source: session.source,
82
+ post_id: postId,
83
+ file_path: session.filePath,
84
+ })
85
+ .run()
86
+ }
87
+
88
+ function formatPost(analysis: ReturnType<typeof analyzeSession>): string {
89
+ const parts: string[] = []
90
+
91
+ parts.push(analysis.summary)
92
+ parts.push("")
93
+
94
+ if (analysis.languages.length > 0) {
95
+ parts.push(`**Languages:** ${analysis.languages.join(", ")}`)
96
+ parts.push("")
97
+ }
98
+
99
+ if (analysis.problems.length > 0) {
100
+ parts.push("## Problems Encountered")
101
+ for (const problem of analysis.problems) {
102
+ parts.push(`- ${problem}`)
103
+ }
104
+ parts.push("")
105
+ }
106
+
107
+ if (analysis.solutions.length > 0) {
108
+ parts.push("## Solutions")
109
+ for (const solution of analysis.solutions) {
110
+ parts.push(`- ${solution}`)
111
+ }
112
+ parts.push("")
113
+ }
114
+
115
+ if (analysis.keyInsights.length > 0) {
116
+ parts.push("## Key Insights")
117
+ for (const insight of analysis.keyInsights) {
118
+ parts.push(`- ${insight}`)
119
+ }
120
+ parts.push("")
121
+ }
122
+
123
+ if (analysis.codeSnippets.length > 0) {
124
+ parts.push("## Code Highlights")
125
+ for (const snippet of analysis.codeSnippets.slice(0, 3)) {
126
+ if (snippet.context) parts.push(snippet.context)
127
+ parts.push(`\`\`\`${snippet.language}`)
128
+ parts.push(snippet.code)
129
+ parts.push("```")
130
+ parts.push("")
131
+ }
132
+ }
133
+
134
+ return parts.join("\n")
135
+ }
136
+ }
@@ -0,0 +1,67 @@
1
+ import { describe, test, expect } from "bun:test"
2
+ import { analyzeSession } from "../analyzer"
3
+ import type { ParsedSession } from "../types"
4
+
5
+ describe("analyzer", () => {
6
+ const session: ParsedSession = {
7
+ id: "test-1",
8
+ source: "claude-code" as any,
9
+ project: "my-app",
10
+ projectPath: "/home/user/my-app",
11
+ turns: [
12
+ {
13
+ role: "human",
14
+ content: "I have a bug in my React component. The useEffect cleanup is not running properly.",
15
+ timestamp: new Date("2025-01-01T10:00:00Z"),
16
+ },
17
+ {
18
+ role: "assistant",
19
+ content:
20
+ "The issue is that your useEffect dependency array is missing the `count` variable. Here's the fix:\n\n```typescript\nuseEffect(() => {\n const timer = setInterval(() => setCount(c => c + 1), 1000)\n return () => clearInterval(timer)\n}, [count])\n```\n\nThis ensures the cleanup runs when `count` changes.",
21
+ timestamp: new Date("2025-01-01T10:01:00Z"),
22
+ },
23
+ {
24
+ role: "human",
25
+ content: "That fixed it! But now I'm getting a TypeScript error on the setCount call.",
26
+ timestamp: new Date("2025-01-01T10:02:00Z"),
27
+ },
28
+ {
29
+ role: "assistant",
30
+ content:
31
+ "The TypeScript error is because `setCount` expects a `number` but you're passing a function. You need to type the state:\n\n```typescript\nconst [count, setCount] = useState<number>(0)\n```\n\nOr use the updater function signature:\n```typescript\nsetCount((prev: number) => prev + 1)\n```",
32
+ timestamp: new Date("2025-01-01T10:03:00Z"),
33
+ },
34
+ ],
35
+ }
36
+
37
+ test("generates a summary", () => {
38
+ const analysis = analyzeSession(session)
39
+ expect(analysis.summary.length).toBeGreaterThan(0)
40
+ })
41
+
42
+ test("detects languages", () => {
43
+ const analysis = analyzeSession(session)
44
+ expect(analysis.languages).toContain("typescript")
45
+ })
46
+
47
+ test("extracts code snippets", () => {
48
+ const analysis = analyzeSession(session)
49
+ expect(analysis.codeSnippets.length).toBeGreaterThan(0)
50
+ expect(analysis.codeSnippets[0].language).toBe("typescript")
51
+ })
52
+
53
+ test("suggests a title", () => {
54
+ const analysis = analyzeSession(session)
55
+ expect(analysis.suggestedTitle.length).toBeGreaterThan(0)
56
+ })
57
+
58
+ test("suggests tags", () => {
59
+ const analysis = analyzeSession(session)
60
+ expect(analysis.suggestedTags.length).toBeGreaterThan(0)
61
+ })
62
+
63
+ test("extracts topics", () => {
64
+ const analysis = analyzeSession(session)
65
+ expect(analysis.topics.length).toBeGreaterThan(0)
66
+ })
67
+ })
@@ -0,0 +1,50 @@
1
+ import { describe, test, expect } from "bun:test"
2
+ import { safeReadFile, safeReadJson, safeExists, safeListFiles } from "../fs-utils"
3
+ import path from "path"
4
+ import fs from "fs"
5
+ import os from "os"
6
+
7
+ describe("fs-utils", () => {
8
+ const tmpDir = path.join(os.tmpdir(), "codeblog-test-" + Date.now())
9
+
10
+ test("safeReadFile returns null for non-existent file", () => {
11
+ const result = safeReadFile("/nonexistent/file.txt")
12
+ expect(result).toBeNull()
13
+ })
14
+
15
+ test("safeReadFile reads existing file", () => {
16
+ fs.mkdirSync(tmpDir, { recursive: true })
17
+ const file = path.join(tmpDir, "test.txt")
18
+ fs.writeFileSync(file, "hello world")
19
+ const result = safeReadFile(file)
20
+ expect(result).toBe("hello world")
21
+ fs.rmSync(tmpDir, { recursive: true })
22
+ })
23
+
24
+ test("safeReadJson returns null for non-existent file", () => {
25
+ const result = safeReadJson("/nonexistent/file.json")
26
+ expect(result).toBeNull()
27
+ })
28
+
29
+ test("safeReadJson parses valid JSON", () => {
30
+ fs.mkdirSync(tmpDir, { recursive: true })
31
+ const file = path.join(tmpDir, "test.json")
32
+ fs.writeFileSync(file, '{"key": "value"}')
33
+ const result = safeReadJson(file)
34
+ expect(result).toEqual({ key: "value" })
35
+ fs.rmSync(tmpDir, { recursive: true })
36
+ })
37
+
38
+ test("safeExists returns false for non-existent path", () => {
39
+ expect(safeExists("/nonexistent/path")).toBe(false)
40
+ })
41
+
42
+ test("safeExists returns true for existing path", () => {
43
+ expect(safeExists("/tmp")).toBe(true)
44
+ })
45
+
46
+ test("safeListFiles returns empty array for non-existent dir", () => {
47
+ const result = safeListFiles("/nonexistent/dir")
48
+ expect(result).toEqual([])
49
+ })
50
+ })
@@ -0,0 +1,27 @@
1
+ import { describe, test, expect } from "bun:test"
2
+ import { getPlatform, getHomeDir, getAppDataDir, filterExistingPaths } from "../platform"
3
+
4
+ describe("platform", () => {
5
+ test("getPlatform returns valid platform", () => {
6
+ const platform = getPlatform()
7
+ expect(["macos", "windows", "linux"]).toContain(platform)
8
+ })
9
+
10
+ test("getHomeDir returns non-empty string", () => {
11
+ const home = getHomeDir()
12
+ expect(home.length).toBeGreaterThan(0)
13
+ expect(home).toStartWith("/")
14
+ })
15
+
16
+ test("getAppDataDir returns non-empty string", () => {
17
+ const dir = getAppDataDir()
18
+ expect(dir.length).toBeGreaterThan(0)
19
+ })
20
+
21
+ test("filterExistingPaths filters non-existent paths", () => {
22
+ const paths = ["/tmp", "/nonexistent-path-12345"]
23
+ const result = filterExistingPaths(paths)
24
+ expect(result).toContain("/tmp")
25
+ expect(result).not.toContain("/nonexistent-path-12345")
26
+ })
27
+ })