@vibescore/tracker 0.2.3 → 0.2.5
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 +27 -1
- package/README.zh-CN.md +27 -1
- package/package.json +4 -2
- package/src/cli.js +10 -7
- package/src/commands/init.js +453 -119
- package/src/commands/uninstall.js +2 -1
- package/src/lib/browser-auth.js +2 -1
- package/src/lib/cli-ui.js +179 -0
package/README.md
CHANGED
|
@@ -33,7 +33,7 @@ _Real-time AI Analytics for Codex CLI_
|
|
|
33
33
|
|
|
34
34
|
We believe your code and thoughts are your own. VibeScore is built with strict privacy pillars to ensure your data never leaves your control.
|
|
35
35
|
|
|
36
|
-
- 🛡️ **
|
|
36
|
+
- 🛡️ **No Content Upload**: We never upload prompts or responses. We only compute token counts locally and send counts plus minimal metadata (timestamps, model, device).
|
|
37
37
|
- 📡 **Local Aggregation**: All token consumption analysis happens on your machine. We only relay quantized 30-minute usage buckets to the cloud.
|
|
38
38
|
- 🔐 **Hashed Identity**: Device tokens are hashed using SHA-256 server-side. Your raw credentials never exist in our database.
|
|
39
39
|
- 🔦 **Full Transparency**: Audit the sync logic yourself in `src/lib/rollout.js`. We literally only capture numbers and timestamps.
|
|
@@ -63,6 +63,8 @@ Initialize your environment once and forget it. VibeScore handles all synchroniz
|
|
|
63
63
|
npx --yes @vibescore/tracker init
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
+
Note: `init` shows a consent prompt in interactive shells. Use `--yes` to skip prompts in non-interactive environments.
|
|
67
|
+
Optional: `--dry-run` previews planned changes without writing files.
|
|
66
68
|
Note: If `~/.code/config.toml` exists (or `CODE_HOME`), `init` also configures Every Code `notify` automatically. No further user intervention is required for data sync.
|
|
67
69
|
Note: If Gemini CLI home exists, `init` installs a `SessionEnd` hook in `~/.gemini/settings.json` and sets `tools.enableHooks = true` so hooks execute. This enables all Gemini hooks; disable by setting `tools.enableHooks = false` (or disabling the `vibescore-tracker` hook).
|
|
68
70
|
|
|
@@ -134,6 +136,30 @@ npm install
|
|
|
134
136
|
npm run dev
|
|
135
137
|
```
|
|
136
138
|
|
|
139
|
+
### Debug Payload (Usage Endpoints)
|
|
140
|
+
|
|
141
|
+
When `debug=1` is included in a usage endpoint request, the response adds a `debug` object that helps the dashboard attribute slow queries without relying on response headers.
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
const res = await fetch(
|
|
145
|
+
`${baseUrl}/functions/vibescore-usage-summary?from=2025-12-30&to=2025-12-30&debug=1`,
|
|
146
|
+
{
|
|
147
|
+
headers: { Authorization: `Bearer ${userJwt}` }
|
|
148
|
+
}
|
|
149
|
+
);
|
|
150
|
+
const data = await res.json();
|
|
151
|
+
|
|
152
|
+
if (data.debug) {
|
|
153
|
+
console.debug('usage debug', {
|
|
154
|
+
requestId: data.debug.request_id,
|
|
155
|
+
status: data.debug.status,
|
|
156
|
+
queryMs: data.debug.query_ms,
|
|
157
|
+
slowThresholdMs: data.debug.slow_threshold_ms,
|
|
158
|
+
slowQuery: data.debug.slow_query
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
137
163
|
### Architecture Validation
|
|
138
164
|
|
|
139
165
|
```bash
|
package/README.zh-CN.md
CHANGED
|
@@ -33,7 +33,7 @@ _Codex CLI 实时 AI 分析工具_
|
|
|
33
33
|
|
|
34
34
|
我们坚信你的代码和思想属于你自己。VibeScore 建立在严格的隐私支柱之上,确保你的数据始终处于受控状态。
|
|
35
35
|
|
|
36
|
-
- 🛡️
|
|
36
|
+
- 🛡️ **内容不出本地**:我们从不上传 Prompt 或响应内容。只在本地计算 Token 数量,并上传 Token 计数与最小元数据(时间、模型、设备)。
|
|
37
37
|
- 📡 **本地聚合**:所有 Token 消耗分析均在你的机器上完成。我们仅将量化的 30 分钟使用桶(Usage Buckets)中继到云端。
|
|
38
38
|
- 🔐 **身份哈希**:设备令牌在服务端使用 SHA-256 进行哈希处理。你的原始凭据绝不会存在于我们的数据库中。
|
|
39
39
|
- 🔦 **全程透明**:你可以亲自审计 `src/lib/rollout.js` 中的同步逻辑。我们真正采集的只有数字和时间戳。
|
|
@@ -63,6 +63,8 @@ _Codex CLI 实时 AI 分析工具_
|
|
|
63
63
|
npx --yes @vibescore/tracker init
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
+
说明:交互式终端会显示授权菜单;非交互环境可使用 `--yes` 跳过。
|
|
67
|
+
可选:`--dry-run` 仅预览将发生的变更,不写入任何文件。
|
|
66
68
|
说明:若存在 `~/.code/config.toml`(或 `CODE_HOME`),`init` 会自动配置 Every Code 的 `notify`。配置完成后,数据同步完全自动化,无需后续人工干预。
|
|
67
69
|
说明:若检测到 Gemini CLI home,`init` 会在 `~/.gemini/settings.json` 安装 `SessionEnd` hook,并将 `tools.enableHooks = true` 以确保 hook 生效。这会启用所有 Gemini hooks;如需关闭,可将 `tools.enableHooks = false`(或禁用 `vibescore-tracker` hook)。
|
|
68
70
|
|
|
@@ -134,6 +136,30 @@ npm install
|
|
|
134
136
|
npm run dev
|
|
135
137
|
```
|
|
136
138
|
|
|
139
|
+
### 调试字段(Usage 接口)
|
|
140
|
+
|
|
141
|
+
当请求包含 `debug=1` 时,usage 接口会在响应体中附带 `debug` 对象,方便前端定位慢查询而不依赖响应头。
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
const res = await fetch(
|
|
145
|
+
`${baseUrl}/functions/vibescore-usage-summary?from=2025-12-30&to=2025-12-30&debug=1`,
|
|
146
|
+
{
|
|
147
|
+
headers: { Authorization: `Bearer ${userJwt}` }
|
|
148
|
+
}
|
|
149
|
+
);
|
|
150
|
+
const data = await res.json();
|
|
151
|
+
|
|
152
|
+
if (data.debug) {
|
|
153
|
+
console.debug('usage debug', {
|
|
154
|
+
requestId: data.debug.request_id,
|
|
155
|
+
status: data.debug.status,
|
|
156
|
+
queryMs: data.debug.query_ms,
|
|
157
|
+
slowThresholdMs: data.debug.slow_threshold_ms,
|
|
158
|
+
slowQuery: data.debug.slow_query
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
137
163
|
### 整体架构验证
|
|
138
164
|
|
|
139
165
|
```bash
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vibescore/tracker",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
4
4
|
"description": "Codex CLI token usage tracker (macOS-first, notify-driven).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"publishConfig": {
|
|
@@ -18,7 +18,9 @@
|
|
|
18
18
|
"dev:shim": "node scripts/dev-bin-shim.cjs",
|
|
19
19
|
"validate:copy": "node scripts/validate-copy-registry.cjs",
|
|
20
20
|
"copy:pull": "node scripts/copy-sync.cjs pull",
|
|
21
|
-
"copy:push": "node scripts/copy-sync.cjs push"
|
|
21
|
+
"copy:push": "node scripts/copy-sync.cjs push",
|
|
22
|
+
"architecture:canvas": "node scripts/ops/architecture-canvas.cjs",
|
|
23
|
+
"validate:guardrails": "node scripts/validate-architecture-guardrails.cjs"
|
|
22
24
|
},
|
|
23
25
|
"bin": {
|
|
24
26
|
"tracker": "bin/tracker.js",
|
package/src/cli.js
CHANGED
|
@@ -40,19 +40,22 @@ function printHelp() {
|
|
|
40
40
|
'@vibescore/tracker',
|
|
41
41
|
'',
|
|
42
42
|
'Usage:',
|
|
43
|
-
' npx @vibescore/tracker [--debug] init',
|
|
43
|
+
' npx @vibescore/tracker [--debug] init [--yes] [--dry-run] [--no-open] [--link-code <code>]',
|
|
44
44
|
' npx @vibescore/tracker [--debug] sync [--auto] [--drain]',
|
|
45
45
|
' npx @vibescore/tracker [--debug] status',
|
|
46
46
|
' npx @vibescore/tracker [--debug] diagnostics [--out diagnostics.json]',
|
|
47
47
|
' npx @vibescore/tracker [--debug] uninstall [--purge]',
|
|
48
48
|
'',
|
|
49
49
|
'Notes:',
|
|
50
|
-
' - init
|
|
51
|
-
' -
|
|
52
|
-
' -
|
|
53
|
-
' - optional:
|
|
54
|
-
' -
|
|
55
|
-
' -
|
|
50
|
+
' - init: consent first, local setup next, browser sign-in last.',
|
|
51
|
+
' - --yes skips the consent menu (non-interactive safe).',
|
|
52
|
+
' - --dry-run previews changes without writing files.',
|
|
53
|
+
' - optional: --link-code <code> skips browser login when provided by Dashboard.',
|
|
54
|
+
' - Every Code notify installs when ~/.code/config.toml exists.',
|
|
55
|
+
' - auto sync waits for a device token.',
|
|
56
|
+
' - optional: VIBESCORE_DASHBOARD_URL or --dashboard-url for hosted landing.',
|
|
57
|
+
' - sync parses ~/.codex/sessions/**/rollout-*.jsonl and ~/.code/sessions/**/rollout-*.jsonl, then uploads token deltas.',
|
|
58
|
+
' - --debug shows original backend errors.',
|
|
56
59
|
''
|
|
57
60
|
].join('\n')
|
|
58
61
|
);
|
package/src/commands/init.js
CHANGED
|
@@ -9,24 +9,50 @@ const { ensureDir, writeFileAtomic, readJson, writeJson, chmod600IfPossible } =
|
|
|
9
9
|
const { prompt, promptHidden } = require('../lib/prompt');
|
|
10
10
|
const {
|
|
11
11
|
upsertCodexNotify,
|
|
12
|
-
loadCodexNotifyOriginal,
|
|
13
12
|
upsertEveryCodeNotify,
|
|
14
|
-
|
|
13
|
+
readCodexNotify,
|
|
14
|
+
readEveryCodeNotify
|
|
15
15
|
} = require('../lib/codex-config');
|
|
16
|
-
const { upsertClaudeHook, buildClaudeHookCommand } = require('../lib/claude-config');
|
|
16
|
+
const { upsertClaudeHook, buildClaudeHookCommand, isClaudeHookConfigured } = require('../lib/claude-config');
|
|
17
17
|
const {
|
|
18
18
|
resolveGeminiConfigDir,
|
|
19
19
|
resolveGeminiSettingsPath,
|
|
20
20
|
buildGeminiHookCommand,
|
|
21
|
-
upsertGeminiHook
|
|
21
|
+
upsertGeminiHook,
|
|
22
|
+
isGeminiHookConfigured
|
|
22
23
|
} = require('../lib/gemini-config');
|
|
23
|
-
const { resolveOpencodeConfigDir, upsertOpencodePlugin } = require('../lib/opencode-config');
|
|
24
|
-
const { beginBrowserAuth } = require('../lib/browser-auth');
|
|
24
|
+
const { resolveOpencodeConfigDir, upsertOpencodePlugin, isOpencodePluginInstalled } = require('../lib/opencode-config');
|
|
25
|
+
const { beginBrowserAuth, openInBrowser } = require('../lib/browser-auth');
|
|
25
26
|
const {
|
|
26
27
|
issueDeviceTokenWithPassword,
|
|
27
28
|
issueDeviceTokenWithAccessToken,
|
|
28
29
|
issueDeviceTokenWithLinkCode
|
|
29
30
|
} = require('../lib/insforge');
|
|
31
|
+
const {
|
|
32
|
+
BOLD,
|
|
33
|
+
DIM,
|
|
34
|
+
CYAN,
|
|
35
|
+
RESET,
|
|
36
|
+
color,
|
|
37
|
+
underline,
|
|
38
|
+
renderBox,
|
|
39
|
+
isInteractive,
|
|
40
|
+
promptMenu,
|
|
41
|
+
promptEnter,
|
|
42
|
+
createSpinner,
|
|
43
|
+
formatSummaryLine
|
|
44
|
+
} = require('../lib/cli-ui');
|
|
45
|
+
|
|
46
|
+
const ASCII_LOGO = [
|
|
47
|
+
'██╗ ██╗██╗██████╗ ███████╗███████╗ ██████╗ ██████╗ ██████╗ ███████╗',
|
|
48
|
+
'██║ ██║██║██╔══██╗██╔════╝██╔════╝██╔════╝ ██╔═══██╗██╔══██╗██╔════╝',
|
|
49
|
+
'██║ ██║██║██████╔╝█████╗ ███████╗██║ ██║ ██║██████╔╝█████╗',
|
|
50
|
+
'╚██╗ ██╔╝██║██╔══██╗██╔══╝ ╚════██║██║ ██║ ██║██╔══██╗██╔══╝',
|
|
51
|
+
' ╚████╔╝ ██║██████╔╝███████╗███████║╚██████╗ ╚██████╔╝██║ ██║███████╗',
|
|
52
|
+
' ╚═══╝ ╚═╝╚═════╝ ╚══════╝╚══════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝'
|
|
53
|
+
].join('\n');
|
|
54
|
+
|
|
55
|
+
const DIVIDER = '----------------------------------------------';
|
|
30
56
|
|
|
31
57
|
async function cmdInit(argv) {
|
|
32
58
|
const opts = parseArgs(argv);
|
|
@@ -36,9 +62,6 @@ async function cmdInit(argv) {
|
|
|
36
62
|
const trackerDir = path.join(rootDir, 'tracker');
|
|
37
63
|
const binDir = path.join(rootDir, 'bin');
|
|
38
64
|
|
|
39
|
-
await ensureDir(trackerDir);
|
|
40
|
-
await ensureDir(binDir);
|
|
41
|
-
|
|
42
65
|
const configPath = path.join(trackerDir, 'config.json');
|
|
43
66
|
const notifyOriginalPath = path.join(trackerDir, 'codex_notify_original.json');
|
|
44
67
|
const linkCodeStatePath = path.join(trackerDir, 'link_code_state.json');
|
|
@@ -49,11 +72,210 @@ async function cmdInit(argv) {
|
|
|
49
72
|
const appDir = path.join(trackerDir, 'app');
|
|
50
73
|
const trackerBinPath = path.join(appDir, 'bin', 'tracker.js');
|
|
51
74
|
|
|
75
|
+
renderWelcome();
|
|
76
|
+
|
|
77
|
+
if (opts.dryRun) {
|
|
78
|
+
process.stdout.write(`${color('Dry run: preview only (no changes applied).', DIM)}\n\n`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (isInteractive() && !opts.yes && !opts.dryRun) {
|
|
82
|
+
const choice = await promptMenu({
|
|
83
|
+
message: '? How would you like to proceed?',
|
|
84
|
+
options: ['Start Setup (Recommended)', 'Exit'],
|
|
85
|
+
defaultIndex: 0
|
|
86
|
+
});
|
|
87
|
+
if (choice.toLowerCase().startsWith('exit')) {
|
|
88
|
+
process.stdout.write('Setup cancelled.\n');
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (opts.dryRun) {
|
|
94
|
+
const preview = await buildDryRunSummary({
|
|
95
|
+
opts,
|
|
96
|
+
home,
|
|
97
|
+
trackerDir,
|
|
98
|
+
configPath,
|
|
99
|
+
notifyPath
|
|
100
|
+
});
|
|
101
|
+
renderTransparencyReport({ summary: preview.summary, isDryRun: true });
|
|
102
|
+
if (preview.pendingBrowserAuth) {
|
|
103
|
+
process.stdout.write('Account linking would be required for full setup.\n');
|
|
104
|
+
} else if (!preview.deviceToken) {
|
|
105
|
+
renderAccountNotLinked({ context: 'dry-run' });
|
|
106
|
+
}
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const spinner = createSpinner({ text: 'Analyzing and configuring local environment...' });
|
|
111
|
+
spinner.start();
|
|
112
|
+
let setup;
|
|
113
|
+
try {
|
|
114
|
+
setup = await runSetup({
|
|
115
|
+
opts,
|
|
116
|
+
home,
|
|
117
|
+
baseUrl,
|
|
118
|
+
trackerDir,
|
|
119
|
+
binDir,
|
|
120
|
+
configPath,
|
|
121
|
+
notifyOriginalPath,
|
|
122
|
+
linkCodeStatePath,
|
|
123
|
+
notifyPath,
|
|
124
|
+
appDir,
|
|
125
|
+
trackerBinPath
|
|
126
|
+
});
|
|
127
|
+
} catch (err) {
|
|
128
|
+
spinner.stop();
|
|
129
|
+
throw err;
|
|
130
|
+
}
|
|
131
|
+
spinner.stop();
|
|
132
|
+
|
|
133
|
+
renderTransparencyReport({
|
|
134
|
+
summary: setup.summary,
|
|
135
|
+
isDryRun: false,
|
|
136
|
+
includeDivider: setup.pendingBrowserAuth
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
let deviceToken = setup.deviceToken;
|
|
140
|
+
let deviceId = setup.deviceId;
|
|
141
|
+
|
|
142
|
+
if (setup.pendingBrowserAuth) {
|
|
143
|
+
const deviceName = opts.deviceName || os.hostname();
|
|
144
|
+
if (!dashboardUrl) dashboardUrl = await detectLocalDashboardUrl();
|
|
145
|
+
const flow = await beginBrowserAuth({ baseUrl, dashboardUrl, timeoutMs: 10 * 60_000, open: false });
|
|
146
|
+
const canAutoOpen = !opts.noOpen;
|
|
147
|
+
renderFinalStep({ authUrl: flow.authUrl, canAutoOpen });
|
|
148
|
+
if (canAutoOpen && isInteractive()) {
|
|
149
|
+
await promptEnter('');
|
|
150
|
+
}
|
|
151
|
+
if (canAutoOpen) {
|
|
152
|
+
if (isInteractive()) await sleep(250);
|
|
153
|
+
openInBrowser(flow.authUrl);
|
|
154
|
+
}
|
|
155
|
+
const callback = await flow.waitForCallback();
|
|
156
|
+
const issued = await issueDeviceTokenWithAccessToken({ baseUrl, accessToken: callback.accessToken, deviceName });
|
|
157
|
+
deviceToken = issued.token;
|
|
158
|
+
deviceId = issued.deviceId;
|
|
159
|
+
await writeJson(configPath, { baseUrl, deviceToken, deviceId, installedAt: setup.installedAt });
|
|
160
|
+
await chmod600IfPossible(configPath);
|
|
161
|
+
renderSuccessBox({ deviceId, configPath });
|
|
162
|
+
} else if (deviceToken) {
|
|
163
|
+
renderSuccessBox({ deviceId, configPath });
|
|
164
|
+
} else {
|
|
165
|
+
renderAccountNotLinked();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
spawnInitSync({ trackerBinPath, packageName: '@vibescore/tracker' });
|
|
170
|
+
} catch (err) {
|
|
171
|
+
const msg = err && err.message ? err.message : 'unknown error';
|
|
172
|
+
process.stderr.write(`Initial sync spawn failed: ${msg}\n`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function renderWelcome() {
|
|
177
|
+
process.stdout.write(
|
|
178
|
+
[
|
|
179
|
+
ASCII_LOGO,
|
|
180
|
+
'',
|
|
181
|
+
`${BOLD}Welcome to VibeScore CLI${RESET}`,
|
|
182
|
+
DIVIDER,
|
|
183
|
+
`${CYAN}Privacy First: Your content stays local. We only upload token counts and minimal metadata, never prompts or responses.${RESET}`,
|
|
184
|
+
DIVIDER,
|
|
185
|
+
'',
|
|
186
|
+
'This tool will:',
|
|
187
|
+
' - Analyze your local AI CLI configurations (Codex, Every Code, Claude, Gemini, Opencode)',
|
|
188
|
+
' - Set up lightweight hooks to track your flow state',
|
|
189
|
+
' - Link your device to your VibeScore account',
|
|
190
|
+
'',
|
|
191
|
+
'(Nothing will be changed until you confirm below)',
|
|
192
|
+
''
|
|
193
|
+
].join('\n')
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function renderTransparencyReport({ summary, isDryRun, includeDivider = false }) {
|
|
198
|
+
const header = isDryRun ? 'Dry run complete. Preview only; no changes were applied.' : 'Local setup complete.';
|
|
199
|
+
const lines = [header, '', "We've integrated VibeScore with:"];
|
|
200
|
+
for (const item of summary) lines.push(formatSummaryLine(item));
|
|
201
|
+
if (includeDivider) lines.push('', DIVIDER, '');
|
|
202
|
+
process.stdout.write(`${lines.join('\n')}\n`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function renderFinalStep({ authUrl, canAutoOpen }) {
|
|
206
|
+
const lines = [
|
|
207
|
+
'Final Step: Link your account',
|
|
208
|
+
'',
|
|
209
|
+
canAutoOpen ? 'Press [Enter] to open your browser and sign in.' : 'Open the link below to sign in.'
|
|
210
|
+
];
|
|
211
|
+
if (authUrl) lines.push(`(Or visit: ${underline(authUrl)})`);
|
|
212
|
+
lines.push('');
|
|
213
|
+
process.stdout.write(lines.join('\n'));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function renderSuccessBox({ deviceId, configPath }) {
|
|
217
|
+
const identityLine = deviceId ? `Device ID: ${deviceId}` : 'Account linked.';
|
|
218
|
+
const lines = [
|
|
219
|
+
'You are all set!',
|
|
220
|
+
'',
|
|
221
|
+
identityLine,
|
|
222
|
+
`Token saved to: ${configPath}`,
|
|
223
|
+
'',
|
|
224
|
+
'VibeScore is now running in the background.',
|
|
225
|
+
'You can close this terminal window.'
|
|
226
|
+
];
|
|
227
|
+
process.stdout.write(`${renderBox(lines)}\n`);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function renderAccountNotLinked({ context } = {}) {
|
|
231
|
+
if (context === 'dry-run') {
|
|
232
|
+
process.stdout.write(['', 'Account not linked (dry run).', 'Run init without --dry-run to link your account.', ''].join('\n'));
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
process.stdout.write(['', 'Account not linked.', 'Set VIBESCORE_DEVICE_TOKEN then re-run init.', ''].join('\n'));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function shouldUseBrowserAuth({ deviceToken, opts }) {
|
|
239
|
+
if (deviceToken) return false;
|
|
240
|
+
if (opts.noAuth) return false;
|
|
241
|
+
if (opts.linkCode) return false;
|
|
242
|
+
if (opts.email || opts.password) return false;
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function buildDryRunSummary({ opts, home, trackerDir, configPath, notifyPath }) {
|
|
247
|
+
const existingConfig = await readJson(configPath);
|
|
248
|
+
const deviceTokenFromEnv = process.env.VIBESCORE_DEVICE_TOKEN || null;
|
|
249
|
+
const deviceToken = deviceTokenFromEnv || existingConfig?.deviceToken || null;
|
|
250
|
+
const pendingBrowserAuth = shouldUseBrowserAuth({ deviceToken, opts });
|
|
251
|
+
const context = buildIntegrationTargets({ home, trackerDir, notifyPath });
|
|
252
|
+
const summary = await previewIntegrations({ context });
|
|
253
|
+
return { summary, pendingBrowserAuth, deviceToken };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async function runSetup({
|
|
257
|
+
opts,
|
|
258
|
+
home,
|
|
259
|
+
baseUrl,
|
|
260
|
+
trackerDir,
|
|
261
|
+
binDir,
|
|
262
|
+
configPath,
|
|
263
|
+
notifyOriginalPath,
|
|
264
|
+
linkCodeStatePath,
|
|
265
|
+
notifyPath,
|
|
266
|
+
appDir,
|
|
267
|
+
trackerBinPath
|
|
268
|
+
}) {
|
|
269
|
+
await ensureDir(trackerDir);
|
|
270
|
+
await ensureDir(binDir);
|
|
271
|
+
|
|
52
272
|
const existingConfig = await readJson(configPath);
|
|
53
273
|
const deviceTokenFromEnv = process.env.VIBESCORE_DEVICE_TOKEN || null;
|
|
54
274
|
|
|
55
275
|
let deviceToken = deviceTokenFromEnv || existingConfig?.deviceToken || null;
|
|
56
276
|
let deviceId = existingConfig?.deviceId || null;
|
|
277
|
+
const installedAt = existingConfig?.installedAt || new Date().toISOString();
|
|
278
|
+
let pendingBrowserAuth = false;
|
|
57
279
|
|
|
58
280
|
await installLocalTrackerApp({ appDir });
|
|
59
281
|
|
|
@@ -96,21 +318,7 @@ async function cmdInit(argv) {
|
|
|
96
318
|
deviceToken = issued.token;
|
|
97
319
|
deviceId = issued.deviceId;
|
|
98
320
|
} else {
|
|
99
|
-
|
|
100
|
-
const flow = await beginBrowserAuth({ baseUrl, dashboardUrl, timeoutMs: 10 * 60_000, open: !opts.noOpen });
|
|
101
|
-
process.stdout.write(
|
|
102
|
-
[
|
|
103
|
-
'',
|
|
104
|
-
'Connect your account:',
|
|
105
|
-
`- Open: ${flow.authUrl}`,
|
|
106
|
-
'- Finish sign in/up in your browser, then come back here.',
|
|
107
|
-
''
|
|
108
|
-
].join('\n')
|
|
109
|
-
);
|
|
110
|
-
const callback = await flow.waitForCallback();
|
|
111
|
-
const issued = await issueDeviceTokenWithAccessToken({ baseUrl, accessToken: callback.accessToken, deviceName });
|
|
112
|
-
deviceToken = issued.token;
|
|
113
|
-
deviceId = issued.deviceId;
|
|
321
|
+
pendingBrowserAuth = true;
|
|
114
322
|
}
|
|
115
323
|
}
|
|
116
324
|
|
|
@@ -118,120 +326,228 @@ async function cmdInit(argv) {
|
|
|
118
326
|
baseUrl,
|
|
119
327
|
deviceToken,
|
|
120
328
|
deviceId,
|
|
121
|
-
installedAt
|
|
329
|
+
installedAt
|
|
122
330
|
};
|
|
123
331
|
|
|
124
332
|
await writeJson(configPath, config);
|
|
125
333
|
await chmod600IfPossible(configPath);
|
|
126
334
|
|
|
127
|
-
// Install notify handler (non-blocking; chains the previous notify if present).
|
|
128
335
|
await writeFileAtomic(
|
|
129
336
|
notifyPath,
|
|
130
337
|
buildNotifyHandler({ trackerDir, trackerBinPath, packageName: '@vibescore/tracker' })
|
|
131
338
|
);
|
|
132
339
|
await fs.chmod(notifyPath, 0o755).catch(() => {});
|
|
133
340
|
|
|
134
|
-
|
|
135
|
-
|
|
341
|
+
const summary = await applyIntegrationSetup({
|
|
342
|
+
home,
|
|
343
|
+
trackerDir,
|
|
344
|
+
notifyPath,
|
|
345
|
+
notifyOriginalPath
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
return {
|
|
349
|
+
summary,
|
|
350
|
+
pendingBrowserAuth,
|
|
351
|
+
deviceToken,
|
|
352
|
+
deviceId,
|
|
353
|
+
installedAt
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function buildIntegrationTargets({ home, trackerDir, notifyPath }) {
|
|
358
|
+
const codexHome = process.env.CODEX_HOME || path.join(home, '.codex');
|
|
359
|
+
const codexConfigPath = path.join(codexHome, 'config.toml');
|
|
136
360
|
const codeHome = process.env.CODE_HOME || path.join(home, '.code');
|
|
137
361
|
const codeConfigPath = path.join(codeHome, 'config.toml');
|
|
362
|
+
const notifyOriginalPath = path.join(trackerDir, 'codex_notify_original.json');
|
|
363
|
+
const codeNotifyOriginalPath = path.join(trackerDir, 'code_notify_original.json');
|
|
138
364
|
const notifyCmd = ['/usr/bin/env', 'node', notifyPath];
|
|
139
|
-
const
|
|
365
|
+
const codeNotifyCmd = ['/usr/bin/env', 'node', notifyPath, '--source=every-code'];
|
|
366
|
+
const claudeDir = path.join(home, '.claude');
|
|
367
|
+
const claudeSettingsPath = path.join(claudeDir, 'settings.json');
|
|
368
|
+
const claudeHookCommand = buildClaudeHookCommand(notifyPath);
|
|
369
|
+
const geminiConfigDir = resolveGeminiConfigDir({ home, env: process.env });
|
|
370
|
+
const geminiSettingsPath = resolveGeminiSettingsPath({ configDir: geminiConfigDir });
|
|
371
|
+
const geminiHookCommand = buildGeminiHookCommand(notifyPath);
|
|
372
|
+
const opencodeConfigDir = resolveOpencodeConfigDir({ home, env: process.env });
|
|
373
|
+
|
|
374
|
+
return {
|
|
140
375
|
codexConfigPath,
|
|
376
|
+
codeConfigPath,
|
|
377
|
+
notifyOriginalPath,
|
|
378
|
+
codeNotifyOriginalPath,
|
|
141
379
|
notifyCmd,
|
|
142
|
-
|
|
143
|
-
|
|
380
|
+
codeNotifyCmd,
|
|
381
|
+
claudeDir,
|
|
382
|
+
claudeSettingsPath,
|
|
383
|
+
claudeHookCommand,
|
|
384
|
+
geminiConfigDir,
|
|
385
|
+
geminiSettingsPath,
|
|
386
|
+
geminiHookCommand,
|
|
387
|
+
opencodeConfigDir
|
|
388
|
+
};
|
|
389
|
+
}
|
|
144
390
|
|
|
145
|
-
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
391
|
+
async function applyIntegrationSetup({ home, trackerDir, notifyPath, notifyOriginalPath }) {
|
|
392
|
+
const context = buildIntegrationTargets({ home, trackerDir, notifyPath });
|
|
393
|
+
context.notifyOriginalPath = notifyOriginalPath;
|
|
394
|
+
|
|
395
|
+
const summary = [];
|
|
396
|
+
|
|
397
|
+
const codexProbe = await probeFile(context.codexConfigPath);
|
|
398
|
+
if (codexProbe.exists) {
|
|
399
|
+
const result = await upsertCodexNotify({
|
|
400
|
+
codexConfigPath: context.codexConfigPath,
|
|
401
|
+
notifyCmd: context.notifyCmd,
|
|
402
|
+
notifyOriginalPath: context.notifyOriginalPath
|
|
403
|
+
});
|
|
404
|
+
summary.push({
|
|
405
|
+
label: 'Codex CLI',
|
|
406
|
+
status: result.changed ? 'updated' : 'set',
|
|
407
|
+
detail: result.changed ? 'Updated config' : 'Config already set'
|
|
156
408
|
});
|
|
157
|
-
|
|
409
|
+
} else {
|
|
410
|
+
summary.push({ label: 'Codex CLI', status: 'skipped', detail: renderSkipDetail(codexProbe) });
|
|
158
411
|
}
|
|
159
412
|
|
|
160
|
-
const
|
|
161
|
-
const claudeSettingsPath = path.join(claudeDir, 'settings.json');
|
|
162
|
-
const claudeDirExists = await isDir(claudeDir);
|
|
163
|
-
let claudeResult = null;
|
|
413
|
+
const claudeDirExists = await isDir(context.claudeDir);
|
|
164
414
|
if (claudeDirExists) {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
hookCommand: claudeHookCommand
|
|
415
|
+
await upsertClaudeHook({
|
|
416
|
+
settingsPath: context.claudeSettingsPath,
|
|
417
|
+
hookCommand: context.claudeHookCommand
|
|
169
418
|
});
|
|
419
|
+
summary.push({ label: 'Claude', status: 'installed', detail: 'Hooks installed' });
|
|
420
|
+
} else {
|
|
421
|
+
summary.push({ label: 'Claude', status: 'skipped', detail: 'Config not found' });
|
|
170
422
|
}
|
|
171
423
|
|
|
172
|
-
const
|
|
173
|
-
const geminiConfigExists = await isDir(geminiConfigDir);
|
|
174
|
-
const geminiSettingsPath = resolveGeminiSettingsPath({ configDir: geminiConfigDir });
|
|
175
|
-
let geminiResult = null;
|
|
424
|
+
const geminiConfigExists = await isDir(context.geminiConfigDir);
|
|
176
425
|
if (geminiConfigExists) {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
hookCommand: geminiHookCommand
|
|
426
|
+
await upsertGeminiHook({
|
|
427
|
+
settingsPath: context.geminiSettingsPath,
|
|
428
|
+
hookCommand: context.geminiHookCommand
|
|
181
429
|
});
|
|
430
|
+
summary.push({ label: 'Gemini', status: 'installed', detail: 'Hooks installed' });
|
|
431
|
+
} else {
|
|
432
|
+
summary.push({ label: 'Gemini', status: 'skipped', detail: 'Config not found' });
|
|
182
433
|
}
|
|
183
434
|
|
|
184
|
-
const opencodeConfigDir = resolveOpencodeConfigDir({ home, env: process.env });
|
|
185
435
|
const opencodeResult = await upsertOpencodePlugin({
|
|
186
|
-
configDir: opencodeConfigDir,
|
|
436
|
+
configDir: context.opencodeConfigDir,
|
|
187
437
|
notifyPath
|
|
188
438
|
});
|
|
439
|
+
if (opencodeResult?.skippedReason === 'config-missing') {
|
|
440
|
+
summary.push({ label: 'Opencode Plugin', status: 'skipped', detail: 'Config not found' });
|
|
441
|
+
} else {
|
|
442
|
+
summary.push({ label: 'Opencode Plugin', status: opencodeResult?.changed ? 'installed' : 'set', detail: 'Plugin installed' });
|
|
443
|
+
}
|
|
189
444
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
? codeChained
|
|
206
|
-
? '- Every Code notify: chained (original preserved)'
|
|
207
|
-
: '- Every Code notify: no original'
|
|
208
|
-
: null,
|
|
209
|
-
claudeDirExists
|
|
210
|
-
? claudeResult?.changed
|
|
211
|
-
? `- Claude hooks: updated (${claudeSettingsPath})`
|
|
212
|
-
: `- Claude hooks: already set (${claudeSettingsPath})`
|
|
213
|
-
: '- Claude hooks: skipped (~/.claude not found)',
|
|
214
|
-
geminiConfigExists
|
|
215
|
-
? geminiResult?.changed
|
|
216
|
-
? `- Gemini hooks: updated (${geminiSettingsPath})`
|
|
217
|
-
: `- Gemini hooks: already set (${geminiSettingsPath})`
|
|
218
|
-
: `- Gemini hooks: skipped (${geminiConfigDir} not found)`,
|
|
219
|
-
opencodeResult?.skippedReason === 'config-missing'
|
|
220
|
-
? '- Opencode plugin: skipped (config dir missing)'
|
|
221
|
-
: opencodeResult?.changed
|
|
222
|
-
? `- Opencode plugin: updated (${opencodeConfigDir})`
|
|
223
|
-
: `- Opencode plugin: already set (${opencodeConfigDir})`,
|
|
224
|
-
deviceToken ? `- Device token: stored (${maskSecret(deviceToken)})` : '- Device token: not configured (set VIBESCORE_DEVICE_TOKEN and re-run init)',
|
|
225
|
-
''
|
|
226
|
-
].join('\n')
|
|
227
|
-
);
|
|
445
|
+
const codeProbe = await probeFile(context.codeConfigPath);
|
|
446
|
+
if (codeProbe.exists) {
|
|
447
|
+
const result = await upsertEveryCodeNotify({
|
|
448
|
+
codeConfigPath: context.codeConfigPath,
|
|
449
|
+
notifyCmd: context.codeNotifyCmd,
|
|
450
|
+
notifyOriginalPath: context.codeNotifyOriginalPath
|
|
451
|
+
});
|
|
452
|
+
summary.push({
|
|
453
|
+
label: 'Every Code',
|
|
454
|
+
status: result.changed ? 'updated' : 'set',
|
|
455
|
+
detail: result.changed ? 'Updated config' : 'Config already set'
|
|
456
|
+
});
|
|
457
|
+
} else {
|
|
458
|
+
summary.push({ label: 'Every Code', status: 'skipped', detail: renderSkipDetail(codeProbe) });
|
|
459
|
+
}
|
|
228
460
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
461
|
+
return summary;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
async function previewIntegrations({ context }) {
|
|
465
|
+
const summary = [];
|
|
466
|
+
|
|
467
|
+
const codexProbe = await probeFile(context.codexConfigPath);
|
|
468
|
+
if (codexProbe.exists) {
|
|
469
|
+
const existing = await readCodexNotify(context.codexConfigPath);
|
|
470
|
+
const matches = arraysEqual(existing, context.notifyCmd);
|
|
471
|
+
summary.push({
|
|
472
|
+
label: 'Codex CLI',
|
|
473
|
+
status: matches ? 'set' : 'updated',
|
|
474
|
+
detail: matches ? 'Already configured' : 'Will update config'
|
|
475
|
+
});
|
|
476
|
+
} else {
|
|
477
|
+
summary.push({ label: 'Codex CLI', status: 'skipped', detail: renderSkipDetail(codexProbe) });
|
|
234
478
|
}
|
|
479
|
+
|
|
480
|
+
const claudeDirExists = await isDir(context.claudeDir);
|
|
481
|
+
if (claudeDirExists) {
|
|
482
|
+
const configured = await isClaudeHookConfigured({
|
|
483
|
+
settingsPath: context.claudeSettingsPath,
|
|
484
|
+
hookCommand: context.claudeHookCommand
|
|
485
|
+
});
|
|
486
|
+
summary.push({
|
|
487
|
+
label: 'Claude',
|
|
488
|
+
status: 'installed',
|
|
489
|
+
detail: configured ? 'Hooks already installed' : 'Will install hooks'
|
|
490
|
+
});
|
|
491
|
+
} else {
|
|
492
|
+
summary.push({ label: 'Claude', status: 'skipped', detail: 'Config not found' });
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const geminiConfigExists = await isDir(context.geminiConfigDir);
|
|
496
|
+
if (geminiConfigExists) {
|
|
497
|
+
const configured = await isGeminiHookConfigured({
|
|
498
|
+
settingsPath: context.geminiSettingsPath,
|
|
499
|
+
hookCommand: context.geminiHookCommand
|
|
500
|
+
});
|
|
501
|
+
summary.push({
|
|
502
|
+
label: 'Gemini',
|
|
503
|
+
status: 'installed',
|
|
504
|
+
detail: configured ? 'Hooks already installed' : 'Will install hooks'
|
|
505
|
+
});
|
|
506
|
+
} else {
|
|
507
|
+
summary.push({ label: 'Gemini', status: 'skipped', detail: 'Config not found' });
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const opencodeDirExists = await isDir(context.opencodeConfigDir);
|
|
511
|
+
const installed = await isOpencodePluginInstalled({ configDir: context.opencodeConfigDir });
|
|
512
|
+
const opencodeDetail = installed
|
|
513
|
+
? 'Plugin already installed'
|
|
514
|
+
: opencodeDirExists
|
|
515
|
+
? 'Will install plugin'
|
|
516
|
+
: 'Will create config and install plugin';
|
|
517
|
+
summary.push({
|
|
518
|
+
label: 'Opencode Plugin',
|
|
519
|
+
status: 'installed',
|
|
520
|
+
detail: opencodeDetail
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
const codeProbe = await probeFile(context.codeConfigPath);
|
|
524
|
+
if (codeProbe.exists) {
|
|
525
|
+
const existing = await readEveryCodeNotify(context.codeConfigPath);
|
|
526
|
+
const matches = arraysEqual(existing, context.codeNotifyCmd);
|
|
527
|
+
summary.push({
|
|
528
|
+
label: 'Every Code',
|
|
529
|
+
status: matches ? 'set' : 'updated',
|
|
530
|
+
detail: matches ? 'Already configured' : 'Will update config'
|
|
531
|
+
});
|
|
532
|
+
} else {
|
|
533
|
+
summary.push({ label: 'Every Code', status: 'skipped', detail: renderSkipDetail(codeProbe) });
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
return summary;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function renderSkipDetail(probe) {
|
|
540
|
+
if (!probe || probe.reason === 'missing') return 'Config not found';
|
|
541
|
+
if (probe.reason === 'permission-denied') return 'Permission denied';
|
|
542
|
+
if (probe.reason === 'not-file') return 'Invalid config';
|
|
543
|
+
return 'Unavailable';
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function arraysEqual(a, b) {
|
|
547
|
+
if (!Array.isArray(a) || !Array.isArray(b)) return false;
|
|
548
|
+
if (a.length !== b.length) return false;
|
|
549
|
+
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
550
|
+
return true;
|
|
235
551
|
}
|
|
236
552
|
|
|
237
553
|
function parseArgs(argv) {
|
|
@@ -243,7 +559,9 @@ function parseArgs(argv) {
|
|
|
243
559
|
deviceName: null,
|
|
244
560
|
linkCode: null,
|
|
245
561
|
noAuth: false,
|
|
246
|
-
noOpen: false
|
|
562
|
+
noOpen: false,
|
|
563
|
+
yes: false,
|
|
564
|
+
dryRun: false
|
|
247
565
|
};
|
|
248
566
|
|
|
249
567
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -256,14 +574,16 @@ function parseArgs(argv) {
|
|
|
256
574
|
else if (a === '--link-code') out.linkCode = argv[++i] || null;
|
|
257
575
|
else if (a === '--no-auth') out.noAuth = true;
|
|
258
576
|
else if (a === '--no-open') out.noOpen = true;
|
|
577
|
+
else if (a === '--yes') out.yes = true;
|
|
578
|
+
else if (a === '--dry-run') out.dryRun = true;
|
|
259
579
|
else throw new Error(`Unknown option: ${a}`);
|
|
260
580
|
}
|
|
261
581
|
return out;
|
|
262
582
|
}
|
|
263
583
|
|
|
264
|
-
function
|
|
265
|
-
if (
|
|
266
|
-
return
|
|
584
|
+
function sleep(ms) {
|
|
585
|
+
if (!ms || ms <= 0) return Promise.resolve();
|
|
586
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
267
587
|
}
|
|
268
588
|
|
|
269
589
|
function normalizePlatform(value) {
|
|
@@ -311,7 +631,8 @@ const signalPath = ${JSON.stringify(queueSignalPath)};
|
|
|
311
631
|
const codexOriginalPath = ${JSON.stringify(originalPath)};
|
|
312
632
|
const codeOriginalPath = ${JSON.stringify(path.join(trackerDir, 'code_notify_original.json'))};
|
|
313
633
|
const trackerBinPath = ${JSON.stringify(trackerBinPath)};
|
|
314
|
-
const depsMarkerPath = path.join(trackerDir, 'app', 'node_modules', '@insforge', 'sdk', 'package.json');
|
|
634
|
+
const depsMarkerPath = path.join(trackerDir, 'app', 'node_modules', '@insforge', 'sdk', 'package.json');
|
|
635
|
+
const configPath = path.join(trackerDir, 'config.json');
|
|
315
636
|
const fallbackPkg = ${JSON.stringify(fallbackPkg)};
|
|
316
637
|
const selfPath = path.resolve(__filename);
|
|
317
638
|
const home = os.homedir();
|
|
@@ -323,11 +644,19 @@ try {
|
|
|
323
644
|
|
|
324
645
|
// Throttle spawn: at most once per 20 seconds.
|
|
325
646
|
try {
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
647
|
+
const throttlePath = path.join(trackerDir, 'sync.throttle');
|
|
648
|
+
let deviceToken = process.env.VIBESCORE_DEVICE_TOKEN || null;
|
|
649
|
+
if (!deviceToken) {
|
|
650
|
+
try {
|
|
651
|
+
const cfg = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
652
|
+
if (cfg && typeof cfg.deviceToken === 'string') deviceToken = cfg.deviceToken;
|
|
653
|
+
} catch (_) {}
|
|
654
|
+
}
|
|
655
|
+
const canSync = Boolean(deviceToken && deviceToken.length > 0);
|
|
656
|
+
const now = Date.now();
|
|
657
|
+
let last = 0;
|
|
658
|
+
try { last = Number(fs.readFileSync(throttlePath, 'utf8')) || 0; } catch (_) {}
|
|
659
|
+
if (canSync && now - last > 20_000) {
|
|
331
660
|
try { fs.writeFileSync(throttlePath, String(now), 'utf8'); } catch (_) {}
|
|
332
661
|
const hasLocalRuntime = fs.existsSync(trackerBinPath);
|
|
333
662
|
const hasLocalDeps = fs.existsSync(depsMarkerPath);
|
|
@@ -420,12 +749,15 @@ async function checkUrlReachable(url) {
|
|
|
420
749
|
}
|
|
421
750
|
}
|
|
422
751
|
|
|
423
|
-
async function
|
|
752
|
+
async function probeFile(p) {
|
|
424
753
|
try {
|
|
425
754
|
const st = await fs.stat(p);
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
755
|
+
if (st.isFile()) return { exists: true, reason: null };
|
|
756
|
+
return { exists: false, reason: 'not-file' };
|
|
757
|
+
} catch (e) {
|
|
758
|
+
if (e?.code === 'ENOENT' || e?.code === 'ENOTDIR') return { exists: false, reason: 'missing' };
|
|
759
|
+
if (e?.code === 'EACCES' || e?.code === 'EPERM') return { exists: false, reason: 'permission-denied' };
|
|
760
|
+
return { exists: false, reason: 'error', code: e?.code || 'unknown' };
|
|
429
761
|
}
|
|
430
762
|
}
|
|
431
763
|
|
|
@@ -473,7 +805,9 @@ function spawnInitSync({ trackerBinPath, packageName }) {
|
|
|
473
805
|
});
|
|
474
806
|
child.on('error', (err) => {
|
|
475
807
|
const msg = err && err.message ? err.message : 'unknown error';
|
|
476
|
-
process.
|
|
808
|
+
const detail = process.env.VIBESCORE_DEBUG === '1' ? ` (${msg})` : '';
|
|
809
|
+
process.stderr.write(`Minor issue: Background sync could not start${detail}.\n`);
|
|
810
|
+
process.stderr.write('Run: npx --yes @vibescore/tracker sync\n');
|
|
477
811
|
});
|
|
478
812
|
child.unref();
|
|
479
813
|
}
|
|
@@ -17,7 +17,8 @@ async function cmdUninstall(argv) {
|
|
|
17
17
|
const home = os.homedir();
|
|
18
18
|
const trackerDir = path.join(home, '.vibescore', 'tracker');
|
|
19
19
|
const binDir = path.join(home, '.vibescore', 'bin');
|
|
20
|
-
const
|
|
20
|
+
const codexHome = process.env.CODEX_HOME || path.join(home, '.codex');
|
|
21
|
+
const codexConfigPath = path.join(codexHome, 'config.toml');
|
|
21
22
|
const codeHome = process.env.CODE_HOME || path.join(home, '.code');
|
|
22
23
|
const codeConfigPath = path.join(codeHome, 'config.toml');
|
|
23
24
|
const claudeSettingsPath = path.join(home, '.claude', 'settings.json');
|
package/src/lib/browser-auth.js
CHANGED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
const readline = require('node:readline');
|
|
2
|
+
|
|
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
|
+
|
|
12
|
+
const SPINNER_FRAMES = ['|', '/', '-', '\\'];
|
|
13
|
+
|
|
14
|
+
function isInteractive() {
|
|
15
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function formatLine(line, width) {
|
|
19
|
+
if (!width) return line;
|
|
20
|
+
const raw = String(line || '');
|
|
21
|
+
const pad = Math.max(0, width - raw.length);
|
|
22
|
+
return raw + ' '.repeat(pad);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function renderBox(lines, { padding = 1 } = {}) {
|
|
26
|
+
const content = lines.map((line) => String(line || ''));
|
|
27
|
+
const maxLen = content.reduce((max, line) => Math.max(max, line.length), 0);
|
|
28
|
+
const innerWidth = maxLen + padding * 2;
|
|
29
|
+
const top = `+${'-'.repeat(innerWidth)}+`;
|
|
30
|
+
const bottom = `+${'-'.repeat(innerWidth)}+`;
|
|
31
|
+
const body = content.map((line) => {
|
|
32
|
+
const padded = ' '.repeat(padding) + formatLine(line, maxLen) + ' '.repeat(padding);
|
|
33
|
+
return `|${padded}|`;
|
|
34
|
+
});
|
|
35
|
+
return [top, ...body, bottom].join('\n');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function color(text, token) {
|
|
39
|
+
return `${token}${text}${RESET}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function underline(text) {
|
|
43
|
+
return `${UNDERLINE}${text}${RESET}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function promptEnter(message) {
|
|
47
|
+
if (!isInteractive()) return;
|
|
48
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
49
|
+
await new Promise((resolve) => rl.question(message, () => resolve()));
|
|
50
|
+
rl.close();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function promptMenu({ message, options, defaultIndex = 0 }) {
|
|
54
|
+
if (!isInteractive()) return options[defaultIndex] || options[0];
|
|
55
|
+
|
|
56
|
+
const safeOptions = Array.isArray(options) ? options : [];
|
|
57
|
+
if (safeOptions.length === 0) return '';
|
|
58
|
+
|
|
59
|
+
const maxIndex = safeOptions.length - 1;
|
|
60
|
+
let currentIndex = Math.min(Math.max(defaultIndex, 0), maxIndex);
|
|
61
|
+
const promptMessage = `${message} (Use Up/Down arrows, Enter)`;
|
|
62
|
+
const linesCount = safeOptions.length + 1;
|
|
63
|
+
|
|
64
|
+
const renderLines = () => {
|
|
65
|
+
const lines = [promptMessage];
|
|
66
|
+
safeOptions.forEach((opt, idx) => {
|
|
67
|
+
const prefix = idx === currentIndex ? '>' : ' ';
|
|
68
|
+
lines.push(`${prefix} ${opt}`);
|
|
69
|
+
});
|
|
70
|
+
process.stdout.write(lines.join('\n'));
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const rerender = () => {
|
|
74
|
+
for (let i = 0; i < linesCount; i += 1) {
|
|
75
|
+
process.stdout.write('\x1b[2K');
|
|
76
|
+
if (i < linesCount - 1) process.stdout.write('\x1b[1A');
|
|
77
|
+
}
|
|
78
|
+
process.stdout.write('\r');
|
|
79
|
+
renderLines();
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
renderLines();
|
|
83
|
+
|
|
84
|
+
return await new Promise((resolve) => {
|
|
85
|
+
const cleanup = () => {
|
|
86
|
+
process.stdin.off('keypress', onKeypress);
|
|
87
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
88
|
+
process.stdin.pause();
|
|
89
|
+
process.stdout.write('\n');
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const onKeypress = (str, key = {}) => {
|
|
93
|
+
if (key.ctrl && key.name === 'c') {
|
|
94
|
+
cleanup();
|
|
95
|
+
return resolve(safeOptions[currentIndex]);
|
|
96
|
+
}
|
|
97
|
+
if (key.name === 'up' || str === 'k') {
|
|
98
|
+
currentIndex = currentIndex === 0 ? maxIndex : currentIndex - 1;
|
|
99
|
+
rerender();
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (key.name === 'down' || str === 'j') {
|
|
103
|
+
currentIndex = currentIndex === maxIndex ? 0 : currentIndex + 1;
|
|
104
|
+
rerender();
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (key.name === 'return') {
|
|
108
|
+
cleanup();
|
|
109
|
+
return resolve(safeOptions[currentIndex]);
|
|
110
|
+
}
|
|
111
|
+
if (str && /^[1-9]$/.test(str)) {
|
|
112
|
+
const idx = Number.parseInt(str, 10) - 1;
|
|
113
|
+
if (idx >= 0 && idx <= maxIndex) {
|
|
114
|
+
currentIndex = idx;
|
|
115
|
+
cleanup();
|
|
116
|
+
return resolve(safeOptions[currentIndex]);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
readline.emitKeypressEvents(process.stdin);
|
|
122
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(true);
|
|
123
|
+
process.stdin.resume();
|
|
124
|
+
process.stdin.on('keypress', onKeypress);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function createSpinner({ text, intervalMs = 80 }) {
|
|
129
|
+
let frame = 0;
|
|
130
|
+
let timer = null;
|
|
131
|
+
|
|
132
|
+
function start() {
|
|
133
|
+
if (!isInteractive()) {
|
|
134
|
+
process.stdout.write(`${text}\n`);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
timer = setInterval(() => {
|
|
138
|
+
const glyph = SPINNER_FRAMES[frame % SPINNER_FRAMES.length];
|
|
139
|
+
frame += 1;
|
|
140
|
+
process.stdout.write(`\r${glyph} ${text}`);
|
|
141
|
+
}, intervalMs);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function stop(successText) {
|
|
145
|
+
if (timer) clearInterval(timer);
|
|
146
|
+
if (isInteractive()) {
|
|
147
|
+
process.stdout.write(`\r${' '.repeat(text.length + 4)}\r`);
|
|
148
|
+
}
|
|
149
|
+
if (successText) process.stdout.write(`${successText}\n`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return { start, stop };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function formatSummaryLine({ label, status, detail }) {
|
|
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
|
+
const line = ` ${bullet} ${label.padEnd(22)} [${statusLabel}]`;
|
|
160
|
+
return isSuccess ? line : color(line, DIM);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
module.exports = {
|
|
164
|
+
BOLD,
|
|
165
|
+
DIM,
|
|
166
|
+
CYAN,
|
|
167
|
+
GREEN,
|
|
168
|
+
YELLOW,
|
|
169
|
+
BLUE,
|
|
170
|
+
RESET,
|
|
171
|
+
color,
|
|
172
|
+
underline,
|
|
173
|
+
renderBox,
|
|
174
|
+
isInteractive,
|
|
175
|
+
promptMenu,
|
|
176
|
+
promptEnter,
|
|
177
|
+
createSpinner,
|
|
178
|
+
formatSummaryLine
|
|
179
|
+
};
|