vibeusage 0.2.21 → 0.2.22
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/README.md +306 -173
- package/README.old.md +324 -0
- package/README.zh-CN.md +304 -188
- package/package.json +32 -32
- package/src/cli.js +41 -37
- package/src/commands/activate-if-needed.js +41 -0
- package/src/commands/diagnostics.js +8 -9
- package/src/commands/doctor.js +31 -26
- package/src/commands/init.js +285 -218
- package/src/commands/status.js +86 -83
- package/src/commands/sync.js +182 -130
- package/src/commands/uninstall.js +66 -62
- package/src/lib/activation-check.js +290 -0
- package/src/lib/browser-auth.js +52 -54
- package/src/lib/claude-config.js +25 -25
- package/src/lib/cli-ui.js +35 -35
- package/src/lib/codex-config.js +40 -36
- package/src/lib/debug-flags.js +2 -2
- package/src/lib/diagnostics.js +70 -57
- package/src/lib/doctor.js +139 -132
- package/src/lib/fs.js +17 -17
- package/src/lib/gemini-config.js +44 -40
- package/src/lib/init-flow.js +16 -22
- package/src/lib/insforge-client.js +10 -10
- package/src/lib/insforge.js +9 -3
- package/src/lib/openclaw-hook.js +89 -66
- package/src/lib/openclaw-session-plugin.js +116 -92
- package/src/lib/opencode-config.js +31 -32
- package/src/lib/opencode-usage-audit.js +34 -31
- package/src/lib/progress.js +12 -13
- package/src/lib/project-usage-purge.js +23 -17
- package/src/lib/prompt.js +8 -4
- package/src/lib/rollout.js +342 -241
- package/src/lib/runtime-config.js +34 -22
- package/src/lib/subscriptions.js +94 -92
- package/src/lib/tracker-paths.js +6 -6
- package/src/lib/upload-throttle.js +35 -16
- package/src/lib/uploader.js +33 -29
- package/src/lib/vibeusage-api.js +72 -56
- package/src/lib/vibeusage-public-repo.js +41 -24
package/src/lib/browser-auth.js
CHANGED
|
@@ -1,24 +1,22 @@
|
|
|
1
|
-
const http = require(
|
|
2
|
-
const crypto = require(
|
|
3
|
-
const cp = require(
|
|
1
|
+
const http = require("node:http");
|
|
2
|
+
const crypto = require("node:crypto");
|
|
3
|
+
const cp = require("node:child_process");
|
|
4
4
|
|
|
5
|
-
const { DEFAULT_BASE_URL } = require(
|
|
5
|
+
const { DEFAULT_BASE_URL } = require("./runtime-config");
|
|
6
6
|
|
|
7
7
|
async function beginBrowserAuth({ baseUrl, dashboardUrl, timeoutMs, open }) {
|
|
8
|
-
const nonce = crypto.randomBytes(16).toString(
|
|
8
|
+
const nonce = crypto.randomBytes(16).toString("hex");
|
|
9
9
|
const callbackPath = `/vibeusage/callback/${nonce}`;
|
|
10
|
-
const authUrl = dashboardUrl
|
|
11
|
-
? new URL('/', dashboardUrl)
|
|
12
|
-
: new URL('/auth/sign-up', baseUrl);
|
|
10
|
+
const authUrl = dashboardUrl ? new URL("/", dashboardUrl) : new URL("/auth/sign-up", baseUrl);
|
|
13
11
|
const postAuthRedirect = resolvePostAuthRedirect({ dashboardUrl, authUrl });
|
|
14
12
|
const { callbackUrl, waitForCallback } = await startLocalCallbackServer({
|
|
15
13
|
callbackPath,
|
|
16
14
|
timeoutMs,
|
|
17
|
-
redirectUrl: postAuthRedirect
|
|
15
|
+
redirectUrl: postAuthRedirect,
|
|
18
16
|
});
|
|
19
|
-
authUrl.searchParams.set(
|
|
17
|
+
authUrl.searchParams.set("redirect", callbackUrl);
|
|
20
18
|
if (dashboardUrl && baseUrl && baseUrl !== DEFAULT_BASE_URL) {
|
|
21
|
-
authUrl.searchParams.set(
|
|
19
|
+
authUrl.searchParams.set("base_url", baseUrl);
|
|
22
20
|
}
|
|
23
21
|
|
|
24
22
|
if (open !== false) openInBrowser(authUrl.toString());
|
|
@@ -38,29 +36,29 @@ async function startLocalCallbackServer({ callbackPath, timeoutMs, redirectUrl }
|
|
|
38
36
|
|
|
39
37
|
const server = http.createServer((req, res) => {
|
|
40
38
|
if (resolved) {
|
|
41
|
-
res.writeHead(409, {
|
|
42
|
-
res.end(
|
|
39
|
+
res.writeHead(409, { "Content-Type": "text/plain; charset=utf-8" });
|
|
40
|
+
res.end("Already authenticated.\n");
|
|
43
41
|
return;
|
|
44
42
|
}
|
|
45
43
|
|
|
46
|
-
const method = req.method ||
|
|
47
|
-
if (method !==
|
|
48
|
-
res.writeHead(405, {
|
|
49
|
-
res.end(
|
|
44
|
+
const method = req.method || "GET";
|
|
45
|
+
if (method !== "GET") {
|
|
46
|
+
res.writeHead(405, { "Content-Type": "text/plain; charset=utf-8" });
|
|
47
|
+
res.end("Method not allowed.\n");
|
|
50
48
|
return;
|
|
51
49
|
}
|
|
52
50
|
|
|
53
|
-
const url = new URL(req.url ||
|
|
51
|
+
const url = new URL(req.url || "/", "http://127.0.0.1");
|
|
54
52
|
if (url.pathname !== callbackPath) {
|
|
55
|
-
res.writeHead(404, {
|
|
56
|
-
res.end(
|
|
53
|
+
res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
|
|
54
|
+
res.end("Not found.\n");
|
|
57
55
|
return;
|
|
58
56
|
}
|
|
59
57
|
|
|
60
|
-
const accessToken = url.searchParams.get(
|
|
58
|
+
const accessToken = url.searchParams.get("access_token") || "";
|
|
61
59
|
if (!accessToken) {
|
|
62
|
-
res.writeHead(400, {
|
|
63
|
-
res.end(
|
|
60
|
+
res.writeHead(400, { "Content-Type": "text/plain; charset=utf-8" });
|
|
61
|
+
res.end("Missing access_token.\n");
|
|
64
62
|
return;
|
|
65
63
|
}
|
|
66
64
|
|
|
@@ -68,50 +66,50 @@ async function startLocalCallbackServer({ callbackPath, timeoutMs, redirectUrl }
|
|
|
68
66
|
if (redirectUrl) {
|
|
69
67
|
res.writeHead(302, {
|
|
70
68
|
Location: redirectUrl,
|
|
71
|
-
|
|
69
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
72
70
|
});
|
|
73
71
|
res.end(
|
|
74
72
|
[
|
|
75
|
-
|
|
73
|
+
"<!doctype html>",
|
|
76
74
|
'<html><head><meta charset="utf-8"><title>VibeScore</title></head>',
|
|
77
|
-
|
|
78
|
-
|
|
75
|
+
"<body>",
|
|
76
|
+
"<h2>Login succeeded</h2>",
|
|
79
77
|
`<p>Redirecting to <a href="${redirectUrl}">dashboard</a>...</p>`,
|
|
80
|
-
|
|
81
|
-
].join(
|
|
78
|
+
"</body></html>",
|
|
79
|
+
].join(""),
|
|
82
80
|
);
|
|
83
81
|
} else {
|
|
84
|
-
res.writeHead(200, {
|
|
82
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
85
83
|
res.end(
|
|
86
84
|
[
|
|
87
|
-
|
|
85
|
+
"<!doctype html>",
|
|
88
86
|
'<html><head><meta charset="utf-8"><title>VibeScore</title></head>',
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
].join(
|
|
87
|
+
"<body>",
|
|
88
|
+
"<h2>Login succeeded</h2>",
|
|
89
|
+
"<p>You can close this tab and return to the CLI.</p>",
|
|
90
|
+
"</body></html>",
|
|
91
|
+
].join(""),
|
|
94
92
|
);
|
|
95
93
|
}
|
|
96
94
|
|
|
97
95
|
resolveResult({
|
|
98
96
|
accessToken,
|
|
99
|
-
userId: url.searchParams.get(
|
|
100
|
-
email: url.searchParams.get(
|
|
101
|
-
name: url.searchParams.get(
|
|
97
|
+
userId: url.searchParams.get("user_id") || null,
|
|
98
|
+
email: url.searchParams.get("email") || null,
|
|
99
|
+
name: url.searchParams.get("name") || null,
|
|
102
100
|
});
|
|
103
101
|
});
|
|
104
102
|
|
|
105
103
|
await new Promise((resolve, reject) => {
|
|
106
|
-
server.once(
|
|
107
|
-
server.listen(0,
|
|
104
|
+
server.once("error", reject);
|
|
105
|
+
server.listen(0, "127.0.0.1", resolve);
|
|
108
106
|
});
|
|
109
107
|
|
|
110
108
|
const addr = server.address();
|
|
111
|
-
const port = typeof addr ===
|
|
109
|
+
const port = typeof addr === "object" && addr ? addr.port : null;
|
|
112
110
|
if (!port) {
|
|
113
111
|
server.close();
|
|
114
|
-
throw new Error(
|
|
112
|
+
throw new Error("Failed to bind local callback server");
|
|
115
113
|
}
|
|
116
114
|
|
|
117
115
|
const callbackUrl = `http://127.0.0.1:${port}${callbackPath}`;
|
|
@@ -119,7 +117,7 @@ async function startLocalCallbackServer({ callbackPath, timeoutMs, redirectUrl }
|
|
|
119
117
|
const timer = setTimeout(() => {
|
|
120
118
|
if (resolved) return;
|
|
121
119
|
resolved = true;
|
|
122
|
-
rejectResult(new Error(
|
|
120
|
+
rejectResult(new Error("Authentication timed out"));
|
|
123
121
|
server.close();
|
|
124
122
|
}, timeoutMs);
|
|
125
123
|
|
|
@@ -141,19 +139,19 @@ function openInBrowser(url) {
|
|
|
141
139
|
let cmd = null;
|
|
142
140
|
let args = [];
|
|
143
141
|
|
|
144
|
-
if (platform ===
|
|
145
|
-
cmd =
|
|
142
|
+
if (platform === "darwin") {
|
|
143
|
+
cmd = "open";
|
|
146
144
|
args = [url];
|
|
147
|
-
} else if (platform ===
|
|
148
|
-
cmd =
|
|
149
|
-
args = [
|
|
145
|
+
} else if (platform === "win32") {
|
|
146
|
+
cmd = "cmd";
|
|
147
|
+
args = ["/c", "start", "", url];
|
|
150
148
|
} else {
|
|
151
|
-
cmd =
|
|
149
|
+
cmd = "xdg-open";
|
|
152
150
|
args = [url];
|
|
153
151
|
}
|
|
154
152
|
|
|
155
153
|
try {
|
|
156
|
-
const child = cp.spawn(cmd, args, { stdio:
|
|
154
|
+
const child = cp.spawn(cmd, args, { stdio: "ignore", detached: true });
|
|
157
155
|
child.unref();
|
|
158
156
|
} catch (_e) {}
|
|
159
157
|
}
|
|
@@ -161,8 +159,8 @@ function openInBrowser(url) {
|
|
|
161
159
|
function resolvePostAuthRedirect({ dashboardUrl, authUrl }) {
|
|
162
160
|
try {
|
|
163
161
|
if (dashboardUrl) {
|
|
164
|
-
const target = new URL(
|
|
165
|
-
if (target.protocol ===
|
|
162
|
+
const target = new URL("/", dashboardUrl);
|
|
163
|
+
if (target.protocol === "http:" || target.protocol === "https:") {
|
|
166
164
|
return target.toString();
|
|
167
165
|
}
|
|
168
166
|
return null;
|
|
@@ -175,5 +173,5 @@ function resolvePostAuthRedirect({ dashboardUrl, authUrl }) {
|
|
|
175
173
|
|
|
176
174
|
module.exports = {
|
|
177
175
|
beginBrowserAuth,
|
|
178
|
-
openInBrowser
|
|
176
|
+
openInBrowser,
|
|
179
177
|
};
|
package/src/lib/claude-config.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
const fs = require(
|
|
2
|
-
const path = require(
|
|
1
|
+
const fs = require("node:fs/promises");
|
|
2
|
+
const path = require("node:path");
|
|
3
3
|
|
|
4
|
-
const { ensureDir, readJson, writeJson } = require(
|
|
4
|
+
const { ensureDir, readJson, writeJson } = require("./fs");
|
|
5
5
|
|
|
6
|
-
const DEFAULT_EVENT =
|
|
6
|
+
const DEFAULT_EVENT = "SessionEnd";
|
|
7
7
|
|
|
8
8
|
async function upsertClaudeHook({ settingsPath, hookCommand, event = DEFAULT_EVENT }) {
|
|
9
9
|
const existing = await readJson(settingsPath);
|
|
@@ -23,7 +23,7 @@ async function upsertClaudeHook({ settingsPath, hookCommand, event = DEFAULT_EVE
|
|
|
23
23
|
return { changed: false, backupPath: null };
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
const nextEntries = entries.concat([{ hooks: [{ type:
|
|
26
|
+
const nextEntries = entries.concat([{ hooks: [{ type: "command", command: hookCommand }] }]);
|
|
27
27
|
const nextHooks = { ...hooks, [event]: nextEntries };
|
|
28
28
|
const nextSettings = { ...settings, hooks: nextHooks };
|
|
29
29
|
|
|
@@ -33,12 +33,12 @@ async function upsertClaudeHook({ settingsPath, hookCommand, event = DEFAULT_EVE
|
|
|
33
33
|
|
|
34
34
|
async function removeClaudeHook({ settingsPath, hookCommand, event = DEFAULT_EVENT }) {
|
|
35
35
|
const existing = await readJson(settingsPath);
|
|
36
|
-
if (!existing) return { removed: false, skippedReason:
|
|
36
|
+
if (!existing) return { removed: false, skippedReason: "settings-missing" };
|
|
37
37
|
|
|
38
38
|
const settings = normalizeSettings(existing);
|
|
39
39
|
const hooks = normalizeHooks(settings.hooks);
|
|
40
40
|
const entries = normalizeEntries(hooks[event]);
|
|
41
|
-
if (entries.length === 0) return { removed: false, skippedReason:
|
|
41
|
+
if (entries.length === 0) return { removed: false, skippedReason: "hook-missing" };
|
|
42
42
|
|
|
43
43
|
let removed = false;
|
|
44
44
|
const nextEntries = [];
|
|
@@ -48,7 +48,7 @@ async function removeClaudeHook({ settingsPath, hookCommand, event = DEFAULT_EVE
|
|
|
48
48
|
if (res.entry) nextEntries.push(res.entry);
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
if (!removed) return { removed: false, skippedReason:
|
|
51
|
+
if (!removed) return { removed: false, skippedReason: "hook-missing" };
|
|
52
52
|
|
|
53
53
|
const nextHooks = { ...hooks };
|
|
54
54
|
if (nextEntries.length > 0) nextHooks[event] = nextEntries;
|
|
@@ -64,24 +64,24 @@ async function removeClaudeHook({ settingsPath, hookCommand, event = DEFAULT_EVE
|
|
|
64
64
|
|
|
65
65
|
async function isClaudeHookConfigured({ settingsPath, hookCommand, event = DEFAULT_EVENT }) {
|
|
66
66
|
const settings = await readJson(settingsPath);
|
|
67
|
-
if (!settings || typeof settings !==
|
|
67
|
+
if (!settings || typeof settings !== "object") return false;
|
|
68
68
|
const hooks = settings.hooks;
|
|
69
|
-
if (!hooks || typeof hooks !==
|
|
69
|
+
if (!hooks || typeof hooks !== "object") return false;
|
|
70
70
|
const entries = normalizeEntries(hooks[event]);
|
|
71
71
|
return hasHook(entries, hookCommand);
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
function buildClaudeHookCommand(notifyPath) {
|
|
75
|
-
const cmd = typeof notifyPath ===
|
|
75
|
+
const cmd = typeof notifyPath === "string" ? notifyPath : "";
|
|
76
76
|
return `/usr/bin/env node ${quoteArg(cmd)} --source=claude`;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
function normalizeSettings(raw) {
|
|
80
|
-
return raw && typeof raw ===
|
|
80
|
+
return raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
function normalizeHooks(raw) {
|
|
84
|
-
return raw && typeof raw ===
|
|
84
|
+
return raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
function normalizeEntries(raw) {
|
|
@@ -89,14 +89,14 @@ function normalizeEntries(raw) {
|
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
function normalizeCommand(cmd) {
|
|
92
|
-
if (Array.isArray(cmd)) return cmd.map((v) => String(v)).join(
|
|
93
|
-
if (typeof cmd ===
|
|
92
|
+
if (Array.isArray(cmd)) return cmd.map((v) => String(v)).join("\u0000");
|
|
93
|
+
if (typeof cmd === "string") return cmd.trim();
|
|
94
94
|
return null;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
function hasHook(entries, hookCommand) {
|
|
98
98
|
for (const entry of entries) {
|
|
99
|
-
if (!entry || typeof entry !==
|
|
99
|
+
if (!entry || typeof entry !== "object") continue;
|
|
100
100
|
if (entry.command && commandsEqual(entry.command, hookCommand)) return true;
|
|
101
101
|
const hooks = Array.isArray(entry.hooks) ? entry.hooks : [];
|
|
102
102
|
for (const hook of hooks) {
|
|
@@ -107,7 +107,7 @@ function hasHook(entries, hookCommand) {
|
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
function stripHookFromEntry(entry, hookCommand) {
|
|
110
|
-
if (!entry || typeof entry !==
|
|
110
|
+
if (!entry || typeof entry !== "object") return { entry, removed: false };
|
|
111
111
|
|
|
112
112
|
if (entry.command) {
|
|
113
113
|
if (commandsEqual(entry.command, hookCommand)) return { entry: null, removed: true };
|
|
@@ -127,11 +127,11 @@ function stripHookFromEntry(entry, hookCommand) {
|
|
|
127
127
|
function normalizeEntriesForCommand(entries, hookCommand) {
|
|
128
128
|
let changed = false;
|
|
129
129
|
const nextEntries = entries.map((entry) => {
|
|
130
|
-
if (!entry || typeof entry !==
|
|
130
|
+
if (!entry || typeof entry !== "object") return entry;
|
|
131
131
|
if (entry.command && commandsEqual(entry.command, hookCommand)) {
|
|
132
|
-
if (entry.type !==
|
|
132
|
+
if (entry.type !== "command") {
|
|
133
133
|
changed = true;
|
|
134
|
-
return { ...entry, type:
|
|
134
|
+
return { ...entry, type: "command" };
|
|
135
135
|
}
|
|
136
136
|
return entry;
|
|
137
137
|
}
|
|
@@ -139,9 +139,9 @@ function normalizeEntriesForCommand(entries, hookCommand) {
|
|
|
139
139
|
let hooksChanged = false;
|
|
140
140
|
const nextHooks = entry.hooks.map((hook) => {
|
|
141
141
|
if (hook && commandsEqual(hook.command, hookCommand)) {
|
|
142
|
-
if (hook.type !==
|
|
142
|
+
if (hook.type !== "command") {
|
|
143
143
|
hooksChanged = true;
|
|
144
|
-
return { ...hook, type:
|
|
144
|
+
return { ...hook, type: "command" };
|
|
145
145
|
}
|
|
146
146
|
}
|
|
147
147
|
return hook;
|
|
@@ -160,7 +160,7 @@ function commandsEqual(a, b) {
|
|
|
160
160
|
}
|
|
161
161
|
|
|
162
162
|
function quoteArg(value) {
|
|
163
|
-
const v = typeof value ===
|
|
163
|
+
const v = typeof value === "string" ? value : "";
|
|
164
164
|
if (!v) return '""';
|
|
165
165
|
if (/^[A-Za-z0-9_\-./:@]+$/.test(v)) return v;
|
|
166
166
|
return `"${v.replace(/"/g, '\\"')}"`;
|
|
@@ -172,7 +172,7 @@ async function writeClaudeSettings({ settingsPath, settings }) {
|
|
|
172
172
|
try {
|
|
173
173
|
const st = await fs.stat(settingsPath);
|
|
174
174
|
if (st && st.isFile()) {
|
|
175
|
-
backupPath = `${settingsPath}.bak.${new Date().toISOString().replace(/[:.]/g,
|
|
175
|
+
backupPath = `${settingsPath}.bak.${new Date().toISOString().replace(/[:.]/g, "-")}`;
|
|
176
176
|
await fs.copyFile(settingsPath, backupPath);
|
|
177
177
|
}
|
|
178
178
|
} catch (_e) {
|
|
@@ -186,5 +186,5 @@ module.exports = {
|
|
|
186
186
|
upsertClaudeHook,
|
|
187
187
|
removeClaudeHook,
|
|
188
188
|
isClaudeHookConfigured,
|
|
189
|
-
buildClaudeHookCommand
|
|
189
|
+
buildClaudeHookCommand,
|
|
190
190
|
};
|
package/src/lib/cli-ui.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
const readline = require(
|
|
1
|
+
const readline = require("node:readline");
|
|
2
2
|
|
|
3
|
-
const RESET =
|
|
4
|
-
const BOLD =
|
|
5
|
-
const DIM =
|
|
6
|
-
const CYAN =
|
|
7
|
-
const GREEN =
|
|
8
|
-
const YELLOW =
|
|
9
|
-
const BLUE =
|
|
10
|
-
const UNDERLINE =
|
|
3
|
+
const RESET = "\x1b[0m";
|
|
4
|
+
const BOLD = "\x1b[1m";
|
|
5
|
+
const DIM = "\x1b[2m";
|
|
6
|
+
const CYAN = "\x1b[36m";
|
|
7
|
+
const GREEN = "\x1b[32m";
|
|
8
|
+
const YELLOW = "\x1b[33m";
|
|
9
|
+
const BLUE = "\x1b[34m";
|
|
10
|
+
const UNDERLINE = "\x1b[4m";
|
|
11
11
|
|
|
12
|
-
const SPINNER_FRAMES = [
|
|
12
|
+
const SPINNER_FRAMES = ["|", "/", "-", "\\"];
|
|
13
13
|
|
|
14
14
|
function isInteractive() {
|
|
15
15
|
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
@@ -17,22 +17,22 @@ function isInteractive() {
|
|
|
17
17
|
|
|
18
18
|
function formatLine(line, width) {
|
|
19
19
|
if (!width) return line;
|
|
20
|
-
const raw = String(line ||
|
|
20
|
+
const raw = String(line || "");
|
|
21
21
|
const pad = Math.max(0, width - raw.length);
|
|
22
|
-
return raw +
|
|
22
|
+
return raw + " ".repeat(pad);
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
function renderBox(lines, { padding = 1 } = {}) {
|
|
26
|
-
const content = lines.map((line) => String(line ||
|
|
26
|
+
const content = lines.map((line) => String(line || ""));
|
|
27
27
|
const maxLen = content.reduce((max, line) => Math.max(max, line.length), 0);
|
|
28
28
|
const innerWidth = maxLen + padding * 2;
|
|
29
|
-
const top = `+${
|
|
30
|
-
const bottom = `+${
|
|
29
|
+
const top = `+${"-".repeat(innerWidth)}+`;
|
|
30
|
+
const bottom = `+${"-".repeat(innerWidth)}+`;
|
|
31
31
|
const body = content.map((line) => {
|
|
32
|
-
const padded =
|
|
32
|
+
const padded = " ".repeat(padding) + formatLine(line, maxLen) + " ".repeat(padding);
|
|
33
33
|
return `|${padded}|`;
|
|
34
34
|
});
|
|
35
|
-
return [top, ...body, bottom].join(
|
|
35
|
+
return [top, ...body, bottom].join("\n");
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
function color(text, token) {
|
|
@@ -54,7 +54,7 @@ async function promptMenu({ message, options, defaultIndex = 0 }) {
|
|
|
54
54
|
if (!isInteractive()) return options[defaultIndex] || options[0];
|
|
55
55
|
|
|
56
56
|
const safeOptions = Array.isArray(options) ? options : [];
|
|
57
|
-
if (safeOptions.length === 0) return
|
|
57
|
+
if (safeOptions.length === 0) return "";
|
|
58
58
|
|
|
59
59
|
const maxIndex = safeOptions.length - 1;
|
|
60
60
|
let currentIndex = Math.min(Math.max(defaultIndex, 0), maxIndex);
|
|
@@ -64,18 +64,18 @@ async function promptMenu({ message, options, defaultIndex = 0 }) {
|
|
|
64
64
|
const renderLines = () => {
|
|
65
65
|
const lines = [promptMessage];
|
|
66
66
|
safeOptions.forEach((opt, idx) => {
|
|
67
|
-
const prefix = idx === currentIndex ?
|
|
67
|
+
const prefix = idx === currentIndex ? ">" : " ";
|
|
68
68
|
lines.push(`${prefix} ${opt}`);
|
|
69
69
|
});
|
|
70
|
-
process.stdout.write(lines.join(
|
|
70
|
+
process.stdout.write(lines.join("\n"));
|
|
71
71
|
};
|
|
72
72
|
|
|
73
73
|
const rerender = () => {
|
|
74
74
|
for (let i = 0; i < linesCount; i += 1) {
|
|
75
|
-
process.stdout.write(
|
|
76
|
-
if (i < linesCount - 1) process.stdout.write(
|
|
75
|
+
process.stdout.write("\x1b[2K");
|
|
76
|
+
if (i < linesCount - 1) process.stdout.write("\x1b[1A");
|
|
77
77
|
}
|
|
78
|
-
process.stdout.write(
|
|
78
|
+
process.stdout.write("\r");
|
|
79
79
|
renderLines();
|
|
80
80
|
};
|
|
81
81
|
|
|
@@ -83,28 +83,28 @@ async function promptMenu({ message, options, defaultIndex = 0 }) {
|
|
|
83
83
|
|
|
84
84
|
return await new Promise((resolve) => {
|
|
85
85
|
const cleanup = () => {
|
|
86
|
-
process.stdin.off(
|
|
86
|
+
process.stdin.off("keypress", onKeypress);
|
|
87
87
|
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
88
88
|
process.stdin.pause();
|
|
89
|
-
process.stdout.write(
|
|
89
|
+
process.stdout.write("\n");
|
|
90
90
|
};
|
|
91
91
|
|
|
92
92
|
const onKeypress = (str, key = {}) => {
|
|
93
|
-
if (key.ctrl && key.name ===
|
|
93
|
+
if (key.ctrl && key.name === "c") {
|
|
94
94
|
cleanup();
|
|
95
95
|
return resolve(safeOptions[currentIndex]);
|
|
96
96
|
}
|
|
97
|
-
if (key.name ===
|
|
97
|
+
if (key.name === "up" || str === "k") {
|
|
98
98
|
currentIndex = currentIndex === 0 ? maxIndex : currentIndex - 1;
|
|
99
99
|
rerender();
|
|
100
100
|
return;
|
|
101
101
|
}
|
|
102
|
-
if (key.name ===
|
|
102
|
+
if (key.name === "down" || str === "j") {
|
|
103
103
|
currentIndex = currentIndex === maxIndex ? 0 : currentIndex + 1;
|
|
104
104
|
rerender();
|
|
105
105
|
return;
|
|
106
106
|
}
|
|
107
|
-
if (key.name ===
|
|
107
|
+
if (key.name === "return") {
|
|
108
108
|
cleanup();
|
|
109
109
|
return resolve(safeOptions[currentIndex]);
|
|
110
110
|
}
|
|
@@ -121,7 +121,7 @@ async function promptMenu({ message, options, defaultIndex = 0 }) {
|
|
|
121
121
|
readline.emitKeypressEvents(process.stdin);
|
|
122
122
|
if (process.stdin.isTTY) process.stdin.setRawMode(true);
|
|
123
123
|
process.stdin.resume();
|
|
124
|
-
process.stdin.on(
|
|
124
|
+
process.stdin.on("keypress", onKeypress);
|
|
125
125
|
});
|
|
126
126
|
}
|
|
127
127
|
|
|
@@ -144,7 +144,7 @@ function createSpinner({ text, intervalMs = 80 }) {
|
|
|
144
144
|
function stop(successText) {
|
|
145
145
|
if (timer) clearInterval(timer);
|
|
146
146
|
if (isInteractive()) {
|
|
147
|
-
process.stdout.write(`\r${
|
|
147
|
+
process.stdout.write(`\r${" ".repeat(text.length + 4)}\r`);
|
|
148
148
|
}
|
|
149
149
|
if (successText) process.stdout.write(`${successText}\n`);
|
|
150
150
|
}
|
|
@@ -153,9 +153,9 @@ function createSpinner({ text, intervalMs = 80 }) {
|
|
|
153
153
|
}
|
|
154
154
|
|
|
155
155
|
function formatSummaryLine({ label, status, detail }) {
|
|
156
|
-
const isSuccess = status ===
|
|
157
|
-
const bullet = isSuccess ? color(
|
|
158
|
-
const statusLabel = isSuccess ?
|
|
156
|
+
const isSuccess = status === "updated" || status === "set" || status === "installed";
|
|
157
|
+
const bullet = isSuccess ? color("*", GREEN) : "o";
|
|
158
|
+
const statusLabel = isSuccess ? detail || status : detail ? `Skipped - ${detail}` : "Skipped";
|
|
159
159
|
const line = ` ${bullet} ${label.padEnd(22)} [${statusLabel}]`;
|
|
160
160
|
return isSuccess ? line : color(line, DIM);
|
|
161
161
|
}
|
|
@@ -175,5 +175,5 @@ module.exports = {
|
|
|
175
175
|
promptMenu,
|
|
176
176
|
promptEnter,
|
|
177
177
|
createSpinner,
|
|
178
|
-
formatSummaryLine
|
|
178
|
+
formatSummaryLine,
|
|
179
179
|
};
|