castle-web-cli 0.4.0 → 0.4.2

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 (158) hide show
  1. package/dist/api.d.ts +53 -5
  2. package/dist/api.js +42 -15
  3. package/dist/config.d.ts +2 -0
  4. package/dist/config.js +25 -11
  5. package/dist/get-deck.d.ts +3 -0
  6. package/dist/get-deck.js +64 -0
  7. package/dist/ide-client.d.ts +1 -0
  8. package/dist/ide-client.js +537 -0
  9. package/dist/ide.d.ts +16 -0
  10. package/dist/ide.js +546 -0
  11. package/dist/index.js +84 -57
  12. package/dist/init.d.ts +3 -1
  13. package/dist/init.js +170 -24
  14. package/dist/localPaths.d.ts +6 -0
  15. package/dist/localPaths.js +33 -0
  16. package/dist/login.js +1 -1
  17. package/dist/preview.d.ts +4 -1
  18. package/dist/preview.js +63 -41
  19. package/dist/save-deck.d.ts +2 -0
  20. package/dist/{push.js → save-deck.js} +66 -5
  21. package/dist/serve.d.ts +2 -0
  22. package/dist/serve.js +293 -22
  23. package/kits/basic-2d/.prettierrc +8 -0
  24. package/kits/basic-2d/CLAUDE.md +131 -0
  25. package/kits/basic-2d/behaviors/Camera.jsx +43 -0
  26. package/kits/basic-2d/behaviors/Collider.jsx +71 -0
  27. package/kits/basic-2d/behaviors/Drawing.jsx +139 -0
  28. package/kits/basic-2d/behaviors/Layout.jsx +16 -0
  29. package/kits/basic-2d/drawings/floor.drawing +70 -0
  30. package/kits/basic-2d/editors/App.jsx +152 -0
  31. package/kits/basic-2d/editors/CodeEditor.jsx +112 -0
  32. package/kits/basic-2d/editors/DrawingEditor.jsx +222 -0
  33. package/kits/basic-2d/editors/FileBrowser.jsx +143 -0
  34. package/kits/basic-2d/editors/PlayOnly.jsx +21 -0
  35. package/kits/basic-2d/editors/SceneEditor.jsx +1012 -0
  36. package/kits/basic-2d/editors/behaviorRegistry.js +24 -0
  37. package/kits/basic-2d/editors/editorHistory.js +52 -0
  38. package/kits/basic-2d/engine/ScenePlayer.jsx +83 -0
  39. package/kits/basic-2d/engine/SceneUI.jsx +67 -0
  40. package/kits/basic-2d/engine/TouchControls.jsx +136 -0
  41. package/kits/basic-2d/engine/autoInspector.jsx +51 -0
  42. package/kits/basic-2d/engine/files.js +62 -0
  43. package/kits/basic-2d/engine/scene.js +420 -0
  44. package/kits/basic-2d/engine/ui.jsx +344 -0
  45. package/kits/basic-2d/engine/ui.module.css +928 -0
  46. package/kits/basic-2d/eslint.config.js +50 -0
  47. package/kits/basic-2d/index.html +11 -0
  48. package/kits/basic-2d/main.jsx +10 -0
  49. package/kits/basic-2d/package-lock.json +2706 -0
  50. package/kits/basic-2d/package.json +41 -0
  51. package/kits/basic-2d/scenes/main.scene +108 -0
  52. package/kits/basic-2d/vite.config.js +1 -0
  53. package/kits/basic-2d-frozen/.prettierrc +8 -0
  54. package/kits/basic-2d-frozen/CLAUDE.md +131 -0
  55. package/kits/basic-2d-frozen/behaviors/Camera.jsx +43 -0
  56. package/kits/basic-2d-frozen/behaviors/Collider.jsx +71 -0
  57. package/kits/basic-2d-frozen/behaviors/Drawing.jsx +139 -0
  58. package/kits/basic-2d-frozen/behaviors/Layout.jsx +16 -0
  59. package/kits/basic-2d-frozen/drawings/floor.drawing +70 -0
  60. package/kits/basic-2d-frozen/editors/App.jsx +152 -0
  61. package/kits/basic-2d-frozen/editors/CodeEditor.jsx +112 -0
  62. package/kits/basic-2d-frozen/editors/DrawingEditor.jsx +222 -0
  63. package/kits/basic-2d-frozen/editors/FileBrowser.jsx +143 -0
  64. package/kits/basic-2d-frozen/editors/PlayOnly.jsx +21 -0
  65. package/kits/basic-2d-frozen/editors/SceneEditor.jsx +1012 -0
  66. package/kits/basic-2d-frozen/editors/behaviorRegistry.js +24 -0
  67. package/kits/basic-2d-frozen/editors/editorHistory.js +52 -0
  68. package/kits/basic-2d-frozen/engine/ScenePlayer.jsx +83 -0
  69. package/kits/basic-2d-frozen/engine/SceneUI.jsx +67 -0
  70. package/kits/basic-2d-frozen/engine/TouchControls.jsx +136 -0
  71. package/kits/basic-2d-frozen/engine/autoInspector.jsx +51 -0
  72. package/kits/basic-2d-frozen/engine/files.js +62 -0
  73. package/kits/basic-2d-frozen/engine/scene.js +420 -0
  74. package/kits/basic-2d-frozen/engine/ui.jsx +344 -0
  75. package/kits/basic-2d-frozen/engine/ui.module.css +928 -0
  76. package/kits/basic-2d-frozen/eslint.config.js +50 -0
  77. package/kits/basic-2d-frozen/index.html +11 -0
  78. package/kits/basic-2d-frozen/main.jsx +10 -0
  79. package/kits/basic-2d-frozen/package-lock.json +2706 -0
  80. package/kits/basic-2d-frozen/package.json +41 -0
  81. package/kits/basic-2d-frozen/scenes/main.scene +108 -0
  82. package/kits/basic-2d-frozen/vite.config.js +1 -0
  83. package/kits/rpg-2d/.prettierrc +8 -0
  84. package/kits/rpg-2d/behaviors/Camera.tsx +52 -0
  85. package/kits/rpg-2d/behaviors/Collider.tsx +98 -0
  86. package/kits/rpg-2d/behaviors/Dialog.tsx +184 -0
  87. package/kits/rpg-2d/behaviors/Drawing.tsx +161 -0
  88. package/kits/rpg-2d/behaviors/Friend.tsx +45 -0
  89. package/kits/rpg-2d/behaviors/Layout.tsx +29 -0
  90. package/kits/rpg-2d/behaviors/PlayerController.tsx +255 -0
  91. package/kits/rpg-2d/behaviors/Portal.tsx +60 -0
  92. package/kits/rpg-2d/behaviors/QuestLog.tsx +90 -0
  93. package/kits/rpg-2d/behaviors/SaveMenu.tsx +123 -0
  94. package/kits/rpg-2d/behaviors/Tilemap.tsx +90 -0
  95. package/kits/rpg-2d/drawings/bld-home.drawing +8136 -0
  96. package/kits/rpg-2d/drawings/env-crate.drawing +509 -0
  97. package/kits/rpg-2d/drawings/env-fence.drawing +536 -0
  98. package/kits/rpg-2d/drawings/env-flower-bed.drawing +607 -0
  99. package/kits/rpg-2d/drawings/env-fountain.drawing +2622 -0
  100. package/kits/rpg-2d/drawings/env-hedge.drawing +601 -0
  101. package/kits/rpg-2d/drawings/env-house-blue.drawing +1 -0
  102. package/kits/rpg-2d/drawings/env-house-green.drawing +1 -0
  103. package/kits/rpg-2d/drawings/env-tree-oak.drawing +1540 -0
  104. package/kits/rpg-2d/drawings/env-tree-pine.drawing +1315 -0
  105. package/kits/rpg-2d/drawings/floor.drawing +70 -0
  106. package/kits/rpg-2d/drawings/fx-sparkle.drawing +926 -0
  107. package/kits/rpg-2d/drawings/npc-juno-idle-down.drawing +1099 -0
  108. package/kits/rpg-2d/drawings/npc-juno-walk-down.drawing +4177 -0
  109. package/kits/rpg-2d/drawings/npc-opal-idle-down.drawing +1099 -0
  110. package/kits/rpg-2d/drawings/npc-opal-walk-down.drawing +4177 -0
  111. package/kits/rpg-2d/drawings/player-idle-down.drawing +1070 -0
  112. package/kits/rpg-2d/drawings/player-idle-left.drawing +1070 -0
  113. package/kits/rpg-2d/drawings/player-idle-right.drawing +1070 -0
  114. package/kits/rpg-2d/drawings/player-idle-up.drawing +1070 -0
  115. package/kits/rpg-2d/drawings/player-walk-down.drawing +4148 -0
  116. package/kits/rpg-2d/drawings/player-walk-left.drawing +4148 -0
  117. package/kits/rpg-2d/drawings/player-walk-right.drawing +4148 -0
  118. package/kits/rpg-2d/drawings/player-walk-up.drawing +4148 -0
  119. package/kits/rpg-2d/editors/App.tsx +163 -0
  120. package/kits/rpg-2d/editors/CodeEditor.tsx +120 -0
  121. package/kits/rpg-2d/editors/DrawingEditor.tsx +278 -0
  122. package/kits/rpg-2d/editors/FileBrowser.tsx +191 -0
  123. package/kits/rpg-2d/editors/PlayOnly.tsx +26 -0
  124. package/kits/rpg-2d/editors/SceneEditor.tsx +1093 -0
  125. package/kits/rpg-2d/editors/behaviorRegistry.ts +33 -0
  126. package/kits/rpg-2d/editors/editorHistory.ts +75 -0
  127. package/kits/rpg-2d/editors/editorProps.ts +10 -0
  128. package/kits/rpg-2d/engine/ScenePlayer.tsx +130 -0
  129. package/kits/rpg-2d/engine/SceneUI.tsx +74 -0
  130. package/kits/rpg-2d/engine/TouchControls.tsx +157 -0
  131. package/kits/rpg-2d/engine/autoInspector.tsx +111 -0
  132. package/kits/rpg-2d/engine/drawing.ts +81 -0
  133. package/kits/rpg-2d/engine/files.ts +215 -0
  134. package/kits/rpg-2d/engine/scene.ts +484 -0
  135. package/kits/rpg-2d/engine/ui.module.css +928 -0
  136. package/kits/rpg-2d/engine/ui.tsx +483 -0
  137. package/kits/rpg-2d/eslint.config.js +46 -0
  138. package/kits/rpg-2d/index.html +11 -0
  139. package/kits/rpg-2d/main.tsx +14 -0
  140. package/kits/rpg-2d/package-lock.json +3149 -0
  141. package/kits/rpg-2d/package.json +46 -0
  142. package/kits/rpg-2d/scenes/main.scene +203 -0
  143. package/kits/rpg-2d/tsconfig.json +17 -0
  144. package/kits/rpg-2d/vite-env.d.ts +7 -0
  145. package/kits/rpg-2d/vite.config.js +1 -0
  146. package/package.json +27 -5
  147. package/AGENTS.md +0 -24
  148. package/dist/push.d.ts +0 -1
  149. package/src/api.ts +0 -160
  150. package/src/bundle.ts +0 -28
  151. package/src/config.ts +0 -36
  152. package/src/index.ts +0 -110
  153. package/src/init.ts +0 -71
  154. package/src/login.ts +0 -24
  155. package/src/preview.ts +0 -93
  156. package/src/push.ts +0 -118
  157. package/src/serve.ts +0 -128
  158. package/tsconfig.json +0 -13
