castle-web-cli 0.4.63 → 0.4.64

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,14 +31,20 @@ 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.6';
38
+ const PUBLISHED_SDK_VERSION = "0.4.6";
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(['node_modules', '.castle', 'dist', '.git', 'castle.json']);
41
+ const KIT_COPY_EXCLUDE = new Set([
42
+ "node_modules",
43
+ ".castle",
44
+ "dist",
45
+ ".git",
46
+ "castle.json",
47
+ ]);
42
48
  // Resolve how a scaffolded deck should reference the sdk + cli. Both the bare
43
49
  // and kit scaffold paths go through here so they stay in sync.
44
50
  // workspace mode (sdk/ sits next to cli/, i.e. running from a checkout):
@@ -52,11 +58,17 @@ function resolveScaffoldRefs() {
52
58
  // npm package and need the published refs + `castle-web` binary.
53
59
  const workspaceMode = fs.existsSync(sdkPath);
54
60
  const sdkPathPosix = workspaceMode ? toPosixPath(sdkPath) : null;
55
- const cliDistAbs = workspaceMode ? toPosixPath(path.dirname(getCliEntryPath())) : null;
61
+ const cliDistAbs = workspaceMode
62
+ ? toPosixPath(path.dirname(getCliEntryPath()))
63
+ : null;
56
64
  return {
57
65
  workspaceMode,
58
- sdkRef: workspaceMode ? `file:${sdkPathPosix}` : `^${PUBLISHED_SDK_VERSION}`,
59
- cliCommand: workspaceMode ? `node ${toPosixPath(getCliEntryPath())}` : 'castle-web',
66
+ sdkRef: workspaceMode
67
+ ? `file:${sdkPathPosix}`
68
+ : `^${PUBLISHED_SDK_VERSION}`,
69
+ cliCommand: workspaceMode
70
+ ? `node ${toPosixPath(getCliEntryPath())}`
71
+ : "castle-web",
60
72
  cliDistAbs,
61
73
  sdkPathPosix,
62
74
  };
@@ -66,9 +78,9 @@ function makeClaudeMd() {
66
78
  // castle-experimental-web checkout, but breaks when the scaffold lives
67
79
  // outside the repo (the relative path no longer resolves).
68
80
  const repoRoot = getRepoRoot();
69
- const upstream = path.join(repoRoot, 'CLAUDE.md');
81
+ const upstream = path.join(repoRoot, "CLAUDE.md");
70
82
  try {
71
- return fs.readFileSync(upstream, 'utf8').trimEnd() + '\n';
83
+ return fs.readFileSync(upstream, "utf8").trimEnd() + "\n";
72
84
  }
73
85
  catch {
74
86
  return `# Castle Experimental Web\n\nSee https://github.com/castle-xyz/castle-experimental-web for the agent guide.\n`;
@@ -78,13 +90,15 @@ function makeClaudeMd() {
78
90
  // the kit's (or bare) CLAUDE.md is written; the AGENTS.md symlink picks the
79
91
  // appended content up for free.
80
92
  function appendCommonInstructions(projectDir) {
81
- const claudePath = path.join(projectDir, 'CLAUDE.md');
82
- const existing = fs.existsSync(claudePath) ? fs.readFileSync(claudePath, 'utf8').trimEnd() + '\n\n' : '';
93
+ const claudePath = path.join(projectDir, "CLAUDE.md");
94
+ const existing = fs.existsSync(claudePath)
95
+ ? fs.readFileSync(claudePath, "utf8").trimEnd() + "\n\n"
96
+ : "";
83
97
  fs.writeFileSync(claudePath, existing + COMMON_INSTRUCTIONS);
84
98
  }
85
99
  function tryMakeAgentsSymlink(agentsPath) {
86
100
  try {
87
- fs.symlinkSync('CLAUDE.md', agentsPath);
101
+ fs.symlinkSync("CLAUDE.md", agentsPath);
88
102
  }
89
103
  catch {
90
104
  // symlink already exists / unsupported FS — non-fatal
@@ -95,37 +109,37 @@ function makePackageJson(projectDir) {
95
109
  return {
96
110
  name: path.basename(projectDir),
97
111
  private: true,
98
- type: 'module',
112
+ type: "module",
99
113
  scripts: {
100
114
  restart: `${cliCommand} restart .`,
101
115
  screenshot: `${cliCommand} screenshot .`,
102
- 'save-deck': `${cliCommand} save-deck .`,
116
+ "save-deck": `${cliCommand} save-deck .`,
103
117
  },
104
118
  dependencies: {
105
- 'castle-web-sdk': sdkRef,
119
+ "castle-web-sdk": sdkRef,
106
120
  },
107
121
  };
108
122
  }
109
123
  // Some coding agents read AGENTS.md by convention. Symlink so they get the
110
124
  // same guidance without a duplicate copy.
111
125
  function ensureAgentsSymlink(projectDir) {
112
- const agentsPath = path.join(projectDir, 'AGENTS.md');
126
+ const agentsPath = path.join(projectDir, "AGENTS.md");
113
127
  if (fs.lstatSync(agentsPath, { throwIfNoEntry: false }))
114
128
  return;
115
129
  // Don't create a dangling link — only symlink when CLAUDE.md is present.
116
- if (!fs.existsSync(path.join(projectDir, 'CLAUDE.md')))
130
+ if (!fs.existsSync(path.join(projectDir, "CLAUDE.md")))
117
131
  return;
118
132
  tryMakeAgentsSymlink(agentsPath);
119
133
  }
120
134
  // Bare scaffold: a plain code-only deck with no kit framework.
121
135
  function scaffoldBare(projectDir) {
122
136
  fs.mkdirSync(projectDir, { recursive: true });
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());
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());
126
140
  appendCommonInstructions(projectDir);
127
141
  ensureAgentsSymlink(projectDir);
128
- fs.writeFileSync(path.join(projectDir, 'package.json'), JSON.stringify(makePackageJson(projectDir), null, 2) + '\n');
142
+ fs.writeFileSync(path.join(projectDir, "package.json"), JSON.stringify(makePackageJson(projectDir), null, 2) + "\n");
129
143
  }
130
144
  // Copy a framework kit from kits/<kit>/ into the new deck dir, dropping
131
145
  // build/dependency junk and castle.json.
@@ -133,7 +147,7 @@ function scaffoldFromKit(kit, projectDir) {
133
147
  const kitDir = path.join(getKitsDir(), kit);
134
148
  if (!fs.existsSync(kitDir) || !fs.statSync(kitDir).isDirectory()) {
135
149
  console.error(`Kit "${kit}" not found at ${kitDir}.`);
136
- console.error('Available kits:');
150
+ console.error("Available kits:");
137
151
  try {
138
152
  const kits = fs
139
153
  .readdirSync(getKitsDir())
@@ -142,12 +156,12 @@ function scaffoldFromKit(kit, projectDir) {
142
156
  for (const name of kits)
143
157
  console.error(` ${name}`);
144
158
  else
145
- console.error(' (none)');
159
+ console.error(" (none)");
146
160
  }
147
161
  catch {
148
- console.error(' (none — kits/ directory is missing)');
162
+ console.error(" (none — kits/ directory is missing)");
149
163
  }
150
- console.error('Or use `--kit none` for a bare code-only deck.');
164
+ console.error("Or use `--kit none` for a bare code-only deck.");
151
165
  process.exit(1);
152
166
  }
153
167
  fs.cpSync(kitDir, projectDir, {
@@ -161,10 +175,10 @@ function scaffoldFromKit(kit, projectDir) {
161
175
  // deck lives at castle-experimental-web/decks/<name>/. Rewrite both to
162
176
  // absolute paths so the scaffolded deck works anywhere -- including under
163
177
  // /tmp where macOS's /tmp -> /private/tmp symlink breaks relative-path math.
164
- const pkgPath = path.join(projectDir, 'package.json');
178
+ const pkgPath = path.join(projectDir, "package.json");
165
179
  if (fs.existsSync(pkgPath)) {
166
180
  try {
167
- const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
181
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
168
182
  pkg.name = path.basename(projectDir);
169
183
  // Local-dev paths (`file:../../sdk` / `node ../../cli/dist/index.js`) only
170
184
  // work when the deck lives inside the castle-experimental-web workspace.
@@ -173,13 +187,13 @@ function scaffoldFromKit(kit, projectDir) {
173
187
  // the bare scaffold path uses.
174
188
  const { workspaceMode, sdkRef, cliDistAbs, sdkPathPosix } = resolveScaffoldRefs();
175
189
  if (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;
190
+ typeof pkg.dependencies["castle-web-sdk"] === "string" &&
191
+ pkg.dependencies["castle-web-sdk"].startsWith("file:")) {
192
+ pkg.dependencies["castle-web-sdk"] = sdkRef;
179
193
  }
180
194
  if (pkg.scripts) {
181
195
  for (const k of Object.keys(pkg.scripts)) {
182
- if (typeof pkg.scripts[k] !== 'string')
196
+ if (typeof pkg.scripts[k] !== "string")
183
197
  continue;
184
198
  if (workspaceMode) {
185
199
  pkg.scripts[k] = pkg.scripts[k]
@@ -189,13 +203,13 @@ function scaffoldFromKit(kit, projectDir) {
189
203
  else {
190
204
  // Globally-installed: route through the `castle-web` binary on PATH.
191
205
  pkg.scripts[k] = pkg.scripts[k]
192
- .replace(/node\s+\.\.\/\.\.\/cli\/dist\/index\.js/g, 'castle-web')
206
+ .replace(/node\s+\.\.\/\.\.\/cli\/dist\/index\.js/g, "castle-web")
193
207
  .replace(/await import\((['"])\.\.\/\.\.\/cli\/dist\/bundle\.js\1\)/g, "await import('castle-web-cli/dist/bundle.js')")
194
- .replace(/\.\.\/\.\.\/sdk/g, '');
208
+ .replace(/\.\.\/\.\.\/sdk/g, "");
195
209
  }
196
210
  }
197
211
  }
198
- fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
212
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
199
213
  }
200
214
  catch {
201
215
  // kit shipped an unparseable package.json — leave it for the user to fix
@@ -203,7 +217,7 @@ function scaffoldFromKit(kit, projectDir) {
203
217
  }
204
218
  // Every deck needs a CLAUDE.md so coding agents know how castle-web works.
205
219
  // Keep the kit's own if it ships one; otherwise generate from the upstream.
206
- const claudePath = path.join(projectDir, 'CLAUDE.md');
220
+ const claudePath = path.join(projectDir, "CLAUDE.md");
207
221
  if (!fs.existsSync(claudePath)) {
208
222
  fs.writeFileSync(claudePath, makeClaudeMd());
209
223
  }
@@ -212,7 +226,7 @@ function scaffoldFromKit(kit, projectDir) {
212
226
  }
213
227
  function hasPnpm() {
214
228
  try {
215
- execSync('pnpm --version', { stdio: 'ignore' });
229
+ execSync("pnpm --version", { stdio: "ignore" });
216
230
  return true;
217
231
  }
218
232
  catch {
@@ -229,18 +243,34 @@ function hasPnpm() {
229
243
  // fast + deterministic); used in published mode where the kit lockfile matches.
230
244
  function installDeps(projectDir, frozen) {
231
245
  if (hasPnpm()) {
232
- console.log('Installing deps (pnpm)...');
233
- const frozenFlag = frozen ? '--frozen-lockfile ' : '';
234
- execSync(`pnpm install ${frozenFlag}--prefer-offline --ignore-scripts`, {
235
- cwd: projectDir,
236
- stdio: 'inherit',
237
- });
246
+ console.log("Installing deps (pnpm)...");
247
+ const frozenFlag = frozen ? "--frozen-lockfile " : "";
248
+ try {
249
+ execSync(`pnpm install ${frozenFlag}--prefer-offline --ignore-scripts`, {
250
+ cwd: projectDir,
251
+ stdio: "inherit",
252
+ });
253
+ }
254
+ catch (err) {
255
+ if (!frozen)
256
+ throw err;
257
+ // A frozen install hard-fails when the shipped kit lockfile has drifted
258
+ // from the scaffold's package.json (e.g. the sdk version bumped but the
259
+ // kit's pnpm-lock.yaml wasn't regenerated). Rather than leave the deck
260
+ // with no node_modules, retry with resolution allowed -- a little slower
261
+ // (re-resolves + may fetch), but self-heals the drift instead of breaking.
262
+ console.warn("Frozen install failed; retrying with resolution (kit lockfile drift?)...");
263
+ execSync("pnpm install --prefer-offline --ignore-scripts", {
264
+ cwd: projectDir,
265
+ stdio: "inherit",
266
+ });
267
+ }
238
268
  }
239
269
  else {
240
- console.log('Installing deps (npm)...');
241
- execSync('npm install --no-audit --no-fund --loglevel=error', {
270
+ console.log("Installing deps (npm)...");
271
+ execSync("npm install --no-audit --no-fund --loglevel=error", {
242
272
  cwd: projectDir,
243
- stdio: 'inherit',
273
+ stdio: "inherit",
244
274
  });
245
275
  }
246
276
  }
@@ -251,14 +281,14 @@ export async function init(dir, opts = {}) {
251
281
  process.exit(1);
252
282
  }
253
283
  const kit = opts.kit ?? DEFAULT_KIT;
254
- const bare = kit === 'none' || kit === 'bare';
284
+ const bare = kit === "none" || kit === "bare";
255
285
  if (bare) {
256
286
  scaffoldBare(projectDir);
257
287
  }
258
288
  else {
259
289
  scaffoldFromKit(kit, projectDir);
260
290
  }
261
- console.log(`Created project in ${projectDir}/${bare ? '' : ` (from kit "${kit}")`}`);
291
+ console.log(`Created project in ${projectDir}/${bare ? "" : ` (from kit "${kit}")`}`);
262
292
  // Always install deps so the deck is ready to serve/edit immediately.
263
293
  // `--no-serve` only skips the serve step below (callers like the cloud
264
294
  // launcher run their own serve, but still want deps in place).
@@ -268,18 +298,18 @@ export async function init(dir, opts = {}) {
268
298
  // decks rewrite the sdk to file:../../sdk, which the shipped (published)
269
299
  // lockfile won't match -> drop it and let pnpm resolve. Bare decks have no
270
300
  // lockfile -> non-frozen.
271
- const lockPath = path.join(projectDir, 'pnpm-lock.yaml');
301
+ const lockPath = path.join(projectDir, "pnpm-lock.yaml");
272
302
  if (resolveScaffoldRefs().workspaceMode)
273
303
  fs.rmSync(lockPath, { force: true });
274
304
  const frozen = fs.existsSync(lockPath);
275
- console.log('');
305
+ console.log("");
276
306
  let installed = false;
277
307
  try {
278
308
  installDeps(projectDir, frozen);
279
309
  installed = true;
280
310
  }
281
311
  catch {
282
- console.error('dependency install failed; re-run `pnpm install` (or `npm install`) in the deck.');
312
+ console.error("dependency install failed; re-run `pnpm install` (or `npm install`) in the deck.");
283
313
  }
284
314
  const autoServe = opts.serve !== false;
285
315
  if (autoServe && installed) {
@@ -289,16 +319,16 @@ export async function init(dir, opts = {}) {
289
319
  // the served page; users can override host on a subsequent serve call.
290
320
  // Open in the user's default browser unless we're clearly headless (SSH
291
321
  // 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' ||
322
+ const noOpen = process.env.CASTLE_WEB_CLI_NO_OPEN === "1" ||
293
323
  !!process.env.SSH_CONNECTION ||
294
324
  !!process.env.SSH_TTY;
295
- await serve(projectDir, { host: '0.0.0.0', detach: true, open: !noOpen });
325
+ await serve(projectDir, { host: "0.0.0.0", detach: true, open: !noOpen });
296
326
  return;
297
327
  }
298
- console.log('');
299
- console.log('Next steps:');
328
+ console.log("");
329
+ console.log("Next steps:");
300
330
  console.log(` cd ${dir}`);
301
331
  if (!installed)
302
- console.log(' pnpm install # or: npm install');
303
- console.log(' castle-web serve . # & in your shell to background it');
332
+ console.log(" pnpm install # or: npm install");
333
+ console.log(" castle-web serve . # & in your shell to background it");
304
334
  }
@@ -30,8 +30,8 @@ importers:
30
30
  specifier: ^1.2.3
31
31
  version: 1.2.3
32
32
  castle-web-sdk:
33
- specifier: ^0.4.4
34
- version: 0.4.4
33
+ specifier: ^0.4.6
34
+ version: 0.4.6
35
35
  codemirror:
36
36
  specifier: ^6.0.2
37
37
  version: 6.0.2
@@ -291,8 +291,8 @@ packages:
291
291
  resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
292
292
  engines: {node: '>=6'}
293
293
 
294
- castle-web-sdk@0.4.4:
295
- resolution: {integrity: sha512-s69tJO1mYjiwbEQsaQgXvkr0wvOxIuxskbILWy17loRaagCQJg/4dbOXH5L+XGY8w7pHCOiW1IwmxdpbA1c/cQ==}
294
+ castle-web-sdk@0.4.6:
295
+ resolution: {integrity: sha512-b0su+RXWYh1/A4TTbKDt3PeZ4Is5WG/vSbQ0eTEm2UpNWffmJu6k4xQT/WUCBXju3L8azaRRAG1sQsFJ7f74jA==}
296
296
 
297
297
  chalk@4.1.2:
298
298
  resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
@@ -1149,7 +1149,7 @@ snapshots:
1149
1149
 
1150
1150
  callsites@3.1.0: {}
1151
1151
 
1152
- castle-web-sdk@0.4.4: {}
1152
+ castle-web-sdk@0.4.6: {}
1153
1153
 
1154
1154
  chalk@4.1.2:
1155
1155
  dependencies:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "castle-web-cli",
3
- "version": "0.4.63",
3
+ "version": "0.4.64",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "castle-web": "./dist/index.js"