castle-web-cli 0.4.92 → 0.4.93

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.
Files changed (42) hide show
  1. package/dist/agent.js +141 -22
  2. package/dist/api.d.ts +8 -0
  3. package/dist/api.js +10 -0
  4. package/dist/bundle.js +2 -2
  5. package/dist/get-deck.d.ts +1 -1
  6. package/dist/get-deck.js +93 -22
  7. package/dist/ide.js +123 -15
  8. package/dist/imports.d.ts +22 -0
  9. package/dist/imports.js +549 -0
  10. package/dist/index.js +45 -15
  11. package/dist/init.js +169 -1
  12. package/dist/install.d.ts +1 -1
  13. package/dist/install.js +14 -1
  14. package/dist/metering.d.ts +1 -0
  15. package/dist/metering.js +1 -1
  16. package/dist/native/loop.js +1 -0
  17. package/dist/native/openrouter.d.ts +1 -0
  18. package/dist/native/openrouter.js +13 -6
  19. package/dist/native/types.d.ts +1 -0
  20. package/dist/normalize.js +4 -0
  21. package/dist/openrouter-catalog.d.ts +3 -1
  22. package/dist/openrouter-catalog.js +15 -10
  23. package/dist/save-deck.d.ts +2 -0
  24. package/dist/save-deck.js +25 -19
  25. package/dist/serve.js +2 -2
  26. package/dist/shell/assets/{index-DSIr52Kl.css → index-CWNH9QiB.css} +1 -1
  27. package/dist/shell/assets/{index-BFCG4tLs.js → index-_C2BvstY.js} +21 -21
  28. package/dist/shell/index.html +2 -2
  29. package/dist/vitePlugins.d.ts +1 -0
  30. package/dist/vitePlugins.js +33 -0
  31. package/kits/basic-2d/CLAUDE.md +20 -0
  32. package/kits/basic-2d/behaviors/Sprite.jsx +6 -1
  33. package/kits/basic-2d/editors/BlueprintLibrary.jsx +14 -8
  34. package/kits/basic-2d/editors/behaviorRegistry.js +8 -2
  35. package/kits/basic-2d/engine/behaviorExtensions.js +5 -1
  36. package/kits/basic-2d/engine/blueprint.js +39 -3
  37. package/kits/basic-2d/engine/files.js +26 -5
  38. package/kits/basic-2d/engine/scene.js +4 -1
  39. package/kits/basic-2d/engine/systemRegistry.js +5 -1
  40. package/package.json +1 -1
  41. package/dist/pull.d.ts +0 -4
  42. package/dist/pull.js +0 -119
package/dist/init.js CHANGED
@@ -2,6 +2,7 @@ import * as fs from "fs";
2
2
  import * as path from "path";
3
3
  import { COMMON_INSTRUCTIONS } from "./commonInstructions.js";
4
4
  import { installDeps } from "./install.js";
5
+ import { IMPORTS_DIR, lockImportTree } from "./imports.js";
5
6
  import { getCliEntryPath, getKitsDir, getRepoRoot, getSdkPackagePath, toPosixPath, } from "./localPaths.js";
6
7
  import { serve } from "./serve.js";
