castle-web-cli 0.4.115 → 0.4.116

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.
Files changed (55) hide show
  1. package/dist/agent-prompts.js +2 -2
  2. package/dist/agent.d.ts +9 -9
  3. package/dist/agent.js +590 -589
  4. package/dist/castleJson.d.ts +6 -0
  5. package/dist/castleJson.js +10 -0
  6. package/dist/editorConfig.js +27 -3
  7. package/dist/imports.d.ts +4 -0
  8. package/dist/imports.js +59 -10
  9. package/dist/init.d.ts +3 -0
  10. package/dist/init.js +99 -87
  11. package/dist/install.d.ts +1 -1
  12. package/dist/install.js +26 -24
  13. package/dist/shell/assets/{index-CUamb8rK.js → index-DiPlPGyg.js} +2 -2
  14. package/dist/shell/index.html +1 -1
  15. package/dist/vitePlugins.js +1 -1
  16. package/kits/physics-2d/CLAUDE.md +28 -16
  17. package/kits/physics-2d/behaviors/Joints.jsx +1 -1
  18. package/kits/physics-2d/behaviors/Sprite.jsx +17 -12
  19. package/kits/physics-2d/blueprints/ball.scene +1 -1
  20. package/kits/physics-2d/blueprints/block.scene +1 -1
  21. package/kits/physics-2d/blueprints/cauldron.scene +1 -1
  22. package/kits/physics-2d/blueprints/crate.scene +1 -1
  23. package/kits/physics-2d/castle.json +11 -3
  24. package/kits/physics-2d/docs/pxart-format.md +537 -51
  25. package/kits/physics-2d/editors/PxArtEditor.jsx +1794 -65
  26. package/kits/physics-2d/editors/brushFit.js +535 -0
  27. package/kits/physics-2d/editors/brushShapes.js +140 -0
  28. package/kits/physics-2d/editors/mediaFile.js +12 -1
  29. package/kits/physics-2d/editors/pathOverlay.js +340 -0
  30. package/kits/physics-2d/editors/pathTools.js +1906 -0
  31. package/kits/physics-2d/editors/pixelCanvas.js +13 -0
  32. package/kits/physics-2d/editors/pixelEditorChrome.jsx +2 -2
  33. package/kits/physics-2d/editors/pixelGeometry.js +4 -2
  34. package/kits/physics-2d/editors/pixelInspector.jsx +410 -37
  35. package/kits/physics-2d/editors/pxArtEditorModel.js +172 -16
  36. package/kits/physics-2d/editors/pxArtTimeline.jsx +163 -43
  37. package/kits/physics-2d/editors/pxArtTimeline.module.css +31 -5
  38. package/kits/physics-2d/engine/assets.js +1 -1
  39. package/kits/physics-2d/engine/blueprint.js +3 -3
  40. package/kits/physics-2d/engine/files.js +3 -1
  41. package/kits/physics-2d/engine/liveReload.js +1 -1
  42. package/kits/physics-2d/engine/physics/jointArt.js +3 -3
  43. package/kits/physics-2d/engine/pxart.js +153 -35
  44. package/kits/physics-2d/engine/pxartPath.js +1356 -0
  45. package/kits/physics-2d/engine/pxartSmooth.js +276 -125
  46. package/kits/physics-2d/engine/ui.jsx +22 -1
  47. package/kits/physics-2d/engine/ui.module.css +36 -12
  48. package/kits/physics-2d/package-lock.json +1 -1
  49. package/kits/physics-2d/scripts/draw.mjs +7 -7
  50. package/kits/physics-2d/scripts/import-svg.mjs +1231 -0
  51. package/kits/physics-2d/scripts/svg-emission-guide.md +92 -0
  52. package/package.json +1 -1
  53. /package/kits/physics-2d/drawings/{block.pxart → block.sprite} +0 -0
  54. /package/kits/physics-2d/drawings/{cauldron.pxart → cauldron.sprite} +0 -0
  55. /package/kits/physics-2d/drawings/{joint-rope.pxart → joint-rope.sprite} +0 -0
