castle-web-cli 0.4.106 → 0.4.107

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
@@ -37,29 +37,16 @@ const DEFAULT_KIT = "physics-2d";
37
37
  // globally-installed castle-web (not from inside the workspace). Bumped
38
38
  // alongside cli/sdk version bumps.
39
39
  const PUBLISHED_SDK_VERSION = "0.4.11";
40
- // Never copied into a fresh deck: build/dependency junk. castle.json IS copied
41
- // (the kit ships a config-only one with the editor layout / file filters), but
42
- // `scaffoldFromKit` strips any identity fields off it first -- a fresh deck has
43
- // no deckId until its first save-deck, which owns those fields.
44
40
  // The account the first-party kits are (to be) published under, so a kit
45
41
  // imported from the CLI's copy is named the same as one fetched from the server.
46
42
  const KIT_AUTHOR = "castle";
43
+ // Never copied into a deck's import: build/dependency junk.
47
44
  const KIT_COPY_EXCLUDE = new Set([
48
45
  "node_modules",
49
46
  ".castle",
50
47
  "dist",
51
48
  ".git",
52
49
  ]);
53
- // Identity fields owned by save-deck, never shipped in a kit's config-only
54
- // castle.json. Stripped from a copied castle.json (and absent from the bare
55
- // default) so a fresh deck has no deckId until its first save.
56
- const CASTLE_JSON_IDENTITY_FIELDS = [
57
- "deckId",
58
- "cardId",
59
- "title",
60
- "caption",
61
- "visibility",
62
- ];
63
50
  // The bare (`--kit none`) deck's default editor config: just the Files panel and
64
51
  // a Play panel. No file filtering -- a bare deck is hand-rolled, so the author
65
52
  // knows what's there; show everything for now.
@@ -70,22 +57,6 @@ const BARE_CASTLE_JSON = {
70
57
  visiblePaths: [],
71
58
  },
72
59
  };
73
- // Strip save-deck-owned identity fields from a copied kit castle.json, leaving
74
- // only its config (the editor block). No-op if the kit shipped no castle.json.
75
- function stripCastleJsonIdentity(projectDir) {
76
- const castlePath = path.join(projectDir, "castle.json");
77
- if (!fs.existsSync(castlePath))
78
- return;
79
- try {
80
- const data = JSON.parse(fs.readFileSync(castlePath, "utf8"));
81
- for (const field of CASTLE_JSON_IDENTITY_FIELDS)
82
- delete data[field];
83
- fs.writeFileSync(castlePath, JSON.stringify(data, null, 2) + "\n");
84
- }
85
- catch {
86
- // kit shipped an unparseable castle.json -- leave it for the user to fix
87
- }
88
- }
89
60
  // Resolve how a scaffolded deck should reference the sdk + cli. Both the bare
90
61
  // and kit scaffold paths go through here so they stay in sync.
91
62
  // workspace mode (sdk/ sits next to cli/, i.e. running from a checkout):
@@ -354,7 +325,8 @@ function scaffoldFromKitImport(kit, projectDir) {
354
325
  });
355
326
  const kitConfig = readJsonFile(path.join(kitDir, "castle.json")) ?? {};
356
327
  const kitPkg = readJsonFile(path.join(kitDir, "package.json")) ?? {};
357
- writeDeckIndexHtml(projectDir, alias, String(kitConfig.title ?? kit));
328
+ const title = typeof kitConfig.title === "string" ? kitConfig.title : kit;
329
+ writeDeckIndexHtml(projectDir, alias, title);
358
330
  writeStarterScene(projectDir, kitDir, alias);
359
331
  // The deck's own castle.json: the kit's editor config (panel layout, file
360
332
  // filters) as a starting point -- it is the deck's to edit from here -- plus
@@ -385,91 +357,6 @@ function scaffoldFromKitImport(kit, projectDir) {
385
357
  appendCommonInstructions(projectDir);
386
358
  lockImportTree(importDir);
387
359
  }