7
8
  const INDEX_HTML = `<!DOCTYPE html>
@@ -40,6 +41,9 @@ const PUBLISHED_SDK_VERSION = "0.4.10";
40
41
  // (the kit ships a config-only one with the editor layout / file filters), but
41
42
  // `scaffoldFromKit` strips any identity fields off it first -- a fresh deck has
42
43
  // no deckId until its first save-deck, which owns those fields.
44
+ // The account the first-party kits are (to be) published under, so a kit
45
+ // imported from the CLI's copy is named the same as one fetched from the server.
46
+ const KIT_AUTHOR = "castle";
43
47
  const KIT_COPY_EXCLUDE = new Set([
44
48
  "node_modules",
45
49
  ".castle",
@@ -181,6 +185,170 @@ function scaffoldBare(projectDir) {
181
185
  }
182
186
  // Copy a framework kit from kits/<kit>/ into the new deck dir, dropping
183
187
  // build/dependency junk and castle.json.
188
+ // Shared by both scaffold paths: resolve the kit directory or exit with the
189
+ // list of kits that do exist.
190
+ function requireKitDir(kit) {
191
+ const kitDir = path.join(getKitsDir(), kit);
192
+ if (fs.existsSync(kitDir) && fs.statSync(kitDir).isDirectory())
193
+ return kitDir;
194
+ console.error(`Kit "${kit}" not found at ${kitDir}.`);
195
+ console.error("Available kits:");
196
+ try {
197
+ const kits = fs
198
+ .readdirSync(getKitsDir())
199
+ .filter((name) => fs.statSync(path.join(getKitsDir(), name)).isDirectory());
200
+ if (kits.length)
201
+ for (const name of kits)
202
+ console.error(` ${name}`);
203
+ else
204
+ console.error(" (none)");
205
+ }
206
+ catch {
207
+ console.error(" (none — kits/ directory is missing)");
208
+ }
209
+ console.error("Or use `--kit none` for a bare code-only deck.");
210
+ return process.exit(1);
211
+ }
212
+ function readJsonFile(file) {
213
+ try {
214
+ return JSON.parse(fs.readFileSync(file, "utf8"));
215
+ }
216
+ catch {
217
+ return null;
218
+ }
219
+ }
220
+ function writeJsonFile(file, value) {
221
+ fs.writeFileSync(file, JSON.stringify(value, null, 2) + "\n", "utf8");
222
+ }
223
+ function getCliVersion() {
224
+ const pkg = readJsonFile(path.join(getKitsDir(), "..", "package.json"));
225
+ return typeof pkg?.version === "string" ? pkg.version : "0.0.0";
226
+ }
227
+ // The deck's entry point is the KIT's -- the deck has no engine code of its own
228
+ // -- so index.html points into the import. Everything the kit's main.jsx imports
229
+ // is relative to itself, so it needs no rewriting to run from there.
230
+ function writeDeckIndexHtml(projectDir, alias, title) {
231
+ fs.writeFileSync(path.join(projectDir, "index.html"), `<!doctype html>
232
+ <html>
233
+ <head>
234
+ <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />
235
+ <meta name="color-scheme" content="dark" />
236
+ <title>${title}</title>
237
+ <!-- Declare the dark scheme before the JS-imported CSS loads, so the frame
238
+ never paints light first (Safari won't repaint a composited iframe on a
239
+ color-scheme change). -->
240
+ <style>
241
+ html {
242
+ color-scheme: dark;
243
+ background: #0a0a0a;
244
+ }
245
+ </style>
246
+ </head>
247
+ <body>
248
+ <div id="root"></div>
249
+ <!-- Engine and editors come from the imported kit; this deck holds its own
250
+ scenes, drawings and behaviors. -->
251
+ <script type="module" src="/${IMPORTS_DIR}/${alias}/main.jsx"></script>
252
+ </body>
253
+ </html>
254
+ `, "utf8");
255
+ }
256
+ // The deck starts with its own copy of the kit's starter scene, so there is
257
+ // something to open and edit on the first run. Refs inside it are rewritten to
258
+ // point at the kit's copies of what they name (the scene is the deck's now, but
259
+ // the blueprint and art it places still live in the import).
260
+ function writeStarterScene(projectDir, kitDir, alias) {
261
+ const scene = readJsonFile(path.join(kitDir, "scenes", "main.scene"));
262
+ const prefix = `${IMPORTS_DIR}/${alias}/`;
263
+ const rewrite = (value) => {
264
+ if (typeof value === "string") {
265
+ return fs.existsSync(path.join(kitDir, value)) ? prefix + value : value;
266
+ }
267
+ if (Array.isArray(value))
268
+ return value.map(rewrite);
269
+ if (value && typeof value === "object") {
270
+ return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, rewrite(v)]));
271
+ }
272
+ return value;
273
+ };
274
+ fs.mkdirSync(path.join(projectDir, "scenes"), { recursive: true });
275
+ writeJsonFile(path.join(projectDir, "scenes", "main.scene"), scene ? rewrite(scene) : { name: "Main", actors: [] });
276
+ }
277
+ function makeImportedKitClaudeMd(alias) {
278
+ return `# Castle deck
279
+
280
+ The engine, editors and built-in behaviors come from the **${alias}** kit, imported
281
+ at \`${IMPORTS_DIR}/${alias}/\` and read-only. Read \`${IMPORTS_DIR}/${alias}/CLAUDE.md\`
282
+ for the authoring guide: how scenes, blueprints, drawings and behaviors work.
283
+
284
+ This deck holds only its own content -- \`scenes/\`, \`drawings/\`,
285
+ \`blueprints/\`, \`behaviors/\` -- plus \`index.html\` (which points at the kit's
286
+ entry point) and \`castle.json\` (editor config and the import pin).
287
+
288
+ Do not edit anything under \`${IMPORTS_DIR}/\`: those files belong to the deck they
289
+ came from, and are replaced wholesale when the import is updated. To change how
290
+ something in the kit behaves, define your own behavior of the same name in
291
+ \`behaviors/\` -- the deck's own files win over an import's.
292
+ `;
293
+ }
294
+ // Scaffold a deck that IMPORTS its kit instead of copying it. The kit's files
295
+ // land in `imports/<kit>/` (read-only, like any dependency) and the deck itself
296
+ // holds only what is genuinely its own: an entry point, its config, and its
297
+ // starting content. A kit fix then reaches the deck by re-fetching the import,
298
+ // rather than being frozen into the deck at scaffold time -- which is what
299
+ // copying the kit in meant.
300
+ //
301
+ // The kit comes from the copy shipped with this CLI, so `init` works offline and
302
+ // needs no account. The pin records that origin; once kits are published as
303
+ // decks it becomes a deckId + version and the files are fetched instead, with
304
+ // the same deck shape either way.
305
+ function scaffoldFromKitImport(kit, projectDir) {
306
+ const kitDir = requireKitDir(kit);
307
+ // Qualified like any other import (`<author>.<deck>`), and qualified NOW even
308
+ // though this copy is the CLI's: the alias is baked into every ref the deck
309
+ // writes, so it can't change later without rewriting them. Publishing the
310
+ // kits under the castle account turns the pin into a deckId; the name it is
311
+ // already known by stays put.
312
+ const alias = `${KIT_AUTHOR}.${kit}`;
313
+ const importDir = path.join(projectDir, IMPORTS_DIR, alias);
314
+ fs.mkdirSync(importDir, { recursive: true });
315
+ fs.cpSync(kitDir, importDir, {
316
+ recursive: true,
317
+ verbatimSymlinks: true,
318
+ filter: (src) => src === kitDir || !KIT_COPY_EXCLUDE.has(path.basename(src)),
319
+ });
320
+ const kitConfig = readJsonFile(path.join(kitDir, "castle.json")) ?? {};
321
+ const kitPkg = readJsonFile(path.join(kitDir, "package.json")) ?? {};
322
+ writeDeckIndexHtml(projectDir, alias, String(kitConfig.title ?? kit));
323
+ writeStarterScene(projectDir, kitDir, alias);
324
+ // The deck's own castle.json: the kit's editor config (panel layout, file
325
+ // filters) as a starting point -- it is the deck's to edit from here -- plus
326
+ // the import pin.
327
+ writeJsonFile(path.join(projectDir, "castle.json"), {
328
+ ...(kitConfig.editor ? { editor: kitConfig.editor } : {}),
329
+ imports: { [alias]: { source: "builtin", kit, version: getCliVersion() } },
330
+ });
331
+ // The kit's code runs from THIS deck's node_modules, so its dependencies are
332
+ // declared here (see syncImportDependencies, which does the same for imports
333
+ // added later).
334
+ const { sdkRef } = resolveScaffoldRefs();
335
+ const dependencies = {};
336
+ for (const [name, range] of Object.entries(kitPkg.dependencies ?? {})) {
337
+ dependencies[name] =
338
+ name === "castle-web-sdk" && String(range).startsWith("file:")
339
+ ? sdkRef
340
+ : String(range);
341
+ }
342
+ writeJsonFile(path.join(projectDir, "package.json"), {
343
+ name: path.basename(projectDir),
344
+ private: true,
345
+ type: "module",
346
+ dependencies,
347
+ });
348
+ fs.writeFileSync(path.join(projectDir, "CLAUDE.md"), makeImportedKitClaudeMd(alias));
349
+ appendCommonInstructions(projectDir);
350
+ lockImportTree(importDir);
351
+ }
184
352
  function scaffoldFromKit(kit, projectDir) {
185
353
  const kitDir = path.join(getKitsDir(), kit);
186
354
  if (!fs.existsSync(kitDir) || !fs.statSync(kitDir).isDirectory()) {
@@ -278,7 +446,7 @@ export async function init(dir, opts = {}) {
278
446
  scaffoldBare(projectDir);
279
447
  }
280
448
  else {
281
- scaffoldFromKit(kit, projectDir);
449
+ scaffoldFromKitImport(kit, projectDir);
282
450
  }
283
451
  console.log(`Created project in ${projectDir}/${bare ? "" : ` (from kit "${kit}")`}`);
284
452
  // Always install deps so the deck is ready to serve/edit immediately.
package/dist/install.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  export declare function installDeps(projectDir: string, frozen: boolean): void;
2
- export declare function install(dir: string): void;
2
+ export declare function install(dir: string): Promise<void>;
package/dist/install.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { execSync } from "child_process";
2
2
  import * as fs from "fs";
3
3
  import * as path from "path";
4
+ import { restoreMissingImports, syncImportDependencies } from "./imports.js";
4
5
  function hasPnpm() {
5
6
  try {
6
7
  execSync("pnpm --version", { stdio: "ignore" });
@@ -53,12 +54,24 @@ export function installDeps(projectDir, frozen) {
53
54
  // `castle-web install <dir>`: put node_modules in place for a deck that already has
54
55
  // its source -- one fetched with `get-deck`, or one whose node_modules was dropped
55
56
  // (the cloud hosts treat it as regenerable and don't snapshot it).
56
- export function install(dir) {
57
+ export async function install(dir) {
57
58
  const projectDir = path.resolve(dir);
58
59
  if (!fs.existsSync(path.join(projectDir, "package.json"))) {
59
60
  console.error(`No package.json in ${projectDir}.`);
60
61
  process.exit(1);
61
62
  }
63
+ // A fetched deck carries its import pins but not the files (imports/ is left
64
+ // out of the archive), so put them back before anything depends on them.
65
+ const restored = await restoreMissingImports(projectDir);
66
+ for (const alias of restored)
67
+ console.log(`Restored import ${alias}`);
68
+ // Imports declare packages the deck has to install (see syncImportDependencies).
69
+ // Re-done here, not just at add-import time, so a deck fetched with get-deck --
70
+ // which brings castle.json's pins but not the dependency files -- still ends up
71
+ // with everything its imports need once they are back.
72
+ const added = syncImportDependencies(projectDir);
73
+ for (const dep of added)
74
+ console.log(`Added dependency ${dep} from an import`);
62
75
  installDeps(projectDir, fs.existsSync(path.join(projectDir, "pnpm-lock.yaml")));
63
76
  console.log(`Installed deps in ${projectDir}`);
64
77
  }
@@ -11,6 +11,7 @@ export declare function meteringHeaders(opts: {
11
11
  deckDir: string;
12
12
  sessionId: string;
13
13
  route: MeteringRoute;
14
+ direct: boolean;
14
15
  }): Record<string, string>;
15
16
  /**
16
17
  * Merge metering headers into a claude spawn env. ANTHROPIC_CUSTOM_HEADERS is
package/dist/metering.js CHANGED
@@ -49,7 +49,7 @@ function deckIdFor(deckDir) {
49
49
  return parsed.deckId.replace(/[\r\n]/g, "") || null;
50
50
  }
51
51
  export function meteringHeaders(opts) {
52
- if (!proxyInjected(opts.route))
52
+ if (opts.direct || !proxyInjected(opts.route))
53
53
  return {};
54
54
  const deckId = deckIdFor(opts.deckDir);
55
55
  return {
@@ -599,6 +599,7 @@ async function runLoop(opts, toolSchemas, log) {
599
599
  const iterationStartedAt = Date.now();
600
600
  const streamResult = await streamChatCompletion({
601
601
  apiKey: opts.apiKey,
602
+ baseUrl: opts.baseUrl,
602
603
  model: opts.model,
603
604
  messages,
604
605
  tools: toolSchemas,
@@ -33,6 +33,7 @@ export type ORRoutingMode = "balanced" | "nitro" | "exacto" | "floor";
33
33
  export declare function applyRoutingMode(model: string, routing?: ORRoutingMode): string;
34
34
  export interface StreamChatOpts {
35
35
  apiKey: string;
36
+ baseUrl?: string;
36
37
  model: string;
37
38
  messages: ORMessage[];
38
39
  tools?: unknown[];
@@ -19,11 +19,13 @@ import { failureForStatus } from "../agent-failures.js";
19
19
  // a local fake server instead of the real API -- read per-call (not hoisted
20
20
  // to a module-level const) so a test process that imports this module once
21
21
  // can still point successive runAgentNative calls at different fake servers.
22
- function openrouterUrl() {
23
- // In a Castle sandbox the host injects OPENROUTER_BASE_URL pointing at the per-host
24
- // llm-proxy (holds the real key, meters usage); otherwise talk to openrouter.ai directly.
22
+ function openrouterUrl(base) {
23
+ // A user's OWN OpenRouter key passes `base` (openrouter.ai) so the run bypasses
24
+ // the proxy. Otherwise, in a Castle sandbox the host injects OPENROUTER_BASE_URL
25
+ // pointing at the per-host llm-proxy (holds the real key, meters usage); else
26
+ // talk to openrouter.ai directly. The test override wins over all of it.
25
27
  return (process.env.CASTLE_OPENROUTER_URL ||
26
- `${process.env.OPENROUTER_BASE_URL || "https://openrouter.ai/api/v1"}/chat/completions`);
28
+ `${base || process.env.OPENROUTER_BASE_URL || "https://openrouter.ai/api/v1"}/chat/completions`);
27
29
  }
28
30
  const DEFAULT_MAX_RETRIES = 2; // -> 3 total connect attempts
29
31
  const RETRY_BASE_MS = 500;
@@ -91,7 +93,7 @@ async function connectWithRetry(init, opts) {
91
93
  }
92
94
  let res;
93
95
  try {
94
- res = await fetch(openrouterUrl(), init);
96
+ res = await fetch(opts.url, init);
95
97
  }
96
98
  catch (err) {
97
99
  if (err instanceof Error && err.name === "AbortError")
@@ -253,7 +255,12 @@ export async function streamChatCompletion(opts) {
253
255
  },
254
256
  body: JSON.stringify(body),
255
257
  signal: opts.signal,
256
- }, { signal: opts.signal, maxRetries: opts.maxRetries, onRetry: opts.onRetry });
258
+ }, {
259
+ url: openrouterUrl(opts.baseUrl),
260
+ signal: opts.signal,
261
+ maxRetries: opts.maxRetries,
262
+ onRetry: opts.onRetry,
263
+ });
257
264
  }
258
265
  catch (err) {
259
266
  if (err instanceof Error && err.name === "AbortError") {
@@ -18,6 +18,7 @@ export interface NativeRunOpts {
18
18
  role: NativeRole;
19
19
  model: string;
20
20
  apiKey: string;
21
+ baseUrl?: string;
21
22
  reasoningEffort?: ORReasoningEffort;
22
23
  routing?: ORRoutingMode;
23
24
  providerTier?: string;
package/dist/normalize.js CHANGED
@@ -60,7 +60,11 @@ export function normalizeDeckPackageJson(projectDir) {
60
60
  }
61
61
  }
62
62
  if (changed) {
63
+ const prev = fs.statSync(pkgPath);
63
64
  fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf-8');
65
+ // Machine wiring, not an edit: keep the old mtime so get-deck's staleness
66
+ // guard doesn't read a freshly fetched deck as locally modified.
67
+ fs.utimesSync(pkgPath, prev.atime, prev.mtime);
64
68
  }
65
69
  return changed;
66
70
  }
@@ -23,6 +23,8 @@ export type KeyCheck = {
23
23
  } | {
24
24
  status: "unavailable";
25
25
  };
26
- export declare function checkOpenrouterKey(apiKey: string): Promise<KeyCheck>;
26
+ export declare function checkOpenrouterKey(apiKey: string, opts?: {
27
+ direct?: boolean;
28
+ }): Promise<KeyCheck>;
27
29
  export declare function checkOpenrouterModel(slug: string): Promise<ModelCheck>;
28
30
  export declare function openrouterCatalogEntry(slug: string): Promise<CatalogEntry | null>;
@@ -28,12 +28,17 @@ function modelsUrl() {
28
28
  return (process.env.CASTLE_OPENROUTER_MODELS_URL ??
29
29
  "https://openrouter.ai/api/v1/models");
30
30
  }
31
- // The key check sends the credential, so in a Castle sandbox it goes through the llm-proxy
32
- // (OPENROUTER_BASE_URL) — validating the real key the proxy swaps in. modelsUrl above is a
33
- // public GET and stays on openrouter.ai (the proxy requires a token).
34
- function keyUrl() {
35
- return (process.env.CASTLE_OPENROUTER_KEY_URL ??
36
- `${process.env.OPENROUTER_BASE_URL || "https://openrouter.ai/api/v1"}/key`);
31
+ // The key check sends the credential, so by default (proxy mode) it goes through the llm-proxy
32
+ // (OPENROUTER_BASE_URL) — validating the real key the proxy swaps in. `direct` (a user's OWN
33
+ // OpenRouter key, which the proxy would reject) validates against openrouter.ai instead.
34
+ // modelsUrl above is a public GET and stays on openrouter.ai (the proxy requires a token).
35
+ function keyUrl(direct) {
36
+ if (process.env.CASTLE_OPENROUTER_KEY_URL)
37
+ return process.env.CASTLE_OPENROUTER_KEY_URL;
38
+ const base = direct
39
+ ? "https://openrouter.ai/api/v1"
40
+ : process.env.OPENROUTER_BASE_URL || "https://openrouter.ai/api/v1";
41
+ return `${base}/key`;
37
42
  }
38
43
  function cachePath() {
39
44
  return (process.env.CASTLE_OPENROUTER_CATALOG_CACHE ??
@@ -220,11 +225,11 @@ function keyHandle(apiKey) {
220
225
  h = (Math.imul(h, 31) + apiKey.charCodeAt(i)) | 0;
221
226
  return `k${(h >>> 0).toString(36)}`;
222
227
  }
223
- async function fetchKeyCheck(apiKey) {
228
+ async function fetchKeyCheck(apiKey, direct) {
224
229
  const controller = new AbortController();
225
230
  const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
226
231
  try {
227
- const res = await fetch(keyUrl(), {
232
+ const res = await fetch(keyUrl(direct), {
228
233
  headers: { authorization: `Bearer ${apiKey}` },
229
234
  signal: controller.signal,
230
235
  });
@@ -249,7 +254,7 @@ async function fetchKeyCheck(apiKey) {
249
254
  clearTimeout(timer);
250
255
  }
251
256
  }
252
- export async function checkOpenrouterKey(apiKey) {
257
+ export async function checkOpenrouterKey(apiKey, opts) {
253
258
  if (!apiKey)
254
259
  return { status: "bad-key" };
255
260
  const handle = keyHandle(apiKey);
@@ -260,7 +265,7 @@ export async function checkOpenrouterKey(apiKey) {
260
265
  const existing = keyInflight.get(handle);
261
266
  if (existing)
262
267
  return existing;
263
- const p = fetchKeyCheck(apiKey)
268
+ const p = fetchKeyCheck(apiKey, opts?.direct)
264
269
  .then((result) => {
265
270
  // An "unavailable" verdict is deliberately NOT cached -- it means the
266
271
  // network hiccuped, and caching it would suppress validation for the
@@ -1,3 +1,5 @@
1
+ export declare const SOURCE_ARCHIVE_EXCLUDES: string[];
2
+ export declare function runTar(args: string[]): Promise<void>;
1
3
  export declare function archiveSource(projectDir: string): Promise<Buffer>;
2
4
  export type SaveVisibility = 'unlisted' | 'private';
3
5
  export interface SaveDeckOptions {
package/dist/save-deck.js CHANGED
@@ -6,38 +6,44 @@ import { nanoid } from 'nanoid';
6
6
  import * as api from './api.js';
7
7
  import * as config from './config.js';
8
8
  import { bundleProject } from './bundle.js';
9
- const SOURCE_ARCHIVE_EXCLUDES = ['node_modules', 'dist', '.castle', '.git'];
10
- export function archiveSource(projectDir) {
9
+ // `imports/` is derived state like node_modules: a dependency's files belong to
10
+ // the deck that published them, and castle.json's import pins are enough to fetch
11
+ // them again -- so they neither bloat this deck's archive nor get republished as
12
+ // part of it. (Being on this list also means `get-deck` leaves an existing
13
+ // `imports/` in place when it refreshes a deck.)
14
+ export const SOURCE_ARCHIVE_EXCLUDES = ['node_modules', 'dist', '.castle', '.git', 'imports'];
15
+ export function runTar(args) {
11
16
  return new Promise((resolve, reject) => {
12
- const tmpFile = path.join(os.tmpdir(), `castle-source-${nanoid(8)}.tar.gz`);
13
- const args = ['-czf', tmpFile];
14
- for (const ex of SOURCE_ARCHIVE_EXCLUDES)
15
- args.push(`--exclude=./${ex}`);
16
- args.push('-C', projectDir, '.');
17
17
  const child = spawn('tar', args, { stdio: ['ignore', 'ignore', 'pipe'] });
18
18
  let stderr = '';
19
19
  child.stderr?.on('data', (chunk) => { stderr += chunk.toString(); });
20
20
  child.on('error', reject);
21
21
  child.on('close', (code) => {
22
22
  if (code !== 0) {
23
- try {
24
- fs.unlinkSync(tmpFile);
25
- }
26
- catch { /* nothing to clean */ }
27
23
  reject(new Error(`tar exited with code ${code}: ${stderr}`));
28
24
  return;
29
25
  }
30
- try {
31
- const buf = fs.readFileSync(tmpFile);
32
- fs.unlinkSync(tmpFile);
33
- resolve(buf);
34
- }
35
- catch (e) {
36
- reject(e instanceof Error ? e : new Error(String(e)));
37
- }
26
+ resolve();
38
27
  });
39
28
  });
