@runtimescope/collector 0.9.3 → 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-MM44DN7Y.js → chunk-WWFIEANS.js} +394 -14
- 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 +65 -1
- package/dist/index.js +1 -1
- package/dist/standalone.js +17 -6
- package/dist/standalone.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-MM44DN7Y.js.map +0 -1
|
@@ -929,7 +929,7 @@ var CollectorServer = class {
|
|
|
929
929
|
this.disconnectCallbacks.push(cb);
|
|
930
930
|
}
|
|
931
931
|
start(options = {}) {
|
|
932
|
-
const port = options.port ??
|
|
932
|
+
const port = options.port ?? 6767;
|
|
933
933
|
const host = options.host ?? "127.0.0.1";
|
|
934
934
|
const maxRetries = options.maxRetries ?? 5;
|
|
935
935
|
const retryDelayMs = options.retryDelayMs ?? 1e3;
|
|
@@ -1090,7 +1090,15 @@ var CollectorServer = class {
|
|
|
1090
1090
|
switch (msg.type) {
|
|
1091
1091
|
case "handshake": {
|
|
1092
1092
|
const payload = msg.payload;
|
|
1093
|
-
|
|
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) {
|
|
1094
1102
|
if (!this.authManager.isAuthorized(payload.authToken)) {
|
|
1095
1103
|
try {
|
|
1096
1104
|
ws.send(JSON.stringify({
|
|
@@ -1103,15 +1111,27 @@ var CollectorServer = class {
|
|
|
1103
1111
|
ws.close(4001, "Authentication failed");
|
|
1104
1112
|
return;
|
|
1105
1113
|
}
|
|
1106
|
-
this.pendingHandshakes.delete(ws);
|
|
1107
1114
|
}
|
|
1115
|
+
this.pendingHandshakes.delete(ws);
|
|
1108
1116
|
const projectName = payload.appName;
|
|
1109
1117
|
const projectId = payload.projectId ?? (this.projectManager ? resolveProjectId(this.projectManager, projectName, this.pmStore) : void 0);
|
|
1110
1118
|
this.clients.set(ws, {
|
|
1111
1119
|
sessionId: payload.sessionId,
|
|
1112
1120
|
projectName,
|
|
1113
|
-
projectId
|
|
1121
|
+
projectId,
|
|
1122
|
+
workspaceId: workspaceFromKey?.id
|
|
1114
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
|
+
}
|
|
1115
1135
|
const sqliteStore = this.ensureSqliteStore(projectName);
|
|
1116
1136
|
if (sqliteStore) {
|
|
1117
1137
|
const sessionInfo = {
|
|
@@ -1273,9 +1293,9 @@ import { mkdirSync, readFileSync as readFileSync2, writeFileSync, existsSync as
|
|
|
1273
1293
|
import { join } from "path";
|
|
1274
1294
|
import { homedir } from "os";
|
|
1275
1295
|
var DEFAULT_GLOBAL_CONFIG = {
|
|
1276
|
-
defaultPort:
|
|
1296
|
+
defaultPort: 6767,
|
|
1277
1297
|
bufferSize: 1e4,
|
|
1278
|
-
httpPort:
|
|
1298
|
+
httpPort: 6768
|
|
1279
1299
|
};
|
|
1280
1300
|
var ProjectManager = class {
|
|
1281
1301
|
baseDir;
|
|
@@ -1547,7 +1567,7 @@ function scaffoldProjectConfig(projectDir, opts) {
|
|
|
1547
1567
|
return existing;
|
|
1548
1568
|
}
|
|
1549
1569
|
const projectId = generateProjectId();
|
|
1550
|
-
const httpPort = process.env.RUNTIMESCOPE_HTTP_PORT ?? "
|
|
1570
|
+
const httpPort = process.env.RUNTIMESCOPE_HTTP_PORT ?? "6768";
|
|
1551
1571
|
const dsn = `runtimescope://${projectId}@localhost:${httpPort}/${opts.appName}`;
|
|
1552
1572
|
const config = {
|
|
1553
1573
|
projectId,
|
|
@@ -2156,7 +2176,11 @@ function createPmRouter(pmStore, discovery, helpers, broadcastDevServer) {
|
|
|
2156
2176
|
helpers.json(res, { ...project, stats });
|
|
2157
2177
|
return;
|
|
2158
2178
|
}
|
|
2159
|
-
|
|
2179
|
+
let projects = pmStore.listProjects();
|
|
2180
|
+
const workspaceId = params.get("workspace_id");
|
|
2181
|
+
if (workspaceId) {
|
|
2182
|
+
projects = projects.filter((p) => p.workspaceId === workspaceId);
|
|
2183
|
+
}
|
|
2160
2184
|
helpers.json(res, { data: projects, count: projects.length });
|
|
2161
2185
|
});
|
|
2162
2186
|
route("GET", "/api/pm/projects/export-csv", (_req, res, params) => {
|
|
@@ -2264,6 +2288,142 @@ function createPmRouter(pmStore, discovery, helpers, broadcastDevServer) {
|
|
|
2264
2288
|
pmStore.deleteProject(id);
|
|
2265
2289
|
helpers.json(res, { ok: true, deleted: project.name });
|
|
2266
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
|
+
});
|
|
2267
2427
|
route("GET", "/api/pm/tasks", (_req, res, params) => {
|
|
2268
2428
|
const projectId = params.get("project_id") ?? void 0;
|
|
2269
2429
|
const status = params.get("status") ?? void 0;
|
|
@@ -3164,6 +3324,16 @@ async function readRuleFile(filePath) {
|
|
|
3164
3324
|
}
|
|
3165
3325
|
|
|
3166
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
|
+
})();
|
|
3167
3337
|
var HttpServer = class {
|
|
3168
3338
|
server = null;
|
|
3169
3339
|
wss = null;
|
|
@@ -3177,7 +3347,7 @@ var HttpServer = class {
|
|
|
3177
3347
|
routes = /* @__PURE__ */ new Map();
|
|
3178
3348
|
pmRouter = null;
|
|
3179
3349
|
sdkBundlePath = null;
|
|
3180
|
-
activePort =
|
|
3350
|
+
activePort = 6768;
|
|
3181
3351
|
startedAt = Date.now();
|
|
3182
3352
|
connectedSessionsGetter = null;
|
|
3183
3353
|
pmStore = null;
|
|
@@ -3203,6 +3373,7 @@ var HttpServer = class {
|
|
|
3203
3373
|
this.routes.set("GET /api/health", (_req, res) => {
|
|
3204
3374
|
this.json(res, {
|
|
3205
3375
|
status: "ok",
|
|
3376
|
+
version: COLLECTOR_VERSION,
|
|
3206
3377
|
timestamp: Date.now(),
|
|
3207
3378
|
uptime: Math.floor((Date.now() - this.startedAt) / 1e3),
|
|
3208
3379
|
sessions: this.store.getSessionInfo().filter((s) => s.isConnected).length,
|
|
@@ -3426,6 +3597,21 @@ var HttpServer = class {
|
|
|
3426
3597
|
} catch {
|
|
3427
3598
|
}
|
|
3428
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
|
+
}
|
|
3429
3615
|
}
|
|
3430
3616
|
const VALID_EVENT_TYPES = /* @__PURE__ */ new Set([
|
|
3431
3617
|
"network",
|
|
@@ -3492,7 +3678,7 @@ var HttpServer = class {
|
|
|
3492
3678
|
return null;
|
|
3493
3679
|
}
|
|
3494
3680
|
async start(options = {}) {
|
|
3495
|
-
const basePort = options.port ?? parseInt(process.env.RUNTIMESCOPE_HTTP_PORT ?? "
|
|
3681
|
+
const basePort = options.port ?? parseInt(process.env.RUNTIMESCOPE_HTTP_PORT ?? "6768", 10);
|
|
3496
3682
|
const host = options.host ?? "127.0.0.1";
|
|
3497
3683
|
const tls = options.tls;
|
|
3498
3684
|
const maxRetries = 5;
|
|
@@ -3624,7 +3810,9 @@ var HttpServer = class {
|
|
|
3624
3810
|
const isPublic = url.pathname === "/api/health" || url.pathname === "/runtimescope.js" || url.pathname === "/snippet";
|
|
3625
3811
|
if (!isPublic && this.authManager?.isEnabled()) {
|
|
3626
3812
|
const token = AuthManager.extractBearer(req.headers.authorization);
|
|
3627
|
-
|
|
3813
|
+
const isGlobal = this.authManager.isAuthorized(token);
|
|
3814
|
+
const isWorkspaceToken = !!(token && this.pmStore?.getWorkspaceByApiKey(token));
|
|
3815
|
+
if (!isGlobal && !isWorkspaceToken) {
|
|
3628
3816
|
this.json(res, { error: "Unauthorized", code: "AUTH_FAILED" }, 401);
|
|
3629
3817
|
return;
|
|
3630
3818
|
}
|
|
@@ -3756,6 +3944,7 @@ function numParam(params, key) {
|
|
|
3756
3944
|
|
|
3757
3945
|
// src/pm/pm-store.ts
|
|
3758
3946
|
import Database from "better-sqlite3";
|
|
3947
|
+
import { randomBytes as randomBytes3 } from "crypto";
|
|
3759
3948
|
var PmStore = class {
|
|
3760
3949
|
db;
|
|
3761
3950
|
constructor(options) {
|
|
@@ -3888,6 +4077,30 @@ var PmStore = class {
|
|
|
3888
4077
|
deleted_at INTEGER NOT NULL
|
|
3889
4078
|
);
|
|
3890
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);
|
|
3891
4104
|
`);
|
|
3892
4105
|
}
|
|
3893
4106
|
runMigrations() {
|
|
@@ -3907,18 +4120,45 @@ var PmStore = class {
|
|
|
3907
4120
|
this.db.exec("ALTER TABLE pm_projects ADD COLUMN runtime_project_id TEXT DEFAULT NULL");
|
|
3908
4121
|
} catch {
|
|
3909
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);
|
|
3910
4147
|
}
|
|
3911
4148
|
// ============================================================
|
|
3912
4149
|
// Projects
|
|
3913
4150
|
// ============================================================
|
|
3914
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;
|
|
3915
4154
|
this.db.prepare(`
|
|
3916
|
-
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,
|
|
3917
4156
|
phase, management_authorized, probable_to_complete, project_status,
|
|
3918
4157
|
category, sdk_installed, runtime_apps,
|
|
3919
4158
|
created_at, updated_at, metadata)
|
|
3920
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
4159
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
3921
4160
|
ON CONFLICT(id) DO UPDATE SET
|
|
4161
|
+
workspace_id = COALESCE(pm_projects.workspace_id, excluded.workspace_id),
|
|
3922
4162
|
name = excluded.name,
|
|
3923
4163
|
path = COALESCE(excluded.path, pm_projects.path),
|
|
3924
4164
|
claude_project_key = COALESCE(excluded.claude_project_key, pm_projects.claude_project_key),
|
|
@@ -3929,6 +4169,7 @@ var PmStore = class {
|
|
|
3929
4169
|
metadata = COALESCE(excluded.metadata, pm_projects.metadata)
|
|
3930
4170
|
`).run(
|
|
3931
4171
|
project.id,
|
|
4172
|
+
resolvedWorkspaceId,
|
|
3932
4173
|
project.name,
|
|
3933
4174
|
project.path ?? null,
|
|
3934
4175
|
project.claudeProjectKey ?? null,
|
|
@@ -4064,9 +4305,123 @@ var PmStore = class {
|
|
|
4064
4305
|
const rows = this.db.prepare("SELECT DISTINCT category FROM pm_projects WHERE category IS NOT NULL ORDER BY category ASC").all();
|
|
4065
4306
|
return rows.map((r) => r.category);
|
|
4066
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
|
+
}
|
|
4067
4421
|
mapProjectRow(row) {
|
|
4068
4422
|
return {
|
|
4069
4423
|
id: row.id,
|
|
4424
|
+
workspaceId: row.workspace_id ?? void 0,
|
|
4070
4425
|
name: row.name,
|
|
4071
4426
|
path: row.path ?? void 0,
|
|
4072
4427
|
claudeProjectKey: row.claude_project_key ?? void 0,
|
|
@@ -4857,6 +5212,31 @@ var PmStore = class {
|
|
|
4857
5212
|
this.db.close();
|
|
4858
5213
|
}
|
|
4859
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
|
+
}
|
|
4860
5240
|
|
|
4861
5241
|
// src/pm/session-parser.ts
|
|
4862
5242
|
import { createReadStream } from "fs";
|
|
@@ -5665,4 +6045,4 @@ export {
|
|
|
5665
6045
|
parseSessionJsonl,
|
|
5666
6046
|
ProjectDiscovery
|
|
5667
6047
|
};
|
|
5668
|
-
//# sourceMappingURL=chunk-
|
|
6048
|
+
//# sourceMappingURL=chunk-WWFIEANS.js.map
|