@playdrop/playdrop-cli 0.5.2 → 0.5.4

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 (118) hide show
  1. package/config/client-meta.json +4 -4
  2. package/dist/apps/build.js +49 -6
  3. package/dist/apps/index.d.ts +2 -0
  4. package/dist/apps/index.js +2 -0
  5. package/dist/apps/upload.d.ts +2 -0
  6. package/dist/apps/upload.js +126 -28
  7. package/dist/assetSpecs.d.ts +16 -0
  8. package/dist/assetSpecs.js +263 -0
  9. package/dist/assets/model-artifacts.js +3 -0
  10. package/dist/catalogue.d.ts +57 -3
  11. package/dist/catalogue.js +342 -16
  12. package/dist/commandContext.d.ts +6 -2
  13. package/dist/commandContext.js +144 -20
  14. package/dist/commands/accounts.d.ts +2 -0
  15. package/dist/commands/accounts.js +48 -0
  16. package/dist/commands/ads.d.ts +8 -0
  17. package/dist/commands/ads.js +124 -0
  18. package/dist/commands/boosts.d.ts +25 -0
  19. package/dist/commands/boosts.js +209 -0
  20. package/dist/commands/browse.d.ts +6 -1
  21. package/dist/commands/browse.js +365 -124
  22. package/dist/commands/capture.js +30 -9
  23. package/dist/commands/captureListing.d.ts +53 -0
  24. package/dist/commands/captureListing.js +815 -0
  25. package/dist/commands/create.d.ts +1 -0
  26. package/dist/commands/create.js +183 -3
  27. package/dist/commands/credits.d.ts +6 -0
  28. package/dist/commands/credits.js +47 -1
  29. package/dist/commands/detail.js +38 -4
  30. package/dist/commands/dev.js +169 -192
  31. package/dist/commands/devServer.d.ts +26 -3
  32. package/dist/commands/devServer.js +415 -72
  33. package/dist/commands/login.js +10 -2
  34. package/dist/commands/logout.d.ts +6 -1
  35. package/dist/commands/logout.js +25 -3
  36. package/dist/commands/search.d.ts +5 -0
  37. package/dist/commands/search.js +139 -17
  38. package/dist/commands/tags.d.ts +7 -0
  39. package/dist/commands/tags.js +63 -0
  40. package/dist/commands/upload-content.d.ts +13 -3
  41. package/dist/commands/upload-content.js +86 -20
  42. package/dist/commands/upload.d.ts +2 -0
  43. package/dist/commands/upload.js +187 -11
  44. package/dist/commands/validate.js +163 -2
  45. package/dist/commands/versionsBrowse.js +128 -91
  46. package/dist/commands/whoami.js +10 -2
  47. package/dist/config.d.ts +37 -0
  48. package/dist/config.js +205 -3
  49. package/dist/index.js +177 -5
  50. package/dist/refs.d.ts +2 -2
  51. package/dist/refs.js +13 -1
  52. package/dist/taskSelection.js +6 -3
  53. package/dist/taskUtils.d.ts +2 -2
  54. package/dist/taskUtils.js +1 -0
  55. package/dist/uploadLog.d.ts +1 -1
  56. package/dist/uploadLog.js +2 -2
  57. package/dist/workspaceAuth.d.ts +14 -0
  58. package/dist/workspaceAuth.js +75 -0
  59. package/node_modules/@playdrop/ai-client/package.json +1 -1
  60. package/node_modules/@playdrop/api-client/dist/client.d.ts +139 -10
  61. package/node_modules/@playdrop/api-client/dist/client.d.ts.map +1 -1
  62. package/node_modules/@playdrop/api-client/dist/client.js +6 -0
  63. package/node_modules/@playdrop/api-client/dist/domains/admin.d.ts +9 -1
  64. package/node_modules/@playdrop/api-client/dist/domains/admin.d.ts.map +1 -1
  65. package/node_modules/@playdrop/api-client/dist/domains/admin.js +45 -0
  66. package/node_modules/@playdrop/api-client/dist/domains/apps.d.ts +7 -1
  67. package/node_modules/@playdrop/api-client/dist/domains/apps.d.ts.map +1 -1
  68. package/node_modules/@playdrop/api-client/dist/domains/apps.js +58 -0
  69. package/node_modules/@playdrop/api-client/dist/domains/asset-packs.d.ts +2 -0
  70. package/node_modules/@playdrop/api-client/dist/domains/asset-packs.d.ts.map +1 -1
  71. package/node_modules/@playdrop/api-client/dist/domains/asset-packs.js +16 -0
  72. package/node_modules/@playdrop/api-client/dist/domains/assets.d.ts +44 -2
  73. package/node_modules/@playdrop/api-client/dist/domains/assets.d.ts.map +1 -1
  74. package/node_modules/@playdrop/api-client/dist/domains/assets.js +260 -3
  75. package/node_modules/@playdrop/api-client/dist/domains/payments.d.ts +22 -1
  76. package/node_modules/@playdrop/api-client/dist/domains/payments.d.ts.map +1 -1
  77. package/node_modules/@playdrop/api-client/dist/domains/payments.js +228 -0
  78. package/node_modules/@playdrop/api-client/dist/domains/search.d.ts.map +1 -1
  79. package/node_modules/@playdrop/api-client/dist/domains/search.js +39 -11
  80. package/node_modules/@playdrop/api-client/dist/domains/tags.d.ts +34 -0
  81. package/node_modules/@playdrop/api-client/dist/domains/tags.d.ts.map +1 -0
  82. package/node_modules/@playdrop/api-client/dist/domains/tags.js +111 -0
  83. package/node_modules/@playdrop/api-client/dist/index.d.ts +69 -1
  84. package/node_modules/@playdrop/api-client/dist/index.d.ts.map +1 -1
  85. package/node_modules/@playdrop/api-client/dist/index.js +74 -0
  86. package/node_modules/@playdrop/api-client/package.json +1 -1
  87. package/node_modules/@playdrop/boxel-core/package.json +1 -1
  88. package/node_modules/@playdrop/boxel-three/package.json +1 -1
  89. package/node_modules/@playdrop/config/client-meta.json +4 -4
  90. package/node_modules/@playdrop/config/dist/src/constants.d.ts +11 -0
  91. package/node_modules/@playdrop/config/dist/src/constants.d.ts.map +1 -1
  92. package/node_modules/@playdrop/config/dist/src/constants.js +12 -1
  93. package/node_modules/@playdrop/config/dist/tsconfig.tsbuildinfo +1 -1
  94. package/node_modules/@playdrop/config/package.json +1 -1
  95. package/node_modules/@playdrop/types/dist/api.d.ts +366 -6
  96. package/node_modules/@playdrop/types/dist/api.d.ts.map +1 -1
  97. package/node_modules/@playdrop/types/dist/api.js +52 -1
  98. package/node_modules/@playdrop/types/dist/asset-pack.d.ts +7 -1
  99. package/node_modules/@playdrop/types/dist/asset-pack.d.ts.map +1 -1
  100. package/node_modules/@playdrop/types/dist/asset-spec-contract-meta-schema.json +86 -0
  101. package/node_modules/@playdrop/types/dist/asset-spec.d.ts +163 -0
  102. package/node_modules/@playdrop/types/dist/asset-spec.d.ts.map +1 -0
  103. package/node_modules/@playdrop/types/dist/asset-spec.js +101 -0
  104. package/node_modules/@playdrop/types/dist/asset.d.ts +23 -6
  105. package/node_modules/@playdrop/types/dist/asset.d.ts.map +1 -1
  106. package/node_modules/@playdrop/types/dist/asset.js +4 -1
  107. package/node_modules/@playdrop/types/dist/graph.d.ts +4 -2
  108. package/node_modules/@playdrop/types/dist/graph.d.ts.map +1 -1
  109. package/node_modules/@playdrop/types/dist/graph.js +9 -2
  110. package/node_modules/@playdrop/types/dist/index.d.ts +1 -0
  111. package/node_modules/@playdrop/types/dist/index.d.ts.map +1 -1
  112. package/node_modules/@playdrop/types/dist/index.js +1 -0
  113. package/node_modules/@playdrop/types/dist/version.d.ts +13 -0
  114. package/node_modules/@playdrop/types/dist/version.d.ts.map +1 -1
  115. package/node_modules/@playdrop/types/dist/version.js +21 -0
  116. package/node_modules/@playdrop/types/package.json +6 -1
  117. package/node_modules/@playdrop/vox-three/package.json +1 -1
  118. package/package.json +3 -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,36 +140,227 @@ 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',
