@vibescore/tracker 0.2.4 → 0.2.6
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 +407 -147
- package/src/lib/browser-auth.js +2 -1
- package/src/lib/cli-ui.js +179 -0
- package/src/lib/init-flow.js +48 -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.6",
|
|
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,47 @@ 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
|
+
isInteractive,
|
|
38
|
+
promptMenu,
|
|
39
|
+
createSpinner
|
|
40
|
+
} = require('../lib/cli-ui');
|
|
41
|
+
const { renderLocalReport, renderAuthTransition, renderSuccessBox } = require('../lib/init-flow');
|
|
42
|
+
|
|
43
|
+
const ASCII_LOGO = [
|
|
44
|
+
'██╗ ██╗██╗██████╗ ███████╗███████╗ ██████╗ ██████╗ ██████╗ ███████╗',
|
|
45
|
+
'██║ ██║██║██╔══██╗██╔════╝██╔════╝██╔════╝ ██╔═══██╗██╔══██╗██╔════╝',
|
|
46
|
+
'██║ ██║██║██████╔╝█████╗ ███████╗██║ ██║ ██║██████╔╝█████╗',
|
|
47
|
+
'╚██╗ ██╔╝██║██╔══██╗██╔══╝ ╚════██║██║ ██║ ██║██╔══██╗██╔══╝',
|
|
48
|
+
' ╚████╔╝ ██║██████╔╝███████╗███████║╚██████╗ ╚██████╔╝██║ ██║███████╗',
|
|
49
|
+
' ╚═══╝ ╚═╝╚═════╝ ╚══════╝╚══════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝'
|
|
50
|
+
].join('\n');
|
|
51
|
+
|
|
52
|
+
const DIVIDER = '----------------------------------------------';
|
|
30
53
|
|
|
31
54
|
async function cmdInit(argv) {
|
|
32
55
|
const opts = parseArgs(argv);
|
|
@@ -36,9 +59,6 @@ async function cmdInit(argv) {
|
|
|
36
59
|
const trackerDir = path.join(rootDir, 'tracker');
|
|
37
60
|
const binDir = path.join(rootDir, 'bin');
|
|
38
61
|
|
|
39
|
-
await ensureDir(trackerDir);
|
|
40
|
-
await ensureDir(binDir);
|
|
41
|
-
|
|
42
62
|
const configPath = path.join(trackerDir, 'config.json');
|
|
43
63
|
const notifyOriginalPath = path.join(trackerDir, 'codex_notify_original.json');
|
|
44
64
|
const linkCodeStatePath = path.join(trackerDir, 'link_code_state.json');
|
|
@@ -49,11 +69,174 @@ async function cmdInit(argv) {
|
|
|
49
69
|
const appDir = path.join(trackerDir, 'app');
|
|
50
70
|
const trackerBinPath = path.join(appDir, 'bin', 'tracker.js');
|
|
51
71
|
|
|
72
|
+
renderWelcome();
|
|
73
|
+
|
|
74
|
+
if (opts.dryRun) {
|
|
75
|
+
process.stdout.write(`${color('Dry run: preview only (no changes applied).', DIM)}\n\n`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (isInteractive() && !opts.yes && !opts.dryRun) {
|
|
79
|
+
const choice = await promptMenu({
|
|
80
|
+
message: '? Proceed with installation?',
|
|
81
|
+
options: ['Yes, configure my environment', 'No, exit'],
|
|
82
|
+
defaultIndex: 0
|
|
83
|
+
});
|
|
84
|
+
const normalizedChoice = String(choice || '').trim().toLowerCase();
|
|
85
|
+
if (normalizedChoice.startsWith('no') || normalizedChoice.includes('exit')) {
|
|
86
|
+
process.stdout.write('Setup cancelled.\n');
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (opts.dryRun) {
|
|
92
|
+
const preview = await buildDryRunSummary({
|
|
93
|
+
opts,
|
|
94
|
+
home,
|
|
95
|
+
trackerDir,
|
|
96
|
+
configPath,
|
|
97
|
+
notifyPath
|
|
98
|
+
});
|
|
99
|
+
renderLocalReport({ summary: preview.summary, isDryRun: true });
|
|
100
|
+
if (preview.pendingBrowserAuth) {
|
|
101
|
+
process.stdout.write('Account linking would be required for full setup.\n');
|
|
102
|
+
} else if (!preview.deviceToken) {
|
|
103
|
+
renderAccountNotLinked({ context: 'dry-run' });
|
|
104
|
+
}
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const spinner = createSpinner({ text: 'Analyzing and configuring local environment...' });
|
|
109
|
+
spinner.start();
|
|
110
|
+
let setup;
|
|
111
|
+
try {
|
|
112
|
+
setup = await runSetup({
|
|
113
|
+
opts,
|
|
114
|
+
home,
|
|
115
|
+
baseUrl,
|
|
116
|
+
trackerDir,
|
|
117
|
+
binDir,
|
|
118
|
+
configPath,
|
|
119
|
+
notifyOriginalPath,
|
|
120
|
+
linkCodeStatePath,
|
|
121
|
+
notifyPath,
|
|
122
|
+
appDir,
|
|
123
|
+
trackerBinPath
|
|
124
|
+
});
|
|
125
|
+
} catch (err) {
|
|
126
|
+
spinner.stop();
|
|
127
|
+
throw err;
|
|
128
|
+
}
|
|
129
|
+
spinner.stop();
|
|
130
|
+
|
|
131
|
+
renderLocalReport({ summary: setup.summary, isDryRun: false });
|
|
132
|
+
|
|
133
|
+
let deviceToken = setup.deviceToken;
|
|
134
|
+
let deviceId = setup.deviceId;
|
|
135
|
+
|
|
136
|
+
if (setup.pendingBrowserAuth) {
|
|
137
|
+
const deviceName = opts.deviceName || os.hostname();
|
|
138
|
+
if (!dashboardUrl) dashboardUrl = await detectLocalDashboardUrl();
|
|
139
|
+
const flow = await beginBrowserAuth({ baseUrl, dashboardUrl, timeoutMs: 10 * 60_000, open: false });
|
|
140
|
+
const canAutoOpen = !opts.noOpen;
|
|
141
|
+
renderAuthTransition({ authUrl: flow.authUrl, canAutoOpen });
|
|
142
|
+
if (canAutoOpen) {
|
|
143
|
+
if (isInteractive()) await sleep(250);
|
|
144
|
+
openInBrowser(flow.authUrl);
|
|
145
|
+
}
|
|
146
|
+
const callback = await flow.waitForCallback();
|
|
147
|
+
const issued = await issueDeviceTokenWithAccessToken({ baseUrl, accessToken: callback.accessToken, deviceName });
|
|
148
|
+
deviceToken = issued.token;
|
|
149
|
+
deviceId = issued.deviceId;
|
|
150
|
+
await writeJson(configPath, { baseUrl, deviceToken, deviceId, installedAt: setup.installedAt });
|
|
151
|
+
await chmod600IfPossible(configPath);
|
|
152
|
+
const resolvedDashboardUrl = dashboardUrl || null;
|
|
153
|
+
renderSuccessBox({ configPath, dashboardUrl: resolvedDashboardUrl });
|
|
154
|
+
} else if (deviceToken) {
|
|
155
|
+
if (!dashboardUrl) dashboardUrl = await detectLocalDashboardUrl();
|
|
156
|
+
const resolvedDashboardUrl = dashboardUrl || null;
|
|
157
|
+
renderSuccessBox({ configPath, dashboardUrl: resolvedDashboardUrl });
|
|
158
|
+
} else {
|
|
159
|
+
renderAccountNotLinked();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
spawnInitSync({ trackerBinPath, packageName: '@vibescore/tracker' });
|
|
164
|
+
} catch (err) {
|
|
165
|
+
const msg = err && err.message ? err.message : 'unknown error';
|
|
166
|
+
process.stderr.write(`Initial sync spawn failed: ${msg}\n`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function renderWelcome() {
|
|
171
|
+
process.stdout.write(
|
|
172
|
+
[
|
|
173
|
+
ASCII_LOGO,
|
|
174
|
+
'',
|
|
175
|
+
`${BOLD}Welcome to VibeScore CLI${RESET}`,
|
|
176
|
+
DIVIDER,
|
|
177
|
+
`${CYAN}Privacy First: Your content stays local. We only upload token counts and minimal metadata, never prompts or responses.${RESET}`,
|
|
178
|
+
DIVIDER,
|
|
179
|
+
'',
|
|
180
|
+
'This tool will:',
|
|
181
|
+
' - Analyze your local AI CLI configurations (Codex, Every Code, Claude, Gemini, Opencode)',
|
|
182
|
+
' - Set up lightweight hooks to track your flow state',
|
|
183
|
+
' - Link your device to your VibeScore account',
|
|
184
|
+
'',
|
|
185
|
+
'(Nothing will be changed until you confirm below)',
|
|
186
|
+
''
|
|
187
|
+
].join('\n')
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function renderAccountNotLinked({ context } = {}) {
|
|
192
|
+
if (context === 'dry-run') {
|
|
193
|
+
process.stdout.write(['', 'Account not linked (dry run).', 'Run init without --dry-run to link your account.', ''].join('\n'));
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
process.stdout.write(['', 'Account not linked.', 'Set VIBESCORE_DEVICE_TOKEN then re-run init.', ''].join('\n'));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function shouldUseBrowserAuth({ deviceToken, opts }) {
|
|
200
|
+
if (deviceToken) return false;
|
|
201
|
+
if (opts.noAuth) return false;
|
|
202
|
+
if (opts.linkCode) return false;
|
|
203
|
+
if (opts.email || opts.password) return false;
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function buildDryRunSummary({ opts, home, trackerDir, configPath, notifyPath }) {
|
|
208
|
+
const existingConfig = await readJson(configPath);
|
|
209
|
+
const deviceTokenFromEnv = process.env.VIBESCORE_DEVICE_TOKEN || null;
|
|
210
|
+
const deviceToken = deviceTokenFromEnv || existingConfig?.deviceToken || null;
|
|
211
|
+
const pendingBrowserAuth = shouldUseBrowserAuth({ deviceToken, opts });
|
|
212
|
+
const context = buildIntegrationTargets({ home, trackerDir, notifyPath });
|
|
213
|
+
const summary = await previewIntegrations({ context });
|
|
214
|
+
return { summary, pendingBrowserAuth, deviceToken };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function runSetup({
|
|
218
|
+
opts,
|
|
219
|
+
home,
|
|
220
|
+
baseUrl,
|
|
221
|
+
trackerDir,
|
|
222
|
+
binDir,
|
|
223
|
+
configPath,
|
|
224
|
+
notifyOriginalPath,
|
|
225
|
+
linkCodeStatePath,
|
|
226
|
+
notifyPath,
|
|
227
|
+
appDir,
|
|
228
|
+
trackerBinPath
|
|
229
|
+
}) {
|
|
230
|
+
await ensureDir(trackerDir);
|
|
231
|
+
await ensureDir(binDir);
|
|
232
|
+
|
|
52
233
|
const existingConfig = await readJson(configPath);
|
|
53
234
|
const deviceTokenFromEnv = process.env.VIBESCORE_DEVICE_TOKEN || null;
|
|
54
235
|
|
|
55
236
|
let deviceToken = deviceTokenFromEnv || existingConfig?.deviceToken || null;
|
|
56
237
|
let deviceId = existingConfig?.deviceId || null;
|
|
238
|
+
const installedAt = existingConfig?.installedAt || new Date().toISOString();
|
|
239
|
+
let pendingBrowserAuth = false;
|
|
57
240
|
|
|
58
241
|
await installLocalTrackerApp({ appDir });
|
|
59
242
|
|
|
@@ -96,21 +279,7 @@ async function cmdInit(argv) {
|
|
|
96
279
|
deviceToken = issued.token;
|
|
97
280
|
deviceId = issued.deviceId;
|
|
98
281
|
} 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;
|
|
282
|
+
pendingBrowserAuth = true;
|
|
114
283
|
}
|
|
115
284
|
}
|
|
116
285
|
|
|
@@ -118,135 +287,228 @@ async function cmdInit(argv) {
|
|
|
118
287
|
baseUrl,
|
|
119
288
|
deviceToken,
|
|
120
289
|
deviceId,
|
|
121
|
-
installedAt
|
|
290
|
+
installedAt
|
|
122
291
|
};
|
|
123
292
|
|
|
124
293
|
await writeJson(configPath, config);
|
|
125
294
|
await chmod600IfPossible(configPath);
|
|
126
295
|
|
|
127
|
-
// Install notify handler (non-blocking; chains the previous notify if present).
|
|
128
296
|
await writeFileAtomic(
|
|
129
297
|
notifyPath,
|
|
130
298
|
buildNotifyHandler({ trackerDir, trackerBinPath, packageName: '@vibescore/tracker' })
|
|
131
299
|
);
|
|
132
300
|
await fs.chmod(notifyPath, 0o755).catch(() => {});
|
|
133
301
|
|
|
134
|
-
|
|
302
|
+
const summary = await applyIntegrationSetup({
|
|
303
|
+
home,
|
|
304
|
+
trackerDir,
|
|
305
|
+
notifyPath,
|
|
306
|
+
notifyOriginalPath
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
return {
|
|
310
|
+
summary,
|
|
311
|
+
pendingBrowserAuth,
|
|
312
|
+
deviceToken,
|
|
313
|
+
deviceId,
|
|
314
|
+
installedAt
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function buildIntegrationTargets({ home, trackerDir, notifyPath }) {
|
|
135
319
|
const codexHome = process.env.CODEX_HOME || path.join(home, '.codex');
|
|
136
320
|
const codexConfigPath = path.join(codexHome, 'config.toml');
|
|
137
321
|
const codeHome = process.env.CODE_HOME || path.join(home, '.code');
|
|
138
322
|
const codeConfigPath = path.join(codeHome, 'config.toml');
|
|
323
|
+
const notifyOriginalPath = path.join(trackerDir, 'codex_notify_original.json');
|
|
324
|
+
const codeNotifyOriginalPath = path.join(trackerDir, 'code_notify_original.json');
|
|
139
325
|
const notifyCmd = ['/usr/bin/env', 'node', notifyPath];
|
|
140
|
-
const
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
326
|
+
const codeNotifyCmd = ['/usr/bin/env', 'node', notifyPath, '--source=every-code'];
|
|
327
|
+
const claudeDir = path.join(home, '.claude');
|
|
328
|
+
const claudeSettingsPath = path.join(claudeDir, 'settings.json');
|
|
329
|
+
const claudeHookCommand = buildClaudeHookCommand(notifyPath);
|
|
330
|
+
const geminiConfigDir = resolveGeminiConfigDir({ home, env: process.env });
|
|
331
|
+
const geminiSettingsPath = resolveGeminiSettingsPath({ configDir: geminiConfigDir });
|
|
332
|
+
const geminiHookCommand = buildGeminiHookCommand(notifyPath);
|
|
333
|
+
const opencodeConfigDir = resolveOpencodeConfigDir({ home, env: process.env });
|
|
334
|
+
|
|
335
|
+
return {
|
|
336
|
+
codexConfigPath,
|
|
337
|
+
codeConfigPath,
|
|
338
|
+
notifyOriginalPath,
|
|
339
|
+
codeNotifyOriginalPath,
|
|
340
|
+
notifyCmd,
|
|
341
|
+
codeNotifyCmd,
|
|
342
|
+
claudeDir,
|
|
343
|
+
claudeSettingsPath,
|
|
344
|
+
claudeHookCommand,
|
|
345
|
+
geminiConfigDir,
|
|
346
|
+
geminiSettingsPath,
|
|
347
|
+
geminiHookCommand,
|
|
348
|
+
opencodeConfigDir
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
async function applyIntegrationSetup({ home, trackerDir, notifyPath, notifyOriginalPath }) {
|
|
353
|
+
const context = buildIntegrationTargets({ home, trackerDir, notifyPath });
|
|
354
|
+
context.notifyOriginalPath = notifyOriginalPath;
|
|
355
|
+
|
|
356
|
+
const summary = [];
|
|
357
|
+
|
|
358
|
+
const codexProbe = await probeFile(context.codexConfigPath);
|
|
359
|
+
if (codexProbe.exists) {
|
|
360
|
+
const result = await upsertCodexNotify({
|
|
361
|
+
codexConfigPath: context.codexConfigPath,
|
|
362
|
+
notifyCmd: context.notifyCmd,
|
|
363
|
+
notifyOriginalPath: context.notifyOriginalPath
|
|
149
364
|
});
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
const codeConfigExists = codeProbe.exists;
|
|
155
|
-
let codeResult = null;
|
|
156
|
-
let codeChained = null;
|
|
157
|
-
if (codeConfigExists) {
|
|
158
|
-
const codeNotifyCmd = ['/usr/bin/env', 'node', notifyPath, '--source=every-code'];
|
|
159
|
-
codeResult = await upsertEveryCodeNotify({
|
|
160
|
-
codeConfigPath,
|
|
161
|
-
notifyCmd: codeNotifyCmd,
|
|
162
|
-
notifyOriginalPath: codeNotifyOriginalPath
|
|
365
|
+
summary.push({
|
|
366
|
+
label: 'Codex CLI',
|
|
367
|
+
status: result.changed ? 'updated' : 'set',
|
|
368
|
+
detail: result.changed ? 'Updated config' : 'Config already set'
|
|
163
369
|
});
|
|
164
|
-
|
|
370
|
+
} else {
|
|
371
|
+
summary.push({ label: 'Codex CLI', status: 'skipped', detail: renderSkipDetail(codexProbe) });
|
|
165
372
|
}
|
|
166
373
|
|
|
167
|
-
const
|
|
168
|
-
const claudeSettingsPath = path.join(claudeDir, 'settings.json');
|
|
169
|
-
const claudeDirExists = await isDir(claudeDir);
|
|
170
|
-
let claudeResult = null;
|
|
374
|
+
const claudeDirExists = await isDir(context.claudeDir);
|
|
171
375
|
if (claudeDirExists) {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
hookCommand: claudeHookCommand
|
|
376
|
+
await upsertClaudeHook({
|
|
377
|
+
settingsPath: context.claudeSettingsPath,
|
|
378
|
+
hookCommand: context.claudeHookCommand
|
|
176
379
|
});
|
|
380
|
+
summary.push({ label: 'Claude', status: 'installed', detail: 'Hooks installed' });
|
|
381
|
+
} else {
|
|
382
|
+
summary.push({ label: 'Claude', status: 'skipped', detail: 'Config not found' });
|
|
177
383
|
}
|
|
178
384
|
|
|
179
|
-
const
|
|
180
|
-
const geminiConfigExists = await isDir(geminiConfigDir);
|
|
181
|
-
const geminiSettingsPath = resolveGeminiSettingsPath({ configDir: geminiConfigDir });
|
|
182
|
-
let geminiResult = null;
|
|
385
|
+
const geminiConfigExists = await isDir(context.geminiConfigDir);
|
|
183
386
|
if (geminiConfigExists) {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
hookCommand: geminiHookCommand
|
|
387
|
+
await upsertGeminiHook({
|
|
388
|
+
settingsPath: context.geminiSettingsPath,
|
|
389
|
+
hookCommand: context.geminiHookCommand
|
|
188
390
|
});
|
|
391
|
+
summary.push({ label: 'Gemini', status: 'installed', detail: 'Hooks installed' });
|
|
392
|
+
} else {
|
|
393
|
+
summary.push({ label: 'Gemini', status: 'skipped', detail: 'Config not found' });
|
|
189
394
|
}
|
|
190
395
|
|
|
191
|
-
const opencodeConfigDir = resolveOpencodeConfigDir({ home, env: process.env });
|
|
192
396
|
const opencodeResult = await upsertOpencodePlugin({
|
|
193
|
-
configDir: opencodeConfigDir,
|
|
397
|
+
configDir: context.opencodeConfigDir,
|
|
194
398
|
notifyPath
|
|
195
399
|
});
|
|
400
|
+
if (opencodeResult?.skippedReason === 'config-missing') {
|
|
401
|
+
summary.push({ label: 'Opencode Plugin', status: 'skipped', detail: 'Config not found' });
|
|
402
|
+
} else {
|
|
403
|
+
summary.push({ label: 'Opencode Plugin', status: opencodeResult?.changed ? 'installed' : 'set', detail: 'Plugin installed' });
|
|
404
|
+
}
|
|
196
405
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
? `- Every Code config: ${codeConfigPath}`
|
|
213
|
-
: `- Every Code notify: skipped (${renderProbeSkip(codeConfigPath, codeProbe)})`,
|
|
214
|
-
codeConfigExists && codeResult
|
|
215
|
-
? codeResult.changed
|
|
216
|
-
? '- Every Code notify: updated'
|
|
217
|
-
: '- Every Code notify: already set'
|
|
218
|
-
: null,
|
|
219
|
-
codeConfigExists
|
|
220
|
-
? codeChained
|
|
221
|
-
? '- Every Code notify: chained (original preserved)'
|
|
222
|
-
: '- Every Code notify: no original'
|
|
223
|
-
: null,
|
|
224
|
-
claudeDirExists
|
|
225
|
-
? claudeResult?.changed
|
|
226
|
-
? `- Claude hooks: updated (${claudeSettingsPath})`
|
|
227
|
-
: `- Claude hooks: already set (${claudeSettingsPath})`
|
|
228
|
-
: '- Claude hooks: skipped (~/.claude not found)',
|
|
229
|
-
geminiConfigExists
|
|
230
|
-
? geminiResult?.changed
|
|
231
|
-
? `- Gemini hooks: updated (${geminiSettingsPath})`
|
|
232
|
-
: `- Gemini hooks: already set (${geminiSettingsPath})`
|
|
233
|
-
: `- Gemini hooks: skipped (${geminiConfigDir} not found)`,
|
|
234
|
-
opencodeResult?.skippedReason === 'config-missing'
|
|
235
|
-
? '- Opencode plugin: skipped (config dir missing)'
|
|
236
|
-
: opencodeResult?.changed
|
|
237
|
-
? `- Opencode plugin: updated (${opencodeConfigDir})`
|
|
238
|
-
: `- Opencode plugin: already set (${opencodeConfigDir})`,
|
|
239
|
-
deviceToken ? `- Device token: stored (${maskSecret(deviceToken)})` : '- Device token: not configured (set VIBESCORE_DEVICE_TOKEN and re-run init)',
|
|
240
|
-
''
|
|
241
|
-
].join('\n')
|
|
242
|
-
);
|
|
406
|
+
const codeProbe = await probeFile(context.codeConfigPath);
|
|
407
|
+
if (codeProbe.exists) {
|
|
408
|
+
const result = await upsertEveryCodeNotify({
|
|
409
|
+
codeConfigPath: context.codeConfigPath,
|
|
410
|
+
notifyCmd: context.codeNotifyCmd,
|
|
411
|
+
notifyOriginalPath: context.codeNotifyOriginalPath
|
|
412
|
+
});
|
|
413
|
+
summary.push({
|
|
414
|
+
label: 'Every Code',
|
|
415
|
+
status: result.changed ? 'updated' : 'set',
|
|
416
|
+
detail: result.changed ? 'Updated config' : 'Config already set'
|
|
417
|
+
});
|
|
418
|
+
} else {
|
|
419
|
+
summary.push({ label: 'Every Code', status: 'skipped', detail: renderSkipDetail(codeProbe) });
|
|
420
|
+
}
|
|
243
421
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
422
|
+
return summary;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
async function previewIntegrations({ context }) {
|
|
426
|
+
const summary = [];
|
|
427
|
+
|
|
428
|
+
const codexProbe = await probeFile(context.codexConfigPath);
|
|
429
|
+
if (codexProbe.exists) {
|
|
430
|
+
const existing = await readCodexNotify(context.codexConfigPath);
|
|
431
|
+
const matches = arraysEqual(existing, context.notifyCmd);
|
|
432
|
+
summary.push({
|
|
433
|
+
label: 'Codex CLI',
|
|
434
|
+
status: matches ? 'set' : 'updated',
|
|
435
|
+
detail: matches ? 'Already configured' : 'Will update config'
|
|
436
|
+
});
|
|
437
|
+
} else {
|
|
438
|
+
summary.push({ label: 'Codex CLI', status: 'skipped', detail: renderSkipDetail(codexProbe) });
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const claudeDirExists = await isDir(context.claudeDir);
|
|
442
|
+
if (claudeDirExists) {
|
|
443
|
+
const configured = await isClaudeHookConfigured({
|
|
444
|
+
settingsPath: context.claudeSettingsPath,
|
|
445
|
+
hookCommand: context.claudeHookCommand
|
|
446
|
+
});
|
|
447
|
+
summary.push({
|
|
448
|
+
label: 'Claude',
|
|
449
|
+
status: 'installed',
|
|
450
|
+
detail: configured ? 'Hooks already installed' : 'Will install hooks'
|
|
451
|
+
});
|
|
452
|
+
} else {
|
|
453
|
+
summary.push({ label: 'Claude', status: 'skipped', detail: 'Config not found' });
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const geminiConfigExists = await isDir(context.geminiConfigDir);
|
|
457
|
+
if (geminiConfigExists) {
|
|
458
|
+
const configured = await isGeminiHookConfigured({
|
|
459
|
+
settingsPath: context.geminiSettingsPath,
|
|
460
|
+
hookCommand: context.geminiHookCommand
|
|
461
|
+
});
|
|
462
|
+
summary.push({
|
|
463
|
+
label: 'Gemini',
|
|
464
|
+
status: 'installed',
|
|
465
|
+
detail: configured ? 'Hooks already installed' : 'Will install hooks'
|
|
466
|
+
});
|
|
467
|
+
} else {
|
|
468
|
+
summary.push({ label: 'Gemini', status: 'skipped', detail: 'Config not found' });
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const opencodeDirExists = await isDir(context.opencodeConfigDir);
|
|
472
|
+
const installed = await isOpencodePluginInstalled({ configDir: context.opencodeConfigDir });
|
|
473
|
+
const opencodeDetail = installed
|
|
474
|
+
? 'Plugin already installed'
|
|
475
|
+
: opencodeDirExists
|
|
476
|
+
? 'Will install plugin'
|
|
477
|
+
: 'Will create config and install plugin';
|
|
478
|
+
summary.push({
|
|
479
|
+
label: 'Opencode Plugin',
|
|
480
|
+
status: 'installed',
|
|
481
|
+
detail: opencodeDetail
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
const codeProbe = await probeFile(context.codeConfigPath);
|
|
485
|
+
if (codeProbe.exists) {
|
|
486
|
+
const existing = await readEveryCodeNotify(context.codeConfigPath);
|
|
487
|
+
const matches = arraysEqual(existing, context.codeNotifyCmd);
|
|
488
|
+
summary.push({
|
|
489
|
+
label: 'Every Code',
|
|
490
|
+
status: matches ? 'set' : 'updated',
|
|
491
|
+
detail: matches ? 'Already configured' : 'Will update config'
|
|
492
|
+
});
|
|
493
|
+
} else {
|
|
494
|
+
summary.push({ label: 'Every Code', status: 'skipped', detail: renderSkipDetail(codeProbe) });
|
|
249
495
|
}
|
|
496
|
+
|
|
497
|
+
return summary;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function renderSkipDetail(probe) {
|
|
501
|
+
if (!probe || probe.reason === 'missing') return 'Config not found';
|
|
502
|
+
if (probe.reason === 'permission-denied') return 'Permission denied';
|
|
503
|
+
if (probe.reason === 'not-file') return 'Invalid config';
|
|
504
|
+
return 'Unavailable';
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function arraysEqual(a, b) {
|
|
508
|
+
if (!Array.isArray(a) || !Array.isArray(b)) return false;
|
|
509
|
+
if (a.length !== b.length) return false;
|
|
510
|
+
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
511
|
+
return true;
|
|
250
512
|
}
|
|
251
513
|
|
|
252
514
|
function parseArgs(argv) {
|
|
@@ -258,7 +520,9 @@ function parseArgs(argv) {
|
|
|
258
520
|
deviceName: null,
|
|
259
521
|
linkCode: null,
|
|
260
522
|
noAuth: false,
|
|
261
|
-
noOpen: false
|
|
523
|
+
noOpen: false,
|
|
524
|
+
yes: false,
|
|
525
|
+
dryRun: false
|
|
262
526
|
};
|
|
263
527
|
|
|
264
528
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -271,14 +535,16 @@ function parseArgs(argv) {
|
|
|
271
535
|
else if (a === '--link-code') out.linkCode = argv[++i] || null;
|
|
272
536
|
else if (a === '--no-auth') out.noAuth = true;
|
|
273
537
|
else if (a === '--no-open') out.noOpen = true;
|
|
538
|
+
else if (a === '--yes') out.yes = true;
|
|
539
|
+
else if (a === '--dry-run') out.dryRun = true;
|
|
274
540
|
else throw new Error(`Unknown option: ${a}`);
|
|
275
541
|
}
|
|
276
542
|
return out;
|
|
277
543
|
}
|
|
278
544
|
|
|
279
|
-
function
|
|
280
|
-
if (
|
|
281
|
-
return
|
|
545
|
+
function sleep(ms) {
|
|
546
|
+
if (!ms || ms <= 0) return Promise.resolve();
|
|
547
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
282
548
|
}
|
|
283
549
|
|
|
284
550
|
function normalizePlatform(value) {
|
|
@@ -326,7 +592,8 @@ const signalPath = ${JSON.stringify(queueSignalPath)};
|
|
|
326
592
|
const codexOriginalPath = ${JSON.stringify(originalPath)};
|
|
327
593
|
const codeOriginalPath = ${JSON.stringify(path.join(trackerDir, 'code_notify_original.json'))};
|
|
328
594
|
const trackerBinPath = ${JSON.stringify(trackerBinPath)};
|
|
329
|
-
const depsMarkerPath = path.join(trackerDir, 'app', 'node_modules', '@insforge', 'sdk', 'package.json');
|
|
595
|
+
const depsMarkerPath = path.join(trackerDir, 'app', 'node_modules', '@insforge', 'sdk', 'package.json');
|
|
596
|
+
const configPath = path.join(trackerDir, 'config.json');
|
|
330
597
|
const fallbackPkg = ${JSON.stringify(fallbackPkg)};
|
|
331
598
|
const selfPath = path.resolve(__filename);
|
|
332
599
|
const home = os.homedir();
|
|
@@ -338,11 +605,19 @@ try {
|
|
|
338
605
|
|
|
339
606
|
// Throttle spawn: at most once per 20 seconds.
|
|
340
607
|
try {
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
608
|
+
const throttlePath = path.join(trackerDir, 'sync.throttle');
|
|
609
|
+
let deviceToken = process.env.VIBESCORE_DEVICE_TOKEN || null;
|
|
610
|
+
if (!deviceToken) {
|
|
611
|
+
try {
|
|
612
|
+
const cfg = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
613
|
+
if (cfg && typeof cfg.deviceToken === 'string') deviceToken = cfg.deviceToken;
|
|
614
|
+
} catch (_) {}
|
|
615
|
+
}
|
|
616
|
+
const canSync = Boolean(deviceToken && deviceToken.length > 0);
|
|
617
|
+
const now = Date.now();
|
|
618
|
+
let last = 0;
|
|
619
|
+
try { last = Number(fs.readFileSync(throttlePath, 'utf8')) || 0; } catch (_) {}
|
|
620
|
+
if (canSync && now - last > 20_000) {
|
|
346
621
|
try { fs.writeFileSync(throttlePath, String(now), 'utf8'); } catch (_) {}
|
|
347
622
|
const hasLocalRuntime = fs.existsSync(trackerBinPath);
|
|
348
623
|
const hasLocalDeps = fs.existsSync(depsMarkerPath);
|
|
@@ -435,15 +710,6 @@ async function checkUrlReachable(url) {
|
|
|
435
710
|
}
|
|
436
711
|
}
|
|
437
712
|
|
|
438
|
-
async function isFile(p) {
|
|
439
|
-
try {
|
|
440
|
-
const st = await fs.stat(p);
|
|
441
|
-
return st.isFile();
|
|
442
|
-
} catch (_e) {
|
|
443
|
-
return false;
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
|
|
447
713
|
async function probeFile(p) {
|
|
448
714
|
try {
|
|
449
715
|
const st = await fs.stat(p);
|
|
@@ -456,14 +722,6 @@ async function probeFile(p) {
|
|
|
456
722
|
}
|
|
457
723
|
}
|
|
458
724
|
|
|
459
|
-
function renderProbeSkip(pathname, probe) {
|
|
460
|
-
if (!probe || probe.reason === 'missing') return `${pathname} not found`;
|
|
461
|
-
if (probe.reason === 'not-file') return `${pathname} is not a file`;
|
|
462
|
-
if (probe.reason === 'permission-denied') return `permission denied: ${pathname}`;
|
|
463
|
-
const code = probe.code ? ` (${probe.code})` : '';
|
|
464
|
-
return `unavailable: ${pathname}${code}`;
|
|
465
|
-
}
|
|
466
|
-
|
|
467
725
|
async function isDir(p) {
|
|
468
726
|
try {
|
|
469
727
|
const st = await fs.stat(p);
|
|
@@ -508,7 +766,9 @@ function spawnInitSync({ trackerBinPath, packageName }) {
|
|
|
508
766
|
});
|
|
509
767
|
child.on('error', (err) => {
|
|
510
768
|
const msg = err && err.message ? err.message : 'unknown error';
|
|
511
|
-
process.
|
|
769
|
+
const detail = process.env.VIBESCORE_DEBUG === '1' ? ` (${msg})` : '';
|
|
770
|
+
process.stderr.write(`Minor issue: Background sync could not start${detail}.\n`);
|
|
771
|
+
process.stderr.write('Run: npx --yes @vibescore/tracker sync\n');
|
|
512
772
|
});
|
|
513
773
|
child.unref();
|
|
514
774
|
}
|
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
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { formatSummaryLine, renderBox, underline } = require('./cli-ui');
|
|
4
|
+
|
|
5
|
+
const DIVIDER = '----------------------------------------------';
|
|
6
|
+
|
|
7
|
+
function renderLocalReport({ summary, isDryRun }) {
|
|
8
|
+
const header = isDryRun
|
|
9
|
+
? 'Dry run complete. Preview only; no changes were applied.'
|
|
10
|
+
: 'Local configuration complete.';
|
|
11
|
+
const lines = [header, '', 'Integration Status:'];
|
|
12
|
+
for (const item of summary || []) lines.push(formatSummaryLine(item));
|
|
13
|
+
process.stdout.write(`${lines.join('\n')}\n`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function renderAuthTransition({ authUrl, canAutoOpen }) {
|
|
17
|
+
const lines = ['', DIVIDER, '', 'Next: Registering device...'];
|
|
18
|
+
if (canAutoOpen) {
|
|
19
|
+
lines.push('Opening your browser to link account...');
|
|
20
|
+
if (authUrl) lines.push(`If it does not open, visit: ${underline(authUrl)}`);
|
|
21
|
+
} else {
|
|
22
|
+
lines.push('Open the link below to sign in.');
|
|
23
|
+
if (authUrl) lines.push(`Visit: ${underline(authUrl)}`);
|
|
24
|
+
}
|
|
25
|
+
lines.push('');
|
|
26
|
+
process.stdout.write(`${lines.join('\n')}\n`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function renderSuccessBox({ configPath, dashboardUrl }) {
|
|
30
|
+
const identityLine = 'Account linked.';
|
|
31
|
+
const lines = [
|
|
32
|
+
'You are all set!',
|
|
33
|
+
'',
|
|
34
|
+
identityLine,
|
|
35
|
+
`Token saved to: ${configPath}`,
|
|
36
|
+
''
|
|
37
|
+
];
|
|
38
|
+
if (dashboardUrl) lines.push(`View your stats at: ${dashboardUrl}`);
|
|
39
|
+
lines.push('You can close this terminal window.');
|
|
40
|
+
process.stdout.write(`${renderBox(lines)}\n`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = {
|
|
44
|
+
DIVIDER,
|
|
45
|
+
renderLocalReport,
|
|
46
|
+
renderAuthTransition,
|
|
47
|
+
renderSuccessBox
|
|
48
|
+
};
|