codeblog-app 2.1.6 → 2.1.7
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 +6 -6
- package/src/cli/cmd/uninstall.ts +168 -43
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "codeblog-app",
|
|
4
|
-
"version": "2.1.
|
|
4
|
+
"version": "2.1.7",
|
|
5
5
|
"description": "CLI client for CodeBlog — the forum where AI writes the posts",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"license": "MIT",
|
|
@@ -56,11 +56,11 @@
|
|
|
56
56
|
"typescript": "5.8.2"
|
|
57
57
|
},
|
|
58
58
|
"optionalDependencies": {
|
|
59
|
-
"codeblog-app-darwin-arm64": "2.1.
|
|
60
|
-
"codeblog-app-darwin-x64": "2.1.
|
|
61
|
-
"codeblog-app-linux-arm64": "2.1.
|
|
62
|
-
"codeblog-app-linux-x64": "2.1.
|
|
63
|
-
"codeblog-app-windows-x64": "2.1.
|
|
59
|
+
"codeblog-app-darwin-arm64": "2.1.7",
|
|
60
|
+
"codeblog-app-darwin-x64": "2.1.7",
|
|
61
|
+
"codeblog-app-linux-arm64": "2.1.7",
|
|
62
|
+
"codeblog-app-linux-x64": "2.1.7",
|
|
63
|
+
"codeblog-app-windows-x64": "2.1.7"
|
|
64
64
|
},
|
|
65
65
|
"dependencies": {
|
|
66
66
|
"@ai-sdk/anthropic": "^3.0.44",
|
package/src/cli/cmd/uninstall.ts
CHANGED
|
@@ -5,6 +5,39 @@ import fs from "fs/promises"
|
|
|
5
5
|
import path from "path"
|
|
6
6
|
import os from "os"
|
|
7
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
|
+
|
|
8
41
|
export const UninstallCommand: CommandModule = {
|
|
9
42
|
command: "uninstall",
|
|
10
43
|
describe: "Uninstall codeblog CLI and remove all local data",
|
|
@@ -15,35 +48,86 @@ export const UninstallCommand: CommandModule = {
|
|
|
15
48
|
default: false,
|
|
16
49
|
}),
|
|
17
50
|
handler: async (args) => {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
+
}
|
|
27
79
|
}
|
|
28
|
-
UI.println("")
|
|
29
80
|
|
|
30
|
-
|
|
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
|
+
|
|
31
109
|
if (answer.toLowerCase() !== "yes") {
|
|
32
|
-
|
|
110
|
+
line()
|
|
111
|
+
line(`Uninstall cancelled.`)
|
|
112
|
+
line()
|
|
113
|
+
console.log(` ${DIM}└${"─".repeat(W)}┘${RESET}`)
|
|
114
|
+
console.log("")
|
|
33
115
|
return
|
|
34
116
|
}
|
|
35
117
|
|
|
36
|
-
|
|
118
|
+
line()
|
|
37
119
|
|
|
120
|
+
// Execute uninstall steps
|
|
38
121
|
// 1. Remove data directories
|
|
39
|
-
if (!
|
|
122
|
+
if (!keepData) {
|
|
40
123
|
const dirs = [Global.Path.config, Global.Path.data, Global.Path.cache, Global.Path.state]
|
|
41
124
|
for (const dir of dirs) {
|
|
42
125
|
try {
|
|
126
|
+
await fs.access(dir)
|
|
43
127
|
await fs.rm(dir, { recursive: true, force: true })
|
|
44
|
-
|
|
128
|
+
lineSuccess(`Removed ${dir}`)
|
|
45
129
|
} catch {
|
|
46
|
-
//
|
|
130
|
+
// dir doesn't exist
|
|
47
131
|
}
|
|
48
132
|
}
|
|
49
133
|
}
|
|
@@ -53,67 +137,109 @@ export const UninstallCommand: CommandModule = {
|
|
|
53
137
|
await cleanShellRc()
|
|
54
138
|
}
|
|
55
139
|
|
|
56
|
-
// 3. Remove the binary
|
|
57
|
-
const binPath = process.execPath
|
|
140
|
+
// 3. Remove the binary
|
|
58
141
|
const binDir = path.dirname(binPath)
|
|
59
142
|
|
|
60
143
|
if (os.platform() === "win32") {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
UI.println(` ${UI.Style.TEXT_HIGHLIGHT}del "${binPath}"${UI.Style.TEXT_NORMAL}`)
|
|
65
|
-
|
|
66
|
-
// Try to remove from PATH
|
|
144
|
+
lineInfo(`Binary at ${binPath}`)
|
|
145
|
+
lineWarn(`On Windows, delete manually after exit:`)
|
|
146
|
+
line(` ${CYAN}del "${binPath}"${RESET}`)
|
|
67
147
|
await cleanWindowsPath(binDir)
|
|
68
148
|
} else {
|
|
69
149
|
try {
|
|
70
150
|
await fs.unlink(binPath)
|
|
71
|
-
|
|
151
|
+
lineSuccess(`Removed ${binPath}`)
|
|
72
152
|
} catch (e: any) {
|
|
73
153
|
if (e.code === "EBUSY" || e.code === "ETXTBSY") {
|
|
74
|
-
// Binary is running, schedule delete via shell
|
|
75
154
|
const { spawn } = await import("child_process")
|
|
76
155
|
spawn("sh", ["-c", `sleep 1 && rm -f "${binPath}"`], {
|
|
77
156
|
detached: true,
|
|
78
157
|
stdio: "ignore",
|
|
79
158
|
}).unref()
|
|
80
|
-
|
|
159
|
+
lineSuccess(`Binary will be removed: ${binPath}`)
|
|
81
160
|
} else {
|
|
82
|
-
|
|
83
|
-
|
|
161
|
+
lineWarn(`Could not remove binary: ${e.message}`)
|
|
162
|
+
line(` Run manually: ${CYAN}rm "${binPath}"${RESET}`)
|
|
84
163
|
}
|
|
85
164
|
}
|
|
86
165
|
}
|
|
87
166
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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("")
|
|
91
178
|
},
|
|
92
179
|
}
|
|
93
180
|
|
|
94
|
-
|
|
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[] {
|
|
95
222
|
const home = os.homedir()
|
|
96
|
-
|
|
223
|
+
return [
|
|
97
224
|
path.join(home, ".zshrc"),
|
|
98
225
|
path.join(home, ".bashrc"),
|
|
99
226
|
path.join(home, ".profile"),
|
|
100
227
|
]
|
|
228
|
+
}
|
|
101
229
|
|
|
102
|
-
|
|
230
|
+
async function cleanShellRc() {
|
|
231
|
+
for (const rc of getShellRcFiles()) {
|
|
103
232
|
try {
|
|
104
233
|
const content = await fs.readFile(rc, "utf-8")
|
|
105
234
|
if (!content.includes("# codeblog")) continue
|
|
106
235
|
|
|
107
|
-
// Remove the "# codeblog" line and the export PATH line that follows
|
|
108
236
|
const lines = content.split("\n")
|
|
109
237
|
const filtered: string[] = []
|
|
110
238
|
for (let i = 0; i < lines.length; i++) {
|
|
111
239
|
if (lines[i]!.trim() === "# codeblog") {
|
|
112
|
-
// Skip this line and the next export PATH line
|
|
113
240
|
if (i + 1 < lines.length && lines[i + 1]!.includes("export PATH=")) {
|
|
114
|
-
i++
|
|
241
|
+
i++
|
|
115
242
|
}
|
|
116
|
-
// Also skip a preceding blank line if present
|
|
117
243
|
if (filtered.length > 0 && filtered[filtered.length - 1]!.trim() === "") {
|
|
118
244
|
filtered.pop()
|
|
119
245
|
}
|
|
@@ -123,7 +249,7 @@ async function cleanShellRc() {
|
|
|
123
249
|
}
|
|
124
250
|
|
|
125
251
|
await fs.writeFile(rc, filtered.join("\n"), "utf-8")
|
|
126
|
-
|
|
252
|
+
lineSuccess(`Cleaned PATH from ${rc}`)
|
|
127
253
|
} catch {
|
|
128
254
|
// file doesn't exist or not readable
|
|
129
255
|
}
|
|
@@ -136,7 +262,6 @@ async function cleanWindowsPath(binDir: string) {
|
|
|
136
262
|
const { promisify } = await import("util")
|
|
137
263
|
const execAsync = promisify(exec)
|
|
138
264
|
|
|
139
|
-
// Read current user PATH
|
|
140
265
|
const { stdout } = await execAsync(
|
|
141
266
|
`powershell -Command "[Environment]::GetEnvironmentVariable('Path','User')"`,
|
|
142
267
|
)
|
|
@@ -148,9 +273,9 @@ async function cleanWindowsPath(binDir: string) {
|
|
|
148
273
|
await execAsync(
|
|
149
274
|
`powershell -Command "[Environment]::SetEnvironmentVariable('Path','${newPath}','User')"`,
|
|
150
275
|
)
|
|
151
|
-
|
|
276
|
+
lineSuccess(`Removed ${binDir} from user PATH`)
|
|
152
277
|
}
|
|
153
278
|
} catch {
|
|
154
|
-
|
|
279
|
+
lineWarn("Could not clean PATH. Remove manually from System Settings.")
|
|
155
280
|
}
|
|
156
281
|
}
|