70
178
  env: { ...process.env },
71
179
  });
72
- child.on('exit', code => {
73
- if (code !== 0) {
74
- console.warn(`npm run dev exited with code ${code}.`);
180
+ return child;
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;
75
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),
76
241
  });
77
- return child;
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
+ }
78
273
  }
79
274
  async function startDevServer(options) {
80
- const { appName, htmlPath, port, projectInfo } = options;
275
+ const { appName, appType, creatorUsername, htmlPath, port, projectInfo } = options;
276
+ const ownerPid = process.pid;
81
277
  let closing = false;
82
278
  const devProcess = spawnDevScript(projectInfo);
279
+ if (devProcess) {
280
+ devProcess.on('exit', code => {
281
+ if (closing) {
282
+ return;
283
+ }
284
+ if (code !== 0 && code !== null) {
285
+ console.warn(`npm run dev exited with code ${code}.`);
286
+ }
287
+ });
288
+ }
83
289
  const startedDevProcess = Boolean(devProcess);
84
290
  const staticRoot = (0, node_path_1.resolve)((0, node_path_1.dirname)(htmlPath));
85
- 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) => {
86
361
  const method = req.method || 'GET';
87
- // Enable CORS for development
88
362
  res.setHeader('Access-Control-Allow-Origin', '*');
89
- res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
363
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
90
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');
91
365
  res.setHeader('Access-Control-Allow-Private-Network', 'true');
92
366
  if (method === 'OPTIONS') {
@@ -95,23 +369,134 @@ async function startDevServer(options) {
95
369
  res.end();
96
370
  return;
97
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
+ }
98
480
  if (method !== 'GET' && method !== 'HEAD') {
99
- res.setHeader('Allow', 'GET, HEAD, OPTIONS');
481
+ res.setHeader('Allow', 'GET, HEAD, POST, DELETE, OPTIONS');
100
482
  respondWithText(res, method, 405, 'Method not allowed');
101
483
  return;
102
484
  }
103
- const url = new URL(req.url || '', `http://localhost:${port}`);
104
- let pathname;
105
- try {
106
- pathname = decodeURIComponent(url.pathname);
485
+ cleanupStaleRouterMounts();
486
+ const parsedMountPath = parseMountPath(pathname);
487
+ if (!parsedMountPath) {
488
+ respondWithText(res, method, 404, 'Not found');
489
+ return;
107
490
  }
108
- catch {
109
- 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');
110
495
  return;
111
496
  }
112
- if (pathname === `/apps/${appName}.html` || pathname === `/${appName}.html`) {
497
+ if (parsedMountPath.assetPath === 'index.html') {
113
498
  try {
114
- const freshHtml = (0, node_fs_1.readFileSync)(htmlPath);
499
+ const freshHtml = (0, node_fs_1.readFileSync)(mount.htmlPath);
115
500
  respondWithBuffer(res, method, 200, 'text/html; charset=utf-8', freshHtml);
116
501
  }
117
502
  catch (error) {
@@ -119,12 +504,7 @@ async function startDevServer(options) {
119
504
  }
120
505
  return;
121
506
  }
122
- if (!pathname.startsWith('/apps/')) {
123
- respondWithText(res, method, 404, 'Not found');
124
- return;
125
- }
126
- const relativePath = pathname.slice('/apps/'.length);
127
- const absolutePath = resolveStaticPath(staticRoot, relativePath);
507
+ const absolutePath = resolveStaticPath(mount.staticRoot, parsedMountPath.assetPath);
128
508
  if (!absolutePath) {
129
509
  respondWithText(res, method, 400, 'Invalid static asset path');
130
510
  return;
@@ -141,61 +521,24 @@ async function startDevServer(options) {
141
521
  catch {
142
522
  respondWithText(res, method, 404, 'Not found');
143
523
  }
524
+ };
525
+ const server = node_http_1.default.createServer((req, res) => {
526
+ void handleRequest(req, res);
144
527
  });
145
- await new Promise((resolve, reject) => {
528
+ await new Promise((resolveListen, rejectListen) => {
146
529
  const onError = (error) => {
147
530
  server.off('listening', onListening);
148
- reject(error);
531
+ rejectListen(error);
149
532
  };
150
533
  const onListening = () => {
151
534
  server.off('error', onError);
152
- resolve();
535
+ resolveListen();
153
536
  };
154
537
  server.once('error', onError);
155
538
  server.once('listening', onListening);
156
- server.listen(port);
539
+ server.listen(port, DEV_ROUTER_HOST);
540
+ });
541
+ await new Promise(() => {
542
+ // Keep the router alive until the process exits.
157
543
  });
158
- const handle = {
159
- server,
160
- devProcess,
161
- startedDevProcess,
162
- appUrl: `http://localhost:${port}/apps/${appName}.html`,
163
- close: async () => {
164
- if (closing)
165
- return;
166
- closing = true;
167
- await new Promise((resolveClose, rejectClose) => {
168
- server.close(error => {
169
- if (error) {
170
- rejectClose(error);
171
- }
172
- else {
173
- resolveClose();
174
- }
175
- });
176
- });
177
- if (devProcess && !devProcess.killed) {
178
- devProcess.kill();
179
- }
180
- },
181
- };
182
- return handle;
183
- }
184
- async function isDevServerAvailable(appName, port, timeoutMs = 1000) {
185
- const controller = new AbortController();
186
- const timeout = setTimeout(() => controller.abort(), timeoutMs);
187
- timeout.unref?.();
188
- try {
189
- await fetch(`http://127.0.0.1:${port}/apps/${encodeURIComponent(appName)}.html`, {
190
- method: 'GET',
191
- signal: controller.signal,
192
- });
193
- return true;
194
- }
195
- catch {
196
- return false;
197
- }
198
- finally {
199
- clearTimeout(timeout);
200
- }
201
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
  }
@@ -3,10 +3,15 @@ type SearchOptions = {
3
3
  appType?: string;
4
4
  assetCategory?: string;
5
5
  assetSubcategory?: string;
6
+ assetSpec?: string;
7
+ assetSpecOwner?: string;
8
+ assetSpecName?: string;
6
9
  packContainsCategory?: string;
7
10
  packContainsSubcategory?: string;
11
+ sort?: string;
8
12
  limit?: string | number;
9
13
  offset?: string | number;
14
+ tag?: string[];
10
15
  json?: boolean;
11
16
  };
12
17
  export declare function search(query: string | undefined, options?: SearchOptions): Promise<void>;