codeblog-app 2.3.2 → 2.3.3
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/drizzle/0000_init.sql +34 -0
- package/drizzle/meta/_journal.json +13 -0
- package/drizzle.config.ts +10 -0
- package/package.json +73 -8
- package/src/ai/__tests__/chat.test.ts +188 -0
- package/src/ai/__tests__/compat.test.ts +46 -0
- package/src/ai/__tests__/home.ai-stream.integration.test.ts +77 -0
- package/src/ai/__tests__/provider-registry.test.ts +98 -0
- package/src/ai/__tests__/provider.test.ts +239 -0
- package/src/ai/__tests__/stream-events.test.ts +152 -0
- package/src/ai/__tests__/tools.test.ts +93 -0
- package/src/ai/chat.ts +336 -0
- package/src/ai/configure.ts +144 -0
- package/src/ai/models.ts +67 -0
- package/src/ai/provider-registry.ts +150 -0
- package/src/ai/provider.ts +264 -0
- package/src/ai/stream-events.ts +64 -0
- package/src/ai/tools.ts +118 -0
- package/src/ai/types.ts +105 -0
- package/src/auth/index.ts +49 -0
- package/src/auth/oauth.ts +141 -0
- package/src/cli/__tests__/commands.test.ts +229 -0
- package/src/cli/cmd/agent.ts +97 -0
- package/src/cli/cmd/ai.ts +10 -0
- package/src/cli/cmd/chat.ts +190 -0
- package/src/cli/cmd/comment.ts +67 -0
- package/src/cli/cmd/config.ts +154 -0
- package/src/cli/cmd/feed.ts +53 -0
- package/src/cli/cmd/forum.ts +106 -0
- package/src/cli/cmd/login.ts +45 -0
- package/src/cli/cmd/logout.ts +14 -0
- package/src/cli/cmd/me.ts +188 -0
- package/src/cli/cmd/post.ts +25 -0
- package/src/cli/cmd/publish.ts +64 -0
- package/src/cli/cmd/scan.ts +78 -0
- package/src/cli/cmd/search.ts +35 -0
- package/src/cli/cmd/setup.ts +632 -0
- package/src/cli/cmd/tui.ts +20 -0
- package/src/cli/cmd/uninstall.ts +281 -0
- package/src/cli/cmd/update.ts +139 -0
- package/src/cli/cmd/vote.ts +50 -0
- package/src/cli/cmd/whoami.ts +18 -0
- package/src/cli/mcp-print.ts +6 -0
- package/src/cli/ui.ts +357 -0
- package/src/config/index.ts +125 -0
- package/src/flag/index.ts +23 -0
- package/src/global/index.ts +38 -0
- package/src/id/index.ts +20 -0
- package/src/index.ts +212 -0
- package/src/mcp/__tests__/client.test.ts +149 -0
- package/src/mcp/__tests__/e2e.ts +331 -0
- package/src/mcp/__tests__/integration.ts +148 -0
- package/src/mcp/client.ts +118 -0
- package/src/server/index.ts +48 -0
- package/src/storage/chat.ts +73 -0
- package/src/storage/db.ts +85 -0
- package/src/storage/schema.sql.ts +39 -0
- package/src/storage/schema.ts +1 -0
- package/src/tui/__tests__/input-intent.test.ts +27 -0
- package/src/tui/__tests__/stream-assembler.test.ts +33 -0
- package/src/tui/ai-stream.ts +28 -0
- package/src/tui/app.tsx +224 -0
- package/src/tui/commands.ts +224 -0
- package/src/tui/context/exit.tsx +15 -0
- package/src/tui/context/helper.tsx +25 -0
- package/src/tui/context/route.tsx +24 -0
- package/src/tui/context/theme.tsx +471 -0
- package/src/tui/input-intent.ts +26 -0
- package/src/tui/routes/home.tsx +1053 -0
- package/src/tui/routes/model.tsx +213 -0
- package/src/tui/routes/notifications.tsx +87 -0
- package/src/tui/routes/post.tsx +102 -0
- package/src/tui/routes/search.tsx +105 -0
- package/src/tui/routes/setup.tsx +267 -0
- package/src/tui/routes/trending.tsx +107 -0
- package/src/tui/stream-assembler.ts +49 -0
- package/src/util/__tests__/context.test.ts +31 -0
- package/src/util/__tests__/lazy.test.ts +37 -0
- package/src/util/context.ts +23 -0
- package/src/util/error.ts +46 -0
- package/src/util/lazy.ts +18 -0
- package/src/util/log.ts +144 -0
- package/tsconfig.json +11 -0
|
@@ -0,0 +1,281 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import type { CommandModule } from "yargs"
|
|
2
|
+
import { UI } from "../ui"
|
|
3
|
+
|
|
4
|
+
export const UpdateCommand: CommandModule = {
|
|
5
|
+
command: "update",
|
|
6
|
+
describe: "Update codeblog CLI to the latest version",
|
|
7
|
+
builder: (yargs) =>
|
|
8
|
+
yargs.option("force", {
|
|
9
|
+
describe: "Force update even if already on latest",
|
|
10
|
+
type: "boolean",
|
|
11
|
+
default: false,
|
|
12
|
+
}),
|
|
13
|
+
handler: async (args) => {
|
|
14
|
+
const pkg = await import("../../../package.json")
|
|
15
|
+
const current = pkg.version
|
|
16
|
+
|
|
17
|
+
UI.info(`Current version: v${current}`)
|
|
18
|
+
UI.info("Checking for updates...")
|
|
19
|
+
|
|
20
|
+
const checkController = new AbortController()
|
|
21
|
+
const checkTimeout = setTimeout(() => checkController.abort(), 10_000)
|
|
22
|
+
let res: Response
|
|
23
|
+
try {
|
|
24
|
+
res = await fetch("https://registry.npmjs.org/codeblog-app/latest", { signal: checkController.signal })
|
|
25
|
+
} catch (e: any) {
|
|
26
|
+
clearTimeout(checkTimeout)
|
|
27
|
+
if (e.name === "AbortError") {
|
|
28
|
+
UI.error("Version check timed out (10s). Please check your network and try again.")
|
|
29
|
+
} else {
|
|
30
|
+
UI.error(`Failed to check for updates: ${e.message}`)
|
|
31
|
+
}
|
|
32
|
+
process.exitCode = 1
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
clearTimeout(checkTimeout)
|
|
36
|
+
if (!res.ok) {
|
|
37
|
+
UI.error("Failed to check for updates")
|
|
38
|
+
process.exitCode = 1
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const data = await res.json() as { version: string }
|
|
43
|
+
const latest = data.version
|
|
44
|
+
|
|
45
|
+
if (current === latest && !args.force) {
|
|
46
|
+
UI.success(`Already on latest version v${current}`)
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
UI.info(`Updating v${current} → v${latest}...`)
|
|
51
|
+
|
|
52
|
+
const os = process.platform === "win32" ? "windows" : process.platform === "darwin" ? "darwin" : "linux"
|
|
53
|
+
const arch = process.arch === "arm64" ? "arm64" : "x64"
|
|
54
|
+
const platform = `${os}-${arch}`
|
|
55
|
+
const pkg_name = `codeblog-app-${platform}`
|
|
56
|
+
const url = `https://registry.npmjs.org/${pkg_name}/-/${pkg_name}-${latest}.tgz`
|
|
57
|
+
|
|
58
|
+
const tmpdir = (await import("os")).tmpdir()
|
|
59
|
+
const path = await import("path")
|
|
60
|
+
const fs = await import("fs/promises")
|
|
61
|
+
const tmp = path.join(tmpdir, `codeblog-update-${Date.now()}`)
|
|
62
|
+
await fs.mkdir(tmp, { recursive: true })
|
|
63
|
+
|
|
64
|
+
UI.info("Downloading...")
|
|
65
|
+
const tgz = path.join(tmp, "pkg.tgz")
|
|
66
|
+
const dlController = new AbortController()
|
|
67
|
+
const dlTimeout = setTimeout(() => dlController.abort(), 60_000)
|
|
68
|
+
let dlRes: Response
|
|
69
|
+
try {
|
|
70
|
+
dlRes = await fetch(url, { signal: dlController.signal })
|
|
71
|
+
} catch (e: any) {
|
|
72
|
+
clearTimeout(dlTimeout)
|
|
73
|
+
await fs.rm(tmp, { recursive: true, force: true }).catch(() => {})
|
|
74
|
+
if (e.name === "AbortError") {
|
|
75
|
+
UI.error("Download timed out (60s). Please check your network and try again.")
|
|
76
|
+
} else {
|
|
77
|
+
UI.error(`Download failed: ${e.message}`)
|
|
78
|
+
}
|
|
79
|
+
process.exitCode = 1
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
clearTimeout(dlTimeout)
|
|
83
|
+
if (!dlRes.ok) {
|
|
84
|
+
UI.error(`Failed to download update for ${platform} (HTTP ${dlRes.status})`)
|
|
85
|
+
await fs.rm(tmp, { recursive: true, force: true }).catch(() => {})
|
|
86
|
+
process.exitCode = 1
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const arrayBuf = await dlRes.arrayBuffer()
|
|
91
|
+
await fs.writeFile(tgz, Buffer.from(arrayBuf))
|
|
92
|
+
|
|
93
|
+
UI.info("Extracting...")
|
|
94
|
+
const proc = Bun.spawn(["tar", "-xzf", tgz, "-C", tmp], { stdout: "ignore", stderr: "ignore" })
|
|
95
|
+
await proc.exited
|
|
96
|
+
|
|
97
|
+
const bin = process.execPath
|
|
98
|
+
const ext = os === "windows" ? ".exe" : ""
|
|
99
|
+
const src = path.join(tmp, "package", "bin", `codeblog${ext}`)
|
|
100
|
+
|
|
101
|
+
UI.info("Installing...")
|
|
102
|
+
// On macOS/Linux, remove the running binary first to avoid ETXTBSY
|
|
103
|
+
if (os !== "windows") {
|
|
104
|
+
try {
|
|
105
|
+
await fs.unlink(bin)
|
|
106
|
+
} catch {
|
|
107
|
+
// ignore if removal fails
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
await fs.copyFile(src, bin)
|
|
111
|
+
if (os !== "windows") {
|
|
112
|
+
await fs.chmod(bin, 0o755)
|
|
113
|
+
}
|
|
114
|
+
if (os === "darwin") {
|
|
115
|
+
await Bun.spawn(["codesign", "--remove-signature", bin], { stdout: "ignore", stderr: "ignore" }).exited
|
|
116
|
+
const cs = Bun.spawn(["codesign", "--sign", "-", "--force", bin], { stdout: "ignore", stderr: "ignore" })
|
|
117
|
+
if ((await cs.exited) !== 0) {
|
|
118
|
+
await fs.rm(tmp, { recursive: true, force: true })
|
|
119
|
+
UI.error("Update installed but macOS code signing failed. Please reinstall with install.sh.")
|
|
120
|
+
process.exitCode = 1
|
|
121
|
+
return
|
|
122
|
+
}
|
|
123
|
+
const verify = Bun.spawn(["codesign", "--verify", "--deep", "--strict", bin], {
|
|
124
|
+
stdout: "ignore",
|
|
125
|
+
stderr: "ignore",
|
|
126
|
+
})
|
|
127
|
+
if ((await verify.exited) !== 0) {
|
|
128
|
+
await fs.rm(tmp, { recursive: true, force: true })
|
|
129
|
+
UI.error("Update installed but signature verification failed. Please reinstall with install.sh.")
|
|
130
|
+
process.exitCode = 1
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
await fs.rm(tmp, { recursive: true, force: true })
|
|
136
|
+
|
|
137
|
+
UI.success(`Updated to v${latest}!`)
|
|
138
|
+
},
|
|
139
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { CommandModule } from "yargs"
|
|
2
|
+
import { McpBridge } from "../../mcp/client"
|
|
3
|
+
import { UI } from "../ui"
|
|
4
|
+
|
|
5
|
+
export const VoteCommand: CommandModule = {
|
|
6
|
+
command: "vote <post_id>",
|
|
7
|
+
describe: "Vote on a post (up/down/remove)",
|
|
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("up", {
|
|
16
|
+
alias: "u",
|
|
17
|
+
describe: "Upvote",
|
|
18
|
+
type: "boolean",
|
|
19
|
+
})
|
|
20
|
+
.option("down", {
|
|
21
|
+
alias: "d",
|
|
22
|
+
describe: "Downvote",
|
|
23
|
+
type: "boolean",
|
|
24
|
+
})
|
|
25
|
+
.option("remove", {
|
|
26
|
+
describe: "Remove existing vote",
|
|
27
|
+
type: "boolean",
|
|
28
|
+
})
|
|
29
|
+
.conflicts("up", "down")
|
|
30
|
+
.conflicts("up", "remove")
|
|
31
|
+
.conflicts("down", "remove"),
|
|
32
|
+
handler: async (args) => {
|
|
33
|
+
let value = 1 // default upvote
|
|
34
|
+
if (args.down) value = -1
|
|
35
|
+
if (args.remove) value = 0
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const text = await McpBridge.callTool("vote_on_post", {
|
|
39
|
+
post_id: args.post_id,
|
|
40
|
+
value,
|
|
41
|
+
})
|
|
42
|
+
console.log("")
|
|
43
|
+
console.log(` ${text}`)
|
|
44
|
+
console.log("")
|
|
45
|
+
} catch (err) {
|
|
46
|
+
UI.error(`Vote failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
47
|
+
process.exitCode = 1
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { CommandModule } from "yargs"
|
|
2
|
+
import { mcpPrint } from "../mcp-print"
|
|
3
|
+
import { UI } from "../ui"
|
|
4
|
+
|
|
5
|
+
export const WhoamiCommand: CommandModule = {
|
|
6
|
+
command: "whoami",
|
|
7
|
+
describe: "Show current auth status",
|
|
8
|
+
handler: async () => {
|
|
9
|
+
try {
|
|
10
|
+
console.log("")
|
|
11
|
+
await mcpPrint("codeblog_status")
|
|
12
|
+
console.log("")
|
|
13
|
+
} catch (err) {
|
|
14
|
+
UI.error(`Status check failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
15
|
+
process.exitCode = 1
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
}
|