vibeusage 0.2.21 → 0.2.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +306 -173
- package/README.old.md +324 -0
- package/README.zh-CN.md +304 -188
- package/package.json +32 -32
- package/src/cli.js +37 -37
- package/src/commands/activate-if-needed.js +41 -0
- package/src/commands/diagnostics.js +8 -9
- package/src/commands/doctor.js +31 -26
- package/src/commands/init.js +285 -218
- package/src/commands/status.js +86 -83
- package/src/commands/sync.js +178 -130
- package/src/commands/uninstall.js +66 -62
- package/src/lib/activation-check.js +341 -0
- package/src/lib/browser-auth.js +52 -54
- package/src/lib/claude-config.js +25 -25
- package/src/lib/cli-ui.js +35 -35
- package/src/lib/codex-config.js +40 -36
- package/src/lib/debug-flags.js +2 -2
- package/src/lib/diagnostics.js +70 -57
- package/src/lib/doctor.js +139 -132
- package/src/lib/fs.js +17 -17
- package/src/lib/gemini-config.js +44 -40
- package/src/lib/init-flow.js +16 -22
- package/src/lib/insforge-client.js +10 -10
- package/src/lib/insforge.js +9 -3
- package/src/lib/openclaw-hook.js +89 -66
- package/src/lib/openclaw-session-plugin.js +116 -92
- package/src/lib/opencode-config.js +31 -32
- package/src/lib/opencode-usage-audit.js +34 -31
- package/src/lib/progress.js +12 -13
- package/src/lib/project-usage-purge.js +23 -17
- package/src/lib/prompt.js +8 -4
- package/src/lib/rollout.js +342 -241
- package/src/lib/runtime-config.js +34 -22
- package/src/lib/subscriptions.js +94 -92
- package/src/lib/tracker-paths.js +6 -6
- package/src/lib/upload-throttle.js +35 -16
- package/src/lib/uploader.js +33 -29
- package/src/lib/vibeusage-api.js +72 -56
- package/src/lib/vibeusage-public-repo.js +41 -24
package/src/lib/uploader.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
const fs = require(
|
|
2
|
-
const fssync = require(
|
|
3
|
-
const readline = require(
|
|
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(
|
|
6
|
-
const { ingestHourly } = require(
|
|
5
|
+
const { ensureDir, readJson, writeJson } = require("./fs");
|
|
6
|
+
const { ingestHourly } = require("./vibeusage-api");
|
|
7
7
|
|
|
8
|
-
const DEFAULT_SOURCE =
|
|
9
|
-
const DEFAULT_MODEL =
|
|
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(
|
|
26
|
-
const projectQueueEnabled = typeof projectQueuePath ===
|
|
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(
|
|
28
|
+
throw new Error("projectQueueStatePath is required when projectQueuePath is set");
|
|
29
29
|
}
|
|
30
30
|
if (projectQueueEnabled) {
|
|
31
|
-
await ensureDir(require(
|
|
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
|
|
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 ===
|
|
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 ===
|
|
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
|
|
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:
|
|
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,
|
|
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 ===
|
|
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:
|
|
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,
|
|
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 ===
|
|
174
|
-
const projectKey = typeof bucket?.project_key ===
|
|
175
|
-
const projectRef = typeof bucket?.project_ref ===
|
|
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 !==
|
|
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 !==
|
|
219
|
+
if (typeof value !== "string") return null;
|
|
216
220
|
const trimmed = value.trim();
|
|
217
221
|
return trimmed.length > 0 ? trimmed : null;
|
|
218
222
|
}
|
package/src/lib/vibeusage-api.js
CHANGED
|
@@ -1,68 +1,75 @@
|
|
|
1
|
-
|
|
1
|
+
"use strict";
|
|
2
2
|
|
|
3
|
-
const { createInsforgeClient } = require(
|
|
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,
|
|
8
|
+
if (error) throw normalizeSdkError(error, "Sign-in failed");
|
|
9
9
|
|
|
10
10
|
const accessToken = data?.accessToken;
|
|
11
|
-
if (typeof accessToken !==
|
|
12
|
-
throw new Error(
|
|
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 =
|
|
18
|
+
async function issueDeviceToken({ baseUrl, accessToken, deviceName, platform = "macos" }) {
|
|
19
19
|
const data = await invokeFunction({
|
|
20
20
|
baseUrl,
|
|
21
21
|
accessToken,
|
|
22
|
-
slug:
|
|
23
|
-
method:
|
|
22
|
+
slug: "vibeusage-device-token-issue",
|
|
23
|
+
method: "POST",
|
|
24
24
|
body: { device_name: deviceName, platform },
|
|
25
|
-
errorPrefix:
|
|
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 !==
|
|
31
|
-
|
|
32
|
-
|
|
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 =
|
|
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:
|
|
43
|
-
method:
|
|
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:
|
|
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 !==
|
|
56
|
-
throw new Error(
|
|
56
|
+
if (typeof token !== "string" || token.length === 0) {
|
|
57
|
+
throw new Error("Link code exchange failed: missing token");
|
|
57
58
|
}
|
|
58
|
-
if (typeof deviceId !==
|
|
59
|
-
throw new Error(
|
|
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({
|
|
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:
|
|
75
|
-
method:
|
|
81
|
+
slug: "vibeusage-ingest",
|
|
82
|
+
method: "POST",
|
|
76
83
|
body,
|
|
77
|
-
errorPrefix:
|
|
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:
|
|
92
|
-
method:
|
|
98
|
+
slug: "vibeusage-sync-ping",
|
|
99
|
+
method: "POST",
|
|
93
100
|
body: {},
|
|
94
|
-
errorPrefix:
|
|
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 ===
|
|
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({
|
|
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 ===
|
|
141
|
-
if (typeof 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
|
|
151
|
-
const message = typeof error.message ===
|
|
152
|
-
const code = typeof error.error ===
|
|
153
|
-
if (message && message !==
|
|
154
|
-
if (code && 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 ||
|
|
162
|
-
return
|
|
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 ||
|
|
181
|
+
const s = String(message || "").toLowerCase();
|
|
167
182
|
if (!s) return false;
|
|
168
|
-
if (s.includes(
|
|
169
|
-
if (s.includes(
|
|
170
|
-
if (s.includes(
|
|
171
|
-
if (s.includes(
|
|
172
|
-
if (s.includes(
|
|
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 ||
|
|
196
|
+
const s = String(message || "").toLowerCase();
|
|
182
197
|
if (!s) return false;
|
|
183
198
|
if (isBackendRuntimeDownMessage(s)) return true;
|
|
184
|
-
if (s.includes(
|
|
185
|
-
if (s.includes(
|
|
186
|
-
if (s.includes(
|
|
187
|
-
if (s.includes(
|
|
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 =
|
|
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 ===
|
|
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(
|
|
1
|
+
const crypto = require("node:crypto");
|
|
2
2
|
|
|
3
|
-
const GITHUB_HOST =
|
|
4
|
-
const GITHUB_API_BASE =
|
|
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 !==
|
|
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(
|
|
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]
|
|
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(
|
|
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?.(
|
|
30
|
-
if (remaining ===
|
|
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 (
|
|
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:
|
|
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:
|
|
52
|
-
|
|
53
|
-
}
|
|
58
|
+
Accept: "application/vnd.github+json",
|
|
59
|
+
"User-Agent": "vibeusage-cli",
|
|
60
|
+
},
|
|
54
61
|
});
|
|
55
62
|
} catch (_err) {
|
|
56
|
-
return {
|
|
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 ===
|
|
62
|
-
const visibility = typeof body?.visibility ===
|
|
63
|
-
const isPublic = isPrivate === false || visibility ===
|
|
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 ?
|
|
77
|
+
status: isPublic ? "public_verified" : "blocked",
|
|
66
78
|
projectKey: parsed.repoId,
|
|
67
79
|
projectRef,
|
|
68
|
-
reason: isPublic ?
|
|
80
|
+
reason: isPublic ? "public" : "private",
|
|
69
81
|
};
|
|
70
82
|
}
|
|
71
83
|
|
|
72
84
|
if (res.status === 404) {
|
|
73
|
-
return { status:
|
|
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 {
|
|
90
|
+
return {
|
|
91
|
+
status: "pending_public",
|
|
92
|
+
projectKey: parsed.repoId,
|
|
93
|
+
projectRef,
|
|
94
|
+
reason: "rate_limited",
|
|
95
|
+
};
|
|
79
96
|
}
|
|
80
97
|
|
|
81
|
-
return { status:
|
|
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
|
};
|