@runtimescope/collector 0.9.1 → 0.9.3
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/dist/{chunk-HZWALDZM.js → chunk-MM44DN7Y.js} +561 -68
- package/dist/chunk-MM44DN7Y.js.map +1 -0
- package/dist/chunk-UP2VWCW5.js +38 -0
- package/dist/chunk-UP2VWCW5.js.map +1 -0
- package/dist/dashboard.d.ts +1 -0
- package/dist/dashboard.js +145 -0
- package/dist/dashboard.js.map +1 -0
- package/dist/excel-3DUFQWCD.js +64818 -0
- package/dist/excel-3DUFQWCD.js.map +1 -0
- package/dist/index.d.ts +153 -9
- package/dist/index.js +16 -4
- package/dist/index.js.map +1 -1
- package/dist/standalone.js +16 -1
- package/dist/standalone.js.map +1 -1
- package/package.json +3 -2
- package/dist/chunk-HZWALDZM.js.map +0 -1
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
}
|
|
4
|
-
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
5
|
-
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
6
|
-
});
|
|
1
|
+
import {
|
|
2
|
+
__require
|
|
3
|
+
} from "./chunk-UP2VWCW5.js";
|
|
7
4
|
|
|
8
5
|
// src/ring-buffer.ts
|
|
9
6
|
var RingBuffer = class {
|
|
@@ -53,6 +50,12 @@ var RingBuffer = class {
|
|
|
53
50
|
};
|
|
54
51
|
|
|
55
52
|
// src/store.ts
|
|
53
|
+
function matchesSessionFilter(eventSessionId, filterSessionId) {
|
|
54
|
+
if (filterSessionId.includes(",")) {
|
|
55
|
+
return filterSessionId.split(",").includes(eventSessionId);
|
|
56
|
+
}
|
|
57
|
+
return eventSessionId === filterSessionId;
|
|
58
|
+
}
|
|
56
59
|
var EventStore = class {
|
|
57
60
|
buffer;
|
|
58
61
|
sessions = /* @__PURE__ */ new Map();
|
|
@@ -113,7 +116,7 @@ var EventStore = class {
|
|
|
113
116
|
const since = filter.sinceSeconds ? Date.now() - filter.sinceSeconds * 1e3 : 0;
|
|
114
117
|
return this.buffer.query((e) => {
|
|
115
118
|
if (e.eventType !== "network") return false;
|
|
116
|
-
if (filter.sessionId && e.sessionId
|
|
119
|
+
if (filter.sessionId && !matchesSessionFilter(e.sessionId, filter.sessionId)) return false;
|
|
117
120
|
if (filter.projectId && !this.matchesProjectId(e.sessionId, filter.projectId)) return false;
|
|
118
121
|
const ne = e;
|
|
119
122
|
if (ne.timestamp < since) return false;
|
|
@@ -128,7 +131,7 @@ var EventStore = class {
|
|
|
128
131
|
const since = filter.sinceSeconds ? Date.now() - filter.sinceSeconds * 1e3 : 0;
|
|
129
132
|
return this.buffer.query((e) => {
|
|
130
133
|
if (e.eventType !== "console") return false;
|
|
131
|
-
if (filter.sessionId && e.sessionId
|
|
134
|
+
if (filter.sessionId && !matchesSessionFilter(e.sessionId, filter.sessionId)) return false;
|
|
132
135
|
if (filter.projectId && !this.matchesProjectId(e.sessionId, filter.projectId)) return false;
|
|
133
136
|
const ce = e;
|
|
134
137
|
if (ce.timestamp < since) return false;
|
|
@@ -150,7 +153,7 @@ var EventStore = class {
|
|
|
150
153
|
const typeSet = filter.eventTypes ? new Set(filter.eventTypes) : null;
|
|
151
154
|
return this.buffer.toArray().filter((e) => {
|
|
152
155
|
if (e.timestamp < since) return false;
|
|
153
|
-
if (filter.sessionId && e.sessionId
|
|
156
|
+
if (filter.sessionId && !matchesSessionFilter(e.sessionId, filter.sessionId)) return false;
|
|
154
157
|
if (filter.projectId && !this.matchesProjectId(e.sessionId, filter.projectId)) return false;
|
|
155
158
|
if (typeSet && !typeSet.has(e.eventType)) return false;
|
|
156
159
|
return true;
|
|
@@ -171,16 +174,28 @@ var EventStore = class {
|
|
|
171
174
|
getSessionIdsForProjectId(projectId) {
|
|
172
175
|
return Array.from(this.sessions.values()).filter((s) => s.projectId === projectId).map((s) => s.sessionId);
|
|
173
176
|
}
|
|
177
|
+
/** Re-tag all sessions with oldProjectId to use newProjectId. */
|
|
178
|
+
retagSessions(oldProjectId, newProjectId) {
|
|
179
|
+
for (const [, session] of this.sessions) {
|
|
180
|
+
if (session.projectId === oldProjectId) {
|
|
181
|
+
session.projectId = newProjectId;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
174
185
|
/** Check if an event belongs to the given projectId (via its session). */
|
|
175
186
|
matchesProjectId(sessionId, projectId) {
|
|
176
187
|
const session = this.sessions.get(sessionId);
|
|
177
|
-
|
|
188
|
+
if (!session?.projectId) return false;
|
|
189
|
+
if (projectId.includes(",")) {
|
|
190
|
+
return projectId.split(",").includes(session.projectId);
|
|
191
|
+
}
|
|
192
|
+
return session.projectId === projectId;
|
|
178
193
|
}
|
|
179
194
|
getStateEvents(filter = {}) {
|
|
180
195
|
const since = filter.sinceSeconds ? Date.now() - filter.sinceSeconds * 1e3 : 0;
|
|
181
196
|
return this.buffer.query((e) => {
|
|
182
197
|
if (e.eventType !== "state") return false;
|
|
183
|
-
if (filter.sessionId && e.sessionId
|
|
198
|
+
if (filter.sessionId && !matchesSessionFilter(e.sessionId, filter.sessionId)) return false;
|
|
184
199
|
if (filter.projectId && !this.matchesProjectId(e.sessionId, filter.projectId)) return false;
|
|
185
200
|
const se = e;
|
|
186
201
|
if (se.timestamp < since) return false;
|
|
@@ -192,7 +207,7 @@ var EventStore = class {
|
|
|
192
207
|
const since = filter.sinceSeconds ? Date.now() - filter.sinceSeconds * 1e3 : 0;
|
|
193
208
|
return this.buffer.query((e) => {
|
|
194
209
|
if (e.eventType !== "render") return false;
|
|
195
|
-
if (filter.sessionId && e.sessionId
|
|
210
|
+
if (filter.sessionId && !matchesSessionFilter(e.sessionId, filter.sessionId)) return false;
|
|
196
211
|
if (filter.projectId && !this.matchesProjectId(e.sessionId, filter.projectId)) return false;
|
|
197
212
|
const re = e;
|
|
198
213
|
if (re.timestamp < since) return false;
|
|
@@ -209,7 +224,7 @@ var EventStore = class {
|
|
|
209
224
|
const since = filter.sinceSeconds ? Date.now() - filter.sinceSeconds * 1e3 : 0;
|
|
210
225
|
return this.buffer.query((e) => {
|
|
211
226
|
if (e.eventType !== "performance") return false;
|
|
212
|
-
if (filter.sessionId && e.sessionId
|
|
227
|
+
if (filter.sessionId && !matchesSessionFilter(e.sessionId, filter.sessionId)) return false;
|
|
213
228
|
if (filter.projectId && !this.matchesProjectId(e.sessionId, filter.projectId)) return false;
|
|
214
229
|
const pe = e;
|
|
215
230
|
if (pe.timestamp < since) return false;
|
|
@@ -221,7 +236,7 @@ var EventStore = class {
|
|
|
221
236
|
const since = filter.sinceSeconds ? Date.now() - filter.sinceSeconds * 1e3 : 0;
|
|
222
237
|
return this.buffer.query((e) => {
|
|
223
238
|
if (e.eventType !== "database") return false;
|
|
224
|
-
if (filter.sessionId && e.sessionId
|
|
239
|
+
if (filter.sessionId && !matchesSessionFilter(e.sessionId, filter.sessionId)) return false;
|
|
225
240
|
if (filter.projectId && !this.matchesProjectId(e.sessionId, filter.projectId)) return false;
|
|
226
241
|
const de = e;
|
|
227
242
|
if (de.timestamp < since) return false;
|
|
@@ -243,7 +258,7 @@ var EventStore = class {
|
|
|
243
258
|
const since = filter.sinceSeconds ? Date.now() - filter.sinceSeconds * 1e3 : 0;
|
|
244
259
|
return this.buffer.query((e) => {
|
|
245
260
|
if (e.eventType !== "custom") return false;
|
|
246
|
-
if (filter.sessionId && e.sessionId
|
|
261
|
+
if (filter.sessionId && !matchesSessionFilter(e.sessionId, filter.sessionId)) return false;
|
|
247
262
|
if (filter.projectId && !this.matchesProjectId(e.sessionId, filter.projectId)) return false;
|
|
248
263
|
const ce = e;
|
|
249
264
|
if (ce.timestamp < since) return false;
|
|
@@ -255,7 +270,7 @@ var EventStore = class {
|
|
|
255
270
|
const since = filter.sinceSeconds ? Date.now() - filter.sinceSeconds * 1e3 : 0;
|
|
256
271
|
return this.buffer.query((e) => {
|
|
257
272
|
if (e.eventType !== "ui") return false;
|
|
258
|
-
if (filter.sessionId && e.sessionId
|
|
273
|
+
if (filter.sessionId && !matchesSessionFilter(e.sessionId, filter.sessionId)) return false;
|
|
259
274
|
if (filter.projectId && !this.matchesProjectId(e.sessionId, filter.projectId)) return false;
|
|
260
275
|
const ue = e;
|
|
261
276
|
if (ue.timestamp < since) return false;
|
|
@@ -270,7 +285,7 @@ var EventStore = class {
|
|
|
270
285
|
const since = filter.sinceSeconds ? Date.now() - filter.sinceSeconds * 1e3 : 0;
|
|
271
286
|
const results = this.buffer.query((e) => {
|
|
272
287
|
if (e.eventType !== eventType) return false;
|
|
273
|
-
if (filter.sessionId && e.sessionId
|
|
288
|
+
if (filter.sessionId && !matchesSessionFilter(e.sessionId, filter.sessionId)) return false;
|
|
274
289
|
if (filter.projectId && !this.matchesProjectId(e.sessionId, filter.projectId)) return false;
|
|
275
290
|
if (e.timestamp < since) return false;
|
|
276
291
|
if (filter.url) {
|
|
@@ -285,7 +300,7 @@ var EventStore = class {
|
|
|
285
300
|
const since = filter.sinceSeconds ? Date.now() - filter.sinceSeconds * 1e3 : 0;
|
|
286
301
|
return this.buffer.query((e) => {
|
|
287
302
|
if (e.eventType !== eventType) return false;
|
|
288
|
-
if (filter.sessionId && e.sessionId
|
|
303
|
+
if (filter.sessionId && !matchesSessionFilter(e.sessionId, filter.sessionId)) return false;
|
|
289
304
|
if (filter.projectId && !this.matchesProjectId(e.sessionId, filter.projectId)) return false;
|
|
290
305
|
if (e.timestamp < since) return false;
|
|
291
306
|
if (filter.url) {
|
|
@@ -352,6 +367,18 @@ function getOrCreateProjectId(projectManager, appName) {
|
|
|
352
367
|
projectManager.setProjectIdForApp(appName, projectId);
|
|
353
368
|
return projectId;
|
|
354
369
|
}
|
|
370
|
+
function resolveProjectId(projectManager, appName, pmStore) {
|
|
371
|
+
const fromIndex = projectManager.resolveAppProjectId(appName);
|
|
372
|
+
if (fromIndex) return fromIndex;
|
|
373
|
+
if (pmStore) {
|
|
374
|
+
const fromPm = pmStore.findProjectIdByApp(appName);
|
|
375
|
+
if (fromPm) {
|
|
376
|
+
projectManager.setProjectIdForApp(appName, fromPm);
|
|
377
|
+
return fromPm;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return getOrCreateProjectId(projectManager, appName);
|
|
381
|
+
}
|
|
355
382
|
|
|
356
383
|
// src/sqlite-store.ts
|
|
357
384
|
import { renameSync, existsSync } from "fs";
|
|
@@ -855,6 +882,7 @@ var CollectorServer = class {
|
|
|
855
882
|
pruneTimer = null;
|
|
856
883
|
heartbeatTimer = null;
|
|
857
884
|
tlsConfig = null;
|
|
885
|
+
pmStore = null;
|
|
858
886
|
constructor(options = {}) {
|
|
859
887
|
this.store = new EventStore(options.bufferSize ?? 1e4);
|
|
860
888
|
this.projectManager = options.projectManager ?? null;
|
|
@@ -890,6 +918,10 @@ var CollectorServer = class {
|
|
|
890
918
|
getRateLimiter() {
|
|
891
919
|
return this.rateLimiter;
|
|
892
920
|
}
|
|
921
|
+
/** Set the PmStore for project ID resolution (called after construction when PmStore is available). */
|
|
922
|
+
setPmStore(pmStore) {
|
|
923
|
+
this.pmStore = pmStore;
|
|
924
|
+
}
|
|
893
925
|
onConnect(cb) {
|
|
894
926
|
this.connectCallbacks.push(cb);
|
|
895
927
|
}
|
|
@@ -1074,7 +1106,7 @@ var CollectorServer = class {
|
|
|
1074
1106
|
this.pendingHandshakes.delete(ws);
|
|
1075
1107
|
}
|
|
1076
1108
|
const projectName = payload.appName;
|
|
1077
|
-
const projectId = payload.projectId ?? (this.projectManager ?
|
|
1109
|
+
const projectId = payload.projectId ?? (this.projectManager ? resolveProjectId(this.projectManager, projectName, this.pmStore) : void 0);
|
|
1078
1110
|
this.clients.set(ws, {
|
|
1079
1111
|
sessionId: payload.sessionId,
|
|
1080
1112
|
projectName,
|
|
@@ -1247,6 +1279,7 @@ var DEFAULT_GLOBAL_CONFIG = {
|
|
|
1247
1279
|
};
|
|
1248
1280
|
var ProjectManager = class {
|
|
1249
1281
|
baseDir;
|
|
1282
|
+
appProjectIndex = /* @__PURE__ */ new Map();
|
|
1250
1283
|
constructor(baseDir) {
|
|
1251
1284
|
this.baseDir = baseDir ?? join(homedir(), ".runtimescope");
|
|
1252
1285
|
}
|
|
@@ -1359,6 +1392,60 @@ var ProjectManager = class {
|
|
|
1359
1392
|
}
|
|
1360
1393
|
return null;
|
|
1361
1394
|
}
|
|
1395
|
+
// --- Reverse index: appName → projectId ---
|
|
1396
|
+
/**
|
|
1397
|
+
* Build reverse index: appName -> projectId.
|
|
1398
|
+
* Scans all project configs, PM projects with runtimeApps + runtimeProjectId,
|
|
1399
|
+
* and project-level .runtimescope/config.json files from PM project paths.
|
|
1400
|
+
*/
|
|
1401
|
+
rebuildAppIndex(pmStore) {
|
|
1402
|
+
this.appProjectIndex.clear();
|
|
1403
|
+
for (const name of this.listProjects()) {
|
|
1404
|
+
const config = this.getProjectConfig(name);
|
|
1405
|
+
if (config?.projectId) {
|
|
1406
|
+
this.appProjectIndex.set(name.toLowerCase(), config.projectId);
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
if (pmStore) {
|
|
1410
|
+
for (const p of pmStore.listProjects()) {
|
|
1411
|
+
if (p.runtimeProjectId && p.runtimeApps) {
|
|
1412
|
+
for (const app of p.runtimeApps) {
|
|
1413
|
+
this.appProjectIndex.set(app.toLowerCase(), p.runtimeProjectId);
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
if (pmStore) {
|
|
1419
|
+
for (const p of pmStore.listProjects()) {
|
|
1420
|
+
if (p.path) {
|
|
1421
|
+
try {
|
|
1422
|
+
const configPath = join(p.path, ".runtimescope", "config.json");
|
|
1423
|
+
if (existsSync2(configPath)) {
|
|
1424
|
+
const content = readFileSync2(configPath, "utf-8");
|
|
1425
|
+
const config = JSON.parse(content);
|
|
1426
|
+
if (config.projectId) {
|
|
1427
|
+
if (config.appName) {
|
|
1428
|
+
this.appProjectIndex.set(config.appName.toLowerCase(), config.projectId);
|
|
1429
|
+
}
|
|
1430
|
+
if (Array.isArray(config.sdks)) {
|
|
1431
|
+
for (const sdk of config.sdks) {
|
|
1432
|
+
if (sdk.appName) {
|
|
1433
|
+
this.appProjectIndex.set(sdk.appName.toLowerCase(), config.projectId);
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
} catch {
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
/** O(1) lookup from the cached index. */
|
|
1446
|
+
resolveAppProjectId(appName) {
|
|
1447
|
+
return this.appProjectIndex.get(appName.toLowerCase()) ?? null;
|
|
1448
|
+
}
|
|
1362
1449
|
// --- Environment variable resolution ---
|
|
1363
1450
|
resolveEnvVars(value) {
|
|
1364
1451
|
return value.replace(/\$\{([^}]+)\}/g, (_match, varName) => {
|
|
@@ -1412,6 +1499,129 @@ var ProjectManager = class {
|
|
|
1412
1499
|
}
|
|
1413
1500
|
};
|
|
1414
1501
|
|
|
1502
|
+
// src/project-config.ts
|
|
1503
|
+
import { readFileSync as readFileSync3, existsSync as existsSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
|
|
1504
|
+
import { join as join2 } from "path";
|
|
1505
|
+
var DEFAULT_CAPTURE = {
|
|
1506
|
+
network: true,
|
|
1507
|
+
console: true,
|
|
1508
|
+
xhr: true,
|
|
1509
|
+
body: false,
|
|
1510
|
+
performance: true,
|
|
1511
|
+
renders: true,
|
|
1512
|
+
navigation: true,
|
|
1513
|
+
clicks: false,
|
|
1514
|
+
http: false,
|
|
1515
|
+
errors: true,
|
|
1516
|
+
stackTraces: false
|
|
1517
|
+
};
|
|
1518
|
+
function readProjectConfig(projectDir) {
|
|
1519
|
+
const configPath = join2(projectDir, ".runtimescope", "config.json");
|
|
1520
|
+
if (!existsSync3(configPath)) return null;
|
|
1521
|
+
try {
|
|
1522
|
+
const content = readFileSync3(configPath, "utf-8");
|
|
1523
|
+
return JSON.parse(content);
|
|
1524
|
+
} catch {
|
|
1525
|
+
return null;
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
function writeProjectConfig(projectDir, config) {
|
|
1529
|
+
const dir = join2(projectDir, ".runtimescope");
|
|
1530
|
+
if (!existsSync3(dir)) mkdirSync2(dir, { recursive: true });
|
|
1531
|
+
writeFileSync2(join2(dir, "config.json"), JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
1532
|
+
}
|
|
1533
|
+
function scaffoldProjectConfig(projectDir, opts) {
|
|
1534
|
+
const existing = readProjectConfig(projectDir);
|
|
1535
|
+
if (existing) {
|
|
1536
|
+
if (opts.sdkType) {
|
|
1537
|
+
const alreadyHas = existing.sdks.some((s) => s.type === opts.sdkType);
|
|
1538
|
+
if (!alreadyHas) {
|
|
1539
|
+
existing.sdks.push({
|
|
1540
|
+
type: opts.sdkType,
|
|
1541
|
+
framework: opts.framework,
|
|
1542
|
+
appName: opts.appName !== existing.appName ? opts.appName : void 0
|
|
1543
|
+
});
|
|
1544
|
+
writeProjectConfig(projectDir, existing);
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
return existing;
|
|
1548
|
+
}
|
|
1549
|
+
const projectId = generateProjectId();
|
|
1550
|
+
const httpPort = process.env.RUNTIMESCOPE_HTTP_PORT ?? "9091";
|
|
1551
|
+
const dsn = `runtimescope://${projectId}@localhost:${httpPort}/${opts.appName}`;
|
|
1552
|
+
const config = {
|
|
1553
|
+
projectId,
|
|
1554
|
+
dsn,
|
|
1555
|
+
appName: opts.appName,
|
|
1556
|
+
description: opts.description,
|
|
1557
|
+
sdks: opts.sdkType ? [{ type: opts.sdkType, framework: opts.framework }] : [],
|
|
1558
|
+
capture: { ...DEFAULT_CAPTURE },
|
|
1559
|
+
category: opts.category
|
|
1560
|
+
};
|
|
1561
|
+
writeProjectConfig(projectDir, config);
|
|
1562
|
+
const gitignorePath = join2(projectDir, ".runtimescope", ".gitignore");
|
|
1563
|
+
if (!existsSync3(gitignorePath)) {
|
|
1564
|
+
writeFileSync2(gitignorePath, "# Keep config.json committed, ignore local state\n*.log\n*.db\n.env\n", "utf-8");
|
|
1565
|
+
}
|
|
1566
|
+
return config;
|
|
1567
|
+
}
|
|
1568
|
+
function resolveProjectAppNames(config) {
|
|
1569
|
+
const names = /* @__PURE__ */ new Set([config.appName]);
|
|
1570
|
+
for (const sdk of config.sdks) {
|
|
1571
|
+
if (sdk.appName) names.add(sdk.appName);
|
|
1572
|
+
}
|
|
1573
|
+
return Array.from(names);
|
|
1574
|
+
}
|
|
1575
|
+
function migrateProjectIds(projectManager, pmStore) {
|
|
1576
|
+
const result = { unified: 0, skipped: 0, details: [] };
|
|
1577
|
+
if (!pmStore) return result;
|
|
1578
|
+
for (const project of pmStore.listProjects()) {
|
|
1579
|
+
if (!project.runtimeApps || project.runtimeApps.length < 2) continue;
|
|
1580
|
+
if (!project.path) {
|
|
1581
|
+
result.skipped++;
|
|
1582
|
+
continue;
|
|
1583
|
+
}
|
|
1584
|
+
let canonicalId = null;
|
|
1585
|
+
try {
|
|
1586
|
+
const configPath = join2(project.path, ".runtimescope", "config.json");
|
|
1587
|
+
if (existsSync3(configPath)) {
|
|
1588
|
+
const config = JSON.parse(readFileSync3(configPath, "utf-8"));
|
|
1589
|
+
if (config.projectId) canonicalId = config.projectId;
|
|
1590
|
+
}
|
|
1591
|
+
} catch {
|
|
1592
|
+
}
|
|
1593
|
+
if (!canonicalId && project.runtimeProjectId) {
|
|
1594
|
+
canonicalId = project.runtimeProjectId;
|
|
1595
|
+
}
|
|
1596
|
+
if (!canonicalId) {
|
|
1597
|
+
for (const appName of project.runtimeApps) {
|
|
1598
|
+
const existing = projectManager.getProjectIdForApp(appName);
|
|
1599
|
+
if (existing) {
|
|
1600
|
+
canonicalId = existing;
|
|
1601
|
+
break;
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
if (!canonicalId) {
|
|
1606
|
+
result.skipped++;
|
|
1607
|
+
continue;
|
|
1608
|
+
}
|
|
1609
|
+
for (const appName of project.runtimeApps) {
|
|
1610
|
+
const current = projectManager.getProjectIdForApp(appName);
|
|
1611
|
+
if (current !== canonicalId) {
|
|
1612
|
+
projectManager.setProjectIdForApp(appName, canonicalId);
|
|
1613
|
+
result.details.push(`Unified ${appName}: ${current ?? "none"} \u2192 ${canonicalId}`);
|
|
1614
|
+
result.unified++;
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
if (project.runtimeProjectId !== canonicalId) {
|
|
1618
|
+
pmStore.updateProject(project.id, { runtimeProjectId: canonicalId });
|
|
1619
|
+
result.details.push(`PM project ${project.name}: runtimeProjectId \u2192 ${canonicalId}`);
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
return result;
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1415
1625
|
// src/auth.ts
|
|
1416
1626
|
import { randomBytes as randomBytes2, timingSafeEqual } from "crypto";
|
|
1417
1627
|
var AuthManager = class {
|
|
@@ -1576,7 +1786,7 @@ var Redactor = class {
|
|
|
1576
1786
|
// src/platform.ts
|
|
1577
1787
|
import { execFileSync, execSync } from "child_process";
|
|
1578
1788
|
import { readlinkSync, readdirSync as readdirSync2 } from "fs";
|
|
1579
|
-
import { join as
|
|
1789
|
+
import { join as join3 } from "path";
|
|
1580
1790
|
var IS_WIN = process.platform === "win32";
|
|
1581
1791
|
var IS_LINUX = process.platform === "linux";
|
|
1582
1792
|
function runFile(cmd, args, timeoutMs = 5e3) {
|
|
@@ -1695,7 +1905,7 @@ function findPidsInDir_linux(dir) {
|
|
|
1695
1905
|
const pid = parseInt(entry, 10);
|
|
1696
1906
|
if (isNaN(pid) || pid <= 1) continue;
|
|
1697
1907
|
try {
|
|
1698
|
-
const cwd = readlinkSync(
|
|
1908
|
+
const cwd = readlinkSync(join3("/proc", entry, "cwd"));
|
|
1699
1909
|
if (cwd.startsWith(dir)) pids.push(pid);
|
|
1700
1910
|
} catch {
|
|
1701
1911
|
}
|
|
@@ -1899,15 +2109,15 @@ var SessionManager = class {
|
|
|
1899
2109
|
// src/http-server.ts
|
|
1900
2110
|
import { createServer } from "http";
|
|
1901
2111
|
import { createServer as createHttpsServer2 } from "https";
|
|
1902
|
-
import { readFileSync as
|
|
2112
|
+
import { readFileSync as readFileSync4, existsSync as existsSync5 } from "fs";
|
|
1903
2113
|
import { resolve, dirname } from "path";
|
|
1904
2114
|
import { fileURLToPath } from "url";
|
|
1905
2115
|
import { WebSocketServer as WebSocketServer2 } from "ws";
|
|
1906
2116
|
|
|
1907
2117
|
// src/pm/pm-routes.ts
|
|
1908
2118
|
import { readdir, readFile, writeFile, unlink, mkdir } from "fs/promises";
|
|
1909
|
-
import { existsSync as
|
|
1910
|
-
import { join as
|
|
2119
|
+
import { existsSync as existsSync4 } from "fs";
|
|
2120
|
+
import { join as join4 } from "path";
|
|
1911
2121
|
import { homedir as homedir2 } from "os";
|
|
1912
2122
|
import { spawn, execFileSync as execFileSync2 } from "child_process";
|
|
1913
2123
|
var LOG_RING_SIZE = 500;
|
|
@@ -2044,6 +2254,16 @@ function createPmRouter(pmStore, discovery, helpers, broadcastDevServer) {
|
|
|
2044
2254
|
helpers.json(res, { error: err.message }, 400);
|
|
2045
2255
|
}
|
|
2046
2256
|
});
|
|
2257
|
+
route("DELETE", "/api/pm/projects/:id", (_req, res, params) => {
|
|
2258
|
+
const id = params.get("id");
|
|
2259
|
+
const project = pmStore.getProject(id);
|
|
2260
|
+
if (!project) {
|
|
2261
|
+
helpers.json(res, { error: "Project not found" }, 404);
|
|
2262
|
+
return;
|
|
2263
|
+
}
|
|
2264
|
+
pmStore.deleteProject(id);
|
|
2265
|
+
helpers.json(res, { ok: true, deleted: project.name });
|
|
2266
|
+
});
|
|
2047
2267
|
route("GET", "/api/pm/tasks", (_req, res, params) => {
|
|
2048
2268
|
const projectId = params.get("project_id") ?? void 0;
|
|
2049
2269
|
const status = params.get("status") ?? void 0;
|
|
@@ -2219,13 +2439,13 @@ function createPmRouter(pmStore, discovery, helpers, broadcastDevServer) {
|
|
|
2219
2439
|
helpers.json(res, { data: [], count: 0 });
|
|
2220
2440
|
return;
|
|
2221
2441
|
}
|
|
2222
|
-
const memoryDir =
|
|
2442
|
+
const memoryDir = join4(homedir2(), ".claude", "projects", project.claudeProjectKey, "memory");
|
|
2223
2443
|
try {
|
|
2224
2444
|
const files = await readdir(memoryDir);
|
|
2225
2445
|
const mdFiles = files.filter((f) => f.endsWith(".md"));
|
|
2226
2446
|
const result = await Promise.all(
|
|
2227
2447
|
mdFiles.map(async (filename) => {
|
|
2228
|
-
const content = await readFile(
|
|
2448
|
+
const content = await readFile(join4(memoryDir, filename), "utf-8");
|
|
2229
2449
|
return { filename, content, sizeBytes: Buffer.byteLength(content) };
|
|
2230
2450
|
})
|
|
2231
2451
|
);
|
|
@@ -2242,7 +2462,7 @@ function createPmRouter(pmStore, discovery, helpers, broadcastDevServer) {
|
|
|
2242
2462
|
helpers.json(res, { error: "Project not found" }, 404);
|
|
2243
2463
|
return;
|
|
2244
2464
|
}
|
|
2245
|
-
const filePath =
|
|
2465
|
+
const filePath = join4(homedir2(), ".claude", "projects", project.claudeProjectKey, "memory", filename);
|
|
2246
2466
|
try {
|
|
2247
2467
|
const content = await readFile(filePath, "utf-8");
|
|
2248
2468
|
helpers.json(res, { filename, content, sizeBytes: Buffer.byteLength(content) });
|
|
@@ -2265,9 +2485,9 @@ function createPmRouter(pmStore, discovery, helpers, broadcastDevServer) {
|
|
|
2265
2485
|
}
|
|
2266
2486
|
try {
|
|
2267
2487
|
const { content } = JSON.parse(body);
|
|
2268
|
-
const memoryDir =
|
|
2488
|
+
const memoryDir = join4(homedir2(), ".claude", "projects", project.claudeProjectKey, "memory");
|
|
2269
2489
|
await mkdir(memoryDir, { recursive: true });
|
|
2270
|
-
await writeFile(
|
|
2490
|
+
await writeFile(join4(memoryDir, filename), content, "utf-8");
|
|
2271
2491
|
helpers.json(res, { ok: true });
|
|
2272
2492
|
} catch (err) {
|
|
2273
2493
|
helpers.json(res, { error: err.message }, 500);
|
|
@@ -2281,7 +2501,7 @@ function createPmRouter(pmStore, discovery, helpers, broadcastDevServer) {
|
|
|
2281
2501
|
helpers.json(res, { error: "Project not found" }, 404);
|
|
2282
2502
|
return;
|
|
2283
2503
|
}
|
|
2284
|
-
const filePath =
|
|
2504
|
+
const filePath = join4(homedir2(), ".claude", "projects", project.claudeProjectKey, "memory", filename);
|
|
2285
2505
|
try {
|
|
2286
2506
|
await unlink(filePath);
|
|
2287
2507
|
helpers.json(res, { ok: true });
|
|
@@ -2341,7 +2561,7 @@ function createPmRouter(pmStore, discovery, helpers, broadcastDevServer) {
|
|
|
2341
2561
|
const { content } = JSON.parse(body);
|
|
2342
2562
|
const paths = getRulesPaths(project.claudeProjectKey, project.path);
|
|
2343
2563
|
const filePath = paths[scope];
|
|
2344
|
-
const dir =
|
|
2564
|
+
const dir = join4(filePath, "..");
|
|
2345
2565
|
await mkdir(dir, { recursive: true });
|
|
2346
2566
|
await writeFile(filePath, content, "utf-8");
|
|
2347
2567
|
helpers.json(res, { ok: true });
|
|
@@ -2361,7 +2581,7 @@ function createPmRouter(pmStore, discovery, helpers, broadcastDevServer) {
|
|
|
2361
2581
|
return;
|
|
2362
2582
|
}
|
|
2363
2583
|
try {
|
|
2364
|
-
const pkgPath =
|
|
2584
|
+
const pkgPath = join4(project.path, "package.json");
|
|
2365
2585
|
const pkg = JSON.parse(await readFile(pkgPath, "utf-8"));
|
|
2366
2586
|
const scripts = pkg.scripts ?? {};
|
|
2367
2587
|
const recommended = ["dev", "start", "serve"].find((s) => s in scripts) ?? null;
|
|
@@ -2757,6 +2977,106 @@ function createPmRouter(pmStore, discovery, helpers, broadcastDevServer) {
|
|
|
2757
2977
|
});
|
|
2758
2978
|
res.end(csv);
|
|
2759
2979
|
});
|
|
2980
|
+
route("GET", "/api/pm/capex-report/:projectId", async (_req, res, params) => {
|
|
2981
|
+
const projectId = params.get("projectId");
|
|
2982
|
+
const startDate = params.get("start_date") ?? void 0;
|
|
2983
|
+
const endDate = params.get("end_date") ?? void 0;
|
|
2984
|
+
try {
|
|
2985
|
+
const buffer = await pmStore.exportCapexXlsx(projectId, { startDate, endDate });
|
|
2986
|
+
res.writeHead(200, {
|
|
2987
|
+
"Content-Type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
2988
|
+
"Content-Disposition": `attachment; filename="capex-${projectId}.xlsx"`
|
|
2989
|
+
});
|
|
2990
|
+
res.end(buffer);
|
|
2991
|
+
} catch {
|
|
2992
|
+
const csv = pmStore.exportCapexCsv(projectId, { startDate, endDate });
|
|
2993
|
+
res.writeHead(200, {
|
|
2994
|
+
"Content-Type": "text/csv",
|
|
2995
|
+
"Content-Disposition": `attachment; filename="capex-${projectId}.csv"`
|
|
2996
|
+
});
|
|
2997
|
+
res.end(csv);
|
|
2998
|
+
}
|
|
2999
|
+
});
|
|
3000
|
+
route("GET", "/api/pm/capex-all", (_req, res, params) => {
|
|
3001
|
+
const category = params.get("category") ?? void 0;
|
|
3002
|
+
const projects = pmStore.listProjects();
|
|
3003
|
+
const filteredProjects = category ? projects.filter((p) => p.category === category) : projects;
|
|
3004
|
+
const allEntries = [];
|
|
3005
|
+
let totalCost = 0;
|
|
3006
|
+
let totalCapitalizable = 0;
|
|
3007
|
+
let totalExpensed = 0;
|
|
3008
|
+
let totalMinutes = 0;
|
|
3009
|
+
let totalConfirmed = 0;
|
|
3010
|
+
let totalUnconfirmed = 0;
|
|
3011
|
+
const byProject = [];
|
|
3012
|
+
for (const project of filteredProjects) {
|
|
3013
|
+
const entries = pmStore.listCapexEntries(project.id);
|
|
3014
|
+
let projCost = 0;
|
|
3015
|
+
let projCap = 0;
|
|
3016
|
+
let projExp = 0;
|
|
3017
|
+
let projMins = 0;
|
|
3018
|
+
let projConfirmed = 0;
|
|
3019
|
+
for (const e of entries) {
|
|
3020
|
+
allEntries.push({ ...e, projectName: project.name });
|
|
3021
|
+
projCost += e.adjustedCostMicrodollars;
|
|
3022
|
+
projMins += e.activeMinutes;
|
|
3023
|
+
if (e.classification === "capitalizable") projCap += e.adjustedCostMicrodollars;
|
|
3024
|
+
else projExp += e.adjustedCostMicrodollars;
|
|
3025
|
+
if (e.confirmed) projConfirmed++;
|
|
3026
|
+
}
|
|
3027
|
+
if (entries.length > 0) {
|
|
3028
|
+
byProject.push({
|
|
3029
|
+
projectId: project.id,
|
|
3030
|
+
projectName: project.name,
|
|
3031
|
+
category: project.category,
|
|
3032
|
+
totalCost: projCost,
|
|
3033
|
+
capitalizable: projCap,
|
|
3034
|
+
expensed: projExp,
|
|
3035
|
+
activeMinutes: projMins,
|
|
3036
|
+
activeHours: +(projMins / 60).toFixed(2),
|
|
3037
|
+
confirmed: projConfirmed,
|
|
3038
|
+
total: entries.length
|
|
3039
|
+
});
|
|
3040
|
+
}
|
|
3041
|
+
totalCost += projCost;
|
|
3042
|
+
totalCapitalizable += projCap;
|
|
3043
|
+
totalExpensed += projExp;
|
|
3044
|
+
totalMinutes += projMins;
|
|
3045
|
+
totalConfirmed += projConfirmed;
|
|
3046
|
+
totalUnconfirmed += entries.length - projConfirmed;
|
|
3047
|
+
}
|
|
3048
|
+
helpers.json(res, {
|
|
3049
|
+
data: {
|
|
3050
|
+
summary: {
|
|
3051
|
+
totalCost,
|
|
3052
|
+
capitalizable: totalCapitalizable,
|
|
3053
|
+
expensed: totalExpensed,
|
|
3054
|
+
activeMinutes: totalMinutes,
|
|
3055
|
+
activeHours: +(totalMinutes / 60).toFixed(2),
|
|
3056
|
+
confirmed: totalConfirmed,
|
|
3057
|
+
unconfirmed: totalUnconfirmed,
|
|
3058
|
+
projectCount: byProject.length
|
|
3059
|
+
},
|
|
3060
|
+
byProject,
|
|
3061
|
+
entries: allEntries.sort((a, b) => b.createdAt - a.createdAt)
|
|
3062
|
+
}
|
|
3063
|
+
});
|
|
3064
|
+
});
|
|
3065
|
+
route("GET", "/api/pm/capex-report-all", async (_req, res, params) => {
|
|
3066
|
+
const startDate = params.get("start_date") ?? void 0;
|
|
3067
|
+
const endDate = params.get("end_date") ?? void 0;
|
|
3068
|
+
const category = params.get("category") ?? void 0;
|
|
3069
|
+
try {
|
|
3070
|
+
const buffer = await pmStore.exportCapexXlsxAll({ startDate, endDate, category });
|
|
3071
|
+
res.writeHead(200, {
|
|
3072
|
+
"Content-Type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
3073
|
+
"Content-Disposition": 'attachment; filename="capex-all-projects.xlsx"'
|
|
3074
|
+
});
|
|
3075
|
+
res.end(buffer);
|
|
3076
|
+
} catch (err) {
|
|
3077
|
+
helpers.json(res, { error: err.message }, 500);
|
|
3078
|
+
}
|
|
3079
|
+
});
|
|
2760
3080
|
return {
|
|
2761
3081
|
match(method, pathname) {
|
|
2762
3082
|
const pathSegments = pathname.split("/");
|
|
@@ -2787,9 +3107,9 @@ function sanitizeFilename(name) {
|
|
|
2787
3107
|
function getRulesPaths(claudeProjectKey, projectPath) {
|
|
2788
3108
|
const home = homedir2();
|
|
2789
3109
|
return {
|
|
2790
|
-
global:
|
|
2791
|
-
project: claudeProjectKey ?
|
|
2792
|
-
local: projectPath ?
|
|
3110
|
+
global: join4(home, ".claude", "CLAUDE.md"),
|
|
3111
|
+
project: claudeProjectKey ? join4(home, ".claude", "projects", claudeProjectKey, "CLAUDE.md") : join4(projectPath ?? "", ".claude", "CLAUDE.md"),
|
|
3112
|
+
local: projectPath ? join4(projectPath, "CLAUDE.md") : join4(home, "CLAUDE.md")
|
|
2793
3113
|
};
|
|
2794
3114
|
}
|
|
2795
3115
|
function execGit(args, cwd) {
|
|
@@ -2834,7 +3154,7 @@ function parseGitStatus(porcelain) {
|
|
|
2834
3154
|
}
|
|
2835
3155
|
async function readRuleFile(filePath) {
|
|
2836
3156
|
try {
|
|
2837
|
-
if (
|
|
3157
|
+
if (existsSync4(filePath)) {
|
|
2838
3158
|
const content = await readFile(filePath, "utf-8");
|
|
2839
3159
|
return { path: filePath, content, exists: true };
|
|
2840
3160
|
}
|
|
@@ -3079,7 +3399,7 @@ var HttpServer = class {
|
|
|
3079
3399
|
return;
|
|
3080
3400
|
}
|
|
3081
3401
|
const payload = parsed;
|
|
3082
|
-
const projectId = typeof payload.projectId === "string" ? payload.projectId : payload.appName && this.projectManager ?
|
|
3402
|
+
const projectId = typeof payload.projectId === "string" ? payload.projectId : payload.appName && this.projectManager ? resolveProjectId(this.projectManager, payload.appName, this.pmStore) : void 0;
|
|
3083
3403
|
if (!payload.sessionId || !Array.isArray(payload.events) || payload.events.length === 0) {
|
|
3084
3404
|
this.json(res, {
|
|
3085
3405
|
error: "Required: sessionId (string), events (non-empty array)",
|
|
@@ -3164,7 +3484,7 @@ var HttpServer = class {
|
|
|
3164
3484
|
// npm installed
|
|
3165
3485
|
];
|
|
3166
3486
|
for (const p of candidates) {
|
|
3167
|
-
if (
|
|
3487
|
+
if (existsSync5(p)) {
|
|
3168
3488
|
this.sdkBundlePath = p;
|
|
3169
3489
|
return p;
|
|
3170
3490
|
}
|
|
@@ -3312,7 +3632,7 @@ var HttpServer = class {
|
|
|
3312
3632
|
if (req.method === "GET" && url.pathname === "/runtimescope.js") {
|
|
3313
3633
|
const sdkPath = this.resolveSdkPath();
|
|
3314
3634
|
if (sdkPath) {
|
|
3315
|
-
const bundle =
|
|
3635
|
+
const bundle = readFileSync4(sdkPath, "utf-8");
|
|
3316
3636
|
res.writeHead(200, {
|
|
3317
3637
|
"Content-Type": "application/javascript",
|
|
3318
3638
|
"Cache-Control": "no-cache"
|
|
@@ -3326,14 +3646,12 @@ var HttpServer = class {
|
|
|
3326
3646
|
}
|
|
3327
3647
|
if (req.method === "GET" && url.pathname === "/snippet") {
|
|
3328
3648
|
const appName = (url.searchParams.get("app") || "my-app").replace(/[^a-zA-Z0-9_-]/g, "");
|
|
3329
|
-
const
|
|
3649
|
+
const projectId = url.searchParams.get("project_id") || "proj_xxx";
|
|
3650
|
+
const dsn = `runtimescope://${projectId}@localhost:${this.activePort}/${appName}`;
|
|
3330
3651
|
const snippet = `<!-- RuntimeScope SDK \u2014 paste before </body> -->
|
|
3331
3652
|
<script src="http://localhost:${this.activePort}/runtimescope.js"></script>
|
|
3332
3653
|
<script>
|
|
3333
|
-
RuntimeScope.init({
|
|
3334
|
-
appName: '${appName}',
|
|
3335
|
-
endpoint: 'ws://localhost:${wsPort}',
|
|
3336
|
-
});
|
|
3654
|
+
RuntimeScope.init({ dsn: '${dsn}' });
|
|
3337
3655
|
</script>`;
|
|
3338
3656
|
res.writeHead(200, {
|
|
3339
3657
|
"Content-Type": "text/plain"
|
|
@@ -3563,6 +3881,13 @@ var PmStore = class {
|
|
|
3563
3881
|
CREATE INDEX IF NOT EXISTS idx_pm_capex_project ON pm_capex_entries(project_id);
|
|
3564
3882
|
CREATE INDEX IF NOT EXISTS idx_pm_capex_period ON pm_capex_entries(period);
|
|
3565
3883
|
CREATE INDEX IF NOT EXISTS idx_pm_capex_confirmed ON pm_capex_entries(confirmed);
|
|
3884
|
+
|
|
3885
|
+
CREATE TABLE IF NOT EXISTS pm_deleted_projects (
|
|
3886
|
+
path TEXT PRIMARY KEY,
|
|
3887
|
+
name TEXT,
|
|
3888
|
+
deleted_at INTEGER NOT NULL
|
|
3889
|
+
);
|
|
3890
|
+
CREATE INDEX IF NOT EXISTS idx_deleted_path ON pm_deleted_projects(path);
|
|
3566
3891
|
`);
|
|
3567
3892
|
}
|
|
3568
3893
|
runMigrations() {
|
|
@@ -3730,6 +4055,11 @@ var PmStore = class {
|
|
|
3730
4055
|
this.updateProject(match.id, updates);
|
|
3731
4056
|
return match.id;
|
|
3732
4057
|
}
|
|
4058
|
+
/** Find a project's runtimeProjectId by checking if appName appears in any project's runtimeApps. */
|
|
4059
|
+
findProjectIdByApp(appName) {
|
|
4060
|
+
const row = this.db.prepare(`SELECT runtime_project_id FROM pm_projects WHERE runtime_apps LIKE ? AND runtime_project_id IS NOT NULL LIMIT 1`).get(`%"${appName}"%`);
|
|
4061
|
+
return row?.runtime_project_id ?? null;
|
|
4062
|
+
}
|
|
3733
4063
|
listCategories() {
|
|
3734
4064
|
const rows = this.db.prepare("SELECT DISTINCT category FROM pm_projects WHERE category IS NOT NULL ORDER BY category ASC").all();
|
|
3735
4065
|
return rows.map((r) => r.category);
|
|
@@ -4345,6 +4675,129 @@ var PmStore = class {
|
|
|
4345
4675
|
});
|
|
4346
4676
|
return [headers.join(","), ...rows].join("\n");
|
|
4347
4677
|
}
|
|
4678
|
+
async exportCapexXlsx(projectId, opts) {
|
|
4679
|
+
const ExcelJS = await import("./excel-3DUFQWCD.js");
|
|
4680
|
+
const workbook = new ExcelJS.Workbook();
|
|
4681
|
+
const project = this.getProject(projectId);
|
|
4682
|
+
const summary = this.getCapexSummary(projectId, opts);
|
|
4683
|
+
const entries = this.listCapexEntries(projectId, { month: opts?.startDate });
|
|
4684
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
4685
|
+
for (const entry of entries) {
|
|
4686
|
+
if (!sessions.has(entry.sessionId)) {
|
|
4687
|
+
const s = this.getSession(entry.sessionId);
|
|
4688
|
+
if (s) sessions.set(entry.sessionId, s);
|
|
4689
|
+
}
|
|
4690
|
+
}
|
|
4691
|
+
const summarySheet = workbook.addWorksheet("Summary");
|
|
4692
|
+
const summaryData = [
|
|
4693
|
+
["Project", project?.name ?? projectId],
|
|
4694
|
+
["Phase", project?.phase ?? ""],
|
|
4695
|
+
["Status", project?.projectStatus ?? ""],
|
|
4696
|
+
["Total Active Hours", (summary.totalActiveMinutes / 60).toFixed(2)],
|
|
4697
|
+
["Total Cost", `$${(summary.totalCostMicrodollars / 1e6).toFixed(2)}`],
|
|
4698
|
+
["Capitalizable", `$${(summary.capitalizableCostMicrodollars / 1e6).toFixed(2)}`],
|
|
4699
|
+
["Expensed", `$${(summary.expensedCostMicrodollars / 1e6).toFixed(2)}`],
|
|
4700
|
+
["Sessions", String(summary.totalSessions)],
|
|
4701
|
+
["Confirmed", `${summary.confirmedCount}/${summary.totalSessions}`]
|
|
4702
|
+
];
|
|
4703
|
+
summaryData.forEach(([field, value]) => {
|
|
4704
|
+
const row = summarySheet.addRow([field, value]);
|
|
4705
|
+
row.getCell(1).font = { bold: true };
|
|
4706
|
+
});
|
|
4707
|
+
summarySheet.getColumn(1).width = 20;
|
|
4708
|
+
summarySheet.getColumn(2).width = 30;
|
|
4709
|
+
const detailSheet = workbook.addWorksheet("Daily Detail");
|
|
4710
|
+
detailSheet.addRow(["Date", "Session", "Model", "Active Hours", "Cost (USD)", "Classification", "Work Type", "Confirmed", "Notes"]);
|
|
4711
|
+
detailSheet.getRow(1).font = { bold: true };
|
|
4712
|
+
for (const e of entries) {
|
|
4713
|
+
const s = sessions.get(e.sessionId);
|
|
4714
|
+
const date = s?.startedAt ? new Date(s.startedAt).toISOString().split("T")[0] : e.period;
|
|
4715
|
+
detailSheet.addRow([
|
|
4716
|
+
date,
|
|
4717
|
+
s?.slug ?? e.sessionId.slice(0, 12),
|
|
4718
|
+
s?.model ?? "",
|
|
4719
|
+
Number((e.activeMinutes / 60).toFixed(2)),
|
|
4720
|
+
Number((e.costMicrodollars / 1e6).toFixed(4)),
|
|
4721
|
+
e.classification,
|
|
4722
|
+
e.workType ?? "",
|
|
4723
|
+
e.confirmed ? "Yes" : "No",
|
|
4724
|
+
e.notes ?? ""
|
|
4725
|
+
]);
|
|
4726
|
+
}
|
|
4727
|
+
detailSheet.columns.forEach((col) => {
|
|
4728
|
+
col.width = 16;
|
|
4729
|
+
});
|
|
4730
|
+
detailSheet.getColumn(1).width = 12;
|
|
4731
|
+
detailSheet.getColumn(2).width = 24;
|
|
4732
|
+
const monthlySheet = workbook.addWorksheet("Monthly Totals");
|
|
4733
|
+
monthlySheet.addRow(["Period", "Active Hours", "Capitalizable ($)", "Expensed ($)", "Total ($)"]);
|
|
4734
|
+
monthlySheet.getRow(1).font = { bold: true };
|
|
4735
|
+
for (const m of summary.byMonth) {
|
|
4736
|
+
monthlySheet.addRow([
|
|
4737
|
+
m.period,
|
|
4738
|
+
Number((m.activeMinutes / 60).toFixed(2)),
|
|
4739
|
+
Number((m.capitalizable / 1e6).toFixed(2)),
|
|
4740
|
+
Number((m.expensed / 1e6).toFixed(2)),
|
|
4741
|
+
Number(((m.capitalizable + m.expensed) / 1e6).toFixed(2))
|
|
4742
|
+
]);
|
|
4743
|
+
}
|
|
4744
|
+
monthlySheet.columns.forEach((col) => {
|
|
4745
|
+
col.width = 18;
|
|
4746
|
+
});
|
|
4747
|
+
return Buffer.from(await workbook.xlsx.writeBuffer());
|
|
4748
|
+
}
|
|
4749
|
+
async exportCapexXlsxAll(opts) {
|
|
4750
|
+
const ExcelJS = await import("./excel-3DUFQWCD.js");
|
|
4751
|
+
const workbook = new ExcelJS.Workbook();
|
|
4752
|
+
let projects = this.listProjects();
|
|
4753
|
+
if (opts?.category) {
|
|
4754
|
+
projects = projects.filter((p) => p.category === opts.category);
|
|
4755
|
+
}
|
|
4756
|
+
const overviewSheet = workbook.addWorksheet("Overview");
|
|
4757
|
+
overviewSheet.addRow(["Project", "Category", "Phase", "Total Hours", "Capitalizable ($)", "Expensed ($)", "Total ($)"]);
|
|
4758
|
+
overviewSheet.getRow(1).font = { bold: true };
|
|
4759
|
+
for (const p of projects) {
|
|
4760
|
+
const summary = this.getCapexSummary(p.id, opts);
|
|
4761
|
+
overviewSheet.addRow([
|
|
4762
|
+
p.name,
|
|
4763
|
+
p.category ?? "",
|
|
4764
|
+
p.phase,
|
|
4765
|
+
Number((summary.totalActiveMinutes / 60).toFixed(2)),
|
|
4766
|
+
Number((summary.capitalizableCostMicrodollars / 1e6).toFixed(2)),
|
|
4767
|
+
Number((summary.expensedCostMicrodollars / 1e6).toFixed(2)),
|
|
4768
|
+
Number((summary.totalCostMicrodollars / 1e6).toFixed(2))
|
|
4769
|
+
]);
|
|
4770
|
+
}
|
|
4771
|
+
overviewSheet.columns.forEach((col) => {
|
|
4772
|
+
col.width = 18;
|
|
4773
|
+
});
|
|
4774
|
+
overviewSheet.getColumn(1).width = 28;
|
|
4775
|
+
const allSheet = workbook.addWorksheet("All Entries");
|
|
4776
|
+
allSheet.addRow(["Project", "Date", "Session", "Active Hours", "Cost (USD)", "Classification", "Work Type"]);
|
|
4777
|
+
allSheet.getRow(1).font = { bold: true };
|
|
4778
|
+
for (const p of projects) {
|
|
4779
|
+
const entries = this.listCapexEntries(p.id, { month: opts?.startDate });
|
|
4780
|
+
for (const e of entries) {
|
|
4781
|
+
const s = this.getSession(e.sessionId);
|
|
4782
|
+
const date = s?.startedAt ? new Date(s.startedAt).toISOString().split("T")[0] : e.period;
|
|
4783
|
+
allSheet.addRow([
|
|
4784
|
+
p.name,
|
|
4785
|
+
date,
|
|
4786
|
+
s?.slug ?? e.sessionId.slice(0, 12),
|
|
4787
|
+
Number((e.activeMinutes / 60).toFixed(2)),
|
|
4788
|
+
Number((e.costMicrodollars / 1e6).toFixed(4)),
|
|
4789
|
+
e.classification,
|
|
4790
|
+
e.workType ?? ""
|
|
4791
|
+
]);
|
|
4792
|
+
}
|
|
4793
|
+
}
|
|
4794
|
+
allSheet.columns.forEach((col) => {
|
|
4795
|
+
col.width = 16;
|
|
4796
|
+
});
|
|
4797
|
+
allSheet.getColumn(1).width = 24;
|
|
4798
|
+
allSheet.getColumn(3).width = 24;
|
|
4799
|
+
return Buffer.from(await workbook.xlsx.writeBuffer());
|
|
4800
|
+
}
|
|
4348
4801
|
mapCapexRow(row) {
|
|
4349
4802
|
return {
|
|
4350
4803
|
id: row.id,
|
|
@@ -4366,6 +4819,38 @@ var PmStore = class {
|
|
|
4366
4819
|
};
|
|
4367
4820
|
}
|
|
4368
4821
|
// ============================================================
|
|
4822
|
+
// Deleted Projects Blocklist
|
|
4823
|
+
// ============================================================
|
|
4824
|
+
deleteProject(id) {
|
|
4825
|
+
const project = this.getProject(id);
|
|
4826
|
+
if (!project) return;
|
|
4827
|
+
if (project.path) {
|
|
4828
|
+
this.db.prepare(
|
|
4829
|
+
"INSERT OR REPLACE INTO pm_deleted_projects (path, name, deleted_at) VALUES (?, ?, ?)"
|
|
4830
|
+
).run(project.path, project.name, Date.now());
|
|
4831
|
+
}
|
|
4832
|
+
if (project.claudeProjectKey) {
|
|
4833
|
+
this.db.prepare(
|
|
4834
|
+
"INSERT OR REPLACE INTO pm_deleted_projects (path, name, deleted_at) VALUES (?, ?, ?)"
|
|
4835
|
+
).run(project.claudeProjectKey, project.name, Date.now());
|
|
4836
|
+
}
|
|
4837
|
+
this.db.prepare("DELETE FROM pm_capex_entries WHERE project_id = ?").run(id);
|
|
4838
|
+
this.db.prepare("DELETE FROM pm_notes WHERE project_id = ?").run(id);
|
|
4839
|
+
this.db.prepare("DELETE FROM pm_tasks WHERE project_id = ?").run(id);
|
|
4840
|
+
this.db.prepare("DELETE FROM pm_sessions WHERE project_id = ?").run(id);
|
|
4841
|
+
this.db.prepare("DELETE FROM pm_projects WHERE id = ?").run(id);
|
|
4842
|
+
}
|
|
4843
|
+
isDeletedPath(path) {
|
|
4844
|
+
const row = this.db.prepare("SELECT 1 FROM pm_deleted_projects WHERE path = ?").get(path);
|
|
4845
|
+
return !!row;
|
|
4846
|
+
}
|
|
4847
|
+
recoverProject(path) {
|
|
4848
|
+
this.db.prepare("DELETE FROM pm_deleted_projects WHERE path = ?").run(path);
|
|
4849
|
+
}
|
|
4850
|
+
listDeletedProjects() {
|
|
4851
|
+
return this.db.prepare("SELECT path, name, deleted_at as deletedAt FROM pm_deleted_projects ORDER BY deleted_at DESC").all();
|
|
4852
|
+
}
|
|
4853
|
+
// ============================================================
|
|
4369
4854
|
// Cleanup
|
|
4370
4855
|
// ============================================================
|
|
4371
4856
|
close() {
|
|
@@ -4578,13 +5063,13 @@ async function parseSessionJsonl(jsonlPath, sessionId, projectId) {
|
|
|
4578
5063
|
|
|
4579
5064
|
// src/pm/project-discovery.ts
|
|
4580
5065
|
import { readdir as readdir2, readFile as readFile2, stat as stat2 } from "fs/promises";
|
|
4581
|
-
import { join as
|
|
4582
|
-
import { existsSync as
|
|
5066
|
+
import { join as join5, basename as basename2 } from "path";
|
|
5067
|
+
import { existsSync as existsSync6 } from "fs";
|
|
4583
5068
|
import { homedir as homedir3 } from "os";
|
|
4584
5069
|
var LOG_PREFIX = "[RuntimeScope PM]";
|
|
4585
5070
|
async function detectSdkInstalled(projectPath) {
|
|
4586
5071
|
try {
|
|
4587
|
-
const pkgPath =
|
|
5072
|
+
const pkgPath = join5(projectPath, "package.json");
|
|
4588
5073
|
const pkg = JSON.parse(await readFile2(pkgPath, "utf-8"));
|
|
4589
5074
|
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
4590
5075
|
if ("@runtimescope/sdk" in allDeps || "@runtimescope/server-sdk" in allDeps) {
|
|
@@ -4594,13 +5079,13 @@ async function detectSdkInstalled(projectPath) {
|
|
|
4594
5079
|
const workspaces = Array.isArray(pkg.workspaces) ? pkg.workspaces : pkg.workspaces.packages ?? [];
|
|
4595
5080
|
for (const ws of workspaces) {
|
|
4596
5081
|
const wsBase = ws.replace(/\/?\*$/, "");
|
|
4597
|
-
const wsDir =
|
|
5082
|
+
const wsDir = join5(projectPath, wsBase);
|
|
4598
5083
|
try {
|
|
4599
5084
|
const entries = await readdir2(wsDir, { withFileTypes: true });
|
|
4600
5085
|
for (const entry of entries) {
|
|
4601
5086
|
if (!entry.isDirectory()) continue;
|
|
4602
5087
|
try {
|
|
4603
|
-
const wsPkg = JSON.parse(await readFile2(
|
|
5088
|
+
const wsPkg = JSON.parse(await readFile2(join5(wsDir, entry.name, "package.json"), "utf-8"));
|
|
4604
5089
|
const wsDeps = { ...wsPkg.dependencies, ...wsPkg.devDependencies };
|
|
4605
5090
|
if ("@runtimescope/sdk" in wsDeps || "@runtimescope/server-sdk" in wsDeps) {
|
|
4606
5091
|
return true;
|
|
@@ -4615,7 +5100,7 @@ async function detectSdkInstalled(projectPath) {
|
|
|
4615
5100
|
} catch {
|
|
4616
5101
|
}
|
|
4617
5102
|
try {
|
|
4618
|
-
await stat2(
|
|
5103
|
+
await stat2(join5(projectPath, "node_modules", "@runtimescope"));
|
|
4619
5104
|
return true;
|
|
4620
5105
|
} catch {
|
|
4621
5106
|
return false;
|
|
@@ -4646,7 +5131,7 @@ function slugifyPath(fsPath) {
|
|
|
4646
5131
|
}
|
|
4647
5132
|
function decodeClaudeKey(key) {
|
|
4648
5133
|
const naive = "/" + key.slice(1).replace(/-/g, "/");
|
|
4649
|
-
if (
|
|
5134
|
+
if (existsSync6(naive)) return naive;
|
|
4650
5135
|
const parts = key.slice(1).split("-");
|
|
4651
5136
|
return resolvePathSegments(parts);
|
|
4652
5137
|
}
|
|
@@ -4654,16 +5139,16 @@ function resolvePathSegments(parts) {
|
|
|
4654
5139
|
if (parts.length === 0) return null;
|
|
4655
5140
|
function tryResolve(prefix, remaining) {
|
|
4656
5141
|
if (remaining.length === 0) {
|
|
4657
|
-
return
|
|
5142
|
+
return existsSync6(prefix) ? prefix : null;
|
|
4658
5143
|
}
|
|
4659
5144
|
for (let count = remaining.length; count >= 1; count--) {
|
|
4660
5145
|
const segment = remaining.slice(0, count).join("-");
|
|
4661
|
-
const candidate =
|
|
5146
|
+
const candidate = join5(prefix, segment);
|
|
4662
5147
|
if (count === remaining.length) {
|
|
4663
|
-
if (
|
|
5148
|
+
if (existsSync6(candidate)) return candidate;
|
|
4664
5149
|
} else {
|
|
4665
5150
|
try {
|
|
4666
|
-
if (
|
|
5151
|
+
if (existsSync6(candidate)) {
|
|
4667
5152
|
const result = tryResolve(candidate, remaining.slice(count));
|
|
4668
5153
|
if (result) return result;
|
|
4669
5154
|
}
|
|
@@ -4679,13 +5164,14 @@ function toPeriod(timestampMs) {
|
|
|
4679
5164
|
const d = new Date(timestampMs);
|
|
4680
5165
|
const year = d.getFullYear();
|
|
4681
5166
|
const month = String(d.getMonth() + 1).padStart(2, "0");
|
|
4682
|
-
|
|
5167
|
+
const day = String(d.getDate()).padStart(2, "0");
|
|
5168
|
+
return `${year}-${month}-${day}`;
|
|
4683
5169
|
}
|
|
4684
5170
|
var ProjectDiscovery = class {
|
|
4685
5171
|
constructor(pmStore, projectManager, claudeBaseDir) {
|
|
4686
5172
|
this.pmStore = pmStore;
|
|
4687
5173
|
this.projectManager = projectManager;
|
|
4688
|
-
this.claudeBaseDir = claudeBaseDir ??
|
|
5174
|
+
this.claudeBaseDir = claudeBaseDir ?? join5(homedir3(), ".claude");
|
|
4689
5175
|
}
|
|
4690
5176
|
claudeBaseDir;
|
|
4691
5177
|
/**
|
|
@@ -4718,7 +5204,7 @@ var ProjectDiscovery = class {
|
|
|
4718
5204
|
sessionsUpdated: 0,
|
|
4719
5205
|
errors: []
|
|
4720
5206
|
};
|
|
4721
|
-
const projectsDir =
|
|
5207
|
+
const projectsDir = join5(this.claudeBaseDir, "projects");
|
|
4722
5208
|
try {
|
|
4723
5209
|
await stat2(projectsDir);
|
|
4724
5210
|
} catch {
|
|
@@ -4785,6 +5271,7 @@ var ProjectDiscovery = class {
|
|
|
4785
5271
|
await this.pmStore.upsertProject(updated);
|
|
4786
5272
|
result.projectsUpdated = (result.projectsUpdated ?? 0) + 1;
|
|
4787
5273
|
} else {
|
|
5274
|
+
if (this.pmStore.isDeletedPath(sourcePath)) continue;
|
|
4788
5275
|
const fsPath = projectDir;
|
|
4789
5276
|
const project = {
|
|
4790
5277
|
id,
|
|
@@ -4830,10 +5317,10 @@ var ProjectDiscovery = class {
|
|
|
4830
5317
|
if (!project.claudeProjectKey) {
|
|
4831
5318
|
return 0;
|
|
4832
5319
|
}
|
|
4833
|
-
const projectDir =
|
|
5320
|
+
const projectDir = join5(this.claudeBaseDir, "projects", project.claudeProjectKey);
|
|
4834
5321
|
let sessionsIndexed = 0;
|
|
4835
5322
|
try {
|
|
4836
|
-
const indexPath =
|
|
5323
|
+
const indexPath = join5(projectDir, "sessions-index.json");
|
|
4837
5324
|
let indexEntries = null;
|
|
4838
5325
|
try {
|
|
4839
5326
|
const indexContent = await readFile2(indexPath, "utf-8");
|
|
@@ -4846,7 +5333,7 @@ var ProjectDiscovery = class {
|
|
|
4846
5333
|
for (const jsonlFile of jsonlFiles) {
|
|
4847
5334
|
try {
|
|
4848
5335
|
const sessionId = jsonlFile.replace(".jsonl", "");
|
|
4849
|
-
const jsonlPath =
|
|
5336
|
+
const jsonlPath = join5(projectDir, jsonlFile);
|
|
4850
5337
|
const fileStat = await stat2(jsonlPath);
|
|
4851
5338
|
const fileSize = fileStat.size;
|
|
4852
5339
|
const existingSession = await this.pmStore.getSession(sessionId);
|
|
@@ -4881,7 +5368,7 @@ var ProjectDiscovery = class {
|
|
|
4881
5368
|
* Process a single Claude project directory key.
|
|
4882
5369
|
*/
|
|
4883
5370
|
async processClaudeProject(key, result) {
|
|
4884
|
-
const projectDir =
|
|
5371
|
+
const projectDir = join5(this.claudeBaseDir, "projects", key);
|
|
4885
5372
|
let fsPath = decodeClaudeKey(key);
|
|
4886
5373
|
if (!fsPath) {
|
|
4887
5374
|
fsPath = await this.resolvePathFromIndex(projectDir);
|
|
@@ -4907,6 +5394,8 @@ var ProjectDiscovery = class {
|
|
|
4907
5394
|
await this.pmStore.upsertProject(updated);
|
|
4908
5395
|
result.projectsUpdated = (result.projectsUpdated ?? 0) + 1;
|
|
4909
5396
|
} else {
|
|
5397
|
+
if (fsPath && this.pmStore.isDeletedPath(fsPath)) return;
|
|
5398
|
+
if (this.pmStore.isDeletedPath(key)) return;
|
|
4910
5399
|
const project = {
|
|
4911
5400
|
id,
|
|
4912
5401
|
name,
|
|
@@ -4933,7 +5422,7 @@ var ProjectDiscovery = class {
|
|
|
4933
5422
|
*/
|
|
4934
5423
|
async resolvePathFromIndex(projectDir) {
|
|
4935
5424
|
try {
|
|
4936
|
-
const indexPath =
|
|
5425
|
+
const indexPath = join5(projectDir, "sessions-index.json");
|
|
4937
5426
|
const content = await readFile2(indexPath, "utf-8");
|
|
4938
5427
|
const index = JSON.parse(content);
|
|
4939
5428
|
const entry = index.entries?.find((e) => e.projectPath);
|
|
@@ -4948,11 +5437,11 @@ var ProjectDiscovery = class {
|
|
|
4948
5437
|
*/
|
|
4949
5438
|
async indexSessionsForClaudeProject(projectId, claudeKey) {
|
|
4950
5439
|
const counts = { discovered: 0, updated: 0 };
|
|
4951
|
-
const projectDir =
|
|
5440
|
+
const projectDir = join5(this.claudeBaseDir, "projects", claudeKey);
|
|
4952
5441
|
try {
|
|
4953
5442
|
let indexEntries = null;
|
|
4954
5443
|
try {
|
|
4955
|
-
const indexPath =
|
|
5444
|
+
const indexPath = join5(projectDir, "sessions-index.json");
|
|
4956
5445
|
const indexContent = await readFile2(indexPath, "utf-8");
|
|
4957
5446
|
const index = JSON.parse(indexContent);
|
|
4958
5447
|
indexEntries = index.entries ?? [];
|
|
@@ -4963,7 +5452,7 @@ var ProjectDiscovery = class {
|
|
|
4963
5452
|
for (const jsonlFile of jsonlFiles) {
|
|
4964
5453
|
try {
|
|
4965
5454
|
const sessionId = jsonlFile.replace(".jsonl", "");
|
|
4966
|
-
const jsonlPath =
|
|
5455
|
+
const jsonlPath = join5(projectDir, jsonlFile);
|
|
4967
5456
|
const fileStat = await stat2(jsonlPath);
|
|
4968
5457
|
const fileSize = fileStat.size;
|
|
4969
5458
|
const existingSession = await this.pmStore.getSession(sessionId);
|
|
@@ -5141,7 +5630,6 @@ var ProjectDiscovery = class {
|
|
|
5141
5630
|
};
|
|
5142
5631
|
|
|
5143
5632
|
export {
|
|
5144
|
-
__require,
|
|
5145
5633
|
RingBuffer,
|
|
5146
5634
|
EventStore,
|
|
5147
5635
|
generateProjectId,
|
|
@@ -5154,6 +5642,11 @@ export {
|
|
|
5154
5642
|
resolveTlsConfig,
|
|
5155
5643
|
CollectorServer,
|
|
5156
5644
|
ProjectManager,
|
|
5645
|
+
readProjectConfig,
|
|
5646
|
+
writeProjectConfig,
|
|
5647
|
+
scaffoldProjectConfig,
|
|
5648
|
+
resolveProjectAppNames,
|
|
5649
|
+
migrateProjectIds,
|
|
5157
5650
|
AuthManager,
|
|
5158
5651
|
generateApiKey,
|
|
5159
5652
|
BUILT_IN_RULES,
|
|
@@ -5172,4 +5665,4 @@ export {
|
|
|
5172
5665
|
parseSessionJsonl,
|
|
5173
5666
|
ProjectDiscovery
|
|
5174
5667
|
};
|
|
5175
|
-
//# sourceMappingURL=chunk-
|
|
5668
|
+
//# sourceMappingURL=chunk-MM44DN7Y.js.map
|