@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.
- package/config/client-meta.json +4 -4
- package/dist/commandContext.d.ts +6 -2
- package/dist/commandContext.js +144 -20
- package/dist/commands/accounts.d.ts +2 -0
- package/dist/commands/accounts.js +48 -0
- package/dist/commands/capture.js +30 -9
- package/dist/commands/captureListing.js +16 -5
- package/dist/commands/dev.js +169 -192
- package/dist/commands/devServer.d.ts +26 -3
- package/dist/commands/devServer.js +406 -68
- package/dist/commands/login.js +10 -2
- package/dist/commands/logout.d.ts +6 -1
- package/dist/commands/logout.js +25 -3
- package/dist/commands/whoami.js +10 -2
- package/dist/config.d.ts +37 -0
- package/dist/config.js +205 -3
- package/dist/index.js +32 -2
- package/dist/workspaceAuth.d.ts +14 -0
- package/dist/workspaceAuth.js +75 -0
- package/node_modules/@playdrop/ai-client/package.json +1 -1
- package/node_modules/@playdrop/api-client/dist/client.d.ts +10 -1
- package/node_modules/@playdrop/api-client/dist/client.d.ts.map +1 -1
- package/node_modules/@playdrop/api-client/dist/domains/admin.d.ts +2 -1
- package/node_modules/@playdrop/api-client/dist/domains/admin.d.ts.map +1 -1
- package/node_modules/@playdrop/api-client/dist/domains/admin.js +11 -0
- package/node_modules/@playdrop/api-client/dist/domains/apps.d.ts +4 -1
- package/node_modules/@playdrop/api-client/dist/domains/apps.d.ts.map +1 -1
- package/node_modules/@playdrop/api-client/dist/domains/apps.js +31 -0
- package/node_modules/@playdrop/api-client/dist/domains/payments.d.ts +5 -0
- package/node_modules/@playdrop/api-client/dist/domains/payments.d.ts.map +1 -1
- package/node_modules/@playdrop/api-client/dist/domains/payments.js +55 -0
- package/node_modules/@playdrop/api-client/dist/index.d.ts +11 -0
- package/node_modules/@playdrop/api-client/dist/index.d.ts.map +1 -1
- package/node_modules/@playdrop/api-client/dist/index.js +27 -0
- package/node_modules/@playdrop/api-client/package.json +1 -1
- package/node_modules/@playdrop/boxel-core/package.json +1 -1
- package/node_modules/@playdrop/boxel-three/package.json +1 -1
- package/node_modules/@playdrop/config/client-meta.json +4 -4
- package/node_modules/@playdrop/config/package.json +1 -1
- package/node_modules/@playdrop/types/dist/api.d.ts +26 -0
- package/node_modules/@playdrop/types/dist/api.d.ts.map +1 -1
- package/node_modules/@playdrop/types/dist/version.d.ts +1 -1
- package/node_modules/@playdrop/types/dist/version.d.ts.map +1 -1
- package/node_modules/@playdrop/types/dist/version.js +1 -0
- package/node_modules/@playdrop/types/package.json +1 -1
- package/node_modules/@playdrop/vox-three/package.json +1 -1
- 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
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
485
|
+
cleanupStaleRouterMounts();
|
|
486
|
+
const parsedMountPath = parseMountPath(pathname);
|
|
487
|
+
if (!parsedMountPath) {
|
|
488
|
+
respondWithText(res, method, 404, 'Not found');
|
|
489
|
+
return;
|
|
112
490
|
}
|
|
113
|
-
|
|
114
|
-
|
|
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 (
|
|
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
|
-
|
|
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((
|
|
528
|
+
await new Promise((resolveListen, rejectListen) => {
|
|
151
529
|
const onError = (error) => {
|
|
152
530
|
server.off('listening', onListening);
|
|
153
|
-
|
|
531
|
+
rejectListen(error);
|
|
154
532
|
};
|
|
155
533
|
const onListening = () => {
|
|
156
534
|
server.off('error', onError);
|
|
157
|
-
|
|
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
|
}
|
package/dist/commands/login.js
CHANGED
|
@@ -31,8 +31,16 @@ function storeLogin(env, data) {
|
|
|
31
31
|
process.exitCode = 1;
|
|
32
32
|
return;
|
|
33
33
|
}
|
|
34
|
-
|
|
35
|
-
|
|
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
|
}
|
package/dist/commands/logout.js
CHANGED
|
@@ -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.
|
|
7
|
-
|
|
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
|
}
|
package/dist/commands/whoami.js
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|