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.
Files changed (40) hide show
  1. package/README.md +306 -173
  2. package/README.old.md +324 -0
  3. package/README.zh-CN.md +304 -188
  4. package/package.json +32 -32
  5. package/src/cli.js +41 -37
  6. package/src/commands/activate-if-needed.js +41 -0
  7. package/src/commands/diagnostics.js +8 -9
  8. package/src/commands/doctor.js +31 -26
  9. package/src/commands/init.js +285 -218
  10. package/src/commands/status.js +86 -83
  11. package/src/commands/sync.js +182 -130
  12. package/src/commands/uninstall.js +66 -62
  13. package/src/lib/activation-check.js +290 -0
  14. package/src/lib/browser-auth.js +52 -54
  15. package/src/lib/claude-config.js +25 -25
  16. package/src/lib/cli-ui.js +35 -35
  17. package/src/lib/codex-config.js +40 -36
  18. package/src/lib/debug-flags.js +2 -2
  19. package/src/lib/diagnostics.js +70 -57
  20. package/src/lib/doctor.js +139 -132
  21. package/src/lib/fs.js +17 -17
  22. package/src/lib/gemini-config.js +44 -40
  23. package/src/lib/init-flow.js +16 -22
  24. package/src/lib/insforge-client.js +10 -10
  25. package/src/lib/insforge.js +9 -3
  26. package/src/lib/openclaw-hook.js +89 -66
  27. package/src/lib/openclaw-session-plugin.js +116 -92
  28. package/src/lib/opencode-config.js +31 -32
  29. package/src/lib/opencode-usage-audit.js +34 -31
  30. package/src/lib/progress.js +12 -13
  31. package/src/lib/project-usage-purge.js +23 -17
  32. package/src/lib/prompt.js +8 -4
  33. package/src/lib/rollout.js +342 -241
  34. package/src/lib/runtime-config.js +34 -22
  35. package/src/lib/subscriptions.js +94 -92
  36. package/src/lib/tracker-paths.js +6 -6
  37. package/src/lib/upload-throttle.js +35 -16
  38. package/src/lib/uploader.js +33 -29
  39. package/src/lib/vibeusage-api.js +72 -56
  40. package/src/lib/vibeusage-public-repo.js +41 -24
@@ -1,13 +1,13 @@
1
- const fs = require('node:fs/promises');
2
- const fssync = require('node:fs');
3
- const readline = require('node:readline');
1
+ const fs = require("node:fs/promises");
2
+ const fssync = require("node:fs");
3
+ const readline = require("node:readline");
4
4
 
5
- const { ensureDir, readJson, writeJson } = require('./fs');
6
- const { ingestHourly } = require('./vibeusage-api');
5
+ const { ensureDir, readJson, writeJson } = require("./fs");
6
+ const { ingestHourly } = require("./vibeusage-api");
7
7
 
8
- const DEFAULT_SOURCE = 'codex';
9
- const DEFAULT_MODEL = 'unknown';
10
- const BUCKET_SEPARATOR = '|';
8
+ const DEFAULT_SOURCE = "codex";
9
+ const DEFAULT_MODEL = "unknown";
10
+ const BUCKET_SEPARATOR = "|";
11
11
  const MAX_INGEST_BUCKETS = 500;
12
12
 
