@runtimescope/collector 0.9.2 → 0.10.0
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-GENCCHYK.js → chunk-WWFIEANS.js} +792 -73
- package/dist/chunk-WWFIEANS.js.map +1 -0
- package/dist/dashboard.js +2 -2
- package/dist/dashboard.js.map +1 -1
- package/dist/index.d.ts +134 -12
- package/dist/index.js +8 -72
- package/dist/index.js.map +1 -1
- package/dist/standalone.js +31 -6
- package/dist/standalone.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-GENCCHYK.js.map +0 -1
|
@@ -50,6 +50,12 @@ var RingBuffer = class {
|
|
|
50
50
|
};
|
|
51
51
|
|
|
52
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
|
+
}
|
|
53
59
|
var EventStore = class {
|
|
54
60
|
buffer;
|
|
55
61
|
sessions = /* @__PURE__ */ new Map();
|
|
@@ -110,7 +116,7 @@ var EventStore = class {
|
|
|
110
116
|
const since = filter.sinceSeconds ? Date.now() - filter.sinceSeconds * 1e3 : 0;
|
|
111
117
|
return this.buffer.query((e) => {
|
|
112
118
|
if (e.eventType !== "network") return false;
|
|
113
|
-
if (filter.sessionId && e.sessionId
|
|
119
|
+
if (filter.sessionId && !matchesSessionFilter(e.sessionId, filter.sessionId)) return false;
|
|
114
120
|
if (filter.projectId && !this.matchesProjectId(e.sessionId, filter.projectId)) return false;
|
|
115
121
|
const ne = e;
|
|
116
122
|
if (ne.timestamp < since) return false;
|
|
@@ -125,7 +131,7 @@ var EventStore = class {
|
|
|
125
131
|
const since = filter.sinceSeconds ? Date.now() - filter.sinceSeconds * 1e3 : 0;
|
|
126
132
|
return this.buffer.query((e) => {
|
|
127
133
|
if (e.eventType !== "console") return false;
|
|
128
|
-
if (filter.sessionId && e.sessionId
|
|
134
|
+
if (filter.sessionId && !matchesSessionFilter(e.sessionId, filter.sessionId)) return false;
|
|
129
135
|
if (filter.projectId && !this.matchesProjectId(e.sessionId, filter.projectId)) return false;
|
|
130
136
|
const ce = e;
|
|
131
137
|
if (ce.timestamp < since) return false;
|
|
@@ -147,7 +153,7 @@ var EventStore = class {
|
|
|
147
153
|
const typeSet = filter.eventTypes ? new Set(filter.eventTypes) : null;
|
|
148
154
|
return this.buffer.toArray().filter((e) => {
|
|
149
155
|
if (e.timestamp < since) return false;
|
|
150
|
-
if (filter.sessionId && e.sessionId
|
|
156
|
+
if (filter.sessionId && !matchesSessionFilter(e.sessionId, filter.sessionId)) return false;
|
|
151
157
|
if (filter.projectId && !this.matchesProjectId(e.sessionId, filter.projectId)) return false;
|
|
152
158
|
if (typeSet && !typeSet.has(e.eventType)) return false;
|
|
153
159
|
return true;
|
|
@@ -168,16 +174,28 @@ var EventStore = class {
|
|
|
168
174
|
getSessionIdsForProjectId(projectId) {
|
|
169
175
|
return Array.from(this.sessions.values()).filter((s) => s.projectId === projectId).map((s) => s.sessionId);
|
|
170
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
|
+
}
|
|
171
185
|
/** Check if an event belongs to the given projectId (via its session). */
|
|
172
186
|
matchesProjectId(sessionId, projectId) {
|
|
173
187
|
const session = this.sessions.get(sessionId);
|
|
174
|
-
|
|
188
|
+
if (!session?.projectId) return false;
|
|
189
|
+
if (projectId.includes(",")) {
|
|
190
|
+
return projectId.split(",").includes(session.projectId);
|
|
191
|
+
}
|
|
192
|
+
return session.projectId === projectId;
|
|
175
193
|
}
|
|
176
194
|
getStateEvents(filter = {}) {
|
|
177
195
|
const since = filter.sinceSeconds ? Date.now() - filter.sinceSeconds * 1e3 : 0;
|
|
178
196
|
return this.buffer.query((e) => {
|
|
179
197
|
if (e.eventType !== "state") return false;
|
|
180
|
-
if (filter.sessionId && e.sessionId
|
|
198
|
+
if (filter.sessionId && !matchesSessionFilter(e.sessionId, filter.sessionId)) return false;
|
|
181
199
|
if (filter.projectId && !this.matchesProjectId(e.sessionId, filter.projectId)) return false;
|
|
182
200
|
const se = e;
|
|
183
201
|
if (se.timestamp < since) return false;
|
|
@@ -189,7 +207,7 @@ var EventStore = class {
|
|
|
189
207
|
const since = filter.sinceSeconds ? Date.now() - filter.sinceSeconds * 1e3 : 0;
|
|
190
208
|
return this.buffer.query((e) => {
|
|
191
209
|
if (e.eventType !== "render") return false;
|
|
192
|
-
if (filter.sessionId && e.sessionId
|
|
210
|
+
if (filter.sessionId && !matchesSessionFilter(e.sessionId, filter.sessionId)) return false;
|
|
193
211
|
if (filter.projectId && !this.matchesProjectId(e.sessionId, filter.projectId)) return false;
|
|
194
212
|
const re = e;
|
|
195
213
|
if (re.timestamp < since) return false;
|
|
@@ -206,7 +224,7 @@ var EventStore = class {
|
|
|
206
224
|
const since = filter.sinceSeconds ? Date.now() - filter.sinceSeconds * 1e3 : 0;
|
|
207
225
|
return this.buffer.query((e) => {
|
|
208
226
|
if (e.eventType !== "performance") return false;
|
|
209
|
-
if (filter.sessionId && e.sessionId
|
|
227
|
+
if (filter.sessionId && !matchesSessionFilter(e.sessionId, filter.sessionId)) return false;
|
|
210
228
|
if (filter.projectId && !this.matchesProjectId(e.sessionId, filter.projectId)) return false;
|
|
211
229
|
const pe = e;
|
|
212
230
|
if (pe.timestamp < since) return false;
|
|
@@ -218,7 +236,7 @@ var EventStore = class {
|
|
|
218
236
|
const since = filter.sinceSeconds ? Date.now() - filter.sinceSeconds * 1e3 : 0;
|
|
219
237
|
return this.buffer.query((e) => {
|
|
220
238
|
if (e.eventType !== "database") return false;
|
|
221
|
-
if (filter.sessionId && e.sessionId
|
|
239
|
+
if (filter.sessionId && !matchesSessionFilter(e.sessionId, filter.sessionId)) return false;
|
|
222
240
|
if (filter.projectId && !this.matchesProjectId(e.sessionId, filter.projectId)) return false;
|
|
223
241
|
const de = e;
|
|
224
242
|
if (de.timestamp < since) return false;
|
|
@@ -240,7 +258,7 @@ var EventStore = class {
|
|
|
240
258
|
const since = filter.sinceSeconds ? Date.now() - filter.sinceSeconds * 1e3 : 0;
|
|
241
259
|
return this.buffer.query((e) => {
|
|
242
260
|
if (e.eventType !== "custom") return false;
|
|
243
|
-
if (filter.sessionId && e.sessionId
|
|
261
|
+
if (filter.sessionId && !matchesSessionFilter(e.sessionId, filter.sessionId)) return false;
|
|
244
262
|
if (filter.projectId && !this.matchesProjectId(e.sessionId, filter.projectId)) return false;
|
|
245
263
|
const ce = e;
|
|
246
264
|
if (ce.timestamp < since) return false;
|
|
@@ -252,7 +270,7 @@ var EventStore = class {
|
|
|
252
270
|
const since = filter.sinceSeconds ? Date.now() - filter.sinceSeconds * 1e3 : 0;
|
|
253
271
|
return this.buffer.query((e) => {
|
|
254
272
|
if (e.eventType !== "ui") return false;
|
|
255
|
-
if (filter.sessionId && e.sessionId
|
|
273
|
+
if (filter.sessionId && !matchesSessionFilter(e.sessionId, filter.sessionId)) return false;
|
|
256
274
|
if (filter.projectId && !this.matchesProjectId(e.sessionId, filter.projectId)) return false;
|
|
257
275
|
const ue = e;
|
|
258
276
|
if (ue.timestamp < since) return false;
|
|
@@ -267,7 +285,7 @@ var EventStore = class {
|
|
|
267
285
|
const since = filter.sinceSeconds ? Date.now() - filter.sinceSeconds * 1e3 : 0;
|
|
268
286
|
const results = this.buffer.query((e) => {
|
|
269
287
|
if (e.eventType !== eventType) return false;
|
|
270
|
-
if (filter.sessionId && e.sessionId
|
|
288
|
+
if (filter.sessionId && !matchesSessionFilter(e.sessionId, filter.sessionId)) return false;
|
|
271
289
|
if (filter.projectId && !this.matchesProjectId(e.sessionId, filter.projectId)) return false;
|
|
272
290
|
if (e.timestamp < since) return false;
|
|
273
291
|
if (filter.url) {
|
|
@@ -282,7 +300,7 @@ var EventStore = class {
|
|
|
282
300
|
const since = filter.sinceSeconds ? Date.now() - filter.sinceSeconds * 1e3 : 0;
|
|
283
301
|
return this.buffer.query((e) => {
|
|
284
302
|
if (e.eventType !== eventType) return false;
|
|
285
|
-
if (filter.sessionId && e.sessionId
|
|
303
|
+
if (filter.sessionId && !matchesSessionFilter(e.sessionId, filter.sessionId)) return false;
|
|
286
304
|
if (filter.projectId && !this.matchesProjectId(e.sessionId, filter.projectId)) return false;
|
|
287
305
|
if (e.timestamp < since) return false;
|
|
288
306
|
if (filter.url) {
|
|
@@ -349,6 +367,18 @@ function getOrCreateProjectId(projectManager, appName) {
|
|
|
349
367
|
projectManager.setProjectIdForApp(appName, projectId);
|
|
350
368
|
return projectId;
|
|
351
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
|
+
}
|
|
352
382
|
|
|
353
383
|
// src/sqlite-store.ts
|
|
354
384
|
import { renameSync, existsSync } from "fs";
|
|
@@ -852,6 +882,7 @@ var CollectorServer = class {
|
|
|
852
882
|
pruneTimer = null;
|
|
853
883
|
heartbeatTimer = null;
|
|
854
884
|
tlsConfig = null;
|
|
885
|
+
pmStore = null;
|
|
855
886
|
constructor(options = {}) {
|
|
856
887
|
this.store = new EventStore(options.bufferSize ?? 1e4);
|
|
857
888
|
this.projectManager = options.projectManager ?? null;
|
|
@@ -887,6 +918,10 @@ var CollectorServer = class {
|
|
|
887
918
|
getRateLimiter() {
|
|
888
919
|
return this.rateLimiter;
|
|
889
920
|
}
|
|
921
|
+
/** Set the PmStore for project ID resolution (called after construction when PmStore is available). */
|
|
922
|
+
setPmStore(pmStore) {
|
|
923
|
+
this.pmStore = pmStore;
|
|
924
|
+
}
|
|
890
925
|
onConnect(cb) {
|
|
891
926
|
this.connectCallbacks.push(cb);
|
|
892
927
|
}
|
|
@@ -894,7 +929,7 @@ var CollectorServer = class {
|
|
|
894
929
|
this.disconnectCallbacks.push(cb);
|
|
895
930
|
}
|
|
896
931
|
start(options = {}) {
|
|
897
|
-
const port = options.port ??
|
|
932
|
+
const port = options.port ?? 6767;
|
|
898
933
|
const host = options.host ?? "127.0.0.1";
|
|
899
934
|
const maxRetries = options.maxRetries ?? 5;
|
|
900
935
|
const retryDelayMs = options.retryDelayMs ?? 1e3;
|
|
@@ -1055,7 +1090,15 @@ var CollectorServer = class {
|
|
|
1055
1090
|
switch (msg.type) {
|
|
1056
1091
|
case "handshake": {
|
|
1057
1092
|
const payload = msg.payload;
|
|
1058
|
-
|
|
1093
|
+
let workspaceFromKey = null;
|
|
1094
|
+
if (payload.authToken && this.pmStore?.getWorkspaceByApiKey) {
|
|
1095
|
+
try {
|
|
1096
|
+
const ws2 = this.pmStore.getWorkspaceByApiKey(payload.authToken);
|
|
1097
|
+
if (ws2) workspaceFromKey = { id: ws2.id, slug: ws2.slug };
|
|
1098
|
+
} catch {
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
if (this.authManager?.isEnabled() && !workspaceFromKey) {
|
|
1059
1102
|
if (!this.authManager.isAuthorized(payload.authToken)) {
|
|
1060
1103
|
try {
|
|
1061
1104
|
ws.send(JSON.stringify({
|
|
@@ -1068,15 +1111,27 @@ var CollectorServer = class {
|
|
|
1068
1111
|
ws.close(4001, "Authentication failed");
|
|
1069
1112
|
return;
|
|
1070
1113
|
}
|
|
1071
|
-
this.pendingHandshakes.delete(ws);
|
|
1072
1114
|
}
|
|
1115
|
+
this.pendingHandshakes.delete(ws);
|
|
1073
1116
|
const projectName = payload.appName;
|
|
1074
|
-
const projectId = payload.projectId ?? (this.projectManager ?
|
|
1117
|
+
const projectId = payload.projectId ?? (this.projectManager ? resolveProjectId(this.projectManager, projectName, this.pmStore) : void 0);
|
|
1075
1118
|
this.clients.set(ws, {
|
|
1076
1119
|
sessionId: payload.sessionId,
|
|
1077
1120
|
projectName,
|
|
1078
|
-
projectId
|
|
1121
|
+
projectId,
|
|
1122
|
+
workspaceId: workspaceFromKey?.id
|
|
1079
1123
|
});
|
|
1124
|
+
if (workspaceFromKey && projectId && this.pmStore?.listProjects && this.pmStore.setProjectWorkspace) {
|
|
1125
|
+
try {
|
|
1126
|
+
const existing = this.pmStore.listProjects().find(
|
|
1127
|
+
(p) => p.runtimeProjectId === projectId
|
|
1128
|
+
);
|
|
1129
|
+
if (existing && !existing.workspaceId) {
|
|
1130
|
+
this.pmStore.setProjectWorkspace(existing.id, workspaceFromKey.id);
|
|
1131
|
+
}
|
|
1132
|
+
} catch {
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1080
1135
|
const sqliteStore = this.ensureSqliteStore(projectName);
|
|
1081
1136
|
if (sqliteStore) {
|
|
1082
1137
|
const sessionInfo = {
|
|
@@ -1238,12 +1293,13 @@ import { mkdirSync, readFileSync as readFileSync2, writeFileSync, existsSync as
|
|
|
1238
1293
|
import { join } from "path";
|
|
1239
1294
|
import { homedir } from "os";
|
|
1240
1295
|
var DEFAULT_GLOBAL_CONFIG = {
|
|
1241
|
-
defaultPort:
|
|
1296
|
+
defaultPort: 6767,
|
|
1242
1297
|
bufferSize: 1e4,
|
|
1243
|
-
httpPort:
|
|
1298
|
+
httpPort: 6768
|
|
1244
1299
|
};
|
|
1245
1300
|
var ProjectManager = class {
|
|
1246
1301
|
baseDir;
|
|
1302
|
+
appProjectIndex = /* @__PURE__ */ new Map();
|
|
1247
1303
|
constructor(baseDir) {
|
|
1248
1304
|
this.baseDir = baseDir ?? join(homedir(), ".runtimescope");
|
|
1249
1305
|
}
|
|
@@ -1356,6 +1412,60 @@ var ProjectManager = class {
|
|
|
1356
1412
|
}
|
|
1357
1413
|
return null;
|
|
1358
1414
|
}
|
|
1415
|
+
// --- Reverse index: appName → projectId ---
|
|
1416
|
+
/**
|
|
1417
|
+
* Build reverse index: appName -> projectId.
|
|
1418
|
+
* Scans all project configs, PM projects with runtimeApps + runtimeProjectId,
|
|
1419
|
+
* and project-level .runtimescope/config.json files from PM project paths.
|
|
1420
|
+
*/
|
|
1421
|
+
rebuildAppIndex(pmStore) {
|
|
1422
|
+
this.appProjectIndex.clear();
|
|
1423
|
+
for (const name of this.listProjects()) {
|
|
1424
|
+
const config = this.getProjectConfig(name);
|
|
1425
|
+
if (config?.projectId) {
|
|
1426
|
+
this.appProjectIndex.set(name.toLowerCase(), config.projectId);
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
if (pmStore) {
|
|
1430
|
+
for (const p of pmStore.listProjects()) {
|
|
1431
|
+
if (p.runtimeProjectId && p.runtimeApps) {
|
|
1432
|
+
for (const app of p.runtimeApps) {
|
|
1433
|
+
this.appProjectIndex.set(app.toLowerCase(), p.runtimeProjectId);
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
if (pmStore) {
|
|
1439
|
+
for (const p of pmStore.listProjects()) {
|
|
1440
|
+
if (p.path) {
|
|
1441
|
+
try {
|
|
1442
|
+
const configPath = join(p.path, ".runtimescope", "config.json");
|
|
1443
|
+
if (existsSync2(configPath)) {
|
|
1444
|
+
const content = readFileSync2(configPath, "utf-8");
|
|
1445
|
+
const config = JSON.parse(content);
|
|
1446
|
+
if (config.projectId) {
|
|
1447
|
+
if (config.appName) {
|
|
1448
|
+
this.appProjectIndex.set(config.appName.toLowerCase(), config.projectId);
|
|
1449
|
+
}
|
|
1450
|
+
if (Array.isArray(config.sdks)) {
|
|
1451
|
+
for (const sdk of config.sdks) {
|
|
1452
|
+
if (sdk.appName) {
|
|
1453
|
+
this.appProjectIndex.set(sdk.appName.toLowerCase(), config.projectId);
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
} catch {
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
/** O(1) lookup from the cached index. */
|
|
1466
|
+
resolveAppProjectId(appName) {
|
|
1467
|
+
return this.appProjectIndex.get(appName.toLowerCase()) ?? null;
|
|
1468
|
+
}
|
|
1359
1469
|
// --- Environment variable resolution ---
|
|
1360
1470
|
resolveEnvVars(value) {
|
|
1361
1471
|
return value.replace(/\$\{([^}]+)\}/g, (_match, varName) => {
|
|
@@ -1409,6 +1519,129 @@ var ProjectManager = class {
|
|
|
1409
1519
|
}
|
|
1410
1520
|
};
|
|
1411
1521
|
|
|
1522
|
+
// src/project-config.ts
|
|
1523
|
+
import { readFileSync as readFileSync3, existsSync as existsSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
|
|
1524
|
+
import { join as join2 } from "path";
|
|
1525
|
+
var DEFAULT_CAPTURE = {
|
|
1526
|
+
network: true,
|
|
1527
|
+
console: true,
|
|
1528
|
+
xhr: true,
|
|
1529
|
+
body: false,
|
|
1530
|
+
performance: true,
|
|
1531
|
+
renders: true,
|
|
1532
|
+
navigation: true,
|
|
1533
|
+
clicks: false,
|
|
1534
|
+
http: false,
|
|
1535
|
+
errors: true,
|
|
1536
|
+
stackTraces: false
|
|
1537
|
+
};
|
|
1538
|
+
function readProjectConfig(projectDir) {
|
|
1539
|
+
const configPath = join2(projectDir, ".runtimescope", "config.json");
|
|
1540
|
+
if (!existsSync3(configPath)) return null;
|
|
1541
|
+
try {
|
|
1542
|
+
const content = readFileSync3(configPath, "utf-8");
|
|
1543
|
+
return JSON.parse(content);
|
|
1544
|
+
} catch {
|
|
1545
|
+
return null;
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
function writeProjectConfig(projectDir, config) {
|
|
1549
|
+
const dir = join2(projectDir, ".runtimescope");
|
|
1550
|
+
if (!existsSync3(dir)) mkdirSync2(dir, { recursive: true });
|
|
1551
|
+
writeFileSync2(join2(dir, "config.json"), JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
1552
|
+
}
|
|
1553
|
+
function scaffoldProjectConfig(projectDir, opts) {
|
|
1554
|
+
const existing = readProjectConfig(projectDir);
|
|
1555
|
+
if (existing) {
|
|
1556
|
+
if (opts.sdkType) {
|
|
1557
|
+
const alreadyHas = existing.sdks.some((s) => s.type === opts.sdkType);
|
|
1558
|
+
if (!alreadyHas) {
|
|
1559
|
+
existing.sdks.push({
|
|
1560
|
+
type: opts.sdkType,
|
|
1561
|
+
framework: opts.framework,
|
|
1562
|
+
appName: opts.appName !== existing.appName ? opts.appName : void 0
|
|
1563
|
+
});
|
|
1564
|
+
writeProjectConfig(projectDir, existing);
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
return existing;
|
|
1568
|
+
}
|
|
1569
|
+
const projectId = generateProjectId();
|
|
1570
|
+
const httpPort = process.env.RUNTIMESCOPE_HTTP_PORT ?? "6768";
|
|
1571
|
+
const dsn = `runtimescope://${projectId}@localhost:${httpPort}/${opts.appName}`;
|
|
1572
|
+
const config = {
|
|
1573
|
+
projectId,
|
|
1574
|
+
dsn,
|
|
1575
|
+
appName: opts.appName,
|
|
1576
|
+
description: opts.description,
|
|
1577
|
+
sdks: opts.sdkType ? [{ type: opts.sdkType, framework: opts.framework }] : [],
|
|
1578
|
+
capture: { ...DEFAULT_CAPTURE },
|
|
1579
|
+
category: opts.category
|
|
1580
|
+
};
|
|
1581
|
+
writeProjectConfig(projectDir, config);
|
|
1582
|
+
const gitignorePath = join2(projectDir, ".runtimescope", ".gitignore");
|
|
1583
|
+
if (!existsSync3(gitignorePath)) {
|
|
1584
|
+
writeFileSync2(gitignorePath, "# Keep config.json committed, ignore local state\n*.log\n*.db\n.env\n", "utf-8");
|
|
1585
|
+
}
|
|
1586
|
+
return config;
|
|
1587
|
+
}
|
|
1588
|
+
function resolveProjectAppNames(config) {
|
|
1589
|
+
const names = /* @__PURE__ */ new Set([config.appName]);
|
|
1590
|
+
for (const sdk of config.sdks) {
|
|
1591
|
+
if (sdk.appName) names.add(sdk.appName);
|
|
1592
|
+
}
|
|
1593
|
+
return Array.from(names);
|
|
1594
|
+
}
|
|
1595
|
+
function migrateProjectIds(projectManager, pmStore) {
|
|
1596
|
+
const result = { unified: 0, skipped: 0, details: [] };
|
|
1597
|
+
if (!pmStore) return result;
|
|
1598
|
+
for (const project of pmStore.listProjects()) {
|
|
1599
|
+
if (!project.runtimeApps || project.runtimeApps.length < 2) continue;
|
|
1600
|
+
if (!project.path) {
|
|
1601
|
+
result.skipped++;
|
|
1602
|
+
continue;
|
|
1603
|
+
}
|
|
1604
|
+
let canonicalId = null;
|
|
1605
|
+
try {
|
|
1606
|
+
const configPath = join2(project.path, ".runtimescope", "config.json");
|
|
1607
|
+
if (existsSync3(configPath)) {
|
|
1608
|
+
const config = JSON.parse(readFileSync3(configPath, "utf-8"));
|
|
1609
|
+
if (config.projectId) canonicalId = config.projectId;
|
|
1610
|
+
}
|
|
1611
|
+
} catch {
|
|
1612
|
+
}
|
|
1613
|
+
if (!canonicalId && project.runtimeProjectId) {
|
|
1614
|
+
canonicalId = project.runtimeProjectId;
|
|
1615
|
+
}
|
|
1616
|
+
if (!canonicalId) {
|
|
1617
|
+
for (const appName of project.runtimeApps) {
|
|
1618
|
+
const existing = projectManager.getProjectIdForApp(appName);
|
|
1619
|
+
if (existing) {
|
|
1620
|
+
canonicalId = existing;
|
|
1621
|
+
break;
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
if (!canonicalId) {
|
|
1626
|
+
result.skipped++;
|
|
1627
|
+
continue;
|
|
1628
|
+
}
|
|
1629
|
+
for (const appName of project.runtimeApps) {
|
|
1630
|
+
const current = projectManager.getProjectIdForApp(appName);
|
|
1631
|
+
if (current !== canonicalId) {
|
|
1632
|
+
projectManager.setProjectIdForApp(appName, canonicalId);
|
|
1633
|
+
result.details.push(`Unified ${appName}: ${current ?? "none"} \u2192 ${canonicalId}`);
|
|
1634
|
+
result.unified++;
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
if (project.runtimeProjectId !== canonicalId) {
|
|
1638
|
+
pmStore.updateProject(project.id, { runtimeProjectId: canonicalId });
|
|
1639
|
+
result.details.push(`PM project ${project.name}: runtimeProjectId \u2192 ${canonicalId}`);
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
return result;
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1412
1645
|
// src/auth.ts
|
|
1413
1646
|
import { randomBytes as randomBytes2, timingSafeEqual } from "crypto";
|
|
1414
1647
|
var AuthManager = class {
|
|
@@ -1573,7 +1806,7 @@ var Redactor = class {
|
|
|
1573
1806
|
// src/platform.ts
|
|
1574
1807
|
import { execFileSync, execSync } from "child_process";
|
|
1575
1808
|
import { readlinkSync, readdirSync as readdirSync2 } from "fs";
|
|
1576
|
-
import { join as
|
|
1809
|
+
import { join as join3 } from "path";
|
|
1577
1810
|
var IS_WIN = process.platform === "win32";
|
|
1578
1811
|
var IS_LINUX = process.platform === "linux";
|
|
1579
1812
|
function runFile(cmd, args, timeoutMs = 5e3) {
|
|
@@ -1692,7 +1925,7 @@ function findPidsInDir_linux(dir) {
|
|
|
1692
1925
|
const pid = parseInt(entry, 10);
|
|
1693
1926
|
if (isNaN(pid) || pid <= 1) continue;
|
|
1694
1927
|
try {
|
|
1695
|
-
const cwd = readlinkSync(
|
|
1928
|
+
const cwd = readlinkSync(join3("/proc", entry, "cwd"));
|
|
1696
1929
|
if (cwd.startsWith(dir)) pids.push(pid);
|
|
1697
1930
|
} catch {
|
|
1698
1931
|
}
|
|
@@ -1896,15 +2129,15 @@ var SessionManager = class {
|
|
|
1896
2129
|
// src/http-server.ts
|
|
1897
2130
|
import { createServer } from "http";
|
|
1898
2131
|
import { createServer as createHttpsServer2 } from "https";
|
|
1899
|
-
import { readFileSync as
|
|
2132
|
+
import { readFileSync as readFileSync4, existsSync as existsSync5 } from "fs";
|
|
1900
2133
|
import { resolve, dirname } from "path";
|
|
1901
2134
|
import { fileURLToPath } from "url";
|
|
1902
2135
|
import { WebSocketServer as WebSocketServer2 } from "ws";
|
|
1903
2136
|
|
|
1904
2137
|
// src/pm/pm-routes.ts
|
|
1905
2138
|
import { readdir, readFile, writeFile, unlink, mkdir } from "fs/promises";
|
|
1906
|
-
import { existsSync as
|
|
1907
|
-
import { join as
|
|
2139
|
+
import { existsSync as existsSync4 } from "fs";
|
|
2140
|
+
import { join as join4 } from "path";
|
|
1908
2141
|
import { homedir as homedir2 } from "os";
|
|
1909
2142
|
import { spawn, execFileSync as execFileSync2 } from "child_process";
|
|
1910
2143
|
var LOG_RING_SIZE = 500;
|
|
@@ -1943,7 +2176,11 @@ function createPmRouter(pmStore, discovery, helpers, broadcastDevServer) {
|
|
|
1943
2176
|
helpers.json(res, { ...project, stats });
|
|
1944
2177
|
return;
|
|
1945
2178
|
}
|
|
1946
|
-
|
|
2179
|
+
let projects = pmStore.listProjects();
|
|
2180
|
+
const workspaceId = params.get("workspace_id");
|
|
2181
|
+
if (workspaceId) {
|
|
2182
|
+
projects = projects.filter((p) => p.workspaceId === workspaceId);
|
|
2183
|
+
}
|
|
1947
2184
|
helpers.json(res, { data: projects, count: projects.length });
|
|
1948
2185
|
});
|
|
1949
2186
|
route("GET", "/api/pm/projects/export-csv", (_req, res, params) => {
|
|
@@ -2041,6 +2278,152 @@ function createPmRouter(pmStore, discovery, helpers, broadcastDevServer) {
|
|
|
2041
2278
|
helpers.json(res, { error: err.message }, 400);
|
|
2042
2279
|
}
|
|
2043
2280
|
});
|
|
2281
|
+
route("DELETE", "/api/pm/projects/:id", (_req, res, params) => {
|
|
2282
|
+
const id = params.get("id");
|
|
2283
|
+
const project = pmStore.getProject(id);
|
|
2284
|
+
if (!project) {
|
|
2285
|
+
helpers.json(res, { error: "Project not found" }, 404);
|
|
2286
|
+
return;
|
|
2287
|
+
}
|
|
2288
|
+
pmStore.deleteProject(id);
|
|
2289
|
+
helpers.json(res, { ok: true, deleted: project.name });
|
|
2290
|
+
});
|
|
2291
|
+
route("PUT", "/api/pm/projects/:id/workspace", async (req, res, params) => {
|
|
2292
|
+
const id = params.get("id");
|
|
2293
|
+
const body = await helpers.readBody(req, 4096);
|
|
2294
|
+
if (!body) {
|
|
2295
|
+
helpers.json(res, { error: "Missing body" }, 400);
|
|
2296
|
+
return;
|
|
2297
|
+
}
|
|
2298
|
+
let parsed;
|
|
2299
|
+
try {
|
|
2300
|
+
parsed = JSON.parse(body);
|
|
2301
|
+
} catch {
|
|
2302
|
+
helpers.json(res, { error: "Invalid JSON" }, 400);
|
|
2303
|
+
return;
|
|
2304
|
+
}
|
|
2305
|
+
if (!parsed.workspace_id) {
|
|
2306
|
+
helpers.json(res, { error: "Missing workspace_id" }, 400);
|
|
2307
|
+
return;
|
|
2308
|
+
}
|
|
2309
|
+
try {
|
|
2310
|
+
pmStore.setProjectWorkspace(id, parsed.workspace_id);
|
|
2311
|
+
helpers.json(res, { ok: true });
|
|
2312
|
+
} catch (err) {
|
|
2313
|
+
helpers.json(res, { error: err.message }, 400);
|
|
2314
|
+
}
|
|
2315
|
+
});
|
|
2316
|
+
route("GET", "/api/pm/workspaces", (_req, res) => {
|
|
2317
|
+
helpers.json(res, { data: pmStore.listWorkspaces() });
|
|
2318
|
+
});
|
|
2319
|
+
route("POST", "/api/pm/workspaces", async (req, res) => {
|
|
2320
|
+
const body = await helpers.readBody(req, 4096);
|
|
2321
|
+
if (!body) {
|
|
2322
|
+
helpers.json(res, { error: "Missing body" }, 400);
|
|
2323
|
+
return;
|
|
2324
|
+
}
|
|
2325
|
+
let parsed;
|
|
2326
|
+
try {
|
|
2327
|
+
parsed = JSON.parse(body);
|
|
2328
|
+
} catch {
|
|
2329
|
+
helpers.json(res, { error: "Invalid JSON" }, 400);
|
|
2330
|
+
return;
|
|
2331
|
+
}
|
|
2332
|
+
if (!parsed.name || typeof parsed.name !== "string") {
|
|
2333
|
+
helpers.json(res, { error: "Missing name" }, 400);
|
|
2334
|
+
return;
|
|
2335
|
+
}
|
|
2336
|
+
try {
|
|
2337
|
+
const ws = pmStore.createWorkspace({ name: parsed.name, slug: parsed.slug, description: parsed.description });
|
|
2338
|
+
helpers.json(res, ws, 201);
|
|
2339
|
+
} catch (err) {
|
|
2340
|
+
helpers.json(res, { error: err.message }, 400);
|
|
2341
|
+
}
|
|
2342
|
+
});
|
|
2343
|
+
route("GET", "/api/pm/workspaces/:id", (_req, res, params) => {
|
|
2344
|
+
const id = params.get("id");
|
|
2345
|
+
const ws = pmStore.getWorkspace(id);
|
|
2346
|
+
if (!ws) {
|
|
2347
|
+
helpers.json(res, { error: "Workspace not found" }, 404);
|
|
2348
|
+
return;
|
|
2349
|
+
}
|
|
2350
|
+
helpers.json(res, ws);
|
|
2351
|
+
});
|
|
2352
|
+
route("PUT", "/api/pm/workspaces/:id", async (req, res, params) => {
|
|
2353
|
+
const id = params.get("id");
|
|
2354
|
+
if (!pmStore.getWorkspace(id)) {
|
|
2355
|
+
helpers.json(res, { error: "Workspace not found" }, 404);
|
|
2356
|
+
return;
|
|
2357
|
+
}
|
|
2358
|
+
const body = await helpers.readBody(req, 4096);
|
|
2359
|
+
if (!body) {
|
|
2360
|
+
helpers.json(res, { error: "Missing body" }, 400);
|
|
2361
|
+
return;
|
|
2362
|
+
}
|
|
2363
|
+
let parsed;
|
|
2364
|
+
try {
|
|
2365
|
+
parsed = JSON.parse(body);
|
|
2366
|
+
} catch {
|
|
2367
|
+
helpers.json(res, { error: "Invalid JSON" }, 400);
|
|
2368
|
+
return;
|
|
2369
|
+
}
|
|
2370
|
+
try {
|
|
2371
|
+
pmStore.updateWorkspace(id, parsed);
|
|
2372
|
+
helpers.json(res, pmStore.getWorkspace(id));
|
|
2373
|
+
} catch (err) {
|
|
2374
|
+
helpers.json(res, { error: err.message }, 400);
|
|
2375
|
+
}
|
|
2376
|
+
});
|
|
2377
|
+
route("DELETE", "/api/pm/workspaces/:id", (_req, res, params) => {
|
|
2378
|
+
const id = params.get("id");
|
|
2379
|
+
try {
|
|
2380
|
+
pmStore.deleteWorkspace(id);
|
|
2381
|
+
helpers.json(res, { ok: true });
|
|
2382
|
+
} catch (err) {
|
|
2383
|
+
helpers.json(res, { error: err.message }, 400);
|
|
2384
|
+
}
|
|
2385
|
+
});
|
|
2386
|
+
route("GET", "/api/pm/workspaces/:id/api-keys", (_req, res, params) => {
|
|
2387
|
+
const id = params.get("id");
|
|
2388
|
+
if (!pmStore.getWorkspace(id)) {
|
|
2389
|
+
helpers.json(res, { error: "Workspace not found" }, 404);
|
|
2390
|
+
return;
|
|
2391
|
+
}
|
|
2392
|
+
helpers.json(res, { data: pmStore.listApiKeys(id) });
|
|
2393
|
+
});
|
|
2394
|
+
route("POST", "/api/pm/workspaces/:id/api-keys", async (req, res, params) => {
|
|
2395
|
+
const id = params.get("id");
|
|
2396
|
+
if (!pmStore.getWorkspace(id)) {
|
|
2397
|
+
helpers.json(res, { error: "Workspace not found" }, 404);
|
|
2398
|
+
return;
|
|
2399
|
+
}
|
|
2400
|
+
const body = await helpers.readBody(req, 4096);
|
|
2401
|
+
if (!body) {
|
|
2402
|
+
helpers.json(res, { error: "Missing body" }, 400);
|
|
2403
|
+
return;
|
|
2404
|
+
}
|
|
2405
|
+
let parsed;
|
|
2406
|
+
try {
|
|
2407
|
+
parsed = JSON.parse(body);
|
|
2408
|
+
} catch {
|
|
2409
|
+
helpers.json(res, { error: "Invalid JSON" }, 400);
|
|
2410
|
+
return;
|
|
2411
|
+
}
|
|
2412
|
+
if (!parsed.label) {
|
|
2413
|
+
helpers.json(res, { error: "Missing label" }, 400);
|
|
2414
|
+
return;
|
|
2415
|
+
}
|
|
2416
|
+
try {
|
|
2417
|
+
const key = pmStore.createApiKey(id, parsed.label, parsed.expires_at);
|
|
2418
|
+
helpers.json(res, key, 201);
|
|
2419
|
+
} catch (err) {
|
|
2420
|
+
helpers.json(res, { error: err.message }, 400);
|
|
2421
|
+
}
|
|
2422
|
+
});
|
|
2423
|
+
route("DELETE", "/api/pm/api-keys/:key", (_req, res, params) => {
|
|
2424
|
+
pmStore.revokeApiKey(params.get("key"));
|
|
2425
|
+
helpers.json(res, { ok: true });
|
|
2426
|
+
});
|
|
2044
2427
|
route("GET", "/api/pm/tasks", (_req, res, params) => {
|
|
2045
2428
|
const projectId = params.get("project_id") ?? void 0;
|
|
2046
2429
|
const status = params.get("status") ?? void 0;
|
|
@@ -2216,13 +2599,13 @@ function createPmRouter(pmStore, discovery, helpers, broadcastDevServer) {
|
|
|
2216
2599
|
helpers.json(res, { data: [], count: 0 });
|
|
2217
2600
|
return;
|
|
2218
2601
|
}
|
|
2219
|
-
const memoryDir =
|
|
2602
|
+
const memoryDir = join4(homedir2(), ".claude", "projects", project.claudeProjectKey, "memory");
|
|
2220
2603
|
try {
|
|
2221
2604
|
const files = await readdir(memoryDir);
|
|
2222
2605
|
const mdFiles = files.filter((f) => f.endsWith(".md"));
|
|
2223
2606
|
const result = await Promise.all(
|
|
2224
2607
|
mdFiles.map(async (filename) => {
|
|
2225
|
-
const content = await readFile(
|
|
2608
|
+
const content = await readFile(join4(memoryDir, filename), "utf-8");
|
|
2226
2609
|
return { filename, content, sizeBytes: Buffer.byteLength(content) };
|
|
2227
2610
|
})
|
|
2228
2611
|
);
|
|
@@ -2239,7 +2622,7 @@ function createPmRouter(pmStore, discovery, helpers, broadcastDevServer) {
|
|
|
2239
2622
|
helpers.json(res, { error: "Project not found" }, 404);
|
|
2240
2623
|
return;
|
|
2241
2624
|
}
|
|
2242
|
-
const filePath =
|
|
2625
|
+
const filePath = join4(homedir2(), ".claude", "projects", project.claudeProjectKey, "memory", filename);
|
|
2243
2626
|
try {
|
|
2244
2627
|
const content = await readFile(filePath, "utf-8");
|
|
2245
2628
|
helpers.json(res, { filename, content, sizeBytes: Buffer.byteLength(content) });
|
|
@@ -2262,9 +2645,9 @@ function createPmRouter(pmStore, discovery, helpers, broadcastDevServer) {
|
|
|
2262
2645
|
}
|
|
2263
2646
|
try {
|
|
2264
2647
|
const { content } = JSON.parse(body);
|
|
2265
|
-
const memoryDir =
|
|
2648
|
+
const memoryDir = join4(homedir2(), ".claude", "projects", project.claudeProjectKey, "memory");
|
|
2266
2649
|
await mkdir(memoryDir, { recursive: true });
|
|
2267
|
-
await writeFile(
|
|
2650
|
+
await writeFile(join4(memoryDir, filename), content, "utf-8");
|
|
2268
2651
|
helpers.json(res, { ok: true });
|
|
2269
2652
|
} catch (err) {
|
|
2270
2653
|
helpers.json(res, { error: err.message }, 500);
|
|
@@ -2278,7 +2661,7 @@ function createPmRouter(pmStore, discovery, helpers, broadcastDevServer) {
|
|
|
2278
2661
|
helpers.json(res, { error: "Project not found" }, 404);
|
|
2279
2662
|
return;
|
|
2280
2663
|
}
|
|
2281
|
-
const filePath =
|
|
2664
|
+
const filePath = join4(homedir2(), ".claude", "projects", project.claudeProjectKey, "memory", filename);
|
|
2282
2665
|
try {
|
|
2283
2666
|
await unlink(filePath);
|
|
2284
2667
|
helpers.json(res, { ok: true });
|
|
@@ -2338,7 +2721,7 @@ function createPmRouter(pmStore, discovery, helpers, broadcastDevServer) {
|
|
|
2338
2721
|
const { content } = JSON.parse(body);
|
|
2339
2722
|
const paths = getRulesPaths(project.claudeProjectKey, project.path);
|
|
2340
2723
|
const filePath = paths[scope];
|
|
2341
|
-
const dir =
|
|
2724
|
+
const dir = join4(filePath, "..");
|
|
2342
2725
|
await mkdir(dir, { recursive: true });
|
|
2343
2726
|
await writeFile(filePath, content, "utf-8");
|
|
2344
2727
|
helpers.json(res, { ok: true });
|
|
@@ -2358,7 +2741,7 @@ function createPmRouter(pmStore, discovery, helpers, broadcastDevServer) {
|
|
|
2358
2741
|
return;
|
|
2359
2742
|
}
|
|
2360
2743
|
try {
|
|
2361
|
-
const pkgPath =
|
|
2744
|
+
const pkgPath = join4(project.path, "package.json");
|
|
2362
2745
|
const pkg = JSON.parse(await readFile(pkgPath, "utf-8"));
|
|
2363
2746
|
const scripts = pkg.scripts ?? {};
|
|
2364
2747
|
const recommended = ["dev", "start", "serve"].find((s) => s in scripts) ?? null;
|
|
@@ -2774,6 +3157,71 @@ function createPmRouter(pmStore, discovery, helpers, broadcastDevServer) {
|
|
|
2774
3157
|
res.end(csv);
|
|
2775
3158
|
}
|
|
2776
3159
|
});
|
|
3160
|
+
route("GET", "/api/pm/capex-all", (_req, res, params) => {
|
|
3161
|
+
const category = params.get("category") ?? void 0;
|
|
3162
|
+
const projects = pmStore.listProjects();
|
|
3163
|
+
const filteredProjects = category ? projects.filter((p) => p.category === category) : projects;
|
|
3164
|
+
const allEntries = [];
|
|
3165
|
+
let totalCost = 0;
|
|
3166
|
+
let totalCapitalizable = 0;
|
|
3167
|
+
let totalExpensed = 0;
|
|
3168
|
+
let totalMinutes = 0;
|
|
3169
|
+
let totalConfirmed = 0;
|
|
3170
|
+
let totalUnconfirmed = 0;
|
|
3171
|
+
const byProject = [];
|
|
3172
|
+
for (const project of filteredProjects) {
|
|
3173
|
+
const entries = pmStore.listCapexEntries(project.id);
|
|
3174
|
+
let projCost = 0;
|
|
3175
|
+
let projCap = 0;
|
|
3176
|
+
let projExp = 0;
|
|
3177
|
+
let projMins = 0;
|
|
3178
|
+
let projConfirmed = 0;
|
|
3179
|
+
for (const e of entries) {
|
|
3180
|
+
allEntries.push({ ...e, projectName: project.name });
|
|
3181
|
+
projCost += e.adjustedCostMicrodollars;
|
|
3182
|
+
projMins += e.activeMinutes;
|
|
3183
|
+
if (e.classification === "capitalizable") projCap += e.adjustedCostMicrodollars;
|
|
3184
|
+
else projExp += e.adjustedCostMicrodollars;
|
|
3185
|
+
if (e.confirmed) projConfirmed++;
|
|
3186
|
+
}
|
|
3187
|
+
if (entries.length > 0) {
|
|
3188
|
+
byProject.push({
|
|
3189
|
+
projectId: project.id,
|
|
3190
|
+
projectName: project.name,
|
|
3191
|
+
category: project.category,
|
|
3192
|
+
totalCost: projCost,
|
|
3193
|
+
capitalizable: projCap,
|
|
3194
|
+
expensed: projExp,
|
|
3195
|
+
activeMinutes: projMins,
|
|
3196
|
+
activeHours: +(projMins / 60).toFixed(2),
|
|
3197
|
+
confirmed: projConfirmed,
|
|
3198
|
+
total: entries.length
|
|
3199
|
+
});
|
|
3200
|
+
}
|
|
3201
|
+
totalCost += projCost;
|
|
3202
|
+
totalCapitalizable += projCap;
|
|
3203
|
+
totalExpensed += projExp;
|
|
3204
|
+
totalMinutes += projMins;
|
|
3205
|
+
totalConfirmed += projConfirmed;
|
|
3206
|
+
totalUnconfirmed += entries.length - projConfirmed;
|
|
3207
|
+
}
|
|
3208
|
+
helpers.json(res, {
|
|
3209
|
+
data: {
|
|
3210
|
+
summary: {
|
|
3211
|
+
totalCost,
|
|
3212
|
+
capitalizable: totalCapitalizable,
|
|
3213
|
+
expensed: totalExpensed,
|
|
3214
|
+
activeMinutes: totalMinutes,
|
|
3215
|
+
activeHours: +(totalMinutes / 60).toFixed(2),
|
|
3216
|
+
confirmed: totalConfirmed,
|
|
3217
|
+
unconfirmed: totalUnconfirmed,
|
|
3218
|
+
projectCount: byProject.length
|
|
3219
|
+
},
|
|
3220
|
+
byProject,
|
|
3221
|
+
entries: allEntries.sort((a, b) => b.createdAt - a.createdAt)
|
|
3222
|
+
}
|
|
3223
|
+
});
|
|
3224
|
+
});
|
|
2777
3225
|
route("GET", "/api/pm/capex-report-all", async (_req, res, params) => {
|
|
2778
3226
|
const startDate = params.get("start_date") ?? void 0;
|
|
2779
3227
|
const endDate = params.get("end_date") ?? void 0;
|
|
@@ -2819,9 +3267,9 @@ function sanitizeFilename(name) {
|
|
|
2819
3267
|
function getRulesPaths(claudeProjectKey, projectPath) {
|
|
2820
3268
|
const home = homedir2();
|
|
2821
3269
|
return {
|
|
2822
|
-
global:
|
|
2823
|
-
project: claudeProjectKey ?
|
|
2824
|
-
local: projectPath ?
|
|
3270
|
+
global: join4(home, ".claude", "CLAUDE.md"),
|
|
3271
|
+
project: claudeProjectKey ? join4(home, ".claude", "projects", claudeProjectKey, "CLAUDE.md") : join4(projectPath ?? "", ".claude", "CLAUDE.md"),
|
|
3272
|
+
local: projectPath ? join4(projectPath, "CLAUDE.md") : join4(home, "CLAUDE.md")
|
|
2825
3273
|
};
|
|
2826
3274
|
}
|
|
2827
3275
|
function execGit(args, cwd) {
|
|
@@ -2866,7 +3314,7 @@ function parseGitStatus(porcelain) {
|
|
|
2866
3314
|
}
|
|
2867
3315
|
async function readRuleFile(filePath) {
|
|
2868
3316
|
try {
|
|
2869
|
-
if (
|
|
3317
|
+
if (existsSync4(filePath)) {
|
|
2870
3318
|
const content = await readFile(filePath, "utf-8");
|
|
2871
3319
|
return { path: filePath, content, exists: true };
|
|
2872
3320
|
}
|
|
@@ -2876,6 +3324,16 @@ async function readRuleFile(filePath) {
|
|
|
2876
3324
|
}
|
|
2877
3325
|
|
|
2878
3326
|
// src/http-server.ts
|
|
3327
|
+
var COLLECTOR_VERSION = (() => {
|
|
3328
|
+
try {
|
|
3329
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
3330
|
+
const pkgJson = readFileSync4(resolve(here, "..", "package.json"), "utf-8");
|
|
3331
|
+
const pkg = JSON.parse(pkgJson);
|
|
3332
|
+
return pkg.version ?? "unknown";
|
|
3333
|
+
} catch {
|
|
3334
|
+
return "unknown";
|
|
3335
|
+
}
|
|
3336
|
+
})();
|
|
2879
3337
|
var HttpServer = class {
|
|
2880
3338
|
server = null;
|
|
2881
3339
|
wss = null;
|
|
@@ -2889,7 +3347,7 @@ var HttpServer = class {
|
|
|
2889
3347
|
routes = /* @__PURE__ */ new Map();
|
|
2890
3348
|
pmRouter = null;
|
|
2891
3349
|
sdkBundlePath = null;
|
|
2892
|
-
activePort =
|
|
3350
|
+
activePort = 6768;
|
|
2893
3351
|
startedAt = Date.now();
|
|
2894
3352
|
connectedSessionsGetter = null;
|
|
2895
3353
|
pmStore = null;
|
|
@@ -2915,6 +3373,7 @@ var HttpServer = class {
|
|
|
2915
3373
|
this.routes.set("GET /api/health", (_req, res) => {
|
|
2916
3374
|
this.json(res, {
|
|
2917
3375
|
status: "ok",
|
|
3376
|
+
version: COLLECTOR_VERSION,
|
|
2918
3377
|
timestamp: Date.now(),
|
|
2919
3378
|
uptime: Math.floor((Date.now() - this.startedAt) / 1e3),
|
|
2920
3379
|
sessions: this.store.getSessionInfo().filter((s) => s.isConnected).length,
|
|
@@ -3111,7 +3570,7 @@ var HttpServer = class {
|
|
|
3111
3570
|
return;
|
|
3112
3571
|
}
|
|
3113
3572
|
const payload = parsed;
|
|
3114
|
-
const projectId = typeof payload.projectId === "string" ? payload.projectId : payload.appName && this.projectManager ?
|
|
3573
|
+
const projectId = typeof payload.projectId === "string" ? payload.projectId : payload.appName && this.projectManager ? resolveProjectId(this.projectManager, payload.appName, this.pmStore) : void 0;
|
|
3115
3574
|
if (!payload.sessionId || !Array.isArray(payload.events) || payload.events.length === 0) {
|
|
3116
3575
|
this.json(res, {
|
|
3117
3576
|
error: "Required: sessionId (string), events (non-empty array)",
|
|
@@ -3138,6 +3597,21 @@ var HttpServer = class {
|
|
|
3138
3597
|
} catch {
|
|
3139
3598
|
}
|
|
3140
3599
|
}
|
|
3600
|
+
if (this.pmStore) {
|
|
3601
|
+
try {
|
|
3602
|
+
const token = AuthManager.extractBearer(req.headers.authorization);
|
|
3603
|
+
if (token) {
|
|
3604
|
+
const ws = this.pmStore.getWorkspaceByApiKey(token);
|
|
3605
|
+
if (ws && projectId) {
|
|
3606
|
+
const existing = this.pmStore.listProjects().find((p) => p.runtimeProjectId === projectId);
|
|
3607
|
+
if (existing && !existing.workspaceId) {
|
|
3608
|
+
this.pmStore.setProjectWorkspace(existing.id, ws.id);
|
|
3609
|
+
}
|
|
3610
|
+
}
|
|
3611
|
+
}
|
|
3612
|
+
} catch {
|
|
3613
|
+
}
|
|
3614
|
+
}
|
|
3141
3615
|
}
|
|
3142
3616
|
const VALID_EVENT_TYPES = /* @__PURE__ */ new Set([
|
|
3143
3617
|
"network",
|
|
@@ -3196,7 +3670,7 @@ var HttpServer = class {
|
|
|
3196
3670
|
// npm installed
|
|
3197
3671
|
];
|
|
3198
3672
|
for (const p of candidates) {
|
|
3199
|
-
if (
|
|
3673
|
+
if (existsSync5(p)) {
|
|
3200
3674
|
this.sdkBundlePath = p;
|
|
3201
3675
|
return p;
|
|
3202
3676
|
}
|
|
@@ -3204,7 +3678,7 @@ var HttpServer = class {
|
|
|
3204
3678
|
return null;
|
|
3205
3679
|
}
|
|
3206
3680
|
async start(options = {}) {
|
|
3207
|
-
const basePort = options.port ?? parseInt(process.env.RUNTIMESCOPE_HTTP_PORT ?? "
|
|
3681
|
+
const basePort = options.port ?? parseInt(process.env.RUNTIMESCOPE_HTTP_PORT ?? "6768", 10);
|
|
3208
3682
|
const host = options.host ?? "127.0.0.1";
|
|
3209
3683
|
const tls = options.tls;
|
|
3210
3684
|
const maxRetries = 5;
|
|
@@ -3336,7 +3810,9 @@ var HttpServer = class {
|
|
|
3336
3810
|
const isPublic = url.pathname === "/api/health" || url.pathname === "/runtimescope.js" || url.pathname === "/snippet";
|
|
3337
3811
|
if (!isPublic && this.authManager?.isEnabled()) {
|
|
3338
3812
|
const token = AuthManager.extractBearer(req.headers.authorization);
|
|
3339
|
-
|
|
3813
|
+
const isGlobal = this.authManager.isAuthorized(token);
|
|
3814
|
+
const isWorkspaceToken = !!(token && this.pmStore?.getWorkspaceByApiKey(token));
|
|
3815
|
+
if (!isGlobal && !isWorkspaceToken) {
|
|
3340
3816
|
this.json(res, { error: "Unauthorized", code: "AUTH_FAILED" }, 401);
|
|
3341
3817
|
return;
|
|
3342
3818
|
}
|
|
@@ -3344,7 +3820,7 @@ var HttpServer = class {
|
|
|
3344
3820
|
if (req.method === "GET" && url.pathname === "/runtimescope.js") {
|
|
3345
3821
|
const sdkPath = this.resolveSdkPath();
|
|
3346
3822
|
if (sdkPath) {
|
|
3347
|
-
const bundle =
|
|
3823
|
+
const bundle = readFileSync4(sdkPath, "utf-8");
|
|
3348
3824
|
res.writeHead(200, {
|
|
3349
3825
|
"Content-Type": "application/javascript",
|
|
3350
3826
|
"Cache-Control": "no-cache"
|
|
@@ -3358,14 +3834,12 @@ var HttpServer = class {
|
|
|
3358
3834
|
}
|
|
3359
3835
|
if (req.method === "GET" && url.pathname === "/snippet") {
|
|
3360
3836
|
const appName = (url.searchParams.get("app") || "my-app").replace(/[^a-zA-Z0-9_-]/g, "");
|
|
3361
|
-
const
|
|
3837
|
+
const projectId = url.searchParams.get("project_id") || "proj_xxx";
|
|
3838
|
+
const dsn = `runtimescope://${projectId}@localhost:${this.activePort}/${appName}`;
|
|
3362
3839
|
const snippet = `<!-- RuntimeScope SDK \u2014 paste before </body> -->
|
|
3363
3840
|
<script src="http://localhost:${this.activePort}/runtimescope.js"></script>
|
|
3364
3841
|
<script>
|
|
3365
|
-
RuntimeScope.init({
|
|
3366
|
-
appName: '${appName}',
|
|
3367
|
-
endpoint: 'ws://localhost:${wsPort}',
|
|
3368
|
-
});
|
|
3842
|
+
RuntimeScope.init({ dsn: '${dsn}' });
|
|
3369
3843
|
</script>`;
|
|
3370
3844
|
res.writeHead(200, {
|
|
3371
3845
|
"Content-Type": "text/plain"
|
|
@@ -3470,6 +3944,7 @@ function numParam(params, key) {
|
|
|
3470
3944
|
|
|
3471
3945
|
// src/pm/pm-store.ts
|
|
3472
3946
|
import Database from "better-sqlite3";
|
|
3947
|
+
import { randomBytes as randomBytes3 } from "crypto";
|
|
3473
3948
|
var PmStore = class {
|
|
3474
3949
|
db;
|
|
3475
3950
|
constructor(options) {
|
|
@@ -3595,6 +4070,37 @@ var PmStore = class {
|
|
|
3595
4070
|
CREATE INDEX IF NOT EXISTS idx_pm_capex_project ON pm_capex_entries(project_id);
|
|
3596
4071
|
CREATE INDEX IF NOT EXISTS idx_pm_capex_period ON pm_capex_entries(period);
|
|
3597
4072
|
CREATE INDEX IF NOT EXISTS idx_pm_capex_confirmed ON pm_capex_entries(confirmed);
|
|
4073
|
+
|
|
4074
|
+
CREATE TABLE IF NOT EXISTS pm_deleted_projects (
|
|
4075
|
+
path TEXT PRIMARY KEY,
|
|
4076
|
+
name TEXT,
|
|
4077
|
+
deleted_at INTEGER NOT NULL
|
|
4078
|
+
);
|
|
4079
|
+
CREATE INDEX IF NOT EXISTS idx_deleted_path ON pm_deleted_projects(path);
|
|
4080
|
+
|
|
4081
|
+
-- Multi-tenant workspaces (Phase 1) --
|
|
4082
|
+
CREATE TABLE IF NOT EXISTS pm_workspaces (
|
|
4083
|
+
id TEXT PRIMARY KEY,
|
|
4084
|
+
name TEXT NOT NULL,
|
|
4085
|
+
slug TEXT UNIQUE NOT NULL,
|
|
4086
|
+
description TEXT,
|
|
4087
|
+
is_default INTEGER NOT NULL DEFAULT 0,
|
|
4088
|
+
created_at INTEGER NOT NULL,
|
|
4089
|
+
updated_at INTEGER NOT NULL
|
|
4090
|
+
);
|
|
4091
|
+
CREATE INDEX IF NOT EXISTS idx_workspaces_slug ON pm_workspaces(slug);
|
|
4092
|
+
|
|
4093
|
+
CREATE TABLE IF NOT EXISTS pm_api_keys (
|
|
4094
|
+
key TEXT PRIMARY KEY,
|
|
4095
|
+
workspace_id TEXT NOT NULL,
|
|
4096
|
+
label TEXT NOT NULL,
|
|
4097
|
+
created_at INTEGER NOT NULL,
|
|
4098
|
+
last_used_at INTEGER,
|
|
4099
|
+
expires_at INTEGER,
|
|
4100
|
+
revoked_at INTEGER,
|
|
4101
|
+
FOREIGN KEY (workspace_id) REFERENCES pm_workspaces(id) ON DELETE CASCADE
|
|
4102
|
+
);
|
|
4103
|
+
CREATE INDEX IF NOT EXISTS idx_api_keys_workspace ON pm_api_keys(workspace_id);
|
|
3598
4104
|
`);
|
|
3599
4105
|
}
|
|
3600
4106
|
runMigrations() {
|
|
@@ -3614,18 +4120,45 @@ var PmStore = class {
|
|
|
3614
4120
|
this.db.exec("ALTER TABLE pm_projects ADD COLUMN runtime_project_id TEXT DEFAULT NULL");
|
|
3615
4121
|
} catch {
|
|
3616
4122
|
}
|
|
4123
|
+
try {
|
|
4124
|
+
this.db.exec("ALTER TABLE pm_projects ADD COLUMN workspace_id TEXT DEFAULT NULL");
|
|
4125
|
+
} catch {
|
|
4126
|
+
}
|
|
4127
|
+
this.ensureDefaultWorkspace();
|
|
4128
|
+
}
|
|
4129
|
+
/**
|
|
4130
|
+
* Ensure a default "personal" workspace exists and every project has a
|
|
4131
|
+
* workspace_id. Runs on every startup — idempotent.
|
|
4132
|
+
*/
|
|
4133
|
+
ensureDefaultWorkspace() {
|
|
4134
|
+
const existing = this.db.prepare("SELECT id FROM pm_workspaces WHERE is_default = 1").get();
|
|
4135
|
+
let defaultId;
|
|
4136
|
+
if (existing) {
|
|
4137
|
+
defaultId = existing.id;
|
|
4138
|
+
} else {
|
|
4139
|
+
defaultId = generateWorkspaceId();
|
|
4140
|
+
const now = Date.now();
|
|
4141
|
+
this.db.prepare(
|
|
4142
|
+
`INSERT INTO pm_workspaces (id, name, slug, description, is_default, created_at, updated_at)
|
|
4143
|
+
VALUES (?, ?, ?, ?, 1, ?, ?)`
|
|
4144
|
+
).run(defaultId, "Personal", "personal", "Your personal workspace", now, now);
|
|
4145
|
+
}
|
|
4146
|
+
this.db.prepare("UPDATE pm_projects SET workspace_id = ? WHERE workspace_id IS NULL").run(defaultId);
|
|
3617
4147
|
}
|
|
3618
4148
|
// ============================================================
|
|
3619
4149
|
// Projects
|
|
3620
4150
|
// ============================================================
|
|
3621
4151
|
upsertProject(project) {
|
|
4152
|
+
const workspaceId = project.workspaceId ?? this.db.prepare("SELECT id FROM pm_workspaces WHERE is_default = 1 LIMIT 1").get();
|
|
4153
|
+
const resolvedWorkspaceId = typeof workspaceId === "string" ? workspaceId : workspaceId?.id ?? null;
|
|
3622
4154
|
this.db.prepare(`
|
|
3623
|
-
INSERT INTO pm_projects (id, name, path, claude_project_key, runtimescope_project,
|
|
4155
|
+
INSERT INTO pm_projects (id, workspace_id, name, path, claude_project_key, runtimescope_project,
|
|
3624
4156
|
phase, management_authorized, probable_to_complete, project_status,
|
|
3625
4157
|
category, sdk_installed, runtime_apps,
|
|
3626
4158
|
created_at, updated_at, metadata)
|
|
3627
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
4159
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
3628
4160
|
ON CONFLICT(id) DO UPDATE SET
|
|
4161
|
+
workspace_id = COALESCE(pm_projects.workspace_id, excluded.workspace_id),
|
|
3629
4162
|
name = excluded.name,
|
|
3630
4163
|
path = COALESCE(excluded.path, pm_projects.path),
|
|
3631
4164
|
claude_project_key = COALESCE(excluded.claude_project_key, pm_projects.claude_project_key),
|
|
@@ -3636,6 +4169,7 @@ var PmStore = class {
|
|
|
3636
4169
|
metadata = COALESCE(excluded.metadata, pm_projects.metadata)
|
|
3637
4170
|
`).run(
|
|
3638
4171
|
project.id,
|
|
4172
|
+
resolvedWorkspaceId,
|
|
3639
4173
|
project.name,
|
|
3640
4174
|
project.path ?? null,
|
|
3641
4175
|
project.claudeProjectKey ?? null,
|
|
@@ -3762,13 +4296,132 @@ var PmStore = class {
|
|
|
3762
4296
|
this.updateProject(match.id, updates);
|
|
3763
4297
|
return match.id;
|
|
3764
4298
|
}
|
|
4299
|
+
/** Find a project's runtimeProjectId by checking if appName appears in any project's runtimeApps. */
|
|
4300
|
+
findProjectIdByApp(appName) {
|
|
4301
|
+
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}"%`);
|
|
4302
|
+
return row?.runtime_project_id ?? null;
|
|
4303
|
+
}
|
|
3765
4304
|
listCategories() {
|
|
3766
4305
|
const rows = this.db.prepare("SELECT DISTINCT category FROM pm_projects WHERE category IS NOT NULL ORDER BY category ASC").all();
|
|
3767
4306
|
return rows.map((r) => r.category);
|
|
3768
4307
|
}
|
|
4308
|
+
// ============================================================
|
|
4309
|
+
// Workspaces (multi-tenant)
|
|
4310
|
+
// ============================================================
|
|
4311
|
+
listWorkspaces() {
|
|
4312
|
+
const rows = this.db.prepare("SELECT * FROM pm_workspaces ORDER BY is_default DESC, name ASC").all();
|
|
4313
|
+
return rows.map(mapWorkspaceRow);
|
|
4314
|
+
}
|
|
4315
|
+
getWorkspace(id) {
|
|
4316
|
+
const row = this.db.prepare("SELECT * FROM pm_workspaces WHERE id = ?").get(id);
|
|
4317
|
+
return row ? mapWorkspaceRow(row) : null;
|
|
4318
|
+
}
|
|
4319
|
+
getWorkspaceBySlug(slug) {
|
|
4320
|
+
const row = this.db.prepare("SELECT * FROM pm_workspaces WHERE slug = ?").get(slug);
|
|
4321
|
+
return row ? mapWorkspaceRow(row) : null;
|
|
4322
|
+
}
|
|
4323
|
+
getDefaultWorkspace() {
|
|
4324
|
+
const row = this.db.prepare("SELECT * FROM pm_workspaces WHERE is_default = 1 LIMIT 1").get();
|
|
4325
|
+
if (!row) {
|
|
4326
|
+
throw new Error("Default workspace missing \u2014 ensureDefaultWorkspace() must run first");
|
|
4327
|
+
}
|
|
4328
|
+
return mapWorkspaceRow(row);
|
|
4329
|
+
}
|
|
4330
|
+
createWorkspace(input) {
|
|
4331
|
+
const id = generateWorkspaceId();
|
|
4332
|
+
const slug = (input.slug ?? input.name).toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
4333
|
+
if (!slug) {
|
|
4334
|
+
throw new Error("Workspace slug cannot be empty");
|
|
4335
|
+
}
|
|
4336
|
+
if (this.getWorkspaceBySlug(slug)) {
|
|
4337
|
+
throw new Error(`Workspace with slug "${slug}" already exists`);
|
|
4338
|
+
}
|
|
4339
|
+
const now = Date.now();
|
|
4340
|
+
this.db.prepare(
|
|
4341
|
+
`INSERT INTO pm_workspaces (id, name, slug, description, is_default, created_at, updated_at)
|
|
4342
|
+
VALUES (?, ?, ?, ?, 0, ?, ?)`
|
|
4343
|
+
).run(id, input.name, slug, input.description ?? null, now, now);
|
|
4344
|
+
return { id, name: input.name, slug, description: input.description, createdAt: now, updatedAt: now, isDefault: false };
|
|
4345
|
+
}
|
|
4346
|
+
updateWorkspace(id, updates) {
|
|
4347
|
+
const sets = [];
|
|
4348
|
+
const params = [];
|
|
4349
|
+
if (updates.name !== void 0) {
|
|
4350
|
+
sets.push("name = ?");
|
|
4351
|
+
params.push(updates.name);
|
|
4352
|
+
}
|
|
4353
|
+
if (updates.slug !== void 0) {
|
|
4354
|
+
sets.push("slug = ?");
|
|
4355
|
+
params.push(updates.slug);
|
|
4356
|
+
}
|
|
4357
|
+
if (updates.description !== void 0) {
|
|
4358
|
+
sets.push("description = ?");
|
|
4359
|
+
params.push(updates.description);
|
|
4360
|
+
}
|
|
4361
|
+
if (!sets.length) return;
|
|
4362
|
+
sets.push("updated_at = ?");
|
|
4363
|
+
params.push(Date.now());
|
|
4364
|
+
params.push(id);
|
|
4365
|
+
this.db.prepare(`UPDATE pm_workspaces SET ${sets.join(", ")} WHERE id = ?`).run(...params);
|
|
4366
|
+
}
|
|
4367
|
+
deleteWorkspace(id) {
|
|
4368
|
+
const ws = this.getWorkspace(id);
|
|
4369
|
+
if (!ws) return;
|
|
4370
|
+
if (ws.isDefault) {
|
|
4371
|
+
throw new Error("Cannot delete the default workspace");
|
|
4372
|
+
}
|
|
4373
|
+
const def = this.getDefaultWorkspace();
|
|
4374
|
+
this.db.prepare("UPDATE pm_projects SET workspace_id = ? WHERE workspace_id = ?").run(def.id, id);
|
|
4375
|
+
this.db.prepare("DELETE FROM pm_api_keys WHERE workspace_id = ?").run(id);
|
|
4376
|
+
this.db.prepare("DELETE FROM pm_workspaces WHERE id = ?").run(id);
|
|
4377
|
+
}
|
|
4378
|
+
/** Move a project between workspaces. */
|
|
4379
|
+
setProjectWorkspace(projectId, workspaceId) {
|
|
4380
|
+
const ws = this.getWorkspace(workspaceId);
|
|
4381
|
+
if (!ws) throw new Error(`Workspace ${workspaceId} does not exist`);
|
|
4382
|
+
this.db.prepare("UPDATE pm_projects SET workspace_id = ?, updated_at = ? WHERE id = ?").run(workspaceId, Date.now(), projectId);
|
|
4383
|
+
}
|
|
4384
|
+
// ============================================================
|
|
4385
|
+
// API Keys (workspace-scoped)
|
|
4386
|
+
// ============================================================
|
|
4387
|
+
createApiKey(workspaceId, label, expiresAt) {
|
|
4388
|
+
const ws = this.getWorkspace(workspaceId);
|
|
4389
|
+
if (!ws) throw new Error(`Workspace ${workspaceId} does not exist`);
|
|
4390
|
+
const key = `tk_${randomBytes3(24).toString("hex")}`;
|
|
4391
|
+
const now = Date.now();
|
|
4392
|
+
this.db.prepare(
|
|
4393
|
+
`INSERT INTO pm_api_keys (key, workspace_id, label, created_at, expires_at)
|
|
4394
|
+
VALUES (?, ?, ?, ?, ?)`
|
|
4395
|
+
).run(key, workspaceId, label, now, expiresAt ?? null);
|
|
4396
|
+
return { key, workspaceId, label, createdAt: now, expiresAt };
|
|
4397
|
+
}
|
|
4398
|
+
listApiKeys(workspaceId) {
|
|
4399
|
+
const rows = this.db.prepare(
|
|
4400
|
+
`SELECT * FROM pm_api_keys WHERE workspace_id = ? AND revoked_at IS NULL ORDER BY created_at DESC`
|
|
4401
|
+
).all(workspaceId);
|
|
4402
|
+
return rows.map(mapApiKeyRow);
|
|
4403
|
+
}
|
|
4404
|
+
revokeApiKey(key) {
|
|
4405
|
+
this.db.prepare("UPDATE pm_api_keys SET revoked_at = ? WHERE key = ?").run(Date.now(), key);
|
|
4406
|
+
}
|
|
4407
|
+
getWorkspaceByApiKey(key) {
|
|
4408
|
+
const row = this.db.prepare(
|
|
4409
|
+
`SELECT w.* FROM pm_api_keys k
|
|
4410
|
+
JOIN pm_workspaces w ON w.id = k.workspace_id
|
|
4411
|
+
WHERE k.key = ? AND k.revoked_at IS NULL
|
|
4412
|
+
AND (k.expires_at IS NULL OR k.expires_at > ?)`
|
|
4413
|
+
).get(key, Date.now());
|
|
4414
|
+
if (!row) return null;
|
|
4415
|
+
try {
|
|
4416
|
+
this.db.prepare("UPDATE pm_api_keys SET last_used_at = ? WHERE key = ?").run(Date.now(), key);
|
|
4417
|
+
} catch {
|
|
4418
|
+
}
|
|
4419
|
+
return mapWorkspaceRow(row);
|
|
4420
|
+
}
|
|
3769
4421
|
mapProjectRow(row) {
|
|
3770
4422
|
return {
|
|
3771
4423
|
id: row.id,
|
|
4424
|
+
workspaceId: row.workspace_id ?? void 0,
|
|
3772
4425
|
name: row.name,
|
|
3773
4426
|
path: row.path ?? void 0,
|
|
3774
4427
|
claudeProjectKey: row.claude_project_key ?? void 0,
|
|
@@ -4521,12 +5174,69 @@ var PmStore = class {
|
|
|
4521
5174
|
};
|
|
4522
5175
|
}
|
|
4523
5176
|
// ============================================================
|
|
5177
|
+
// Deleted Projects Blocklist
|
|
5178
|
+
// ============================================================
|
|
5179
|
+
deleteProject(id) {
|
|
5180
|
+
const project = this.getProject(id);
|
|
5181
|
+
if (!project) return;
|
|
5182
|
+
if (project.path) {
|
|
5183
|
+
this.db.prepare(
|
|
5184
|
+
"INSERT OR REPLACE INTO pm_deleted_projects (path, name, deleted_at) VALUES (?, ?, ?)"
|
|
5185
|
+
).run(project.path, project.name, Date.now());
|
|
5186
|
+
}
|
|
5187
|
+
if (project.claudeProjectKey) {
|
|
5188
|
+
this.db.prepare(
|
|
5189
|
+
"INSERT OR REPLACE INTO pm_deleted_projects (path, name, deleted_at) VALUES (?, ?, ?)"
|
|
5190
|
+
).run(project.claudeProjectKey, project.name, Date.now());
|
|
5191
|
+
}
|
|
5192
|
+
this.db.prepare("DELETE FROM pm_capex_entries WHERE project_id = ?").run(id);
|
|
5193
|
+
this.db.prepare("DELETE FROM pm_notes WHERE project_id = ?").run(id);
|
|
5194
|
+
this.db.prepare("DELETE FROM pm_tasks WHERE project_id = ?").run(id);
|
|
5195
|
+
this.db.prepare("DELETE FROM pm_sessions WHERE project_id = ?").run(id);
|
|
5196
|
+
this.db.prepare("DELETE FROM pm_projects WHERE id = ?").run(id);
|
|
5197
|
+
}
|
|
5198
|
+
isDeletedPath(path) {
|
|
5199
|
+
const row = this.db.prepare("SELECT 1 FROM pm_deleted_projects WHERE path = ?").get(path);
|
|
5200
|
+
return !!row;
|
|
5201
|
+
}
|
|
5202
|
+
recoverProject(path) {
|
|
5203
|
+
this.db.prepare("DELETE FROM pm_deleted_projects WHERE path = ?").run(path);
|
|
5204
|
+
}
|
|
5205
|
+
listDeletedProjects() {
|
|
5206
|
+
return this.db.prepare("SELECT path, name, deleted_at as deletedAt FROM pm_deleted_projects ORDER BY deleted_at DESC").all();
|
|
5207
|
+
}
|
|
5208
|
+
// ============================================================
|
|
4524
5209
|
// Cleanup
|
|
4525
5210
|
// ============================================================
|
|
4526
5211
|
close() {
|
|
4527
5212
|
this.db.close();
|
|
4528
5213
|
}
|
|
4529
5214
|
};
|
|
5215
|
+
function generateWorkspaceId() {
|
|
5216
|
+
return `ws_${randomBytes3(8).toString("hex")}`;
|
|
5217
|
+
}
|
|
5218
|
+
function mapWorkspaceRow(row) {
|
|
5219
|
+
return {
|
|
5220
|
+
id: row.id,
|
|
5221
|
+
name: row.name,
|
|
5222
|
+
slug: row.slug,
|
|
5223
|
+
description: row.description ?? void 0,
|
|
5224
|
+
isDefault: row.is_default === 1,
|
|
5225
|
+
createdAt: row.created_at,
|
|
5226
|
+
updatedAt: row.updated_at
|
|
5227
|
+
};
|
|
5228
|
+
}
|
|
5229
|
+
function mapApiKeyRow(row) {
|
|
5230
|
+
return {
|
|
5231
|
+
key: row.key,
|
|
5232
|
+
workspaceId: row.workspace_id,
|
|
5233
|
+
label: row.label,
|
|
5234
|
+
createdAt: row.created_at,
|
|
5235
|
+
lastUsedAt: row.last_used_at ?? void 0,
|
|
5236
|
+
expiresAt: row.expires_at ?? void 0,
|
|
5237
|
+
revokedAt: row.revoked_at ?? void 0
|
|
5238
|
+
};
|
|
5239
|
+
}
|
|
4530
5240
|
|
|
4531
5241
|
// src/pm/session-parser.ts
|
|
4532
5242
|
import { createReadStream } from "fs";
|
|
@@ -4733,13 +5443,13 @@ async function parseSessionJsonl(jsonlPath, sessionId, projectId) {
|
|
|
4733
5443
|
|
|
4734
5444
|
// src/pm/project-discovery.ts
|
|
4735
5445
|
import { readdir as readdir2, readFile as readFile2, stat as stat2 } from "fs/promises";
|
|
4736
|
-
import { join as
|
|
4737
|
-
import { existsSync as
|
|
5446
|
+
import { join as join5, basename as basename2 } from "path";
|
|
5447
|
+
import { existsSync as existsSync6 } from "fs";
|
|
4738
5448
|
import { homedir as homedir3 } from "os";
|
|
4739
5449
|
var LOG_PREFIX = "[RuntimeScope PM]";
|
|
4740
5450
|
async function detectSdkInstalled(projectPath) {
|
|
4741
5451
|
try {
|
|
4742
|
-
const pkgPath =
|
|
5452
|
+
const pkgPath = join5(projectPath, "package.json");
|
|
4743
5453
|
const pkg = JSON.parse(await readFile2(pkgPath, "utf-8"));
|
|
4744
5454
|
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
4745
5455
|
if ("@runtimescope/sdk" in allDeps || "@runtimescope/server-sdk" in allDeps) {
|
|
@@ -4749,13 +5459,13 @@ async function detectSdkInstalled(projectPath) {
|
|
|
4749
5459
|
const workspaces = Array.isArray(pkg.workspaces) ? pkg.workspaces : pkg.workspaces.packages ?? [];
|
|
4750
5460
|
for (const ws of workspaces) {
|
|
4751
5461
|
const wsBase = ws.replace(/\/?\*$/, "");
|
|
4752
|
-
const wsDir =
|
|
5462
|
+
const wsDir = join5(projectPath, wsBase);
|
|
4753
5463
|
try {
|
|
4754
5464
|
const entries = await readdir2(wsDir, { withFileTypes: true });
|
|
4755
5465
|
for (const entry of entries) {
|
|
4756
5466
|
if (!entry.isDirectory()) continue;
|
|
4757
5467
|
try {
|
|
4758
|
-
const wsPkg = JSON.parse(await readFile2(
|
|
5468
|
+
const wsPkg = JSON.parse(await readFile2(join5(wsDir, entry.name, "package.json"), "utf-8"));
|
|
4759
5469
|
const wsDeps = { ...wsPkg.dependencies, ...wsPkg.devDependencies };
|
|
4760
5470
|
if ("@runtimescope/sdk" in wsDeps || "@runtimescope/server-sdk" in wsDeps) {
|
|
4761
5471
|
return true;
|
|
@@ -4770,7 +5480,7 @@ async function detectSdkInstalled(projectPath) {
|
|
|
4770
5480
|
} catch {
|
|
4771
5481
|
}
|
|
4772
5482
|
try {
|
|
4773
|
-
await stat2(
|
|
5483
|
+
await stat2(join5(projectPath, "node_modules", "@runtimescope"));
|
|
4774
5484
|
return true;
|
|
4775
5485
|
} catch {
|
|
4776
5486
|
return false;
|
|
@@ -4801,7 +5511,7 @@ function slugifyPath(fsPath) {
|
|
|
4801
5511
|
}
|
|
4802
5512
|
function decodeClaudeKey(key) {
|
|
4803
5513
|
const naive = "/" + key.slice(1).replace(/-/g, "/");
|
|
4804
|
-
if (
|
|
5514
|
+
if (existsSync6(naive)) return naive;
|
|
4805
5515
|
const parts = key.slice(1).split("-");
|
|
4806
5516
|
return resolvePathSegments(parts);
|
|
4807
5517
|
}
|
|
@@ -4809,16 +5519,16 @@ function resolvePathSegments(parts) {
|
|
|
4809
5519
|
if (parts.length === 0) return null;
|
|
4810
5520
|
function tryResolve(prefix, remaining) {
|
|
4811
5521
|
if (remaining.length === 0) {
|
|
4812
|
-
return
|
|
5522
|
+
return existsSync6(prefix) ? prefix : null;
|
|
4813
5523
|
}
|
|
4814
5524
|
for (let count = remaining.length; count >= 1; count--) {
|
|
4815
5525
|
const segment = remaining.slice(0, count).join("-");
|
|
4816
|
-
const candidate =
|
|
5526
|
+
const candidate = join5(prefix, segment);
|
|
4817
5527
|
if (count === remaining.length) {
|
|
4818
|
-
if (
|
|
5528
|
+
if (existsSync6(candidate)) return candidate;
|
|
4819
5529
|
} else {
|
|
4820
5530
|
try {
|
|
4821
|
-
if (
|
|
5531
|
+
if (existsSync6(candidate)) {
|
|
4822
5532
|
const result = tryResolve(candidate, remaining.slice(count));
|
|
4823
5533
|
if (result) return result;
|
|
4824
5534
|
}
|
|
@@ -4834,13 +5544,14 @@ function toPeriod(timestampMs) {
|
|
|
4834
5544
|
const d = new Date(timestampMs);
|
|
4835
5545
|
const year = d.getFullYear();
|
|
4836
5546
|
const month = String(d.getMonth() + 1).padStart(2, "0");
|
|
4837
|
-
|
|
5547
|
+
const day = String(d.getDate()).padStart(2, "0");
|
|
5548
|
+
return `${year}-${month}-${day}`;
|
|
4838
5549
|
}
|
|
4839
5550
|
var ProjectDiscovery = class {
|
|
4840
5551
|
constructor(pmStore, projectManager, claudeBaseDir) {
|
|
4841
5552
|
this.pmStore = pmStore;
|
|
4842
5553
|
this.projectManager = projectManager;
|
|
4843
|
-
this.claudeBaseDir = claudeBaseDir ??
|
|
5554
|
+
this.claudeBaseDir = claudeBaseDir ?? join5(homedir3(), ".claude");
|
|
4844
5555
|
}
|
|
4845
5556
|
claudeBaseDir;
|
|
4846
5557
|
/**
|
|
@@ -4873,7 +5584,7 @@ var ProjectDiscovery = class {
|
|
|
4873
5584
|
sessionsUpdated: 0,
|
|
4874
5585
|
errors: []
|
|
4875
5586
|
};
|
|
4876
|
-
const projectsDir =
|
|
5587
|
+
const projectsDir = join5(this.claudeBaseDir, "projects");
|
|
4877
5588
|
try {
|
|
4878
5589
|
await stat2(projectsDir);
|
|
4879
5590
|
} catch {
|
|
@@ -4940,6 +5651,7 @@ var ProjectDiscovery = class {
|
|
|
4940
5651
|
await this.pmStore.upsertProject(updated);
|
|
4941
5652
|
result.projectsUpdated = (result.projectsUpdated ?? 0) + 1;
|
|
4942
5653
|
} else {
|
|
5654
|
+
if (this.pmStore.isDeletedPath(sourcePath)) continue;
|
|
4943
5655
|
const fsPath = projectDir;
|
|
4944
5656
|
const project = {
|
|
4945
5657
|
id,
|
|
@@ -4985,10 +5697,10 @@ var ProjectDiscovery = class {
|
|
|
4985
5697
|
if (!project.claudeProjectKey) {
|
|
4986
5698
|
return 0;
|
|
4987
5699
|
}
|
|
4988
|
-
const projectDir =
|
|
5700
|
+
const projectDir = join5(this.claudeBaseDir, "projects", project.claudeProjectKey);
|
|
4989
5701
|
let sessionsIndexed = 0;
|
|
4990
5702
|
try {
|
|
4991
|
-
const indexPath =
|
|
5703
|
+
const indexPath = join5(projectDir, "sessions-index.json");
|
|
4992
5704
|
let indexEntries = null;
|
|
4993
5705
|
try {
|
|
4994
5706
|
const indexContent = await readFile2(indexPath, "utf-8");
|
|
@@ -5001,7 +5713,7 @@ var ProjectDiscovery = class {
|
|
|
5001
5713
|
for (const jsonlFile of jsonlFiles) {
|
|
5002
5714
|
try {
|
|
5003
5715
|
const sessionId = jsonlFile.replace(".jsonl", "");
|
|
5004
|
-
const jsonlPath =
|
|
5716
|
+
const jsonlPath = join5(projectDir, jsonlFile);
|
|
5005
5717
|
const fileStat = await stat2(jsonlPath);
|
|
5006
5718
|
const fileSize = fileStat.size;
|
|
5007
5719
|
const existingSession = await this.pmStore.getSession(sessionId);
|
|
@@ -5036,7 +5748,7 @@ var ProjectDiscovery = class {
|
|
|
5036
5748
|
* Process a single Claude project directory key.
|
|
5037
5749
|
*/
|
|
5038
5750
|
async processClaudeProject(key, result) {
|
|
5039
|
-
const projectDir =
|
|
5751
|
+
const projectDir = join5(this.claudeBaseDir, "projects", key);
|
|
5040
5752
|
let fsPath = decodeClaudeKey(key);
|
|
5041
5753
|
if (!fsPath) {
|
|
5042
5754
|
fsPath = await this.resolvePathFromIndex(projectDir);
|
|
@@ -5062,6 +5774,8 @@ var ProjectDiscovery = class {
|
|
|
5062
5774
|
await this.pmStore.upsertProject(updated);
|
|
5063
5775
|
result.projectsUpdated = (result.projectsUpdated ?? 0) + 1;
|
|
5064
5776
|
} else {
|
|
5777
|
+
if (fsPath && this.pmStore.isDeletedPath(fsPath)) return;
|
|
5778
|
+
if (this.pmStore.isDeletedPath(key)) return;
|
|
5065
5779
|
const project = {
|
|
5066
5780
|
id,
|
|
5067
5781
|
name,
|
|
@@ -5088,7 +5802,7 @@ var ProjectDiscovery = class {
|
|
|
5088
5802
|
*/
|
|
5089
5803
|
async resolvePathFromIndex(projectDir) {
|
|
5090
5804
|
try {
|
|
5091
|
-
const indexPath =
|
|
5805
|
+
const indexPath = join5(projectDir, "sessions-index.json");
|
|
5092
5806
|
const content = await readFile2(indexPath, "utf-8");
|
|
5093
5807
|
const index = JSON.parse(content);
|
|
5094
5808
|
const entry = index.entries?.find((e) => e.projectPath);
|
|
@@ -5103,11 +5817,11 @@ var ProjectDiscovery = class {
|
|
|
5103
5817
|
*/
|
|
5104
5818
|
async indexSessionsForClaudeProject(projectId, claudeKey) {
|
|
5105
5819
|
const counts = { discovered: 0, updated: 0 };
|
|
5106
|
-
const projectDir =
|
|
5820
|
+
const projectDir = join5(this.claudeBaseDir, "projects", claudeKey);
|
|
5107
5821
|
try {
|
|
5108
5822
|
let indexEntries = null;
|
|
5109
5823
|
try {
|
|
5110
|
-
const indexPath =
|
|
5824
|
+
const indexPath = join5(projectDir, "sessions-index.json");
|
|
5111
5825
|
const indexContent = await readFile2(indexPath, "utf-8");
|
|
5112
5826
|
const index = JSON.parse(indexContent);
|
|
5113
5827
|
indexEntries = index.entries ?? [];
|
|
@@ -5118,7 +5832,7 @@ var ProjectDiscovery = class {
|
|
|
5118
5832
|
for (const jsonlFile of jsonlFiles) {
|
|
5119
5833
|
try {
|
|
5120
5834
|
const sessionId = jsonlFile.replace(".jsonl", "");
|
|
5121
|
-
const jsonlPath =
|
|
5835
|
+
const jsonlPath = join5(projectDir, jsonlFile);
|
|
5122
5836
|
const fileStat = await stat2(jsonlPath);
|
|
5123
5837
|
const fileSize = fileStat.size;
|
|
5124
5838
|
const existingSession = await this.pmStore.getSession(sessionId);
|
|
@@ -5308,6 +6022,11 @@ export {
|
|
|
5308
6022
|
resolveTlsConfig,
|
|
5309
6023
|
CollectorServer,
|
|
5310
6024
|
ProjectManager,
|
|
6025
|
+
readProjectConfig,
|
|
6026
|
+
writeProjectConfig,
|
|
6027
|
+
scaffoldProjectConfig,
|
|
6028
|
+
resolveProjectAppNames,
|
|
6029
|
+
migrateProjectIds,
|
|
5311
6030
|
AuthManager,
|
|
5312
6031
|
generateApiKey,
|
|
5313
6032
|
BUILT_IN_RULES,
|
|
@@ -5326,4 +6045,4 @@ export {
|
|
|
5326
6045
|
parseSessionJsonl,
|
|
5327
6046
|
ProjectDiscovery
|
|
5328
6047
|
};
|
|
5329
|
-
//# sourceMappingURL=chunk-
|
|
6048
|
+
//# sourceMappingURL=chunk-WWFIEANS.js.map
|