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/src/index.ts
DELETED
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import * as fs from 'fs';
|
|
4
|
-
import * as path from 'path';
|
|
5
|
-
import { login } from './login.js';
|
|
6
|
-
import { serve } from './serve.js';
|
|
7
|
-
import { push } from './push.js';
|
|
8
|
-
import { init } from './init.js';
|
|
9
|
-
import { savePreviewImage, savePreviewIfNeeded } from './preview.js';
|
|
10
|
-
|
|
11
|
-
const args = process.argv.slice(2);
|
|
12
|
-
const command = args[0];
|
|
13
|
-
|
|
14
|
-
const FLAGS_WITH_VALUES = new Set(['--port', '--out']);
|
|
15
|
-
|
|
16
|
-
function findPositionalDir(): string {
|
|
17
|
-
for (let i = 1; i < args.length; i++) {
|
|
18
|
-
if (args[i].startsWith('--')) {
|
|
19
|
-
if (FLAGS_WITH_VALUES.has(args[i])) i++;
|
|
20
|
-
continue;
|
|
21
|
-
}
|
|
22
|
-
return args[i];
|
|
23
|
-
}
|
|
24
|
-
return '.';
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function getFlagValue(flag: string): string | undefined {
|
|
28
|
-
const idx = args.indexOf(flag);
|
|
29
|
-
return idx >= 0 ? args[idx + 1] : undefined;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function readServeWsPort(dir: string): number | undefined {
|
|
33
|
-
try {
|
|
34
|
-
const serveJson = JSON.parse(fs.readFileSync(path.join(path.resolve(dir), '.castle', 'serve.json'), 'utf-8'));
|
|
35
|
-
return serveJson.wsPort;
|
|
36
|
-
} catch {
|
|
37
|
-
return undefined;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function getWsPort(dir: string): number {
|
|
42
|
-
const explicit = getFlagValue('--port');
|
|
43
|
-
if (explicit) return parseInt(explicit, 10);
|
|
44
|
-
const fromServe = readServeWsPort(dir);
|
|
45
|
-
if (fromServe) return fromServe;
|
|
46
|
-
return 3738;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function usage() {
|
|
50
|
-
console.log(`Usage:
|
|
51
|
-
castle-web init <dir>
|
|
52
|
-
castle-web serve [dir] [--port PORT] [--open]
|
|
53
|
-
castle-web restart [--port PORT]
|
|
54
|
-
castle-web screenshot [--out FILE] [--port PORT]
|
|
55
|
-
castle-web save-preview-image [dir] [--port PORT] [--no-restart]
|
|
56
|
-
castle-web push [dir]
|
|
57
|
-
castle-web login`);
|
|
58
|
-
process.exit(1);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
async function main() {
|
|
62
|
-
switch (command) {
|
|
63
|
-
case 'init': {
|
|
64
|
-
const dir = args[1];
|
|
65
|
-
if (!dir) { console.error('Usage: castle-web init <dir>'); process.exit(1); }
|
|
66
|
-
init(dir);
|
|
67
|
-
break;
|
|
68
|
-
}
|
|
69
|
-
case 'serve': {
|
|
70
|
-
const dir = findPositionalDir();
|
|
71
|
-
const port = getFlagValue('--port');
|
|
72
|
-
const open = args.includes('--open');
|
|
73
|
-
await serve(dir, { port, open });
|
|
74
|
-
break;
|
|
75
|
-
}
|
|
76
|
-
case 'push': {
|
|
77
|
-
const dir = findPositionalDir();
|
|
78
|
-
await push(dir);
|
|
79
|
-
const wsPort = getWsPort(dir);
|
|
80
|
-
await savePreviewIfNeeded(dir, wsPort);
|
|
81
|
-
break;
|
|
82
|
-
}
|
|
83
|
-
case 'restart': {
|
|
84
|
-
const dir = findPositionalDir();
|
|
85
|
-
const wsPort = getWsPort(dir);
|
|
86
|
-
const { default: WS } = await import('ws');
|
|
87
|
-
const ws = new WS(`ws://localhost:${wsPort}`);
|
|
88
|
-
ws.on('open', () => {
|
|
89
|
-
ws.send(JSON.stringify({ type: 'restart' }));
|
|
90
|
-
setTimeout(() => { ws.close(); process.exit(0); }, 100);
|
|
91
|
-
});
|
|
92
|
-
ws.on('error', () => { console.error('Could not connect. Is castle-web serve running?'); process.exit(1); });
|
|
93
|
-
break;
|
|
94
|
-
}
|
|
95
|
-
case 'screenshot': {
|
|
96
|
-
const dir = findPositionalDir();
|
|
97
|
-
const outFile = getFlagValue('--out') ?? 'screenshot.png';
|
|
98
|
-
const wsPort = getWsPort(dir);
|
|
99
|
-
const requestId = Math.random().toString(36).slice(2);
|
|
100
|
-
const { default: WS } = await import('ws');
|
|
101
|
-
const ws = new WS(`ws://localhost:${wsPort}`);
|
|
102
|
-
ws.on('open', () => {
|
|
103
|
-
ws.send(JSON.stringify({ type: 'screenshot_request', requestId }));
|
|
104
|
-
});
|
|
105
|
-
const timeout = setTimeout(() => {
|
|
106
|
-
console.error('Screenshot timed out. Is castle-web serve running?');
|
|
107
|
-
ws.close();
|
|
108
|
-
process.exit(1);
|
|
109
|
-
}, 3000);
|
|
110
|
-
ws.on('message', (raw: Buffer) => {
|
|
111
|
-
try {
|
|
112
|
-
const msg = JSON.parse(raw.toString());
|
|
113
|
-
if (msg.type === 'screenshot_response' && msg.requestId === requestId) {
|
|
114
|
-
clearTimeout(timeout);
|
|
115
|
-
const base64 = msg.data.replace(/^data:image\/png;base64,/, '');
|
|
116
|
-
fs.writeFileSync(outFile, Buffer.from(base64, 'base64'));
|
|
117
|
-
console.log(`Saved ${outFile}`);
|
|
118
|
-
ws.close();
|
|
119
|
-
process.exit(0);
|
|
120
|
-
}
|
|
121
|
-
} catch {}
|
|
122
|
-
});
|
|
123
|
-
break;
|
|
124
|
-
}
|
|
125
|
-
case 'save-preview-image': {
|
|
126
|
-
const dir = findPositionalDir();
|
|
127
|
-
const wsPort = getWsPort(dir);
|
|
128
|
-
const noRestart = args.includes('--no-restart');
|
|
129
|
-
await savePreviewImage(dir, wsPort, noRestart);
|
|
130
|
-
process.exit(0);
|
|
131
|
-
}
|
|
132
|
-
case 'login':
|
|
133
|
-
await login();
|
|
134
|
-
break;
|
|
135
|
-
default:
|
|
136
|
-
usage();
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
main().catch((e) => {
|
|
141
|
-
console.error(e.message ?? e);
|
|
142
|
-
process.exit(1);
|
|
143
|
-
});
|
package/src/init.ts
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import * as fs from 'fs';
|
|
2
|
-
import * as path from 'path';
|
|
3
|
-
|
|
4
|
-
const INDEX_HTML = `<!DOCTYPE html>
|
|
5
|
-
<html>
|
|
6
|
-
<head>
|
|
7
|
-
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
|
|
8
|
-
<title>My Castle Game</title>
|
|
9
|
-
</head>
|
|
10
|
-
<body>
|
|
11
|
-
<script type="module" src="game.js"></script>
|
|
12
|
-
</body>
|
|
13
|
-
</html>
|
|
14
|
-
`;
|
|
15
|
-
|
|
16
|
-
const GAME_JS = `import { setup, initCard } from 'castle-web-sdk';
|
|
17
|
-
|
|
18
|
-
setup();
|
|
19
|
-
const card = initCard();
|
|
20
|
-
|
|
21
|
-
const el = document.createElement('div');
|
|
22
|
-
el.style.cssText = \`
|
|
23
|
-
width: 100%; height: 100%;
|
|
24
|
-
display: flex; align-items: center; justify-content: center;
|
|
25
|
-
font-family: system-ui; color: #fff; font-size: 24px;
|
|
26
|
-
\`;
|
|
27
|
-
el.textContent = 'Hello Castle!';
|
|
28
|
-
card.appendChild(el);
|
|
29
|
-
`;
|
|
30
|
-
|
|
31
|
-
const CLAUDE_MD = `@node_modules/castle-web-cli/AGENTS.md
|
|
32
|
-
@node_modules/castle-web-sdk/AGENTS.md
|
|
33
|
-
`;
|
|
34
|
-
|
|
35
|
-
export async function init(dir: string) {
|
|
36
|
-
const projectDir = path.resolve(dir);
|
|
37
|
-
|
|
38
|
-
if (fs.existsSync(projectDir) && fs.readdirSync(projectDir).length > 0) {
|
|
39
|
-
console.error(`Directory "${dir}" is not empty.`);
|
|
40
|
-
process.exit(1);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
fs.mkdirSync(projectDir, { recursive: true });
|
|
44
|
-
fs.writeFileSync(path.join(projectDir, 'index.html'), INDEX_HTML);
|
|
45
|
-
fs.writeFileSync(path.join(projectDir, 'game.js'), GAME_JS);
|
|
46
|
-
fs.writeFileSync(path.join(projectDir, 'CLAUDE.md'), CLAUDE_MD);
|
|
47
|
-
|
|
48
|
-
const packageJson = {
|
|
49
|
-
name: path.basename(projectDir),
|
|
50
|
-
private: true,
|
|
51
|
-
type: 'module',
|
|
52
|
-
dependencies: {
|
|
53
|
-
'castle-web-sdk': '*',
|
|
54
|
-
},
|
|
55
|
-
devDependencies: {
|
|
56
|
-
'castle-web-cli': '*',
|
|
57
|
-
},
|
|
58
|
-
};
|
|
59
|
-
fs.writeFileSync(path.join(projectDir, 'package.json'), JSON.stringify(packageJson, null, 2) + '\n');
|
|
60
|
-
|
|
61
|
-
const { execSync } = await import('child_process');
|
|
62
|
-
execSync('npm install', { cwd: projectDir, stdio: 'inherit' });
|
|
63
|
-
|
|
64
|
-
console.log(`Created project in ${projectDir}/`);
|
|
65
|
-
console.log('');
|
|
66
|
-
console.log('Next steps:');
|
|
67
|
-
console.log(` cd ${dir}`);
|
|
68
|
-
console.log(' npx castle-web-cli serve --open');
|
|
69
|
-
console.log(' npx castle-web-cli login');
|
|
70
|
-
console.log(' npx castle-web-cli push');
|
|
71
|
-
}
|
package/src/login.ts
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import * as api from './api.js';
|
|
2
|
-
import * as config from './config.js';
|
|
3
|
-
|
|
4
|
-
export async function login(): Promise<void> {
|
|
5
|
-
const { pollToken, url } = await api.startCLILogin();
|
|
6
|
-
console.log(`Opening browser to log in: ${url}`);
|
|
7
|
-
const { default: open } = await import('open');
|
|
8
|
-
await open(url);
|
|
9
|
-
console.log('Waiting for login...');
|
|
10
|
-
|
|
11
|
-
const MAX_POLLS = 600;
|
|
12
|
-
for (let i = 0; i < MAX_POLLS; i++) {
|
|
13
|
-
await new Promise((r) => setTimeout(r, 1000));
|
|
14
|
-
const user = await api.pollForCLILogin(pollToken);
|
|
15
|
-
if (user) {
|
|
16
|
-
config.setToken(user.token);
|
|
17
|
-
console.log(`Logged in as @${user.username}`);
|
|
18
|
-
return;
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
console.error('Login timed out.');
|
|
23
|
-
process.exit(1);
|
|
24
|
-
}
|
package/src/preview.ts
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import * as fs from 'fs';
|
|
2
|
-
import * as path from 'path';
|
|
3
|
-
import * as api from './api.js';
|
|
4
|
-
import * as config from './config.js';
|
|
5
|
-
|
|
6
|
-
function connectWS(wsPort: number): Promise<any> {
|
|
7
|
-
return new Promise(async (resolve, reject) => {
|
|
8
|
-
const { default: WS } = await import('ws');
|
|
9
|
-
const ws = new WS(`ws://localhost:${wsPort}`);
|
|
10
|
-
ws.on('error', () => reject(new Error('Could not connect. Is castle-web serve running?')));
|
|
11
|
-
ws.on('open', () => resolve(ws));
|
|
12
|
-
});
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function takeScreenshot(ws: any): Promise<string> {
|
|
16
|
-
return new Promise((resolve, reject) => {
|
|
17
|
-
const requestId = Math.random().toString(36).slice(2);
|
|
18
|
-
const timeout = setTimeout(() => {
|
|
19
|
-
reject(new Error('Screenshot timed out.'));
|
|
20
|
-
}, 10000);
|
|
21
|
-
ws.on('message', (raw: Buffer) => {
|
|
22
|
-
try {
|
|
23
|
-
const msg = JSON.parse(raw.toString());
|
|
24
|
-
if (msg.type === 'screenshot_response' && msg.requestId === requestId) {
|
|
25
|
-
clearTimeout(timeout);
|
|
26
|
-
resolve(msg.data.replace(/^data:image\/png;base64,/, ''));
|
|
27
|
-
}
|
|
28
|
-
} catch {}
|
|
29
|
-
});
|
|
30
|
-
ws.send(JSON.stringify({ type: 'screenshot_request', requestId }));
|
|
31
|
-
});
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function waitForRestart(ws: any): Promise<void> {
|
|
35
|
-
return new Promise((resolve) => {
|
|
36
|
-
ws.send(JSON.stringify({ type: 'restart' }));
|
|
37
|
-
setTimeout(resolve, 2000);
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export async function savePreviewImage(dir: string, wsPort: number, noRestart = false): Promise<void> {
|
|
42
|
-
const projectDir = path.resolve(dir);
|
|
43
|
-
const castleJsonPath = path.join(projectDir, 'castle.json');
|
|
44
|
-
if (!fs.existsSync(castleJsonPath)) {
|
|
45
|
-
console.error('No castle.json found. Push the deck first.');
|
|
46
|
-
process.exit(1);
|
|
47
|
-
}
|
|
48
|
-
const castleJson = JSON.parse(fs.readFileSync(castleJsonPath, 'utf-8'));
|
|
49
|
-
|
|
50
|
-
if (!config.getToken()) {
|
|
51
|
-
console.error('Not logged in. Run `castle-web login` first.');
|
|
52
|
-
process.exit(1);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const ws = await connectWS(wsPort);
|
|
56
|
-
if (!noRestart) await waitForRestart(ws);
|
|
57
|
-
const base64 = await takeScreenshot(ws);
|
|
58
|
-
ws.close();
|
|
59
|
-
|
|
60
|
-
fs.writeFileSync(path.join(projectDir, 'preview.png'), Buffer.from(base64, 'base64'));
|
|
61
|
-
console.log('Saved preview.png');
|
|
62
|
-
|
|
63
|
-
const file = await api.uploadBase64(base64, 'preview.png');
|
|
64
|
-
await api.updateCardCustomBackgroundImage(castleJson.cardId, file.fileId);
|
|
65
|
-
console.log('Set deck preview image.');
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export async function savePreviewIfNeeded(dir: string, wsPort: number): Promise<void> {
|
|
69
|
-
const projectDir = path.resolve(dir);
|
|
70
|
-
if (fs.existsSync(path.join(projectDir, 'preview.png'))) {
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
let ws: any;
|
|
74
|
-
try {
|
|
75
|
-
ws = await connectWS(wsPort);
|
|
76
|
-
await waitForRestart(ws);
|
|
77
|
-
const base64 = await takeScreenshot(ws);
|
|
78
|
-
ws.close();
|
|
79
|
-
|
|
80
|
-
const castleJsonPath = path.join(projectDir, 'castle.json');
|
|
81
|
-
if (!fs.existsSync(castleJsonPath)) return;
|
|
82
|
-
const castleJson = JSON.parse(fs.readFileSync(castleJsonPath, 'utf-8'));
|
|
83
|
-
|
|
84
|
-
fs.writeFileSync(path.join(projectDir, 'preview.png'), Buffer.from(base64, 'base64'));
|
|
85
|
-
console.log('Saved preview.png');
|
|
86
|
-
|
|
87
|
-
const file = await api.uploadBase64(base64, 'preview.png');
|
|
88
|
-
await api.updateCardCustomBackgroundImage(castleJson.cardId, file.fileId);
|
|
89
|
-
console.log('Set deck preview image.');
|
|
90
|
-
} catch (e: any) {
|
|
91
|
-
if (ws) ws.close();
|
|
92
|
-
console.log(`Preview skipped: ${e?.message ?? e}`);
|
|
93
|
-
}
|
|
94
|
-
}
|
package/src/push.ts
DELETED
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
import * as fs from 'fs';
|
|
2
|
-
import * as path from 'path';
|
|
3
|
-
import { nanoid } from 'nanoid';
|
|
4
|
-
import * as api from './api.js';
|
|
5
|
-
import * as config from './config.js';
|
|
6
|
-
import { bundleProject } from './bundle.js';
|
|
7
|
-
|
|
8
|
-
interface CastleJson {
|
|
9
|
-
deckId: string;
|
|
10
|
-
cardId: string;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function readCastleJson(dir: string): CastleJson | null {
|
|
14
|
-
const p = path.join(dir, 'castle.json');
|
|
15
|
-
if (!fs.existsSync(p)) return null;
|
|
16
|
-
return JSON.parse(fs.readFileSync(p, 'utf-8'));
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function buildSceneData(bundle: string): any {
|
|
20
|
-
return {
|
|
21
|
-
experimentalWeb: {
|
|
22
|
-
enabled: true,
|
|
23
|
-
bundle,
|
|
24
|
-
},
|
|
25
|
-
snapshot: {
|
|
26
|
-
sceneProperties: {
|
|
27
|
-
backgroundColor: { r: 0, g: 0, b: 0, a: 1 },
|
|
28
|
-
coordinateSystemVersion: 2,
|
|
29
|
-
},
|
|
30
|
-
actors: [],
|
|
31
|
-
library: {},
|
|
32
|
-
linkTargetDeckIds: [],
|
|
33
|
-
},
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export async function push(dir: string) {
|
|
38
|
-
if (!config.getToken()) {
|
|
39
|
-
console.error('Not logged in. Run `castle-web login` first.');
|
|
40
|
-
process.exit(1);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const projectDir = path.resolve(dir);
|
|
44
|
-
if (!fs.existsSync(path.join(projectDir, 'index.html'))) {
|
|
45
|
-
console.error(`No index.html found in ${projectDir}`);
|
|
46
|
-
process.exit(1);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
let castleJson = readCastleJson(projectDir);
|
|
50
|
-
|
|
51
|
-
console.log('Bundling...');
|
|
52
|
-
const bundle = await bundleProject(projectDir);
|
|
53
|
-
const sizeKB = bundle.length / 1024;
|
|
54
|
-
console.log(`Bundle size: ${sizeKB.toFixed(1)}KB`);
|
|
55
|
-
if (sizeKB > 1024) {
|
|
56
|
-
console.warn(`Warning: Bundle is ${(sizeKB / 1024).toFixed(1)}MB. Large bundles may load slowly.`);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const sceneData = buildSceneData(bundle);
|
|
60
|
-
const isNew = !castleJson;
|
|
61
|
-
const cardId = castleJson?.cardId ?? nanoid(12);
|
|
62
|
-
const deckId = castleJson?.deckId ?? nanoid(12);
|
|
63
|
-
|
|
64
|
-
// Upload scene data to S3
|
|
65
|
-
const configs = await api.createSceneDataUploadConfig([cardId]);
|
|
66
|
-
if (!configs?.length) {
|
|
67
|
-
console.error('Failed to get upload config.');
|
|
68
|
-
process.exit(1);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const uploadConfig = configs[0];
|
|
72
|
-
const formData = new FormData();
|
|
73
|
-
formData.append('Content-Type', 'application/json');
|
|
74
|
-
for (const [k, v] of Object.entries(uploadConfig.postFields)) {
|
|
75
|
-
formData.append(k, `${v}`);
|
|
76
|
-
}
|
|
77
|
-
formData.append('file', new Blob([JSON.stringify(sceneData)]));
|
|
78
|
-
|
|
79
|
-
const s3Res = await fetch(uploadConfig.postUrl, {
|
|
80
|
-
method: 'POST',
|
|
81
|
-
body: formData,
|
|
82
|
-
signal: AbortSignal.timeout(30000),
|
|
83
|
-
});
|
|
84
|
-
if (s3Res.status >= 300) {
|
|
85
|
-
throw new Error(`Upload failed: HTTP ${s3Res.status}`);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// Create or update via updateCardAndDeckV2
|
|
89
|
-
const title = path.basename(path.resolve(projectDir));
|
|
90
|
-
try {
|
|
91
|
-
const result = await api.updateCardAndDeckV2(
|
|
92
|
-
{ deckId, title, visibility: 'unlisted' },
|
|
93
|
-
{ cardId, blocks: [], uploadId: uploadConfig.uploadId, makeInitialCard: true }
|
|
94
|
-
);
|
|
95
|
-
|
|
96
|
-
if (isNew) {
|
|
97
|
-
const newCastleJson = { deckId: result.deckId, cardId: result.cardId };
|
|
98
|
-
fs.writeFileSync(
|
|
99
|
-
path.join(projectDir, 'castle.json'),
|
|
100
|
-
JSON.stringify(newCastleJson, null, 2) + '\n',
|
|
101
|
-
'utf-8'
|
|
102
|
-
);
|
|
103
|
-
console.log(`Created deck "${title}" (${result.deckId}). Saved castle.json.`);
|
|
104
|
-
} else {
|
|
105
|
-
console.log('Pushed to Castle.');
|
|
106
|
-
}
|
|
107
|
-
} catch (e: any) {
|
|
108
|
-
const code = e?.extensions?.code ?? '';
|
|
109
|
-
if (code === 'LOGIN_REQUIRED') {
|
|
110
|
-
console.error('Not logged in. Run `castle-web login` first.');
|
|
111
|
-
} else if (code === 'DECK_INVALID_PERMISSIONS') {
|
|
112
|
-
console.error('You do not have permission to push to this deck.');
|
|
113
|
-
} else {
|
|
114
|
-
throw e;
|
|
115
|
-
}
|
|
116
|
-
process.exit(1);
|
|
117
|
-
}
|
|
118
|
-
}
|
package/src/serve.ts
DELETED
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
import * as fs from 'fs';
|
|
2
|
-
import * as net from 'net';
|
|
3
|
-
import * as path from 'path';
|
|
4
|
-
import { createServer, type Plugin } from 'vite';
|
|
5
|
-
import { WebSocketServer, WebSocket } from 'ws';
|
|
6
|
-
|
|
7
|
-
function isPortFree(port: number): Promise<boolean> {
|
|
8
|
-
return new Promise((resolve) => {
|
|
9
|
-
const srv = net.createServer();
|
|
10
|
-
srv.once('error', () => resolve(false));
|
|
11
|
-
srv.listen(port, () => { srv.close(() => resolve(true)); });
|
|
12
|
-
});
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
async function findFreePorts(startPort: number): Promise<{ port: number; wsPort: number }> {
|
|
16
|
-
for (let p = startPort; p < startPort + 100; p += 2) {
|
|
17
|
-
if (await isPortFree(p) && await isPortFree(p + 1)) {
|
|
18
|
-
return { port: p, wsPort: p + 1 };
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
throw new Error(`No free port pair found starting from ${startPort}`);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function castlePlugin(wsPort: number): Plugin {
|
|
25
|
-
return {
|
|
26
|
-
name: 'castle-dev',
|
|
27
|
-
configureServer(server) {
|
|
28
|
-
server.middlewares.use((req, res, next) => {
|
|
29
|
-
if (req.url === '/__castle/ws-port') {
|
|
30
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
31
|
-
res.end(JSON.stringify({ port: wsPort }));
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
next();
|
|
35
|
-
});
|
|
36
|
-
},
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export async function serve(
|
|
41
|
-
dir: string,
|
|
42
|
-
options: { port?: string; open?: boolean } = {}
|
|
43
|
-
) {
|
|
44
|
-
const projectDir = path.resolve(dir);
|
|
45
|
-
if (!fs.existsSync(projectDir)) {
|
|
46
|
-
console.error(`Directory not found: ${projectDir}`);
|
|
47
|
-
process.exit(1);
|
|
48
|
-
}
|
|
49
|
-
if (!fs.existsSync(path.join(projectDir, 'index.html'))) {
|
|
50
|
-
console.error(`No index.html found in ${projectDir}`);
|
|
51
|
-
process.exit(1);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const startPort = parseInt(options.port ?? '3737', 10);
|
|
55
|
-
const { port, wsPort } = options.port
|
|
56
|
-
? { port: startPort, wsPort: startPort + 1 }
|
|
57
|
-
: await findFreePorts(startPort);
|
|
58
|
-
|
|
59
|
-
// Local WebSocket server for SDK communication
|
|
60
|
-
const castleDir = path.join(projectDir, '.castle');
|
|
61
|
-
if (!fs.existsSync(castleDir)) fs.mkdirSync(castleDir, { recursive: true });
|
|
62
|
-
const logFile = path.join(castleDir, 'logs.txt');
|
|
63
|
-
const screenshotsDir = path.join(castleDir, 'screenshots');
|
|
64
|
-
|
|
65
|
-
const serveJsonPath = path.join(castleDir, 'serve.json');
|
|
66
|
-
fs.writeFileSync(serveJsonPath, JSON.stringify({ port, wsPort }) + '\n');
|
|
67
|
-
process.on('exit', () => { try { fs.unlinkSync(serveJsonPath); } catch {} });
|
|
68
|
-
process.on('SIGINT', () => process.exit());
|
|
69
|
-
process.on('SIGTERM', () => process.exit());
|
|
70
|
-
|
|
71
|
-
startWSServer(wsPort, logFile, screenshotsDir);
|
|
72
|
-
|
|
73
|
-
const vite = await createServer({
|
|
74
|
-
root: projectDir,
|
|
75
|
-
plugins: [castlePlugin(wsPort)],
|
|
76
|
-
server: {
|
|
77
|
-
port,
|
|
78
|
-
strictPort: true,
|
|
79
|
-
open: options.open ? true : undefined,
|
|
80
|
-
hmr: false,
|
|
81
|
-
fs: { strict: false },
|
|
82
|
-
},
|
|
83
|
-
logLevel: 'info',
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
await vite.listen();
|
|
87
|
-
vite.printUrls();
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function startWSServer(port: number, logFile: string, screenshotsDir: string): WebSocketServer {
|
|
91
|
-
const wss = new WebSocketServer({ port });
|
|
92
|
-
const clients = new Set<WebSocket>();
|
|
93
|
-
|
|
94
|
-
wss.on('connection', (ws) => {
|
|
95
|
-
clients.add(ws);
|
|
96
|
-
|
|
97
|
-
ws.on('message', (raw) => {
|
|
98
|
-
try {
|
|
99
|
-
const msg = JSON.parse(raw.toString());
|
|
100
|
-
|
|
101
|
-
if (msg.type === 'restart' || msg.type === 'screenshot_request') {
|
|
102
|
-
const fwd = JSON.stringify(msg);
|
|
103
|
-
for (const c of clients) {
|
|
104
|
-
if (c !== ws && c.readyState === WebSocket.OPEN) c.send(fwd);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
if (msg.type === 'log') {
|
|
109
|
-
const line = `[${msg.level}] ${msg.msg}\n`;
|
|
110
|
-
fs.appendFileSync(logFile, line);
|
|
111
|
-
if (msg.level === 'error') process.stderr.write(line);
|
|
112
|
-
else process.stdout.write(line);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
if (msg.type === 'screenshot_response') {
|
|
116
|
-
if (!fs.existsSync(screenshotsDir)) fs.mkdirSync(screenshotsDir, { recursive: true });
|
|
117
|
-
const filename = `screenshot-${Date.now()}.png`;
|
|
118
|
-
const filepath = path.join(screenshotsDir, filename);
|
|
119
|
-
const base64 = msg.data.replace(/^data:image\/png;base64,/, '');
|
|
120
|
-
fs.writeFileSync(filepath, Buffer.from(base64, 'base64'));
|
|
121
|
-
console.log(`Screenshot saved: ${filepath}`);
|
|
122
|
-
const fwd = JSON.stringify(msg);
|
|
123
|
-
for (const c of clients) {
|
|
124
|
-
if (c !== ws && c.readyState === WebSocket.OPEN) c.send(fwd);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
} catch {}
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
ws.on('close', () => { clients.delete(ws); });
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
return wss;
|
|
134
|
-
}
|
package/tsconfig.json
DELETED