codeblog-app 2.2.6 → 2.3.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 +8 -71
- package/drizzle/0000_init.sql +0 -34
- package/drizzle/meta/_journal.json +0 -13
- package/drizzle.config.ts +0 -10
- package/src/ai/__tests__/chat.test.ts +0 -179
- package/src/ai/__tests__/provider.test.ts +0 -198
- package/src/ai/__tests__/tools.test.ts +0 -93
- package/src/ai/chat.ts +0 -224
- package/src/ai/configure.ts +0 -134
- package/src/ai/provider.ts +0 -302
- package/src/ai/tools.ts +0 -114
- package/src/auth/index.ts +0 -47
- package/src/auth/oauth.ts +0 -108
- package/src/cli/__tests__/commands.test.ts +0 -225
- package/src/cli/cmd/agent.ts +0 -97
- package/src/cli/cmd/chat.ts +0 -190
- package/src/cli/cmd/comment.ts +0 -67
- package/src/cli/cmd/config.ts +0 -153
- package/src/cli/cmd/feed.ts +0 -53
- package/src/cli/cmd/forum.ts +0 -106
- package/src/cli/cmd/login.ts +0 -45
- package/src/cli/cmd/logout.ts +0 -12
- package/src/cli/cmd/me.ts +0 -188
- package/src/cli/cmd/post.ts +0 -25
- package/src/cli/cmd/publish.ts +0 -64
- package/src/cli/cmd/scan.ts +0 -78
- package/src/cli/cmd/search.ts +0 -35
- package/src/cli/cmd/setup.ts +0 -352
- package/src/cli/cmd/tui.ts +0 -20
- package/src/cli/cmd/uninstall.ts +0 -281
- package/src/cli/cmd/update.ts +0 -123
- package/src/cli/cmd/vote.ts +0 -50
- package/src/cli/cmd/whoami.ts +0 -18
- package/src/cli/mcp-print.ts +0 -6
- package/src/cli/ui.ts +0 -250
- package/src/config/index.ts +0 -55
- package/src/flag/index.ts +0 -23
- package/src/global/index.ts +0 -38
- package/src/id/index.ts +0 -20
- package/src/index.ts +0 -200
- package/src/mcp/__tests__/client.test.ts +0 -149
- package/src/mcp/__tests__/e2e.ts +0 -327
- package/src/mcp/__tests__/integration.ts +0 -148
- package/src/mcp/client.ts +0 -148
- package/src/server/index.ts +0 -48
- package/src/storage/chat.ts +0 -71
- package/src/storage/db.ts +0 -85
- package/src/storage/schema.sql.ts +0 -39
- package/src/storage/schema.ts +0 -1
- package/src/tui/app.tsx +0 -184
- package/src/tui/commands.ts +0 -186
- package/src/tui/context/exit.tsx +0 -15
- package/src/tui/context/helper.tsx +0 -25
- package/src/tui/context/route.tsx +0 -24
- package/src/tui/context/theme.tsx +0 -470
- package/src/tui/routes/home.tsx +0 -660
- package/src/tui/routes/model.tsx +0 -210
- package/src/tui/routes/notifications.tsx +0 -87
- package/src/tui/routes/post.tsx +0 -102
- package/src/tui/routes/search.tsx +0 -105
- package/src/tui/routes/setup.tsx +0 -255
- package/src/tui/routes/trending.tsx +0 -107
- package/src/util/__tests__/context.test.ts +0 -31
- package/src/util/__tests__/lazy.test.ts +0 -37
- package/src/util/context.ts +0 -23
- package/src/util/error.ts +0 -46
- package/src/util/lazy.ts +0 -18
- package/src/util/log.ts +0 -142
- package/tsconfig.json +0 -11
package/src/cli/cmd/setup.ts
DELETED
|
@@ -1,352 +0,0 @@
|
|
|
1
|
-
import type { CommandModule } from "yargs"
|
|
2
|
-
import { Auth } from "../../auth"
|
|
3
|
-
import { OAuth } from "../../auth/oauth"
|
|
4
|
-
import { McpBridge } from "../../mcp/client"
|
|
5
|
-
import { UI } from "../ui"
|
|
6
|
-
|
|
7
|
-
export let setupCompleted = false
|
|
8
|
-
|
|
9
|
-
// ─── Auth ────────────────────────────────────────────────────────────────────
|
|
10
|
-
|
|
11
|
-
async function authBrowser(): Promise<boolean> {
|
|
12
|
-
try {
|
|
13
|
-
console.log(` ${UI.Style.TEXT_DIM}Opening browser for login...${UI.Style.TEXT_NORMAL}`)
|
|
14
|
-
|
|
15
|
-
await OAuth.login({
|
|
16
|
-
onUrl: (url) => {
|
|
17
|
-
console.log(` ${UI.Style.TEXT_DIM}If the browser didn't open, visit:${UI.Style.TEXT_NORMAL}`)
|
|
18
|
-
console.log(` ${UI.Style.TEXT_HIGHLIGHT}${url}${UI.Style.TEXT_NORMAL}`)
|
|
19
|
-
console.log("")
|
|
20
|
-
console.log(` ${UI.Style.TEXT_DIM}Waiting for authentication...${UI.Style.TEXT_NORMAL}`)
|
|
21
|
-
},
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
return true
|
|
25
|
-
} catch (err) {
|
|
26
|
-
UI.error(`Authentication failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
27
|
-
return false
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// ─── Scan & Publish ──────────────────────────────────────────────────────────
|
|
32
|
-
|
|
33
|
-
async function shimmerLine(text: string, durationMs = 2000): Promise<void> {
|
|
34
|
-
const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
|
|
35
|
-
const startTime = Date.now()
|
|
36
|
-
let i = 0
|
|
37
|
-
while (Date.now() - startTime < durationMs) {
|
|
38
|
-
Bun.stderr.write(`\r ${UI.Style.TEXT_HIGHLIGHT}${frames[i % frames.length]}${UI.Style.TEXT_NORMAL} ${UI.Style.TEXT_DIM}${text}${UI.Style.TEXT_NORMAL}`)
|
|
39
|
-
i++
|
|
40
|
-
await Bun.sleep(80)
|
|
41
|
-
}
|
|
42
|
-
Bun.stderr.write(`\r ${UI.Style.TEXT_SUCCESS}✓${UI.Style.TEXT_NORMAL} ${text}\n`)
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
async function scanAndPublish(): Promise<void> {
|
|
46
|
-
// Scan with shimmer animation
|
|
47
|
-
const scanPromise = McpBridge.callTool("scan_sessions", { limit: 10 })
|
|
48
|
-
await shimmerLine("Scanning local IDE sessions...", 1500)
|
|
49
|
-
|
|
50
|
-
let sessions: Array<{ id: string; source: string; project: string; title: string }>
|
|
51
|
-
try {
|
|
52
|
-
const text = await scanPromise
|
|
53
|
-
try {
|
|
54
|
-
sessions = JSON.parse(text)
|
|
55
|
-
} catch {
|
|
56
|
-
console.log(` ${text}`)
|
|
57
|
-
return
|
|
58
|
-
}
|
|
59
|
-
} catch (err) {
|
|
60
|
-
UI.warn(`Could not scan sessions: ${err instanceof Error ? err.message : String(err)}`)
|
|
61
|
-
await UI.typeText("No worries — you can scan later with /scan in the app.")
|
|
62
|
-
return
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (sessions.length === 0) {
|
|
66
|
-
await UI.typeText("No IDE sessions found yet. That's okay!")
|
|
67
|
-
await UI.typeText("You can scan later with /scan once you've used an AI-powered IDE.")
|
|
68
|
-
return
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Show what we found
|
|
72
|
-
const sources = [...new Set(sessions.map((s) => s.source))]
|
|
73
|
-
console.log("")
|
|
74
|
-
await UI.typeText(
|
|
75
|
-
`Found ${sessions.length} session${sessions.length > 1 ? "s" : ""} across ${sources.length} IDE${sources.length > 1 ? "s" : ""}: ${sources.join(", ")}`,
|
|
76
|
-
{ charDelay: 10 },
|
|
77
|
-
)
|
|
78
|
-
console.log("")
|
|
79
|
-
|
|
80
|
-
for (const s of sessions.slice(0, 3)) {
|
|
81
|
-
console.log(` ${UI.Style.TEXT_INFO}[${s.source}]${UI.Style.TEXT_NORMAL} ${s.project} — ${s.title.slice(0, 60)}`)
|
|
82
|
-
}
|
|
83
|
-
if (sessions.length > 3) {
|
|
84
|
-
console.log(` ${UI.Style.TEXT_DIM}... and ${sessions.length - 3} more${UI.Style.TEXT_NORMAL}`)
|
|
85
|
-
}
|
|
86
|
-
console.log("")
|
|
87
|
-
|
|
88
|
-
// Analyze with shimmer — show the thinking process step by step
|
|
89
|
-
await shimmerLine("Analyzing sessions for interesting insights...", 1200)
|
|
90
|
-
|
|
91
|
-
// Dry run — preview (with shimmer while waiting)
|
|
92
|
-
let preview: string
|
|
93
|
-
try {
|
|
94
|
-
const postPromise = McpBridge.callTool("auto_post", { dry_run: true })
|
|
95
|
-
await shimmerLine("Crafting a blog post from your best session...", 2000)
|
|
96
|
-
preview = await postPromise
|
|
97
|
-
} catch (err) {
|
|
98
|
-
UI.warn(`Could not generate post: ${err instanceof Error ? err.message : String(err)}`)
|
|
99
|
-
await UI.typeText("You can try again later with /publish in the app.")
|
|
100
|
-
return
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// Display preview with structured layout
|
|
104
|
-
const cleaned = UI.cleanMarkdown(preview)
|
|
105
|
-
console.log("")
|
|
106
|
-
UI.divider()
|
|
107
|
-
|
|
108
|
-
// Parse out key fields for better display
|
|
109
|
-
const lines = cleaned.split("\n")
|
|
110
|
-
let title = ""
|
|
111
|
-
let tags = ""
|
|
112
|
-
let category = ""
|
|
113
|
-
const bodyLines: string[] = []
|
|
114
|
-
|
|
115
|
-
for (const line of lines) {
|
|
116
|
-
const trimmed = line.trim()
|
|
117
|
-
if (!trimmed || trimmed.startsWith("DRY RUN") || trimmed === "---" || trimmed.match(/^─+$/)) continue
|
|
118
|
-
if (trimmed.startsWith("Title:")) { title = trimmed.replace("Title:", "").trim(); continue }
|
|
119
|
-
if (trimmed.startsWith("Tags:")) { tags = trimmed.replace("Tags:", "").trim(); continue }
|
|
120
|
-
if (trimmed.startsWith("Category:")) { category = trimmed.replace("Category:", "").trim(); continue }
|
|
121
|
-
if (trimmed.startsWith("Session:")) continue
|
|
122
|
-
bodyLines.push(trimmed)
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Structured display
|
|
126
|
-
if (title) {
|
|
127
|
-
console.log(` ${UI.Style.TEXT_NORMAL_BOLD}📝 ${title}${UI.Style.TEXT_NORMAL}`)
|
|
128
|
-
console.log("")
|
|
129
|
-
}
|
|
130
|
-
if (category || tags) {
|
|
131
|
-
const meta: string[] = []
|
|
132
|
-
if (category) meta.push(`Category: ${category}`)
|
|
133
|
-
if (tags) meta.push(`Tags: ${tags}`)
|
|
134
|
-
console.log(` ${UI.Style.TEXT_DIM}${meta.join(" · ")}${UI.Style.TEXT_NORMAL}`)
|
|
135
|
-
console.log("")
|
|
136
|
-
}
|
|
137
|
-
if (bodyLines.length > 0) {
|
|
138
|
-
// Show a preview snippet (first few meaningful lines)
|
|
139
|
-
const snippet = bodyLines.slice(0, 6)
|
|
140
|
-
for (const line of snippet) {
|
|
141
|
-
console.log(` ${line}`)
|
|
142
|
-
}
|
|
143
|
-
if (bodyLines.length > 6) {
|
|
144
|
-
console.log(` ${UI.Style.TEXT_DIM}... (${bodyLines.length - 6} more lines)${UI.Style.TEXT_NORMAL}`)
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
UI.divider()
|
|
149
|
-
|
|
150
|
-
// Confirm publish
|
|
151
|
-
const choice = await UI.waitEnter("Press Enter to publish, or Esc to skip")
|
|
152
|
-
|
|
153
|
-
if (choice === "escape") {
|
|
154
|
-
await UI.typeText("Skipped. You can publish later with /publish in the app.")
|
|
155
|
-
return
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// Publish with shimmer
|
|
159
|
-
const publishPromise = McpBridge.callTool("auto_post", { dry_run: false })
|
|
160
|
-
await shimmerLine("Publishing your post...", 1500)
|
|
161
|
-
|
|
162
|
-
try {
|
|
163
|
-
const result = await publishPromise
|
|
164
|
-
console.log("")
|
|
165
|
-
|
|
166
|
-
// Extract URL and details from result
|
|
167
|
-
const urlMatch = result.match(/(?:URL|View at|view at)[:\s]*(https?:\/\/\S+)/i)
|
|
168
|
-
if (urlMatch) {
|
|
169
|
-
UI.success("Post published successfully!")
|
|
170
|
-
console.log("")
|
|
171
|
-
if (title) {
|
|
172
|
-
console.log(` ${UI.Style.TEXT_NORMAL_BOLD}${title}${UI.Style.TEXT_NORMAL}`)
|
|
173
|
-
}
|
|
174
|
-
console.log(` ${UI.Style.TEXT_HIGHLIGHT}${urlMatch[1]}${UI.Style.TEXT_NORMAL}`)
|
|
175
|
-
console.log("")
|
|
176
|
-
await UI.typeText("Your first post is live! Others can now read, comment, and vote on it.", { charDelay: 10 })
|
|
177
|
-
} else {
|
|
178
|
-
UI.success("Post published!")
|
|
179
|
-
// Fallback: show cleaned result
|
|
180
|
-
const cleanResult = UI.cleanMarkdown(result)
|
|
181
|
-
for (const line of cleanResult.split("\n").slice(0, 5)) {
|
|
182
|
-
if (line.trim()) console.log(` ${line.trim()}`)
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
} catch (err) {
|
|
186
|
-
UI.error(`Publish failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
187
|
-
await UI.typeText("You can try again later with /publish.")
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// ─── AI Configuration ────────────────────────────────────────────────────────
|
|
192
|
-
|
|
193
|
-
async function aiConfigPrompt(): Promise<void> {
|
|
194
|
-
const { AIProvider } = await import("../../ai/provider")
|
|
195
|
-
const hasKey = await AIProvider.hasAnyKey()
|
|
196
|
-
|
|
197
|
-
if (hasKey) {
|
|
198
|
-
UI.success("AI provider already configured!")
|
|
199
|
-
return
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
UI.divider()
|
|
203
|
-
|
|
204
|
-
await UI.typeText("One more thing — would you like to configure an AI chat provider?")
|
|
205
|
-
console.log("")
|
|
206
|
-
await UI.typeText("With AI configured, you can interact with the forum using natural language:", { charDelay: 8 })
|
|
207
|
-
console.log(` ${UI.Style.TEXT_DIM}"Show me trending posts about TypeScript"${UI.Style.TEXT_NORMAL}`)
|
|
208
|
-
console.log(` ${UI.Style.TEXT_DIM}"Analyze my latest coding session"${UI.Style.TEXT_NORMAL}`)
|
|
209
|
-
console.log(` ${UI.Style.TEXT_DIM}"Write a post about my React refactoring"${UI.Style.TEXT_NORMAL}`)
|
|
210
|
-
console.log("")
|
|
211
|
-
|
|
212
|
-
const choice = await UI.waitEnter("Press Enter to configure AI, or Esc to skip")
|
|
213
|
-
|
|
214
|
-
if (choice === "escape") {
|
|
215
|
-
console.log("")
|
|
216
|
-
await UI.typeText("No problem! You can configure AI later with /ai in the app.")
|
|
217
|
-
console.log("")
|
|
218
|
-
await UI.typeText("Even without AI, you can use slash commands to interact:", { charDelay: 8 })
|
|
219
|
-
console.log(` ${UI.Style.TEXT_HIGHLIGHT}/scan${UI.Style.TEXT_NORMAL} ${UI.Style.TEXT_DIM}Scan IDE sessions${UI.Style.TEXT_NORMAL}`)
|
|
220
|
-
console.log(` ${UI.Style.TEXT_HIGHLIGHT}/publish${UI.Style.TEXT_NORMAL} ${UI.Style.TEXT_DIM}Publish a post${UI.Style.TEXT_NORMAL}`)
|
|
221
|
-
console.log(` ${UI.Style.TEXT_HIGHLIGHT}/feed${UI.Style.TEXT_NORMAL} ${UI.Style.TEXT_DIM}Browse the forum${UI.Style.TEXT_NORMAL}`)
|
|
222
|
-
console.log(` ${UI.Style.TEXT_HIGHLIGHT}/theme${UI.Style.TEXT_NORMAL} ${UI.Style.TEXT_DIM}Change color theme${UI.Style.TEXT_NORMAL}`)
|
|
223
|
-
return
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// AI config flow: URL → Key with ESC support
|
|
227
|
-
console.log("")
|
|
228
|
-
console.log(` ${UI.Style.TEXT_NORMAL_BOLD}API URL${UI.Style.TEXT_NORMAL} ${UI.Style.TEXT_DIM}(or press Enter to skip URL, Esc to cancel):${UI.Style.TEXT_NORMAL}`)
|
|
229
|
-
const urlResult = await UI.inputWithEscape(` ${UI.Style.TEXT_HIGHLIGHT}❯ ${UI.Style.TEXT_NORMAL}`)
|
|
230
|
-
|
|
231
|
-
if (urlResult === null) {
|
|
232
|
-
// User pressed Esc
|
|
233
|
-
console.log("")
|
|
234
|
-
await UI.typeText("Skipped AI configuration. You can configure later with /ai in the app.")
|
|
235
|
-
return
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
const url = urlResult.trim()
|
|
239
|
-
|
|
240
|
-
console.log(` ${UI.Style.TEXT_NORMAL_BOLD}API Key${UI.Style.TEXT_NORMAL} ${UI.Style.TEXT_DIM}(press Esc to cancel):${UI.Style.TEXT_NORMAL}`)
|
|
241
|
-
const keyResult = await UI.inputWithEscape(` ${UI.Style.TEXT_HIGHLIGHT}❯ ${UI.Style.TEXT_NORMAL}`)
|
|
242
|
-
|
|
243
|
-
if (keyResult === null) {
|
|
244
|
-
// User pressed Esc
|
|
245
|
-
console.log("")
|
|
246
|
-
await UI.typeText("Skipped AI configuration. You can configure later with /ai in the app.")
|
|
247
|
-
return
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
const key = keyResult.trim()
|
|
251
|
-
|
|
252
|
-
// Both empty → friendly skip
|
|
253
|
-
if (!url && !key) {
|
|
254
|
-
console.log("")
|
|
255
|
-
UI.info("No AI configuration provided — skipping for now.")
|
|
256
|
-
await UI.typeText("You can configure AI later with /ai in the app.")
|
|
257
|
-
return
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// Key empty but URL provided → friendly skip
|
|
261
|
-
if (!key) {
|
|
262
|
-
console.log("")
|
|
263
|
-
UI.info("No API key provided — skipping AI configuration.")
|
|
264
|
-
await UI.typeText("You can configure AI later with /ai in the app.")
|
|
265
|
-
return
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
if (key.length < 5) {
|
|
269
|
-
UI.warn("API key seems too short, skipping AI configuration.")
|
|
270
|
-
await UI.typeText("You can configure AI later with /ai in the app.")
|
|
271
|
-
return
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
try {
|
|
275
|
-
const { saveProvider } = await import("../../ai/configure")
|
|
276
|
-
await shimmerLine("Detecting API format...", 1500)
|
|
277
|
-
const result = await saveProvider(url, key)
|
|
278
|
-
if (result.error) {
|
|
279
|
-
UI.warn(result.error)
|
|
280
|
-
await UI.typeText("You can try again later with /ai in the app.")
|
|
281
|
-
} else {
|
|
282
|
-
UI.success(`AI configured! (${result.provider})`)
|
|
283
|
-
}
|
|
284
|
-
} catch (err) {
|
|
285
|
-
UI.warn(`Configuration failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
286
|
-
await UI.typeText("You can try again later with /ai in the app.")
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
// ─── Setup Command ───────────────────────────────────────────────────────────
|
|
291
|
-
|
|
292
|
-
export const SetupCommand: CommandModule = {
|
|
293
|
-
command: "setup",
|
|
294
|
-
describe: "First-time setup wizard: authenticate, scan, publish, configure AI",
|
|
295
|
-
handler: async () => {
|
|
296
|
-
// Phase 1: Welcome
|
|
297
|
-
console.log(UI.logo())
|
|
298
|
-
await UI.typeText("Welcome to CodeBlog!", { charDelay: 20 })
|
|
299
|
-
await UI.typeText("The AI-powered coding forum in your terminal.", { charDelay: 15 })
|
|
300
|
-
console.log("")
|
|
301
|
-
|
|
302
|
-
// Phase 2: Authentication
|
|
303
|
-
const alreadyAuthed = await Auth.authenticated()
|
|
304
|
-
let authenticated = alreadyAuthed
|
|
305
|
-
|
|
306
|
-
if (alreadyAuthed) {
|
|
307
|
-
const token = await Auth.get()
|
|
308
|
-
UI.success(`Already authenticated as ${token?.username || "user"}!`)
|
|
309
|
-
} else {
|
|
310
|
-
await UI.typeText("Let's get you set up. First, we need to authenticate your account.")
|
|
311
|
-
await UI.typeText("You may need to sign up or log in on the website first.", { charDelay: 10 })
|
|
312
|
-
console.log("")
|
|
313
|
-
|
|
314
|
-
await UI.waitEnter("Press Enter to open browser...")
|
|
315
|
-
|
|
316
|
-
authenticated = await authBrowser()
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
if (!authenticated) {
|
|
320
|
-
console.log("")
|
|
321
|
-
UI.info("You can try again with: codeblog setup")
|
|
322
|
-
return
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
const token = await Auth.get()
|
|
326
|
-
UI.success(`Authenticated as ${token?.username || "user"}!`)
|
|
327
|
-
|
|
328
|
-
// Phase 3: Interactive scan & publish
|
|
329
|
-
UI.divider()
|
|
330
|
-
|
|
331
|
-
await UI.typeText("Great! Let's see what you've been working on.")
|
|
332
|
-
await UI.typeText("I'll scan your local IDE sessions to find interesting coding experiences.", { charDelay: 10 })
|
|
333
|
-
console.log("")
|
|
334
|
-
|
|
335
|
-
const scanChoice = await UI.waitEnter("Press Enter to continue, or Esc to skip")
|
|
336
|
-
|
|
337
|
-
if (scanChoice === "enter") {
|
|
338
|
-
await scanAndPublish()
|
|
339
|
-
} else {
|
|
340
|
-
await UI.typeText("Skipped. You can scan and publish later in the app.")
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
// Phase 4: AI configuration
|
|
344
|
-
await aiConfigPrompt()
|
|
345
|
-
|
|
346
|
-
// Phase 5: Transition to TUI
|
|
347
|
-
UI.divider()
|
|
348
|
-
setupCompleted = true
|
|
349
|
-
await UI.typeText("All set! Launching CodeBlog...", { charDelay: 20 })
|
|
350
|
-
await Bun.sleep(800)
|
|
351
|
-
},
|
|
352
|
-
}
|
package/src/cli/cmd/tui.ts
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import type { CommandModule } from "yargs"
|
|
2
|
-
|
|
3
|
-
export const TuiCommand: CommandModule = {
|
|
4
|
-
command: "tui",
|
|
5
|
-
aliases: ["ui"],
|
|
6
|
-
describe: "Launch interactive TUI — browse feed, chat with AI, manage posts",
|
|
7
|
-
builder: (yargs) =>
|
|
8
|
-
yargs
|
|
9
|
-
.option("model", {
|
|
10
|
-
alias: "m",
|
|
11
|
-
describe: "Default AI model",
|
|
12
|
-
type: "string",
|
|
13
|
-
}),
|
|
14
|
-
handler: async (args) => {
|
|
15
|
-
const { tui } = await import("../../tui/app")
|
|
16
|
-
await tui({
|
|
17
|
-
onExit: async () => {},
|
|
18
|
-
})
|
|
19
|
-
},
|
|
20
|
-
}
|
package/src/cli/cmd/uninstall.ts
DELETED
|
@@ -1,281 +0,0 @@
|
|
|
1
|
-
import type { CommandModule } from "yargs"
|
|
2
|
-
import { UI } from "../ui"
|
|
3
|
-
import { Global } from "../../global"
|
|
4
|
-
import fs from "fs/promises"
|
|
5
|
-
import path from "path"
|
|
6
|
-
import os from "os"
|
|
7
|
-
|
|
8
|
-
const DIM = "\x1b[90m"
|
|
9
|
-
const RESET = "\x1b[0m"
|
|
10
|
-
const BOLD = "\x1b[1m"
|
|
11
|
-
const RED = "\x1b[91m"
|
|
12
|
-
const GREEN = "\x1b[92m"
|
|
13
|
-
const YELLOW = "\x1b[93m"
|
|
14
|
-
const CYAN = "\x1b[36m"
|
|
15
|
-
|
|
16
|
-
const W = 60 // inner width of the box
|
|
17
|
-
const BAR = `${DIM}│${RESET}`
|
|
18
|
-
|
|
19
|
-
/** Strip ANSI escape sequences to get visible character length */
|
|
20
|
-
function visLen(s: string): number {
|
|
21
|
-
return s.replace(/\x1b\[[0-9;]*m/g, "").length
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function line(text = "") {
|
|
25
|
-
const pad = Math.max(0, W - visLen(text) - 1)
|
|
26
|
-
console.log(` ${BAR} ${text}${" ".repeat(pad)}${BAR}`)
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function lineSuccess(text: string) {
|
|
30
|
-
line(`${GREEN}✓${RESET} ${text}`)
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function lineWarn(text: string) {
|
|
34
|
-
line(`${YELLOW}⚠${RESET} ${text}`)
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function lineInfo(text: string) {
|
|
38
|
-
line(`${DIM}${text}${RESET}`)
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export const UninstallCommand: CommandModule = {
|
|
42
|
-
command: "uninstall",
|
|
43
|
-
describe: "Uninstall codeblog CLI and remove all local data",
|
|
44
|
-
builder: (yargs) =>
|
|
45
|
-
yargs.option("keep-data", {
|
|
46
|
-
describe: "Keep config, data, and cache (only remove binary)",
|
|
47
|
-
type: "boolean",
|
|
48
|
-
default: false,
|
|
49
|
-
}),
|
|
50
|
-
handler: async (args) => {
|
|
51
|
-
const keepData = args["keep-data"] as boolean
|
|
52
|
-
const binPath = process.execPath
|
|
53
|
-
const pkg = await import("../../../package.json")
|
|
54
|
-
|
|
55
|
-
console.log(UI.logo())
|
|
56
|
-
|
|
57
|
-
// Top border
|
|
58
|
-
console.log(` ${DIM}┌${"─".repeat(W)}┐${RESET}`)
|
|
59
|
-
line()
|
|
60
|
-
line(`${RED}${BOLD}Uninstall CodeBlog${RESET} ${DIM}v${pkg.version}${RESET}`)
|
|
61
|
-
line()
|
|
62
|
-
|
|
63
|
-
// Show what will be removed
|
|
64
|
-
line(`${BOLD}The following will be removed:${RESET}`)
|
|
65
|
-
line()
|
|
66
|
-
line(` ${DIM}Binary${RESET} ${binPath}`)
|
|
67
|
-
|
|
68
|
-
if (!keepData) {
|
|
69
|
-
const dirs = [Global.Path.config, Global.Path.data, Global.Path.cache, Global.Path.state]
|
|
70
|
-
for (const dir of dirs) {
|
|
71
|
-
const label = dir.includes("config") ? "Config" : dir.includes("data") || dir.includes("share") ? "Data" : dir.includes("cache") ? "Cache" : "State"
|
|
72
|
-
try {
|
|
73
|
-
await fs.access(dir)
|
|
74
|
-
line(` ${DIM}${label.padEnd(10)}${RESET}${dir}`)
|
|
75
|
-
} catch {
|
|
76
|
-
// dir doesn't exist, skip
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if (os.platform() !== "win32") {
|
|
82
|
-
const rcFiles = getShellRcFiles()
|
|
83
|
-
for (const rc of rcFiles) {
|
|
84
|
-
try {
|
|
85
|
-
const content = await fs.readFile(rc, "utf-8")
|
|
86
|
-
if (content.includes("# codeblog")) {
|
|
87
|
-
line(` ${DIM}Shell RC${RESET} ${rc} ${DIM}(PATH entry)${RESET}`)
|
|
88
|
-
}
|
|
89
|
-
} catch {}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
line()
|
|
94
|
-
|
|
95
|
-
// Separator
|
|
96
|
-
console.log(` ${DIM}├${"─".repeat(W)}┤${RESET}`)
|
|
97
|
-
line()
|
|
98
|
-
|
|
99
|
-
// Confirm
|
|
100
|
-
line(`${BOLD}Type "yes" to confirm uninstall:${RESET}`)
|
|
101
|
-
process.stderr.write(` ${BAR} ${DIM}> ${RESET}`)
|
|
102
|
-
const answer = await readLine()
|
|
103
|
-
// Print the line with right border after input
|
|
104
|
-
const inputDisplay = answer || ""
|
|
105
|
-
const inputLine = `${DIM}> ${RESET}${inputDisplay}`
|
|
106
|
-
const inputPad = Math.max(0, W - visLen(inputLine) - 1)
|
|
107
|
-
process.stderr.write(`\x1b[A\r ${BAR} ${inputLine}${" ".repeat(inputPad)}${BAR}\n`)
|
|
108
|
-
|
|
109
|
-
if (answer.toLowerCase() !== "yes") {
|
|
110
|
-
line()
|
|
111
|
-
line(`Uninstall cancelled.`)
|
|
112
|
-
line()
|
|
113
|
-
console.log(` ${DIM}└${"─".repeat(W)}┘${RESET}`)
|
|
114
|
-
console.log("")
|
|
115
|
-
return
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
line()
|
|
119
|
-
|
|
120
|
-
// Execute uninstall steps
|
|
121
|
-
// 1. Remove data directories
|
|
122
|
-
if (!keepData) {
|
|
123
|
-
const dirs = [Global.Path.config, Global.Path.data, Global.Path.cache, Global.Path.state]
|
|
124
|
-
for (const dir of dirs) {
|
|
125
|
-
try {
|
|
126
|
-
await fs.access(dir)
|
|
127
|
-
await fs.rm(dir, { recursive: true, force: true })
|
|
128
|
-
lineSuccess(`Removed ${dir}`)
|
|
129
|
-
} catch {
|
|
130
|
-
// dir doesn't exist
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// 2. Clean shell rc PATH entries (macOS/Linux only)
|
|
136
|
-
if (os.platform() !== "win32") {
|
|
137
|
-
await cleanShellRc()
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// 3. Remove the binary
|
|
141
|
-
const binDir = path.dirname(binPath)
|
|
142
|
-
|
|
143
|
-
if (os.platform() === "win32") {
|
|
144
|
-
lineInfo(`Binary at ${binPath}`)
|
|
145
|
-
lineWarn(`On Windows, delete manually after exit:`)
|
|
146
|
-
line(` ${CYAN}del "${binPath}"${RESET}`)
|
|
147
|
-
await cleanWindowsPath(binDir)
|
|
148
|
-
} else {
|
|
149
|
-
try {
|
|
150
|
-
await fs.unlink(binPath)
|
|
151
|
-
lineSuccess(`Removed ${binPath}`)
|
|
152
|
-
} catch (e: any) {
|
|
153
|
-
if (e.code === "EBUSY" || e.code === "ETXTBSY") {
|
|
154
|
-
const { spawn } = await import("child_process")
|
|
155
|
-
spawn("sh", ["-c", `sleep 1 && rm -f "${binPath}"`], {
|
|
156
|
-
detached: true,
|
|
157
|
-
stdio: "ignore",
|
|
158
|
-
}).unref()
|
|
159
|
-
lineSuccess(`Binary will be removed: ${binPath}`)
|
|
160
|
-
} else {
|
|
161
|
-
lineWarn(`Could not remove binary: ${e.message}`)
|
|
162
|
-
line(` Run manually: ${CYAN}rm "${binPath}"${RESET}`)
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
line()
|
|
168
|
-
|
|
169
|
-
// Separator
|
|
170
|
-
console.log(` ${DIM}├${"─".repeat(W)}┤${RESET}`)
|
|
171
|
-
line()
|
|
172
|
-
line(`${GREEN}${BOLD}CodeBlog has been uninstalled.${RESET} Goodbye!`)
|
|
173
|
-
line()
|
|
174
|
-
|
|
175
|
-
// Bottom border
|
|
176
|
-
console.log(` ${DIM}└${"─".repeat(W)}┘${RESET}`)
|
|
177
|
-
console.log("")
|
|
178
|
-
},
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
182
|
-
|
|
183
|
-
function readLine(): Promise<string> {
|
|
184
|
-
const stdin = process.stdin
|
|
185
|
-
return new Promise((resolve) => {
|
|
186
|
-
const wasRaw = stdin.isRaw
|
|
187
|
-
if (stdin.isTTY && stdin.setRawMode) stdin.setRawMode(true)
|
|
188
|
-
|
|
189
|
-
let buf = ""
|
|
190
|
-
const onData = (ch: Buffer) => {
|
|
191
|
-
const c = ch.toString("utf8")
|
|
192
|
-
if (c === "\u0003") {
|
|
193
|
-
if (stdin.isTTY && stdin.setRawMode) stdin.setRawMode(wasRaw ?? false)
|
|
194
|
-
stdin.removeListener("data", onData)
|
|
195
|
-
process.exit(130)
|
|
196
|
-
}
|
|
197
|
-
if (c === "\r" || c === "\n") {
|
|
198
|
-
if (stdin.isTTY && stdin.setRawMode) stdin.setRawMode(wasRaw ?? false)
|
|
199
|
-
stdin.removeListener("data", onData)
|
|
200
|
-
process.stderr.write("\n")
|
|
201
|
-
resolve(buf)
|
|
202
|
-
return
|
|
203
|
-
}
|
|
204
|
-
if (c === "\u007f" || c === "\b") {
|
|
205
|
-
if (buf.length > 0) {
|
|
206
|
-
buf = buf.slice(0, -1)
|
|
207
|
-
process.stderr.write("\b \b")
|
|
208
|
-
}
|
|
209
|
-
return
|
|
210
|
-
}
|
|
211
|
-
const clean = c.replace(/[\x00-\x1f\x7f]/g, "")
|
|
212
|
-
if (clean) {
|
|
213
|
-
buf += clean
|
|
214
|
-
process.stderr.write(clean)
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
stdin.on("data", onData)
|
|
218
|
-
})
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
function getShellRcFiles(): string[] {
|
|
222
|
-
const home = os.homedir()
|
|
223
|
-
return [
|
|
224
|
-
path.join(home, ".zshrc"),
|
|
225
|
-
path.join(home, ".bashrc"),
|
|
226
|
-
path.join(home, ".profile"),
|
|
227
|
-
]
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
async function cleanShellRc() {
|
|
231
|
-
for (const rc of getShellRcFiles()) {
|
|
232
|
-
try {
|
|
233
|
-
const content = await fs.readFile(rc, "utf-8")
|
|
234
|
-
if (!content.includes("# codeblog")) continue
|
|
235
|
-
|
|
236
|
-
const lines = content.split("\n")
|
|
237
|
-
const filtered: string[] = []
|
|
238
|
-
for (let i = 0; i < lines.length; i++) {
|
|
239
|
-
if (lines[i]!.trim() === "# codeblog") {
|
|
240
|
-
if (i + 1 < lines.length && lines[i + 1]!.includes("export PATH=")) {
|
|
241
|
-
i++
|
|
242
|
-
}
|
|
243
|
-
if (filtered.length > 0 && filtered[filtered.length - 1]!.trim() === "") {
|
|
244
|
-
filtered.pop()
|
|
245
|
-
}
|
|
246
|
-
continue
|
|
247
|
-
}
|
|
248
|
-
filtered.push(lines[i]!)
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
await fs.writeFile(rc, filtered.join("\n"), "utf-8")
|
|
252
|
-
lineSuccess(`Cleaned PATH from ${rc}`)
|
|
253
|
-
} catch {
|
|
254
|
-
// file doesn't exist or not readable
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
async function cleanWindowsPath(binDir: string) {
|
|
260
|
-
try {
|
|
261
|
-
const { exec } = await import("child_process")
|
|
262
|
-
const { promisify } = await import("util")
|
|
263
|
-
const execAsync = promisify(exec)
|
|
264
|
-
|
|
265
|
-
const { stdout } = await execAsync(
|
|
266
|
-
`powershell -Command "[Environment]::GetEnvironmentVariable('Path','User')"`,
|
|
267
|
-
)
|
|
268
|
-
const currentPath = stdout.trim()
|
|
269
|
-
const parts = currentPath.split(";").filter((p) => p && p !== binDir)
|
|
270
|
-
const newPath = parts.join(";")
|
|
271
|
-
|
|
272
|
-
if (newPath !== currentPath) {
|
|
273
|
-
await execAsync(
|
|
274
|
-
`powershell -Command "[Environment]::SetEnvironmentVariable('Path','${newPath}','User')"`,
|
|
275
|
-
)
|
|
276
|
-
lineSuccess(`Removed ${binDir} from user PATH`)
|
|
277
|
-
}
|
|
278
|
-
} catch {
|
|
279
|
-
lineWarn("Could not clean PATH. Remove manually from System Settings.")
|
|
280
|
-
}
|
|
281
|
-
}
|