castle-web-cli 0.4.37 → 0.4.38

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.
@@ -0,0 +1 @@
1
+ export declare function buildKitArchives(kits?: string[]): void;
@@ -0,0 +1,108 @@
1
+ import { execFileSync } from 'child_process';
2
+ import * as fs from 'fs';
3
+ import * as os from 'os';
4
+ import * as path from 'path';
5
+ import { ARCHIVE_NAME, PUBLISHED_SDK_VERSION, copyKitSource, getKitArchivePath, rewriteKitPackageJson, } from './init.js';
6
+ import { getKitsDir, getSdkPackagePath, toPosixPath } from './localPaths.js';
7
+ // Refs forced to "published" so the archived deck looks like one scaffolded
8
+ // from a globally-installed castle-web (registry sdk + `castle-web` binary),
9
+ // regardless of the workspace checkout we generate it from.
10
+ const PUBLISHED_REFS = {
11
+ workspaceMode: false,
12
+ sdkRef: `^${PUBLISHED_SDK_VERSION}`,
13
+ cliCommand: 'castle-web',
14
+ cliDistAbs: null,
15
+ sdkPathPosix: null,
16
+ };
17
+ // Build the local sdk and `npm pack` it into a temp dir, returning the tarball
18
+ // path. Installing a tarball (not a `file:` dir) gives the archive a real copy
19
+ // of the sdk in node_modules instead of a symlink to the local checkout.
20
+ function packSdk() {
21
+ const sdkDir = getSdkPackagePath();
22
+ if (!fs.existsSync(sdkDir)) {
23
+ throw new Error(`sdk/ not found at ${sdkDir}; build-archives must run from a workspace checkout.`);
24
+ }
25
+ console.log('Building sdk...');
26
+ execFileSync('npm', ['run', 'build'], { cwd: sdkDir, stdio: 'inherit' });
27
+ const dest = fs.mkdtempSync(path.join(os.tmpdir(), 'castle-sdk-pack-'));
28
+ console.log('Packing sdk...');
29
+ const out = execFileSync('npm', ['pack', '--pack-destination', dest], {
30
+ cwd: sdkDir,
31
+ encoding: 'utf8',
32
+ }).trim();
33
+ // npm pack prints the created tarball filename on its last line.
34
+ const file = out.split('\n').pop().trim();
35
+ return path.join(dest, file);
36
+ }
37
+ // Find every real kit under kits/ (a subdir with a package.json).
38
+ function discoverKits(kitsDir) {
39
+ return fs.readdirSync(kitsDir).filter((name) => {
40
+ const dir = path.join(kitsDir, name);
41
+ return fs.statSync(dir).isDirectory() && fs.existsSync(path.join(dir, 'package.json'));
42
+ });
43
+ }
44
+ // Generate one ready-to-serve archive for `kit`: kit source (minus build junk)
45
+ // + installed node_modules + a published-mode package.json, gzipped into the
46
+ // bundled kit dir (cli/kits/<kit>/published-deck.tgz).
47
+ function buildKitArchive(kit, sdkTarball) {
48
+ const kitDir = path.join(getKitsDir(), kit);
49
+ if (!fs.existsSync(path.join(kitDir, 'package.json'))) {
50
+ throw new Error(`Kit "${kit}" has no package.json at ${kitDir}.`);
51
+ }
52
+ const stagingRoot = fs.mkdtempSync(path.join(os.tmpdir(), `castle-archive-${kit}-`));
53
+ const deck = path.join(stagingRoot, 'deck');
54
+ try {
55
+ // Same filter the live scaffold uses, plus drop any prior archive.
56
+ copyKitSource(kitDir, deck, new Set([ARCHIVE_NAME]));
57
+ // A stale workspace lock would pin file:../../sdk; drop it before install.
58
+ fs.rmSync(path.join(deck, 'package-lock.json'), { force: true });
59
+ // Published-mode package.json, but point the sdk at the local tarball so
60
+ // the install copies our freshly-built sdk into node_modules.
61
+ const pkgPath = path.join(deck, 'package.json');
62
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
63
+ rewriteKitPackageJson(pkg, kit, PUBLISHED_REFS);
64
+ pkg.dependencies['castle-web-sdk'] = `file:${toPosixPath(sdkTarball)}`;
65
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
66
+ // Deck deps are pure JS (no native binaries), so this mac-built
67
+ // node_modules runs as-is on linux/e2b.
68
+ console.log(`Installing deps for ${kit}...`);
69
+ execFileSync('npm', ['install', '--no-audit', '--no-fund', '--loglevel=error'], {
70
+ cwd: deck,
71
+ stdio: 'inherit',
72
+ });
73
+ // Restore the published sdk range (node_modules already holds the real
74
+ // sdk) and drop the lock pinning the local tarball path.
75
+ pkg.dependencies['castle-web-sdk'] = `^${PUBLISHED_SDK_VERSION}`;
76
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
77
+ fs.rmSync(path.join(deck, 'package-lock.json'), { force: true });
78
+ const archivePath = getKitArchivePath(kit);
79
+ fs.mkdirSync(path.dirname(archivePath), { recursive: true });
80
+ fs.rmSync(archivePath, { force: true });
81
+ console.log(`Archiving ${kit} -> ${archivePath}`);
82
+ execFileSync('tar', ['-czf', archivePath, '-C', deck, '.'], { stdio: 'inherit' });
83
+ const sizeMb = (fs.statSync(archivePath).size / 1e6).toFixed(1);
84
+ console.log(` ${kit} archive: ${sizeMb} MB`);
85
+ }
86
+ finally {
87
+ fs.rmSync(stagingRoot, { recursive: true, force: true });
88
+ }
89
+ }
90
+ // Generate archives for the given kits (default: every kit under kits/). Builds
91
+ // + packs the sdk once, then archives each kit against that same tarball.
92
+ export function buildKitArchives(kits) {
93
+ const kitsDir = getKitsDir();
94
+ const targets = kits ?? discoverKits(kitsDir);
95
+ if (targets.length === 0) {
96
+ console.error(`No kits found under ${kitsDir}.`);
97
+ process.exit(1);
98
+ }
99
+ const sdkTarball = packSdk();
100
+ try {
101
+ for (const kit of targets)
102
+ buildKitArchive(kit, sdkTarball);
103
+ }
104
+ finally {
105
+ fs.rmSync(path.dirname(sdkTarball), { recursive: true, force: true });
106
+ }
107
+ console.log(`Done. Archived: ${targets.join(', ')}`);
108
+ }
package/dist/index.js CHANGED
@@ -135,6 +135,14 @@ async function main() {
135
135
  case 'login':
136
136
  await login();
137
137
  break;
138
+ case 'build-archives': {
139
+ // Dev/publish tool: generate the prebuilt per-kit deck archives that
140
+ // `init` extracts in published mode. Run after `npm run build -w cli`.
141
+ const { buildKitArchives } = await import('./buildArchive.js');
142
+ const only = getFlagValue('--kit');
143
+ buildKitArchives(only ? [only] : undefined);
144
+ break;
145
+ }
138
146
  default:
139
147
  usage();
140
148
  }
