castle-web-cli 0.4.112 → 0.4.114
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/list-decks.js +67 -2
- package/dist/serve.js +29 -4
- package/dist/shell/assets/index-CqpY1xZR.js +144 -0
- package/dist/shell/assets/index-DCwVFL5u.css +1 -0
- package/dist/shell/index.html +2 -2
- package/package.json +1 -1
- package/dist/shell/assets/index-ByPpiJ-7.js +0 -144
- package/dist/shell/assets/index-CdXEpv_P.css +0 -1
package/dist/list-decks.js
CHANGED
|
@@ -5,12 +5,67 @@
|
|
|
5
5
|
// The agent needs this because `add-import` takes a deck id, and until now
|
|
6
6
|
// nothing told it which ids exist -- so it could only import a deck the user
|
|
7
7
|
// had already named.
|
|
8
|
+
//
|
|
9
|
+
// Listing needs a Castle session, and there are two places one can be. A local
|
|
10
|
+
// CLI has its own login. A cloud sandbox deliberately has NO token -- so there
|
|
11
|
+
// we ask the browser that has this deck open, over the serve's WebSocket, and
|
|
12
|
+
// the shell answers from its castle-www host (see shell/importApi.ts). Telling
|
|
13
|
+
// a cloud user to `castle-web login` would be pointing at the wrong machine.
|
|
14
|
+
import * as fs from 'fs';
|
|
8
15
|
import * as path from 'path';
|
|
9
16
|
import { listImportable } from './importBrowse.js';
|
|
17
|
+
import { connectWS } from './preview.js';
|
|
10
18
|
const KINDS = ['mine', 'saved', 'kits'];
|
|
19
|
+
// The browser's answer travels on to castle-www and its GraphQL scans, so this
|
|
20
|
+
// is the whole round trip, not one hop.
|
|
21
|
+
const BROWSER_TIMEOUT_MS = 20_000;
|
|
11
22
|
function isKind(value) {
|
|
12
23
|
return KINDS.includes(value);
|
|
13
24
|
}
|
|
25
|
+
function serveWsPort(deckDir) {
|
|
26
|
+
try {
|
|
27
|
+
const raw = fs.readFileSync(path.join(deckDir, '.castle', 'serve.json'), 'utf8');
|
|
28
|
+
const port = JSON.parse(raw).wsPort;
|
|
29
|
+
return typeof port === 'number' ? port : null;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// Null means nobody answered: no serve, or no browser on the other end of it.
|
|
36
|
+
// A browser that answers "not signed in" is an ANSWER and comes back as one.
|
|
37
|
+
async function askBrowser(deckDir, kind) {
|
|
38
|
+
const wsPort = serveWsPort(deckDir);
|
|
39
|
+
if (wsPort === null)
|
|
40
|
+
return null;
|
|
41
|
+
const ws = await connectWS(wsPort).catch(() => null);
|
|
42
|
+
if (!ws)
|
|
43
|
+
return null;
|
|
44
|
+
const requestId = Math.random().toString(36).slice(2);
|
|
45
|
+
const answer = new Promise((resolve) => {
|
|
46
|
+
const timer = setTimeout(() => resolve(null), BROWSER_TIMEOUT_MS);
|
|
47
|
+
ws.on('message', (raw) => {
|
|
48
|
+
try {
|
|
49
|
+
const msg = JSON.parse(raw.toString());
|
|
50
|
+
if (msg.type !== 'import_list_response' || msg.requestId !== requestId)
|
|
51
|
+
return;
|
|
52
|
+
clearTimeout(timer);
|
|
53
|
+
resolve(msg.list ?? null);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// the serve broadcasts other traffic on this socket; ignore what we
|
|
57
|
+
// can't read
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
ws.send(JSON.stringify({ type: 'import_list_request', requestId, tab: kind }));
|
|
62
|
+
try {
|
|
63
|
+
return await answer;
|
|
64
|
+
}
|
|
65
|
+
finally {
|
|
66
|
+
ws.close();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
14
69
|
// One deck per line, id first: the id is what `add-import` takes, so it reads
|
|
15
70
|
// straight out of the listing without the agent having to parse a sentence.
|
|
16
71
|
function formatDeck(deck) {
|
|
@@ -27,9 +82,19 @@ export async function listDecks(dir, options = {}) {
|
|
|
27
82
|
console.error(`Unknown kind "${kind}". Use one of: ${KINDS.join(', ')}.`);
|
|
28
83
|
process.exit(1);
|
|
29
84
|
}
|
|
30
|
-
const
|
|
85
|
+
const deckDir = path.resolve(dir);
|
|
86
|
+
const local = await listImportable(deckDir, kind);
|
|
87
|
+
const list = local.signedIn ? local : await askBrowser(deckDir, kind);
|
|
88
|
+
if (!list) {
|
|
89
|
+
// No serve at all means this is a plain local CLI, where the login is ours
|
|
90
|
+
// to have. With a serve up, the missing piece is a browser on it instead.
|
|
91
|
+
console.error(serveWsPort(deckDir) === null
|
|
92
|
+
? 'Not signed in. Run `castle-web login` first.'
|
|
93
|
+
: 'Cannot list decks: nothing is connected to this deck in a browser, and the listing comes from the browser session. Open this deck in the editor and run this again.');
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
31
96
|
if (!list.signedIn) {
|
|
32
|
-
console.error('
|
|
97
|
+
console.error('Cannot list decks: that browser is not signed in to Castle. Sign in and run this again.');
|
|
33
98
|
process.exit(1);
|
|
34
99
|
}
|
|
35
100
|
if (list.decks.length === 0) {
|
package/dist/serve.js
CHANGED
|
@@ -149,7 +149,10 @@ function readDeckContext(projectDir) {
|
|
|
149
149
|
}
|
|
150
150
|
}
|
|
151
151
|
const DEFAULT_PORT = 5757;
|
|
152
|
-
|
|
152
|
+
// How long a forwarded request stays routable. Long enough for the slowest of
|
|
153
|
+
// them (an import listing goes on to the browser's castle-www host and its
|
|
154
|
+
// GraphQL scans); an unanswered id just sits in the map until this expires.
|
|
155
|
+
const PENDING_REQUEST_TTL_MS = 30_000;
|
|
153
156
|
function readServeJson(projectDir) {
|
|
154
157
|
try {
|
|
155
158
|
const raw = fs.readFileSync(path.join(projectDir, '.castle', 'serve.json'), 'utf8');
|
|
@@ -440,19 +443,41 @@ function startWSServer(port, projectDir, logFile, screenshotsDir, viteHolder) {
|
|
|
440
443
|
c.send(fwd);
|
|
441
444
|
}
|
|
442
445
|
}
|
|
443
|
-
|
|
446
|
+
// Things one client asks the OTHERS to answer: a tab's canvas for
|
|
447
|
+
// `screenshot`, and the browser's signed-in deck listing for
|
|
448
|
+
// `list-decks` (a cloud sandbox holds no Castle token; the browser
|
|
449
|
+
// that has the deck open does -- see shell/importApi.ts).
|
|
450
|
+
if (msg.type === 'screenshot_request' || msg.type === 'import_list_request') {
|
|
444
451
|
// Record the requester, then forward to the other clients (the tab).
|
|
445
452
|
if (typeof msg.requestId === 'string') {
|
|
446
453
|
forgetRequest(msg.requestId);
|
|
447
454
|
const timeout = setTimeout(() => {
|
|
448
455
|
requestOrigins.delete(msg.requestId);
|
|
449
|
-
},
|
|
456
|
+
}, PENDING_REQUEST_TTL_MS);
|
|
450
457
|
requestOrigins.set(msg.requestId, { origin: ws, timeout });
|
|
451
458
|
}
|
|
452
459
|
const fwd = JSON.stringify(msg);
|
|
460
|
+
let delivered = 0;
|
|
453
461
|
for (const c of clients) {
|
|
454
|
-
if (c !== ws && c.readyState === WebSocket.OPEN)
|
|
462
|
+
if (c !== ws && c.readyState === WebSocket.OPEN) {
|
|
455
463
|
c.send(fwd);
|
|
464
|
+
delivered += 1;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
// Nobody to ask -- say so now rather than leaving the CLI to sit out
|
|
468
|
+
// its whole timeout for an answer that can't come.
|
|
469
|
+
if (delivered === 0 && msg.type === 'import_list_request' && typeof msg.requestId === 'string') {
|
|
470
|
+
forgetRequest(msg.requestId);
|
|
471
|
+
ws.send(JSON.stringify({ type: 'import_list_response', requestId: msg.requestId }));
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
// The shell's answer, back to just the asker. A second tab answering
|
|
475
|
+
// the same request finds no pending origin and is dropped.
|
|
476
|
+
if (msg.type === 'import_list_response' && typeof msg.requestId === 'string') {
|
|
477
|
+
const pending = requestOrigins.get(msg.requestId);
|
|
478
|
+
forgetRequest(msg.requestId);
|
|
479
|
+
if (pending && pending.origin.readyState === WebSocket.OPEN) {
|
|
480
|
+
pending.origin.send(JSON.stringify(msg));
|
|
456
481
|
}
|
|
457
482
|
}
|
|
458
483
|
if (msg.type === 'log') {
|