castle-web-cli 0.4.1 → 0.4.3

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 +36 -41
  12. package/dist/init.d.ts +3 -1
  13. package/dist/init.js +173 -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 +3 -0
  18. package/dist/preview.js +53 -34
  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 +290 -27
  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 -25
  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 -143
  153. package/src/init.ts +0 -71
  154. package/src/login.ts +0 -24
  155. package/src/preview.ts +0 -94
  156. package/src/push.ts +0 -118
  157. package/src/serve.ts +0 -134
  158. package/tsconfig.json +0 -13
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,186 @@ 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
+ // "Workspace mode" = the cli is running from a castle-experimental-web
146
+ // checkout (sdk/ exists next to cli/). Otherwise we're a globally-
147
+ // installed npm package and need to use the published refs + binary.
148
+ const workspaceMode = fs.existsSync(sdkPath);
149
+ const sdkRef = workspaceMode ? `file:${toPosixPath(sdkPath)}` : `^${PUBLISHED_SDK_VERSION}`;
150
+ const cliDistAbs = workspaceMode
151
+ ? toPosixPath(path.dirname(getCliEntryPath()))
152
+ : null;
153
+ if (pkg.dependencies &&
154
+ typeof pkg.dependencies['castle-web-sdk'] === 'string' &&
155
+ pkg.dependencies['castle-web-sdk'].startsWith('file:')) {
156
+ pkg.dependencies['castle-web-sdk'] = sdkRef;
157
+ }
158
+ if (pkg.scripts) {
159
+ for (const k of Object.keys(pkg.scripts)) {
160
+ if (typeof pkg.scripts[k] !== 'string')
161
+ continue;
162
+ if (cliDistAbs) {
163
+ pkg.scripts[k] = pkg.scripts[k]
164
+ .replace(/\.\.\/\.\.\/cli\/dist/g, cliDistAbs)
165
+ .replace(/\.\.\/\.\.\/sdk/g, sdkIsLocal ? toPosixPath(sdkPath) : '');
166
+ }
167
+ else {
168
+ // Globally-installed: route through the `castle-web` binary on PATH.
169
+ pkg.scripts[k] = pkg.scripts[k]
170
+ .replace(/node\s+\.\.\/\.\.\/cli\/dist\/index\.js/g, 'castle-web')
171
+ .replace(/await import\((['"])\.\.\/\.\.\/cli\/dist\/bundle\.js\1\)/g, "await import('castle-web-cli/dist/bundle.js')")
172
+ .replace(/\.\.\/\.\.\/sdk/g, '');
173
+ }
174
+ }
175
+ }
176
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
177
+ }
178
+ catch {
179
+ // kit shipped an unparseable package.json — leave it for the user to fix
180
+ }
181
+ }
182
+ // Every deck needs a CLAUDE.md so coding agents know how castle-web works.
183
+ // Keep the kit's own if it ships one; otherwise generate from the upstream.
184
+ const claudePath = path.join(projectDir, 'CLAUDE.md');
185
+ if (!fs.existsSync(claudePath)) {
186
+ fs.writeFileSync(claudePath, makeClaudeMd());
187
+ }
188
+ ensureAgentsSymlink(projectDir);
189
+ }
190
+ export function init(dir, opts = {}) {
191
+ const projectDir = path.resolve(dir);
192
+ if (fs.existsSync(projectDir) && fs.readdirSync(projectDir).length > 0) {
193
+ console.error(`Directory "${dir}" is not empty.`);
194
+ process.exit(1);
195
+ }
196
+ const kit = opts.kit ?? DEFAULT_KIT;
197
+ const bare = kit === 'none' || kit === 'bare';
198
+ if (bare) {
199
+ scaffoldBare(projectDir);
200
+ }
201
+ else {
202
+ scaffoldFromKit(kit, projectDir);
203
+ }
204
+ console.log(`Created project in ${projectDir}/${bare ? '' : ` (from kit "${kit}")`}`);
56
205
  console.log('');
57
206
  console.log('Next steps:');
58
207
  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');
208
+ console.log(' npm install');
209
+ console.log(' npm run serve');
210
+ console.log(' npm run save-deck');
62
211
  }
@@ -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
+ 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>;
1
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,30 +2,46 @@ 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.'));
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.'));
18
26
  }, 10000);
19
- ws.on('message', (raw) => {
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
  }
@@ -35,14 +51,28 @@ function waitForRestart(ws) {
35
51
  setTimeout(resolve, 2000);
36
52
  });
37
53
  }
