@playdrop/playdrop-cli 0.5.3 → 0.5.5

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.
Files changed (47) hide show
  1. package/config/client-meta.json +4 -4
  2. package/dist/commandContext.d.ts +6 -2
  3. package/dist/commandContext.js +144 -20
  4. package/dist/commands/accounts.d.ts +2 -0
  5. package/dist/commands/accounts.js +48 -0
  6. package/dist/commands/capture.js +30 -9
  7. package/dist/commands/captureListing.js +16 -5
  8. package/dist/commands/dev.js +169 -192
  9. package/dist/commands/devServer.d.ts +26 -3
  10. package/dist/commands/devServer.js +406 -68
  11. package/dist/commands/login.js +10 -2
  12. package/dist/commands/logout.d.ts +6 -1
  13. package/dist/commands/logout.js +25 -3
  14. package/dist/commands/whoami.js +10 -2
  15. package/dist/config.d.ts +37 -0
  16. package/dist/config.js +205 -3
  17. package/dist/index.js +32 -2
  18. package/dist/workspaceAuth.d.ts +14 -0
  19. package/dist/workspaceAuth.js +75 -0
  20. package/node_modules/@playdrop/ai-client/package.json +1 -1
  21. package/node_modules/@playdrop/api-client/dist/client.d.ts +10 -1
  22. package/node_modules/@playdrop/api-client/dist/client.d.ts.map +1 -1
  23. package/node_modules/@playdrop/api-client/dist/domains/admin.d.ts +2 -1
  24. package/node_modules/@playdrop/api-client/dist/domains/admin.d.ts.map +1 -1
  25. package/node_modules/@playdrop/api-client/dist/domains/admin.js +11 -0
  26. package/node_modules/@playdrop/api-client/dist/domains/apps.d.ts +4 -1
  27. package/node_modules/@playdrop/api-client/dist/domains/apps.d.ts.map +1 -1
  28. package/node_modules/@playdrop/api-client/dist/domains/apps.js +31 -0
  29. package/node_modules/@playdrop/api-client/dist/domains/payments.d.ts +5 -0
  30. package/node_modules/@playdrop/api-client/dist/domains/payments.d.ts.map +1 -1
  31. package/node_modules/@playdrop/api-client/dist/domains/payments.js +55 -0
  32. package/node_modules/@playdrop/api-client/dist/index.d.ts +11 -0
  33. package/node_modules/@playdrop/api-client/dist/index.d.ts.map +1 -1
  34. package/node_modules/@playdrop/api-client/dist/index.js +27 -0
  35. package/node_modules/@playdrop/api-client/package.json +1 -1
  36. package/node_modules/@playdrop/boxel-core/package.json +1 -1
  37. package/node_modules/@playdrop/boxel-three/package.json +1 -1
  38. package/node_modules/@playdrop/config/client-meta.json +4 -4
  39. package/node_modules/@playdrop/config/package.json +1 -1
  40. package/node_modules/@playdrop/types/dist/api.d.ts +26 -0
  41. package/node_modules/@playdrop/types/dist/api.d.ts.map +1 -1
  42. package/node_modules/@playdrop/types/dist/version.d.ts +1 -1
  43. package/node_modules/@playdrop/types/dist/version.d.ts.map +1 -1
  44. package/node_modules/@playdrop/types/dist/version.js +1 -0
  45. package/node_modules/@playdrop/types/package.json +1 -1
  46. package/node_modules/@playdrop/vox-three/package.json +1 -1
  47. package/package.json +1 -1
@@ -3,12 +3,21 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.DEV_ROUTER_PORT = void 0;
7
+ exports.parseMountConflictError = parseMountConflictError;
8
+ exports.buildLocalDevAppUrl = buildLocalDevAppUrl;
9
+ exports.ensureDevRouterRunning = ensureDevRouterRunning;
6
10
  exports.startDevServer = startDevServer;
7
11
  exports.isDevServerAvailable = isDevServerAvailable;
12
+ exports.runDevRouterServer = runDevRouterServer;
8
13
  const node_http_1 = __importDefault(require("node:http"));
9
14
  const node_fs_1 = require("node:fs");