@@ -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;
@@ -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`.
@@ -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
- // Everything else in the block (`initialPanels`, `visiblePaths`/`hiddenPaths`,
14
- // `defaultPlayFile`) stays the deck's own, read from the deck root only.
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' ? editor.defaultPlayFile : undefined,
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 || !['node_modules', '.castle', 'dist', '.git'].includes(path.basename(src)),
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).map((p) => p?.deckId).filter((id) => !!id));
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
- return { deckId, alias, replaced, transitive, dependencies };
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.dependencies.length > 0)
504
- console.log('Run `castle-web install` to install them.');
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 "fs";
2
- import * as path from "path";
3
- import { COMMON_INSTRUCTIONS } from "./commonInstructions.js";
4
- import { installDeps } from "./install.js";
5
- import { IMPORTS_DIR, lockImportTree } from "./imports.js";
6
- import { getCliEntryPath, getKitsDir, getRepoRoot, getSdkPackagePath, toPosixPath, } from "./localPaths.js";
7
- import { serve } from "./serve.js";
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 = "physics-2d";
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 = "0.4.12";
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 = "castle";
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: "files" }, { type: "playtest" }],
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
- ? `file:${sdkPathPosix}`
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, "CLAUDE.md");
83
+ const upstream = path.join(repoRoot, 'CLAUDE.md');
94
84
  try {
95
- return fs.readFileSync(upstream, "utf8").trimEnd() + "\n";
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, "CLAUDE.md");
95
+ const claudePath = path.join(projectDir, 'CLAUDE.md');
106
96
  const existing = fs.existsSync(claudePath)
107
- ? fs.readFileSync(claudePath, "utf8").trimEnd() + "\n\n"
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("CLAUDE.md", agentsPath);
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
- "save-deck": `${cliCommand} save-deck .`,
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, "scripts", "draw.mjs"))) {
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,59 @@ function makePackageJson(projectDir) {
138
128
  return {
139
129
  name: path.basename(projectDir),
140
130
  private: true,
141
- type: "module",
131
+ type: 'module',
142
132
  scripts: {
143
133
  restart: `${cliCommand} restart .`,
144
134
  screenshot: `${cliCommand} screenshot .`,
145
- "save-deck": `${cliCommand} save-deck .`,
135
+ 'save-deck': `${cliCommand} save-deck .`,
146
136
  },
147
137
  dependencies: {
148
- "castle-web-sdk": sdkRef,
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, "AGENTS.md");
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, "CLAUDE.md")))
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
+ export function adoptImportEntry(deckDir, alias, mainFile) {
170
+ const file = path.join(deckDir, 'index.html');
171
+ const html = fs.readFileSync(file, 'utf8');
172
+ fs.writeFileSync(file, html.replace(/<script type="module" src="[^"]*"><\/script>/, `<script type="module" src="/${IMPORTS_DIR}/${alias}/${mainFile}"></script>`), 'utf8');
173
+ fs.rmSync(path.join(deckDir, 'game.js'), { force: true });
174
+ }
164
175
  function scaffoldBare(projectDir) {
165
176
  fs.mkdirSync(projectDir, { recursive: true });
166
- fs.writeFileSync(path.join(projectDir, "index.html"), INDEX_HTML);
167
- fs.writeFileSync(path.join(projectDir, "game.js"), GAME_JS);
168
- fs.writeFileSync(path.join(projectDir, "castle.json"), JSON.stringify(BARE_CASTLE_JSON, null, 2) + "\n");
169
- fs.writeFileSync(path.join(projectDir, "CLAUDE.md"), makeClaudeMd());
177
+ fs.writeFileSync(path.join(projectDir, 'index.html'), INDEX_HTML);
178
+ fs.writeFileSync(path.join(projectDir, 'game.js'), GAME_JS);
179
+ fs.writeFileSync(path.join(projectDir, 'castle.json'), JSON.stringify(BARE_CASTLE_JSON, null, 2) + '\n');
180
+ fs.writeFileSync(path.join(projectDir, 'CLAUDE.md'), makeClaudeMd());
170
181
  appendCommonInstructions(projectDir);
171
182
  ensureAgentsSymlink(projectDir);
172
- fs.writeFileSync(path.join(projectDir, "package.json"), JSON.stringify(makePackageJson(projectDir), null, 2) + "\n");
183
+ fs.writeFileSync(path.join(projectDir, 'package.json'), JSON.stringify(makePackageJson(projectDir), null, 2) + '\n');
173
184
  }
174
185
  // Copy a framework kit from kits/<kit>/ into the new deck dir, dropping
175
186
  // build/dependency junk and castle.json.
@@ -180,7 +191,7 @@ function requireKitDir(kit) {
180
191
  if (fs.existsSync(kitDir) && fs.statSync(kitDir).isDirectory())
181
192
  return kitDir;
182
193
  console.error(`Kit "${kit}" not found at ${kitDir}.`);
183
- console.error("Available kits:");
194
+ console.error('Available kits:');
184
195
  try {
185
196
  const kits = fs
186
197
  .readdirSync(getKitsDir())
@@ -189,34 +200,34 @@ function requireKitDir(kit) {
189
200
  for (const name of kits)
190
201
  console.error(` ${name}`);
191
202
  else
192
- console.error(" (none)");
203
+ console.error(' (none)');
193
204
  }
194
205
  catch {
195
- console.error(" (none — kits/ directory is missing)");
206
+ console.error(' (none — kits/ directory is missing)');
196
207
  }
197
- console.error("Or use `--kit none` for a bare code-only deck.");
208
+ console.error('Or use `--kit none` for a bare code-only deck.');
198
209
  return process.exit(1);
199
210
  }
200
211
  function readJsonFile(file) {
201
212
  try {
202
- return JSON.parse(fs.readFileSync(file, "utf8"));
213
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
203
214
  }
204
215
  catch {
205
216
  return null;
206
217
  }
207
218
  }
208
219
  function writeJsonFile(file, value) {
209
- fs.writeFileSync(file, JSON.stringify(value, null, 2) + "\n", "utf8");
220
+ fs.writeFileSync(file, JSON.stringify(value, null, 2) + '\n', 'utf8');
210
221
  }
211
222
  export function getCliVersion() {
212
- const pkg = readJsonFile(path.join(getKitsDir(), "..", "package.json"));
213
- return typeof pkg?.version === "string" ? pkg.version : "0.0.0";
223
+ const pkg = readJsonFile(path.join(getKitsDir(), '..', 'package.json'));
224
+ return typeof pkg?.version === 'string' ? pkg.version : '0.0.0';
214
225
  }
215
226
  // The deck's entry point is the KIT's -- the deck has no engine code of its own
216
227
  // -- so index.html points into the import. Everything the kit's main.jsx imports
217
228
  // 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, "index.html"), `<!doctype html>