package/dist/init.d.ts CHANGED
@@ -1,4 +1,22 @@
1
+ export declare const PUBLISHED_SDK_VERSION = "0.4.4";
2
+ export declare const KIT_COPY_EXCLUDE: Set<string>;
3
+ export declare const ARCHIVE_NAME = "published-deck.tgz";
4
+ export declare function getKitArchivePath(kit: string): string;
5
+ export declare function copyKitSource(kitDir: string, dest: string, extraExclude?: Set<string>): void;
6
+ export declare function rewriteKitPackageJson(pkg: {
7
+ name?: string;
8
+ dependencies?: Record<string, string>;
9
+ scripts?: Record<string, string>;
10
+ }, name: string, refs: ReturnType<typeof resolveScaffoldRefs>): void;
11
+ declare function resolveScaffoldRefs(): {
12
+ workspaceMode: boolean;
13
+ sdkRef: string;
14
+ cliCommand: string;
15
+ cliDistAbs: string | null;
16
+ sdkPathPosix: string | null;
17
+ };
1
18
  export declare function init(dir: string, opts?: {
2
19
  kit?: string;
3
20
  serve?: boolean;
4
21
  }): Promise<void>;
22
+ export {};
package/dist/init.js CHANGED
@@ -1,4 +1,4 @@
1
- import { execSync } from 'child_process';
1
+ import { execFileSync, execSync } from 'child_process';
2
2
  import * as fs from 'fs';
3
3
  import * as path from 'path';
4
4
  import { COMMON_INSTRUCTIONS } from './commonInstructions.js';
