castle-web-cli 0.4.110 → 0.4.112
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/agent-prompts.js +1 -0
- package/dist/api.d.ts +24 -5
- package/dist/api.js +71 -9
- package/dist/commonInstructions.d.ts +1 -1
- package/dist/commonInstructions.js +9 -1
- package/dist/httpJson.d.ts +4 -0
- package/dist/httpJson.js +21 -0
- package/dist/ide.d.ts +2 -0
- package/dist/ide.js +34 -15
- package/dist/importBrowse.d.ts +21 -0
- package/dist/importBrowse.js +240 -0
- package/dist/imports.d.ts +21 -1
- package/dist/imports.js +96 -24
- package/dist/index.js +11 -4
- package/dist/list-decks.d.ts +3 -0
- package/dist/list-decks.js +44 -0
- package/dist/save-deck.d.ts +3 -1
- package/dist/save-deck.js +7 -3
- package/dist/serve.js +10 -2
- package/dist/shell/assets/index-ByPpiJ-7.js +144 -0
- package/dist/shell/assets/index-CdXEpv_P.css +1 -0
- package/dist/shell/favicon-16x16.png +0 -0
- package/dist/shell/favicon-32x32.png +0 -0
- package/dist/shell/favicon.ico +0 -0
- package/dist/shell/index.html +8 -2
- package/kits/physics-2d/castle.json +1 -1
- package/kits/physics-2d/engine/ui.module.css +19 -0
- package/package.json +2 -2
- package/dist/shell/assets/index-BK2M69q4.js +0 -144
- package/dist/shell/assets/index-wU4ol--C.css +0 -1
package/dist/imports.js
CHANGED
|
@@ -25,6 +25,37 @@ import { readCastleJsonOrThrow as readCastleJson, } from './castleJson.js';
|
|
|
25
25
|
// node_modules gets. That also means `get-deck` leaves an existing `imports/`
|
|
26
26
|
// alone when it refreshes a deck.
|
|
27
27
|
export const IMPORTS_DIR = 'imports';
|
|
28
|
+
export class ImportError extends Error {
|
|
29
|
+
code;
|
|
30
|
+
constructor(code, message) {
|
|
31
|
+
super(message);
|
|
32
|
+
this.name = 'ImportError';
|
|
33
|
+
this.code = code;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
// What someone has in hand is a deck id or the URL of the page they were just
|
|
37
|
+
// looking at, so both are accepted here rather than in a separate resolve step.
|
|
38
|
+
// Recognized: a bare id, a deck page (`castle.xyz/d/<id>`), and the cloud editor
|
|
39
|
+
// (`castle.xyz/admin/cloud/<id>`) -- for anything else the last path segment is
|
|
40
|
+
// tried, since every castle deck URL ends in the id.
|
|
41
|
+
export function parseDeckRef(raw) {
|
|
42
|
+
const trimmed = raw.trim();
|
|
43
|
+
if (!trimmed)
|
|
44
|
+
return null;
|
|
45
|
+
if (!trimmed.includes('/'))
|
|
46
|
+
return api.isDeckIdShaped(trimmed) ? trimmed : null;
|
|
47
|
+
let url;
|
|
48
|
+
try {
|
|
49
|
+
url = new URL(trimmed.includes('://') ? trimmed : `https://${trimmed}`);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
const segments = url.pathname.split('/').filter(Boolean);
|
|
55
|
+
const afterD = segments.indexOf('d');
|
|
56
|
+
const candidate = afterD >= 0 ? segments[afterD + 1] : segments[segments.length - 1];
|
|
57
|
+
return candidate && api.isDeckIdShaped(candidate) ? candidate : null;
|
|
58
|
+
}
|
|
28
59
|
// Only the `imports` key is ours; everything else in castle.json (deck identity,
|
|
29
60
|
// editor config) is preserved as-is.
|
|
30
61
|
function writeImportPin(dir, alias, pin) {
|
|
@@ -70,6 +101,25 @@ function qualifiedAlias(meta, deckId) {
|
|
|
70
101
|
return `${user}.${title}`;
|
|
71
102
|
return title || slugifyAlias(deckId) || deckId;
|
|
72
103
|
}
|
|
104
|
+
// A derived alias can already be taken -- two people with a same-named deck, or
|
|
105
|
+
// the same title twice. Reusing it would silently replace a different
|
|
106
|
+
// dependency, so a taken name gets a number. A name already pointing at THIS
|
|
107
|
+
// deck is not a collision: that one is the update case, and keeping it is what
|
|
108
|
+
// makes re-importing a deck update it in place.
|
|
109
|
+
const MAX_ALIAS_ATTEMPTS = 100;
|
|
110
|
+
function availableAlias(deckDir, alias, deckId) {
|
|
111
|
+
const pins = readCastleJson(deckDir)?.imports ?? {};
|
|
112
|
+
const taken = (name) => pins[name] !== undefined
|
|
113
|
+
? pins[name].deckId !== deckId
|
|
114
|
+
: fs.existsSync(path.join(deckDir, IMPORTS_DIR, name));
|
|
115
|
+
if (!taken(alias))
|
|
116
|
+
return alias;
|
|
117
|
+
for (let n = 2; n <= MAX_ALIAS_ATTEMPTS; n++) {
|
|
118
|
+
if (!taken(`${alias}-${n}`))
|
|
119
|
+
return `${alias}-${n}`;
|
|
120
|
+
}
|
|
121
|
+
throw new ImportError('bad-alias', `Too many imports already named "${alias}"; pass --as to name this one.`);
|
|
122
|
+
}
|
|
73
123
|
// Dependencies are made read-only ON DISK, not just refused by the files API.
|
|
74
124
|
// The API guard only covers the editor; an agent or a shell command writes
|
|
75
125
|
// straight to the filesystem, and the common "write a temp file and rename it"
|
|
@@ -386,49 +436,71 @@ async function addTransitiveImports(deckDir, viaAlias, seen) {
|
|
|
386
436
|
}
|
|
387
437
|
// Every deckId this deck already has, so a transitive walk doesn't refetch or
|
|
388
438
|
// loop on a cycle.
|
|
389
|
-
function importedDeckIds(deckDir) {
|
|
439
|
+
export function importedDeckIds(deckDir) {
|
|
390
440
|
const pins = readCastleJson(deckDir)?.imports ?? {};
|
|
391
441
|
return new Set(Object.values(pins).map((p) => p?.deckId).filter((id) => !!id));
|
|
392
442
|
}
|
|
393
|
-
|
|
443
|
+
// The import itself. Throws ImportError rather than exiting, because this also
|
|
444
|
+
// runs inside the serve (from the editor's Import panel), where taking the
|
|
445
|
+
// process down is not an option. The CLI wrapper below prints and exits.
|
|
446
|
+
export async function addImportTo(dir, options = {}) {
|
|
394
447
|
const targetDir = path.resolve(dir);
|
|
395
|
-
const
|
|
448
|
+
const ref = options.deckRef?.trim();
|
|
449
|
+
if (!ref) {
|
|
450
|
+
throw new ImportError('no-deck-ref', 'No deck given. Pass a deck id or a castle.xyz deck link.');
|
|
451
|
+
}
|
|
452
|
+
const deckId = parseDeckRef(ref);
|
|
396
453
|
if (!deckId) {
|
|
397
|
-
|
|
398
|
-
process.exit(1);
|
|
454
|
+
throw new ImportError('bad-deck-ref', `Not a deck id or a castle deck link: ${ref}`);
|
|
399
455
|
}
|
|
400
456
|
if (!fs.existsSync(targetDir)) {
|
|
401
|
-
|
|
402
|
-
process.exit(1);
|
|
457
|
+
throw new ImportError('no-such-dir', `No such directory: ${targetDir}`);
|
|
403
458
|
}
|
|
404
459
|
if (readCastleJson(targetDir)?.deckId === deckId) {
|
|
405
|
-
|
|
406
|
-
process.exit(1);
|
|
460
|
+
throw new ImportError('self-import', `Deck ${deckId} is this deck -- a deck can't import itself.`);
|
|
407
461
|
}
|
|
408
462
|
const [source, meta] = await Promise.all([
|
|
409
463
|
api.webDeckSource(deckId),
|
|
410
464
|
api.deckMeta(deckId).catch(() => null),
|
|
411
465
|
]);
|
|
412
466
|
if (!source) {
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
process.exit(1);
|
|
467
|
+
throw new ImportError('no-source', `No source archive on the server for deck ${deckId}. ` +
|
|
468
|
+
'Its owner needs to run `castle-web save-deck` before it can be imported.');
|
|
416
469
|
}
|
|
417
|
-
const
|
|
418
|
-
if (!
|
|
419
|
-
|
|
420
|
-
process.exit(1);
|
|
470
|
+
const given = options.alias ? slugifyGivenAlias(options.alias) : '';
|
|
471
|
+
if (options.alias && !given) {
|
|
472
|
+
throw new ImportError('bad-alias', 'Could not derive a usable name; pick one with letters or digits.');
|
|
421
473
|
}
|
|
474
|
+
// An explicit `--as` is the caller's call, collision and all. A derived one
|
|
475
|
+
// gets a number instead, since nothing asked for that particular name.
|
|
476
|
+
const alias = given || availableAlias(targetDir, qualifiedAlias(meta, deckId), deckId);
|
|
422
477
|
const { replaced } = await placeImport(targetDir, alias, deckId, source);
|
|
423
|
-
|
|
424
|
-
const
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
478
|
+
const transitive = await addTransitiveImports(targetDir, alias, importedDeckIds(targetDir));
|
|
479
|
+
const dependencies = syncImportDependencies(targetDir);
|
|
480
|
+
return { deckId, alias, replaced, transitive, dependencies };
|
|
481
|
+
}
|
|
482
|
+
export async function addImport(dir, options = {}) {
|
|
483
|
+
let result;
|
|
484
|
+
try {
|
|
485
|
+
result = await addImportTo(dir, options);
|
|
486
|
+
}
|
|
487
|
+
catch (err) {
|
|
488
|
+
if (!(err instanceof ImportError))
|
|
489
|
+
throw err;
|
|
490
|
+
console.error(err.message);
|
|
491
|
+
if (err.code === 'no-deck-ref') {
|
|
492
|
+
console.error('Usage: castle-web add-import <deckId|url> [dir] [--as ALIAS]');
|
|
493
|
+
}
|
|
494
|
+
process.exit(1);
|
|
495
|
+
}
|
|
496
|
+
console.log(`${result.replaced ? 'Updated' : 'Imported'} ${result.deckId} at ` +
|
|
497
|
+
`${IMPORTS_DIR}/${result.alias} (read-only on disk).`);
|
|
498
|
+
for (const t of result.transitive) {
|
|
499
|
+
console.log(`Also imported ${IMPORTS_DIR}/${t} (needed by ${result.alias})`);
|
|
500
|
+
}
|
|
501
|
+
for (const dep of result.dependencies)
|
|
430
502
|
console.log(`Added dependency ${dep}`);
|
|
431
|
-
if (
|
|
503
|
+
if (result.dependencies.length > 0)
|
|
432
504
|
console.log('Run `castle-web install` to install them.');
|
|
433
505
|
}
|
|
434
506
|
// `castle-web update-import [alias] [dir]`: re-fetch an import (or all of them)
|
package/dist/index.js
CHANGED
|
@@ -6,6 +6,7 @@ import { serve } from './serve.js';
|
|
|
6
6
|
import { saveDeck } from './save-deck.js';
|
|
7
7
|
import { getDeck } from './get-deck.js';
|
|
8
8
|
import { addImport, updateImport } from './imports.js';
|
|
9
|
+
import { listDecks } from './list-decks.js';
|
|
9
10
|
import { getCliVersion, init } from './init.js';
|
|
10
11
|
import { install } from './install.js';
|
|
11
12
|
import { connectWS, savePreviewImage, savePreviewIfNeeded, takeScreenshot } from './preview.js';
|
|
@@ -21,6 +22,7 @@ const FLAGS_WITH_VALUES = new Set([
|
|
|
21
22
|
'--caption',
|
|
22
23
|
'--visibility',
|
|
23
24
|
'--as',
|
|
25
|
+
'--kind',
|
|
24
26
|
]);
|
|
25
27
|
function findPositionalDir() {
|
|
26
28
|
for (let i = 1; i < args.length; i++) {
|
|
@@ -82,7 +84,8 @@ function usage() {
|
|
|
82
84
|
castle-web save-preview-image [dir] [--port PORT] [--no-restart]
|
|
83
85
|
castle-web save-deck [dir] [--title TITLE] [--caption TEXT] [--visibility unlisted|private]
|
|
84
86
|
castle-web get-deck [dir] [--deck-id ID] [--force] (replaces the source if the deck is already there)
|
|
85
|
-
castle-web
|
|
87
|
+
castle-web list-decks [dir] [--kind mine|saved|kits] (decks this one could import; ids are what add-import takes)
|
|
88
|
+
castle-web add-import <deckId|url> [dir] [--as ALIAS] (adds another deck as a read-only dependency in imports/)
|
|
86
89
|
castle-web update-import [alias] [dir] [--check] [--revert] (re-fetches imports; no alias means all)
|
|
87
90
|
castle-web install [dir]
|
|
88
91
|
castle-web login
|
|
@@ -146,12 +149,16 @@ async function main() {
|
|
|
146
149
|
});
|
|
147
150
|
break;
|
|
148
151
|
}
|
|
152
|
+
case 'list-decks': {
|
|
153
|
+
await listDecks(findPositionalDir(), { kind: getFlagValue('--kind') });
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
149
156
|
case 'add-import': {
|
|
150
|
-
// The deck id
|
|
151
|
-
// optional second, defaulting to the cwd.
|
|
157
|
+
// The deck -- an id or the link to its page -- is the first positional;
|
|
158
|
+
// the importing deck's dir is an optional second, defaulting to the cwd.
|
|
152
159
|
const positionals = readPositionals();
|
|
153
160
|
await addImport(positionals[1] ?? '.', {
|
|
154
|
-
|
|
161
|
+
deckRef: getFlagValue('--deck-id') ?? positionals[0],
|
|
155
162
|
alias: getFlagValue('--as'),
|
|
156
163
|
});
|
|
157
164
|
break;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// `castle-web list-decks` -- what this deck could import, for an agent rather
|
|
2
|
+
// than for the editor's picker. Same answer the picker gets (importBrowse's
|
|
3
|
+
// `/__castle/import/*` backend), printed one deck per line.
|
|
4
|
+
//
|
|
5
|
+
// The agent needs this because `add-import` takes a deck id, and until now
|
|
6
|
+
// nothing told it which ids exist -- so it could only import a deck the user
|
|
7
|
+
// had already named.
|
|
8
|
+
import * as path from 'path';
|
|
9
|
+
import { listImportable } from './importBrowse.js';
|
|
10
|
+
const KINDS = ['mine', 'saved', 'kits'];
|
|
11
|
+
function isKind(value) {
|
|
12
|
+
return KINDS.includes(value);
|
|
13
|
+
}
|
|
14
|
+
// One deck per line, id first: the id is what `add-import` takes, so it reads
|
|
15
|
+
// straight out of the listing without the agent having to parse a sentence.
|
|
16
|
+
function formatDeck(deck) {
|
|
17
|
+
const parts = [deck.deckId, deck.title];
|
|
18
|
+
if (deck.creator)
|
|
19
|
+
parts.push(`@${deck.creator}`);
|
|
20
|
+
if (deck.imported)
|
|
21
|
+
parts.push('(already imported)');
|
|
22
|
+
return parts.join(' ');
|
|
23
|
+
}
|
|
24
|
+
export async function listDecks(dir, options = {}) {
|
|
25
|
+
const kind = options.kind ?? 'mine';
|
|
26
|
+
if (!isKind(kind)) {
|
|
27
|
+
console.error(`Unknown kind "${kind}". Use one of: ${KINDS.join(', ')}.`);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
const list = await listImportable(path.resolve(dir), kind);
|
|
31
|
+
if (!list.signedIn) {
|
|
32
|
+
console.error('Not signed in. Run `castle-web login` first.');
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
if (list.decks.length === 0) {
|
|
36
|
+
console.log(`No ${kind} decks with source files to import.`);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
for (const deck of list.decks)
|
|
40
|
+
console.log(formatDeck(deck));
|
|
41
|
+
if (list.truncated) {
|
|
42
|
+
console.log(`(first ${list.decks.length}; there are more)`);
|
|
43
|
+
}
|
|
44
|
+
}
|
package/dist/save-deck.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export declare const SOURCE_ARCHIVE_EXCLUDES: string[];
|
|
2
|
-
export declare function runTar(args: string[]
|
|
2
|
+
export declare function runTar(args: string[], opts?: {
|
|
3
|
+
capture?: boolean;
|
|
4
|
+
}): Promise<string>;
|
|
3
5
|
export declare function archiveSource(projectDir: string): Promise<Buffer>;
|
|
4
6
|
export type SaveVisibility = 'unlisted' | 'private';
|
|
5
7
|
export interface SaveDeckOptions {
|
package/dist/save-deck.js
CHANGED
|
@@ -12,10 +12,14 @@ import { bundleProject } from './bundle.js';
|
|
|
12
12
|
// part of it. (Being on this list also means `get-deck` leaves an existing
|
|
13
13
|
// `imports/` in place when it refreshes a deck.)
|
|
14
14
|
export const SOURCE_ARCHIVE_EXCLUDES = ['node_modules', 'dist', '.castle', '.git', 'imports'];
|
|
15
|
-
|
|
15
|
+
// `capture` keeps tar's stdout and resolves with it -- for listing an archive
|
|
16
|
+
// (`-tzf`), where the output IS the answer. Packing and unpacking ignore it.
|
|
17
|
+
export function runTar(args, opts = {}) {
|
|
16
18
|
return new Promise((resolve, reject) => {
|
|
17
|
-
const child = spawn('tar', args, { stdio: ['ignore', 'ignore', 'pipe'] });
|
|
19
|
+
const child = spawn('tar', args, { stdio: ['ignore', opts.capture ? 'pipe' : 'ignore', 'pipe'] });
|
|
20
|
+
let stdout = '';
|
|
18
21
|
let stderr = '';
|
|
22
|
+
child.stdout?.on('data', (chunk) => { stdout += chunk.toString(); });
|
|
19
23
|
child.stderr?.on('data', (chunk) => { stderr += chunk.toString(); });
|
|
20
24
|
child.on('error', reject);
|
|
21
25
|
child.on('close', (code) => {
|
|
@@ -23,7 +27,7 @@ export function runTar(args) {
|
|
|
23
27
|
reject(new Error(`tar exited with code ${code}: ${stderr}`));
|
|
24
28
|
return;
|
|
25
29
|
}
|
|
26
|
-
resolve();
|
|
30
|
+
resolve(stdout);
|
|
27
31
|
});
|
|
28
32
|
});
|
|
29
33
|
}
|
package/dist/serve.js
CHANGED
|
@@ -4,7 +4,7 @@ import * as path from 'path';
|
|
|
4
4
|
import { spawn } from 'child_process';
|
|
5
5
|
import { createServer } from 'vite';
|
|
6
6
|
import { WebSocketServer, WebSocket } from 'ws';
|
|
7
|
-
import { createIdeServer } from './ide.js';
|
|
7
|
+
import { createIdeServer, FAVICON_LINK_TAGS } from './ide.js';
|
|
8
8
|
import { createAgentServer } from './agent.js';
|
|
9
9
|
import { sceneFilesPlugin, importsAliasPlugin } from './vitePlugins.js';
|
|
10
10
|
import { installFilesChangedWatcher } from './filesChanged.js';
|
|
@@ -58,7 +58,15 @@ function castlePlugin(wsPort, ideServer, agentServer) {
|
|
|
58
58
|
transformIndexHtml: {
|
|
59
59
|
order: 'pre',
|
|
60
60
|
handler(html) {
|
|
61
|
-
|
|
61
|
+
// Give the deck page Castle's favicon too -- it's a real page when
|
|
62
|
+
// opened directly (play surface / `?edit=0`), not just the editor's
|
|
63
|
+
// iframe. Injected here rather than in the kit templates so bare
|
|
64
|
+
// `--kit none` decks and already-scaffolded decks get it as well; a
|
|
65
|
+
// deck that declares its own icon keeps it.
|
|
66
|
+
const favicon = /<link[^>]+rel=["']?[^"'>]*icon/i.test(html)
|
|
67
|
+
? ''
|
|
68
|
+
: `\n ${FAVICON_LINK_TAGS}`;
|
|
69
|
+
return html.replace(/<head(\s[^>]*)?>/i, (match) => `${match}${favicon}\n <script>window.CastleEmbed={edit:true,host:'dev'};</script>\n ${CONSOLE_CAPTURE}`);
|
|
62
70
|
},
|
|
63
71
|
},
|
|
64
72
|
configureServer(server) {
|