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/agent.js +64 -33
- package/dist/byo-accounts.d.ts +38 -0
- package/dist/byo-accounts.js +186 -0
- package/dist/byo-auth.d.ts +7 -1
- package/dist/byo-auth.js +198 -9
- package/dist/byo-login.d.ts +14 -0
- package/dist/byo-login.js +196 -0
- package/dist/castleJson.d.ts +15 -0
- package/dist/castleJson.js +24 -0
- package/dist/claude-shim-env.d.ts +1 -0
- package/dist/claude-shim-env.js +6 -0
- package/dist/get-deck.js +1 -11
- package/dist/ide.js +45 -62
- package/dist/imports.js +31 -61
- package/dist/init.js +3 -116
- package/dist/metering.d.ts +7 -0
- package/dist/metering.js +9 -0
- package/dist/native/playtest-browser.js +1 -1
- package/dist/native/tools.js +20 -18
- package/dist/save-deck.js +6 -3
- package/dist/shell/assets/index-B5mHPsrX.js +144 -0
- package/dist/shell/assets/index-wU4ol--C.css +1 -0
- package/dist/shell/index.html +2 -2
- package/package.json +2 -2
- package/dist/shell/assets/index-B-RU1Sa9.js +0 -144
- package/dist/shell/assets/index-B2quTIav.css +0 -1
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
|
-
|
|
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) {
|
package/dist/metering.d.ts
CHANGED
|
@@ -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
|
|
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
|
package/dist/native/tools.js
CHANGED
|
@@ -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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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}`);
|