castle-web-cli 0.4.122 → 0.4.124
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 +6 -2
- package/dist/castleJson.d.ts +2 -0
- package/dist/castleJson.js +39 -0
- package/dist/diffText.d.ts +14 -0
- package/dist/diffText.js +138 -0
- package/dist/headlessCover.d.ts +13 -0
- package/dist/headlessCover.js +99 -0
- package/dist/ide.d.ts +2 -1
- package/dist/ide.js +100 -43
- package/dist/imports.js +42 -2
- package/dist/index.js +49 -11
- package/dist/init.d.ts +1 -0
- package/dist/init.js +45 -1
- package/dist/localPaths.d.ts +1 -0
- package/dist/localPaths.js +5 -0
- package/dist/preview.d.ts +0 -1
- package/dist/preview.js +0 -58
- package/dist/save-deck.js +52 -1
- package/dist/serve.js +16 -0
- package/dist/shell/assets/Basteleur-Bold-CK8LF7Pt.woff +0 -0
- package/dist/shell/assets/Basteleur-Bold-DKFKedNb.woff2 +0 -0
- package/dist/shell/assets/index-BfOPkSej.css +1 -0
- package/dist/shell/assets/index-u0nYFqbF.js +434 -0
- package/dist/shell/index.html +2 -2
- package/dist/versionStore.d.ts +54 -0
- package/dist/versionStore.js +281 -0
- package/dist/versions.d.ts +82 -0
- package/dist/versions.js +446 -0
- package/kits/physics-2d/CLAUDE.md +2 -2
- package/kits/physics-2d/castle.json +10 -2
- package/kits/physics-2d/docs/pxart-format.md +33 -26
- package/kits/physics-2d/editors/PxArtEditor.jsx +120 -49
- package/kits/physics-2d/editors/SingleEditor.jsx +6 -3
- package/kits/physics-2d/editors/StyleEditor.jsx +95 -0
- package/kits/physics-2d/editors/pathOverlay.js +1 -1
- package/kits/physics-2d/editors/pathTools.js +9 -1
- package/kits/physics-2d/editors/pixelGeometry.js +14 -13
- package/kits/physics-2d/editors/pixelInspector.jsx +202 -53
- package/kits/physics-2d/editors/pxArtEditorModel.js +8 -63
- package/kits/physics-2d/editors/pxArtTools.js +3 -43
- package/kits/physics-2d/editors/styleEditor.module.css +105 -0
- package/kits/physics-2d/editors/styleTheme.js +16 -0
- package/kits/physics-2d/engine/files.js +2 -1
- package/kits/physics-2d/engine/liveReload.js +4 -3
- package/kits/physics-2d/engine/palettes.js +636 -0
- package/kits/physics-2d/engine/pxart.js +6 -6
- package/kits/physics-2d/engine/svgImport.js +1056 -0
- package/kits/physics-2d/engine/ui.jsx +2 -0
- package/kits/physics-2d/engine/ui.module.css +54 -9
- package/kits/physics-2d/package-lock.json +1 -1
- package/kits/physics-2d/package.json +1 -0
- package/kits/physics-2d/scripts/deckTheme.mjs +25 -0
- package/kits/physics-2d/scripts/draw.mjs +5 -3
- package/kits/physics-2d/scripts/import-svg.mjs +16 -1069
- package/kits/physics-2d/scripts/palette.mjs +10 -0
- package/kits/physics-2d/scripts/svg-emission-guide.md +5 -3
- package/kits/physics-2d/theme.style +3 -0
- package/package.json +1 -1
- package/dist/shell/assets/index-BkJ87APM.css +0 -1
- package/dist/shell/assets/index-DXBpj3-y.js +0 -434
package/dist/index.js
CHANGED
|
@@ -9,7 +9,8 @@ import { addImport, updateImport } from './imports.js';
|
|
|
9
9
|
import { listDecks } from './list-decks.js';
|
|
10
10
|
import { getCliVersion, init } from './init.js';
|
|
11
11
|
import { install } from './install.js';
|
|
12
|
-
import { connectWS,
|
|
12
|
+
import { connectWS, takeScreenshot } from './preview.js';
|
|
13
|
+
import { listVersions, restoreVersion, saveVersion, showVersion } from './versions.js';
|
|
13
14
|
const args = process.argv.slice(2);
|
|
14
15
|
const command = args[0];
|
|
15
16
|
const FLAGS_WITH_VALUES = new Set([
|
|
@@ -23,10 +24,19 @@ const FLAGS_WITH_VALUES = new Set([
|
|
|
23
24
|
'--visibility',
|
|
24
25
|
'--as',
|
|
25
26
|
'--kind',
|
|
27
|
+
'--dir',
|
|
28
|
+
'--message',
|
|
29
|
+
'-m',
|
|
26
30
|
]);
|
|
31
|
+
// `--foo` is always a flag; a single dash only when it is one we know (`-m`).
|
|
32
|
+
// Anything else starting with `-` is a positional -- a path can begin with one,
|
|
33
|
+
// and so can an opaque version id.
|
|
34
|
+
function isFlag(arg) {
|
|
35
|
+
return arg.startsWith('--') || FLAGS_WITH_VALUES.has(arg);
|
|
36
|
+
}
|
|
27
37
|
function findPositionalDir() {
|
|
28
38
|
for (let i = 1; i < args.length; i++) {
|
|
29
|
-
if (args[i]
|
|
39
|
+
if (isFlag(args[i])) {
|
|
30
40
|
if (FLAGS_WITH_VALUES.has(args[i]))
|
|
31
41
|
i++;
|
|
32
42
|
continue;
|
|
@@ -41,7 +51,7 @@ function findPositionalDir() {
|
|
|
41
51
|
function readPositionals() {
|
|
42
52
|
const out = [];
|
|
43
53
|
for (let i = 1; i < args.length; i++) {
|
|
44
|
-
if (args[i]
|
|
54
|
+
if (isFlag(args[i])) {
|
|
45
55
|
if (FLAGS_WITH_VALUES.has(args[i]))
|
|
46
56
|
i++;
|
|
47
57
|
continue;
|
|
@@ -81,13 +91,16 @@ function usage() {
|
|
|
81
91
|
castle-web serve [dir] [--port PORT] [--host HOST] [--open] [--detach]
|
|
82
92
|
castle-web restart [--port PORT]
|
|
83
93
|
castle-web screenshot [--out FILE] [--port PORT]
|
|
84
|
-
castle-web save-preview-image [dir] [--port PORT] [--no-restart]
|
|
85
94
|
castle-web save-deck [dir] [--title TITLE] [--caption TEXT] [--visibility unlisted|private]
|
|
86
95
|
castle-web get-deck [dir] [--deck-id ID] [--force] (replaces the source if the deck is already there)
|
|
87
96
|
castle-web list-decks [dir] [--kind mine|saved|kits] (decks this one could import; ids are what add-import takes)
|
|
88
97
|
castle-web add-import <deckId|url> [dir] [--as ALIAS] (adds another deck as a read-only dependency in imports/)
|
|
89
98
|
castle-web update-import [alias] [dir] [--check] [--revert] (re-fetches imports; no alias means all)
|
|
90
99
|
castle-web install [dir]
|
|
100
|
+
castle-web save-version [-m MESSAGE] [paths...] [--dir DIR] (no paths saves the whole deck)
|
|
101
|
+
castle-web list-versions [version] [--dir DIR] (defaults to the latest version)
|
|
102
|
+
castle-web show-version <version> [--dir DIR] (diffs of the files that version changed)
|
|
103
|
+
castle-web restore-version <version> [--force] [--dir DIR]
|
|
91
104
|
castle-web login
|
|
92
105
|
castle-web --version
|
|
93
106
|
|
|
@@ -173,6 +186,38 @@ async function main() {
|
|
|
173
186
|
await install(findPositionalDir());
|
|
174
187
|
break;
|
|
175
188
|
}
|
|
189
|
+
// The version commands spend their positionals on their own arguments --
|
|
190
|
+
// paths to save, a version to list from or restore to -- so the deck dir is
|
|
191
|
+
// `--dir`, defaulting to the cwd.
|
|
192
|
+
case 'save-version': {
|
|
193
|
+
saveVersion(getFlagValue('--dir') ?? '.', {
|
|
194
|
+
message: getFlagValue('-m') ?? getFlagValue('--message'),
|
|
195
|
+
paths: readPositionals(),
|
|
196
|
+
});
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
case 'list-versions': {
|
|
200
|
+
listVersions(getFlagValue('--dir') ?? '.', readPositionals()[0]);
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
case 'show-version': {
|
|
204
|
+
const target = readPositionals()[0];
|
|
205
|
+
if (!target) {
|
|
206
|
+
console.error('Usage: castle-web show-version <version>');
|
|
207
|
+
process.exit(1);
|
|
208
|
+
}
|
|
209
|
+
showVersion(getFlagValue('--dir') ?? '.', target);
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
case 'restore-version': {
|
|
213
|
+
const target = readPositionals()[0];
|
|
214
|
+
if (!target) {
|
|
215
|
+
console.error('Usage: castle-web restore-version <version> [--force]');
|
|
216
|
+
process.exit(1);
|
|
217
|
+
}
|
|
218
|
+
restoreVersion(getFlagValue('--dir') ?? '.', target, { force: hasFlag('--force') });
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
176
221
|
case 'restart': {
|
|
177
222
|
const dir = findPositionalDir();
|
|
178
223
|
const wsPort = getWsPort(dir);
|
|
@@ -196,13 +241,6 @@ async function main() {
|
|
|
196
241
|
console.log(`Saved ${outFile}`);
|
|
197
242
|
return;
|
|
198
243
|
}
|
|
199
|
-
case 'save-preview-image': {
|
|
200
|
-
const dir = findPositionalDir();
|
|
201
|
-
const wsPort = getWsPort(dir);
|
|
202
|
-
const noRestart = args.includes('--no-restart');
|
|
203
|
-
await savePreviewImage(dir, wsPort, noRestart);
|
|
204
|
-
return;
|
|
205
|
-
}
|
|
206
244
|
case 'login':
|
|
207
245
|
await login();
|
|
208
246
|
break;
|
package/dist/init.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export declare function isPristineBareDeck(dir: string): boolean;
|
|
|
9
9
|
export declare function adoptImportEntry(deckDir: string, alias: string, mainFile: string): void;
|
|
10
10
|
export declare function getCliVersion(): string;
|
|
11
11
|
export declare function writeStarterScene(projectDir: string, kitDir: string, alias: string): void;
|
|
12
|
+
export declare function writeStarterTheme(projectDir: string, kitDir: string): void;
|
|
12
13
|
export declare function init(dir: string, opts?: {
|
|
13
14
|
kit?: string;
|
|
14
15
|
serve?: boolean;
|
package/dist/init.js
CHANGED
|
@@ -6,6 +6,7 @@ import { deckMainFile, deckStarterScene } from './castleJson.js';
|
|
|
6
6
|
import { IMPORTS_DIR, lockImportTree } from './imports.js';
|
|
7
7
|
import { getCliEntryPath, getKitsDir, getRepoRoot, getSdkPackagePath, toPosixPath, } from './localPaths.js';
|
|
8
8
|
import { serve } from './serve.js';
|
|
9
|
+
import { createVersion } from './versions.js';
|
|
9
10
|
const INDEX_HTML = `<!DOCTYPE html>
|
|
10
11
|
<html>
|
|
11
12
|
<head>
|
|
@@ -37,7 +38,7 @@ const DEFAULT_KIT = 'physics-2d';
|
|
|
37
38
|
// Registry version of castle-web-sdk to inject when scaffolding from a
|
|
38
39
|
// globally-installed castle-web (not from inside the workspace). Bumped
|
|
39
40
|
// alongside cli/sdk version bumps.
|
|
40
|
-
const PUBLISHED_SDK_VERSION = '0.4.
|
|
41
|
+
const PUBLISHED_SDK_VERSION = '0.4.14';
|
|
41
42
|
// The account the first-party kits are (to be) published under, so a kit
|
|
42
43
|
// imported from the CLI's copy is named the same as one fetched from the server.
|
|
43
44
|
const KIT_AUTHOR = 'castle';
|
|
@@ -280,6 +281,19 @@ export function writeStarterScene(projectDir, kitDir, alias) {
|
|
|
280
281
|
fs.mkdirSync(path.join(projectDir, 'scenes'), { recursive: true });
|
|
281
282
|
writeJsonFile(path.join(projectDir, 'scenes', 'main.scene'), scene ? rewrite(scene) : { name: 'Main', actors: [] });
|
|
282
283
|
}
|
|
284
|
+
// A kit may ship a starter `theme.style` (deck-level styling, e.g. the color
|
|
285
|
+
// palette). Like the starter scene, it is deck content: the engine reads it
|
|
286
|
+
// from the DECK root, and it is the creator's to edit -- so it gets seeded once
|
|
287
|
+
// at scaffold time rather than living read-only in the import. Copied verbatim
|
|
288
|
+
// (its values are palette ids/hexes, never kit file paths, so no ref
|
|
289
|
+
// rewriting). Exported for the same two scaffold paths as writeStarterScene.
|
|
290
|
+
export function writeStarterTheme(projectDir, kitDir) {
|
|
291
|
+
const src = path.join(kitDir, 'theme.style');
|
|
292
|
+
const dest = path.join(projectDir, 'theme.style');
|
|
293
|
+
if (!fs.existsSync(src) || fs.existsSync(dest))
|
|
294
|
+
return;
|
|
295
|
+
fs.copyFileSync(src, dest);
|
|
296
|
+
}
|
|
283
297
|
function makeImportedKitClaudeMd(alias) {
|
|
284
298
|
return `# Castle deck
|
|
285
299
|
|
|
@@ -353,6 +367,7 @@ function scaffoldFromKitImport(kit, projectDir) {
|
|
|
353
367
|
const title = typeof kitConfig.title === 'string' ? kitConfig.title : kit;
|
|
354
368
|
writeDeckIndexHtml(projectDir, alias, title, deckMainFile(kitDir));
|
|
355
369
|
writeStarterScene(projectDir, kitDir, alias);
|
|
370
|
+
writeStarterTheme(projectDir, kitDir);
|
|
356
371
|
// The deck's own castle.json: the kit's editor config (panel layout, file
|
|
357
372
|
// filters) as a starting point -- it is the deck's to edit from here -- plus
|
|
358
373
|
// the import pin.
|
|
@@ -386,6 +401,34 @@ function scaffoldFromKitImport(kit, projectDir) {
|
|
|
386
401
|
appendCommonInstructions(projectDir);
|
|
387
402
|
lockImportTree(importDir);
|
|
388
403
|
}
|
|
404
|
+
// The scaffold as version 1, so a creator can always get back to the pristine
|
|
405
|
+
// deck. AFTER the install: it rewrites package.json and writes a lockfile, so a
|
|
406
|
+
// version taken before it describes a tree that never existed on disk, and a
|
|
407
|
+
// later restore would fight the package manager.
|
|
408
|
+
//
|
|
409
|
+
// This is the ordinary save path with no parent -- `parentId: null` and every
|
|
410
|
+
// file as an addition -- so the store has no special case for it. It composes
|
|
411
|
+
// with the no-op rule for free: `save-version` straight after init records
|
|
412
|
+
// nothing, so a fresh deck has exactly one version.
|
|
413
|
+
//
|
|
414
|
+
// The message is the only place a deck's kit lineage is recorded at all.
|
|
415
|
+
// Existing decks get none of this: their history starts at their first save,
|
|
416
|
+
// because a baseline synthesized from mid-work content would be labelled
|
|
417
|
+
// pristine while being nothing of the sort.
|
|
418
|
+
function saveInitialVersion(projectDir, kit) {
|
|
419
|
+
try {
|
|
420
|
+
const { version } = createVersion(projectDir, {
|
|
421
|
+
message: kit ? `New deck from ${kit}` : 'New deck',
|
|
422
|
+
});
|
|
423
|
+
if (version)
|
|
424
|
+
console.log(`Saved version ${version.id} — the deck as scaffolded.`);
|
|
425
|
+
}
|
|
426
|
+
catch (e) {
|
|
427
|
+
// A deck that scaffolded but failed to record its first version is still a
|
|
428
|
+
// deck. Losing the scaffold over it would be the wrong trade.
|
|
429
|
+
console.log(`Could not save the initial version: ${e instanceof Error ? e.message : String(e)}`);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
389
432
|
export async function init(dir, opts = {}) {
|
|
390
433
|
const projectDir = path.resolve(dir);
|
|
391
434
|
if (fs.existsSync(projectDir) && fs.readdirSync(projectDir).length > 0) {
|
|
@@ -423,6 +466,7 @@ export async function init(dir, opts = {}) {
|
|
|
423
466
|
catch {
|
|
424
467
|
console.error('dependency install failed; re-run `pnpm install` (or `npm install`) in the deck.');
|
|
425
468
|
}
|
|
469
|
+
saveInitialVersion(projectDir, bare ? null : kit);
|
|
426
470
|
const autoServe = opts.serve !== false;
|
|
427
471
|
if (autoServe && installed) {
|
|
428
472
|
// Call serve() with detach so init returns once the server is up. serve()
|
package/dist/localPaths.d.ts
CHANGED
|
@@ -3,4 +3,5 @@ export declare function getRepoRoot(): string;
|
|
|
3
3
|
export declare function getCliEntryPath(): string;
|
|
4
4
|
export declare function getSdkPackagePath(): string;
|
|
5
5
|
export declare function getKitsDir(): string;
|
|
6
|
+
export declare const COVER_FILE = "preview.png";
|
|
6
7
|
export declare function toPosixPath(filepath: string): string;
|
package/dist/localPaths.js
CHANGED
|
@@ -28,6 +28,11 @@ export function getKitsDir() {
|
|
|
28
28
|
return bundled;
|
|
29
29
|
return path.join(getRepoRoot(), 'kits');
|
|
30
30
|
}
|
|
31
|
+
// The deck's local cover image, deck-relative. Lives here rather than in the
|
|
32
|
+
// serve that writes it, because the version code needs to know about it too and
|
|
33
|
+
// `ide.ts` already imports that -- naming it here keeps the two from importing
|
|
34
|
+
// each other.
|
|
35
|
+
export const COVER_FILE = 'preview.png';
|
|
31
36
|
export function toPosixPath(filepath) {
|
|
32
37
|
return filepath.split(path.sep).join('/');
|
|
33
38
|
}
|
package/dist/preview.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
1
|
import type { WebSocket as WSClient } from 'ws';
|
|
2
2
|
export declare function connectWS(wsPort: number): Promise<WSClient>;
|
|
3
3
|
export declare function takeScreenshot(ws: WSClient): Promise<string>;
|
|
4
|
-
export declare function savePreviewImage(dir: string, wsPort: number, noRestart?: boolean): Promise<void>;
|
package/dist/preview.js
CHANGED
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
import * as fs from 'fs';
|
|
2
|
-
import * as path from 'path';
|
|
3
|
-
import * as api from './api.js';
|
|
4
|
-
import * as config from './config.js';
|
|
5
1
|
export async function connectWS(wsPort) {
|
|
6
2
|
const { default: WS } = await import('ws');
|
|
7
3
|
return new Promise((resolve, reject) => {
|
|
@@ -45,57 +41,3 @@ export function takeScreenshot(ws) {
|
|
|
45
41
|
ws.send(JSON.stringify({ type: 'screenshot_request', requestId }));
|
|
46
42
|
});
|
|
47
43
|
}
|
|
48
|
-
function waitForRestart(ws) {
|
|
49
|
-
return new Promise((resolve) => {
|
|
50
|
-
ws.send(JSON.stringify({ type: 'restart' }));
|
|
51
|
-
setTimeout(resolve, 2000);
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
// The shared declaration (castleJson.ts) rather than a local copy of the two
|
|
55
|
-
// fields this file reads, so it can't drift from what the rest of the CLI
|
|
56
|
-
// believes castle.json contains.
|
|
57
|
-
function readCastleJson(projectDir) {
|
|
58
|
-
const castleJsonPath = path.join(projectDir, 'castle.json');
|
|
59
|
-
if (!fs.existsSync(castleJsonPath))
|
|
60
|
-
return null;
|
|
61
|
-
return JSON.parse(fs.readFileSync(castleJsonPath, 'utf-8'));
|
|
62
|
-
}
|
|
63
|
-
async function captureAndUpload(projectDir, ws, castleJson) {
|
|
64
|
-
// A deck has no cardId until its first save, and the cover IS that card's
|
|
65
|
-
// background -- so there is nothing to set it on yet. Both callers reach here
|
|
66
|
-
// right after a save, so this is the never-saved edge, not the normal path.
|
|
67
|
-
// Reported back so a caller doesn't record a cover that was never set.
|
|
68
|
-
const { cardId } = castleJson;
|
|
69
|
-
if (!cardId) {
|
|
70
|
-
ws.close();
|
|
71
|
-
console.log('Preview skipped: save the deck first.');
|
|
72
|
-
return false;
|
|
73
|
-
}
|
|
74
|
-
const base64 = await takeScreenshot(ws);
|
|
75
|
-
ws.close();
|
|
76
|
-
fs.writeFileSync(path.join(projectDir, 'preview.png'), Buffer.from(base64, 'base64'));
|
|
77
|
-
console.log('Saved preview.png');
|
|
78
|
-
const file = await api.uploadBase64(base64, 'preview.png');
|
|
79
|
-
await api.updateCardCustomBackgroundImage(cardId, file.fileId);
|
|
80
|
-
console.log('Set deck preview image.');
|
|
81
|
-
return true;
|
|
82
|
-
}
|
|
83
|
-
export async function savePreviewImage(dir, wsPort, noRestart = false) {
|
|
84
|
-
const projectDir = path.resolve(dir);
|
|
85
|
-
const castleJson = readCastleJson(projectDir);
|
|
86
|
-
if (!castleJson) {
|
|
87
|
-
console.error('No castle.json found. Save the deck first.');
|
|
88
|
-
process.exit(1);
|
|
89
|
-
}
|
|
90
|
-
if (!config.getToken()) {
|
|
91
|
-
console.error('Not logged in. Run `castle-web login` first.');
|
|
92
|
-
process.exit(1);
|
|
93
|
-
}
|
|
94
|
-
const ws = await connectWS(wsPort);
|
|
95
|
-
if (!noRestart)
|
|
96
|
-
await waitForRestart(ws);
|
|
97
|
-
// Asking for this by name is choosing a cover, so it goes to the card's
|
|
98
|
-
// CUSTOM slot -- the one that takes precedence over the cover a save
|
|
99
|
-
// publishes. Clearing it is an editor action (Deck settings -> Clear cover).
|
|
100
|
-
await captureAndUpload(projectDir, ws, castleJson);
|
|
101
|
-
}
|
package/dist/save-deck.js
CHANGED
|
@@ -79,8 +79,59 @@ async function uploadSource(projectDir, deckId) {
|
|
|
79
79
|
//
|
|
80
80
|
// Never fatal: a deck that saves but whose cover upload failed is a deck that
|
|
81
81
|
// saved. Losing the push over a thumbnail would be the wrong trade.
|
|
82
|
+
// The deck's own serve, if one is running -- the URL a headless cover capture
|
|
83
|
+
// loads. No serve means no deck to photograph, which is a normal state (a save
|
|
84
|
+
// from a script), not an error.
|
|
85
|
+
function runningServeUrl(projectDir) {
|
|
86
|
+
try {
|
|
87
|
+
const raw = fs.readFileSync(path.join(projectDir, '.castle', 'serve.json'), 'utf8');
|
|
88
|
+
const port = JSON.parse(raw).port;
|
|
89
|
+
return typeof port === 'number' ? `http://localhost:${port}` : null;
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// Photograph the running deck with a real browser, into a scratch file.
|
|
96
|
+
//
|
|
97
|
+
// This OUTRANKS the editor's `preview.png`, and deliberately: both are the
|
|
98
|
+
// AUTOMATIC cover (a creator's hand-picked one lives in the card's custom slot
|
|
99
|
+
// and outranks both), so the question is only which automatic capture is better
|
|
100
|
+
// -- and the in-page one silently drops a WebGL canvas that lacks
|
|
101
|
+
// `preserveDrawingBuffer`, plus anything using `filter`/`clip-path`/`mask`. A
|
|
102
|
+
// browser compositing its own frame has no such list.
|
|
103
|
+
//
|
|
104
|
+
// `preview.png` does have one real advantage -- it is whatever the creator last
|
|
105
|
+
// had on screen, where this is a cold load -- so it stays the fallback. But a
|
|
106
|
+
// cover that is missing the game beats a cover of the opening seconds only if
|
|
107
|
+
// you never look at it, and nobody looks at an unattended capture. Writing to a
|
|
108
|
+
// scratch file rather than over `preview.png` keeps that fallback intact.
|
|
109
|
+
async function captureAutomaticCover(projectDir) {
|
|
110
|
+
const serveUrl = runningServeUrl(projectDir);
|
|
111
|
+
if (!serveUrl)
|
|
112
|
+
return null;
|
|
113
|
+
const outPath = path.join(projectDir, '.castle', 'headless-cover.png');
|
|
114
|
+
const { captureCoverHeadless } = await import('./headlessCover.js');
|
|
115
|
+
const result = await captureCoverHeadless({
|
|
116
|
+
serveUrl,
|
|
117
|
+
outPath,
|
|
118
|
+
onProgress: (message) => console.log(message),
|
|
119
|
+
});
|
|
120
|
+
if (!result.ok) {
|
|
121
|
+
// Never fatal, same as the upload below: a deck that saved without a cover
|
|
122
|
+
// is a deck that saved.
|
|
123
|
+
console.log(`Cover capture skipped: ${result.error}`);
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
return outPath;
|
|
127
|
+
}
|
|
82
128
|
async function uploadLocalCover(projectDir) {
|
|
83
|
-
const
|
|
129
|
+
const captured = await captureAutomaticCover(projectDir);
|
|
130
|
+
const coverPath = captured ?? path.join(projectDir, COVER_FILE);
|
|
131
|
+
if (captured)
|
|
132
|
+
console.log('Captured a cover from the running deck.');
|
|
133
|
+
else if (fs.existsSync(coverPath))
|
|
134
|
+
console.log('Using the cover captured in the editor.');
|
|
84
135
|
if (!fs.existsSync(coverPath))
|
|
85
136
|
return null;
|
|
86
137
|
try {
|
package/dist/serve.js
CHANGED
|
@@ -239,6 +239,22 @@ export async function serve(dir, options = {}) {
|
|
|
239
239
|
console.warn(`auto-update: skipped -- ${e instanceof Error ? e.message : String(e)}`);
|
|
240
240
|
});
|
|
241
241
|
}
|
|
242
|
+
// Start pulling the headless browser now, in the background.
|
|
243
|
+
//
|
|
244
|
+
// `save-deck` photographs the deck with it to make the deck's cover, and the
|
|
245
|
+
// binary is a ~250MB one-time download. Left lazy, that download lands inside
|
|
246
|
+
// the creator's first "Push to Castle" -- the one moment they are watching a
|
|
247
|
+
// spinner and least want an unexplained wait. A serve means someone is editing
|
|
248
|
+
// a deck they will probably push, so this is the cheapest place to get ahead
|
|
249
|
+
// of it.
|
|
250
|
+
//
|
|
251
|
+
// Fire-and-forget and single-flight: it shares one download with the agent's
|
|
252
|
+
// playtest tool, so this is free when that already pulled it, and every
|
|
253
|
+
// failure is swallowed -- the capture re-detects and reports properly at the
|
|
254
|
+
// point it actually matters.
|
|
255
|
+
void import('./native/playtest-browser.js')
|
|
256
|
+
.then((m) => m.createPlaytestBrowserManager().prewarm())
|
|
257
|
+
.catch(() => undefined);
|
|
242
258
|
// Close stdin when not attached to a TTY so the process can be cleanly
|
|
243
259
|
// backgrounded with `&` (otherwise stdin reads can block or hold the parent).
|
|
244
260
|
if (!process.stdin.isTTY) {
|
|
Binary file
|
|
Binary file
|