40
29
  }
30
+ export async function archiveSource(projectDir) {
31
+ const tmpFile = path.join(os.tmpdir(), `castle-source-${nanoid(8)}.tar.gz`);
32
+ const args = ['-czf', tmpFile];
33
+ for (const ex of SOURCE_ARCHIVE_EXCLUDES)
34
+ args.push(`--exclude=./${ex}`);
35
+ args.push('-C', projectDir, '.');
36
+ try {
37
+ await runTar(args);
38
+ return fs.readFileSync(tmpFile);
39
+ }
40
+ finally {
41
+ try {
42
+ fs.unlinkSync(tmpFile);
43
+ }
44
+ catch { /* nothing to clean */ }
45
+ }
46
+ }
41
47
  async function uploadSource(projectDir, deckId) {
42
48
  const archive = await archiveSource(projectDir);
43
49
  const sizeKB = archive.length / 1024;
package/dist/serve.js CHANGED
@@ -6,7 +6,7 @@ import { createServer } from 'vite';
6
6
  import { WebSocketServer, WebSocket } from 'ws';
7
7
  import { createIdeServer } from './ide.js';
8
8
  import { createAgentServer } from './agent.js';
9
- import { sceneFilesPlugin } from './vitePlugins.js';
9
+ import { sceneFilesPlugin, importsAliasPlugin } from './vitePlugins.js';
10
10
  import { installFilesChangedWatcher } from './filesChanged.js';
11
11
  import * as config from './config.js';
12
12
  import { graphql as castleGraphql } from './api.js';
@@ -271,7 +271,7 @@ export async function serve(dir, options = {}) {
271
271
  process.on('exit', () => agentServer.shutdown());
272
272
  const vite = await createServer({
273
273
  root: projectDir,
274
- plugins: [castlePlugin(wsPort, ideServer, agentServer), sceneFilesPlugin()],
274
+ plugins: [castlePlugin(wsPort, ideServer, agentServer), importsAliasPlugin(), sceneFilesPlugin()],
275
275
  server: {
276
276
  port,
277
277
  strictPort: true,