54
+ function readCastleJson(projectDir) {
55
+ const castleJsonPath = path.join(projectDir, 'castle.json');
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) {
61
+ const base64 = await takeScreenshot(ws);
62
+ ws.close();
63
+ fs.writeFileSync(path.join(projectDir, 'preview.png'), Buffer.from(base64, 'base64'));
64
+ console.log('Saved preview.png');
65
+ const file = await api.uploadBase64(base64, 'preview.png');
66
+ await api.updateCardCustomBackgroundImage(castleJson.cardId, file.fileId);
67
+ console.log('Set deck preview image.');
68
+ }
38
69
  export async function savePreviewImage(dir, wsPort, noRestart = false) {
39
70
  const projectDir = path.resolve(dir);
40
- const castleJsonPath = path.join(projectDir, 'castle.json');
41
- if (!fs.existsSync(castleJsonPath)) {
42
- console.error('No castle.json found. Push the deck first.');
71
+ const castleJson = readCastleJson(projectDir);
72
+ if (!castleJson) {
73
+ console.error('No castle.json found. Save the deck first.');
43
74
  process.exit(1);
44
75
  }
45
- const castleJson = JSON.parse(fs.readFileSync(castleJsonPath, 'utf-8'));
46
76
  if (!config.getToken()) {
47
77
  console.error('Not logged in. Run `castle-web login` first.');
48
78
  process.exit(1);
@@ -50,38 +80,27 @@ export async function savePreviewImage(dir, wsPort, noRestart = false) {
50
80
  const ws = await connectWS(wsPort);
51
81
  if (!noRestart)
52
82
  await waitForRestart(ws);
53
- const base64 = await takeScreenshot(ws);
54
- ws.close();
55
- fs.writeFileSync(path.join(projectDir, 'preview.png'), Buffer.from(base64, 'base64'));
56
- console.log('Saved preview.png');
57
- const file = await api.uploadBase64(base64, 'preview.png');
58
- await api.updateCardCustomBackgroundImage(castleJson.cardId, file.fileId);
59
- console.log('Set deck preview image.');
83
+ await captureAndUpload(projectDir, ws, castleJson);
60
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
- }
66
89
  let ws;
67
90
  try {
68
91
  ws = await connectWS(wsPort);
69
92
  await waitForRestart(ws);
70
- const base64 = await takeScreenshot(ws);
71
- ws.close();
72
- const castleJsonPath = path.join(projectDir, 'castle.json');
73
- if (!fs.existsSync(castleJsonPath))
93
+ const castleJson = readCastleJson(projectDir);
94
+ if (!castleJson) {
95
+ ws.close();
74
96
  return;
75
- const castleJson = JSON.parse(fs.readFileSync(castleJsonPath, 'utf-8'));
76
- fs.writeFileSync(path.join(projectDir, 'preview.png'), Buffer.from(base64, 'base64'));
77
- console.log('Saved preview.png');
78
- const file = await api.uploadBase64(base64, 'preview.png');
79
- await api.updateCardCustomBackgroundImage(castleJson.cardId, file.fileId);
80
- console.log('Set deck preview image.');
97
+ }
98
+ await captureAndUpload(projectDir, ws, castleJson);
81
99
  }
82
100
  catch (e) {
83
101
  if (ws)
84
102
  ws.close();
85
- console.log(`Preview skipped: ${e?.message ?? e}`);
103
+ const msg = e instanceof Error ? e.message : String(e);
104
+ console.log(`Preview skipped: ${msg}`);
86
105
  }
87
106
  }
@@ -0,0 +1,2 @@
1
+ export declare function archiveSource(projectDir: string): Promise<Buffer>;
2
+ export declare function saveDeck(dir: string): Promise<void>;
@@ -1,9 +1,67 @@
1
1
  import * as fs from 'fs';
2
+ import * as os from 'os';
2
3
  import * as path from 'path';
4
+ import { spawn } from 'child_process';
3
5
  import { nanoid } from 'nanoid';
4
6
  import * as api from './api.js';
5
7
  import * as config from './config.js';
6
8
  import { bundleProject } from './bundle.js';
9
+ const SOURCE_ARCHIVE_EXCLUDES = ['node_modules', 'dist', '.castle', '.git'];
10
+ export function archiveSource(projectDir) {
11
+ return new Promise((resolve, reject) => {
12
+ const tmpFile = path.join(os.tmpdir(), `castle-source-${nanoid(8)}.tar.gz`);
13
+ const args = ['-czf', tmpFile];
14
+ for (const ex of SOURCE_ARCHIVE_EXCLUDES)
15
+ args.push(`--exclude=./${ex}`);
16
+ args.push('-C', projectDir, '.');
17
+ const child = spawn('tar', args, { stdio: ['ignore', 'ignore', 'pipe'] });
18
+ let stderr = '';
19
+ child.stderr?.on('data', (chunk) => { stderr += chunk.toString(); });
20
+ child.on('error', reject);
21
+ child.on('close', (code) => {
22
+ if (code !== 0) {
23
+ try {
24
+ fs.unlinkSync(tmpFile);
25
+ }
26
+ catch { /* nothing to clean */ }
27
+ reject(new Error(`tar exited with code ${code}: ${stderr}`));
28
+ return;
29
+ }
30
+ try {
31
+ const buf = fs.readFileSync(tmpFile);
32
+ fs.unlinkSync(tmpFile);
33
+ resolve(buf);
34
+ }
35
+ catch (e) {
36
+ reject(e instanceof Error ? e : new Error(String(e)));
37
+ }
38
+ });
39
+ });
40
+ }
41
+ async function uploadSource(projectDir, deckId) {
42
+ const archive = await archiveSource(projectDir);
43
+ const sizeKB = archive.length / 1024;
44
+ console.log(`Source archive: ${sizeKB.toFixed(1)}KB`);
45
+ const uploadConfig = await api.createWebDeckSourceUploadConfig(deckId);
46
+ const formData = new FormData();
47
+ formData.append('Content-Type', 'application/gzip');
48
+ for (const [k, v] of Object.entries(uploadConfig.postFields)) {
49
+ formData.append(k, String(v));
50
+ }
51
+ // FormData expects a Blob; copy the Buffer's bytes so we don't share the underlying ArrayBuffer.
52
+ const view = new Uint8Array(archive.byteLength);
53
+ view.set(archive);
54
+ formData.append('file', new Blob([view], { type: 'application/gzip' }));
55
+ const s3Res = await fetch(uploadConfig.postUrl, {
56
+ method: 'POST',
57
+ body: formData,
58
+ signal: AbortSignal.timeout(60000),
59
+ });
60
+ if (s3Res.status >= 300) {
61
+ throw new Error(`Source upload failed: HTTP ${s3Res.status}`);
62
+ }
63
+ await api.saveWebDeckSource(deckId, uploadConfig.uploadId);
64
+ }
7
65
  function readCastleJson(dir) {
8
66
  const p = path.join(dir, 'castle.json');
9
67
  if (!fs.existsSync(p))
@@ -27,7 +85,7 @@ function buildSceneData(bundle) {
27
85
  },
28
86
  };
29
87
  }
30
- export async function push(dir) {
88
+ export async function saveDeck(dir) {
31
89
  if (!config.getToken()) {
32
90
  console.error('Not logged in. Run `castle-web login` first.');
33
91
  process.exit(1);
@@ -37,7 +95,7 @@ export async function push(dir) {
37
95
  console.error(`No index.html found in ${projectDir}`);
38
96
  process.exit(1);
39
97
  }
40
- let castleJson = readCastleJson(projectDir);
98
+ const castleJson = readCastleJson(projectDir);
41
99
  console.log('Bundling...');
42
100
  const bundle = await bundleProject(projectDir);
43
101
  const sizeKB = bundle.length / 1024;
@@ -59,7 +117,7 @@ export async function push(dir) {
59
117
  const formData = new FormData();
60
118
  formData.append('Content-Type', 'application/json');
61
119
  for (const [k, v] of Object.entries(uploadConfig.postFields)) {
62
- formData.append(k, `${v}`);
120
+ formData.append(k, String(v));
63
121
  }
64
122
  formData.append('file', new Blob([JSON.stringify(sceneData)]));
65
123
  const s3Res = await fetch(uploadConfig.postUrl, {
@@ -80,8 +138,11 @@ export async function push(dir) {
80
138
  console.log(`Created deck "${title}" (${result.deckId}). Saved castle.json.`);
81
139
  }
82
140
  else {
83
- console.log('Pushed to Castle.');
141
+ console.log('Saved to Castle.');
84
142
  }
143
+ console.log('Uploading source archive...');
144
+ await uploadSource(projectDir, result.deckId);
145
+ console.log('Source archive saved.');
85
146
  }
86
147
  catch (e) {
87
148
  const code = e?.extensions?.code ?? '';
@@ -89,7 +150,7 @@ export async function push(dir) {
89
150
  console.error('Not logged in. Run `castle-web login` first.');
90
151
  }
91
152
  else if (code === 'DECK_INVALID_PERMISSIONS') {
92
- console.error('You do not have permission to push to this deck.');
153
+ console.error('You do not have permission to save this deck.');
93
154
  }
94
155
  else {
95
156
  throw e;
package/dist/serve.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  export declare function serve(dir: string, options?: {
2
2
  port?: string;
3
+ host?: string;
3
4
  open?: boolean;
5
+ detach?: boolean;
4
6
  }): Promise<void>;