388
- function scaffoldFromKit(kit, projectDir) {
389
- const kitDir = path.join(getKitsDir(), kit);
390
- if (!fs.existsSync(kitDir) || !fs.statSync(kitDir).isDirectory()) {
391
- console.error(`Kit "${kit}" not found at ${kitDir}.`);
392
- console.error("Available kits:");
393
- try {
394
- const kits = fs
395
- .readdirSync(getKitsDir())
396
- .filter((name) => fs.statSync(path.join(getKitsDir(), name)).isDirectory());
397
- if (kits.length)
398
- for (const name of kits)
399
- console.error(` ${name}`);
400
- else
401
- console.error(" (none)");
402
- }
403
- catch {
404
- console.error(" (none — kits/ directory is missing)");
405
- }
406
- console.error("Or use `--kit none` for a bare code-only deck.");
407
- process.exit(1);
408
- }
409
- fs.cpSync(kitDir, projectDir, {
410
- recursive: true,
411
- // Keep symlinks verbatim so the kit's AGENTS.md -> CLAUDE.md stays a link.
412
- verbatimSymlinks: true,
413
- filter: (src) => src === kitDir || !KIT_COPY_EXCLUDE.has(path.basename(src)),
414
- });
415
- // The kit ships a config-only castle.json (editor layout / file filters); keep
416
- // its config but drop any identity fields so this fresh deck has no deckId
417
- // until its first save-deck.
418
- stripCastleJsonIdentity(projectDir);
419
- // The kit's package.json carries the kit's name; rename it to the deck dir.
420
- // Kit-relative refs to `../../sdk` and `../../cli/dist` only resolve when the
421
- // deck lives at castle-experimental-web/decks/<name>/. Rewrite both to
422
- // absolute paths so the scaffolded deck works anywhere -- including under
423
- // /tmp where macOS's /tmp -> /private/tmp symlink breaks relative-path math.
424
- const pkgPath = path.join(projectDir, "package.json");
425
- if (fs.existsSync(pkgPath)) {
426
- try {
427
- const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
428
- pkg.name = path.basename(projectDir);
429
- // Local-dev paths (`file:../../sdk` / `node ../../cli/dist/index.js`) only
430
- // work when the deck lives inside the castle-experimental-web workspace.
431
- // For a deck scaffolded from a globally-installed castle-web, rewrite to
432
- // the published packages instead. Same workspace-vs-published resolution
433
- // the bare scaffold path uses.
434
- const { workspaceMode, sdkRef, cliDistAbs, sdkPathPosix } = resolveScaffoldRefs();
435
- if (pkg.dependencies &&
436
- typeof pkg.dependencies["castle-web-sdk"] === "string" &&
437
- pkg.dependencies["castle-web-sdk"].startsWith("file:")) {
438
- pkg.dependencies["castle-web-sdk"] = sdkRef;
439
- }
440
- if (pkg.scripts) {
441
- for (const k of Object.keys(pkg.scripts)) {
442
- if (typeof pkg.scripts[k] !== "string")
443
- continue;
444
- if (workspaceMode) {
445
- pkg.scripts[k] = pkg.scripts[k]
446
- .replace(/\.\.\/\.\.\/cli\/dist/g, cliDistAbs)
447
- .replace(/\.\.\/\.\.\/sdk/g, sdkPathPosix);
448
- }
449
- else {
450
- // Globally-installed: route through the `castle-web` binary on PATH.
451
- pkg.scripts[k] = pkg.scripts[k]
452
- .replace(/node\s+\.\.\/\.\.\/cli\/dist\/index\.js/g, "castle-web")
453
- .replace(/await import\((['"])\.\.\/\.\.\/cli\/dist\/bundle\.js\1\)/g, "await import('castle-web-cli/dist/bundle.js')")
454
- .replace(/\.\.\/\.\.\/sdk/g, "");
455
- }
456
- }
457
- }
458
- fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
459
- }
460
- catch {
461
- // kit shipped an unparseable package.json — leave it for the user to fix
462
- }
463
- }
464
- // Every deck needs a CLAUDE.md so coding agents know how castle-web works.
465
- // Keep the kit's own if it ships one; otherwise generate from the upstream.
466
- const claudePath = path.join(projectDir, "CLAUDE.md");
467
- if (!fs.existsSync(claudePath)) {
468
- fs.writeFileSync(claudePath, makeClaudeMd());
469
- }
470
- appendCommonInstructions(projectDir);
471
- ensureAgentsSymlink(projectDir);
472
- }
473
360
  export async function init(dir, opts = {}) {
474
361
  const projectDir = path.resolve(dir);
475
362
  if (fs.existsSync(projectDir) && fs.readdirSync(projectDir).length > 0) {
@@ -30,6 +30,13 @@ export declare function reportCursorRun(opts: {
30
30
  durationMs: number;
31
31
  ok: boolean;
32
32
  }): void;
33
+ /**
34
+ * True when this serve runs inside a Castle sandbox -- i.e. the host injected the
35
+ * llm-proxy pair, which is what makes a run metered and limit-gated in the first
36
+ * place. Outside one there is no Castle budget being spent, so nothing to escape
37
+ * with a credential of your own; the CLIs just use whatever the host machine has.
38
+ */
39
+ export declare function inCastleSandbox(): boolean;
33
40
  /** Mirrors `CastleBudgetResponse` in castle-sandboxes/shared. */
34
41
  export interface CastleBudget {
35
42
  usedMicros: number;
package/dist/metering.js CHANGED
@@ -100,6 +100,15 @@ export function reportCursorRun(opts) {
100
100
  /* best-effort: metering must never surface in a finished run */
101
101
  });
102
102
  }
103
+ /**
104
+ * True when this serve runs inside a Castle sandbox -- i.e. the host injected the
105
+ * llm-proxy pair, which is what makes a run metered and limit-gated in the first
106
+ * place. Outside one there is no Castle budget being spent, so nothing to escape
107
+ * with a credential of your own; the CLIs just use whatever the host machine has.
108
+ */
109
+ export function inCastleSandbox() {
110
+ return Boolean(process.env.CASTLE_LLM_PROXY_URL && process.env.CASTLE_LLM_PROXY_TOKEN);
111
+ }
103
112
  // Path on the proxy that reports this sandbox's daily budget. Must match
104
113
  // CASTLE_BUDGET_PATH in castle-sandboxes/shared/src/index.ts.
105
114
  const CASTLE_BUDGET_PATH = "/castle/budget";
@@ -79,7 +79,7 @@ function launchFailureMessage(err) {
79
79
  return `could not launch chromium: ${message}`;
80
80
  }
81
81
  async function loadPlaywrightReal() {
82
- return (await import("playwright-core"));
82
+ return await import("playwright-core");
83
83
  }
84
84
  // playwright-core's exports map does NOT expose ./cli.js (verified against
85
85
  // 1.61: resolving it throws ERR_PACKAGE_PATH_NOT_EXPORTED), but it DOES
@@ -40,6 +40,18 @@ function resolveInDeck(deckDir, rawPath) {
40
40
  return null;
41
41
  return { abs, rel: rel.split(path.sep).join("/") };
42
42
  }
43
+ function statFile(target) {
44
+ let stat;
45
+ try {
46
+ stat = fs.statSync(target.abs);
47
+ }
48
+ catch {
49
+ return { error: err(`no such file: ${target.rel}`) };
50
+ }
51
+ if (!stat.isFile())
52
+ return { error: err(`not a file: ${target.rel}`) };
53
+ return { stat };
54
+ }
43
55
  // Sorted recursive file walk (deterministic order matters for grep/list_files
44
56
  // results and for tests), skipping IGNORED_DIRS at any depth. Capped as a
45
57
  // safety valve against pathological trees, not as a normal limit -- decks are
@@ -93,15 +105,10 @@ function readFileRun(args, ctx) {
93
105
  const resolved = resolveInDeck(ctx.deckDir, args.path);
94
106
  if (!resolved)
95
107
  return err(`path escapes the deck directory: ${String(args.path)}`);
96
- let stat;
97
- try {
98
- stat = fs.statSync(resolved.abs);
99
- }
100
- catch {
101
- return err(`no such file: ${resolved.rel}`);
102
- }
103
- if (!stat.isFile())
104
- return err(`not a file: ${resolved.rel}`);
108
+ const statted = statFile(resolved);
109
+ if (statted.error)
110
+ return statted.error;
111
+ const stat = statted.stat;
105
112
  const ext = path.extname(resolved.rel).toLowerCase();
106
113
  if (BINARY_READ_EXTS.has(ext)) {
107
114
  return err(`${resolved.rel} is a binary/image file -- it cannot be read as text. Use the view_image tool to look at image files.`);
@@ -167,15 +174,10 @@ function viewImageRun(args, ctx) {
167
174
  if (!mime) {
168
175
  return err(`${resolved.rel} is not a viewable image -- view_image supports png/jpg/jpeg/gif/webp. SVG and other text formats can be read with read_file.`);
169
176
  }
170
- let stat;
171
- try {
172
- stat = fs.statSync(resolved.abs);
173
- }
174
- catch {
175
- return err(`no such file: ${resolved.rel}`);
176
- }
177
- if (!stat.isFile())
178
- return err(`not a file: ${resolved.rel}`);
177
+ const statted = statFile(resolved);
178
+ if (statted.error)
179
+ return statted.error;
180
+ const stat = statted.stat;
179
181
  if (stat.size > VIEW_IMAGE_SIZE_CAP) {
180
182
  return err(`${resolved.rel} is too large to view (${(stat.size / (1024 * 1024)).toFixed(1)}MB > ${VIEW_IMAGE_SIZE_CAP / (1024 * 1024)}MB).`);
181
183
  }
package/dist/save-deck.js CHANGED
@@ -96,11 +96,14 @@ export async function saveDeck(dir, opts = {}) {
96
96
  console.error('Not logged in. Run `castle-web login` first.');
97
97
  process.exit(1);
98
98
  }
99
- const visibility = opts.visibility ?? 'unlisted';
100
- if (visibility !== 'unlisted' && visibility !== 'private') {
101
- console.error(`Invalid visibility "${visibility}". Use unlisted or private.`);
99
+ // `--visibility` arrives as an unchecked string from the command line, so the
100
+ // declared type is no guarantee -- widen to validate what actually came in.
101
+ const requested = opts.visibility ?? 'unlisted';
102
+ if (requested !== 'unlisted' && requested !== 'private') {
103
+ console.error(`Invalid visibility "${requested}". Use unlisted or private.`);
102
104
  process.exit(1);
103
105
  }
106
+ const visibility = requested;
104
107
  const projectDir = path.resolve(dir);
105
108
  if (!fs.existsSync(path.join(projectDir, 'index.html'))) {
106
109
  console.error(`No index.html found in ${projectDir}`);