10
15
  const node_path_1 = require("node:path");
11
16
  const node_child_process_1 = require("node:child_process");
17
+ const node_crypto_1 = require("node:crypto");
18
+ exports.DEV_ROUTER_PORT = 8888;
19
+ const DEV_ROUTER_HOST = '127.0.0.1';
20
+ const CONTROL_PREFIX = '/_playdrop';
12
21
  const CONTENT_TYPE_BY_EXTENSION = {
13
22
  '.html': 'text/html; charset=utf-8',
14
23
  '.js': 'application/javascript; charset=utf-8',
@@ -27,14 +36,57 @@ const CONTENT_TYPE_BY_EXTENSION = {
27
36
  '.mp4': 'video/mp4',
28
37
  '.webm': 'video/webm',
29
38
  };
39
+ const routerMountsById = new Map();
40
+ const routerMountIdsByKey = new Map();
30
41
  function resolveContentType(filePath) {
31
42
  const extension = (0, node_path_1.extname)(filePath).toLowerCase();
32
43
  return CONTENT_TYPE_BY_EXTENSION[extension] || 'application/octet-stream';
33
44
  }
45
+ function getCliEntrypointPath() {
46
+ return (0, node_path_1.resolve)(__dirname, '..', 'index.js');
47
+ }
48
+ function buildMountKey(creatorUsername, appType, appName) {
49
+ return `${creatorUsername}:${appType}:${appName}`;
50
+ }
51
+ function encodeMountConflictError(details) {
52
+ const encoded = Buffer.from(JSON.stringify(details), 'utf8').toString('base64url');
53
+ return `mount_conflict:${encoded}`;
54
+ }
55
+ function parseMountConflictError(message) {
56
+ if (!message.startsWith('mount_conflict:')) {
57
+ return null;
58
+ }
59
+ try {
60
+ const decoded = Buffer.from(message.slice('mount_conflict:'.length), 'base64url').toString('utf8');
61
+ const payload = JSON.parse(decoded);
62
+ if (typeof payload.ref !== 'string'
63
+ || typeof payload.ownerPid !== 'number'
64
+ || !Number.isInteger(payload.ownerPid)
65
+ || payload.ownerPid <= 0
66
+ || typeof payload.repoRoot !== 'string'
67
+ || typeof payload.htmlPath !== 'string') {
68
+ return null;
69
+ }
70
+ return {
71
+ ref: payload.ref,
72
+ ownerPid: payload.ownerPid,
73
+ repoRoot: payload.repoRoot,
74
+ htmlPath: payload.htmlPath,
75
+ };
76
+ }
77
+ catch {
78
+ return null;
79
+ }
80
+ }
81
+ function buildLocalDevAppUrl(input) {
82
+ const port = input.port ?? exports.DEV_ROUTER_PORT;
83
+ return `http://${DEV_ROUTER_HOST}:${port}/apps/dev/${encodeURIComponent(input.creatorUsername)}/${encodeURIComponent(input.appType)}/${encodeURIComponent(input.appName)}/index.html`;
84
+ }
34
85
  function respondWithBuffer(res, method, statusCode, contentType, payload) {
35
86
  res.statusCode = statusCode;
36
87
  res.setHeader('Content-Type', contentType);
37
88
  res.setHeader('Content-Length', String(payload.length));
89
+ res.setHeader('Cache-Control', 'no-store');
38
90
  if (method === 'HEAD') {
39
91
  res.end();
40
92
  return;
@@ -45,6 +97,37 @@ function respondWithText(res, method, statusCode, message) {
45
97
  const payload = Buffer.from(message, 'utf8');
46
98
  respondWithBuffer(res, method, statusCode, 'text/plain; charset=utf-8', payload);
47
99
  }
100
+ function sendJson(res, statusCode, payload) {
101
+ const body = Buffer.from(JSON.stringify(payload), 'utf8');
102
+ respondWithBuffer(res, 'GET', statusCode, 'application/json; charset=utf-8', body);
103
+ }
104
+ function isPidAlive(pid) {
105
+ if (!Number.isInteger(pid) || pid <= 0) {
106
+ return false;
107
+ }
108
+ try {
109
+ process.kill(pid, 0);
110
+ return true;
111
+ }
112
+ catch {
113
+ return false;
114
+ }
115
+ }
116
+ function removeRouterMount(mountId) {
117
+ const mount = routerMountsById.get(mountId);
118
+ if (!mount) {
119
+ return;
120
+ }
121
+ routerMountsById.delete(mountId);
122
+ routerMountIdsByKey.delete(mount.key);
123
+ }
124
+ function cleanupStaleRouterMounts() {
125
+ for (const [mountId, mount] of routerMountsById.entries()) {
126
+ if (!isPidAlive(mount.ownerPid)) {
127
+ removeRouterMount(mountId);
128
+ }
129
+ }
130
+ }
48
131
  function resolveStaticPath(staticRoot, rawRelativePath) {
49
132
  const normalizedRelativePath = (0, node_path_1.normalize)(rawRelativePath.replace(/^\/+/, ''));
50
133
  if (!normalizedRelativePath || normalizedRelativePath === '.' || normalizedRelativePath.startsWith('..')) {
@@ -57,13 +140,38 @@ function resolveStaticPath(staticRoot, rawRelativePath) {
57
140
  }
58
141
  return absolutePath;
59
142
  }
143
+ function parseMountPath(pathname) {
144
+ if (!pathname.startsWith('/apps/dev/')) {
145
+ return null;
146
+ }
147
+ const segments = pathname.split('/').filter(Boolean);
148
+ if (segments.length < 6) {
149
+ return null;
150
+ }
151
+ // filter(Boolean) removes the leading empty segment from absolute paths, so the
152
+ // creator tuple starts at indices 2..4 for /apps/dev/<creator>/<type>/<name>/...
153
+ const [, , creatorUsername, appType, appName, ...rest] = segments;
154
+ if (!creatorUsername || !appType || !appName || rest.length === 0) {
155
+ return null;
156
+ }
157
+ return {
158
+ key: buildMountKey(decodeURIComponent(creatorUsername), decodeURIComponent(appType), decodeURIComponent(appName)),
159
+ assetPath: rest.map((entry) => decodeURIComponent(entry)).join('/'),
160
+ };
161
+ }
162
+ async function readJsonBody(req) {
163
+ const chunks = [];
164
+ for await (const chunk of req) {
165
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
166
+ }
167
+ const raw = Buffer.concat(chunks).toString('utf8').trim();
168
+ return raw ? JSON.parse(raw) : {};
169
+ }
60
170
  function spawnDevScript(projectInfo) {
61
171
  if (!projectInfo.projectDir || !projectInfo.packageJson || typeof projectInfo.packageJson.scripts?.dev !== 'string') {
62
172
  return null;
63
173
  }
64
174
  const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
65
- const projectLabel = (0, node_path_1.relative)(process.cwd(), projectInfo.projectDir) || projectInfo.projectDir;
66
- console.log(`▶ Running "npm run dev" in ${projectLabel}`);
67
175
  const child = (0, node_child_process_1.spawn)(npmCommand, ['run', 'dev'], {
68
176
  cwd: projectInfo.projectDir,
69
177
  stdio: 'inherit',
@@ -71,8 +179,101 @@ function spawnDevScript(projectInfo) {
71
179
  });
72
180
  return child;
73
181
  }
182
+ async function fetchRouterJson(path, init = {}, port = exports.DEV_ROUTER_PORT) {
183
+ const response = await fetch(`http://${DEV_ROUTER_HOST}:${port}${path}`, init);
184
+ if (!response.ok) {
185
+ if (response.status === 404) {
186
+ return null;
187
+ }
188
+ const payload = await response.json().catch(() => null);
189
+ throw new Error(typeof payload?.error === 'string'
190
+ ? payload.error
191
+ : `router_request_failed:${response.status}`);
192
+ }
193
+ return await response.json();
194
+ }
195
+ async function isRouterHealthy(port = exports.DEV_ROUTER_PORT, timeoutMs = 400) {
196
+ const controller = new AbortController();
197
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
198
+ timeout.unref?.();
199
+ try {
200
+ const response = await fetch(`http://${DEV_ROUTER_HOST}:${port}${CONTROL_PREFIX}/health`, {
201
+ method: 'GET',
202
+ signal: controller.signal,
203
+ });
204
+ return response.ok;
205
+ }
206
+ catch {
207
+ return false;
208
+ }
209
+ finally {
210
+ clearTimeout(timeout);
211
+ }
212
+ }
213
+ async function ensureDevRouterRunning(port = exports.DEV_ROUTER_PORT) {
214
+ if (await isRouterHealthy(port)) {
215
+ return;
216
+ }
217
+ const cliEntrypoint = getCliEntrypointPath();
218
+ const child = (0, node_child_process_1.spawn)(process.execPath, [cliEntrypoint, 'project', '_dev-router', 'serve'], {
219
+ detached: true,
220
+ stdio: 'ignore',
221
+ env: {
222
+ ...process.env,
223
+ PLAYDROP_DEV_ROUTER_PORT: String(port),
224
+ },
225
+ });
226
+ child.unref();
227
+ const deadline = Date.now() + 5000;
228
+ while (Date.now() < deadline) {
229
+ if (await isRouterHealthy(port, 250)) {
230
+ return;
231
+ }
232
+ await new Promise((resolveTimeout) => setTimeout(resolveTimeout, 150));
233
+ }
234
+ throw new Error('dev_router_start_failed');
235
+ }
236
+ async function registerDevMount(input, port = exports.DEV_ROUTER_PORT) {
237
+ const response = await fetch(`http://${DEV_ROUTER_HOST}:${port}${CONTROL_PREFIX}/mounts`, {
238
+ method: 'POST',
239
+ headers: { 'Content-Type': 'application/json' },
240
+ body: JSON.stringify(input),
241
+ });
242
+ const payload = await response.json().catch(() => null);
243
+ if (!response.ok || !payload) {
244
+ throw new Error(`mount_register_failed:${response.status}`);
245
+ }
246
+ if ('error' in payload) {
247
+ throw new Error(encodeMountConflictError({
248
+ ref: `${payload.creatorUsername}/${payload.appType}/${payload.appName}`,
249
+ ownerPid: payload.ownerPid,
250
+ repoRoot: payload.repoRoot,
251
+ htmlPath: payload.htmlPath,
252
+ }));
253
+ }
254
+ return {
255
+ mountId: payload.mountId,
256
+ appUrl: payload.appUrl,
257
+ };
258
+ }
259
+ async function heartbeatDevMount(mountId, ownerPid, port = exports.DEV_ROUTER_PORT) {
260
+ await fetchRouterJson(`${CONTROL_PREFIX}/mounts/${encodeURIComponent(mountId)}/heartbeat`, {
261
+ method: 'POST',
262
+ headers: { 'Content-Type': 'application/json' },
263
+ body: JSON.stringify({ ownerPid }),
264
+ }, port);
265
+ }
266
+ async function unregisterDevMount(mountId, port = exports.DEV_ROUTER_PORT) {
267
+ const response = await fetch(`http://${DEV_ROUTER_HOST}:${port}${CONTROL_PREFIX}/mounts/${encodeURIComponent(mountId)}`, {
268
+ method: 'DELETE',
269
+ });
270
+ if (!response.ok && response.status !== 404) {
271
+ throw new Error(`mount_unregister_failed:${response.status}`);
272
+ }
273
+ }
74
274
  async function startDevServer(options) {
75
- const { appName, htmlPath, port, projectInfo } = options;
275
+ const { appName, appType, creatorUsername, htmlPath, port, projectInfo } = options;
276
+ const ownerPid = process.pid;
76
277
  let closing = false;
77
278
  const devProcess = spawnDevScript(projectInfo);
78
279
  if (devProcess) {
@@ -87,11 +288,79 @@ async function startDevServer(options) {
87
288
  }
88
289
  const startedDevProcess = Boolean(devProcess);
89
290
  const staticRoot = (0, node_path_1.resolve)((0, node_path_1.dirname)(htmlPath));
90
- const server = node_http_1.default.createServer((req, res) => {
291
+ const repoRoot = (0, node_path_1.resolve)(projectInfo.projectDir ?? (0, node_path_1.dirname)(htmlPath));
292
+ let mountId = '';
293
+ let appUrl = '';
294
+ try {
295
+ await ensureDevRouterRunning(port);
296
+ const registeredMount = await registerDevMount({
297
+ creatorUsername,
298
+ appType,
299
+ appName,
300
+ htmlPath,
301
+ staticRoot,
302
+ repoRoot,
303
+ ownerPid,
304
+ }, port);
305
+ mountId = registeredMount.mountId;
306
+ appUrl = registeredMount.appUrl;
307
+ }
308
+ catch (error) {
309
+ if (devProcess && !devProcess.killed) {
310
+ devProcess.kill();
311
+ }
312
+ throw error;
313
+ }
314
+ const heartbeat = setInterval(() => {
315
+ heartbeatDevMount(mountId, ownerPid, port).catch(() => {
316
+ // Let the serving request fail naturally if the router disappears.
317
+ });
318
+ }, 2000);
319
+ return {
320
+ server: null,
321
+ devProcess,
322
+ startedDevProcess,
323
+ appUrl,
324
+ close: async () => {
325
+ if (closing)
326
+ return;
327
+ closing = true;
328
+ clearInterval(heartbeat);
329
+ try {
330
+ await unregisterDevMount(mountId, port);
331
+ }
332
+ finally {
333
+ if (devProcess && !devProcess.killed) {
334
+ devProcess.kill();
335
+ }
336
+ }
337
+ },
338
+ };
339
+ }
340
+ async function isDevServerAvailable(input, timeoutMs = 1000) {
341
+ const controller = new AbortController();
342
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
343
+ timeout.unref?.();
344
+ try {
345
+ await fetch(buildLocalDevAppUrl(input), {
346
+ method: 'GET',
347
+ signal: controller.signal,
348
+ });
349
+ return true;
350
+ }
351
+ catch {
352
+ return false;
353
+ }
354
+ finally {
355
+ clearTimeout(timeout);
356
+ }
357
+ }
358
+ async function runDevRouterServer(port = Number(process.env.PLAYDROP_DEV_ROUTER_PORT) || exports.DEV_ROUTER_PORT) {
359
+ // eslint-disable-next-line complexity
360
+ const handleRequest = async (req, res) => {
91
361
  const method = req.method || 'GET';
92
- // Enable CORS for development
93
362
  res.setHeader('Access-Control-Allow-Origin', '*');
94
- res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
363
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
95
364
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Playdrop-Client, X-Playdrop-Client-Version, X-Playdrop-Client-Build, X-Playdrop-Platform, X-Playdrop-Platform-Version');
96
365
  res.setHeader('Access-Control-Allow-Private-Network', 'true');
97
366
  if (method === 'OPTIONS') {
@@ -100,23 +369,134 @@ async function startDevServer(options) {
100
369
  res.end();
101
370
  return;
102
371
  }
372
+ let pathname = '/';
373
+ try {
374
+ pathname = decodeURIComponent(new URL(req.url || '/', `http://${DEV_ROUTER_HOST}:${port}`).pathname);
375
+ }
376
+ catch {
377
+ respondWithText(res, method, 400, 'Invalid URL path');
378
+ return;
379
+ }
380
+ if (pathname === `${CONTROL_PREFIX}/health` && method === 'GET') {
381
+ sendJson(res, 200, { status: 'ok' });
382
+ return;
383
+ }
384
+ if (pathname === `${CONTROL_PREFIX}/mounts` && method === 'POST') {
385
+ cleanupStaleRouterMounts();
386
+ const body = await readJsonBody(req);
387
+ const creatorUsername = body.creatorUsername?.trim();
388
+ const appType = body.appType?.trim();
389
+ const appName = body.appName?.trim();
390
+ const htmlPath = body.htmlPath?.trim();
391
+ const staticRoot = body.staticRoot?.trim();
392
+ const repoRoot = body.repoRoot?.trim();
393
+ const ownerPid = Number(body.ownerPid);
394
+ if (!creatorUsername || !appType || !appName || !htmlPath || !staticRoot || !repoRoot || !Number.isInteger(ownerPid) || ownerPid <= 0) {
395
+ sendJson(res, 400, { error: 'invalid_mount_request' });
396
+ return;
397
+ }
398
+ const key = buildMountKey(creatorUsername, appType, appName);
399
+ const existingMountId = routerMountIdsByKey.get(key);
400
+ if (existingMountId) {
401
+ const existingMount = routerMountsById.get(existingMountId);
402
+ if (existingMount && isPidAlive(existingMount.ownerPid)) {
403
+ if (existingMount.htmlPath !== htmlPath || existingMount.staticRoot !== staticRoot) {
404
+ sendJson(res, 409, {
405
+ error: 'mount_conflict',
406
+ creatorUsername,
407
+ appType,
408
+ appName,
409
+ ownerPid: existingMount.ownerPid,
410
+ repoRoot: existingMount.repoRoot,
411
+ htmlPath: existingMount.htmlPath,
412
+ });
413
+ return;
414
+ }
415
+ existingMount.ownerPid = ownerPid;
416
+ existingMount.updatedAt = Date.now();
417
+ sendJson(res, 200, {
418
+ mountId: existingMount.id,
419
+ appUrl: buildLocalDevAppUrl({ creatorUsername, appType, appName, port }),
420
+ creatorUsername,
421
+ appType,
422
+ appName,
423
+ });
424
+ return;
425
+ }
426
+ removeRouterMount(existingMountId);
427
+ }
428
+ const mount = {
429
+ id: (0, node_crypto_1.randomUUID)(),
430
+ key,
431
+ creatorUsername,
432
+ appType,
433
+ appName,
434
+ htmlPath,
435
+ staticRoot,
436
+ repoRoot,
437
+ ownerPid,
438
+ updatedAt: Date.now(),
439
+ };
440
+ routerMountsById.set(mount.id, mount);
441
+ routerMountIdsByKey.set(key, mount.id);
442
+ sendJson(res, 200, {
443
+ mountId: mount.id,
444
+ appUrl: buildLocalDevAppUrl({ creatorUsername, appType, appName, port }),
445
+ creatorUsername,
446
+ appType,
447
+ appName,
448
+ });
449
+ return;
450
+ }
451
+ if (pathname.startsWith(`${CONTROL_PREFIX}/mounts/`) && pathname.endsWith('/heartbeat') && method === 'POST') {
452
+ cleanupStaleRouterMounts();
453
+ const mountId = pathname.slice(`${CONTROL_PREFIX}/mounts/`.length, -'/heartbeat'.length).replace(/\/$/, '');
454
+ const mount = routerMountsById.get(mountId);
455
+ if (!mount) {
456
+ sendJson(res, 404, { error: 'mount_not_found' });
457
+ return;
458
+ }
459
+ const body = await readJsonBody(req);
460
+ const ownerPid = Number(body.ownerPid);
461
+ if (!Number.isInteger(ownerPid) || ownerPid <= 0) {
462
+ sendJson(res, 400, { error: 'invalid_mount_owner_pid' });
463
+ return;
464
+ }
465
+ if (ownerPid !== mount.ownerPid) {
466
+ sendJson(res, 409, { error: 'mount_owner_pid_mismatch' });
467
+ return;
468
+ }
469
+ mount.ownerPid = ownerPid;
470
+ mount.updatedAt = Date.now();
471
+ sendJson(res, 200, { ok: true });
472
+ return;
473
+ }
474
+ if (pathname.startsWith(`${CONTROL_PREFIX}/mounts/`) && method === 'DELETE') {
475
+ const mountId = pathname.slice(`${CONTROL_PREFIX}/mounts/`.length);
476
+ removeRouterMount(mountId);
477
+ sendJson(res, 200, { ok: true });
478
+ return;
479
+ }
103
480
  if (method !== 'GET' && method !== 'HEAD') {
104
- res.setHeader('Allow', 'GET, HEAD, OPTIONS');
481
+ res.setHeader('Allow', 'GET, HEAD, POST, DELETE, OPTIONS');
105
482
  respondWithText(res, method, 405, 'Method not allowed');
106
483
  return;
107
484
  }
108
- const url = new URL(req.url || '', `http://localhost:${port}`);
109
- let pathname;
110
- try {
111
- pathname = decodeURIComponent(url.pathname);
485
+ cleanupStaleRouterMounts();
486
+ const parsedMountPath = parseMountPath(pathname);
487
+ if (!parsedMountPath) {
488
+ respondWithText(res, method, 404, 'Not found');
489
+ return;
112
490
  }
113
- catch {
114
- respondWithText(res, method, 400, 'Invalid URL path');
491
+ const mountId = routerMountIdsByKey.get(parsedMountPath.key);
492
+ const mount = mountId ? routerMountsById.get(mountId) : null;
493
+ if (!mount) {
494
+ respondWithText(res, method, 404, 'Not found');
115
495
  return;
116
496
  }
117
- if (pathname === `/apps/${appName}.html` || pathname === `/${appName}.html`) {
497
+ if (parsedMountPath.assetPath === 'index.html') {
118
498
  try {
119
- const freshHtml = (0, node_fs_1.readFileSync)(htmlPath);
499
+ const freshHtml = (0, node_fs_1.readFileSync)(mount.htmlPath);
120
500
  respondWithBuffer(res, method, 200, 'text/html; charset=utf-8', freshHtml);
121
501
  }
122
502
  catch (error) {
@@ -124,12 +504,7 @@ async function startDevServer(options) {
124
504
  }
125
505
  return;
126
506
  }
127
- if (!pathname.startsWith('/apps/')) {
128
- respondWithText(res, method, 404, 'Not found');
129
- return;
130
- }
131
- const relativePath = pathname.slice('/apps/'.length);
132
- const absolutePath = resolveStaticPath(staticRoot, relativePath);
507
+ const absolutePath = resolveStaticPath(mount.staticRoot, parsedMountPath.assetPath);
133
508
  if (!absolutePath) {
134
509
  respondWithText(res, method, 400, 'Invalid static asset path');
135
510
  return;
@@ -146,61 +521,24 @@ async function startDevServer(options) {
146
521
  catch {
147
522
  respondWithText(res, method, 404, 'Not found');
148
523
  }
524
+ };
525
+ const server = node_http_1.default.createServer((req, res) => {
526
+ void handleRequest(req, res);
149
527
  });
150
- await new Promise((resolve, reject) => {
528
+ await new Promise((resolveListen, rejectListen) => {
151
529
  const onError = (error) => {
152
530
  server.off('listening', onListening);
153
- reject(error);
531
+ rejectListen(error);
154
532
  };
155
533
  const onListening = () => {
156
534
  server.off('error', onError);
157
- resolve();
535
+ resolveListen();
158
536
  };
159
537
  server.once('error', onError);
160
538
  server.once('listening', onListening);
161
- server.listen(port);
539
+ server.listen(port, DEV_ROUTER_HOST);
540
+ });
541
+ await new Promise(() => {
542
+ // Keep the router alive until the process exits.
162
543
  });
163
- const handle = {
164
- server,
165
- devProcess,
166
- startedDevProcess,
167
- appUrl: `http://localhost:${port}/apps/${appName}.html`,
168
- close: async () => {
169
- if (closing)
170
- return;
171
- closing = true;
172
- await new Promise((resolveClose, rejectClose) => {
173
- server.close(error => {
174
- if (error) {
175
- rejectClose(error);
176
- }
177
- else {
178
- resolveClose();
179
- }
180
- });
181
- });
182
- if (devProcess && !devProcess.killed) {
183
- devProcess.kill();
184
- }
185
- },
186
- };
187
- return handle;
188
- }
189
- async function isDevServerAvailable(appName, port, timeoutMs = 1000) {
190
- const controller = new AbortController();
191
- const timeout = setTimeout(() => controller.abort(), timeoutMs);
192
- timeout.unref?.();
193
- try {
194
- await fetch(`http://127.0.0.1:${port}/apps/${encodeURIComponent(appName)}.html`, {
195
- method: 'GET',
196
- signal: controller.signal,
197
- });
198
- return true;
199
- }
200
- catch {
201
- return false;
202
- }
203
- finally {
204
- clearTimeout(timeout);
205
- }
206
544
  }
@@ -31,8 +31,16 @@ function storeLogin(env, data) {
31
31
  process.exitCode = 1;
32
32
  return;
33
33
  }
34
- (0, config_1.saveConfig)({ token, env });
35
- const username = data.user?.username ?? 'unknown';
34
+ const username = typeof data.user?.username === 'string' ? data.user.username.trim() : '';
35
+ if (!username) {
36
+ (0, messages_1.printErrorWithHelp)('Login succeeded but the account username was missing.', [
37
+ 'Please try again in a moment.',
38
+ 'If the issue persists, contact the Playdrop team.',
39
+ ], { command: 'login' });
40
+ process.exitCode = 1;
41
+ return;
42
+ }
43
+ (0, config_1.saveAccountSession)({ username, env, token });
36
44
  console.log(`Logged in as ${username} on ${env}.`);
37
45
  console.log('Next: run "playdrop auth whoami" to confirm your session.');
38
46
  }
@@ -1 +1,6 @@
1
- export declare function logout(): void;
1
+ type LogoutOptions = {
2
+ username?: string;
3
+ env?: string;
4
+ };
5
+ export declare function logout(options?: LogoutOptions): void;
6
+ export {};
@@ -2,8 +2,30 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.logout = logout;
4
4
  const config_1 = require("../config");
5
- function logout() {
6
- (0, config_1.clearConfig)();
7
- console.log('Logged out.');
5
+ function logout(options = {}) {
6
+ const current = (0, config_1.getCurrentAccountSession)();
7
+ const username = options.username?.trim() || current?.username || '';
8
+ const env = options.env?.trim() || current?.env || '';
9
+ const existingSession = username ? (0, config_1.findAccountSession)(username, env || undefined) : null;
10
+ if (!existingSession) {
11
+ if (username && env) {
12
+ console.log(`No stored session found for ${username} on ${env}.`);
13
+ }
14
+ else if (username) {
15
+ console.log(`No stored session found for ${username}.`);
16
+ }
17
+ else {
18
+ console.log('No stored session found.');
19
+ }
20
+ console.log('Next: run "playdrop auth login" when you need a new session.');
21
+ return;
22
+ }
23
+ (0, config_1.removeAccountSession)({ username, env });
24
+ if (username && env) {
25
+ console.log(`Logged out ${username} on ${env}.`);
26
+ }
27
+ else {
28
+ console.log('Logged out.');
29
+ }
8
30
  console.log('Next: run "playdrop auth login" when you need a new session.');
9
31
  }
@@ -9,6 +9,7 @@ const environment_1 = require("../environment");
9
9
  const messages_1 = require("../messages");
10
10
  async function whoami() {
11
11
  const cfg = (0, config_1.loadConfig)();
12
+ const currentAccount = (0, config_1.getCurrentAccountSession)(cfg);
12
13
  if (!cfg.token || !cfg.env) {
13
14
  (0, messages_1.printLoginRequired)('Checking your Playdrop account status', 'whoami');
14
15
  process.exitCode = 1;
@@ -59,7 +60,7 @@ async function whoami() {
59
60
  }
60
61
  if (!data)
61
62
  return;
62
- const username = data.user?.username;
63
+ const username = data.user?.username?.trim();
63
64
  if (!username) {
64
65
  (0, messages_1.printErrorWithHelp)('The Playdrop API returned an unexpected response.', [
65
66
  'Retry "playdrop auth whoami" in a moment.',
@@ -68,6 +69,13 @@ async function whoami() {
68
69
  process.exitCode = 1;
69
70
  return;
70
71
  }
71
- console.log(`${username} (${cfg.env})`);
72
+ if (!currentAccount) {
73
+ (0, config_1.migrateLegacySession)({
74
+ username,
75
+ env: envConfig.name,
76
+ token: cfg.token,
77
+ });
78
+ }
79
+ console.log(`${username} (${envConfig.name})`);
72
80
  console.log('Next: run "playdrop getting-started" to see the recommended workflow.');
73
81
  }