13
13
  async function drainQueueToCloud({
@@ -20,34 +20,36 @@ async function drainQueueToCloud({
20
20
  projectQueueStatePath,
21
21
  maxBatches,
22
22
  batchSize,
23
- onProgress
23
+ onProgress,
24
24
  }) {
25
- await ensureDir(require('node:path').dirname(queueStatePath));
26
- const projectQueueEnabled = typeof projectQueuePath === 'string' && projectQueuePath.length > 0;
25
+ await ensureDir(require("node:path").dirname(queueStatePath));
26
+ const projectQueueEnabled = typeof projectQueuePath === "string" && projectQueuePath.length > 0;
27
27
  if (projectQueueEnabled && (!projectQueueStatePath || projectQueueStatePath.length === 0)) {
28
- throw new Error('projectQueueStatePath is required when projectQueuePath is set');
28
+ throw new Error("projectQueueStatePath is required when projectQueuePath is set");
29
29
  }
30
30
  if (projectQueueEnabled) {
31
- await ensureDir(require('node:path').dirname(projectQueueStatePath));
31
+ await ensureDir(require("node:path").dirname(projectQueueStatePath));
32
32
  }
33
33
 
34
34
  const state = (await readJson(queueStatePath)) || { offset: 0 };
35
35
  let offset = Number(state.offset || 0);
36
36
  const projectStatePath = projectQueueEnabled ? projectQueueStatePath : null;
37
- const projectState = projectQueueEnabled ? (await readJson(projectStatePath)) || { offset: 0 } : { offset: 0 };
37
+ const projectState = projectQueueEnabled
38
+ ? (await readJson(projectStatePath)) || { offset: 0 }
39
+ : { offset: 0 };
38
40
  let projectOffset = Number(projectState.offset || 0);
39
41
  let inserted = 0;
40
42
  let skipped = 0;
41
43
  let attempted = 0;
42
44
 
43
- const cb = typeof onProgress === 'function' ? onProgress : null;
45
+ const cb = typeof onProgress === "function" ? onProgress : null;
44
46
  const queueSize = await safeFileSize(queuePath);
45
47
  const projectQueueSize = projectQueueEnabled ? await safeFileSize(projectQueuePath) : 0;
46
48
  const maxBuckets = Math.max(1, Math.floor(Number(batchSize || 200)));
47
49
  const totalLimit = Math.min(maxBuckets, MAX_INGEST_BUCKETS);
48
50
 
49
51
  const normalizedSubscriptions = Array.isArray(deviceSubscriptions)
50
- ? deviceSubscriptions.filter((entry) => entry && typeof entry === 'object')
52
+ ? deviceSubscriptions.filter((entry) => entry && typeof entry === "object")
51
53
  : [];
52
54
 
53
55
  for (let batch = 0; batch < maxBatches; batch++) {
@@ -66,7 +68,9 @@ async function drainQueueToCloud({
66
68
  }
67
69
 
68
70
  const res =
69
- hourlyLimit > 0 ? await readBatch(queuePath, offset, hourlyLimit) : { buckets: [], nextOffset: offset };
71
+ hourlyLimit > 0
72
+ ? await readBatch(queuePath, offset, hourlyLimit)
73
+ : { buckets: [], nextOffset: offset };
70
74
  const projectRes =
71
75
  projectLimit > 0
72
76
  ? await readProjectBatch(projectQueuePath, projectOffset, projectLimit)
@@ -79,7 +83,7 @@ async function drainQueueToCloud({
79
83
  deviceToken,
80
84
  hourly: res.buckets,
81
85
  project_hourly: projectRes.buckets,
82
- device_subscriptions: batch === 0 ? normalizedSubscriptions : undefined
86
+ device_subscriptions: batch === 0 ? normalizedSubscriptions : undefined,
83
87
  });
84
88
  inserted += ingest.inserted || 0;
85
89
  skipped += ingest.skipped || 0;
@@ -103,7 +107,7 @@ async function drainQueueToCloud({
103
107
  offset: combinedOffset,
104
108
  queueSize: queueSize + projectQueueSize,
105
109
  inserted,
106
- skipped
110
+ skipped,
107
111
  });
108
112
  }
109
113
  }
@@ -116,14 +120,14 @@ async function readBatch(queuePath, startOffset, maxBuckets) {
116
120
  if (!st || !st.isFile()) return { buckets: [], nextOffset: startOffset };
117
121
  if (startOffset >= st.size) return { buckets: [], nextOffset: startOffset };
118
122
 
119
- const stream = fssync.createReadStream(queuePath, { encoding: 'utf8', start: startOffset });
123
+ const stream = fssync.createReadStream(queuePath, { encoding: "utf8", start: startOffset });
120
124
  const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
121
125
 
122
126
  const bucketMap = new Map();
123
127
  let offset = startOffset;
124
128
  let linesRead = 0;
125
129
  for await (const line of rl) {
126
- const bytes = Buffer.byteLength(line, 'utf8') + 1;
130
+ const bytes = Buffer.byteLength(line, "utf8") + 1;
127
131
  offset += bytes;
128
132
  if (!line.trim()) continue;
129
133
  let bucket;
@@ -132,7 +136,7 @@ async function readBatch(queuePath, startOffset, maxBuckets) {
132
136
  } catch (_e) {
133
137
  continue;
134
138
  }
135
- const hourStart = typeof bucket?.hour_start === 'string' ? bucket.hour_start : null;
139
+ const hourStart = typeof bucket?.hour_start === "string" ? bucket.hour_start : null;
136
140
  if (!hourStart) continue;
137
141
  const source = normalizeSource(bucket?.source) || DEFAULT_SOURCE;
138
142
  const model = normalizeModel(bucket?.model) || DEFAULT_MODEL;
@@ -154,14 +158,14 @@ async function readProjectBatch(queuePath, startOffset, maxBuckets) {
154
158
  if (!st || !st.isFile()) return { buckets: [], nextOffset: startOffset };
155
159
  if (startOffset >= st.size) return { buckets: [], nextOffset: startOffset };
156
160
 
157
- const stream = fssync.createReadStream(queuePath, { encoding: 'utf8', start: startOffset });
161
+ const stream = fssync.createReadStream(queuePath, { encoding: "utf8", start: startOffset });
158
162
  const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
159
163
 
160
164
  const bucketMap = new Map();
161
165
  let offset = startOffset;
162
166
  let linesRead = 0;
163
167
  for await (const line of rl) {
164
- const bytes = Buffer.byteLength(line, 'utf8') + 1;
168
+ const bytes = Buffer.byteLength(line, "utf8") + 1;
165
169
  offset += bytes;
166
170
  if (!line.trim()) continue;
167
171
  let bucket;
@@ -170,9 +174,9 @@ async function readProjectBatch(queuePath, startOffset, maxBuckets) {
170
174
  } catch (_e) {
171
175
  continue;
172
176
  }
173
- const hourStart = typeof bucket?.hour_start === 'string' ? bucket.hour_start : null;
174
- const projectKey = typeof bucket?.project_key === 'string' ? bucket.project_key : null;
175
- const projectRef = typeof bucket?.project_ref === 'string' ? bucket.project_ref : null;
177
+ const hourStart = typeof bucket?.hour_start === "string" ? bucket.hour_start : null;
178
+ const projectKey = typeof bucket?.project_key === "string" ? bucket.project_key : null;
179
+ const projectRef = typeof bucket?.project_ref === "string" ? bucket.project_ref : null;
176
180
  if (!hourStart || !projectKey || !projectRef) continue;
177
181
  const source = normalizeSource(bucket?.source) || DEFAULT_SOURCE;
178
182
  bucket.source = source;
@@ -206,13 +210,13 @@ function projectBucketKey(projectKey, source, hourStart) {
206
210
  }
207
211
 
208
212
  function normalizeSource(value) {
209
- if (typeof value !== 'string') return null;
213
+ if (typeof value !== "string") return null;
210
214
  const trimmed = value.trim().toLowerCase();
211
215
  return trimmed.length > 0 ? trimmed : null;
212
216
  }
213
217
 
214
218
  function normalizeModel(value) {
215
- if (typeof value !== 'string') return null;
219
+ if (typeof value !== "string") return null;
216
220
  const trimmed = value.trim();
217
221
  return trimmed.length > 0 ? trimmed : null;
218
222
  }
@@ -1,68 +1,75 @@
1
- 'use strict';
1
+ "use strict";
2
2
 
3
- const { createInsforgeClient } = require('./insforge-client');
3
+ const { createInsforgeClient } = require("./insforge-client");
4
4
 
5
5
  async function signInWithPassword({ baseUrl, email, password }) {
6
6
  const client = createInsforgeClient({ baseUrl });
7
7
  const { data, error } = await client.auth.signInWithPassword({ email, password });
8
- if (error) throw normalizeSdkError(error, 'Sign-in failed');
8
+ if (error) throw normalizeSdkError(error, "Sign-in failed");
9
9
 
10
10
  const accessToken = data?.accessToken;
11
- if (typeof accessToken !== 'string' || accessToken.length === 0) {
12
- throw new Error('Sign-in failed: missing accessToken');
11
+ if (typeof accessToken !== "string" || accessToken.length === 0) {
12
+ throw new Error("Sign-in failed: missing accessToken");
13
13
  }
14
14
 
15
15
  return accessToken;
16
16
  }
17
17
 
18
- async function issueDeviceToken({ baseUrl, accessToken, deviceName, platform = 'macos' }) {
18
+ async function issueDeviceToken({ baseUrl, accessToken, deviceName, platform = "macos" }) {
19
19
  const data = await invokeFunction({
20
20
  baseUrl,
21
21
  accessToken,
22
- slug: 'vibeusage-device-token-issue',
23
- method: 'POST',
22
+ slug: "vibeusage-device-token-issue",
23
+ method: "POST",
24
24
  body: { device_name: deviceName, platform },
25
- errorPrefix: 'Device token issue failed'
25
+ errorPrefix: "Device token issue failed",
26
26
  });
27
27
 
28
28
  const token = data?.token;
29
29
  const deviceId = data?.device_id;
30
- if (typeof token !== 'string' || token.length === 0) throw new Error('Device token issue failed: missing token');
31
- if (typeof deviceId !== 'string' || deviceId.length === 0) {
32
- throw new Error('Device token issue failed: missing device_id');
30
+ if (typeof token !== "string" || token.length === 0)
31
+ throw new Error("Device token issue failed: missing token");
32
+ if (typeof deviceId !== "string" || deviceId.length === 0) {
33
+ throw new Error("Device token issue failed: missing device_id");
33
34
  }
34
35
 
35
36
  return { token, deviceId };
36
37
  }
37
38
 
38
- async function exchangeLinkCode({ baseUrl, linkCode, requestId, deviceName, platform = 'macos' }) {
39
+ async function exchangeLinkCode({ baseUrl, linkCode, requestId, deviceName, platform = "macos" }) {
39
40
  const data = await invokeFunction({
40
41
  baseUrl,
41
42
  accessToken: null,
42
- slug: 'vibeusage-link-code-exchange',
43
- method: 'POST',
43
+ slug: "vibeusage-link-code-exchange",
44
+ method: "POST",
44
45
  body: {
45
46
  link_code: linkCode,
46
47
  request_id: requestId,
47
48
  device_name: deviceName,
48
- platform
49
+ platform,
49
50
  },
50
- errorPrefix: 'Link code exchange failed'
51
+ errorPrefix: "Link code exchange failed",
51
52
  });
52
53
 
53
54
  const token = data?.token;
54
55
  const deviceId = data?.device_id;
55
- if (typeof token !== 'string' || token.length === 0) {
56
- throw new Error('Link code exchange failed: missing token');
56
+ if (typeof token !== "string" || token.length === 0) {
57
+ throw new Error("Link code exchange failed: missing token");
57
58
  }
58
- if (typeof deviceId !== 'string' || deviceId.length === 0) {
59
- throw new Error('Link code exchange failed: missing device_id');
59
+ if (typeof deviceId !== "string" || deviceId.length === 0) {
60
+ throw new Error("Link code exchange failed: missing device_id");
60
61
  }
61
62
 
62
63
  return { token, deviceId, userId: data?.user_id || null };
63
64
  }
64
65
 
65
- async function ingestHourly({ baseUrl, deviceToken, hourly, project_hourly, device_subscriptions }) {
66
+ async function ingestHourly({
67
+ baseUrl,
68
+ deviceToken,
69
+ hourly,
70
+ project_hourly,
71
+ device_subscriptions,
72
+ }) {
66
73
  const body = { hourly, project_hourly };
67
74
  if (Array.isArray(device_subscriptions) && device_subscriptions.length > 0) {
68
75
  body.device_subscriptions = device_subscriptions;
@@ -71,16 +78,16 @@ async function ingestHourly({ baseUrl, deviceToken, hourly, project_hourly, devi
71
78
  const data = await invokeFunctionWithRetry({
72
79
  baseUrl,
73
80
  accessToken: deviceToken,
74
- slug: 'vibeusage-ingest',
75
- method: 'POST',
81
+ slug: "vibeusage-ingest",
82
+ method: "POST",
76
83
  body,
77
- errorPrefix: 'Ingest failed',
78
- retry: { maxRetries: 3, baseDelayMs: 500, maxDelayMs: 5000 }
84
+ errorPrefix: "Ingest failed",
85
+ retry: { maxRetries: 3, baseDelayMs: 500, maxDelayMs: 5000 },
79
86
  });
80
87
 
81
88
  return {
82
89
  inserted: Number(data?.inserted || 0),
83
- skipped: Number(data?.skipped || 0)
90
+ skipped: Number(data?.skipped || 0),
84
91
  };
85
92
  }
86
93
 
@@ -88,16 +95,16 @@ async function syncHeartbeat({ baseUrl, deviceToken }) {
88
95
  const data = await invokeFunction({
89
96
  baseUrl,
90
97
  accessToken: deviceToken,
91
- slug: 'vibeusage-sync-ping',
92
- method: 'POST',
98
+ slug: "vibeusage-sync-ping",
99
+ method: "POST",
93
100
  body: {},
94
- errorPrefix: 'Sync heartbeat failed'
101
+ errorPrefix: "Sync heartbeat failed",
95
102
  });
96
103
 
97
104
  return {
98
105
  updated: Boolean(data?.updated),
99
- last_sync_at: typeof data?.last_sync_at === 'string' ? data.last_sync_at : null,
100
- min_interval_minutes: Number(data?.min_interval_minutes || 0)
106
+ last_sync_at: typeof data?.last_sync_at === "string" ? data.last_sync_at : null,
107
+ min_interval_minutes: Number(data?.min_interval_minutes || 0),
101
108
  };
102
109
  }
103
110
 
@@ -106,7 +113,7 @@ module.exports = {
106
113
  issueDeviceToken,
107
114
  exchangeLinkCode,
108
115
  ingestHourly,
109
- syncHeartbeat
116
+ syncHeartbeat,
110
117
  };
111
118
 
112
119
  async function invokeFunction({ baseUrl, accessToken, slug, method, body, errorPrefix }) {
@@ -116,7 +123,15 @@ async function invokeFunction({ baseUrl, accessToken, slug, method, body, errorP
116
123
  return data;
117
124
  }
118
125
 
119
- async function invokeFunctionWithRetry({ baseUrl, accessToken, slug, method, body, errorPrefix, retry }) {
126
+ async function invokeFunctionWithRetry({
127
+ baseUrl,
128
+ accessToken,
129
+ slug,
130
+ method,
131
+ body,
132
+ errorPrefix,
133
+ retry,
134
+ }) {
120
135
  const retryOptions = normalizeRetryOptions(retry);
121
136
  let attempt = 0;
122
137
 
@@ -137,8 +152,8 @@ function normalizeSdkError(error, errorPrefix) {
137
152
  const msg = normalizeBackendErrorMessage(raw);
138
153
  const err = new Error(errorPrefix ? `${errorPrefix}: ${msg}` : msg);
139
154
  const status = error?.statusCode ?? error?.status;
140
- const code = typeof error?.error === 'string' ? error.error.trim() : '';
141
- if (typeof status === 'number') err.status = status;
155
+ const code = typeof error?.error === "string" ? error.error.trim() : "";
156
+ if (typeof status === "number") err.status = status;
142
157
  if (code) err.code = code;
143
158
  err.retryable = isRetryableStatus(status) || isRetryableMessage(raw);
144
159
  if (msg !== raw) err.originalMessage = raw;
@@ -147,29 +162,29 @@ function normalizeSdkError(error, errorPrefix) {
147
162
  }
148
163
 
149
164
  function extractSdkErrorMessage(error) {
150
- if (!error) return 'Unknown error';
151
- const message = typeof error.message === 'string' ? error.message.trim() : '';
152
- const code = typeof error.error === 'string' ? error.error.trim() : '';
153
- if (message && message !== 'InsForgeError') return message;
154
- if (code && code !== 'REQUEST_FAILED') return code;
165
+ if (!error) return "Unknown error";
166
+ const message = typeof error.message === "string" ? error.message.trim() : "";
167
+ const code = typeof error.error === "string" ? error.error.trim() : "";
168
+ if (message && message !== "InsForgeError") return message;
169
+ if (code && code !== "REQUEST_FAILED") return code;
155
170
  if (message) return message;
156
171
  if (code) return code;
157
172
  return String(error);
158
173
  }
159
174
 
160
175
  function normalizeBackendErrorMessage(message) {
161
- if (!isBackendRuntimeDownMessage(message)) return String(message || 'Unknown error');
162
- return 'Backend runtime unavailable (InsForge). Please retry later.';
176
+ if (!isBackendRuntimeDownMessage(message)) return String(message || "Unknown error");
177
+ return "Backend runtime unavailable (InsForge). Please retry later.";
163
178
  }
164
179
 
165
180
  function isBackendRuntimeDownMessage(message) {
166
- const s = String(message || '').toLowerCase();
181
+ const s = String(message || "").toLowerCase();
167
182
  if (!s) return false;
168
- if (s.includes('deno:') || s.includes('deno')) return true;
169
- if (s.includes('econnreset') || s.includes('econnrefused')) return true;
170
- if (s.includes('etimedout')) return true;
171
- if (s.includes('timeout') && s.includes('request')) return true;
172
- if (s.includes('upstream') && (s.includes('deno') || s.includes('connect'))) return true;
183
+ if (s.includes("deno:") || s.includes("deno")) return true;
184
+ if (s.includes("econnreset") || s.includes("econnrefused")) return true;
185
+ if (s.includes("etimedout")) return true;
186
+ if (s.includes("timeout") && s.includes("request")) return true;
187
+ if (s.includes("upstream") && (s.includes("deno") || s.includes("connect"))) return true;
173
188
  return false;
174
189
  }
175
190
 
@@ -178,13 +193,13 @@ function isRetryableStatus(status) {
178
193
  }
179
194
 
180
195
  function isRetryableMessage(message) {
181
- const s = String(message || '').toLowerCase();
196
+ const s = String(message || "").toLowerCase();
182
197
  if (!s) return false;
183
198
  if (isBackendRuntimeDownMessage(s)) return true;
184
- if (s.includes('econnreset') || s.includes('econnrefused')) return true;
185
- if (s.includes('etimedout') || s.includes('timeout')) return true;
186
- if (s.includes('networkerror') || s.includes('failed to fetch')) return true;
187
- if (s.includes('socket hang up') || s.includes('connection reset')) return true;
199
+ if (s.includes("econnreset") || s.includes("econnrefused")) return true;
200
+ if (s.includes("etimedout") || s.includes("timeout")) return true;
201
+ if (s.includes("networkerror") || s.includes("failed to fetch")) return true;
202
+ if (s.includes("socket hang up") || s.includes("connection reset")) return true;
188
203
  return false;
189
204
  }
190
205
 
@@ -195,7 +210,8 @@ function normalizeRetryOptions(retry) {
195
210
  const maxRetries = clampInt(retry.maxRetries, 0, 10);
196
211
  const baseDelayMs = clampInt(retry.baseDelayMs ?? 300, 50, 60_000);
197
212
  const maxDelayMs = clampInt(retry.maxDelayMs ?? baseDelayMs * 4, baseDelayMs, 120_000);
198
- const jitterRatio = typeof retry.jitterRatio === 'number' ? Math.max(0, Math.min(0.5, retry.jitterRatio)) : 0.2;
213
+ const jitterRatio =
214
+ typeof retry.jitterRatio === "number" ? Math.max(0, Math.min(0.5, retry.jitterRatio)) : 0.2;
199
215
  return { maxRetries, baseDelayMs, maxDelayMs, jitterRatio };
200
216
  }
201
217
 
@@ -211,7 +227,7 @@ function computeRetryDelayMs({ retryOptions, attempt, err }) {
211
227
  const capped = Math.min(retryOptions.maxDelayMs, exp);
212
228
  const jitter = capped * retryOptions.jitterRatio * Math.random();
213
229
  const backoff = Math.round(capped + jitter);
214
- const retryAfter = typeof err?.retryAfterMs === 'number' ? err.retryAfterMs : 0;
230
+ const retryAfter = typeof err?.retryAfterMs === "number" ? err.retryAfterMs : 0;
215
231
  return Math.max(backoff, retryAfter || 0);
216
232
  }
217
233
 
@@ -1,10 +1,10 @@
1
- const crypto = require('node:crypto');
1
+ const crypto = require("node:crypto");
2
2
 
3
- const GITHUB_HOST = 'github.com';
4
- const GITHUB_API_BASE = 'https://api.github.com';
3
+ const GITHUB_HOST = "github.com";
4
+ const GITHUB_API_BASE = "https://api.github.com";
5
5
 
6
6
  function parseGitHubRepoId(projectRef) {
7
- if (typeof projectRef !== 'string') return null;
7
+ if (typeof projectRef !== "string") return null;
8
8
  let parsed;
9
9
  try {
10
10
  parsed = new URL(projectRef);
@@ -12,24 +12,31 @@ function parseGitHubRepoId(projectRef) {
12
12
  return null;
13
13
  }
14
14
  if (!parsed.hostname || parsed.hostname.toLowerCase() !== GITHUB_HOST) return null;
15
- const segments = parsed.pathname.split('/').filter(Boolean);
15
+ const segments = parsed.pathname.split("/").filter(Boolean);
16
16
  if (segments.length < 2) return null;
17
17
  const owner = segments[0].trim().toLowerCase();
18
- const repo = segments[1].trim().replace(/\.git$/i, '').toLowerCase();
18
+ const repo = segments[1]
19
+ .trim()
20
+ .replace(/\.git$/i, "")
21
+ .toLowerCase();
19
22
  if (!owner || !repo) return null;
20
23
  return { owner, repo, repoId: `${owner}/${repo}` };
21
24
  }
22
25
 
23
26
  function hashRepoRoot(repoRoot) {
24
- return crypto.createHash('sha256').update(String(repoRoot)).digest('hex');
27
+ return crypto.createHash("sha256").update(String(repoRoot)).digest("hex");
25
28
  }
26
29
 
27
30
  function isRateLimited(res, body) {
28
31
  if (!res) return false;
29
- const remaining = res.headers?.get?.('x-ratelimit-remaining');
30
- if (remaining === '0') return true;
32
+ const remaining = res.headers?.get?.("x-ratelimit-remaining");
33
+ if (remaining === "0") return true;
31
34
  if (res.status === 429) return true;
32
- if (body && typeof body.message === 'string' && body.message.toLowerCase().includes('rate limit')) {
35
+ if (
36
+ body &&
37
+ typeof body.message === "string" &&
38
+ body.message.toLowerCase().includes("rate limit")
39
+ ) {
33
40
  return true;
34
41
  }
35
42
  return false;
@@ -38,7 +45,7 @@ function isRateLimited(res, body) {
38
45
  async function resolveGitHubPublicStatus(projectRef, fetchImpl) {
39
46
  const parsed = parseGitHubRepoId(projectRef);
40
47
  if (!parsed) {
41
- return { status: 'blocked', projectKey: null, projectRef, reason: 'non_github' };
48
+ return { status: "blocked", projectKey: null, projectRef, reason: "non_github" };
42
49
  }
43
50
  const fetchFn = fetchImpl || fetch;
44
51
  const apiUrl = `${GITHUB_API_BASE}/repos/${parsed.repoId}`;
@@ -48,41 +55,51 @@ async function resolveGitHubPublicStatus(projectRef, fetchImpl) {
48
55
  try {
49
56
  res = await fetchFn(apiUrl, {
50
57
  headers: {
51
- Accept: 'application/vnd.github+json',
52
- 'User-Agent': 'vibeusage-cli'
53
- }
58
+ Accept: "application/vnd.github+json",
59
+ "User-Agent": "vibeusage-cli",
60
+ },
54
61
  });
55
62
  } catch (_err) {
56
- return { status: 'pending_public', projectKey: parsed.repoId, projectRef, reason: 'fetch_failed' };
63
+ return {
64
+ status: "pending_public",
65
+ projectKey: parsed.repoId,
66
+ projectRef,
67
+ reason: "fetch_failed",
68
+ };
57
69
  }
58
70
 
59
71
  if (res.status === 200) {
60
72
  body = await res.json().catch(() => ({}));
61
- const isPrivate = body && typeof body.private === 'boolean' ? body.private : null;
62
- const visibility = typeof body?.visibility === 'string' ? body.visibility : null;
63
- const isPublic = isPrivate === false || visibility === 'public';
73
+ const isPrivate = body && typeof body.private === "boolean" ? body.private : null;
74
+ const visibility = typeof body?.visibility === "string" ? body.visibility : null;
75
+ const isPublic = isPrivate === false || visibility === "public";
64
76
  return {
65
- status: isPublic ? 'public_verified' : 'blocked',
77
+ status: isPublic ? "public_verified" : "blocked",
66
78
  projectKey: parsed.repoId,
67
79
  projectRef,
68
- reason: isPublic ? 'public' : 'private'
80
+ reason: isPublic ? "public" : "private",
69
81
  };
70
82
  }
71
83
 
72
84
  if (res.status === 404) {
73
- return { status: 'blocked', projectKey: parsed.repoId, projectRef, reason: 'not_found' };
85
+ return { status: "blocked", projectKey: parsed.repoId, projectRef, reason: "not_found" };
74
86
  }
75
87
 
76
88
  body = await res.json().catch(() => ({}));
77
89
  if (isRateLimited(res, body) || res.status === 401 || res.status === 403 || res.status >= 500) {
78
- return { status: 'pending_public', projectKey: parsed.repoId, projectRef, reason: 'rate_limited' };
90
+ return {
91
+ status: "pending_public",
92
+ projectKey: parsed.repoId,
93
+ projectRef,
94
+ reason: "rate_limited",
95
+ };
79
96
  }
80
97
 
81
- return { status: 'pending_public', projectKey: parsed.repoId, projectRef, reason: 'unknown' };
98
+ return { status: "pending_public", projectKey: parsed.repoId, projectRef, reason: "unknown" };
82
99
  }
83
100
 
84
101
  module.exports = {
85
102
  parseGitHubRepoId,
86
103
  hashRepoRoot,
87
- resolveGitHubPublicStatus
104
+ resolveGitHubPublicStatus,
88
105
  };