castle-web-cli 0.4.115 → 0.4.117
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 +2 -2
- package/dist/agent.d.ts +9 -9
- package/dist/agent.js +590 -589
- package/dist/castleJson.d.ts +6 -0
- package/dist/castleJson.js +10 -0
- package/dist/editorConfig.js +27 -3
- package/dist/imports.d.ts +4 -0
- package/dist/imports.js +59 -10
- package/dist/init.d.ts +3 -0
- package/dist/init.js +103 -87
- package/dist/install.d.ts +1 -1
- package/dist/install.js +26 -24
- package/dist/shell/assets/{index-CUamb8rK.js → index-DiPlPGyg.js} +2 -2
- package/dist/shell/index.html +1 -1
- package/dist/vitePlugins.js +1 -1
- package/kits/physics-2d/CLAUDE.md +28 -16
- package/kits/physics-2d/behaviors/Joints.jsx +1 -1
- package/kits/physics-2d/behaviors/Sprite.jsx +17 -12
- package/kits/physics-2d/blueprints/ball.scene +1 -1
- package/kits/physics-2d/blueprints/block.scene +1 -1
- package/kits/physics-2d/blueprints/cauldron.scene +1 -1
- package/kits/physics-2d/blueprints/crate.scene +1 -1
- package/kits/physics-2d/castle.json +11 -3
- package/kits/physics-2d/docs/pxart-format.md +537 -51
- package/kits/physics-2d/editors/PxArtEditor.jsx +1794 -65
- package/kits/physics-2d/editors/brushFit.js +535 -0
- package/kits/physics-2d/editors/brushShapes.js +140 -0
- package/kits/physics-2d/editors/mediaFile.js +12 -1
- package/kits/physics-2d/editors/pathOverlay.js +340 -0
- package/kits/physics-2d/editors/pathTools.js +1906 -0
- package/kits/physics-2d/editors/pixelCanvas.js +13 -0
- package/kits/physics-2d/editors/pixelEditorChrome.jsx +2 -2
- package/kits/physics-2d/editors/pixelGeometry.js +4 -2
- package/kits/physics-2d/editors/pixelInspector.jsx +410 -37
- package/kits/physics-2d/editors/pxArtEditorModel.js +172 -16
- package/kits/physics-2d/editors/pxArtTimeline.jsx +163 -43
- package/kits/physics-2d/editors/pxArtTimeline.module.css +31 -5
- package/kits/physics-2d/engine/assets.js +1 -1
- package/kits/physics-2d/engine/blueprint.js +3 -3
- package/kits/physics-2d/engine/files.js +3 -1
- package/kits/physics-2d/engine/liveReload.js +1 -1
- package/kits/physics-2d/engine/physics/jointArt.js +3 -3
- package/kits/physics-2d/engine/pxart.js +153 -35
- package/kits/physics-2d/engine/pxartPath.js +1356 -0
- package/kits/physics-2d/engine/pxartSmooth.js +276 -125
- package/kits/physics-2d/engine/ui.jsx +22 -1
- package/kits/physics-2d/engine/ui.module.css +36 -12
- package/kits/physics-2d/package-lock.json +1 -1
- package/kits/physics-2d/scripts/draw.mjs +7 -7
- package/kits/physics-2d/scripts/import-svg.mjs +1231 -0
- package/kits/physics-2d/scripts/svg-emission-guide.md +92 -0
- package/package.json +1 -1
- /package/kits/physics-2d/drawings/{block.pxart → block.sprite} +0 -0
- /package/kits/physics-2d/drawings/{cauldron.pxart → cauldron.sprite} +0 -0
- /package/kits/physics-2d/drawings/{joint-rope.pxart → joint-rope.sprite} +0 -0
package/dist/castleJson.d.ts
CHANGED
|
@@ -10,7 +10,13 @@ export interface CastleJson {
|
|
|
10
10
|
cardId?: string;
|
|
11
11
|
imports?: Record<string, DeckImport>;
|
|
12
12
|
autoUpdateWhenImported?: boolean;
|
|
13
|
+
main?: string;
|
|
14
|
+
starterScene?: string;
|
|
13
15
|
[key: string]: unknown;
|
|
14
16
|
}
|
|
17
|
+
export declare const DEFAULT_STARTER_SCENE = "scenes/main.scene";
|
|
18
|
+
export declare function deckStarterScene(dir: string): string;
|
|
19
|
+
export declare const DEFAULT_MAIN = "main.jsx";
|
|
20
|
+
export declare function deckMainFile(dir: string): string;
|
|
15
21
|
export declare function readCastleJson(dir: string): CastleJson | null;
|
|
16
22
|
export declare function readCastleJsonOrThrow(dir: string): CastleJson | null;
|
package/dist/castleJson.js
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
2
|
import * as path from 'path';
|
|
3
|
+
export const DEFAULT_STARTER_SCENE = 'scenes/main.scene';
|
|
4
|
+
export function deckStarterScene(dir) {
|
|
5
|
+
const declared = readCastleJson(dir)?.starterScene;
|
|
6
|
+
return typeof declared === 'string' && declared.trim() ? declared.trim() : DEFAULT_STARTER_SCENE;
|
|
7
|
+
}
|
|
8
|
+
export const DEFAULT_MAIN = 'main.jsx';
|
|
9
|
+
export function deckMainFile(dir) {
|
|
10
|
+
const declared = readCastleJson(dir)?.main;
|
|
11
|
+
return typeof declared === 'string' && declared.trim() ? declared.trim() : DEFAULT_MAIN;
|
|
12
|
+
}
|
|
3
13
|
// A missing castle.json is normal (a deck has none until its first save), so it
|
|
4
14
|
// reads as `null` rather than an error. An unparseable one is not -- `onInvalid`
|
|
5
15
|
// is how a caller says whether that should be fatal here or just another `null`.
|
package/dist/editorConfig.js
CHANGED
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
// sits there like any other), so resolving is a read of each import's own
|
|
11
11
|
// castle.json -- there is no graph to walk.
|
|
12
12
|
//
|
|
13
|
-
//
|
|
14
|
-
// `defaultPlayFile`
|
|
13
|
+
// `initialPanels` and `visiblePaths`/`hiddenPaths` stay the deck's own, read from
|
|
14
|
+
// the deck root only. `defaultPlayFile` falls back to an import's when the deck
|
|
15
|
+
// names none -- see `importedDefaultPlayFile`.
|
|
15
16
|
import * as fs from 'fs';
|
|
16
17
|
import * as path from 'path';
|
|
17
18
|
import { readCastleJson } from './castleJson.js';
|
|
@@ -48,9 +49,32 @@ export function readEditorConfig(deckDir) {
|
|
|
48
49
|
visiblePaths: asStringArray(editor.visiblePaths),
|
|
49
50
|
fileTypes: asFileTypes(editor.fileTypes),
|
|
50
51
|
extensions: asStringArray(editor.extensions) ?? asStringArray(data.editorExtensions),
|
|
51
|
-
defaultPlayFile: typeof editor.defaultPlayFile === 'string'
|
|
52
|
+
defaultPlayFile: typeof editor.defaultPlayFile === 'string'
|
|
53
|
+
? editor.defaultPlayFile
|
|
54
|
+
: importedDefaultPlayFile(deckDir),
|
|
52
55
|
};
|
|
53
56
|
}
|
|
57
|
+
// Which scene the play panel opens with, when the deck names none itself. Only a
|
|
58
|
+
// scene kit has the concept, and a deck that imported one has its scenes without
|
|
59
|
+
// having said so -- without this the panel loads the kit's runtime with no scene
|
|
60
|
+
// and paints nothing, which reads as "the deck is broken".
|
|
61
|
+
//
|
|
62
|
+
// The deck's own value always wins; this is the fallback, and it is deliberately
|
|
63
|
+
// the only other field resolved across imports, because it names a file that
|
|
64
|
+
// must exist in THIS deck (the starter scene the kit's own scaffold wrote).
|
|
65
|
+
function importedDefaultPlayFile(deckDir) {
|
|
66
|
+
for (const alias of importAliases(deckDir)) {
|
|
67
|
+
const value = readCastleJson(path.join(deckDir, IMPORTS_DIR, alias))?.editor;
|
|
68
|
+
const declared = value && typeof value === 'object'
|
|
69
|
+
? value.defaultPlayFile
|
|
70
|
+
: undefined;
|
|
71
|
+
if (typeof declared !== 'string' || !declared.trim())
|
|
72
|
+
continue;
|
|
73
|
+
if (fs.existsSync(path.join(deckDir, declared.trim())))
|
|
74
|
+
return declared.trim();
|
|
75
|
+
}
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
54
78
|
// The deck's imports, in the order the deck pinned them (its castle.json
|
|
55
79
|
// `imports` map), then anything on disk that isn't pinned. Pin order puts an
|
|
56
80
|
// import the deck asked for ahead of one that came in behind it, which is the
|
package/dist/imports.d.ts
CHANGED
|
@@ -26,6 +26,10 @@ export interface AddImportResult {
|
|
|
26
26
|
transitive: string[];
|
|
27
27
|
/** `name@range (from alias)` entries merged into the deck's package.json. */
|
|
28
28
|
dependencies: string[];
|
|
29
|
+
/** The import's declared main, when this deck adopted it as its entry. */
|
|
30
|
+
adoptedMain: string | null;
|
|
31
|
+
/** Set when the import declares a main but this deck already has work in it. */
|
|
32
|
+
offersMain: string | null;
|
|
29
33
|
}
|
|
30
34
|
export declare function addImportTo(dir: string, options?: {
|
|
31
35
|
deckRef?: string;
|
package/dist/imports.js
CHANGED
|
@@ -80,11 +80,7 @@ function slugifyAlias(name) {
|
|
|
80
80
|
// An explicit `--as` may already be qualified (`someone.their-deck`); slugify
|
|
81
81
|
// each part so the separator survives.
|
|
82
82
|
function slugifyGivenAlias(name) {
|
|
83
|
-
return name
|
|
84
|
-
.split('.')
|
|
85
|
-
.map(slugifyAlias)
|
|
86
|
-
.filter(Boolean)
|
|
87
|
-
.join('.');
|
|
83
|
+
return name.split('.').map(slugifyAlias).filter(Boolean).join('.');
|
|
88
84
|
}
|
|
89
85
|
// Aliases are qualified by author -- `lovefromtom.fish-town` -- because deck
|
|
90
86
|
// titles are not unique across users (nor even within one), and an import's name
|
|
@@ -255,7 +251,8 @@ export async function restoreMissingImports(deckDir) {
|
|
|
255
251
|
fs.cpSync(kitDir, destDir, {
|
|
256
252
|
recursive: true,
|
|
257
253
|
verbatimSymlinks: true,
|
|
258
|
-
filter: (src) => src === kitDir ||
|
|
254
|
+
filter: (src) => src === kitDir ||
|
|
255
|
+
!['node_modules', '.castle', 'dist', '.git'].includes(path.basename(src)),
|
|
259
256
|
});
|
|
260
257
|
}
|
|
261
258
|
else {
|
|
@@ -438,7 +435,9 @@ async function addTransitiveImports(deckDir, viaAlias, seen) {
|
|
|
438
435
|
// loop on a cycle.
|
|
439
436
|
export function importedDeckIds(deckDir) {
|
|
440
437
|
const pins = readCastleJson(deckDir)?.imports ?? {};
|
|
441
|
-
return new Set(Object.values(pins)
|
|
438
|
+
return new Set(Object.values(pins)
|
|
439
|
+
.map((p) => p?.deckId)
|
|
440
|
+
.filter((id) => !!id));
|
|
442
441
|
}
|
|
443
442
|
// The import itself. Throws ImportError rather than exiting, because this also
|
|
444
443
|
// runs inside the serve (from the editor's Import panel), where taking the
|
|
@@ -477,7 +476,52 @@ export async function addImportTo(dir, options = {}) {
|
|
|
477
476
|
const { replaced } = await placeImport(targetDir, alias, deckId, source);
|
|
478
477
|
const transitive = await addTransitiveImports(targetDir, alias, importedDeckIds(targetDir));
|
|
479
478
|
const dependencies = syncImportDependencies(targetDir);
|
|
480
|
-
|
|
479
|
+
// A kit declares its entry module (castle.json `main`). Importing one into a
|
|
480
|
+
// deck that is still the untouched `--kit none` scaffold means "start from this
|
|
481
|
+
// kit", so adopt its entry -- otherwise the deck keeps booting its own stub and
|
|
482
|
+
// the kit's runtime and editors never mount. A deck with any work in it is left
|
|
483
|
+
// alone and just told the option exists, since repointing it would go dark.
|
|
484
|
+
const importDir = path.join(targetDir, IMPORTS_DIR, alias);
|
|
485
|
+
const declaredMain = tryReadCastleJson(importDir)?.main;
|
|
486
|
+
let adoptedMain = null;
|
|
487
|
+
if (typeof declaredMain === 'string' && declaredMain.trim()) {
|
|
488
|
+
const { isPristineBareDeck, adoptImportEntry, writeStarterScene } = await import('./init.js');
|
|
489
|
+
if (isPristineBareDeck(targetDir)) {
|
|
490
|
+
adoptImportEntry(targetDir, alias, declaredMain.trim());
|
|
491
|
+
adoptedMain = declaredMain.trim();
|
|
492
|
+
// A kit's runtime with no scene to run is a blank screen, and the agent
|
|
493
|
+
// has nothing to edit. Give the deck the kit's own starter, same as
|
|
494
|
+
// `init --kit`. Only when the deck has no scenes -- never overwrite.
|
|
495
|
+
if (!fs.existsSync(path.join(targetDir, 'scenes'))) {
|
|
496
|
+
try {
|
|
497
|
+
writeStarterScene(targetDir, importDir, alias);
|
|
498
|
+
}
|
|
499
|
+
catch (e) {
|
|
500
|
+
console.warn(`Adopted ${alias} but could not write its starter scene: ${e instanceof Error ? e.message : String(e)}`);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
// An import's packages have to be ON DISK, not just named in package.json:
|
|
506
|
+
// the deck fails to load the moment its code imports one. Telling the caller
|
|
507
|
+
// to run `castle-web install` only worked for the CLI -- an import taken from
|
|
508
|
+
// the editor left the deck broken with no visible reason why.
|
|
509
|
+
if (dependencies.length > 0) {
|
|
510
|
+
try {
|
|
511
|
+
// Imported here rather than at the top: install.ts imports THIS module,
|
|
512
|
+
// and a top-level import back would be a cycle.
|
|
513
|
+
const { installDeps } = await import('./install.js');
|
|
514
|
+
await installDeps(targetDir, false);
|
|
515
|
+
}
|
|
516
|
+
catch (e) {
|
|
517
|
+
// Never fail the import over this: the files are already in place, and a
|
|
518
|
+
// hand-run install still fixes it.
|
|
519
|
+
console.warn(`Imported ${alias}, but installing its dependencies failed -- run \`castle-web install\`. ${e instanceof Error ? e.message : String(e)}`);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
const trimmedMain = typeof declaredMain === 'string' && declaredMain.trim() ? declaredMain.trim() : null;
|
|
523
|
+
const offersMain = trimmedMain && !adoptedMain ? trimmedMain : null;
|
|
524
|
+
return { deckId, alias, replaced, transitive, dependencies, adoptedMain, offersMain };
|
|
481
525
|
}
|
|
482
526
|
export async function addImport(dir, options = {}) {
|
|
483
527
|
let result;
|
|
@@ -500,8 +544,13 @@ export async function addImport(dir, options = {}) {
|
|
|
500
544
|
}
|
|
501
545
|
for (const dep of result.dependencies)
|
|
502
546
|
console.log(`Added dependency ${dep}`);
|
|
503
|
-
if (result.
|
|
504
|
-
console.log(
|
|
547
|
+
if (result.adoptedMain) {
|
|
548
|
+
console.log(`This deck now runs ${result.alias} (index.html -> ${result.adoptedMain}).`);
|
|
549
|
+
}
|
|
550
|
+
else if (result.offersMain) {
|
|
551
|
+
console.log(`${result.alias} is a kit. To run this deck on it, point index.html at ` +
|
|
552
|
+
`/${IMPORTS_DIR}/${result.alias}/${result.offersMain}.`);
|
|
553
|
+
}
|
|
505
554
|
}
|
|
506
555
|
// Take one import's update: replace its files with the version the server has
|
|
507
556
|
// now, then follow that version's own pins for anything newly required. Both
|
package/dist/init.d.ts
CHANGED
|
@@ -5,7 +5,10 @@ export declare function resolveScaffoldRefs(): {
|
|
|
5
5
|
cliDistAbs: string | null;
|
|
6
6
|
sdkPathPosix: string | null;
|
|
7
7
|
};
|
|
8
|
+
export declare function isPristineBareDeck(dir: string): boolean;
|
|
9
|
+
export declare function adoptImportEntry(deckDir: string, alias: string, mainFile: string): void;
|
|
8
10
|
export declare function getCliVersion(): string;
|
|
11
|
+
export declare function writeStarterScene(projectDir: string, kitDir: string, alias: string): void;
|
|
9
12
|
export declare function init(dir: string, opts?: {
|
|
10
13
|
kit?: string;
|
|
11
14
|
serve?: boolean;
|
package/dist/init.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import * as fs from
|
|
2
|
-
import * as path from
|
|
3
|
-
import { COMMON_INSTRUCTIONS } from
|
|
4
|
-
import { installDeps } from
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { COMMON_INSTRUCTIONS } from './commonInstructions.js';
|
|
4
|
+
import { installDeps } from './install.js';
|
|
5
|
+
import { deckMainFile, deckStarterScene } from './castleJson.js';
|
|
6
|
+
import { IMPORTS_DIR, lockImportTree } from './imports.js';
|
|
7
|
+
import { getCliEntryPath, getKitsDir, getRepoRoot, getSdkPackagePath, toPosixPath, } from './localPaths.js';
|
|
8
|
+
import { serve } from './serve.js';
|
|
8
9
|
const INDEX_HTML = `<!DOCTYPE html>
|
|
9
10
|
<html>
|
|
10
11
|
<head>
|
|
@@ -32,27 +33,22 @@ card.appendChild(el);
|
|
|
32
33
|
`;
|
|
33
34
|
// Default kit imported by `init` when no --kit is given. `none`/`bare` skip the
|
|
34
35
|
// kit and produce the minimal index.html + game.js stub above.
|
|
35
|
-
const DEFAULT_KIT =
|
|
36
|
+
const DEFAULT_KIT = 'physics-2d';
|
|
36
37
|
// Registry version of castle-web-sdk to inject when scaffolding from a
|
|
37
38
|
// globally-installed castle-web (not from inside the workspace). Bumped
|
|
38
39
|
// alongside cli/sdk version bumps.
|
|
39
|
-
const PUBLISHED_SDK_VERSION =
|
|
40
|
+
const PUBLISHED_SDK_VERSION = '0.4.12';
|
|
40
41
|
// The account the first-party kits are (to be) published under, so a kit
|
|
41
42
|
// imported from the CLI's copy is named the same as one fetched from the server.
|
|
42
|
-
const KIT_AUTHOR =
|
|
43
|
+
const KIT_AUTHOR = 'castle';
|
|
43
44
|
// Never copied into a deck's import: build/dependency junk.
|
|
44
|
-
const KIT_COPY_EXCLUDE = new Set([
|
|
45
|
-
"node_modules",
|
|
46
|
-
".castle",
|
|
47
|
-
"dist",
|
|
48
|
-
".git",
|
|
49
|
-
]);
|
|
45
|
+
const KIT_COPY_EXCLUDE = new Set(['node_modules', '.castle', 'dist', '.git']);
|
|
50
46
|
// The bare (`--kit none`) deck's default editor config: just the Files panel and
|
|
51
47
|
// a Play panel. No file filtering -- a bare deck is hand-rolled, so the author
|
|
52
48
|
// knows what's there; show everything for now.
|
|
53
49
|
const BARE_CASTLE_JSON = {
|
|
54
50
|
editor: {
|
|
55
|
-
initialPanels: [{ type:
|
|
51
|
+
initialPanels: [{ type: 'files' }, { type: 'playtest' }],
|
|
56
52
|
hiddenPaths: [],
|
|
57
53
|
visiblePaths: [],
|
|
58
54
|
},
|
|
@@ -70,17 +66,11 @@ export function resolveScaffoldRefs() {
|
|
|
70
66
|
// npm package and need the published refs + `castle-web` binary.
|
|
71
67
|
const workspaceMode = fs.existsSync(sdkPath);
|
|
72
68
|
const sdkPathPosix = workspaceMode ? toPosixPath(sdkPath) : null;
|
|
73
|
-
const cliDistAbs = workspaceMode
|
|
74
|
-
? toPosixPath(path.dirname(getCliEntryPath()))
|
|
75
|
-
: null;
|
|
69
|
+
const cliDistAbs = workspaceMode ? toPosixPath(path.dirname(getCliEntryPath())) : null;
|
|
76
70
|
return {
|
|
77
71
|
workspaceMode,
|
|
78
|
-
sdkRef: workspaceMode
|
|
79
|
-
|
|
80
|
-
: `^${PUBLISHED_SDK_VERSION}`,
|
|
81
|
-
cliCommand: workspaceMode
|
|
82
|
-
? `node ${toPosixPath(getCliEntryPath())}`
|
|
83
|
-
: "castle-web",
|
|
72
|
+
sdkRef: workspaceMode ? `file:${sdkPathPosix}` : `^${PUBLISHED_SDK_VERSION}`,
|
|
73
|
+
cliCommand: workspaceMode ? `node ${toPosixPath(getCliEntryPath())}` : 'castle-web',
|
|
84
74
|
cliDistAbs,
|
|
85
75
|
sdkPathPosix,
|
|
86
76
|
};
|
|
@@ -90,9 +80,9 @@ function makeClaudeMd() {
|
|
|
90
80
|
// castle-experimental-web checkout, but breaks when the scaffold lives
|
|
91
81
|
// outside the repo (the relative path no longer resolves).
|
|
92
82
|
const repoRoot = getRepoRoot();
|
|
93
|
-
const upstream = path.join(repoRoot,
|
|
83
|
+
const upstream = path.join(repoRoot, 'CLAUDE.md');
|
|
94
84
|
try {
|
|
95
|
-
return fs.readFileSync(upstream,
|
|
85
|
+
return fs.readFileSync(upstream, 'utf8').trimEnd() + '\n';
|
|
96
86
|
}
|
|
97
87
|
catch {
|
|
98
88
|
return `# Castle Experimental Web\n\nSee https://github.com/castle-xyz/castle-experimental-web for the agent guide.\n`;
|
|
@@ -102,15 +92,15 @@ function makeClaudeMd() {
|
|
|
102
92
|
// the kit's (or bare) CLAUDE.md is written; the AGENTS.md symlink picks the
|
|
103
93
|
// appended content up for free.
|
|
104
94
|
function appendCommonInstructions(projectDir) {
|
|
105
|
-
const claudePath = path.join(projectDir,
|
|
95
|
+
const claudePath = path.join(projectDir, 'CLAUDE.md');
|
|
106
96
|
const existing = fs.existsSync(claudePath)
|
|
107
|
-
? fs.readFileSync(claudePath,
|
|
108
|
-
:
|
|
97
|
+
? fs.readFileSync(claudePath, 'utf8').trimEnd() + '\n\n'
|
|
98
|
+
: '';
|
|
109
99
|
fs.writeFileSync(claudePath, existing + COMMON_INSTRUCTIONS);
|
|
110
100
|
}
|
|
111
101
|
function tryMakeAgentsSymlink(agentsPath) {
|
|
112
102
|
try {
|
|
113
|
-
fs.symlinkSync(
|
|
103
|
+
fs.symlinkSync('CLAUDE.md', agentsPath);
|
|
114
104
|
}
|
|
115
105
|
catch {
|
|
116
106
|
// symlink already exists / unsupported FS — non-fatal
|
|
@@ -124,11 +114,11 @@ function makeKitDeckScripts(kitDir, alias, cliCommand) {
|
|
|
124
114
|
const scripts = {
|
|
125
115
|
restart: `${cliCommand} restart .`,
|
|
126
116
|
screenshot: `${cliCommand} screenshot .`,
|
|
127
|
-
|
|
117
|
+
'save-deck': `${cliCommand} save-deck .`,
|
|
128
118
|
};
|
|
129
119
|
// `draw` is a kit script, not a CLI one, so it only exists if this kit ships
|
|
130
120
|
// it -- and it runs from the deck root, where the sprites belong.
|
|
131
|
-
if (fs.existsSync(path.join(kitDir,
|
|
121
|
+
if (fs.existsSync(path.join(kitDir, 'scripts', 'draw.mjs'))) {
|
|
132
122
|
scripts.draw = `node ${IMPORTS_DIR}/${alias}/scripts/draw.mjs`;
|
|
133
123
|
}
|
|
134
124
|
return scripts;
|
|
@@ -138,38 +128,63 @@ function makePackageJson(projectDir) {
|
|
|
138
128
|
return {
|
|
139
129
|
name: path.basename(projectDir),
|
|
140
130
|
private: true,
|
|
141
|
-
type:
|
|
131
|
+
type: 'module',
|
|
142
132
|
scripts: {
|
|
143
133
|
restart: `${cliCommand} restart .`,
|
|
144
134
|
screenshot: `${cliCommand} screenshot .`,
|
|
145
|
-
|
|
135
|
+
'save-deck': `${cliCommand} save-deck .`,
|
|
146
136
|
},
|
|
147
137
|
dependencies: {
|
|
148
|
-
|
|
138
|
+
'castle-web-sdk': sdkRef,
|
|
149
139
|
},
|
|
150
140
|
};
|
|
151
141
|
}
|
|
152
142
|
// Some coding agents read AGENTS.md by convention. Symlink so they get the
|
|
153
143
|
// same guidance without a duplicate copy.
|
|
154
144
|
function ensureAgentsSymlink(projectDir) {
|
|
155
|
-
const agentsPath = path.join(projectDir,
|
|
145
|
+
const agentsPath = path.join(projectDir, 'AGENTS.md');
|
|
156
146
|
if (fs.lstatSync(agentsPath, { throwIfNoEntry: false }))
|
|
157
147
|
return;
|
|
158
148
|
// Don't create a dangling link — only symlink when CLAUDE.md is present.
|
|
159
|
-
if (!fs.existsSync(path.join(projectDir,
|
|
149
|
+
if (!fs.existsSync(path.join(projectDir, 'CLAUDE.md')))
|
|
160
150
|
return;
|
|
161
151
|
tryMakeAgentsSymlink(agentsPath);
|
|
162
152
|
}
|
|
163
153
|
// Bare scaffold: a plain code-only deck with no kit framework.
|
|
154
|
+
// Whether a deck is still exactly what `--kit none` produced -- both the entry
|
|
155
|
+
// and the stub game it boots, untouched. `addImportTo` asks before repointing a
|
|
156
|
+
// deck's entry at an imported kit: on a pristine scaffold there is nothing to
|
|
157
|
+
// lose, and one edited character anywhere makes this false, so a deck someone
|
|
158
|
+
// has actually worked in is never quietly taken over.
|
|
159
|
+
export function isPristineBareDeck(dir) {
|
|
160
|
+
try {
|
|
161
|
+
return (fs.readFileSync(path.join(dir, 'index.html'), 'utf8') === INDEX_HTML &&
|
|
162
|
+
fs.readFileSync(path.join(dir, 'game.js'), 'utf8') === GAME_JS);
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
// Repoint a deck's entry at an imported deck's main module.
|
|
169
|
+
//
|
|
170
|
+
// Writes the SAME index.html `init --kit` writes, rather than swapping the bare
|
|
171
|
+
// stub's script tag: a kit mounts React into `<div id="root">`, which the bare
|
|
172
|
+
// page has no element for -- the result was a deck that loaded the kit and
|
|
173
|
+
// rendered nothing -- and the bare page also lacks the dark color-scheme
|
|
174
|
+
// declaration, so it painted white before the kit's CSS arrived.
|
|
175
|
+
export function adoptImportEntry(deckDir, alias, mainFile) {
|
|
176
|
+
writeDeckIndexHtml(deckDir, alias, path.basename(deckDir), mainFile);
|
|
177
|
+
fs.rmSync(path.join(deckDir, 'game.js'), { force: true });
|
|
178
|
+
}
|
|
164
179
|
function scaffoldBare(projectDir) {
|
|
165
180
|
fs.mkdirSync(projectDir, { recursive: true });
|
|
166
|
-
fs.writeFileSync(path.join(projectDir,
|
|
167
|
-
fs.writeFileSync(path.join(projectDir,
|
|
168
|
-
fs.writeFileSync(path.join(projectDir,
|
|
169
|
-
fs.writeFileSync(path.join(projectDir,
|
|
181
|
+
fs.writeFileSync(path.join(projectDir, 'index.html'), INDEX_HTML);
|
|
182
|
+
fs.writeFileSync(path.join(projectDir, 'game.js'), GAME_JS);
|
|
183
|
+
fs.writeFileSync(path.join(projectDir, 'castle.json'), JSON.stringify(BARE_CASTLE_JSON, null, 2) + '\n');
|
|
184
|
+
fs.writeFileSync(path.join(projectDir, 'CLAUDE.md'), makeClaudeMd());
|
|
170
185
|
appendCommonInstructions(projectDir);
|
|
171
186
|
ensureAgentsSymlink(projectDir);
|
|
172
|
-
fs.writeFileSync(path.join(projectDir,
|
|
187
|
+
fs.writeFileSync(path.join(projectDir, 'package.json'), JSON.stringify(makePackageJson(projectDir), null, 2) + '\n');
|
|
173
188
|
}
|
|
174
189
|
// Copy a framework kit from kits/<kit>/ into the new deck dir, dropping
|
|
175
190
|
// build/dependency junk and castle.json.
|
|
@@ -180,7 +195,7 @@ function requireKitDir(kit) {
|
|
|
180
195
|
if (fs.existsSync(kitDir) && fs.statSync(kitDir).isDirectory())
|
|
181
196
|
return kitDir;
|
|
182
197
|
console.error(`Kit "${kit}" not found at ${kitDir}.`);
|
|
183
|
-
console.error(
|
|
198
|
+
console.error('Available kits:');
|
|
184
199
|
try {
|
|
185
200
|
const kits = fs
|
|
186
201
|
.readdirSync(getKitsDir())
|
|
@@ -189,34 +204,34 @@ function requireKitDir(kit) {
|
|
|
189
204
|
for (const name of kits)
|
|
190
205
|
console.error(` ${name}`);
|
|
191
206
|
else
|
|
192
|
-
console.error(
|
|
207
|
+
console.error(' (none)');
|
|
193
208
|
}
|
|
194
209
|
catch {
|
|
195
|
-
console.error(
|
|
210
|
+
console.error(' (none — kits/ directory is missing)');
|
|
196
211
|
}
|
|
197
|
-
console.error(
|
|
212
|
+
console.error('Or use `--kit none` for a bare code-only deck.');
|
|
198
213
|
return process.exit(1);
|
|
199
214
|
}
|
|
200
215
|
function readJsonFile(file) {
|
|
201
216
|
try {
|
|
202
|
-
return JSON.parse(fs.readFileSync(file,
|
|
217
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
203
218
|
}
|
|
204
219
|
catch {
|
|
205
220
|
return null;
|
|
206
221
|
}
|
|
207
222
|
}
|
|
208
223
|
function writeJsonFile(file, value) {
|
|
209
|
-
fs.writeFileSync(file, JSON.stringify(value, null, 2) +
|
|
224
|
+
fs.writeFileSync(file, JSON.stringify(value, null, 2) + '\n', 'utf8');
|
|
210
225
|
}
|
|
211
226
|
export function getCliVersion() {
|
|
212
|
-
const pkg = readJsonFile(path.join(getKitsDir(),
|
|
213
|
-
return typeof pkg?.version ===
|
|
227
|
+
const pkg = readJsonFile(path.join(getKitsDir(), '..', 'package.json'));
|
|
228
|
+
return typeof pkg?.version === 'string' ? pkg.version : '0.0.0';
|
|
214
229
|
}
|
|
215
230
|
// The deck's entry point is the KIT's -- the deck has no engine code of its own
|
|
216
231
|
// -- so index.html points into the import. Everything the kit's main.jsx imports
|
|
217
232
|
// is relative to itself, so it needs no rewriting to run from there.
|
|
218
|
-
function writeDeckIndexHtml(projectDir, alias, title) {
|
|
219
|
-
fs.writeFileSync(path.join(projectDir,
|
|
233
|
+
function writeDeckIndexHtml(projectDir, alias, title, mainFile) {
|
|
234
|
+
fs.writeFileSync(path.join(projectDir, 'index.html'), `<!doctype html>
|
|
220
235
|
<html>
|
|
221
236
|
<head>
|
|
222
237
|
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />
|
|
@@ -236,31 +251,34 @@ function writeDeckIndexHtml(projectDir, alias, title) {
|
|
|
236
251
|
<div id="root"></div>
|
|
237
252
|
<!-- Engine and editors come from the imported kit; this deck holds its own
|
|
238
253
|
scenes, drawings and behaviors. -->
|
|
239
|
-
<script type="module" src="/${IMPORTS_DIR}/${alias}
|
|
254
|
+
<script type="module" src="/${IMPORTS_DIR}/${alias}/${mainFile}"></script>
|
|
240
255
|
</body>
|
|
241
256
|
</html>
|
|
242
|
-
`,
|
|
257
|
+
`, 'utf8');
|
|
243
258
|
}
|
|
244
259
|
// The deck starts with its own copy of the kit's starter scene, so there is
|
|
245
260
|
// something to open and edit on the first run. Refs inside it are rewritten to
|
|
246
261
|
// point at the kit's copies of what they name (the scene is the deck's now, but
|
|
247
262
|
// the blueprint and art it places still live in the import).
|
|
248
|
-
|
|
249
|
-
|
|
263
|
+
// Give a deck the scene its kit says to start from, with every path that names a
|
|
264
|
+
// kit file rewritten to reach it through the import. Exported because both
|
|
265
|
+
// scaffold paths need it: `init --kit`, and adopting a kit via `add-import`.
|
|
266
|
+
export function writeStarterScene(projectDir, kitDir, alias) {
|
|
267
|
+
const scene = readJsonFile(path.join(kitDir, deckStarterScene(kitDir)));
|
|
250
268
|
const prefix = `${IMPORTS_DIR}/${alias}/`;
|
|
251
269
|
const rewrite = (value) => {
|
|
252
|
-
if (typeof value ===
|
|
270
|
+
if (typeof value === 'string') {
|
|
253
271
|
return fs.existsSync(path.join(kitDir, value)) ? prefix + value : value;
|
|
254
272
|
}
|
|
255
273
|
if (Array.isArray(value))
|
|
256
274
|
return value.map(rewrite);
|
|
257
|
-
if (value && typeof value ===
|
|
275
|
+
if (value && typeof value === 'object') {
|
|
258
276
|
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, rewrite(v)]));
|
|
259
277
|
}
|
|
260
278
|
return value;
|
|
261
279
|
};
|
|
262
|
-
fs.mkdirSync(path.join(projectDir,
|
|
263
|
-
writeJsonFile(path.join(projectDir,
|
|
280
|
+
fs.mkdirSync(path.join(projectDir, 'scenes'), { recursive: true });
|
|
281
|
+
writeJsonFile(path.join(projectDir, 'scenes', 'main.scene'), scene ? rewrite(scene) : { name: 'Main', actors: [] });
|
|
264
282
|
}
|
|
265
283
|
function makeImportedKitClaudeMd(alias) {
|
|
266
284
|
return `# Castle deck
|
|
@@ -291,10 +309,10 @@ something in the kit behaves, define your own behavior of the same name in
|
|
|
291
309
|
function makeKitPin(kitConfig, kit) {
|
|
292
310
|
const deckId = kitConfig.deckId;
|
|
293
311
|
const version = kitConfig.publishedVersion;
|
|
294
|
-
if (typeof deckId ===
|
|
312
|
+
if (typeof deckId === 'string' && typeof version === 'string') {
|
|
295
313
|
return { deckId, version };
|
|
296
314
|
}
|
|
297
|
-
return { source:
|
|
315
|
+
return { source: 'builtin', kit, version: getCliVersion() };
|
|
298
316
|
}
|
|
299
317
|
// The kit's editor block minus `fileTypes` -- the layout and file filters a new
|
|
300
318
|
// deck starts from, without the vocabulary it resolves from the kit itself.
|
|
@@ -330,10 +348,10 @@ function scaffoldFromKitImport(kit, projectDir) {
|
|
|
330
348
|
verbatimSymlinks: true,
|
|
331
349
|
filter: (src) => src === kitDir || !KIT_COPY_EXCLUDE.has(path.basename(src)),
|
|
332
350
|
});
|
|
333
|
-
const kitConfig = readJsonFile(path.join(kitDir,
|
|
334
|
-
const kitPkg = readJsonFile(path.join(kitDir,
|
|
335
|
-
const title = typeof kitConfig.title ===
|
|
336
|
-
writeDeckIndexHtml(projectDir, alias, title);
|
|
351
|
+
const kitConfig = readJsonFile(path.join(kitDir, 'castle.json')) ?? {};
|
|
352
|
+
const kitPkg = readJsonFile(path.join(kitDir, 'package.json')) ?? {};
|
|
353
|
+
const title = typeof kitConfig.title === 'string' ? kitConfig.title : kit;
|
|
354
|
+
writeDeckIndexHtml(projectDir, alias, title, deckMainFile(kitDir));
|
|
337
355
|
writeStarterScene(projectDir, kitDir, alias);
|
|
338
356
|
// The deck's own castle.json: the kit's editor config (panel layout, file
|
|
339
357
|
// filters) as a starting point -- it is the deck's to edit from here -- plus
|
|
@@ -344,7 +362,7 @@ function scaffoldFromKitImport(kit, projectDir) {
|
|
|
344
362
|
// editorConfig.resolveFileTypes) -- so the kit keeps owning it, a kit fix
|
|
345
363
|
// reaches the deck like any other, and a kit imported LATER contributes its
|
|
346
364
|
// types too, which a copy taken at scaffold time never could.
|
|
347
|
-
writeJsonFile(path.join(projectDir,
|
|
365
|
+
writeJsonFile(path.join(projectDir, 'castle.json'), {
|
|
348
366
|
...(kitConfig.editor ? { editor: withoutFileTypes(kitConfig.editor) } : {}),
|
|
349
367
|
imports: { [alias]: makeKitPin(kitConfig, kit) },
|
|
350
368
|
});
|
|
@@ -355,18 +373,16 @@ function scaffoldFromKitImport(kit, projectDir) {
|
|
|
355
373
|
const dependencies = {};
|
|
356
374
|
for (const [name, range] of Object.entries(kitPkg.dependencies ?? {})) {
|
|
357
375
|
dependencies[name] =
|
|
358
|
-
name ===
|
|
359
|
-
? sdkRef
|
|
360
|
-
: String(range);
|
|
376
|
+
name === 'castle-web-sdk' && String(range).startsWith('file:') ? sdkRef : String(range);
|
|
361
377
|
}
|
|
362
|
-
writeJsonFile(path.join(projectDir,
|
|
378
|
+
writeJsonFile(path.join(projectDir, 'package.json'), {
|
|
363
379
|
name: path.basename(projectDir),
|
|
364
380
|
private: true,
|
|
365
|
-
type:
|
|
381
|
+
type: 'module',
|
|
366
382
|
scripts: makeKitDeckScripts(kitDir, alias, cliCommand),
|
|
367
383
|
dependencies,
|
|
368
384
|
});
|
|
369
|
-
fs.writeFileSync(path.join(projectDir,
|
|
385
|
+
fs.writeFileSync(path.join(projectDir, 'CLAUDE.md'), makeImportedKitClaudeMd(alias));
|
|
370
386
|
appendCommonInstructions(projectDir);
|
|
371
387
|
lockImportTree(importDir);
|
|
372
388
|
}
|
|
@@ -377,14 +393,14 @@ export async function init(dir, opts = {}) {
|
|
|
377
393
|
process.exit(1);
|
|
378
394
|
}
|
|
379
395
|
const kit = opts.kit ?? DEFAULT_KIT;
|
|
380
|
-
const bare = kit ===
|
|
396
|
+
const bare = kit === 'none' || kit === 'bare';
|
|
381
397
|
if (bare) {
|
|
382
398
|
scaffoldBare(projectDir);
|
|
383
399
|
}
|
|
384
400
|
else {
|
|
385
401
|
scaffoldFromKitImport(kit, projectDir);
|
|
386
402
|
}
|
|
387
|
-
console.log(`Created project in ${projectDir}/${bare ?
|
|
403
|
+
console.log(`Created project in ${projectDir}/${bare ? '' : ` (from kit "${kit}")`}`);
|
|
388
404
|
// Always install deps so the deck is ready to serve/edit immediately.
|
|
389
405
|
// `--no-serve` only skips the serve step below (callers like the cloud
|
|
390
406
|
// launcher run their own serve, but still want deps in place).
|
|
@@ -394,18 +410,18 @@ export async function init(dir, opts = {}) {
|
|
|
394
410
|
// decks rewrite the sdk to file:../../sdk, which the shipped (published)
|
|
395
411
|
// lockfile won't match -> drop it and let pnpm resolve. Bare decks have no
|
|
396
412
|
// lockfile -> non-frozen.
|
|
397
|
-
const lockPath = path.join(projectDir,
|
|
413
|
+
const lockPath = path.join(projectDir, 'pnpm-lock.yaml');
|
|
398
414
|
if (resolveScaffoldRefs().workspaceMode)
|
|
399
415
|
fs.rmSync(lockPath, { force: true });
|
|
400
416
|
const frozen = fs.existsSync(lockPath);
|
|
401
|
-
console.log(
|
|
417
|
+
console.log('');
|
|
402
418
|
let installed = false;
|
|
403
419
|
try {
|
|
404
|
-
installDeps(projectDir, frozen);
|
|
420
|
+
await installDeps(projectDir, frozen);
|
|
405
421
|
installed = true;
|
|
406
422
|
}
|
|
407
423
|
catch {
|
|
408
|
-
console.error(
|
|
424
|
+
console.error('dependency install failed; re-run `pnpm install` (or `npm install`) in the deck.');
|
|
409
425
|
}
|
|
410
426
|
const autoServe = opts.serve !== false;
|
|
411
427
|
if (autoServe && installed) {
|
|
@@ -415,16 +431,16 @@ export async function init(dir, opts = {}) {
|
|
|
415
431
|
// the served page; users can override host on a subsequent serve call.
|
|
416
432
|
// Open in the user's default browser unless we're clearly headless (SSH
|
|
417
433
|
// session) or the user has opted out via CASTLE_WEB_CLI_NO_OPEN=1.
|
|
418
|
-
const noOpen = process.env.CASTLE_WEB_CLI_NO_OPEN ===
|
|
434
|
+
const noOpen = process.env.CASTLE_WEB_CLI_NO_OPEN === '1' ||
|
|
419
435
|
!!process.env.SSH_CONNECTION ||
|
|
420
436
|
!!process.env.SSH_TTY;
|
|
421
|
-
await serve(projectDir, { host:
|
|
437
|
+
await serve(projectDir, { host: '0.0.0.0', detach: true, open: !noOpen });
|
|
422
438
|
return;
|
|
423
439
|
}
|
|
424
|
-
console.log(
|
|
425
|
-
console.log(
|
|
440
|
+
console.log('');
|
|
441
|
+
console.log('Next steps:');
|
|
426
442
|
console.log(` cd ${dir}`);
|
|
427
443
|
if (!installed)
|
|
428
|
-
console.log(
|
|
429
|
-
console.log(
|
|
444
|
+
console.log(' pnpm install # or: npm install');
|
|
445
|
+
console.log(' castle-web serve . # & in your shell to background it');
|
|
430
446
|
}
|
package/dist/install.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare function installDeps(projectDir: string, frozen: boolean): void
|
|
1
|
+
export declare function installDeps(projectDir: string, frozen: boolean): Promise<void>;
|
|
2
2
|
export declare function install(dir: string): Promise<void>;
|