@@ -35,10 +35,62 @@ 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.4';
38
+ export const PUBLISHED_SDK_VERSION = '0.4.4';
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
+ export const KIT_COPY_EXCLUDE = new Set(['node_modules', '.castle', 'dist', '.git', 'castle.json']);
42
+ // A globally-installed castle-web ships one prebuilt, ready-to-serve archive
43
+ // per kit (kit source + installed node_modules + published-mode package.json),
44
+ // gzipped. `init` in published mode extracts it instead of cp + `npm install`.
45
+ // Lives inside each bundled kit dir (cli/kits/<kit>/), generated by
46
+ // `build-archives` (see buildArchive.ts). Workspace mode never uses it.
47
+ export const ARCHIVE_NAME = 'published-deck.tgz';
48
+ export function getKitArchivePath(kit) {
49
+ return path.join(getKitsDir(), kit, ARCHIVE_NAME);
50
+ }
51
+ // Copy a framework kit into `dest`, dropping build/dependency junk + castle.json
52
+ // (and any caller-supplied extra names, e.g. a stale archive). Shared by the
53
+ // live scaffold and archive generation so the two stay in sync.
54
+ export function copyKitSource(kitDir, dest, extraExclude) {
55
+ fs.cpSync(kitDir, dest, {
56
+ recursive: true,
57
+ // Keep symlinks verbatim so the kit's AGENTS.md -> CLAUDE.md stays a link.
58
+ verbatimSymlinks: true,
59
+ filter: (src) => src === kitDir ||
60
+ (!KIT_COPY_EXCLUDE.has(path.basename(src)) && !extraExclude?.has(path.basename(src))),
61
+ });
62
+ }
63
+ // Rewrite a kit's package.json (in place) so the scaffolded deck references the
64
+ // sdk + cli per the resolved mode. Workspace mode points at the local checkout
65
+ // (file: sdk + node <abs cli dist>); published mode points at the registry sdk
66
+ // + the `castle-web` binary. Shared by the live scaffold and archive generation.
67
+ export function rewriteKitPackageJson(pkg, name, refs) {
68
+ pkg.name = name;
69
+ const { workspaceMode, sdkRef, cliDistAbs, sdkPathPosix } = refs;
70
+ if (pkg.dependencies &&
71
+ typeof pkg.dependencies['castle-web-sdk'] === 'string' &&
72
+ pkg.dependencies['castle-web-sdk'].startsWith('file:')) {
73
+ pkg.dependencies['castle-web-sdk'] = sdkRef;
74
+ }
75
+ if (!pkg.scripts)
76
+ return;
77
+ for (const k of Object.keys(pkg.scripts)) {
78
+ if (typeof pkg.scripts[k] !== 'string')
79
+ continue;
80
+ if (workspaceMode) {
81
+ pkg.scripts[k] = pkg.scripts[k]
82
+ .replace(/\.\.\/\.\.\/cli\/dist/g, cliDistAbs)
83
+ .replace(/\.\.\/\.\.\/sdk/g, sdkPathPosix);
84
+ }
85
+ else {
86
+ // Globally-installed: route through the `castle-web` binary on PATH.
87
+ pkg.scripts[k] = pkg.scripts[k]
88
+ .replace(/node\s+\.\.\/\.\.\/cli\/dist\/index\.js/g, 'castle-web')
89
+ .replace(/await import\((['"])\.\.\/\.\.\/cli\/dist\/bundle\.js\1\)/g, "await import('castle-web-cli/dist/bundle.js')")
90
+ .replace(/\.\.\/\.\.\/sdk/g, '');
91
+ }
92
+ }
93
+ }
42
94
  // Resolve how a scaffolded deck should reference the sdk + cli. Both the bare
43
95
  // and kit scaffold paths go through here so they stay in sync.
44
96
  // workspace mode (sdk/ sits next to cli/, i.e. running from a checkout):
@@ -127,8 +179,42 @@ function scaffoldBare(projectDir) {
127
179
  ensureAgentsSymlink(projectDir);
128
180
  fs.writeFileSync(path.join(projectDir, 'package.json'), JSON.stringify(makePackageJson(projectDir), null, 2) + '\n');
129
181
  }
182
+ // Finalize a deck whose files are already on disk (mirroring scaffoldFromKit's
183
+ // tail): ensure a CLAUDE.md, append the cli-owned common guidance, and create
184
+ // the AGENTS.md symlink. Used by both the cp path and the archive-extract path.
185
+ function finalizeDeckDocs(projectDir) {
186
+ // Every deck needs a CLAUDE.md so coding agents know how castle-web works.
187
+ // Keep the kit's own if it ships one; otherwise generate from the upstream.
188
+ const claudePath = path.join(projectDir, 'CLAUDE.md');
189
+ if (!fs.existsSync(claudePath)) {
190
+ fs.writeFileSync(claudePath, makeClaudeMd());
191
+ }
192
+ appendCommonInstructions(projectDir);
193
+ ensureAgentsSymlink(projectDir);
194
+ }
195
+ // Published fast path: extract the prebuilt, ready-to-serve archive into the
196
+ // deck dir (no cp, no `npm install`). The archive carries the kit source +
197
+ // installed node_modules + a published-mode package.json with a generic name;
198
+ // rename it to the deck dir, then run the shared doc finalization.
199
+ function extractKitArchive(archivePath, projectDir) {
200
+ fs.mkdirSync(projectDir, { recursive: true });
201
+ execFileSync('tar', ['-xzf', archivePath, '-C', projectDir], { stdio: 'inherit' });
202
+ const pkgPath = path.join(projectDir, 'package.json');
203
+ if (fs.existsSync(pkgPath)) {
204
+ try {
205
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
206
+ pkg.name = path.basename(projectDir);
207
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
208
+ }
209
+ catch {
210
+ // archived an unparseable package.json — leave it for the user to fix
211
+ }
212
+ }
213
+ finalizeDeckDocs(projectDir);
214
+ }
130
215
  // Copy a framework kit from kits/<kit>/ into the new deck dir, dropping
131
- // build/dependency junk and castle.json.
216
+ // build/dependency junk and castle.json. Returns whether deps are already
217
+ // installed (true only on the published archive-extract fast path).
132
218
  function scaffoldFromKit(kit, projectDir) {
133
219
  const kitDir = path.join(getKitsDir(), kit);
134
220
  if (!fs.existsSync(kitDir) || !fs.statSync(kitDir).isDirectory()) {
@@ -150,12 +236,17 @@ function scaffoldFromKit(kit, projectDir) {
150
236
  console.error('Or use `--kit none` for a bare code-only deck.');
151
237
  process.exit(1);
152
238
  }
153
- fs.cpSync(kitDir, projectDir, {
154
- recursive: true,
155
- // Keep symlinks verbatim so the kit's AGENTS.md -> CLAUDE.md stays a link.
156
- verbatimSymlinks: true,
157
- filter: (src) => src === kitDir || !KIT_COPY_EXCLUDE.has(path.basename(src)),
158
- });
239
+ // Published mode (globally-installed castle-web, no sibling sdk/) extracts a
240
+ // prebuilt archive when one ships for this kit -- skipping cp + `npm install`.
241
+ // Workspace mode always does the live cp so local sdk edits stay live.
242
+ const refs = resolveScaffoldRefs();
243
+ const archivePath = getKitArchivePath(kit);
244
+ if (!refs.workspaceMode && fs.existsSync(archivePath)) {
245
+ console.log('Extracting prebuilt deck (deps included)...');
246
+ extractKitArchive(archivePath, projectDir);
247
+ return { depsInstalled: true };
248
+ }
249
+ copyKitSource(kitDir, projectDir);
159
250
  // The kit's package.json carries the kit's name; rename it to the deck dir.
160
251
  // Kit-relative refs to `../../sdk` and `../../cli/dist` only resolve when the
161
252
  // deck lives at castle-experimental-web/decks/<name>/. Rewrite both to
@@ -165,50 +256,15 @@ function scaffoldFromKit(kit, projectDir) {
165
256
  if (fs.existsSync(pkgPath)) {
166
257
  try {
167
258
  const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
168
- pkg.name = path.basename(projectDir);
169
- // Local-dev paths (`file:../../sdk` / `node ../../cli/dist/index.js`) only
170
- // work when the deck lives inside the castle-experimental-web workspace.
171
- // For a deck scaffolded from a globally-installed castle-web, rewrite to
172
- // the published packages instead. Same workspace-vs-published resolution
173
- // the bare scaffold path uses.
174
- const { workspaceMode, sdkRef, cliDistAbs, sdkPathPosix } = resolveScaffoldRefs();
175
- 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;
179
- }
180
- if (pkg.scripts) {
181
- for (const k of Object.keys(pkg.scripts)) {
182
- if (typeof pkg.scripts[k] !== 'string')
183
- continue;
184
- if (workspaceMode) {
185
- pkg.scripts[k] = pkg.scripts[k]
186
- .replace(/\.\.\/\.\.\/cli\/dist/g, cliDistAbs)
187
- .replace(/\.\.\/\.\.\/sdk/g, sdkPathPosix);
188
- }
189
- else {
190
- // Globally-installed: route through the `castle-web` binary on PATH.
191
- pkg.scripts[k] = pkg.scripts[k]
192
- .replace(/node\s+\.\.\/\.\.\/cli\/dist\/index\.js/g, 'castle-web')
193
- .replace(/await import\((['"])\.\.\/\.\.\/cli\/dist\/bundle\.js\1\)/g, "await import('castle-web-cli/dist/bundle.js')")
194
- .replace(/\.\.\/\.\.\/sdk/g, '');
195
- }
196
- }
197
- }
259
+ rewriteKitPackageJson(pkg, path.basename(projectDir), refs);
198
260
  fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
199
261
  }
200
262
  catch {
201
263
  // kit shipped an unparseable package.json — leave it for the user to fix
202
264
  }
203
265
  }
204
- // Every deck needs a CLAUDE.md so coding agents know how castle-web works.
205
- // Keep the kit's own if it ships one; otherwise generate from the upstream.
206
- const claudePath = path.join(projectDir, 'CLAUDE.md');
207
- if (!fs.existsSync(claudePath)) {
208
- fs.writeFileSync(claudePath, makeClaudeMd());
209
- }
210
- appendCommonInstructions(projectDir);
211
- ensureAgentsSymlink(projectDir);
266
+ finalizeDeckDocs(projectDir);
267
+ return { depsInstalled: false };
212
268
  }
213
269
  export async function init(dir, opts = {}) {
214
270
  const projectDir = path.resolve(dir);
@@ -218,11 +274,14 @@ export async function init(dir, opts = {}) {
218
274
  }
219
275
  const kit = opts.kit ?? DEFAULT_KIT;
220
276
  const bare = kit === 'none' || kit === 'bare';
277
+ // depsInstalled is true only when the published archive fast-path ran, which
278
+ // already ships node_modules -- so we can skip `npm install` below.
279
+ let depsInstalled = false;
221
280
  if (bare) {
222
281
  scaffoldBare(projectDir);
223
282
  }
224
283
  else {
225
- scaffoldFromKit(kit, projectDir);
284
+ ({ depsInstalled } = scaffoldFromKit(kit, projectDir));
226
285
  }
227
286
  console.log(`Created project in ${projectDir}/${bare ? '' : ` (from kit "${kit}")`}`);
228
287
  // Auto-run npm install + serve so the page is up the moment the
@@ -230,16 +289,21 @@ export async function init(dir, opts = {}) {
230
289
  const autoServe = opts.serve !== false;
231
290
  if (autoServe) {
232
291
  console.log('');
233
- console.log('Installing deps + serving (pass --no-serve to skip)...');
234
- try {
235
- execSync('npm install --no-audit --no-fund --loglevel=error', {
236
- cwd: projectDir,
237
- stdio: 'inherit',
238
- });
292
+ if (!depsInstalled) {
293
+ console.log('Installing deps + serving (pass --no-serve to skip)...');
294
+ try {
295
+ execSync('npm install --no-audit --no-fund --loglevel=error', {
296
+ cwd: projectDir,
297
+ stdio: 'inherit',
298
+ });
299
+ }
300
+ catch {
301
+ console.error('npm install failed; skipping serve. Re-run yourself with `npm install && castle-web serve .` (& in your shell to background it).');
302
+ return;
303
+ }
239
304
  }
240
- catch {
241
- console.error('npm install failed; skipping serve. Re-run yourself with `npm install && castle-web serve .` (& in your shell to background it).');
242
- return;
305
+ else {
306
+ console.log('Serving (deps prebuilt)...');
243
307
  }
244
308
  // Call serve() with detach so init returns once the server is up. serve()
245
309
  // handles the background spawn internally; init doesn't shell out.
@@ -256,6 +320,7 @@ export async function init(dir, opts = {}) {
256
320
  console.log('');
257
321
  console.log('Next steps:');
258
322
  console.log(` cd ${dir}`);
259
- console.log(' npm install');
323
+ if (!depsInstalled)
324
+ console.log(' npm install');
260
325
  console.log(' castle-web serve . # & in your shell to background it');
261
326
  }
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "castle-web-cli",
3
- "version": "0.4.37",
3
+ "version": "0.4.38",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "castle-web": "./dist/index.js"