package/dist/index.js CHANGED
@@ -1,53 +1,101 @@
1
1
  #!/usr/bin/env node
2
2
  import * as fs from 'fs';
3
+ import * as path from 'path';
3
4
  import { login } from './login.js';
4
5
  import { serve } from './serve.js';
5
- import { push } from './push.js';
6
+ import { saveDeck } from './save-deck.js';
7
+ import { getDeck } from './get-deck.js';
6
8
  import { init } from './init.js';
7
- import { savePreviewImage, savePreviewIfNeeded } from './preview.js';
9
+ import { connectWS, savePreviewImage, savePreviewIfNeeded, takeScreenshot } from './preview.js';
8
10
  const args = process.argv.slice(2);
9
11
  const command = args[0];
12
+ const FLAGS_WITH_VALUES = new Set(['--port', '--out', '--host', '--kit', '--deck-id']);
13
+ function findPositionalDir() {
14
+ for (let i = 1; i < args.length; i++) {
15
+ if (args[i].startsWith('--')) {
16
+ if (FLAGS_WITH_VALUES.has(args[i]))
17
+ i++;
18
+ continue;
19
+ }
20
+ return args[i];
21
+ }
22
+ return '.';
23
+ }
24
+ function getFlagValue(flag) {
25
+ const idx = args.indexOf(flag);
26
+ return idx >= 0 ? args[idx + 1] : undefined;
27
+ }
28
+ function readServeWsPort(dir) {
29
+ try {
30
+ const serveJson = JSON.parse(fs.readFileSync(path.join(path.resolve(dir), '.castle', 'serve.json'), 'utf-8'));
31
+ return serveJson.wsPort;
32
+ }
33
+ catch {
34
+ return undefined;
35
+ }
36
+ }
37
+ function getWsPort(dir) {
38
+ const explicit = getFlagValue('--port');
39
+ if (explicit)
40
+ return parseInt(explicit, 10);
41
+ const fromServe = readServeWsPort(dir);
42
+ if (fromServe)
43
+ return fromServe;
44
+ return 3738;
45
+ }
10
46
  function usage() {
11
47
  console.log(`Usage:
12
- castle-web init <dir>
13
- castle-web serve [dir] [--port PORT] [--open]
48
+ castle-web init <dir> [--kit NAME] (kits: basic-2d (default), rpg-2d, none)
49
+ castle-web serve [dir] [--port PORT] [--host HOST] [--open] [--detach]
14
50
  castle-web restart [--port PORT]
15
51
  castle-web screenshot [--out FILE] [--port PORT]
16
- castle-web save-preview-image [dir] [--port PORT]
17
- castle-web push [dir]
52
+ castle-web save-preview-image [dir] [--port PORT] [--no-restart]
53
+ castle-web save-deck [dir]
54
+ castle-web get-deck <dir> [--deck-id ID]
18
55
  castle-web login`);
19
56
  process.exit(1);
20
57
  }
