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 CHANGED
@@ -1,9 +1,9 @@
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";
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 = "basic-2d";
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 = "0.4.4";
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
- ? `file:${sdkPathPosix}`
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, "CLAUDE.md");
69
+ const upstream = path.join(repoRoot, 'CLAUDE.md');
82
70
  try {
83
- return fs.readFileSync(upstream, "utf8").trimEnd() + "\n";
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, "CLAUDE.md");
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("CLAUDE.md", agentsPath);
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: "module",
98
+ type: 'module',
113
99
  scripts: {
114
100
  restart: `${cliCommand} restart .`,
115
101
  screenshot: `${cliCommand} screenshot .`,
116
- "save-deck": `${cliCommand} save-deck .`,
102
+ 'save-deck': `${cliCommand} save-deck .`,
117
103
  },
118
104
  dependencies: {
119
- "castle-web-sdk": sdkRef,
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, "AGENTS.md");
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, "CLAUDE.md")))
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, "index.html"), INDEX_HTML);
138
- fs.writeFileSync(path.join(projectDir, "game.js"), GAME_JS);
139
- fs.writeFileSync(path.join(projectDir, "CLAUDE.md"), makeClaudeMd());
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, "package.json"), JSON.stringify(makePackageJson(projectDir), null, 2) + "\n");
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("Available kits:");
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(" (none)");
145
+ console.error(' (none)');
160
146
  }
161
147
  catch {
162
- console.error(" (none — kits/ directory is missing)");
148
+ console.error(' (none — kits/ directory is missing)');
163
149
  }
164
- console.error("Or use `--kit none` for a bare code-only deck.");
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, "package.json");
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, "utf8"));
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["castle-web-sdk"] === "string" &&
191
- pkg.dependencies["castle-web-sdk"].startsWith("file:")) {
192
- pkg.dependencies["castle-web-sdk"] = sdkRef;
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] !== "string")
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, "castle-web")
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) + "\n");
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, "CLAUDE.md");
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("pnpm --version", { stdio: "ignore" });
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 `pnpm install` is near-instant (hardlinks from the
238
- // store, no download). Fall back to npm when pnpm isn't on PATH (e.g. a local
239
- // laptop that never installed it). --prefer-offline uses the store/cache first
240
- // and only hits the network for anything missing.
241
- function installDeps(projectDir) {
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("Installing deps (pnpm)...");
244
- execSync("pnpm install --prefer-offline", {
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: "inherit",
236
+ stdio: 'inherit',
247
237
  });
248
238
  }
249
239
  else {
250
- console.log("Installing deps (npm)...");
251
- execSync("npm install --no-audit --no-fund --loglevel=error", {
240
+ console.log('Installing deps (npm)...');
241
+ execSync('npm install --no-audit --no-fund --loglevel=error', {
252
242
  cwd: projectDir,
253
- stdio: "inherit",
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 === "none" || kit === "bare";
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 ? "" : ` (from kit "${kit}")`}`);
272
- // Always install deps so the deck is ready to serve/edit immediately. The
273
- // serve step is what `--no-serve` skips (callers like the cloud launcher run
274
- // their own serve), but they still want the deps in place.
275
- console.log("");
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("dependency install failed; re-run `pnpm install` (or `npm install`) in the deck.");
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 === "1" ||
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: "0.0.0.0", detach: true, open: !noOpen });
295
+ await serve(projectDir, { host: '0.0.0.0', detach: true, open: !noOpen });
296
296
  return;
297
297
  }
298
- console.log("");
299
- console.log("Next steps:");
298
+ console.log('');
299
+ console.log('Next steps:');
300
300
  console.log(` cd ${dir}`);
301
301
  if (!installed)
302
- console.log(" pnpm install # or: npm install");
303
- console.log(" castle-web serve . # & in your shell to background it");
302
+ console.log(' pnpm install # or: npm install');
303
+ console.log(' castle-web serve . # & in your shell to background it');
304
304
  }