vibeusage 0.2.15 → 0.2.16
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 +2 -0
- package/README.zh-CN.md +2 -0
- package/package.json +7 -3
- package/src/commands/init.js +25 -0
- package/src/commands/sync.js +38 -4
- package/src/lib/project-usage-purge.js +100 -0
- package/src/lib/rollout.js +588 -19
- package/src/lib/uploader.js +106 -8
- package/src/lib/vibeusage-api.js +2 -2
- package/src/lib/vibeusage-public-repo.js +88 -0
package/src/lib/rollout.js
CHANGED
|
@@ -4,6 +4,7 @@ const path = require('node:path');
|
|
|
4
4
|
const readline = require('node:readline');
|
|
5
5
|
|
|
6
6
|
const { ensureDir } = require('./fs');
|
|
7
|
+
const { hashRepoRoot, resolveGitHubPublicStatus } = require('./vibeusage-public-repo');
|
|
7
8
|
|
|
8
9
|
const DEFAULT_SOURCE = 'codex';
|
|
9
10
|
const DEFAULT_MODEL = 'unknown';
|
|
@@ -69,7 +70,15 @@ async function listOpencodeMessageFiles(storageDir) {
|
|
|
69
70
|
return out;
|
|
70
71
|
}
|
|
71
72
|
|
|
72
|
-
async function parseRolloutIncremental({
|
|
73
|
+
async function parseRolloutIncremental({
|
|
74
|
+
rolloutFiles,
|
|
75
|
+
cursors,
|
|
76
|
+
queuePath,
|
|
77
|
+
projectQueuePath,
|
|
78
|
+
onProgress,
|
|
79
|
+
source,
|
|
80
|
+
publicRepoResolver
|
|
81
|
+
}) {
|
|
73
82
|
await ensureDir(path.dirname(queuePath));
|
|
74
83
|
let filesProcessed = 0;
|
|
75
84
|
let eventsAggregated = 0;
|
|
@@ -77,6 +86,11 @@ async function parseRolloutIncremental({ rolloutFiles, cursors, queuePath, onPro
|
|
|
77
86
|
const cb = typeof onProgress === 'function' ? onProgress : null;
|
|
78
87
|
const totalFiles = Array.isArray(rolloutFiles) ? rolloutFiles.length : 0;
|
|
79
88
|
const hourlyState = normalizeHourlyState(cursors?.hourly);
|
|
89
|
+
const projectEnabled = typeof projectQueuePath === 'string' && projectQueuePath.length > 0;
|
|
90
|
+
const projectState = projectEnabled ? normalizeProjectState(cursors?.projectHourly) : null;
|
|
91
|
+
const projectTouchedBuckets = projectEnabled ? new Set() : null;
|
|
92
|
+
const projectMetaCache = projectEnabled ? new Map() : null;
|
|
93
|
+
const publicRepoCache = projectEnabled ? new Map() : null;
|
|
80
94
|
const touchedBuckets = new Set();
|
|
81
95
|
const defaultSource = normalizeSourceInput(source) || DEFAULT_SOURCE;
|
|
82
96
|
|
|
@@ -100,6 +114,18 @@ async function parseRolloutIncremental({ rolloutFiles, cursors, queuePath, onPro
|
|
|
100
114
|
const lastTotal = prev && prev.inode === inode ? prev.lastTotal || null : null;
|
|
101
115
|
const lastModel = prev && prev.inode === inode ? prev.lastModel || null : null;
|
|
102
116
|
|
|
117
|
+
const projectContext = projectEnabled
|
|
118
|
+
? await resolveProjectContextForFile({
|
|
119
|
+
filePath,
|
|
120
|
+
projectMetaCache,
|
|
121
|
+
publicRepoCache,
|
|
122
|
+
publicRepoResolver,
|
|
123
|
+
projectState
|
|
124
|
+
})
|
|
125
|
+
: null;
|
|
126
|
+
const projectRef = projectContext?.projectRef || null;
|
|
127
|
+
const projectKey = projectContext?.projectKey || null;
|
|
128
|
+
|
|
103
129
|
const result = await parseRolloutFile({
|
|
104
130
|
filePath,
|
|
105
131
|
startOffset,
|
|
@@ -107,7 +133,14 @@ async function parseRolloutIncremental({ rolloutFiles, cursors, queuePath, onPro
|
|
|
107
133
|
lastModel,
|
|
108
134
|
hourlyState,
|
|
109
135
|
touchedBuckets,
|
|
110
|
-
source: fileSource
|
|
136
|
+
source: fileSource,
|
|
137
|
+
projectState,
|
|
138
|
+
projectTouchedBuckets,
|
|
139
|
+
projectRef,
|
|
140
|
+
projectKey,
|
|
141
|
+
projectMetaCache,
|
|
142
|
+
publicRepoCache,
|
|
143
|
+
publicRepoResolver
|
|
111
144
|
});
|
|
112
145
|
|
|
113
146
|
cursors.files[key] = {
|
|
@@ -134,13 +167,28 @@ async function parseRolloutIncremental({ rolloutFiles, cursors, queuePath, onPro
|
|
|
134
167
|
}
|
|
135
168
|
|
|
136
169
|
const bucketsQueued = await enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets });
|
|
170
|
+
const projectBucketsQueued = projectEnabled
|
|
171
|
+
? await enqueueTouchedProjectBuckets({ projectQueuePath, projectState, projectTouchedBuckets })
|
|
172
|
+
: 0;
|
|
137
173
|
hourlyState.updatedAt = new Date().toISOString();
|
|
138
174
|
cursors.hourly = hourlyState;
|
|
175
|
+
if (projectState) {
|
|
176
|
+
projectState.updatedAt = new Date().toISOString();
|
|
177
|
+
cursors.projectHourly = projectState;
|
|
178
|
+
}
|
|
139
179
|
|
|
140
|
-
return { filesProcessed, eventsAggregated, bucketsQueued };
|
|
180
|
+
return { filesProcessed, eventsAggregated, bucketsQueued, projectBucketsQueued };
|
|
141
181
|
}
|
|
142
182
|
|
|
143
|
-
async function parseClaudeIncremental({
|
|
183
|
+
async function parseClaudeIncremental({
|
|
184
|
+
projectFiles,
|
|
185
|
+
cursors,
|
|
186
|
+
queuePath,
|
|
187
|
+
projectQueuePath,
|
|
188
|
+
onProgress,
|
|
189
|
+
source,
|
|
190
|
+
publicRepoResolver
|
|
191
|
+
}) {
|
|
144
192
|
await ensureDir(path.dirname(queuePath));
|
|
145
193
|
let filesProcessed = 0;
|
|
146
194
|
let eventsAggregated = 0;
|
|
@@ -149,6 +197,11 @@ async function parseClaudeIncremental({ projectFiles, cursors, queuePath, onProg
|
|
|
149
197
|
const files = Array.isArray(projectFiles) ? projectFiles : [];
|
|
150
198
|
const totalFiles = files.length;
|
|
151
199
|
const hourlyState = normalizeHourlyState(cursors?.hourly);
|
|
200
|
+
const projectEnabled = typeof projectQueuePath === 'string' && projectQueuePath.length > 0;
|
|
201
|
+
const projectState = projectEnabled ? normalizeProjectState(cursors?.projectHourly) : null;
|
|
202
|
+
const projectTouchedBuckets = projectEnabled ? new Set() : null;
|
|
203
|
+
const projectMetaCache = projectEnabled ? new Map() : null;
|
|
204
|
+
const publicRepoCache = projectEnabled ? new Map() : null;
|
|
152
205
|
const touchedBuckets = new Set();
|
|
153
206
|
const defaultSource = normalizeSourceInput(source) || 'claude';
|
|
154
207
|
|
|
@@ -170,12 +223,28 @@ async function parseClaudeIncremental({ projectFiles, cursors, queuePath, onProg
|
|
|
170
223
|
const inode = st.ino || 0;
|
|
171
224
|
const startOffset = prev && prev.inode === inode ? prev.offset || 0 : 0;
|
|
172
225
|
|
|
226
|
+
const projectContext = projectEnabled
|
|
227
|
+
? await resolveProjectContextForFile({
|
|
228
|
+
filePath,
|
|
229
|
+
projectMetaCache,
|
|
230
|
+
publicRepoCache,
|
|
231
|
+
publicRepoResolver,
|
|
232
|
+
projectState
|
|
233
|
+
})
|
|
234
|
+
: null;
|
|
235
|
+
const projectRef = projectContext?.projectRef || null;
|
|
236
|
+
const projectKey = projectContext?.projectKey || null;
|
|
237
|
+
|
|
173
238
|
const result = await parseClaudeFile({
|
|
174
239
|
filePath,
|
|
175
240
|
startOffset,
|
|
176
241
|
hourlyState,
|
|
177
242
|
touchedBuckets,
|
|
178
|
-
source: fileSource
|
|
243
|
+
source: fileSource,
|
|
244
|
+
projectState,
|
|
245
|
+
projectTouchedBuckets,
|
|
246
|
+
projectRef,
|
|
247
|
+
projectKey
|
|
179
248
|
});
|
|
180
249
|
|
|
181
250
|
cursors.files[key] = {
|
|
@@ -200,13 +269,28 @@ async function parseClaudeIncremental({ projectFiles, cursors, queuePath, onProg
|
|
|
200
269
|
}
|
|
201
270
|
|
|
202
271
|
const bucketsQueued = await enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets });
|
|
272
|
+
const projectBucketsQueued = projectEnabled
|
|
273
|
+
? await enqueueTouchedProjectBuckets({ projectQueuePath, projectState, projectTouchedBuckets })
|
|
274
|
+
: 0;
|
|
203
275
|
hourlyState.updatedAt = new Date().toISOString();
|
|
204
276
|
cursors.hourly = hourlyState;
|
|
277
|
+
if (projectState) {
|
|
278
|
+
projectState.updatedAt = new Date().toISOString();
|
|
279
|
+
cursors.projectHourly = projectState;
|
|
280
|
+
}
|
|
205
281
|
|
|
206
|
-
return { filesProcessed, eventsAggregated, bucketsQueued };
|
|
282
|
+
return { filesProcessed, eventsAggregated, bucketsQueued, projectBucketsQueued };
|
|
207
283
|
}
|
|
208
284
|
|
|
209
|
-
async function parseGeminiIncremental({
|
|
285
|
+
async function parseGeminiIncremental({
|
|
286
|
+
sessionFiles,
|
|
287
|
+
cursors,
|
|
288
|
+
queuePath,
|
|
289
|
+
projectQueuePath,
|
|
290
|
+
onProgress,
|
|
291
|
+
source,
|
|
292
|
+
publicRepoResolver
|
|
293
|
+
}) {
|
|
210
294
|
await ensureDir(path.dirname(queuePath));
|
|
211
295
|
let filesProcessed = 0;
|
|
212
296
|
let eventsAggregated = 0;
|
|
@@ -215,6 +299,11 @@ async function parseGeminiIncremental({ sessionFiles, cursors, queuePath, onProg
|
|
|
215
299
|
const files = Array.isArray(sessionFiles) ? sessionFiles : [];
|
|
216
300
|
const totalFiles = files.length;
|
|
217
301
|
const hourlyState = normalizeHourlyState(cursors?.hourly);
|
|
302
|
+
const projectEnabled = typeof projectQueuePath === 'string' && projectQueuePath.length > 0;
|
|
303
|
+
const projectState = projectEnabled ? normalizeProjectState(cursors?.projectHourly) : null;
|
|
304
|
+
const projectTouchedBuckets = projectEnabled ? new Set() : null;
|
|
305
|
+
const projectMetaCache = projectEnabled ? new Map() : null;
|
|
306
|
+
const publicRepoCache = projectEnabled ? new Map() : null;
|
|
218
307
|
const touchedBuckets = new Set();
|
|
219
308
|
const defaultSource = normalizeSourceInput(source) || 'gemini';
|
|
220
309
|
|
|
@@ -238,6 +327,18 @@ async function parseGeminiIncremental({ sessionFiles, cursors, queuePath, onProg
|
|
|
238
327
|
let lastTotals = prev && prev.inode === inode ? prev.lastTotals || null : null;
|
|
239
328
|
let lastModel = prev && prev.inode === inode ? prev.lastModel || null : null;
|
|
240
329
|
|
|
330
|
+
const projectContext = projectEnabled
|
|
331
|
+
? await resolveProjectContextForFile({
|
|
332
|
+
filePath,
|
|
333
|
+
projectMetaCache,
|
|
334
|
+
publicRepoCache,
|
|
335
|
+
publicRepoResolver,
|
|
336
|
+
projectState
|
|
337
|
+
})
|
|
338
|
+
: null;
|
|
339
|
+
const projectRef = projectContext?.projectRef || null;
|
|
340
|
+
const projectKey = projectContext?.projectKey || null;
|
|
341
|
+
|
|
241
342
|
const result = await parseGeminiFile({
|
|
242
343
|
filePath,
|
|
243
344
|
startIndex,
|
|
@@ -245,7 +346,11 @@ async function parseGeminiIncremental({ sessionFiles, cursors, queuePath, onProg
|
|
|
245
346
|
lastModel,
|
|
246
347
|
hourlyState,
|
|
247
348
|
touchedBuckets,
|
|
248
|
-
source: fileSource
|
|
349
|
+
source: fileSource,
|
|
350
|
+
projectState,
|
|
351
|
+
projectTouchedBuckets,
|
|
352
|
+
projectRef,
|
|
353
|
+
projectKey
|
|
249
354
|
});
|
|
250
355
|
|
|
251
356
|
cursors.files[key] = {
|
|
@@ -272,13 +377,28 @@ async function parseGeminiIncremental({ sessionFiles, cursors, queuePath, onProg
|
|
|
272
377
|
}
|
|
273
378
|
|
|
274
379
|
const bucketsQueued = await enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets });
|
|
380
|
+
const projectBucketsQueued = projectEnabled
|
|
381
|
+
? await enqueueTouchedProjectBuckets({ projectQueuePath, projectState, projectTouchedBuckets })
|
|
382
|
+
: 0;
|
|
275
383
|
hourlyState.updatedAt = new Date().toISOString();
|
|
276
384
|
cursors.hourly = hourlyState;
|
|
385
|
+
if (projectState) {
|
|
386
|
+
projectState.updatedAt = new Date().toISOString();
|
|
387
|
+
cursors.projectHourly = projectState;
|
|
388
|
+
}
|
|
277
389
|
|
|
278
|
-
return { filesProcessed, eventsAggregated, bucketsQueued };
|
|
390
|
+
return { filesProcessed, eventsAggregated, bucketsQueued, projectBucketsQueued };
|
|
279
391
|
}
|
|
280
392
|
|
|
281
|
-
async function parseOpencodeIncremental({
|
|
393
|
+
async function parseOpencodeIncremental({
|
|
394
|
+
messageFiles,
|
|
395
|
+
cursors,
|
|
396
|
+
queuePath,
|
|
397
|
+
projectQueuePath,
|
|
398
|
+
onProgress,
|
|
399
|
+
source,
|
|
400
|
+
publicRepoResolver
|
|
401
|
+
}) {
|
|
282
402
|
await ensureDir(path.dirname(queuePath));
|
|
283
403
|
let filesProcessed = 0;
|
|
284
404
|
let eventsAggregated = 0;
|
|
@@ -287,6 +407,11 @@ async function parseOpencodeIncremental({ messageFiles, cursors, queuePath, onPr
|
|
|
287
407
|
const files = Array.isArray(messageFiles) ? messageFiles : [];
|
|
288
408
|
const totalFiles = files.length;
|
|
289
409
|
const hourlyState = normalizeHourlyState(cursors?.hourly);
|
|
410
|
+
const projectEnabled = typeof projectQueuePath === 'string' && projectQueuePath.length > 0;
|
|
411
|
+
const projectState = projectEnabled ? normalizeProjectState(cursors?.projectHourly) : null;
|
|
412
|
+
const projectTouchedBuckets = projectEnabled ? new Set() : null;
|
|
413
|
+
const projectMetaCache = projectEnabled ? new Map() : null;
|
|
414
|
+
const publicRepoCache = projectEnabled ? new Map() : null;
|
|
290
415
|
const opencodeState = normalizeOpencodeState(cursors?.opencode);
|
|
291
416
|
const messageIndex = opencodeState.messages;
|
|
292
417
|
const touchedBuckets = new Set();
|
|
@@ -329,6 +454,18 @@ async function parseOpencodeIncremental({ messageFiles, cursors, queuePath, onPr
|
|
|
329
454
|
const fallbackTotals = prev && typeof prev.lastTotals === 'object' ? prev.lastTotals : null;
|
|
330
455
|
const fallbackMessageKey =
|
|
331
456
|
prev && typeof prev.messageKey === 'string' && prev.messageKey.trim() ? prev.messageKey.trim() : null;
|
|
457
|
+
const projectContext = projectEnabled
|
|
458
|
+
? await resolveProjectContextForFile({
|
|
459
|
+
filePath,
|
|
460
|
+
projectMetaCache,
|
|
461
|
+
publicRepoCache,
|
|
462
|
+
publicRepoResolver,
|
|
463
|
+
projectState
|
|
464
|
+
})
|
|
465
|
+
: null;
|
|
466
|
+
const projectRef = projectContext?.projectRef || null;
|
|
467
|
+
const projectKey = projectContext?.projectKey || null;
|
|
468
|
+
|
|
332
469
|
const result = await parseOpencodeMessageFile({
|
|
333
470
|
filePath,
|
|
334
471
|
messageIndex,
|
|
@@ -336,7 +473,11 @@ async function parseOpencodeIncremental({ messageFiles, cursors, queuePath, onPr
|
|
|
336
473
|
fallbackMessageKey,
|
|
337
474
|
hourlyState,
|
|
338
475
|
touchedBuckets,
|
|
339
|
-
source: fileSource
|
|
476
|
+
source: fileSource,
|
|
477
|
+
projectState,
|
|
478
|
+
projectTouchedBuckets,
|
|
479
|
+
projectRef,
|
|
480
|
+
projectKey
|
|
340
481
|
});
|
|
341
482
|
|
|
342
483
|
cursors.files[key] = {
|
|
@@ -371,12 +512,19 @@ async function parseOpencodeIncremental({ messageFiles, cursors, queuePath, onPr
|
|
|
371
512
|
}
|
|
372
513
|
|
|
373
514
|
const bucketsQueued = await enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets });
|
|
515
|
+
const projectBucketsQueued = projectEnabled
|
|
516
|
+
? await enqueueTouchedProjectBuckets({ projectQueuePath, projectState, projectTouchedBuckets })
|
|
517
|
+
: 0;
|
|
374
518
|
hourlyState.updatedAt = new Date().toISOString();
|
|
375
519
|
cursors.hourly = hourlyState;
|
|
376
520
|
opencodeState.updatedAt = new Date().toISOString();
|
|
377
521
|
cursors.opencode = opencodeState;
|
|
522
|
+
if (projectState) {
|
|
523
|
+
projectState.updatedAt = new Date().toISOString();
|
|
524
|
+
cursors.projectHourly = projectState;
|
|
525
|
+
}
|
|
378
526
|
|
|
379
|
-
return { filesProcessed, eventsAggregated, bucketsQueued };
|
|
527
|
+
return { filesProcessed, eventsAggregated, bucketsQueued, projectBucketsQueued };
|
|
380
528
|
}
|
|
381
529
|
|
|
382
530
|
async function parseRolloutFile({
|
|
@@ -386,7 +534,14 @@ async function parseRolloutFile({
|
|
|
386
534
|
lastModel,
|
|
387
535
|
hourlyState,
|
|
388
536
|
touchedBuckets,
|
|
389
|
-
source
|
|
537
|
+
source,
|
|
538
|
+
projectState,
|
|
539
|
+
projectTouchedBuckets,
|
|
540
|
+
projectRef,
|
|
541
|
+
projectKey,
|
|
542
|
+
projectMetaCache,
|
|
543
|
+
publicRepoCache,
|
|
544
|
+
publicRepoResolver
|
|
390
545
|
}) {
|
|
391
546
|
const st = await fs.stat(filePath);
|
|
392
547
|
const endOffset = st.size;
|
|
@@ -399,12 +554,18 @@ async function parseRolloutFile({
|
|
|
399
554
|
|
|
400
555
|
let model = typeof lastModel === 'string' ? lastModel : null;
|
|
401
556
|
let totals = lastTotal && typeof lastTotal === 'object' ? lastTotal : null;
|
|
557
|
+
let currentCwd = null;
|
|
558
|
+
let currentProjectRef = projectRef || null;
|
|
559
|
+
let currentProjectKey = projectKey || null;
|
|
402
560
|
let eventsAggregated = 0;
|
|
403
561
|
|
|
404
562
|
for await (const line of rl) {
|
|
405
563
|
if (!line) continue;
|
|
406
564
|
const maybeTokenCount = line.includes('"token_count"');
|
|
407
|
-
const maybeTurnContext =
|
|
565
|
+
const maybeTurnContext =
|
|
566
|
+
!maybeTokenCount &&
|
|
567
|
+
(line.includes('"turn_context"') || line.includes('"session_meta"')) &&
|
|
568
|
+
(line.includes('"model"') || line.includes('"cwd"'));
|
|
408
569
|
if (!maybeTokenCount && !maybeTurnContext) continue;
|
|
409
570
|
|
|
410
571
|
let obj;
|
|
@@ -414,8 +575,29 @@ async function parseRolloutFile({
|
|
|
414
575
|
continue;
|
|
415
576
|
}
|
|
416
577
|
|
|
417
|
-
if (
|
|
418
|
-
|
|
578
|
+
if (
|
|
579
|
+
(obj?.type === 'turn_context' || obj?.type === 'session_meta') &&
|
|
580
|
+
obj?.payload &&
|
|
581
|
+
typeof obj.payload === 'object'
|
|
582
|
+
) {
|
|
583
|
+
if (typeof obj.payload.model === 'string') {
|
|
584
|
+
model = obj.payload.model;
|
|
585
|
+
}
|
|
586
|
+
if (projectState && typeof obj.payload.cwd === 'string') {
|
|
587
|
+
const nextCwd = obj.payload.cwd.trim();
|
|
588
|
+
if (nextCwd && nextCwd !== currentCwd) {
|
|
589
|
+
const context = await resolveProjectContextForPath({
|
|
590
|
+
startDir: nextCwd,
|
|
591
|
+
projectMetaCache,
|
|
592
|
+
publicRepoCache,
|
|
593
|
+
publicRepoResolver,
|
|
594
|
+
projectState
|
|
595
|
+
});
|
|
596
|
+
currentCwd = nextCwd;
|
|
597
|
+
currentProjectRef = context?.projectRef || null;
|
|
598
|
+
currentProjectKey = context?.projectKey || null;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
419
601
|
continue;
|
|
420
602
|
}
|
|
421
603
|
|
|
@@ -444,13 +626,34 @@ async function parseRolloutFile({
|
|
|
444
626
|
const bucket = getHourlyBucket(hourlyState, source, model, bucketStart);
|
|
445
627
|
addTotals(bucket.totals, delta);
|
|
446
628
|
touchedBuckets.add(bucketKey(source, model, bucketStart));
|
|
629
|
+
if (currentProjectKey && projectState && projectTouchedBuckets) {
|
|
630
|
+
const projectBucket = getProjectBucket(
|
|
631
|
+
projectState,
|
|
632
|
+
currentProjectKey,
|
|
633
|
+
source,
|
|
634
|
+
bucketStart,
|
|
635
|
+
currentProjectRef
|
|
636
|
+
);
|
|
637
|
+
addTotals(projectBucket.totals, delta);
|
|
638
|
+
projectTouchedBuckets.add(projectBucketKey(currentProjectKey, source, bucketStart));
|
|
639
|
+
}
|
|
447
640
|
eventsAggregated += 1;
|
|
448
641
|
}
|
|
449
642
|
|
|
450
643
|
return { endOffset, lastTotal: totals, lastModel: model, eventsAggregated };
|
|
451
644
|
}
|
|
452
645
|
|
|
453
|
-
async function parseClaudeFile({
|
|
646
|
+
async function parseClaudeFile({
|
|
647
|
+
filePath,
|
|
648
|
+
startOffset,
|
|
649
|
+
hourlyState,
|
|
650
|
+
touchedBuckets,
|
|
651
|
+
source,
|
|
652
|
+
projectState,
|
|
653
|
+
projectTouchedBuckets,
|
|
654
|
+
projectRef,
|
|
655
|
+
projectKey
|
|
656
|
+
}) {
|
|
454
657
|
const st = await fs.stat(filePath).catch(() => null);
|
|
455
658
|
if (!st || !st.isFile()) return { endOffset: startOffset, eventsAggregated: 0 };
|
|
456
659
|
|
|
@@ -486,6 +689,11 @@ async function parseClaudeFile({ filePath, startOffset, hourlyState, touchedBuck
|
|
|
486
689
|
const bucket = getHourlyBucket(hourlyState, source, model, bucketStart);
|
|
487
690
|
addTotals(bucket.totals, delta);
|
|
488
691
|
touchedBuckets.add(bucketKey(source, model, bucketStart));
|
|
692
|
+
if (projectKey && projectState && projectTouchedBuckets) {
|
|
693
|
+
const projectBucket = getProjectBucket(projectState, projectKey, source, bucketStart, projectRef);
|
|
694
|
+
addTotals(projectBucket.totals, delta);
|
|
695
|
+
projectTouchedBuckets.add(projectBucketKey(projectKey, source, bucketStart));
|
|
696
|
+
}
|
|
489
697
|
eventsAggregated += 1;
|
|
490
698
|
}
|
|
491
699
|
|
|
@@ -501,7 +709,11 @@ async function parseGeminiFile({
|
|
|
501
709
|
lastModel,
|
|
502
710
|
hourlyState,
|
|
503
711
|
touchedBuckets,
|
|
504
|
-
source
|
|
712
|
+
source,
|
|
713
|
+
projectState,
|
|
714
|
+
projectTouchedBuckets,
|
|
715
|
+
projectRef,
|
|
716
|
+
projectKey
|
|
505
717
|
}) {
|
|
506
718
|
const raw = await fs.readFile(filePath, 'utf8').catch(() => '');
|
|
507
719
|
if (!raw.trim()) return { lastIndex: startIndex, lastTotals, lastModel, eventsAggregated: 0 };
|
|
@@ -554,6 +766,11 @@ async function parseGeminiFile({
|
|
|
554
766
|
const bucket = getHourlyBucket(hourlyState, source, model, bucketStart);
|
|
555
767
|
addTotals(bucket.totals, delta);
|
|
556
768
|
touchedBuckets.add(bucketKey(source, model, bucketStart));
|
|
769
|
+
if (projectKey && projectState && projectTouchedBuckets) {
|
|
770
|
+
const projectBucket = getProjectBucket(projectState, projectKey, source, bucketStart, projectRef);
|
|
771
|
+
addTotals(projectBucket.totals, delta);
|
|
772
|
+
projectTouchedBuckets.add(projectBucketKey(projectKey, source, bucketStart));
|
|
773
|
+
}
|
|
557
774
|
eventsAggregated += 1;
|
|
558
775
|
totals = currentTotals;
|
|
559
776
|
}
|
|
@@ -573,7 +790,11 @@ async function parseOpencodeMessageFile({
|
|
|
573
790
|
fallbackMessageKey,
|
|
574
791
|
hourlyState,
|
|
575
792
|
touchedBuckets,
|
|
576
|
-
source
|
|
793
|
+
source,
|
|
794
|
+
projectState,
|
|
795
|
+
projectTouchedBuckets,
|
|
796
|
+
projectRef,
|
|
797
|
+
projectKey
|
|
577
798
|
}) {
|
|
578
799
|
const fallbackKey =
|
|
579
800
|
typeof fallbackMessageKey === 'string' && fallbackMessageKey.trim() ? fallbackMessageKey.trim() : null;
|
|
@@ -645,6 +866,11 @@ async function parseOpencodeMessageFile({
|
|
|
645
866
|
const bucket = getHourlyBucket(hourlyState, source, model, bucketStart);
|
|
646
867
|
addTotals(bucket.totals, delta);
|
|
647
868
|
touchedBuckets.add(bucketKey(source, model, bucketStart));
|
|
869
|
+
if (projectKey && projectState && projectTouchedBuckets) {
|
|
870
|
+
const projectBucket = getProjectBucket(projectState, projectKey, source, bucketStart, projectRef);
|
|
871
|
+
addTotals(projectBucket.totals, delta);
|
|
872
|
+
projectTouchedBuckets.add(projectBucketKey(projectKey, source, bucketStart));
|
|
873
|
+
}
|
|
648
874
|
return { messageKey, lastTotals: currentTotals, eventsAggregated: 1, shouldUpdate: true };
|
|
649
875
|
}
|
|
650
876
|
|
|
@@ -918,6 +1144,45 @@ async function enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets })
|
|
|
918
1144
|
return toAppend.length;
|
|
919
1145
|
}
|
|
920
1146
|
|
|
1147
|
+
async function enqueueTouchedProjectBuckets({ projectQueuePath, projectState, projectTouchedBuckets }) {
|
|
1148
|
+
if (!projectQueuePath || !projectState || !projectTouchedBuckets || projectTouchedBuckets.size === 0) return 0;
|
|
1149
|
+
|
|
1150
|
+
await ensureDir(path.dirname(projectQueuePath));
|
|
1151
|
+
|
|
1152
|
+
const toAppend = [];
|
|
1153
|
+
for (const key of projectTouchedBuckets) {
|
|
1154
|
+
const bucket = projectState.buckets[key];
|
|
1155
|
+
if (!bucket || !bucket.totals) continue;
|
|
1156
|
+
const totals = bucket.totals;
|
|
1157
|
+
const queuedKey = totalsKey(totals);
|
|
1158
|
+
if (bucket.queuedKey === queuedKey) continue;
|
|
1159
|
+
const projectRef = typeof bucket.project_ref === 'string' ? bucket.project_ref : null;
|
|
1160
|
+
const projectKey = typeof bucket.project_key === 'string' ? bucket.project_key : null;
|
|
1161
|
+
if (!projectRef || !projectKey) continue;
|
|
1162
|
+
|
|
1163
|
+
toAppend.push(
|
|
1164
|
+
JSON.stringify({
|
|
1165
|
+
project_ref: projectRef,
|
|
1166
|
+
project_key: projectKey,
|
|
1167
|
+
source: bucket.source,
|
|
1168
|
+
hour_start: bucket.hour_start,
|
|
1169
|
+
input_tokens: totals.input_tokens,
|
|
1170
|
+
cached_input_tokens: totals.cached_input_tokens,
|
|
1171
|
+
output_tokens: totals.output_tokens,
|
|
1172
|
+
reasoning_output_tokens: totals.reasoning_output_tokens,
|
|
1173
|
+
total_tokens: totals.total_tokens
|
|
1174
|
+
})
|
|
1175
|
+
);
|
|
1176
|
+
bucket.queuedKey = queuedKey;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
if (toAppend.length > 0) {
|
|
1180
|
+
await fs.appendFile(projectQueuePath, toAppend.join('\n') + '\n', 'utf8');
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
return toAppend.length;
|
|
1184
|
+
}
|
|
1185
|
+
|
|
921
1186
|
function pickDominantModel(buckets) {
|
|
922
1187
|
let dominantModel = null;
|
|
923
1188
|
let dominantTotal = -1;
|
|
@@ -1049,6 +1314,31 @@ function normalizeHourlyState(raw) {
|
|
|
1049
1314
|
};
|
|
1050
1315
|
}
|
|
1051
1316
|
|
|
1317
|
+
function normalizeProjectState(raw) {
|
|
1318
|
+
const state = raw && typeof raw === 'object' ? raw : {};
|
|
1319
|
+
const rawBuckets = state.buckets && typeof state.buckets === 'object' ? state.buckets : {};
|
|
1320
|
+
const buckets = {};
|
|
1321
|
+
const rawProjects = state.projects && typeof state.projects === 'object' ? state.projects : {};
|
|
1322
|
+
const projects = {};
|
|
1323
|
+
|
|
1324
|
+
for (const [key, value] of Object.entries(rawBuckets)) {
|
|
1325
|
+
if (!key) continue;
|
|
1326
|
+
buckets[key] = value;
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
for (const [key, value] of Object.entries(rawProjects)) {
|
|
1330
|
+
if (!key || !value || typeof value !== 'object') continue;
|
|
1331
|
+
projects[key] = { ...value };
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
return {
|
|
1335
|
+
version: 2,
|
|
1336
|
+
buckets,
|
|
1337
|
+
projects,
|
|
1338
|
+
updatedAt: typeof state.updatedAt === 'string' ? state.updatedAt : null
|
|
1339
|
+
};
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1052
1342
|
function normalizeOpencodeState(raw) {
|
|
1053
1343
|
const state = raw && typeof raw === 'object' ? raw : {};
|
|
1054
1344
|
const messages = state.messages && typeof state.messages === 'object' ? state.messages : {};
|
|
@@ -1093,6 +1383,40 @@ function getHourlyBucket(state, source, model, hourStart) {
|
|
|
1093
1383
|
return bucket;
|
|
1094
1384
|
}
|
|
1095
1385
|
|
|
1386
|
+
function getProjectBucket(state, projectKey, source, hourStart, projectRef) {
|
|
1387
|
+
const buckets = state.buckets;
|
|
1388
|
+
const normalizedSource = normalizeSourceInput(source) || DEFAULT_SOURCE;
|
|
1389
|
+
const key = projectBucketKey(projectKey, normalizedSource, hourStart);
|
|
1390
|
+
let bucket = buckets[key];
|
|
1391
|
+
if (!bucket || typeof bucket !== 'object') {
|
|
1392
|
+
bucket = {
|
|
1393
|
+
totals: initTotals(),
|
|
1394
|
+
queuedKey: null,
|
|
1395
|
+
project_key: projectKey,
|
|
1396
|
+
project_ref: projectRef,
|
|
1397
|
+
source: normalizedSource,
|
|
1398
|
+
hour_start: hourStart
|
|
1399
|
+
};
|
|
1400
|
+
buckets[key] = bucket;
|
|
1401
|
+
return bucket;
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
if (!bucket.totals || typeof bucket.totals !== 'object') {
|
|
1405
|
+
bucket.totals = initTotals();
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
if (bucket.queuedKey != null && typeof bucket.queuedKey !== 'string') {
|
|
1409
|
+
bucket.queuedKey = null;
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
if (projectRef) bucket.project_ref = projectRef;
|
|
1413
|
+
if (projectKey) bucket.project_key = projectKey;
|
|
1414
|
+
bucket.source = normalizedSource;
|
|
1415
|
+
bucket.hour_start = hourStart;
|
|
1416
|
+
|
|
1417
|
+
return bucket;
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1096
1420
|
function initTotals() {
|
|
1097
1421
|
return {
|
|
1098
1422
|
input_tokens: 0,
|
|
@@ -1146,6 +1470,11 @@ function bucketKey(source, model, hourStart) {
|
|
|
1146
1470
|
return `${safeSource}${BUCKET_SEPARATOR}${safeModel}${BUCKET_SEPARATOR}${hourStart}`;
|
|
1147
1471
|
}
|
|
1148
1472
|
|
|
1473
|
+
function projectBucketKey(projectKey, source, hourStart) {
|
|
1474
|
+
const safeSource = normalizeSourceInput(source) || DEFAULT_SOURCE;
|
|
1475
|
+
return `${projectKey}${BUCKET_SEPARATOR}${safeSource}${BUCKET_SEPARATOR}${hourStart}`;
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1149
1478
|
function groupBucketKey(source, hourStart) {
|
|
1150
1479
|
const safeSource = normalizeSourceInput(source) || DEFAULT_SOURCE;
|
|
1151
1480
|
return `${safeSource}${BUCKET_SEPARATOR}${hourStart}`;
|
|
@@ -1178,6 +1507,246 @@ function normalizeModelInput(value) {
|
|
|
1178
1507
|
return trimmed.length > 0 ? trimmed : null;
|
|
1179
1508
|
}
|
|
1180
1509
|
|
|
1510
|
+
async function resolveProjectMetaForPath(startDir, cache) {
|
|
1511
|
+
if (!startDir || typeof startDir !== 'string') return null;
|
|
1512
|
+
if (cache && cache.has(startDir)) return cache.get(startDir);
|
|
1513
|
+
|
|
1514
|
+
const visited = [];
|
|
1515
|
+
let current = startDir;
|
|
1516
|
+
const root = path.parse(startDir).root;
|
|
1517
|
+
while (current) {
|
|
1518
|
+
if (cache && cache.has(current)) {
|
|
1519
|
+
const cached = cache.get(current);
|
|
1520
|
+
for (const entry of visited) cache.set(entry, cached);
|
|
1521
|
+
return cached;
|
|
1522
|
+
}
|
|
1523
|
+
visited.push(current);
|
|
1524
|
+
|
|
1525
|
+
const configPath = await resolveGitConfigPath(current);
|
|
1526
|
+
if (configPath) {
|
|
1527
|
+
const remoteUrl = await readGitRemoteUrl(configPath);
|
|
1528
|
+
const projectRef = canonicalizeProjectRef(remoteUrl);
|
|
1529
|
+
const meta = { projectRef: projectRef || null, repoRoot: current };
|
|
1530
|
+
if (cache) {
|
|
1531
|
+
for (const entry of visited) cache.set(entry, meta);
|
|
1532
|
+
}
|
|
1533
|
+
return meta;
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
if (current === root) break;
|
|
1537
|
+
const parent = path.dirname(current);
|
|
1538
|
+
if (!parent || parent === current) break;
|
|
1539
|
+
current = parent;
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
if (cache) {
|
|
1543
|
+
for (const entry of visited) cache.set(entry, null);
|
|
1544
|
+
}
|
|
1545
|
+
return null;
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
async function defaultPublicRepoResolver({ projectRef, repoRoot, cache }) {
|
|
1549
|
+
if (!projectRef) {
|
|
1550
|
+
return {
|
|
1551
|
+
status: 'blocked',
|
|
1552
|
+
projectKey: null,
|
|
1553
|
+
projectRef: null,
|
|
1554
|
+
repoRootHash: repoRoot ? hashRepoRoot(repoRoot) : null,
|
|
1555
|
+
reason: 'missing_ref'
|
|
1556
|
+
};
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
const cached = cache && cache.has(projectRef) ? cache.get(projectRef) : null;
|
|
1560
|
+
const base = cached || (await resolveGitHubPublicStatus(projectRef));
|
|
1561
|
+
if (cache && !cached) cache.set(projectRef, base);
|
|
1562
|
+
return {
|
|
1563
|
+
...base,
|
|
1564
|
+
repoRootHash: repoRoot ? hashRepoRoot(repoRoot) : null
|
|
1565
|
+
};
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
function recordProjectMeta(projectState, meta) {
|
|
1569
|
+
if (!projectState || !meta || typeof meta !== 'object') return;
|
|
1570
|
+
const repoRootHash = typeof meta.repoRootHash === 'string' ? meta.repoRootHash : null;
|
|
1571
|
+
let projectKey = typeof meta.projectKey === 'string' ? meta.projectKey : null;
|
|
1572
|
+
if (!projectKey && repoRootHash && projectState.projects && typeof projectState.projects === 'object') {
|
|
1573
|
+
for (const [key, entry] of Object.entries(projectState.projects)) {
|
|
1574
|
+
if (entry && entry.repo_root_hash === repoRootHash) {
|
|
1575
|
+
projectKey = key;
|
|
1576
|
+
break;
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
if (!projectKey) return;
|
|
1581
|
+
if (!projectState.projects || typeof projectState.projects !== 'object') {
|
|
1582
|
+
projectState.projects = {};
|
|
1583
|
+
}
|
|
1584
|
+
const prev = projectState.projects[projectKey] || {};
|
|
1585
|
+
const status = typeof meta.status === 'string' ? meta.status : null;
|
|
1586
|
+
const projectRef = typeof meta.projectRef === 'string' ? meta.projectRef : null;
|
|
1587
|
+
const next = {
|
|
1588
|
+
...prev,
|
|
1589
|
+
project_ref: projectRef || prev.project_ref || null,
|
|
1590
|
+
status: status || prev.status || null,
|
|
1591
|
+
repo_root_hash: repoRootHash || prev.repo_root_hash || null,
|
|
1592
|
+
updated_at: new Date().toISOString()
|
|
1593
|
+
};
|
|
1594
|
+
if (status === 'blocked' && prev.status !== 'blocked') {
|
|
1595
|
+
next.purge_pending = true;
|
|
1596
|
+
} else if (status && status !== 'blocked') {
|
|
1597
|
+
next.purge_pending = false;
|
|
1598
|
+
}
|
|
1599
|
+
projectState.projects[projectKey] = next;
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
async function resolveProjectContextForFile({
|
|
1603
|
+
filePath,
|
|
1604
|
+
projectMetaCache,
|
|
1605
|
+
publicRepoCache,
|
|
1606
|
+
publicRepoResolver,
|
|
1607
|
+
projectState
|
|
1608
|
+
}) {
|
|
1609
|
+
if (!filePath) return null;
|
|
1610
|
+
return resolveProjectContextForPath({
|
|
1611
|
+
startDir: path.dirname(filePath),
|
|
1612
|
+
projectMetaCache,
|
|
1613
|
+
publicRepoCache,
|
|
1614
|
+
publicRepoResolver,
|
|
1615
|
+
projectState
|
|
1616
|
+
});
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
async function resolveProjectContextForPath({
|
|
1620
|
+
startDir,
|
|
1621
|
+
projectMetaCache,
|
|
1622
|
+
publicRepoCache,
|
|
1623
|
+
publicRepoResolver,
|
|
1624
|
+
projectState
|
|
1625
|
+
}) {
|
|
1626
|
+
if (!startDir) return null;
|
|
1627
|
+
const projectMeta = await resolveProjectMetaForPath(startDir, projectMetaCache);
|
|
1628
|
+
if (!projectMeta) return null;
|
|
1629
|
+
const resolver = typeof publicRepoResolver === 'function' ? publicRepoResolver : defaultPublicRepoResolver;
|
|
1630
|
+
const meta = await resolver({
|
|
1631
|
+
projectRef: projectMeta.projectRef,
|
|
1632
|
+
repoRoot: projectMeta.repoRoot,
|
|
1633
|
+
cache: publicRepoCache
|
|
1634
|
+
});
|
|
1635
|
+
const repoRootHash = projectMeta.repoRoot ? hashRepoRoot(projectMeta.repoRoot) : null;
|
|
1636
|
+
const normalized = {
|
|
1637
|
+
...(meta || {}),
|
|
1638
|
+
projectRef: meta?.projectRef || projectMeta.projectRef,
|
|
1639
|
+
projectKey: meta?.projectKey || null,
|
|
1640
|
+
status: meta?.status || 'blocked',
|
|
1641
|
+
repoRootHash: meta?.repoRootHash || repoRootHash
|
|
1642
|
+
};
|
|
1643
|
+
recordProjectMeta(projectState, normalized);
|
|
1644
|
+
if (normalized.status !== 'public_verified') {
|
|
1645
|
+
return { projectRef: normalized.projectRef, projectKey: null, status: normalized.status };
|
|
1646
|
+
}
|
|
1647
|
+
return { projectRef: normalized.projectRef, projectKey: normalized.projectKey, status: normalized.status };
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
async function resolveGitConfigPath(rootDir) {
|
|
1651
|
+
const gitPath = path.join(rootDir, '.git');
|
|
1652
|
+
const st = await fs.stat(gitPath).catch(() => null);
|
|
1653
|
+
if (!st) return null;
|
|
1654
|
+
if (st.isDirectory()) {
|
|
1655
|
+
const configPath = path.join(gitPath, 'config');
|
|
1656
|
+
const cfg = await fs.stat(configPath).catch(() => null);
|
|
1657
|
+
return cfg && cfg.isFile() ? configPath : null;
|
|
1658
|
+
}
|
|
1659
|
+
if (st.isFile()) {
|
|
1660
|
+
const content = await fs.readFile(gitPath, 'utf8').catch(() => '');
|
|
1661
|
+
const match = content.match(/gitdir:\s*(.+)/i);
|
|
1662
|
+
if (!match) return null;
|
|
1663
|
+
let gitDir = match[1].trim();
|
|
1664
|
+
if (!gitDir) return null;
|
|
1665
|
+
if (!path.isAbsolute(gitDir)) {
|
|
1666
|
+
gitDir = path.resolve(rootDir, gitDir);
|
|
1667
|
+
}
|
|
1668
|
+
const configPath = path.join(gitDir, 'config');
|
|
1669
|
+
const cfg = await fs.stat(configPath).catch(() => null);
|
|
1670
|
+
if (cfg && cfg.isFile()) return configPath;
|
|
1671
|
+
|
|
1672
|
+
const commonDirRaw = await fs.readFile(path.join(gitDir, 'commondir'), 'utf8').catch(() => '');
|
|
1673
|
+
const commonDirRel = commonDirRaw.trim();
|
|
1674
|
+
if (!commonDirRel) return null;
|
|
1675
|
+
let commonDir = commonDirRel;
|
|
1676
|
+
if (!path.isAbsolute(commonDir)) {
|
|
1677
|
+
commonDir = path.resolve(gitDir, commonDir);
|
|
1678
|
+
}
|
|
1679
|
+
const commonConfigPath = path.join(commonDir, 'config');
|
|
1680
|
+
const commonCfg = await fs.stat(commonConfigPath).catch(() => null);
|
|
1681
|
+
return commonCfg && commonCfg.isFile() ? commonConfigPath : null;
|
|
1682
|
+
}
|
|
1683
|
+
return null;
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
async function readGitRemoteUrl(configPath) {
|
|
1687
|
+
const raw = await fs.readFile(configPath, 'utf8').catch(() => '');
|
|
1688
|
+
if (!raw.trim()) return null;
|
|
1689
|
+
|
|
1690
|
+
const remotes = new Map();
|
|
1691
|
+
let current = null;
|
|
1692
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
1693
|
+
const sectionHeader = line.match(/^\s*\[[^\]]+\]\s*$/);
|
|
1694
|
+
if (sectionHeader) {
|
|
1695
|
+
const sectionMatch = line.match(/^\s*\[remote\s+"([^"]+)"\]\s*$/i);
|
|
1696
|
+
current = sectionMatch ? sectionMatch[1] : null;
|
|
1697
|
+
continue;
|
|
1698
|
+
}
|
|
1699
|
+
if (!current) continue;
|
|
1700
|
+
const urlMatch = line.match(/^\s*url\s*=\s*(.+)\s*$/i);
|
|
1701
|
+
if (urlMatch) {
|
|
1702
|
+
remotes.set(current, urlMatch[1].trim());
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
if (remotes.has('origin')) return remotes.get('origin');
|
|
1707
|
+
const first = remotes.values().next();
|
|
1708
|
+
return first.done ? null : first.value;
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
function canonicalizeProjectRef(remoteUrl) {
|
|
1712
|
+
if (typeof remoteUrl !== 'string') return null;
|
|
1713
|
+
let ref = remoteUrl.trim();
|
|
1714
|
+
if (!ref) return null;
|
|
1715
|
+
|
|
1716
|
+
if (ref.startsWith('file://')) return null;
|
|
1717
|
+
if (path.isAbsolute(ref) || path.win32.isAbsolute(ref)) return null;
|
|
1718
|
+
|
|
1719
|
+
const gitAtMatch = ref.match(/^git@([^:]+):(.+)$/i);
|
|
1720
|
+
if (gitAtMatch) {
|
|
1721
|
+
ref = `https://${gitAtMatch[1]}/${gitAtMatch[2]}`;
|
|
1722
|
+
} else if (ref.startsWith('ssh://')) {
|
|
1723
|
+
try {
|
|
1724
|
+
const parsed = new URL(ref);
|
|
1725
|
+
ref = `https://${parsed.hostname}${parsed.pathname}`;
|
|
1726
|
+
} catch (_e) {
|
|
1727
|
+
return null;
|
|
1728
|
+
}
|
|
1729
|
+
} else if (ref.startsWith('git://')) {
|
|
1730
|
+
ref = `https://${ref.slice('git://'.length)}`;
|
|
1731
|
+
} else if (ref.startsWith('http://')) {
|
|
1732
|
+
ref = `https://${ref.slice('http://'.length)}`;
|
|
1733
|
+
} else if (!ref.startsWith('https://')) {
|
|
1734
|
+
return null;
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
try {
|
|
1738
|
+
const parsed = new URL(ref);
|
|
1739
|
+
if (!parsed.hostname) return null;
|
|
1740
|
+
ref = `https://${parsed.hostname}${parsed.pathname}`;
|
|
1741
|
+
} catch (_e) {
|
|
1742
|
+
return null;
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
ref = ref.replace(/\.git$/i, '');
|
|
1746
|
+
ref = ref.replace(/\/+$/, '');
|
|
1747
|
+
return ref || null;
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1181
1750
|
function normalizeGeminiTokens(tokens) {
|
|
1182
1751
|
if (!tokens || typeof tokens !== 'object') return null;
|
|
1183
1752
|
const input = toNonNegativeInt(tokens.input);
|