castle-web-cli 0.4.39 → 0.4.40
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 +65 -78
- 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,24 @@ 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
|
-
//
|
|
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, and
|
|
227
|
+
// the deck's deps are all prebuilt pure JS that don't need them).
|
|
241
228
|
function installDeps(projectDir) {
|
|
242
229
|
if (hasPnpm()) {
|
|
243
|
-
console.log(
|
|
244
|
-
execSync(
|
|
230
|
+
console.log('Installing deps (pnpm)...');
|
|
231
|
+
execSync('pnpm install --prefer-offline --ignore-scripts', {
|
|
245
232
|
cwd: projectDir,
|
|
246
|
-
stdio:
|
|
233
|
+
stdio: 'inherit',
|
|
247
234
|
});
|
|
248
235
|
}
|
|
249
236
|
else {
|
|
250
|
-
console.log(
|
|
251
|
-
execSync(
|
|
237
|
+
console.log('Installing deps (npm)...');
|
|
238
|
+
execSync('npm install --no-audit --no-fund --loglevel=error', {
|
|
252
239
|
cwd: projectDir,
|
|
253
|
-
stdio:
|
|
240
|
+
stdio: 'inherit',
|
|
254
241
|
});
|
|
255
242
|
}
|
|
256
243
|
}
|
|
@@ -261,25 +248,25 @@ export async function init(dir, opts = {}) {
|
|
|
261
248
|
process.exit(1);
|
|
262
249
|
}
|
|
263
250
|
const kit = opts.kit ?? DEFAULT_KIT;
|
|
264
|
-
const bare = kit ===
|
|
251
|
+
const bare = kit === 'none' || kit === 'bare';
|
|
265
252
|
if (bare) {
|
|
266
253
|
scaffoldBare(projectDir);
|
|
267
254
|
}
|
|
268
255
|
else {
|
|
269
256
|
scaffoldFromKit(kit, projectDir);
|
|
270
257
|
}
|
|
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
|
-
console.log(
|
|
258
|
+
console.log(`Created project in ${projectDir}/${bare ? '' : ` (from kit "${kit}")`}`);
|
|
259
|
+
// Always install deps so the deck is ready to serve/edit immediately.
|
|
260
|
+
// `--no-serve` only skips the serve step below (callers like the cloud
|
|
261
|
+
// launcher run their own serve, but still want deps in place).
|
|
262
|
+
console.log('');
|
|
276
263
|
let installed = false;
|
|
277
264
|
try {
|
|
278
265
|
installDeps(projectDir);
|
|
279
266
|
installed = true;
|
|
280
267
|
}
|
|
281
268
|
catch {
|
|
282
|
-
console.error(
|
|
269
|
+
console.error('dependency install failed; re-run `pnpm install` (or `npm install`) in the deck.');
|
|
283
270
|
}
|
|
284
271
|
const autoServe = opts.serve !== false;
|
|
285
272
|
if (autoServe && installed) {
|
|
@@ -289,16 +276,16 @@ export async function init(dir, opts = {}) {
|
|
|
289
276
|
// the served page; users can override host on a subsequent serve call.
|
|
290
277
|
// Open in the user's default browser unless we're clearly headless (SSH
|
|
291
278
|
// session) or the user has opted out via CASTLE_WEB_CLI_NO_OPEN=1.
|
|
292
|
-
const noOpen = process.env.CASTLE_WEB_CLI_NO_OPEN ===
|
|
279
|
+
const noOpen = process.env.CASTLE_WEB_CLI_NO_OPEN === '1' ||
|
|
293
280
|
!!process.env.SSH_CONNECTION ||
|
|
294
281
|
!!process.env.SSH_TTY;
|
|
295
|
-
await serve(projectDir, { host:
|
|
282
|
+
await serve(projectDir, { host: '0.0.0.0', detach: true, open: !noOpen });
|
|
296
283
|
return;
|
|
297
284
|
}
|
|
298
|
-
console.log(
|
|
299
|
-
console.log(
|
|
285
|
+
console.log('');
|
|
286
|
+
console.log('Next steps:');
|
|
300
287
|
console.log(` cd ${dir}`);
|
|
301
288
|
if (!installed)
|
|
302
|
-
console.log(
|
|
303
|
-
console.log(
|
|
289
|
+
console.log(' pnpm install # or: npm install');
|
|
290
|
+
console.log(' castle-web serve . # & in your shell to background it');
|
|
304
291
|
}
|