229
+ function writeDeckIndexHtml(projectDir, alias, title, mainFile) {
230
+ fs.writeFileSync(path.join(projectDir, 'index.html'), `<!doctype html>
220
231
  <html>
221
232
  <head>
222
233
  <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />
@@ -236,31 +247,34 @@ function writeDeckIndexHtml(projectDir, alias, title) {
236
247
  <div id="root"></div>
237
248
  <!-- Engine and editors come from the imported kit; this deck holds its own
238
249
  scenes, drawings and behaviors. -->
239
- <script type="module" src="/${IMPORTS_DIR}/${alias}/main.jsx"></script>
250
+ <script type="module" src="/${IMPORTS_DIR}/${alias}/${mainFile}"></script>
240
251
  </body>
241
252
  </html>
242
- `, "utf8");
253
+ `, 'utf8');
243
254
  }
244
255
  // The deck starts with its own copy of the kit's starter scene, so there is
245
256
  // something to open and edit on the first run. Refs inside it are rewritten to
246
257
  // point at the kit's copies of what they name (the scene is the deck's now, but
247
258
  // the blueprint and art it places still live in the import).
248
- function writeStarterScene(projectDir, kitDir, alias) {
249
- const scene = readJsonFile(path.join(kitDir, "scenes", "main.scene"));
259
+ // Give a deck the scene its kit says to start from, with every path that names a
260
+ // kit file rewritten to reach it through the import. Exported because both
261
+ // scaffold paths need it: `init --kit`, and adopting a kit via `add-import`.
262
+ export function writeStarterScene(projectDir, kitDir, alias) {
263
+ const scene = readJsonFile(path.join(kitDir, deckStarterScene(kitDir)));
250
264
  const prefix = `${IMPORTS_DIR}/${alias}/`;
251
265
  const rewrite = (value) => {
252
- if (typeof value === "string") {
266
+ if (typeof value === 'string') {
253
267
  return fs.existsSync(path.join(kitDir, value)) ? prefix + value : value;
254
268
  }
255
269
  if (Array.isArray(value))
256
270
  return value.map(rewrite);
257
- if (value && typeof value === "object") {
271
+ if (value && typeof value === 'object') {
258
272
  return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, rewrite(v)]));
259
273
  }
260
274
  return value;
261
275
  };
262
- fs.mkdirSync(path.join(projectDir, "scenes"), { recursive: true });
263
- writeJsonFile(path.join(projectDir, "scenes", "main.scene"), scene ? rewrite(scene) : { name: "Main", actors: [] });
276
+ fs.mkdirSync(path.join(projectDir, 'scenes'), { recursive: true });
277
+ writeJsonFile(path.join(projectDir, 'scenes', 'main.scene'), scene ? rewrite(scene) : { name: 'Main', actors: [] });
264
278
  }
265
279
  function makeImportedKitClaudeMd(alias) {
266
280
  return `# Castle deck
@@ -291,10 +305,10 @@ something in the kit behaves, define your own behavior of the same name in
291
305
  function makeKitPin(kitConfig, kit) {
292
306
  const deckId = kitConfig.deckId;
293
307
  const version = kitConfig.publishedVersion;
294
- if (typeof deckId === "string" && typeof version === "string") {
308
+ if (typeof deckId === 'string' && typeof version === 'string') {
295
309
  return { deckId, version };
296
310
  }
297
- return { source: "builtin", kit, version: getCliVersion() };
311
+ return { source: 'builtin', kit, version: getCliVersion() };
298
312
  }
299
313
  // The kit's editor block minus `fileTypes` -- the layout and file filters a new
300
314
  // deck starts from, without the vocabulary it resolves from the kit itself.
@@ -330,10 +344,10 @@ function scaffoldFromKitImport(kit, projectDir) {
330
344
  verbatimSymlinks: true,
331
345
  filter: (src) => src === kitDir || !KIT_COPY_EXCLUDE.has(path.basename(src)),
332
346
  });
333
- const kitConfig = readJsonFile(path.join(kitDir, "castle.json")) ?? {};
334
- const kitPkg = readJsonFile(path.join(kitDir, "package.json")) ?? {};
335
- const title = typeof kitConfig.title === "string" ? kitConfig.title : kit;
336
- writeDeckIndexHtml(projectDir, alias, title);
347
+ const kitConfig = readJsonFile(path.join(kitDir, 'castle.json')) ?? {};
348
+ const kitPkg = readJsonFile(path.join(kitDir, 'package.json')) ?? {};
349
+ const title = typeof kitConfig.title === 'string' ? kitConfig.title : kit;
350
+ writeDeckIndexHtml(projectDir, alias, title, deckMainFile(kitDir));
337
351
  writeStarterScene(projectDir, kitDir, alias);
338
352
  // The deck's own castle.json: the kit's editor config (panel layout, file
339
353
  // filters) as a starting point -- it is the deck's to edit from here -- plus
@@ -344,7 +358,7 @@ function scaffoldFromKitImport(kit, projectDir) {
344
358
  // editorConfig.resolveFileTypes) -- so the kit keeps owning it, a kit fix
345
359
  // reaches the deck like any other, and a kit imported LATER contributes its
346
360
  // types too, which a copy taken at scaffold time never could.
347
- writeJsonFile(path.join(projectDir, "castle.json"), {
361
+ writeJsonFile(path.join(projectDir, 'castle.json'), {
348
362
  ...(kitConfig.editor ? { editor: withoutFileTypes(kitConfig.editor) } : {}),
349
363
  imports: { [alias]: makeKitPin(kitConfig, kit) },
350
364
  });
@@ -355,18 +369,16 @@ function scaffoldFromKitImport(kit, projectDir) {
355
369
  const dependencies = {};
356
370
  for (const [name, range] of Object.entries(kitPkg.dependencies ?? {})) {
357
371
  dependencies[name] =
358
- name === "castle-web-sdk" && String(range).startsWith("file:")
359
- ? sdkRef
360
- : String(range);
372
+ name === 'castle-web-sdk' && String(range).startsWith('file:') ? sdkRef : String(range);
361
373
  }
362
- writeJsonFile(path.join(projectDir, "package.json"), {
374
+ writeJsonFile(path.join(projectDir, 'package.json'), {
363
375
  name: path.basename(projectDir),
364
376
  private: true,
365
- type: "module",
377
+ type: 'module',
366
378
  scripts: makeKitDeckScripts(kitDir, alias, cliCommand),
367
379
  dependencies,
368
380
  });
369
- fs.writeFileSync(path.join(projectDir, "CLAUDE.md"), makeImportedKitClaudeMd(alias));
381
+ fs.writeFileSync(path.join(projectDir, 'CLAUDE.md'), makeImportedKitClaudeMd(alias));
370
382
  appendCommonInstructions(projectDir);
371
383
  lockImportTree(importDir);
372
384
  }
@@ -377,14 +389,14 @@ export async function init(dir, opts = {}) {
377
389
  process.exit(1);
378
390
  }
379
391
  const kit = opts.kit ?? DEFAULT_KIT;
380
- const bare = kit === "none" || kit === "bare";
392
+ const bare = kit === 'none' || kit === 'bare';
381
393
  if (bare) {
382
394
  scaffoldBare(projectDir);
383
395
  }
384
396
  else {
385
397
  scaffoldFromKitImport(kit, projectDir);
386
398
  }
387
- console.log(`Created project in ${projectDir}/${bare ? "" : ` (from kit "${kit}")`}`);
399
+ console.log(`Created project in ${projectDir}/${bare ? '' : ` (from kit "${kit}")`}`);
388
400
  // Always install deps so the deck is ready to serve/edit immediately.
389
401
  // `--no-serve` only skips the serve step below (callers like the cloud
390
402
  // launcher run their own serve, but still want deps in place).
@@ -394,18 +406,18 @@ export async function init(dir, opts = {}) {
394
406
  // decks rewrite the sdk to file:../../sdk, which the shipped (published)
395
407
  // lockfile won't match -> drop it and let pnpm resolve. Bare decks have no
396
408
  // lockfile -> non-frozen.
397
- const lockPath = path.join(projectDir, "pnpm-lock.yaml");
409
+ const lockPath = path.join(projectDir, 'pnpm-lock.yaml');
398
410
  if (resolveScaffoldRefs().workspaceMode)
399
411
  fs.rmSync(lockPath, { force: true });
400
412
  const frozen = fs.existsSync(lockPath);
401
- console.log("");
413
+ console.log('');
402
414
  let installed = false;
403
415
  try {
404
- installDeps(projectDir, frozen);
416
+ await installDeps(projectDir, frozen);
405
417
  installed = true;
406
418
  }
407
419
  catch {
408
- console.error("dependency install failed; re-run `pnpm install` (or `npm install`) in the deck.");
420
+ console.error('dependency install failed; re-run `pnpm install` (or `npm install`) in the deck.');
409
421
  }
410
422
  const autoServe = opts.serve !== false;
411
423
  if (autoServe && installed) {
@@ -415,16 +427,16 @@ export async function init(dir, opts = {}) {
415
427
  // the served page; users can override host on a subsequent serve call.
416
428
  // Open in the user's default browser unless we're clearly headless (SSH
417
429
  // session) or the user has opted out via CASTLE_WEB_CLI_NO_OPEN=1.
418
- const noOpen = process.env.CASTLE_WEB_CLI_NO_OPEN === "1" ||
430
+ const noOpen = process.env.CASTLE_WEB_CLI_NO_OPEN === '1' ||
419
431
  !!process.env.SSH_CONNECTION ||
420
432
  !!process.env.SSH_TTY;
421
- await serve(projectDir, { host: "0.0.0.0", detach: true, open: !noOpen });
433
+ await serve(projectDir, { host: '0.0.0.0', detach: true, open: !noOpen });
422
434
  return;
423
435
  }
424
- console.log("");
425
- console.log("Next steps:");
436
+ console.log('');
437
+ console.log('Next steps:');
426
438
  console.log(` cd ${dir}`);
427
439
  if (!installed)
428
- console.log(" pnpm install # or: npm install");
429
- console.log(" castle-web serve . # & in your shell to background it");
440
+ console.log(' pnpm install # or: npm install');
441
+ console.log(' castle-web serve . # & in your shell to background it');
430
442
  }
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>;