21
58
  async function main() {
22
59
  switch (command) {
23
60
  case 'init': {
24
- const dir = args[1];
25
- if (!dir) {
26
- console.error('Usage: castle-web init <dir>');
61
+ const dir = findPositionalDir();
62
+ if (dir === '.') {
63
+ console.error('Usage: castle-web init <dir> [--kit NAME]');
27
64
  process.exit(1);
28
65
  }
29
- init(dir);
66
+ const kit = getFlagValue('--kit');
67
+ init(dir, { kit });
30
68
  break;
31
69
  }
32
70
  case 'serve': {
33
- const dir = args.find((a, i) => i > 0 && !a.startsWith('--')) ?? '.';
34
- const portIdx = args.indexOf('--port');
35
- const port = portIdx >= 0 ? args[portIdx + 1] : undefined;
71
+ const dir = findPositionalDir();
72
+ const port = getFlagValue('--port');
73
+ const host = getFlagValue('--host');
36
74
  const open = args.includes('--open');
37
- await serve(dir, { port, open });
75
+ const detach = args.includes('--detach');
76
+ await serve(dir, { port, host, open, detach });
38
77
  break;
39
78
  }
40
- case 'push': {
41
- const dir = args.find((a, i) => i > 0 && !a.startsWith('--')) ?? '.';
42
- const portIdx = args.indexOf('--port');
43
- const wsPort = parseInt(portIdx >= 0 ? args[portIdx + 1] : '3738', 10);
44
- await push(dir);
79
+ case 'save-deck': {
80
+ const dir = findPositionalDir();
81
+ await saveDeck(dir);
82
+ const wsPort = getWsPort(dir);
45
83
  await savePreviewIfNeeded(dir, wsPort);
46
84
  break;
47
85
  }
86
+ case 'get-deck': {
87
+ const dir = findPositionalDir();
88
+ if (dir === '.') {
89
+ console.error('Usage: castle-web get-deck <dir> [--deck-id ID]');
90
+ process.exit(1);
91
+ }
92
+ const deckId = getFlagValue('--deck-id');
93
+ await getDeck(dir, { deckId });
94
+ break;
95
+ }
48
96
  case 'restart': {
49
- const portIdx = args.indexOf('--port');
50
- const wsPort = parseInt(portIdx >= 0 ? args[portIdx + 1] : '3738', 10);
97
+ const dir = findPositionalDir();
98
+ const wsPort = getWsPort(dir);
51
99
  const { default: WS } = await import('ws');
52
100
  const ws = new WS(`ws://localhost:${wsPort}`);
53
101
  ws.on('open', () => {
@@ -58,43 +106,22 @@ async function main() {
58
106
  break;
59
107
  }
60
108
  case 'screenshot': {
61
- const outIdx = args.indexOf('--out');
62
- const outFile = outIdx >= 0 ? args[outIdx + 1] : 'screenshot.png';
63
- const portIdx = args.indexOf('--port');
64
- const wsPort = parseInt(portIdx >= 0 ? args[portIdx + 1] : '3738', 10);
65
- const requestId = Math.random().toString(36).slice(2);
66
- const { default: WS } = await import('ws');
67
- const ws = new WS(`ws://localhost:${wsPort}`);
68
- ws.on('open', () => {
69
- ws.send(JSON.stringify({ type: 'screenshot_request', requestId }));
70
- });
71
- const timeout = setTimeout(() => {
72
- console.error('Screenshot timed out. Is castle-web serve running?');
73
- ws.close();
74
- process.exit(1);
75
- }, 3000);
76
- ws.on('message', (raw) => {
77
- try {
78
- const msg = JSON.parse(raw.toString());
79
- if (msg.type === 'screenshot_response' && msg.requestId === requestId) {
80
- clearTimeout(timeout);
81
- const base64 = msg.data.replace(/^data:image\/png;base64,/, '');
82
- fs.writeFileSync(outFile, Buffer.from(base64, 'base64'));
83
- console.log(`Saved ${outFile}`);
84
- ws.close();
85
- process.exit(0);
86
- }
87
- }
88
- catch { }
89
- });
90
- break;
109
+ const dir = findPositionalDir();
110
+ const outFile = getFlagValue('--out') ?? 'screenshot.png';
111
+ const wsPort = getWsPort(dir);
112
+ const ws = await connectWS(wsPort);
113
+ const base64 = await takeScreenshot(ws);
114
+ ws.close();
115
+ fs.writeFileSync(outFile, Buffer.from(base64, 'base64'));
116
+ console.log(`Saved ${outFile}`);
117
+ return;
91
118
  }
92
119
  case 'save-preview-image': {
93
- const dir = args.find((a, i) => i > 0 && !a.startsWith('--')) ?? '.';
94
- const portIdx = args.indexOf('--port');
95
- const wsPort = parseInt(portIdx >= 0 ? args[portIdx + 1] : '3738', 10);
96
- await savePreviewImage(dir, wsPort);
97
- process.exit(0);
120
+ const dir = findPositionalDir();
121
+ const wsPort = getWsPort(dir);
122
+ const noRestart = args.includes('--no-restart');
123
+ await savePreviewImage(dir, wsPort, noRestart);
124
+ return;
98
125
  }
99
126
  case 'login':
100
127
  await login();
@@ -104,6 +131,6 @@ async function main() {
104
131
  }
105
132
  }
106
133
  main().catch((e) => {
107
- console.error(e.message ?? e);
134
+ console.error(e instanceof Error ? e.message : String(e));
108
135
  process.exit(1);
109
136
  });
package/dist/init.d.ts CHANGED
@@ -1 +1,3 @@
1
- export declare function init(dir: string): Promise<void>;
1
+ export declare function init(dir: string, opts?: {
2
+ kit?: string;
3
+ }): void;
package/dist/init.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import * as fs from 'fs';
2
2
  import * as path from 'path';
3
+ import { getCliEntryPath, getKitsDir, getRepoRoot, getSdkPackagePath, toPosixPath } from './localPaths.js';
3
4
  const INDEX_HTML = `<!DOCTYPE html>
4
5
  <html>
5
6
  <head>
@@ -25,38 +26,183 @@ el.style.cssText = \`
25
26
  el.textContent = 'Hello Castle!';
26
27
  card.appendChild(el);
27
28
  `;
28
- const CLAUDE_MD = `@node_modules/castle-web-cli/AGENTS.md
29
- @node_modules/castle-web-sdk/AGENTS.md
30
- `;
31
- export async function init(dir) {
32
- const projectDir = path.resolve(dir);
33
- if (fs.existsSync(projectDir) && fs.readdirSync(projectDir).length > 0) {
34
- console.error(`Directory "${dir}" is not empty.`);
35
- process.exit(1);
29
+ // Default kit copied by `init` when no --kit is given. `none`/`bare` skip the
30
+ // kit and produce the minimal index.html + game.js stub above.
31
+ const DEFAULT_KIT = 'basic-2d';
32
+ // Registry version of castle-web-sdk to inject when scaffolding from a
33
+ // globally-installed castle-web (not from inside the workspace). Bumped
34
+ // alongside cli/sdk version bumps.
35
+ const PUBLISHED_SDK_VERSION = '0.4.2';
36
+ // Never copied into a fresh deck: build/dependency junk, and castle.json (a
37
+ // fresh deck has no deckId until its first save-deck).
38
+ const KIT_COPY_EXCLUDE = new Set(['node_modules', '.castle', 'dist', '.git', 'castle.json']);
39
+ function relativeFromProject(projectDir, target) {
40
+ return toPosixPath(path.relative(projectDir, target));
41
+ }
42
+ function makeClaudeMd() {
43
+ // Inline the upstream CLAUDE.md content. An @-import works inside the
44
+ // castle-experimental-web checkout, but breaks when the scaffold lives
45
+ // outside the repo (the relative path no longer resolves).
46
+ const repoRoot = getRepoRoot();
47
+ const upstream = path.join(repoRoot, 'CLAUDE.md');
48
+ try {
49
+ return fs.readFileSync(upstream, 'utf8').trimEnd() + '\n';
36
50
  }
37
- fs.mkdirSync(projectDir, { recursive: true });
38
- fs.writeFileSync(path.join(projectDir, 'index.html'), INDEX_HTML);
39
- fs.writeFileSync(path.join(projectDir, 'game.js'), GAME_JS);
40
- fs.writeFileSync(path.join(projectDir, 'CLAUDE.md'), CLAUDE_MD);
41
- const packageJson = {
51
+ catch {
52
+ return `# Castle Experimental Web\n\nSee https://github.com/castle-xyz/castle-experimental-web for the agent guide.\n`;
53
+ }
54
+ }
55
+ function tryMakeAgentsSymlink(agentsPath) {
56
+ try {
57
+ fs.symlinkSync('CLAUDE.md', agentsPath);
58
+ }
59
+ catch {
60
+ // symlink already exists / unsupported FS — non-fatal
61
+ }
62
+ }
63
+ function makePackageJson(projectDir) {
64
+ const cliEntry = relativeFromProject(projectDir, getCliEntryPath());
65
+ const sdkPackage = relativeFromProject(projectDir, getSdkPackagePath());
66
+ return {
42
67
  name: path.basename(projectDir),
43
68
  private: true,
44
69
  type: 'module',
45
- dependencies: {
46
- 'castle-web-sdk': '*',
70
+ scripts: {
71
+ serve: `node ${cliEntry} serve . --open`,
72
+ restart: `node ${cliEntry} restart .`,
73
+ screenshot: `node ${cliEntry} screenshot .`,
74
+ 'save-deck': `node ${cliEntry} save-deck .`,
47
75
  },
48
- devDependencies: {
49
- 'castle-web-cli': '*',
76
+ dependencies: {
77
+ 'castle-web-sdk': `file:${sdkPackage}`,
50
78
  },
51
79
  };
52
- fs.writeFileSync(path.join(projectDir, 'package.json'), JSON.stringify(packageJson, null, 2) + '\n');
53
- const { execSync } = await import('child_process');
54
- execSync('npm install', { cwd: projectDir, stdio: 'inherit' });
55
- console.log(`Created project in ${projectDir}/`);
80
+ }
81
+ // Some coding agents read AGENTS.md by convention. Symlink so they get the
82
+ // same guidance without a duplicate copy.
83
+ function ensureAgentsSymlink(projectDir) {
84
+ const agentsPath = path.join(projectDir, 'AGENTS.md');
85
+ if (fs.lstatSync(agentsPath, { throwIfNoEntry: false }))
86
+ return;
87
+ // Don't create a dangling link — only symlink when CLAUDE.md is present.
88
+ if (!fs.existsSync(path.join(projectDir, 'CLAUDE.md')))
89
+ return;
90
+ tryMakeAgentsSymlink(agentsPath);
91
+ }
92
+ // Bare scaffold: a plain code-only deck with no kit framework.
93
+ function scaffoldBare(projectDir) {
94
+ fs.mkdirSync(projectDir, { recursive: true });
95
+ fs.writeFileSync(path.join(projectDir, 'index.html'), INDEX_HTML);
96
+ fs.writeFileSync(path.join(projectDir, 'game.js'), GAME_JS);
97
+ fs.writeFileSync(path.join(projectDir, 'CLAUDE.md'), makeClaudeMd());
98
+ ensureAgentsSymlink(projectDir);
99
+ fs.writeFileSync(path.join(projectDir, 'package.json'), JSON.stringify(makePackageJson(projectDir), null, 2) + '\n');
100
+ }
101
+ // Copy a framework kit from kits/<kit>/ into the new deck dir, dropping
102
+ // build/dependency junk and castle.json.
103
+ function scaffoldFromKit(kit, projectDir) {
104
+ const kitDir = path.join(getKitsDir(), kit);
105
+ if (!fs.existsSync(kitDir) || !fs.statSync(kitDir).isDirectory()) {
106
+ console.error(`Kit "${kit}" not found at ${kitDir}.`);
107
+ console.error('Available kits:');
108
+ try {
109
+ const kits = fs
110
+ .readdirSync(getKitsDir())
111
+ .filter((name) => fs.statSync(path.join(getKitsDir(), name)).isDirectory());
112
+ if (kits.length)
113
+ for (const name of kits)
114
+ console.error(` ${name}`);
115
+ else
116
+ console.error(' (none)');
117
+ }
118
+ catch {
119
+ console.error(' (none — kits/ directory is missing)');
120
+ }
121
+ console.error('Or use `--kit none` for a bare code-only deck.');
122
+ process.exit(1);
123
+ }
124
+ fs.cpSync(kitDir, projectDir, {
125
+ recursive: true,
126
+ // Keep symlinks verbatim so the kit's AGENTS.md -> CLAUDE.md stays a link.
127
+ verbatimSymlinks: true,
128
+ filter: (src) => src === kitDir || !KIT_COPY_EXCLUDE.has(path.basename(src)),
129
+ });
130
+ // The kit's package.json carries the kit's name; rename it to the deck dir.
131
+ // Kit-relative refs to `../../sdk` and `../../cli/dist` only resolve when the
132
+ // deck lives at castle-experimental-web/decks/<name>/. Rewrite both to
133
+ // absolute paths so the scaffolded deck works anywhere -- including under
134
+ // /tmp where macOS's /tmp -> /private/tmp symlink breaks relative-path math.
135
+ const pkgPath = path.join(projectDir, 'package.json');
136
+ if (fs.existsSync(pkgPath)) {
137
+ try {
138
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
139
+ pkg.name = path.basename(projectDir);
140
+ // Local-dev paths (`file:../../sdk` / `node ../../cli/dist/index.js`) only
141
+ // work when the deck lives inside the castle-experimental-web workspace.
142
+ // For a deck scaffolded from a globally-installed castle-web, rewrite to
143
+ // the published packages instead.
144
+ const sdkPath = getSdkPackagePath();
145
+ const cliEntry = getCliEntryPath();
146
+ const sdkIsLocal = fs.existsSync(sdkPath);
147
+ const cliIsLocal = fs.existsSync(cliEntry);
148
+ const sdkRef = sdkIsLocal ? `file:${toPosixPath(sdkPath)}` : `^${PUBLISHED_SDK_VERSION}`;
149
+ const cliDistAbs = cliIsLocal ? toPosixPath(path.dirname(cliEntry)) : null;
150
+ if (pkg.dependencies &&
151
+ typeof pkg.dependencies['castle-web-sdk'] === 'string' &&
152
+ pkg.dependencies['castle-web-sdk'].startsWith('file:')) {
153
+ pkg.dependencies['castle-web-sdk'] = sdkRef;
154
+ }
155
+ if (pkg.scripts) {
156
+ for (const k of Object.keys(pkg.scripts)) {
157
+ if (typeof pkg.scripts[k] !== 'string')
158
+ continue;
159
+ if (cliDistAbs) {
160
+ pkg.scripts[k] = pkg.scripts[k]
161
+ .replace(/\.\.\/\.\.\/cli\/dist/g, cliDistAbs)
162
+ .replace(/\.\.\/\.\.\/sdk/g, sdkIsLocal ? toPosixPath(sdkPath) : '');
163
+ }
164
+ else {
165
+ // Globally-installed: route through the `castle-web` binary on PATH.
166
+ pkg.scripts[k] = pkg.scripts[k]
167
+ .replace(/node\s+\.\.\/\.\.\/cli\/dist\/index\.js/g, 'castle-web')
168
+ .replace(/await import\((['"])\.\.\/\.\.\/cli\/dist\/bundle\.js\1\)/g, "await import('castle-web-cli/dist/bundle.js')")
169
+ .replace(/\.\.\/\.\.\/sdk/g, '');
170
+ }
171
+ }
172
+ }
173
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
174
+ }
175
+ catch {
176
+ // kit shipped an unparseable package.json — leave it for the user to fix
177
+ }
178
+ }
179
+ // Every deck needs a CLAUDE.md so coding agents know how castle-web works.
180
+ // Keep the kit's own if it ships one; otherwise generate from the upstream.
181
+ const claudePath = path.join(projectDir, 'CLAUDE.md');
182
+ if (!fs.existsSync(claudePath)) {
183
+ fs.writeFileSync(claudePath, makeClaudeMd());
184
+ }
185
+ ensureAgentsSymlink(projectDir);
186
+ }
187
+ export function init(dir, opts = {}) {
188
+ const projectDir = path.resolve(dir);
189
+ if (fs.existsSync(projectDir) && fs.readdirSync(projectDir).length > 0) {
190
+ console.error(`Directory "${dir}" is not empty.`);
191
+ process.exit(1);
192
+ }
193
+ const kit = opts.kit ?? DEFAULT_KIT;
194
+ const bare = kit === 'none' || kit === 'bare';
195
+ if (bare) {
196
+ scaffoldBare(projectDir);
197
+ }
198
+ else {
199
+ scaffoldFromKit(kit, projectDir);
200
+ }
201
+ console.log(`Created project in ${projectDir}/${bare ? '' : ` (from kit "${kit}")`}`);
56
202
  console.log('');
57
203
  console.log('Next steps:');
58
204
  console.log(` cd ${dir}`);
59
- console.log(' npx castle-web-cli serve --open');
60
- console.log(' npx castle-web-cli login');
61
- console.log(' npx castle-web-cli push');
205
+ console.log(' npm install');
206
+ console.log(' npm run serve');
207
+ console.log(' npm run save-deck');
62
208
  }
@@ -0,0 +1,6 @@
1
+ export declare function getPackageRoot(): string;
2
+ export declare function getRepoRoot(): string;
3
+ export declare function getCliEntryPath(): string;
4
+ export declare function getSdkPackagePath(): string;
5
+ export declare function getKitsDir(): string;
6
+ export declare function toPosixPath(filepath: string): string;
@@ -0,0 +1,33 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ // Root of the castle-web-cli package itself (one up from dist/), regardless of
5
+ // whether we're running from a workspace checkout or a globally-installed npm
6
+ // package.
7
+ export function getPackageRoot() {
8
+ return path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
9
+ }
10
+ // Kept for backward compat: the "repo root" only makes sense in workspace
11
+ // dev mode. Two levels up from dist/, i.e. one above the cli package.
12
+ export function getRepoRoot() {
13
+ return path.resolve(getPackageRoot(), '..');
14
+ }
15
+ export function getCliEntryPath() {
16
+ return path.join(getPackageRoot(), 'dist', 'index.js');
17
+ }
18
+ // Workspace-only: kits live alongside cli/ in the repo. When castle-web is
19
+ // installed globally we bundle kits inside the package itself (see below).
20
+ export function getSdkPackagePath() {
21
+ return path.join(getRepoRoot(), 'sdk');
22
+ }
23
+ export function getKitsDir() {
24
+ // Prefer bundled kits (shipped inside the published cli package);
25
+ // fall back to the workspace `kits/` alongside cli/.
26
+ const bundled = path.join(getPackageRoot(), 'kits');
27
+ if (fs.existsSync(bundled))
28
+ return bundled;
29
+ return path.join(getRepoRoot(), 'kits');
30
+ }
31
+ export function toPosixPath(filepath) {
32
+ return filepath.split(path.sep).join('/');
33
+ }
package/dist/login.js CHANGED
@@ -11,7 +11,7 @@ export async function login() {
11
11
  await new Promise((r) => setTimeout(r, 1000));
12
12
  const user = await api.pollForCLILogin(pollToken);
13
13
  if (user) {
14
- config.setToken(user.token);
14
+ config.setAuth(user.token, user.userId);
15
15
  console.log(`Logged in as @${user.username}`);
16
16
  return;
17
17
  }
package/dist/preview.d.ts CHANGED
@@ -1,2 +1,5 @@
1
- export declare function savePreviewImage(dir: string, wsPort: number): Promise<void>;
1
+ import type { WebSocket as WSClient } from 'ws';
2
+ export declare function connectWS(wsPort: number): Promise<WSClient>;
3
+ export declare function takeScreenshot(ws: WSClient): Promise<string>;
4
+ export declare function savePreviewImage(dir: string, wsPort: number, noRestart?: boolean): Promise<void>;
2
5
  export declare function savePreviewIfNeeded(dir: string, wsPort: number): Promise<void>;
package/dist/preview.js CHANGED
@@ -2,54 +2,62 @@ import * as fs from 'fs';
2
2
  import * as path from 'path';
3
3
  import * as api from './api.js';
4
4
  import * as config from './config.js';
5
- function connectWS(wsPort) {
6
- return new Promise(async (resolve, reject) => {
7
- const { default: WS } = await import('ws');
5
+ export async function connectWS(wsPort) {
6
+ const { default: WS } = await import('ws');
7
+ return new Promise((resolve, reject) => {
8
8
  const ws = new WS(`ws://localhost:${wsPort}`);
9
9
  ws.on('error', () => reject(new Error('Could not connect. Is castle-web serve running?')));
10
10
  ws.on('open', () => resolve(ws));
11
11
  });
12
12
  }
13
- function takeScreenshot(ws) {
13
+ export function takeScreenshot(ws) {
14
14
  return new Promise((resolve, reject) => {
15
15
  const requestId = Math.random().toString(36).slice(2);
16
+ const cleanup = () => {
17
+ clearTimeout(timeout);
18
+ ws.off('message', onMessage);
19
+ };
20
+ const fail = (error) => {
21
+ cleanup();
22
+ reject(error);
23
+ };
16
24
  const timeout = setTimeout(() => {
17
- reject(new Error('Screenshot timed out.'));
18
- }, 5000);
19
- ws.on('message', (raw) => {
25
+ fail(new Error('Screenshot timed out. The deck must be open in a browser tab to capture a screenshot. Start `castle-web serve --open` or open the served URL in a browser.'));
26
+ }, 10000);
27
+ const onMessage = (raw) => {
20
28
  try {
21
29
  const msg = JSON.parse(raw.toString());
22
- if (msg.type === 'screenshot_response' && msg.requestId === requestId) {
23
- clearTimeout(timeout);
30
+ if (msg.type !== 'screenshot_response' || msg.requestId !== requestId)
31
+ return;
32
+ if (typeof msg.data === 'string') {
33
+ cleanup();
24
34
  resolve(msg.data.replace(/^data:image\/png;base64,/, ''));
25
35
  }
36
+ else if (msg.ok === false) {
37
+ fail(new Error(msg.error || 'Could not capture screenshot.'));
38
+ }
39
+ }
40
+ catch {
41
+ // ignore malformed frames
26
42
  }
27
- catch { }
28
- });
43
+ };
44
+ ws.on('message', onMessage);
29
45
  ws.send(JSON.stringify({ type: 'screenshot_request', requestId }));
30
46
  });
31
47
  }
32
48
  function waitForRestart(ws) {
33
49
  return new Promise((resolve) => {
34
- // restart, wait a moment for the deck to render its first frame
35
50
  ws.send(JSON.stringify({ type: 'restart' }));
36
- setTimeout(resolve, 500);
51
+ setTimeout(resolve, 2000);
37
52
  });
38
53
  }
39
- export async function savePreviewImage(dir, wsPort) {
40
- const projectDir = path.resolve(dir);
54
+ function readCastleJson(projectDir) {
41
55
  const castleJsonPath = path.join(projectDir, 'castle.json');
42
- if (!fs.existsSync(castleJsonPath)) {
43
- console.error('No castle.json found. Push the deck first.');
44
- process.exit(1);
45
- }
46
- const castleJson = JSON.parse(fs.readFileSync(castleJsonPath, 'utf-8'));
47
- if (!config.getToken()) {
48
- console.error('Not logged in. Run `castle-web login` first.');
49
- process.exit(1);
50
- }
51
- const ws = await connectWS(wsPort);
52
- await waitForRestart(ws);
56
+ if (!fs.existsSync(castleJsonPath))
57
+ return null;
58
+ return JSON.parse(fs.readFileSync(castleJsonPath, 'utf-8'));
59
+ }
60
+ async function captureAndUpload(projectDir, ws, castleJson) {
53
61
  const base64 = await takeScreenshot(ws);
54
62
  ws.close();
55
63
  fs.writeFileSync(path.join(projectDir, 'preview.png'), Buffer.from(base64, 'base64'));
@@ -58,27 +66,41 @@ export async function savePreviewImage(dir, wsPort) {
58
66
  await api.updateCardCustomBackgroundImage(castleJson.cardId, file.fileId);
59
67
  console.log('Set deck preview image.');
60
68
  }
69
+ export async function savePreviewImage(dir, wsPort, noRestart = false) {
70
+ const projectDir = path.resolve(dir);
71
+ const castleJson = readCastleJson(projectDir);
72
+ if (!castleJson) {
73
+ console.error('No castle.json found. Save the deck first.');
74
+ process.exit(1);
75
+ }
76
+ if (!config.getToken()) {
77
+ console.error('Not logged in. Run `castle-web login` first.');
78
+ process.exit(1);
79
+ }
80
+ const ws = await connectWS(wsPort);
81
+ if (!noRestart)
82
+ await waitForRestart(ws);
83
+ await captureAndUpload(projectDir, ws, castleJson);
84
+ }
61
85
  export async function savePreviewIfNeeded(dir, wsPort) {
62
86
  const projectDir = path.resolve(dir);
63
- if (fs.existsSync(path.join(projectDir, 'preview.png'))) {
87
+ if (fs.existsSync(path.join(projectDir, 'preview.png')))
64
88
  return;
65
- }
89
+ let ws;
66
90
  try {
67
- const ws = await connectWS(wsPort);
91
+ ws = await connectWS(wsPort);
68
92
  await waitForRestart(ws);
69
- const base64 = await takeScreenshot(ws);
70
- ws.close();
71
- const castleJsonPath = path.join(projectDir, 'castle.json');
72
- if (!fs.existsSync(castleJsonPath))
93
+ const castleJson = readCastleJson(projectDir);
94
+ if (!castleJson) {
95
+ ws.close();
73
96
  return;
74
- const castleJson = JSON.parse(fs.readFileSync(castleJsonPath, 'utf-8'));
75
- fs.writeFileSync(path.join(projectDir, 'preview.png'), Buffer.from(base64, 'base64'));
76
- console.log('Saved preview.png');
77
- const file = await api.uploadBase64(base64, 'preview.png');
78
- await api.updateCardCustomBackgroundImage(castleJson.cardId, file.fileId);
79
- console.log('Set deck preview image.');
97
+ }
98
+ await captureAndUpload(projectDir, ws, castleJson);
80
99
  }
81
- catch {
82
- // serve not running, skip preview
100
+ catch (e) {
101
+ if (ws)
102
+ ws.close();
103
+ const msg = e instanceof Error ? e.message : String(e);
104
+ console.log(`Preview skipped: ${msg}`);
83
105
  }
84
106
  }
@@ -0,0 +1,2 @@
1
+ export declare function archiveSource(projectDir: string): Promise<Buffer>;
2
+ export declare function saveDeck(dir: string): Promise<void>;