castle-web-cli 0.4.39 → 0.4.41
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/init.js +80 -80
- package/kits/basic-2d/pnpm-lock.yaml +1761 -0
- package/kits/basic-3d/pnpm-lock.yaml +1769 -0
- package/package.json +1 -1
package/dist/init.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { execSync } from
|
|
2
|
-
import * as fs from
|
|
3
|
-
import * as path from
|
|
4
|
-
import { COMMON_INSTRUCTIONS } from
|
|
5
|
-
import { getCliEntryPath, getKitsDir, getRepoRoot, getSdkPackagePath, toPosixPath
|
|
6
|
-
import { serve } from
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { COMMON_INSTRUCTIONS } from './commonInstructions.js';
|
|
5
|
+
import { getCliEntryPath, getKitsDir, getRepoRoot, getSdkPackagePath, toPosixPath } from './localPaths.js';
|
|
6
|
+
import { serve } from './serve.js';
|
|
7
7
|
const INDEX_HTML = `<!DOCTYPE html>
|
|
8
8
|
<html>
|
|
9
9
|
<head>
|
|
@@ -31,20 +31,14 @@ card.appendChild(el);
|
|
|
31
31
|
`;
|
|
32
32
|
// Default kit copied by `init` when no --kit is given. `none`/`bare` skip the
|
|
33
33
|
// kit and produce the minimal index.html + game.js stub above.
|
|
34
|
-
const DEFAULT_KIT =
|
|
34
|
+
const DEFAULT_KIT = 'basic-2d';
|
|
35
35
|
// Registry version of castle-web-sdk to inject when scaffolding from a
|
|
36
36
|
// globally-installed castle-web (not from inside the workspace). Bumped
|
|
37
37
|
// alongside cli/sdk version bumps.
|
|
38
|
-
const PUBLISHED_SDK_VERSION =
|
|
38
|
+
const PUBLISHED_SDK_VERSION = '0.4.4';
|
|
39
39
|
// Never copied into a fresh deck: build/dependency junk, and castle.json (a
|
|
40
40
|
// fresh deck has no deckId until its first save-deck).
|
|
41
|
-
const KIT_COPY_EXCLUDE = new Set([
|
|
42
|
-
"node_modules",
|
|
43
|
-
".castle",
|
|
44
|
-
"dist",
|
|
45
|
-
".git",
|
|
46
|
-
"castle.json",
|
|
47
|
-
]);
|
|
41
|
+
const KIT_COPY_EXCLUDE = new Set(['node_modules', '.castle', 'dist', '.git', 'castle.json']);
|
|
48
42
|
// Resolve how a scaffolded deck should reference the sdk + cli. Both the bare
|
|
49
43
|
// and kit scaffold paths go through here so they stay in sync.
|
|
50
44
|
// workspace mode (sdk/ sits next to cli/, i.e. running from a checkout):
|
|
@@ -58,17 +52,11 @@ function resolveScaffoldRefs() {
|
|
|
58
52
|
// npm package and need the published refs + `castle-web` binary.
|
|
59
53
|
const workspaceMode = fs.existsSync(sdkPath);
|
|
60
54
|
const sdkPathPosix = workspaceMode ? toPosixPath(sdkPath) : null;
|
|
61
|
-
const cliDistAbs = workspaceMode
|
|
62
|
-
? toPosixPath(path.dirname(getCliEntryPath()))
|
|
63
|
-
: null;
|
|
55
|
+
const cliDistAbs = workspaceMode ? toPosixPath(path.dirname(getCliEntryPath())) : null;
|
|
64
56
|
return {
|
|
65
57
|
workspaceMode,
|
|
66
|
-
sdkRef: workspaceMode
|
|
67
|
-
|
|
68
|
-
: `^${PUBLISHED_SDK_VERSION}`,
|
|
69
|
-
cliCommand: workspaceMode
|
|
70
|
-
? `node ${toPosixPath(getCliEntryPath())}`
|
|
71
|
-
: "castle-web",
|
|
58
|
+
sdkRef: workspaceMode ? `file:${sdkPathPosix}` : `^${PUBLISHED_SDK_VERSION}`,
|
|
59
|
+
cliCommand: workspaceMode ? `node ${toPosixPath(getCliEntryPath())}` : 'castle-web',
|
|
72
60
|
cliDistAbs,
|
|
73
61
|
sdkPathPosix,
|
|
74
62
|
};
|
|
@@ -78,9 +66,9 @@ function makeClaudeMd() {
|
|
|
78
66
|
// castle-experimental-web checkout, but breaks when the scaffold lives
|
|
79
67
|
// outside the repo (the relative path no longer resolves).
|
|
80
68
|
const repoRoot = getRepoRoot();
|
|
81
|
-
const upstream = path.join(repoRoot,
|
|
69
|
+
const upstream = path.join(repoRoot, 'CLAUDE.md');
|
|
82
70
|
try {
|
|
83
|
-
return fs.readFileSync(upstream,
|
|
71
|
+
return fs.readFileSync(upstream, 'utf8').trimEnd() + '\n';
|
|
84
72
|
}
|
|
85
73
|
catch {
|
|
86
74
|
return `# Castle Experimental Web\n\nSee https://github.com/castle-xyz/castle-experimental-web for the agent guide.\n`;
|
|
@@ -90,15 +78,13 @@ function makeClaudeMd() {
|
|
|
90
78
|
// the kit's (or bare) CLAUDE.md is written; the AGENTS.md symlink picks the
|
|
91
79
|
// appended content up for free.
|
|
92
80
|
function appendCommonInstructions(projectDir) {
|
|
93
|
-
const claudePath = path.join(projectDir,
|
|
94
|
-
const existing = fs.existsSync(claudePath)
|
|
95
|
-
? fs.readFileSync(claudePath, "utf8").trimEnd() + "\n\n"
|
|
96
|
-
: "";
|
|
81
|
+
const claudePath = path.join(projectDir, 'CLAUDE.md');
|
|
82
|
+
const existing = fs.existsSync(claudePath) ? fs.readFileSync(claudePath, 'utf8').trimEnd() + '\n\n' : '';
|
|
97
83
|
fs.writeFileSync(claudePath, existing + COMMON_INSTRUCTIONS);
|
|
98
84
|
}
|
|
99
85
|
function tryMakeAgentsSymlink(agentsPath) {
|
|
100
86
|
try {
|
|
101
|
-
fs.symlinkSync(
|
|
87
|
+
fs.symlinkSync('CLAUDE.md', agentsPath);
|
|
102
88
|
}
|
|
103
89
|
catch {
|
|
104
90
|
// symlink already exists / unsupported FS — non-fatal
|
|
@@ -109,37 +95,37 @@ function makePackageJson(projectDir) {
|
|
|
109
95
|
return {
|
|
110
96
|
name: path.basename(projectDir),
|
|
111
97
|
private: true,
|
|
112
|
-
type:
|
|
98
|
+
type: 'module',
|
|
113
99
|
scripts: {
|
|
114
100
|
restart: `${cliCommand} restart .`,
|
|
115
101
|
screenshot: `${cliCommand} screenshot .`,
|
|
116
|
-
|
|
102
|
+
'save-deck': `${cliCommand} save-deck .`,
|
|
117
103
|
},
|
|
118
104
|
dependencies: {
|
|
119
|
-
|
|
105
|
+
'castle-web-sdk': sdkRef,
|
|
120
106
|
},
|
|
121
107
|
};
|
|
122
108
|
}
|
|
123
109
|
// Some coding agents read AGENTS.md by convention. Symlink so they get the
|
|
124
110
|
// same guidance without a duplicate copy.
|
|
125
111
|
function ensureAgentsSymlink(projectDir) {
|
|
126
|
-
const agentsPath = path.join(projectDir,
|
|
112
|
+
const agentsPath = path.join(projectDir, 'AGENTS.md');
|
|
127
113
|
if (fs.lstatSync(agentsPath, { throwIfNoEntry: false }))
|
|
128
114
|
return;
|
|
129
115
|
// Don't create a dangling link — only symlink when CLAUDE.md is present.
|
|
130
|
-
if (!fs.existsSync(path.join(projectDir,
|
|
116
|
+
if (!fs.existsSync(path.join(projectDir, 'CLAUDE.md')))
|
|
131
117
|
return;
|
|
132
118
|
tryMakeAgentsSymlink(agentsPath);
|
|
133
119
|
}
|
|
134
120
|
// Bare scaffold: a plain code-only deck with no kit framework.
|
|
135
121
|
function scaffoldBare(projectDir) {
|
|
136
122
|
fs.mkdirSync(projectDir, { recursive: true });
|
|
137
|
-
fs.writeFileSync(path.join(projectDir,
|
|
138
|
-
fs.writeFileSync(path.join(projectDir,
|
|
139
|
-
fs.writeFileSync(path.join(projectDir,
|
|
123
|
+
fs.writeFileSync(path.join(projectDir, 'index.html'), INDEX_HTML);
|
|
124
|
+
fs.writeFileSync(path.join(projectDir, 'game.js'), GAME_JS);
|
|
125
|
+
fs.writeFileSync(path.join(projectDir, 'CLAUDE.md'), makeClaudeMd());
|
|
140
126
|
appendCommonInstructions(projectDir);
|
|
141
127
|
ensureAgentsSymlink(projectDir);
|
|
142
|
-
fs.writeFileSync(path.join(projectDir,
|
|
128
|
+
fs.writeFileSync(path.join(projectDir, 'package.json'), JSON.stringify(makePackageJson(projectDir), null, 2) + '\n');
|
|
143
129
|
}
|
|
144
130
|
// Copy a framework kit from kits/<kit>/ into the new deck dir, dropping
|
|
145
131
|
// build/dependency junk and castle.json.
|
|
@@ -147,7 +133,7 @@ function scaffoldFromKit(kit, projectDir) {
|
|
|
147
133
|
const kitDir = path.join(getKitsDir(), kit);
|
|
148
134
|
if (!fs.existsSync(kitDir) || !fs.statSync(kitDir).isDirectory()) {
|
|
149
135
|
console.error(`Kit "${kit}" not found at ${kitDir}.`);
|
|
150
|
-
console.error(
|
|
136
|
+
console.error('Available kits:');
|
|
151
137
|
try {
|
|
152
138
|
const kits = fs
|
|
153
139
|
.readdirSync(getKitsDir())
|
|
@@ -156,12 +142,12 @@ function scaffoldFromKit(kit, projectDir) {
|
|
|
156
142
|
for (const name of kits)
|
|
157
143
|
console.error(` ${name}`);
|
|
158
144
|
else
|
|
159
|
-
console.error(
|
|
145
|
+
console.error(' (none)');
|
|
160
146
|
}
|
|
161
147
|
catch {
|
|
162
|
-
console.error(
|
|
148
|
+
console.error(' (none — kits/ directory is missing)');
|
|
163
149
|
}
|
|
164
|
-
console.error(
|
|
150
|
+
console.error('Or use `--kit none` for a bare code-only deck.');
|
|
165
151
|
process.exit(1);
|
|
166
152
|
}
|
|
167
153
|
fs.cpSync(kitDir, projectDir, {
|
|
@@ -175,10 +161,10 @@ function scaffoldFromKit(kit, projectDir) {
|
|
|
175
161
|
// deck lives at castle-experimental-web/decks/<name>/. Rewrite both to
|
|
176
162
|
// absolute paths so the scaffolded deck works anywhere -- including under
|
|
177
163
|
// /tmp where macOS's /tmp -> /private/tmp symlink breaks relative-path math.
|
|
178
|
-
const pkgPath = path.join(projectDir,
|
|
164
|
+
const pkgPath = path.join(projectDir, 'package.json');
|
|
179
165
|
if (fs.existsSync(pkgPath)) {
|
|
180
166
|
try {
|
|
181
|
-
const pkg = JSON.parse(fs.readFileSync(pkgPath,
|
|
167
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
182
168
|
pkg.name = path.basename(projectDir);
|
|
183
169
|
// Local-dev paths (`file:../../sdk` / `node ../../cli/dist/index.js`) only
|
|
184
170
|
// work when the deck lives inside the castle-experimental-web workspace.
|
|
@@ -187,13 +173,13 @@ function scaffoldFromKit(kit, projectDir) {
|
|
|
187
173
|
// the bare scaffold path uses.
|
|
188
174
|
const { workspaceMode, sdkRef, cliDistAbs, sdkPathPosix } = resolveScaffoldRefs();
|
|
189
175
|
if (pkg.dependencies &&
|
|
190
|
-
typeof pkg.dependencies[
|
|
191
|
-
pkg.dependencies[
|
|
192
|
-
pkg.dependencies[
|
|
176
|
+
typeof pkg.dependencies['castle-web-sdk'] === 'string' &&
|
|
177
|
+
pkg.dependencies['castle-web-sdk'].startsWith('file:')) {
|
|
178
|
+
pkg.dependencies['castle-web-sdk'] = sdkRef;
|
|
193
179
|
}
|
|
194
180
|
if (pkg.scripts) {
|
|
195
181
|
for (const k of Object.keys(pkg.scripts)) {
|
|
196
|
-
if (typeof pkg.scripts[k] !==
|
|
182
|
+
if (typeof pkg.scripts[k] !== 'string')
|
|
197
183
|
continue;
|
|
198
184
|
if (workspaceMode) {
|
|
199
185
|
pkg.scripts[k] = pkg.scripts[k]
|
|
@@ -203,13 +189,13 @@ function scaffoldFromKit(kit, projectDir) {
|
|
|
203
189
|
else {
|
|
204
190
|
// Globally-installed: route through the `castle-web` binary on PATH.
|
|
205
191
|
pkg.scripts[k] = pkg.scripts[k]
|
|
206
|
-
.replace(/node\s+\.\.\/\.\.\/cli\/dist\/index\.js/g,
|
|
192
|
+
.replace(/node\s+\.\.\/\.\.\/cli\/dist\/index\.js/g, 'castle-web')
|
|
207
193
|
.replace(/await import\((['"])\.\.\/\.\.\/cli\/dist\/bundle\.js\1\)/g, "await import('castle-web-cli/dist/bundle.js')")
|
|
208
|
-
.replace(/\.\.\/\.\.\/sdk/g,
|
|
194
|
+
.replace(/\.\.\/\.\.\/sdk/g, '');
|
|
209
195
|
}
|
|
210
196
|
}
|
|
211
197
|
}
|
|
212
|
-
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) +
|
|
198
|
+
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
|
|
213
199
|
}
|
|
214
200
|
catch {
|
|
215
201
|
// kit shipped an unparseable package.json — leave it for the user to fix
|
|
@@ -217,7 +203,7 @@ function scaffoldFromKit(kit, projectDir) {
|
|
|
217
203
|
}
|
|
218
204
|
// Every deck needs a CLAUDE.md so coding agents know how castle-web works.
|
|
219
205
|
// Keep the kit's own if it ships one; otherwise generate from the upstream.
|
|
220
|
-
const claudePath = path.join(projectDir,
|
|
206
|
+
const claudePath = path.join(projectDir, 'CLAUDE.md');
|
|
221
207
|
if (!fs.existsSync(claudePath)) {
|
|
222
208
|
fs.writeFileSync(claudePath, makeClaudeMd());
|
|
223
209
|
}
|
|
@@ -226,7 +212,7 @@ function scaffoldFromKit(kit, projectDir) {
|
|
|
226
212
|
}
|
|
227
213
|
function hasPnpm() {
|
|
228
214
|
try {
|
|
229
|
-
execSync(
|
|
215
|
+
execSync('pnpm --version', { stdio: 'ignore' });
|
|
230
216
|
return true;
|
|
231
217
|
}
|
|
232
218
|
catch {
|
|
@@ -234,23 +220,27 @@ function hasPnpm() {
|
|
|
234
220
|
}
|
|
235
221
|
}
|
|
236
222
|
// Install the scaffolded deck's deps. Prefer pnpm -- in the e2b template a pnpm
|
|
237
|
-
// store is baked in, so
|
|
238
|
-
//
|
|
239
|
-
//
|
|
240
|
-
//
|
|
241
|
-
|
|
223
|
+
// store is baked in, so this is near-instant (hardlinks from the store, no
|
|
224
|
+
// download). Fall back to npm when pnpm isn't on PATH (e.g. a laptop that never
|
|
225
|
+
// installed it). --prefer-offline uses the store/cache first; --ignore-scripts
|
|
226
|
+
// skips dep build scripts (pnpm 10+ gates them and exits non-zero otherwise,
|
|
227
|
+
// and the deck's deps are all prebuilt pure JS that don't need them). `frozen`
|
|
228
|
+
// installs straight from the shipped lockfile (skips resolution -> no network,
|
|
229
|
+
// fast + deterministic); used in published mode where the kit lockfile matches.
|
|
230
|
+
function installDeps(projectDir, frozen) {
|
|
242
231
|
if (hasPnpm()) {
|
|
243
|
-
console.log(
|
|
244
|
-
|
|
232
|
+
console.log('Installing deps (pnpm)...');
|
|
233
|
+
const frozenFlag = frozen ? '--frozen-lockfile ' : '';
|
|
234
|
+
execSync(`pnpm install ${frozenFlag}--prefer-offline --ignore-scripts`, {
|
|
245
235
|
cwd: projectDir,
|
|
246
|
-
stdio:
|
|
236
|
+
stdio: 'inherit',
|
|
247
237
|
});
|
|
248
238
|
}
|
|
249
239
|
else {
|
|
250
|
-
console.log(
|
|
251
|
-
execSync(
|
|
240
|
+
console.log('Installing deps (npm)...');
|
|
241
|
+
execSync('npm install --no-audit --no-fund --loglevel=error', {
|
|
252
242
|
cwd: projectDir,
|
|
253
|
-
stdio:
|
|
243
|
+
stdio: 'inherit',
|
|
254
244
|
});
|
|
255
245
|
}
|
|
256
246
|
}
|
|
@@ -261,25 +251,35 @@ export async function init(dir, opts = {}) {
|
|
|
261
251
|
process.exit(1);
|
|
262
252
|
}
|
|
263
253
|
const kit = opts.kit ?? DEFAULT_KIT;
|
|
264
|
-
const bare = kit ===
|
|
254
|
+
const bare = kit === 'none' || kit === 'bare';
|
|
265
255
|
if (bare) {
|
|
266
256
|
scaffoldBare(projectDir);
|
|
267
257
|
}
|
|
268
258
|
else {
|
|
269
259
|
scaffoldFromKit(kit, projectDir);
|
|
270
260
|
}
|
|
271
|
-
console.log(`Created project in ${projectDir}/${bare ?
|
|
272
|
-
// Always install deps so the deck is ready to serve/edit immediately.
|
|
273
|
-
//
|
|
274
|
-
// their own serve
|
|
275
|
-
|
|
261
|
+
console.log(`Created project in ${projectDir}/${bare ? '' : ` (from kit "${kit}")`}`);
|
|
262
|
+
// Always install deps so the deck is ready to serve/edit immediately.
|
|
263
|
+
// `--no-serve` only skips the serve step below (callers like the cloud
|
|
264
|
+
// launcher run their own serve, but still want deps in place).
|
|
265
|
+
//
|
|
266
|
+
// Published kit decks ship a matching pnpm-lock.yaml -> frozen install (no
|
|
267
|
+
// resolution, no network; hardlinks from the template's warm store). Workspace
|
|
268
|
+
// decks rewrite the sdk to file:../../sdk, which the shipped (published)
|
|
269
|
+
// lockfile won't match -> drop it and let pnpm resolve. Bare decks have no
|
|
270
|
+
// lockfile -> non-frozen.
|
|
271
|
+
const lockPath = path.join(projectDir, 'pnpm-lock.yaml');
|
|
272
|
+
if (resolveScaffoldRefs().workspaceMode)
|
|
273
|
+
fs.rmSync(lockPath, { force: true });
|
|
274
|
+
const frozen = fs.existsSync(lockPath);
|
|
275
|
+
console.log('');
|
|
276
276
|
let installed = false;
|
|
277
277
|
try {
|
|
278
|
-
installDeps(projectDir);
|
|
278
|
+
installDeps(projectDir, frozen);
|
|
279
279
|
installed = true;
|
|
280
280
|
}
|
|
281
281
|
catch {
|
|
282
|
-
console.error(
|
|
282
|
+
console.error('dependency install failed; re-run `pnpm install` (or `npm install`) in the deck.');
|
|
283
283
|
}
|
|
284
284
|
const autoServe = opts.serve !== false;
|
|
285
285
|
if (autoServe && installed) {
|
|
@@ -289,16 +289,16 @@ export async function init(dir, opts = {}) {
|
|
|
289
289
|
// the served page; users can override host on a subsequent serve call.
|
|
290
290
|
// Open in the user's default browser unless we're clearly headless (SSH
|
|
291
291
|
// session) or the user has opted out via CASTLE_WEB_CLI_NO_OPEN=1.
|
|
292
|
-
const noOpen = process.env.CASTLE_WEB_CLI_NO_OPEN ===
|
|
292
|
+
const noOpen = process.env.CASTLE_WEB_CLI_NO_OPEN === '1' ||
|
|
293
293
|
!!process.env.SSH_CONNECTION ||
|
|
294
294
|
!!process.env.SSH_TTY;
|
|
295
|
-
await serve(projectDir, { host:
|
|
295
|
+
await serve(projectDir, { host: '0.0.0.0', detach: true, open: !noOpen });
|
|
296
296
|
return;
|
|
297
297
|
}
|
|
298
|
-
console.log(
|
|
299
|
-
console.log(
|
|
298
|
+
console.log('');
|
|
299
|
+
console.log('Next steps:');
|
|
300
300
|
console.log(` cd ${dir}`);
|
|
301
301
|
if (!installed)
|
|
302
|
-
console.log(
|
|
303
|
-
console.log(
|
|
302
|
+
console.log(' pnpm install # or: npm install');
|
|
303
|
+
console.log(' castle-web serve . # & in your shell to background it');
|
|
304
304
|
}
|