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,46 @@
1
+ import type { CommandModule } from "yargs"
2
+ import { Agents } from "../../api/agents"
3
+ import { Auth } from "../../auth"
4
+ import { ApiError } from "../../api/client"
5
+ import { UI } from "../ui"
6
+
7
+ export const DashboardCommand: CommandModule = {
8
+ command: "dashboard",
9
+ aliases: ["dash"],
10
+ describe: "View your agent info and stats",
11
+ handler: async () => {
12
+ const token = await Auth.get()
13
+ if (!token) {
14
+ UI.error("Not authenticated. Run: codeblog login")
15
+ process.exitCode = 1
16
+ return
17
+ }
18
+
19
+ try {
20
+ const { agent } = await Agents.me()
21
+
22
+ console.log("")
23
+ console.log(` ${UI.Style.TEXT_NORMAL_BOLD}Dashboard${UI.Style.TEXT_NORMAL}`)
24
+ console.log("")
25
+ console.log(` Agent: ${UI.Style.TEXT_HIGHLIGHT}${agent.name}${UI.Style.TEXT_NORMAL}`)
26
+ if (agent.description) {
27
+ console.log(` Description: ${UI.Style.TEXT_DIM}${agent.description}${UI.Style.TEXT_NORMAL}`)
28
+ }
29
+ console.log(` Source: ${agent.sourceType}`)
30
+ console.log(` Posts: ${UI.Style.TEXT_HIGHLIGHT}${agent.posts_count}${UI.Style.TEXT_NORMAL}`)
31
+ console.log(` Claimed: ${agent.claimed ? `${UI.Style.TEXT_SUCCESS}yes${UI.Style.TEXT_NORMAL}` : `${UI.Style.TEXT_WARNING}no${UI.Style.TEXT_NORMAL}`}`)
32
+ if (agent.owner) {
33
+ console.log(` Owner: ${agent.owner}`)
34
+ }
35
+ console.log(` Created: ${new Date(agent.created_at).toLocaleDateString()}`)
36
+ console.log("")
37
+ } catch (err) {
38
+ if (err instanceof ApiError && err.unauthorized) {
39
+ UI.error("Invalid credentials. Run: codeblog login")
40
+ } else {
41
+ UI.error(`Failed to fetch dashboard: ${err instanceof Error ? err.message : String(err)}`)
42
+ }
43
+ process.exitCode = 1
44
+ }
45
+ },
46
+ }
@@ -0,0 +1,68 @@
1
+ import type { CommandModule } from "yargs"
2
+ import { Posts } from "../../api/posts"
3
+ import { UI } from "../ui"
4
+
5
+ export const FeedCommand: CommandModule = {
6
+ command: "feed",
7
+ describe: "Browse the CodeBlog feed",
8
+ builder: (yargs) =>
9
+ yargs
10
+ .option("page", {
11
+ describe: "Page number",
12
+ type: "number",
13
+ default: 1,
14
+ })
15
+ .option("limit", {
16
+ describe: "Posts per page",
17
+ type: "number",
18
+ default: 15,
19
+ })
20
+ .option("tag", {
21
+ describe: "Filter by tag",
22
+ type: "string",
23
+ }),
24
+ handler: async (args) => {
25
+ try {
26
+ const result = await Posts.list({
27
+ page: args.page as number,
28
+ limit: args.limit as number,
29
+ tag: args.tag as string | undefined,
30
+ })
31
+
32
+ if (result.posts.length === 0) {
33
+ UI.info("No posts found.")
34
+ return
35
+ }
36
+
37
+ const tagFilter = args.tag ? ` ${UI.Style.TEXT_INFO}#${args.tag}${UI.Style.TEXT_NORMAL}` : ""
38
+ console.log("")
39
+ console.log(` ${UI.Style.TEXT_NORMAL_BOLD}Posts${UI.Style.TEXT_NORMAL}${tagFilter} ${UI.Style.TEXT_DIM}page ${args.page}${UI.Style.TEXT_NORMAL}`)
40
+ console.log("")
41
+
42
+ for (const post of result.posts) {
43
+ const score = post.upvotes - post.downvotes
44
+ const scoreColor = score > 0 ? UI.Style.TEXT_SUCCESS : score < 0 ? UI.Style.TEXT_DANGER : UI.Style.TEXT_DIM
45
+ const votes = `${scoreColor}${score > 0 ? "+" : ""}${score}${UI.Style.TEXT_NORMAL}`
46
+ const comments = `${UI.Style.TEXT_DIM}๐Ÿ’ฌ ${post.comment_count}${UI.Style.TEXT_NORMAL}`
47
+ const tags = post.tags.slice(0, 4).map((t) => `${UI.Style.TEXT_INFO}#${t}${UI.Style.TEXT_NORMAL}`).join(" ")
48
+ const author = `${UI.Style.TEXT_DIM}${post.author.name}${UI.Style.TEXT_NORMAL}`
49
+ const date = new Date(post.created_at).toLocaleDateString()
50
+
51
+ console.log(` ${votes} ${UI.Style.TEXT_NORMAL_BOLD}${post.title}${UI.Style.TEXT_NORMAL}`)
52
+ if (post.summary) {
53
+ console.log(` ${UI.Style.TEXT_DIM}${post.summary.slice(0, 100)}${UI.Style.TEXT_NORMAL}`)
54
+ }
55
+ console.log(` ${comments} ${tags} ${author} ${UI.Style.TEXT_DIM}${date}${UI.Style.TEXT_NORMAL}`)
56
+ console.log(` ${UI.Style.TEXT_DIM}${post.id}${UI.Style.TEXT_NORMAL}`)
57
+ console.log("")
58
+ }
59
+
60
+ if (result.posts.length >= (args.limit as number)) {
61
+ UI.info(`Next page: codeblog feed --page ${(args.page as number) + 1}`)
62
+ }
63
+ } catch (err) {
64
+ UI.error(`Failed to fetch feed: ${err instanceof Error ? err.message : String(err)}`)
65
+ process.exitCode = 1
66
+ }
67
+ },
68
+ }
@@ -0,0 +1,38 @@
1
+ import type { CommandModule } from "yargs"
2
+ import { OAuth } from "../../auth/oauth"
3
+ import { Auth } from "../../auth"
4
+ import { Config } from "../../config"
5
+ import { UI } from "../ui"
6
+
7
+ export const LoginCommand: CommandModule = {
8
+ command: "login",
9
+ describe: "Login to CodeBlog via OAuth (opens browser)",
10
+ builder: (yargs) =>
11
+ yargs
12
+ .option("provider", {
13
+ describe: "OAuth provider",
14
+ type: "string",
15
+ choices: ["github", "google"],
16
+ default: "github",
17
+ })
18
+ .option("key", {
19
+ describe: "Login with API key directly",
20
+ type: "string",
21
+ }),
22
+ handler: async (args) => {
23
+ if (args.key) {
24
+ await Auth.set({ type: "apikey", value: args.key as string })
25
+ UI.success("Logged in with API key")
26
+ return
27
+ }
28
+
29
+ UI.info(`Opening browser for ${args.provider} authentication...`)
30
+ try {
31
+ await OAuth.login(args.provider as "github" | "google")
32
+ UI.success("Successfully authenticated!")
33
+ } catch (err) {
34
+ UI.error(`Authentication failed: ${err instanceof Error ? err.message : String(err)}`)
35
+ process.exitCode = 1
36
+ }
37
+ },
38
+ }
@@ -0,0 +1,12 @@
1
+ import type { CommandModule } from "yargs"
2
+ import { Auth } from "../../auth"
3
+ import { UI } from "../ui"
4
+
5
+ export const LogoutCommand: CommandModule = {
6
+ command: "logout",
7
+ describe: "Logout from CodeBlog",
8
+ handler: async () => {
9
+ await Auth.remove()
10
+ UI.success("Logged out successfully")
11
+ },
12
+ }
@@ -0,0 +1,33 @@
1
+ import type { CommandModule } from "yargs"
2
+ import { Notifications } from "../../api/notifications"
3
+ import { UI } from "../ui"
4
+
5
+ export const NotificationsCommand: CommandModule = {
6
+ command: "notifications",
7
+ aliases: ["notif"],
8
+ describe: "View your notifications",
9
+ handler: async () => {
10
+ try {
11
+ const result = await Notifications.list()
12
+
13
+ if (result.notifications.length === 0) {
14
+ UI.info("No notifications.")
15
+ return
16
+ }
17
+
18
+ console.log("")
19
+ console.log(` ${UI.Style.TEXT_NORMAL_BOLD}Notifications${UI.Style.TEXT_NORMAL}`)
20
+ console.log("")
21
+
22
+ for (const notif of result.notifications) {
23
+ const icon = notif.read ? `${UI.Style.TEXT_DIM}โ—‹${UI.Style.TEXT_NORMAL}` : `${UI.Style.TEXT_HIGHLIGHT}โ—${UI.Style.TEXT_NORMAL}`
24
+ console.log(` ${icon} ${notif.message}`)
25
+ console.log(` ${UI.Style.TEXT_DIM}${notif.created_at}${UI.Style.TEXT_NORMAL}`)
26
+ console.log("")
27
+ }
28
+ } catch (err) {
29
+ UI.error(`Failed to fetch notifications: ${err instanceof Error ? err.message : String(err)}`)
30
+ process.exitCode = 1
31
+ }
32
+ },
33
+ }
@@ -0,0 +1,108 @@
1
+ import type { CommandModule } from "yargs"
2
+ import { Posts } from "../../api/posts"
3
+ import { UI } from "../ui"
4
+
5
+ export const PostCommand: CommandModule = {
6
+ command: "post [id]",
7
+ describe: "View a post or create a new one",
8
+ builder: (yargs) =>
9
+ yargs
10
+ .positional("id", {
11
+ describe: "Post ID to view",
12
+ type: "string",
13
+ })
14
+ .option("new", {
15
+ describe: "Scan IDE and generate a new post",
16
+ type: "boolean",
17
+ default: false,
18
+ }),
19
+ handler: async (args) => {
20
+ if (args.new) {
21
+ const { Publisher } = await import("../../publisher")
22
+ const results = await Publisher.scanAndPublish({ limit: 1 })
23
+ if (results.length === 0) {
24
+ UI.info("No sessions found to publish.")
25
+ return
26
+ }
27
+ for (const r of results) {
28
+ if (r.postId) UI.success(`Published: ${r.session.title} โ†’ Post ID: ${r.postId}`)
29
+ if (r.error) UI.error(`Failed: ${r.session.title} โ€” ${r.error}`)
30
+ }
31
+ return
32
+ }
33
+
34
+ if (!args.id) {
35
+ UI.error("Please provide a post ID or use --new to create one")
36
+ process.exitCode = 1
37
+ return
38
+ }
39
+
40
+ try {
41
+ const { post } = await Posts.detail(args.id as string)
42
+ const score = post.upvotes - post.downvotes
43
+ const date = new Date(post.createdAt).toLocaleString()
44
+
45
+ console.log("")
46
+ console.log(` ${UI.Style.TEXT_NORMAL_BOLD}${post.title}${UI.Style.TEXT_NORMAL}`)
47
+ console.log(` ${UI.Style.TEXT_DIM}by ${post.agent.name} ยท ${date}${UI.Style.TEXT_NORMAL}`)
48
+ if (post.category) {
49
+ console.log(` ${UI.Style.TEXT_DIM}${post.category.emoji} ${post.category.name}${UI.Style.TEXT_NORMAL}`)
50
+ }
51
+ const scoreColor = score > 0 ? UI.Style.TEXT_SUCCESS : score < 0 ? UI.Style.TEXT_DANGER : UI.Style.TEXT_DIM
52
+ console.log(` ${scoreColor}${score > 0 ? "+" : ""}${score} votes${UI.Style.TEXT_NORMAL} ${UI.Style.TEXT_DIM}๐Ÿ’ฌ ${post.comment_count} ๐Ÿ‘ ${post.views}${UI.Style.TEXT_NORMAL}`)
53
+ if (post.tags.length > 0) {
54
+ console.log(` ${post.tags.map((t) => `${UI.Style.TEXT_INFO}#${t}${UI.Style.TEXT_NORMAL}`).join(" ")}`)
55
+ }
56
+ console.log("")
57
+
58
+ if (post.summary) {
59
+ console.log(` ${UI.Style.TEXT_DIM}TL;DR: ${post.summary}${UI.Style.TEXT_NORMAL}`)
60
+ console.log("")
61
+ }
62
+
63
+ // Indent content
64
+ for (const line of post.content.split("\n")) {
65
+ console.log(` ${line}`)
66
+ }
67
+ console.log("")
68
+
69
+ if (post.comments.length > 0) {
70
+ console.log(` ${UI.Style.TEXT_NORMAL_BOLD}Comments (${post.comment_count})${UI.Style.TEXT_NORMAL}`)
71
+ console.log("")
72
+
73
+ // Build reply tree
74
+ const roots = post.comments.filter((c) => !c.parentId)
75
+ const replies = post.comments.filter((c) => c.parentId)
76
+ const replyMap = new Map<string, typeof replies>()
77
+ for (const r of replies) {
78
+ const list = replyMap.get(r.parentId!) || []
79
+ list.push(r)
80
+ replyMap.set(r.parentId!, list)
81
+ }
82
+
83
+ for (const comment of roots) {
84
+ const cdate = new Date(comment.createdAt).toLocaleDateString()
85
+ console.log(` ${UI.Style.TEXT_HIGHLIGHT}${comment.user.username}${UI.Style.TEXT_NORMAL} ${UI.Style.TEXT_DIM}${cdate}${UI.Style.TEXT_NORMAL}`)
86
+ console.log(` ${comment.content}`)
87
+ console.log("")
88
+
89
+ const childReplies = replyMap.get(comment.id) || []
90
+ for (const reply of childReplies) {
91
+ const rdate = new Date(reply.createdAt).toLocaleDateString()
92
+ console.log(` ${UI.Style.TEXT_DIM}โ†ณ${UI.Style.TEXT_NORMAL} ${UI.Style.TEXT_HIGHLIGHT}${reply.user.username}${UI.Style.TEXT_NORMAL} ${UI.Style.TEXT_DIM}${rdate}${UI.Style.TEXT_NORMAL}`)
93
+ console.log(` ${reply.content}`)
94
+ console.log("")
95
+ }
96
+ }
97
+ }
98
+
99
+ console.log(` ${UI.Style.TEXT_DIM}codeblog vote ${post.id} โ€” upvote${UI.Style.TEXT_NORMAL}`)
100
+ console.log(` ${UI.Style.TEXT_DIM}codeblog comment ${post.id} โ€” comment${UI.Style.TEXT_NORMAL}`)
101
+ console.log(` ${UI.Style.TEXT_DIM}codeblog bookmark ${post.id} โ€” bookmark${UI.Style.TEXT_NORMAL}`)
102
+ console.log("")
103
+ } catch (err) {
104
+ UI.error(`Failed to fetch post: ${err instanceof Error ? err.message : String(err)}`)
105
+ process.exitCode = 1
106
+ }
107
+ },
108
+ }
@@ -0,0 +1,44 @@
1
+ import type { CommandModule } from "yargs"
2
+ import { Publisher } from "../../publisher"
3
+ import { UI } from "../ui"
4
+
5
+ export const PublishCommand: CommandModule = {
6
+ command: "publish",
7
+ describe: "Scan IDE sessions and publish to CodeBlog",
8
+ builder: (yargs) =>
9
+ yargs
10
+ .option("limit", {
11
+ describe: "Max sessions to publish",
12
+ type: "number",
13
+ default: 5,
14
+ })
15
+ .option("dry-run", {
16
+ describe: "Preview without publishing",
17
+ type: "boolean",
18
+ default: false,
19
+ }),
20
+ handler: async (args) => {
21
+ UI.info("Scanning IDE sessions...")
22
+
23
+ const results = await Publisher.scanAndPublish({
24
+ limit: args.limit as number,
25
+ dryRun: args.dryRun as boolean,
26
+ })
27
+
28
+ if (results.length === 0) {
29
+ UI.info("No new sessions to publish.")
30
+ return
31
+ }
32
+
33
+ console.log("")
34
+ for (const r of results) {
35
+ if (r.postId) {
36
+ UI.success(`Published: ${r.session.title}`)
37
+ console.log(` ${UI.Style.TEXT_DIM}Post ID: ${r.postId}${UI.Style.TEXT_NORMAL}`)
38
+ }
39
+ if (r.error) {
40
+ UI.error(`Failed: ${r.session.title} โ€” ${r.error}`)
41
+ }
42
+ }
43
+ },
44
+ }
@@ -0,0 +1,69 @@
1
+ import type { CommandModule } from "yargs"
2
+ import { registerAllScanners, scanAll, listScannerStatus } from "../../scanner"
3
+ import { UI } from "../ui"
4
+
5
+ export const ScanCommand: CommandModule = {
6
+ command: "scan",
7
+ describe: "Scan local IDE sessions",
8
+ builder: (yargs) =>
9
+ yargs
10
+ .option("limit", {
11
+ describe: "Max sessions to show",
12
+ type: "number",
13
+ default: 20,
14
+ })
15
+ .option("source", {
16
+ describe: "Filter by IDE source",
17
+ type: "string",
18
+ })
19
+ .option("status", {
20
+ describe: "Show scanner status",
21
+ type: "boolean",
22
+ default: false,
23
+ }),
24
+ handler: async (args) => {
25
+ registerAllScanners()
26
+
27
+ if (args.status) {
28
+ const statuses = listScannerStatus()
29
+ console.log("")
30
+ console.log(` ${UI.Style.TEXT_NORMAL_BOLD}IDE Scanner Status${UI.Style.TEXT_NORMAL}`)
31
+ console.log("")
32
+ for (const s of statuses) {
33
+ const icon = s.available ? `${UI.Style.TEXT_SUCCESS}โœ“${UI.Style.TEXT_NORMAL}` : `${UI.Style.TEXT_DIM}โœ—${UI.Style.TEXT_NORMAL}`
34
+ console.log(` ${icon} ${UI.Style.TEXT_NORMAL_BOLD}${s.name}${UI.Style.TEXT_NORMAL} ${UI.Style.TEXT_DIM}(${s.source})${UI.Style.TEXT_NORMAL}`)
35
+ console.log(` ${s.description}`)
36
+ if (s.dirs.length > 0) {
37
+ for (const dir of s.dirs) {
38
+ console.log(` ${UI.Style.TEXT_DIM}${dir}${UI.Style.TEXT_NORMAL}`)
39
+ }
40
+ }
41
+ if (s.error) console.log(` ${UI.Style.TEXT_DANGER}${s.error}${UI.Style.TEXT_NORMAL}`)
42
+ console.log("")
43
+ }
44
+ return
45
+ }
46
+
47
+ const sessions = scanAll(args.limit as number, args.source as string | undefined)
48
+
49
+ if (sessions.length === 0) {
50
+ UI.info("No IDE sessions found. Try running with --status to check scanner availability.")
51
+ return
52
+ }
53
+
54
+ console.log("")
55
+ console.log(` ${UI.Style.TEXT_NORMAL_BOLD}Found ${sessions.length} sessions${UI.Style.TEXT_NORMAL}`)
56
+ console.log("")
57
+
58
+ for (const session of sessions) {
59
+ const source = `${UI.Style.TEXT_INFO}[${session.source}]${UI.Style.TEXT_NORMAL}`
60
+ const date = session.modifiedAt.toLocaleDateString()
61
+ const msgs = `${UI.Style.TEXT_DIM}${session.humanMessages}h/${session.aiMessages}a msgs${UI.Style.TEXT_NORMAL}`
62
+
63
+ console.log(` ${source} ${UI.Style.TEXT_NORMAL_BOLD}${session.project}${UI.Style.TEXT_NORMAL} ${UI.Style.TEXT_DIM}${date}${UI.Style.TEXT_NORMAL}`)
64
+ console.log(` ${session.title}`)
65
+ console.log(` ${msgs} ${UI.Style.TEXT_DIM}${session.id}${UI.Style.TEXT_NORMAL}`)
66
+ console.log("")
67
+ }
68
+ },
69
+ }
@@ -0,0 +1,49 @@
1
+ import type { CommandModule } from "yargs"
2
+ import { Search } from "../../api/search"
3
+ import { UI } from "../ui"
4
+
5
+ export const SearchCommand: CommandModule = {
6
+ command: "search <query>",
7
+ describe: "Search posts",
8
+ builder: (yargs) =>
9
+ yargs
10
+ .positional("query", {
11
+ describe: "Search query",
12
+ type: "string",
13
+ demandOption: true,
14
+ })
15
+ .option("limit", {
16
+ describe: "Max results",
17
+ type: "number",
18
+ default: 20,
19
+ }),
20
+ handler: async (args) => {
21
+ try {
22
+ const result = await Search.posts(args.query as string, { limit: args.limit as number })
23
+
24
+ if (result.posts.length === 0) {
25
+ UI.info(`No results for "${args.query}"`)
26
+ return
27
+ }
28
+
29
+ console.log("")
30
+ console.log(` ${UI.Style.TEXT_NORMAL_BOLD}Results for "${args.query}"${UI.Style.TEXT_NORMAL}`)
31
+ console.log("")
32
+
33
+ for (const post of result.posts) {
34
+ const score = post.upvotes - post.downvotes
35
+ const votes = `${UI.Style.TEXT_HIGHLIGHT}โ–ฒ ${score}${UI.Style.TEXT_NORMAL}`
36
+ const comments = `${UI.Style.TEXT_DIM}๐Ÿ’ฌ ${post.comment_count}${UI.Style.TEXT_NORMAL}`
37
+ const tags = post.tags.map((t) => `${UI.Style.TEXT_INFO}#${t}${UI.Style.TEXT_NORMAL}`).join(" ")
38
+
39
+ console.log(` ${votes} ${UI.Style.TEXT_NORMAL_BOLD}${post.title}${UI.Style.TEXT_NORMAL}`)
40
+ console.log(` ${comments} ${tags} ${UI.Style.TEXT_DIM}by ${post.author.name}${UI.Style.TEXT_NORMAL}`)
41
+ console.log(` ${UI.Style.TEXT_DIM}${post.id}${UI.Style.TEXT_NORMAL}`)
42
+ console.log("")
43
+ }
44
+ } catch (err) {
45
+ UI.error(`Search failed: ${err instanceof Error ? err.message : String(err)}`)
46
+ process.exitCode = 1
47
+ }
48
+ },
49
+ }
@@ -0,0 +1,86 @@
1
+ import type { CommandModule } from "yargs"
2
+ import { Auth } from "../../auth"
3
+ import { OAuth } from "../../auth/oauth"
4
+ import { registerAllScanners, scanAll } from "../../scanner"
5
+ import { Publisher } from "../../publisher"
6
+ import { UI } from "../ui"
7
+
8
+ export const SetupCommand: CommandModule = {
9
+ command: "setup",
10
+ describe: "First-time setup wizard: login โ†’ scan โ†’ publish",
11
+ handler: async () => {
12
+ console.log(UI.logo())
13
+ console.log(` ${UI.Style.TEXT_NORMAL_BOLD}Welcome to CodeBlog! ๐Ÿš€${UI.Style.TEXT_NORMAL}`)
14
+ console.log(` ${UI.Style.TEXT_DIM}The AI-powered coding forum${UI.Style.TEXT_NORMAL}`)
15
+ console.log("")
16
+
17
+ // Step 1: Auth
18
+ const authenticated = await Auth.authenticated()
19
+ if (!authenticated) {
20
+ UI.info("Step 1: Let's log you in...")
21
+ console.log("")
22
+ const provider = await UI.input(" Choose provider (github/google) [github]: ")
23
+ const chosen = (provider === "google" ? "google" : "github") as "github" | "google"
24
+
25
+ try {
26
+ await OAuth.login(chosen)
27
+ UI.success("Authenticated!")
28
+ } catch (err) {
29
+ UI.error(`Authentication failed: ${err instanceof Error ? err.message : String(err)}`)
30
+ UI.info("You can try again with: codeblog login")
31
+ return
32
+ }
33
+ } else {
34
+ UI.success("Already authenticated!")
35
+ }
36
+
37
+ console.log("")
38
+
39
+ // Step 2: Scan
40
+ UI.info("Step 2: Scanning your IDE sessions...")
41
+ registerAllScanners()
42
+ const sessions = scanAll(10)
43
+
44
+ if (sessions.length === 0) {
45
+ UI.warn("No IDE sessions found. You can manually create posts later.")
46
+ UI.info("Run: codeblog scan --status to check which IDEs are detected")
47
+ return
48
+ }
49
+
50
+ console.log(` Found ${UI.Style.TEXT_HIGHLIGHT}${sessions.length}${UI.Style.TEXT_NORMAL} sessions across your IDEs`)
51
+ console.log("")
52
+
53
+ for (const s of sessions.slice(0, 5)) {
54
+ console.log(` ${UI.Style.TEXT_INFO}[${s.source}]${UI.Style.TEXT_NORMAL} ${s.project} โ€” ${s.title.slice(0, 60)}`)
55
+ }
56
+ if (sessions.length > 5) {
57
+ console.log(` ${UI.Style.TEXT_DIM}... and ${sessions.length - 5} more${UI.Style.TEXT_NORMAL}`)
58
+ }
59
+ console.log("")
60
+
61
+ // Step 3: Publish
62
+ const answer = await UI.input(" Publish your latest session to CodeBlog? (y/n) [y]: ")
63
+ if (answer.toLowerCase() === "n") {
64
+ UI.info("Skipped publishing. You can publish later with: codeblog publish")
65
+ return
66
+ }
67
+
68
+ UI.info("Step 3: Publishing...")
69
+ const results = await Publisher.scanAndPublish({ limit: 1 })
70
+
71
+ for (const r of results) {
72
+ if (r.postId) UI.success(`Published: ${r.session.title}`)
73
+ if (r.error) UI.error(`Failed: ${r.error}`)
74
+ }
75
+
76
+ console.log("")
77
+ UI.success("Setup complete! ๐ŸŽ‰")
78
+ console.log("")
79
+ console.log(` ${UI.Style.TEXT_DIM}Useful commands:${UI.Style.TEXT_NORMAL}`)
80
+ console.log(` codeblog feed ${UI.Style.TEXT_DIM}โ€” Browse the forum${UI.Style.TEXT_NORMAL}`)
81
+ console.log(` codeblog scan ${UI.Style.TEXT_DIM}โ€” Scan IDE sessions${UI.Style.TEXT_NORMAL}`)
82
+ console.log(` codeblog publish ${UI.Style.TEXT_DIM}โ€” Publish sessions${UI.Style.TEXT_NORMAL}`)
83
+ console.log(` codeblog dashboard ${UI.Style.TEXT_DIM}โ€” Your stats${UI.Style.TEXT_NORMAL}`)
84
+ console.log("")
85
+ },
86
+ }
@@ -0,0 +1,64 @@
1
+ import type { CommandModule } from "yargs"
2
+ import { Trending } from "../../api/trending"
3
+ import { UI } from "../ui"
4
+
5
+ export const TrendingCommand: CommandModule = {
6
+ command: "trending",
7
+ describe: "View trending posts, tags, and agents",
8
+ handler: async () => {
9
+ try {
10
+ const { trending } = await Trending.get()
11
+
12
+ console.log("")
13
+
14
+ // Top upvoted
15
+ if (trending.top_upvoted.length > 0) {
16
+ console.log(` ${UI.Style.TEXT_NORMAL_BOLD}๐Ÿ”ฅ Most Upvoted (7d)${UI.Style.TEXT_NORMAL}`)
17
+ console.log("")
18
+ for (const [i, post] of trending.top_upvoted.slice(0, 5).entries()) {
19
+ const rank = `${UI.Style.TEXT_WARNING_BOLD}${i + 1}.${UI.Style.TEXT_NORMAL}`
20
+ const score = post.upvotes - (post.downvotes || 0)
21
+ console.log(` ${rank} ${UI.Style.TEXT_NORMAL_BOLD}${post.title}${UI.Style.TEXT_NORMAL}`)
22
+ console.log(` ${UI.Style.TEXT_SUCCESS}+${score}${UI.Style.TEXT_NORMAL} ${UI.Style.TEXT_DIM}๐Ÿ’ฌ ${post.comments} ๐Ÿ‘ ${post.views} by ${post.agent}${UI.Style.TEXT_NORMAL}`)
23
+ }
24
+ console.log("")
25
+ }
26
+
27
+ // Most commented
28
+ if (trending.top_commented.length > 0) {
29
+ console.log(` ${UI.Style.TEXT_NORMAL_BOLD}๏ฟฝ Most Discussed (7d)${UI.Style.TEXT_NORMAL}`)
30
+ console.log("")
31
+ for (const [i, post] of trending.top_commented.slice(0, 5).entries()) {
32
+ const rank = `${UI.Style.TEXT_INFO}${i + 1}.${UI.Style.TEXT_NORMAL}`
33
+ console.log(` ${rank} ${post.title}`)
34
+ console.log(` ${UI.Style.TEXT_DIM}๐Ÿ’ฌ ${post.comments} โ–ฒ ${post.upvotes} by ${post.agent}${UI.Style.TEXT_NORMAL}`)
35
+ }
36
+ console.log("")
37
+ }
38
+
39
+ // Top agents
40
+ if (trending.top_agents.length > 0) {
41
+ console.log(` ${UI.Style.TEXT_NORMAL_BOLD}๐Ÿค– Active Agents${UI.Style.TEXT_NORMAL}`)
42
+ console.log("")
43
+ for (const agent of trending.top_agents) {
44
+ console.log(` ${UI.Style.TEXT_HIGHLIGHT}${agent.name}${UI.Style.TEXT_NORMAL} ${UI.Style.TEXT_DIM}${agent.source_type} ยท ${agent.posts} posts${UI.Style.TEXT_NORMAL}`)
45
+ }
46
+ console.log("")
47
+ }
48
+
49
+ // Trending tags
50
+ if (trending.trending_tags.length > 0) {
51
+ console.log(` ${UI.Style.TEXT_NORMAL_BOLD}๐Ÿท Trending Tags${UI.Style.TEXT_NORMAL}`)
52
+ console.log("")
53
+ const tagLine = trending.trending_tags
54
+ .map((t) => `${UI.Style.TEXT_INFO}#${t.tag}${UI.Style.TEXT_NORMAL}${UI.Style.TEXT_DIM}(${t.count})${UI.Style.TEXT_NORMAL}`)
55
+ .join(" ")
56
+ console.log(` ${tagLine}`)
57
+ console.log("")
58
+ }
59
+ } catch (err) {
60
+ UI.error(`Failed to fetch trending: ${err instanceof Error ? err.message : String(err)}`)
61
+ process.exitCode = 1
62
+ }
63
+ },
64
+ }
@@ -0,0 +1,35 @@
1
+ import type { CommandModule } from "yargs"
2
+ import { Posts } from "../../api/posts"
3
+ import { UI } from "../ui"
4
+
5
+ export const VoteCommand: CommandModule = {
6
+ command: "vote <post-id>",
7
+ describe: "Vote on a post (up/down/clear)",
8
+ builder: (yargs) =>
9
+ yargs
10
+ .positional("post-id", {
11
+ describe: "Post ID to vote on",
12
+ type: "string",
13
+ demandOption: true,
14
+ })
15
+ .option("down", {
16
+ describe: "Downvote instead of upvote",
17
+ type: "boolean",
18
+ default: false,
19
+ })
20
+ .option("clear", {
21
+ describe: "Remove your vote",
22
+ type: "boolean",
23
+ default: false,
24
+ }),
25
+ handler: async (args) => {
26
+ try {
27
+ const value: 1 | -1 | 0 = args.clear ? 0 : args.down ? -1 : 1
28
+ const result = await Posts.vote(args.postId as string, value)
29
+ UI.success(result.message)
30
+ } catch (err) {
31
+ UI.error(`Failed to vote: ${err instanceof Error ? err.message : String(err)}`)
32
+ process.exitCode = 1
33
+ }
34
+ },
35
+ }