castle-web-cli 0.4.106 → 0.4.108
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 +118 -33
- package/dist/byo-accounts.d.ts +55 -0
- package/dist/byo-accounts.js +240 -0
- package/dist/byo-auth.d.ts +13 -1
- package/dist/byo-auth.js +336 -20
- 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/cli-shim-env.d.ts +1 -0
- package/dist/cli-shim-env.js +13 -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-BU9_JpPe.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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Printed for the editor terminal's shims to eval: the environment this
|
|
2
|
+
// particular run of this particular CLI should have, plus the real binary to
|
|
3
|
+
// exec (see installCliShims in byo-auth.ts for why the decision can't live in
|
|
4
|
+
// the shell's own environment).
|
|
5
|
+
//
|
|
6
|
+
// argv is `<shimDir> <cli> [the run's own arguments...]`.
|
|
7
|
+
import { ensureClaudeOnboarded, shimEnvScript } from "./byo-auth.js";
|
|
8
|
+
const [shimDir = "", cli = "", ...args] = process.argv.slice(2);
|
|
9
|
+
// Its own call rather than a side effect of building the script: this WRITES,
|
|
10
|
+
// and shimEnvScript is read-only by contract.
|
|
11
|
+
if (cli === "claude")
|
|
12
|
+
ensureClaudeOnboarded();
|
|
13
|
+
process.stdout.write(shimEnvScript(cli, process.env, shimDir, args));
|
package/dist/get-deck.js
CHANGED
|
@@ -5,6 +5,7 @@ import { nanoid } from 'nanoid';
|
|
|
5
5
|
import * as api from './api.js';
|
|
6
6
|
import { normalizeDeckPackageJson } from './normalize.js';
|
|
7
7
|
import { archiveSource, runTar, SOURCE_ARCHIVE_EXCLUDES } from './save-deck.js';
|
|
8
|
+
import { readCastleJson } from './castleJson.js';
|
|
8
9
|
// Refreshing a deck already in the target replaces its source, so a file deleted
|
|
9
10
|
// upstream actually disappears here -- untarring over the old tree would leave it
|
|
10
11
|
// behind. What survives is exactly what the source archive doesn't carry, and it
|
|
@@ -12,17 +13,6 @@ import { archiveSource, runTar, SOURCE_ARCHIVE_EXCLUDES } from './save-deck.js';
|
|
|
12
13
|
// below (which is that same archive), so deleting one destroys it outright. They're
|
|
13
14
|
// all machine-local anyway -- deps, build output, .castle runtime state, .git.
|
|
14
15
|
const KEEP = SOURCE_ARCHIVE_EXCLUDES;
|
|
15
|
-
function readCastleJson(dir) {
|
|
16
|
-
const p = path.join(dir, 'castle.json');
|
|
17
|
-
if (!fs.existsSync(p))
|
|
18
|
-
return null;
|
|
19
|
-
try {
|
|
20
|
-
return JSON.parse(fs.readFileSync(p, 'utf-8'));
|
|
21
|
-
}
|
|
22
|
-
catch {
|
|
23
|
-
return null;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
16
|
// Whether the target already holds a deck, as opposed to being new, empty, or
|
|
27
17
|
// holding only the dirs a refresh preserves.
|
|
28
18
|
function hasSource(dir) {
|
package/dist/ide.js
CHANGED
|
@@ -15,7 +15,7 @@ import headlessPkg from "@xterm/headless";
|
|
|
15
15
|
import { SerializeAddon } from "@xterm/addon-serialize";
|
|
16
16
|
import { WebSocketServer } from "ws";
|
|
17
17
|
import { IMPORTS_DIR, importStatuses, updateImport } from "./imports.js";
|
|
18
|
-
import { envForUserShell } from "./byo-auth.js";
|
|
18
|
+
import { envForUserShell, installCliShims } from "./byo-auth.js";
|
|
19
19
|
const HeadlessTerminal = headlessPkg.Terminal;
|
|
20
20
|
const DIST_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
21
21
|
// The bundled shell app (vite build output). `/` serves its index.html and
|
|
@@ -315,7 +315,7 @@ function readRequestBody(req) {
|
|
|
315
315
|
req.on("error", reject);
|
|
316
316
|
});
|
|
317
317
|
}
|
|
318
|
-
function
|
|
318
|
+
function withJsonBody(req, res, handler) {
|
|
319
319
|
void (async () => {
|
|
320
320
|
let body;
|
|
321
321
|
try {
|
|
@@ -324,24 +324,37 @@ function handleFilesWrite(deckDir, req, res) {
|
|
|
324
324
|
catch {
|
|
325
325
|
return sendJson(res, 400, { error: "Invalid JSON body." });
|
|
326
326
|
}
|
|
327
|
+
handler(body);
|
|
328
|
+
})();
|
|
329
|
+
}
|
|
330
|
+
// The shape every single-path mutation handler starts with: parse the body,
|
|
331
|
+
// resolve `path` against the deck, and reject anything outside it.
|
|
332
|
+
function withMutationPath(deckDir, req, res, handler) {
|
|
333
|
+
withJsonBody(req, res, (body) => {
|
|
327
334
|
const resolved = resolveDeckPath(deckDir, body.path, { mutation: true });
|
|
328
335
|
if (!resolved.ok)
|
|
329
336
|
return sendJson(res, 400, { error: resolved.error });
|
|
337
|
+
handler(resolved, body);
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
function sendFailure(res, action, rel, err) {
|
|
341
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
342
|
+
sendJson(res, 500, { error: `Could not ${action} ${rel}: ${message}` });
|
|
343
|
+
}
|
|
344
|
+
function handleFilesWrite(deckDir, req, res) {
|
|
345
|
+
withMutationPath(deckDir, req, res, (target, body) => {
|
|
330
346
|
if (typeof body.contents !== "string") {
|
|
331
347
|
return sendJson(res, 400, { error: "File contents must be a string." });
|
|
332
348
|
}
|
|
333
349
|
try {
|
|
334
|
-
fs.mkdirSync(path.dirname(
|
|
335
|
-
fs.writeFileSync(
|
|
336
|
-
sendJson(res, 200, { ok: true, path:
|
|
350
|
+
fs.mkdirSync(path.dirname(target.abs), { recursive: true });
|
|
351
|
+
fs.writeFileSync(target.abs, body.contents, "utf8");
|
|
352
|
+
sendJson(res, 200, { ok: true, path: target.rel });
|
|
337
353
|
}
|
|
338
354
|
catch (err) {
|
|
339
|
-
|
|
340
|
-
sendJson(res, 500, {
|
|
341
|
-
error: `Could not write ${resolved.rel}: ${message}`,
|
|
342
|
-
});
|
|
355
|
+
sendFailure(res, "write", target.rel, err);
|
|
343
356
|
}
|
|
344
|
-
})
|
|
357
|
+
});
|
|
345
358
|
}
|
|
346
359
|
// Add `<rel>/**` to the deck's editor.visiblePaths so files created in a
|
|
347
360
|
// newly-made folder show in the curated Files tree. Only touches a deck that is
|
|
@@ -378,29 +391,16 @@ function ensureVisiblePath(deckDir, rel) {
|
|
|
378
391
|
}
|
|
379
392
|
}
|
|
380
393
|
function handleFilesMkdir(deckDir, req, res) {
|
|
381
|
-
|
|
382
|
-
let body;
|
|
394
|
+
withMutationPath(deckDir, req, res, (target) => {
|
|
383
395
|
try {
|
|
384
|
-
|
|
385
|
-
}
|
|
386
|
-
catch {
|
|
387
|
-
return sendJson(res, 400, { error: "Invalid JSON body." });
|
|
388
|
-
}
|
|
389
|
-
const resolved = resolveDeckPath(deckDir, body.path, { mutation: true });
|
|
390
|
-
if (!resolved.ok)
|
|
391
|
-
return sendJson(res, 400, { error: resolved.error });
|
|
392
|
-
try {
|
|
393
|
-
fs.mkdirSync(resolved.abs, { recursive: true });
|
|
396
|
+
fs.mkdirSync(target.abs, { recursive: true });
|
|
394
397
|
}
|
|
395
398
|
catch (err) {
|
|
396
|
-
|
|
397
|
-
return sendJson(res, 500, {
|
|
398
|
-
error: `Could not create folder ${resolved.rel}: ${message}`,
|
|
399
|
-
});
|
|
399
|
+
return sendFailure(res, "create folder", target.rel, err);
|
|
400
400
|
}
|
|
401
|
-
const visiblePathAdded = ensureVisiblePath(deckDir,
|
|
402
|
-
sendJson(res, 200, { ok: true, path:
|
|
403
|
-
})
|
|
401
|
+
const visiblePathAdded = ensureVisiblePath(deckDir, target.rel);
|
|
402
|
+
sendJson(res, 200, { ok: true, path: target.rel, visiblePathAdded });
|
|
403
|
+
});
|
|
404
404
|
}
|
|
405
405
|
// True when two paths resolve to the same underlying file (same inode+device) --
|
|
406
406
|
// e.g. the source and target of a case-only rename on a case-insensitive FS.
|
|
@@ -415,14 +415,7 @@ function isSameFile(a, b) {
|
|
|
415
415
|
}
|
|
416
416
|
}
|
|
417
417
|
function handleFilesRename(deckDir, req, res) {
|
|
418
|
-
|
|
419
|
-
let body;
|
|
420
|
-
try {
|
|
421
|
-
body = JSON.parse(await readRequestBody(req));
|
|
422
|
-
}
|
|
423
|
-
catch {
|
|
424
|
-
return sendJson(res, 400, { error: "Invalid JSON body." });
|
|
425
|
-
}
|
|
418
|
+
withJsonBody(req, res, (body) => {
|
|
426
419
|
const from = resolveDeckPath(deckDir, body.from, { mutation: true });
|
|
427
420
|
if (!from.ok)
|
|
428
421
|
return sendJson(res, 400, { error: from.error });
|
|
@@ -447,37 +440,23 @@ function handleFilesRename(deckDir, req, res) {
|
|
|
447
440
|
sendJson(res, 200, { ok: true, path: to.rel });
|
|
448
441
|
}
|
|
449
442
|
catch (err) {
|
|
450
|
-
|
|
451
|
-
sendJson(res, 500, { error: `Could not rename ${from.rel}: ${message}` });
|
|
443
|
+
sendFailure(res, "rename", from.rel, err);
|
|
452
444
|
}
|
|
453
|
-
})
|
|
445
|
+
});
|
|
454
446
|
}
|
|
455
447
|
function handleFilesDelete(deckDir, req, res) {
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
body = JSON.parse(await readRequestBody(req));
|
|
460
|
-
}
|
|
461
|
-
catch {
|
|
462
|
-
return sendJson(res, 400, { error: "Invalid JSON body." });
|
|
463
|
-
}
|
|
464
|
-
const resolved = resolveDeckPath(deckDir, body.path, { mutation: true });
|
|
465
|
-
if (!resolved.ok)
|
|
466
|
-
return sendJson(res, 400, { error: resolved.error });
|
|
467
|
-
if (!fs.existsSync(resolved.abs)) {
|
|
468
|
-
return sendJson(res, 404, { error: `Not found: ${resolved.rel}` });
|
|
448
|
+
withMutationPath(deckDir, req, res, (target) => {
|
|
449
|
+
if (!fs.existsSync(target.abs)) {
|
|
450
|
+
return sendJson(res, 404, { error: `Not found: ${target.rel}` });
|
|
469
451
|
}
|
|
470
452
|
try {
|
|
471
|
-
fs.rmSync(
|
|
472
|
-
sendJson(res, 200, { ok: true, path:
|
|
453
|
+
fs.rmSync(target.abs, { recursive: true, force: true });
|
|
454
|
+
sendJson(res, 200, { ok: true, path: target.rel });
|
|
473
455
|
}
|
|
474
456
|
catch (err) {
|
|
475
|
-
|
|
476
|
-
sendJson(res, 500, {
|
|
477
|
-
error: `Could not delete ${resolved.rel}: ${message}`,
|
|
478
|
-
});
|
|
457
|
+
sendFailure(res, "delete", target.rel, err);
|
|
479
458
|
}
|
|
480
|
-
})
|
|
459
|
+
});
|
|
481
460
|
}
|
|
482
461
|
// The builtin Files + code-editor backend: list / read / write deck files and
|
|
483
462
|
// report kit-owned editor extensions. Paths are deck-relative; resolveDeckPath
|
|
@@ -611,7 +590,7 @@ function defaultShell() {
|
|
|
611
590
|
}) ?? "/bin/sh";
|
|
612
591
|
return { command, args: ["-l"] };
|
|
613
592
|
}
|
|
614
|
-
function ptyEnv() {
|
|
593
|
+
function ptyEnv(shimDir) {
|
|
615
594
|
const env = {
|
|
616
595
|
...envForUserShell(process.env),
|
|
617
596
|
TERM: PTY_TERM,
|
|
@@ -623,6 +602,9 @@ function ptyEnv() {
|
|
|
623
602
|
};
|
|
624
603
|
delete env.NO_COLOR;
|
|
625
604
|
delete env.NODE_DISABLE_COLORS;
|
|
605
|
+
if (shimDir) {
|
|
606
|
+
env.PATH = env.PATH ? `${shimDir}${path.delimiter}${env.PATH}` : shimDir;
|
|
607
|
+
}
|
|
626
608
|
return env;
|
|
627
609
|
}
|
|
628
610
|
function clampSize(value, fallback) {
|
|
@@ -669,6 +651,7 @@ export function createIdeServer(opts) {
|
|
|
669
651
|
let session = null;
|
|
670
652
|
function spawnSession() {
|
|
671
653
|
const { command, args } = defaultShell();
|
|
654
|
+
const shimDir = installCliShims(deckDir);
|
|
672
655
|
const screen = new HeadlessTerminal({
|
|
673
656
|
allowProposedApi: true,
|
|
674
657
|
cols: INITIAL_COLS,
|
|
@@ -683,7 +666,7 @@ export function createIdeServer(opts) {
|
|
|
683
666
|
cols: INITIAL_COLS,
|
|
684
667
|
rows: INITIAL_ROWS,
|
|
685
668
|
cwd: deckDir,
|
|
686
|
-
env: ptyEnv(),
|
|
669
|
+
env: ptyEnv(shimDir),
|
|
687
670
|
});
|
|
688
671
|
const s = {
|
|
689
672
|
pty,
|
package/dist/imports.js
CHANGED
|
@@ -5,6 +5,7 @@ import * as api from './api.js';
|
|
|
5
5
|
import { runTar } from './save-deck.js';
|
|
6
6
|
import { normalizeDeckPackageJson } from './normalize.js';
|
|
7
7
|
import { getKitsDir } from './localPaths.js';
|
|
8
|
+
import { readCastleJsonOrThrow as readCastleJson, } from './castleJson.js';
|
|
8
9
|
// Adding another deck as a dependency (`castle-web add-import`; removing one is a
|
|
9
10
|
// later command). Deliberately NOT `get-deck`: that one
|
|
10
11
|
// replaces THIS deck's own source from the server (and carries guards for the
|
|
@@ -24,17 +25,6 @@ import { getKitsDir } from './localPaths.js';
|
|
|
24
25
|
// node_modules gets. That also means `get-deck` leaves an existing `imports/`
|
|
25
26
|
// alone when it refreshes a deck.
|
|
26
27
|
export const IMPORTS_DIR = 'imports';
|
|
27
|
-
function readCastleJson(dir) {
|
|
28
|
-
const p = path.join(dir, 'castle.json');
|
|
29
|
-
if (!fs.existsSync(p))
|
|
30
|
-
return null;
|
|
31
|
-
try {
|
|
32
|
-
return JSON.parse(fs.readFileSync(p, 'utf-8'));
|
|
33
|
-
}
|
|
34
|
-
catch (e) {
|
|
35
|
-
throw new Error(`Could not read ${p}: ${e instanceof Error ? e.message : String(e)}`);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
28
|
// Only the `imports` key is ours; everything else in castle.json (deck identity,
|
|
39
29
|
// editor config) is preserved as-is.
|
|
40
30
|
function writeImportPin(dir, alias, pin) {
|
|
@@ -88,47 +78,24 @@ function qualifiedAlias(meta, deckId) {
|
|
|
88
78
|
//
|
|
89
79
|
// The cost is that `imports/<alias>` can't be deleted until it's unlocked, which
|
|
90
80
|
// is why every path in this file that replaces a dependency unlocks it first.
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
95
|
-
}
|
|
96
|
-
catch {
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
for (const entry of entries) {
|
|
100
|
-
const child = path.join(dir, entry.name);
|
|
101
|
-
if (entry.isDirectory())
|
|
102
|
-
chmodTree(child, fileMode, dirMode);
|
|
103
|
-
else {
|
|
104
|
-
try {
|
|
105
|
-
fs.chmodSync(child, fileMode);
|
|
106
|
-
}
|
|
107
|
-
catch {
|
|
108
|
-
/* a file we can't chmod is not worth failing the import over */
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
// The directory goes last: its contents have to be reachable while we walk it.
|
|
81
|
+
// A file we can't chmod is not worth failing the import over, so every chmod
|
|
82
|
+
// here is best-effort.
|
|
83
|
+
function chmod(target, mode) {
|
|
113
84
|
try {
|
|
114
|
-
fs.chmodSync(
|
|
85
|
+
fs.chmodSync(target, mode);
|
|
86
|
+
return true;
|
|
115
87
|
}
|
|
116
88
|
catch {
|
|
117
|
-
|
|
89
|
+
return false;
|
|
118
90
|
}
|
|
119
91
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
//
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
try {
|
|
127
|
-
fs.chmodSync(dir, 0o755);
|
|
128
|
-
}
|
|
129
|
-
catch {
|
|
92
|
+
// `dirFirst` is the one thing that differs between locking and unlocking, and it
|
|
93
|
+
// has to: locking chmods a directory AFTER its contents (they must stay
|
|
94
|
+
// reachable while we walk it), unlocking chmods it BEFORE (or the walk can't get
|
|
95
|
+
// in at all -- so a failure there aborts this subtree).
|
|
96
|
+
function chmodTree(dir, modes) {
|
|
97
|
+
if (modes.dirFirst && !chmod(dir, modes.dir))
|
|
130
98
|
return;
|
|
131
|
-
}
|
|
132
99
|
let entries;
|
|
133
100
|
try {
|
|
134
101
|
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
@@ -139,16 +106,19 @@ function unlockImportTree(dir) {
|
|
|
139
106
|
for (const entry of entries) {
|
|
140
107
|
const child = path.join(dir, entry.name);
|
|
141
108
|
if (entry.isDirectory())
|
|
142
|
-
|
|
143
|
-
else
|
|
144
|
-
|
|
145
|
-
fs.chmodSync(child, 0o644);
|
|
146
|
-
}
|
|
147
|
-
catch {
|
|
148
|
-
/* best effort */
|
|
149
|
-
}
|
|
150
|
-
}
|
|
109
|
+
chmodTree(child, modes);
|
|
110
|
+
else
|
|
111
|
+
chmod(child, modes.file);
|
|
151
112
|
}
|
|
113
|
+
if (!modes.dirFirst)
|
|
114
|
+
chmod(dir, modes.dir);
|
|
115
|
+
}
|
|
116
|
+
export function lockImportTree(dir) {
|
|
117
|
+
chmodTree(dir, { file: 0o444, dir: 0o555, dirFirst: false });
|
|
118
|
+
}
|
|
119
|
+
// Restore write permission so the tree can be replaced or removed.
|
|
120
|
+
function unlockImportTree(dir) {
|
|
121
|
+
chmodTree(dir, { file: 0o644, dir: 0o755, dirFirst: true });
|
|
152
122
|
}
|
|
153
123
|
// An import's code runs from the IMPORTING deck's node_modules -- there is one
|
|
154
124
|
// install at the deck root, and a dependency's own node_modules is neither
|
|
@@ -209,7 +179,7 @@ export function syncImportDependencies(deckDir) {
|
|
|
209
179
|
// won't run until they are back. Rebuilding them from the pins is `install`'s
|
|
210
180
|
// job, alongside node_modules.
|
|
211
181
|
export async function restoreMissingImports(deckDir) {
|
|
212
|
-
const pins =
|
|
182
|
+
const pins = readCastleJson(deckDir)?.imports ?? {};
|
|
213
183
|
const restored = [];
|
|
214
184
|
for (const [alias, pin] of Object.entries(pins)) {
|
|
215
185
|
const destDir = path.join(deckDir, IMPORTS_DIR, alias);
|
|
@@ -298,7 +268,7 @@ export async function importStatuses(deckDir) {
|
|
|
298
268
|
const cached = statusCache.get(deckDir);
|
|
299
269
|
if (cached && Date.now() - cached.at < STATUS_TTL_MS)
|
|
300
270
|
return cached.value;
|
|
301
|
-
const pins =
|
|
271
|
+
const pins = readCastleJson(deckDir)?.imports ?? {};
|
|
302
272
|
const value = await Promise.all(Object.entries(pins).map(async ([alias, pin]) => {
|
|
303
273
|
const base = {
|
|
304
274
|
alias,
|
|
@@ -386,12 +356,12 @@ async function placeImport(deckDir, alias, deckId, source, via) {
|
|
|
386
356
|
// silently renamed, since renaming would break the refs of whoever named it.
|
|
387
357
|
async function addTransitiveImports(deckDir, viaAlias, seen) {
|
|
388
358
|
const viaDir = path.join(deckDir, IMPORTS_DIR, viaAlias);
|
|
389
|
-
const pins =
|
|
359
|
+
const pins = readCastleJson(viaDir)?.imports ?? {};
|
|
390
360
|
const added = [];
|
|
391
361
|
for (const [alias, pin] of Object.entries(pins)) {
|
|
392
362
|
if (!pin?.deckId || seen.has(pin.deckId))
|
|
393
363
|
continue;
|
|
394
|
-
const existing =
|
|
364
|
+
const existing = readCastleJson(deckDir)?.imports ?? {};
|
|
395
365
|
const holder = existing[alias];
|
|
396
366
|
if (holder && holder.deckId !== pin.deckId) {
|
|
397
367
|
console.warn(`"${viaAlias}" wants ${pin.deckId} as "${alias}", which is already ${holder.deckId} here -- skipping; ` +
|
|
@@ -417,7 +387,7 @@ async function addTransitiveImports(deckDir, viaAlias, seen) {
|
|
|
417
387
|
// Every deckId this deck already has, so a transitive walk doesn't refetch or
|
|
418
388
|
// loop on a cycle.
|
|
419
389
|
function importedDeckIds(deckDir) {
|
|
420
|
-
const pins =
|
|
390
|
+
const pins = readCastleJson(deckDir)?.imports ?? {};
|
|
421
391
|
return new Set(Object.values(pins).map((p) => p?.deckId).filter((id) => !!id));
|
|
422
392
|
}
|
|
423
393
|
export async function addImport(dir, options = {}) {
|
|
@@ -468,7 +438,7 @@ export async function addImport(dir, options = {}) {
|
|
|
468
438
|
// wasn't.
|
|
469
439
|
export async function updateImport(dir, options = {}) {
|
|
470
440
|
const deckDir = path.resolve(dir);
|
|
471
|
-
const pins =
|
|
441
|
+
const pins = readCastleJson(deckDir)?.imports ?? {};
|
|
472
442
|
const aliases = options.alias ? [options.alias] : Object.keys(pins);
|
|
473
443
|
if (aliases.length === 0) {
|
|
474
444
|
console.log('This deck has no imports.');
|
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
|
}
|