castle-web-cli 0.4.1 → 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.
- package/dist/api.d.ts +53 -5
- package/dist/api.js +42 -15
- package/dist/config.d.ts +2 -0
- package/dist/config.js +25 -11
- package/dist/get-deck.d.ts +3 -0
- package/dist/get-deck.js +64 -0
- package/dist/ide-client.d.ts +1 -0
- package/dist/ide-client.js +537 -0
- package/dist/ide.d.ts +16 -0
- package/dist/ide.js +546 -0
- package/dist/index.js +36 -41
- package/dist/init.d.ts +3 -1
- package/dist/init.js +170 -24
- package/dist/localPaths.d.ts +6 -0
- package/dist/localPaths.js +33 -0
- package/dist/login.js +1 -1
- package/dist/preview.d.ts +3 -0
- package/dist/preview.js +53 -34
- package/dist/save-deck.d.ts +2 -0
- package/dist/{push.js → save-deck.js} +66 -5
- package/dist/serve.d.ts +2 -0
- package/dist/serve.js +290 -27
- package/kits/basic-2d/.prettierrc +8 -0
- package/kits/basic-2d/CLAUDE.md +131 -0
- package/kits/basic-2d/behaviors/Camera.jsx +43 -0
- package/kits/basic-2d/behaviors/Collider.jsx +71 -0
- package/kits/basic-2d/behaviors/Drawing.jsx +139 -0
- package/kits/basic-2d/behaviors/Layout.jsx +16 -0
- package/kits/basic-2d/drawings/floor.drawing +70 -0
- package/kits/basic-2d/editors/App.jsx +152 -0
- package/kits/basic-2d/editors/CodeEditor.jsx +112 -0
- package/kits/basic-2d/editors/DrawingEditor.jsx +222 -0
- package/kits/basic-2d/editors/FileBrowser.jsx +143 -0
- package/kits/basic-2d/editors/PlayOnly.jsx +21 -0
- package/kits/basic-2d/editors/SceneEditor.jsx +1012 -0
- package/kits/basic-2d/editors/behaviorRegistry.js +24 -0
- package/kits/basic-2d/editors/editorHistory.js +52 -0
- package/kits/basic-2d/engine/ScenePlayer.jsx +83 -0
- package/kits/basic-2d/engine/SceneUI.jsx +67 -0
- package/kits/basic-2d/engine/TouchControls.jsx +136 -0
- package/kits/basic-2d/engine/autoInspector.jsx +51 -0
- package/kits/basic-2d/engine/files.js +62 -0
- package/kits/basic-2d/engine/scene.js +420 -0
- package/kits/basic-2d/engine/ui.jsx +344 -0
- package/kits/basic-2d/engine/ui.module.css +928 -0
- package/kits/basic-2d/eslint.config.js +50 -0
- package/kits/basic-2d/index.html +11 -0
- package/kits/basic-2d/main.jsx +10 -0
- package/kits/basic-2d/package-lock.json +2706 -0
- package/kits/basic-2d/package.json +41 -0
- package/kits/basic-2d/scenes/main.scene +108 -0
- package/kits/basic-2d/vite.config.js +1 -0
- package/kits/basic-2d-frozen/.prettierrc +8 -0
- package/kits/basic-2d-frozen/CLAUDE.md +131 -0
- package/kits/basic-2d-frozen/behaviors/Camera.jsx +43 -0
- package/kits/basic-2d-frozen/behaviors/Collider.jsx +71 -0
- package/kits/basic-2d-frozen/behaviors/Drawing.jsx +139 -0
- package/kits/basic-2d-frozen/behaviors/Layout.jsx +16 -0
- package/kits/basic-2d-frozen/drawings/floor.drawing +70 -0
- package/kits/basic-2d-frozen/editors/App.jsx +152 -0
- package/kits/basic-2d-frozen/editors/CodeEditor.jsx +112 -0
- package/kits/basic-2d-frozen/editors/DrawingEditor.jsx +222 -0
- package/kits/basic-2d-frozen/editors/FileBrowser.jsx +143 -0
- package/kits/basic-2d-frozen/editors/PlayOnly.jsx +21 -0
- package/kits/basic-2d-frozen/editors/SceneEditor.jsx +1012 -0
- package/kits/basic-2d-frozen/editors/behaviorRegistry.js +24 -0
- package/kits/basic-2d-frozen/editors/editorHistory.js +52 -0
- package/kits/basic-2d-frozen/engine/ScenePlayer.jsx +83 -0
- package/kits/basic-2d-frozen/engine/SceneUI.jsx +67 -0
- package/kits/basic-2d-frozen/engine/TouchControls.jsx +136 -0
- package/kits/basic-2d-frozen/engine/autoInspector.jsx +51 -0
- package/kits/basic-2d-frozen/engine/files.js +62 -0
- package/kits/basic-2d-frozen/engine/scene.js +420 -0
- package/kits/basic-2d-frozen/engine/ui.jsx +344 -0
- package/kits/basic-2d-frozen/engine/ui.module.css +928 -0
- package/kits/basic-2d-frozen/eslint.config.js +50 -0
- package/kits/basic-2d-frozen/index.html +11 -0
- package/kits/basic-2d-frozen/main.jsx +10 -0
- package/kits/basic-2d-frozen/package-lock.json +2706 -0
- package/kits/basic-2d-frozen/package.json +41 -0
- package/kits/basic-2d-frozen/scenes/main.scene +108 -0
- package/kits/basic-2d-frozen/vite.config.js +1 -0
- package/kits/rpg-2d/.prettierrc +8 -0
- package/kits/rpg-2d/behaviors/Camera.tsx +52 -0
- package/kits/rpg-2d/behaviors/Collider.tsx +98 -0
- package/kits/rpg-2d/behaviors/Dialog.tsx +184 -0
- package/kits/rpg-2d/behaviors/Drawing.tsx +161 -0
- package/kits/rpg-2d/behaviors/Friend.tsx +45 -0
- package/kits/rpg-2d/behaviors/Layout.tsx +29 -0
- package/kits/rpg-2d/behaviors/PlayerController.tsx +255 -0
- package/kits/rpg-2d/behaviors/Portal.tsx +60 -0
- package/kits/rpg-2d/behaviors/QuestLog.tsx +90 -0
- package/kits/rpg-2d/behaviors/SaveMenu.tsx +123 -0
- package/kits/rpg-2d/behaviors/Tilemap.tsx +90 -0
- package/kits/rpg-2d/drawings/bld-home.drawing +8136 -0
- package/kits/rpg-2d/drawings/env-crate.drawing +509 -0
- package/kits/rpg-2d/drawings/env-fence.drawing +536 -0
- package/kits/rpg-2d/drawings/env-flower-bed.drawing +607 -0
- package/kits/rpg-2d/drawings/env-fountain.drawing +2622 -0
- package/kits/rpg-2d/drawings/env-hedge.drawing +601 -0
- package/kits/rpg-2d/drawings/env-house-blue.drawing +1 -0
- package/kits/rpg-2d/drawings/env-house-green.drawing +1 -0
- package/kits/rpg-2d/drawings/env-tree-oak.drawing +1540 -0
- package/kits/rpg-2d/drawings/env-tree-pine.drawing +1315 -0
- package/kits/rpg-2d/drawings/floor.drawing +70 -0
- package/kits/rpg-2d/drawings/fx-sparkle.drawing +926 -0
- package/kits/rpg-2d/drawings/npc-juno-idle-down.drawing +1099 -0
- package/kits/rpg-2d/drawings/npc-juno-walk-down.drawing +4177 -0
- package/kits/rpg-2d/drawings/npc-opal-idle-down.drawing +1099 -0
- package/kits/rpg-2d/drawings/npc-opal-walk-down.drawing +4177 -0
- package/kits/rpg-2d/drawings/player-idle-down.drawing +1070 -0
- package/kits/rpg-2d/drawings/player-idle-left.drawing +1070 -0
- package/kits/rpg-2d/drawings/player-idle-right.drawing +1070 -0
- package/kits/rpg-2d/drawings/player-idle-up.drawing +1070 -0
- package/kits/rpg-2d/drawings/player-walk-down.drawing +4148 -0
- package/kits/rpg-2d/drawings/player-walk-left.drawing +4148 -0
- package/kits/rpg-2d/drawings/player-walk-right.drawing +4148 -0
- package/kits/rpg-2d/drawings/player-walk-up.drawing +4148 -0
- package/kits/rpg-2d/editors/App.tsx +163 -0
- package/kits/rpg-2d/editors/CodeEditor.tsx +120 -0
- package/kits/rpg-2d/editors/DrawingEditor.tsx +278 -0
- package/kits/rpg-2d/editors/FileBrowser.tsx +191 -0
- package/kits/rpg-2d/editors/PlayOnly.tsx +26 -0
- package/kits/rpg-2d/editors/SceneEditor.tsx +1093 -0
- package/kits/rpg-2d/editors/behaviorRegistry.ts +33 -0
- package/kits/rpg-2d/editors/editorHistory.ts +75 -0
- package/kits/rpg-2d/editors/editorProps.ts +10 -0
- package/kits/rpg-2d/engine/ScenePlayer.tsx +130 -0
- package/kits/rpg-2d/engine/SceneUI.tsx +74 -0
- package/kits/rpg-2d/engine/TouchControls.tsx +157 -0
- package/kits/rpg-2d/engine/autoInspector.tsx +111 -0
- package/kits/rpg-2d/engine/drawing.ts +81 -0
- package/kits/rpg-2d/engine/files.ts +215 -0
- package/kits/rpg-2d/engine/scene.ts +484 -0
- package/kits/rpg-2d/engine/ui.module.css +928 -0
- package/kits/rpg-2d/engine/ui.tsx +483 -0
- package/kits/rpg-2d/eslint.config.js +46 -0
- package/kits/rpg-2d/index.html +11 -0
- package/kits/rpg-2d/main.tsx +14 -0
- package/kits/rpg-2d/package-lock.json +3149 -0
- package/kits/rpg-2d/package.json +46 -0
- package/kits/rpg-2d/scenes/main.scene +203 -0
- package/kits/rpg-2d/tsconfig.json +17 -0
- package/kits/rpg-2d/vite-env.d.ts +7 -0
- package/kits/rpg-2d/vite.config.js +1 -0
- package/package.json +27 -5
- package/AGENTS.md +0 -25
- package/dist/push.d.ts +0 -1
- package/src/api.ts +0 -160
- package/src/bundle.ts +0 -28
- package/src/config.ts +0 -36
- package/src/index.ts +0 -143
- package/src/init.ts +0 -71
- package/src/login.ts +0 -24
- package/src/preview.ts +0 -94
- package/src/push.ts +0 -118
- package/src/serve.ts +0 -134
- 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,183 @@ el.style.cssText = \`
|
|
|
25
26
|
el.textContent = 'Hello Castle!';
|
|
26
27
|
card.appendChild(el);
|
|
27
28
|
`;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
46
|
-
|
|
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
|
-
|
|
49
|
-
'castle-web-
|
|
76
|
+
dependencies: {
|
|
77
|
+
'castle-web-sdk': `file:${sdkPackage}`,
|
|
50
78
|
},
|
|
51
79
|
};
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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('
|
|
60
|
-
console.log('
|
|
61
|
-
console.log('
|
|
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.
|
|
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
|
-
|
|
7
|
-
|
|
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
|
-
|
|
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
|
-
|
|
27
|
+
const onMessage = (raw) => {
|
|
20
28
|
try {
|
|
21
29
|
const msg = JSON.parse(raw.toString());
|
|
22
|
-
if (msg.type
|
|
23
|
-
|
|
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
|
-
|
|
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
|
|
41
|
-
if (!
|
|
42
|
-
console.error('No castle.json found.
|
|
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
|
-
|
|
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
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
if (!fs.existsSync(castleJsonPath))
|
|
93
|
+
const castleJson = readCastleJson(projectDir);
|
|
94
|
+
if (!castleJson) {
|
|
95
|
+
ws.close();
|
|
74
96
|
return;
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
103
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
104
|
+
console.log(`Preview skipped: ${msg}`);
|
|
86
105
|
}
|
|
87
106
|
}
|
|
@@ -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
|
|
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
|
-
|
|
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,
|
|
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('
|
|
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
|
|
153
|
+
console.error('You do not have permission to save this deck.');
|
|
93
154
|
}
|
|
94
155
|
else {
|
|
95
156
|
throw e;
|