castle-web-cli 0.4.117 → 0.4.118
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/ide.d.ts +5 -3
- package/dist/ide.js +161 -175
- package/dist/importBrowse.d.ts +3 -3
- package/dist/importBrowse.js +42 -34
- package/dist/serve.js +21 -5
- package/dist/shell/assets/{index-DiPlPGyg.js → index-DsOo_SWO.js} +1 -1
- package/dist/shell/index.html +1 -1
- package/package.json +1 -1
package/dist/importBrowse.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import * as http from
|
|
1
|
+
import * as http from 'http';
|
|
2
2
|
export declare const IMPORT_API_PREFIX = "/__castle/import/";
|
|
3
3
|
export interface ImportCandidate {
|
|
4
4
|
deckId: string;
|
|
@@ -14,8 +14,8 @@ export interface ImportList {
|
|
|
14
14
|
decks: ImportCandidate[];
|
|
15
15
|
truncated: boolean;
|
|
16
16
|
}
|
|
17
|
-
export type ImportTab =
|
|
17
|
+
export type ImportTab = 'mine' | 'saved' | 'kits';
|
|
18
18
|
export declare function listImportable(deckDir: string, tab: ImportTab): Promise<ImportList>;
|
|
19
19
|
export declare function resolveDeckRef(deckDir: string, ref: string): Promise<ImportCandidate>;
|
|
20
20
|
export declare function sourceFileCount(deckId: string): Promise<number | null>;
|
|
21
|
-
export declare function handleImportApi(deckDir: string, req: http.IncomingMessage, res: http.ServerResponse, reqPath: string): boolean;
|
|
21
|
+
export declare function handleImportApi(deckDir: string, req: http.IncomingMessage, res: http.ServerResponse, reqPath: string, onDeckChanged?: () => void): boolean;
|
package/dist/importBrowse.js
CHANGED
|
@@ -10,17 +10,17 @@
|
|
|
10
10
|
// Nothing here goes through the deck iframe or the kit: the Files panel that
|
|
11
11
|
// opens this picker is a builtin shell panel, and it has to keep working on a
|
|
12
12
|
// `--kit none` deck that ships no editor app at all.
|
|
13
|
-
import * as fs from
|
|
14
|
-
import * as os from
|
|
15
|
-
import * as path from
|
|
16
|
-
import { nanoid } from
|
|
17
|
-
import * as api from
|
|
18
|
-
import * as config from
|
|
19
|
-
import { runTar } from
|
|
20
|
-
import { readCastleJson } from
|
|
21
|
-
import { ImportError, addImportTo, importedDeckIds, parseDeckRef } from
|
|
22
|
-
import { errorMessage, readRequestBody, sendJson } from
|
|
23
|
-
export const IMPORT_API_PREFIX =
|
|
13
|
+
import * as fs from 'fs';
|
|
14
|
+
import * as os from 'os';
|
|
15
|
+
import * as path from 'path';
|
|
16
|
+
import { nanoid } from 'nanoid';
|
|
17
|
+
import * as api from './api.js';
|
|
18
|
+
import * as config from './config.js';
|
|
19
|
+
import { runTar } from './save-deck.js';
|
|
20
|
+
import { readCastleJson } from './castleJson.js';
|
|
21
|
+
import { ImportError, addImportTo, importedDeckIds, parseDeckRef } from './imports.js';
|
|
22
|
+
import { errorMessage, readRequestBody, sendJson } from './httpJson.js';
|
|
23
|
+
export const IMPORT_API_PREFIX = '/__castle/import/';
|
|
24
24
|
// Rows per list, and how many decks we are willing to ask the source question
|
|
25
25
|
// about to fill one. An account can hold hundreds of decks and only some are
|
|
26
26
|
// web decks, so the scan has to look past the first fifty -- but not forever.
|
|
@@ -32,24 +32,24 @@ const SOURCE_CHUNK = 100;
|
|
|
32
32
|
const PLAYLIST_CAP = 10;
|
|
33
33
|
// The bookmarks feed already covers this playlist; listing both would just
|
|
34
34
|
// duplicate it.
|
|
35
|
-
const BOOKMARKS_PLAYLIST_PREFIX =
|
|
35
|
+
const BOOKMARKS_PLAYLIST_PREFIX = 'bookmarks-';
|
|
36
36
|
// The kits list, hardcoded. A playlist can't hold it: playlist feeds drop
|
|
37
37
|
// unlisted decks, and a kit is published unlisted. Adding a kit means shipping
|
|
38
38
|
// an update to this list.
|
|
39
39
|
const KIT_DECK_IDS = [
|
|
40
|
-
|
|
40
|
+
'ckRZGFW4iPrx', // physics-2d
|
|
41
41
|
];
|
|
42
42
|
// Listing is several server round trips, and the picker is opened, closed and
|
|
43
43
|
// reopened while someone decides. Cached briefly so that costs once.
|
|
44
44
|
const LIST_TTL_MS = 60_000;
|
|
45
45
|
const listCache = new Map();
|
|
46
46
|
function asTab(value) {
|
|
47
|
-
return value ===
|
|
47
|
+
return value === 'saved' || value === 'kits' ? value : 'mine';
|
|
48
48
|
}
|
|
49
49
|
function toCandidate(row, hasSource) {
|
|
50
50
|
return {
|
|
51
51
|
deckId: row.deckId,
|
|
52
|
-
title: row.title?.trim() ? row.title.trim() :
|
|
52
|
+
title: row.title?.trim() ? row.title.trim() : 'Untitled',
|
|
53
53
|
creator: row.creator?.username ?? null,
|
|
54
54
|
imageUrl: row.initialCard?.backgroundImage?.smallUrl ?? null,
|
|
55
55
|
parentCreator: row.parentDeck?.creator?.username ?? null,
|
|
@@ -101,7 +101,7 @@ async function filterImportable(rows) {
|
|
|
101
101
|
// which of the two something landed in is not a distinction worth a tab.
|
|
102
102
|
async function savedRows() {
|
|
103
103
|
const userId = config.getUserId();
|
|
104
|
-
const bookmarks = await api.feedDecks(
|
|
104
|
+
const bookmarks = await api.feedDecks('bookmarks', LIST_LIMIT);
|
|
105
105
|
if (!userId)
|
|
106
106
|
return bookmarks;
|
|
107
107
|
const playlists = (await api.myPlaylists(userId, PLAYLIST_CAP * 2))
|
|
@@ -111,9 +111,9 @@ async function savedRows() {
|
|
|
111
111
|
return [bookmarks, ...lists].flat();
|
|
112
112
|
}
|
|
113
113
|
async function tabRows(tab) {
|
|
114
|
-
if (tab ===
|
|
114
|
+
if (tab === 'saved')
|
|
115
115
|
return savedRows();
|
|
116
|
-
if (tab ===
|
|
116
|
+
if (tab === 'kits')
|
|
117
117
|
return api.deckRows(KIT_DECK_IDS);
|
|
118
118
|
return api.myDecks();
|
|
119
119
|
}
|
|
@@ -137,10 +137,10 @@ export async function listImportable(deckDir, tab) {
|
|
|
137
137
|
export async function resolveDeckRef(deckDir, ref) {
|
|
138
138
|
const deckId = parseDeckRef(ref);
|
|
139
139
|
if (!deckId) {
|
|
140
|
-
throw new ImportError(
|
|
140
|
+
throw new ImportError('bad-deck-ref', `Not a deck id or a castle deck link: ${ref}`);
|
|
141
141
|
}
|
|
142
142
|
if (readCastleJson(deckDir)?.deckId === deckId) {
|
|
143
|
-
throw new ImportError(
|
|
143
|
+
throw new ImportError('self-import', `That is this deck -- a deck can't import itself.`);
|
|
144
144
|
}
|
|
145
145
|
const [rows, source] = await Promise.all([
|
|
146
146
|
api.deckRows([deckId]),
|
|
@@ -148,7 +148,7 @@ export async function resolveDeckRef(deckDir, ref) {
|
|
|
148
148
|
]);
|
|
149
149
|
const row = rows[0];
|
|
150
150
|
if (!row)
|
|
151
|
-
throw new ImportError(
|
|
151
|
+
throw new ImportError('bad-deck-ref', `No deck ${deckId} on the server.`);
|
|
152
152
|
return stampImported(deckDir, [toCandidate(row, source !== null)])[0];
|
|
153
153
|
}
|
|
154
154
|
// How many files a deck's source holds. Not on any list query, so it is counted
|
|
@@ -172,8 +172,8 @@ export async function sourceFileCount(deckId) {
|
|
|
172
172
|
if (!res.ok)
|
|
173
173
|
return null;
|
|
174
174
|
fs.writeFileSync(tmpFile, Buffer.from(await res.arrayBuffer()));
|
|
175
|
-
const listing = await runTar([
|
|
176
|
-
const count = listing.split(
|
|
175
|
+
const listing = await runTar(['-tzf', tmpFile], { capture: true });
|
|
176
|
+
const count = listing.split('\n').filter((line) => line && !line.endsWith('/')).length;
|
|
177
177
|
fileCountCache.set(key, count);
|
|
178
178
|
return count;
|
|
179
179
|
}
|
|
@@ -201,37 +201,45 @@ function sendResult(res, work) {
|
|
|
201
201
|
sendJson(res, 500, { error: errorMessage(err) });
|
|
202
202
|
});
|
|
203
203
|
}
|
|
204
|
-
export function handleImportApi(deckDir, req, res, reqPath
|
|
204
|
+
export function handleImportApi(deckDir, req, res, reqPath,
|
|
205
|
+
// Called after an import that changed what the page has to load. Adding a kit
|
|
206
|
+
// installs npm packages and can repoint the deck's entry, both under a serve
|
|
207
|
+
// that already optimized deps for the OLD deck -- the running page then holds
|
|
208
|
+
// a module graph in which react resolves to nothing, which surfaces as
|
|
209
|
+
// "Cannot read properties of null (reading 'useState')" until a hand reload.
|
|
210
|
+
onDeckChanged) {
|
|
205
211
|
const action = reqPath.slice(IMPORT_API_PREFIX.length);
|
|
206
|
-
const url = new URL(req.url ??
|
|
207
|
-
if (action ===
|
|
208
|
-
sendResult(res, () => listImportable(deckDir, asTab(url.searchParams.get(
|
|
212
|
+
const url = new URL(req.url ?? '/', 'http://localhost');
|
|
213
|
+
if (action === 'list') {
|
|
214
|
+
sendResult(res, () => listImportable(deckDir, asTab(url.searchParams.get('tab'))));
|
|
209
215
|
return true;
|
|
210
216
|
}
|
|
211
|
-
if (action ===
|
|
212
|
-
const ref = url.searchParams.get(
|
|
217
|
+
if (action === 'resolve') {
|
|
218
|
+
const ref = url.searchParams.get('ref') ?? '';
|
|
213
219
|
sendResult(res, async () => ({ deck: await resolveDeckRef(deckDir, ref) }));
|
|
214
220
|
return true;
|
|
215
221
|
}
|
|
216
|
-
if (action ===
|
|
217
|
-
const deckId = url.searchParams.get(
|
|
222
|
+
if (action === 'files') {
|
|
223
|
+
const deckId = url.searchParams.get('deckId') ?? '';
|
|
218
224
|
sendResult(res, async () => ({ fileCount: await sourceFileCount(deckId) }));
|
|
219
225
|
return true;
|
|
220
226
|
}
|
|
221
|
-
if (action ===
|
|
227
|
+
if (action === 'add') {
|
|
222
228
|
sendResult(res, async () => {
|
|
223
229
|
let body;
|
|
224
230
|
try {
|
|
225
231
|
body = JSON.parse(await readRequestBody(req));
|
|
226
232
|
}
|
|
227
233
|
catch {
|
|
228
|
-
throw new ImportError(
|
|
234
|
+
throw new ImportError('no-deck-ref', 'Invalid JSON body.');
|
|
229
235
|
}
|
|
230
|
-
const deckRef = typeof body.deckRef ===
|
|
236
|
+
const deckRef = typeof body.deckRef === 'string' ? body.deckRef : '';
|
|
231
237
|
const result = await addImportTo(deckDir, { deckRef });
|
|
232
238
|
// The lists said which decks were importable; one of them just stopped
|
|
233
239
|
// being a candidate (or changed version), so the cached answers are stale.
|
|
234
240
|
listCache.clear();
|
|
241
|
+
if (result.dependencies.length > 0 || result.adoptedMain)
|
|
242
|
+
onDeckChanged?.();
|
|
235
243
|
return result;
|
|
236
244
|
});
|
|
237
245
|
return true;
|
package/dist/serve.js
CHANGED
|
@@ -16,12 +16,14 @@ function isPortFree(port) {
|
|
|
16
16
|
return new Promise((resolve) => {
|
|
17
17
|
const srv = net.createServer();
|
|
18
18
|
srv.once('error', () => resolve(false));
|
|
19
|
-
srv.listen(port, () => {
|
|
19
|
+
srv.listen(port, () => {
|
|
20
|
+
srv.close(() => resolve(true));
|
|
21
|
+
});
|
|
20
22
|
});
|
|
21
23
|
}
|
|
22
24
|
async function findFreePorts(startPort) {
|
|
23
25
|
for (let p = startPort; p < startPort + 100; p += 2) {
|
|
24
|
-
if (await isPortFree(p) && await isPortFree(p + 1)) {
|
|
26
|
+
if ((await isPortFree(p)) && (await isPortFree(p + 1))) {
|
|
25
27
|
return { port: p, wsPort: p + 1 };
|
|
26
28
|
}
|
|
27
29
|
}
|
|
@@ -158,7 +160,9 @@ function readServeJson(projectDir) {
|
|
|
158
160
|
try {
|
|
159
161
|
const raw = fs.readFileSync(path.join(projectDir, '.castle', 'serve.json'), 'utf8');
|
|
160
162
|
const info = JSON.parse(raw);
|
|
161
|
-
if (typeof info.port !== 'number' ||
|
|
163
|
+
if (typeof info.port !== 'number' ||
|
|
164
|
+
typeof info.wsPort !== 'number' ||
|
|
165
|
+
typeof info.pid !== 'number')
|
|
162
166
|
return null;
|
|
163
167
|
return { port: info.port, wsPort: info.wsPort, pid: info.pid };
|
|
164
168
|
}
|
|
@@ -275,6 +279,12 @@ export async function serve(dir, options = {}) {
|
|
|
275
279
|
const ideServer = createIdeServer({
|
|
276
280
|
deckDir: projectDir,
|
|
277
281
|
deckLabel: path.basename(projectDir),
|
|
282
|
+
// Same closure the agent server gets: invalidate then reload every tab.
|
|
283
|
+
// `viteHolder` is read lazily because Vite is created further down.
|
|
284
|
+
restart: () => {
|
|
285
|
+
invalidateModuleCaches(viteHolder.vite);
|
|
286
|
+
broadcast({ type: 'restart' });
|
|
287
|
+
},
|
|
278
288
|
});
|
|
279
289
|
process.on('exit', () => ideServer.shutdown());
|
|
280
290
|
// The agent panel backend: router conversation + background task agents,
|
|
@@ -298,7 +308,11 @@ export async function serve(dir, options = {}) {
|
|
|
298
308
|
process.on('exit', () => agentServer.shutdown());
|
|
299
309
|
const vite = await createServer({
|
|
300
310
|
root: projectDir,
|
|
301
|
-
plugins: [
|
|
311
|
+
plugins: [
|
|
312
|
+
castlePlugin(wsPort, ideServer, agentServer),
|
|
313
|
+
importsAliasPlugin(),
|
|
314
|
+
sceneFilesPlugin(),
|
|
315
|
+
],
|
|
302
316
|
server: {
|
|
303
317
|
port,
|
|
304
318
|
strictPort: true,
|
|
@@ -482,7 +496,9 @@ function startWSServer(port, projectDir, logFile, screenshotsDir, viteHolder) {
|
|
|
482
496
|
}
|
|
483
497
|
// Nobody to ask -- say so now rather than leaving the CLI to sit out
|
|
484
498
|
// its whole timeout for an answer that can't come.
|
|
485
|
-
if (delivered === 0 &&
|
|
499
|
+
if (delivered === 0 &&
|
|
500
|
+
msg.type === 'import_list_request' &&
|
|
501
|
+
typeof msg.requestId === 'string') {
|
|
486
502
|
forgetRequest(msg.requestId);
|
|
487
503
|
ws.send(JSON.stringify({ type: 'import_list_response', requestId: msg.requestId }));
|
|
488
504
|
}
|