castle-web-cli 0.4.36 → 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.
- package/dist/agent-prompts.js +10 -12
- package/dist/agent.js +12 -22
- package/dist/buildArchive.d.ts +1 -0
- package/dist/buildArchive.js +108 -0
- package/dist/index.js +8 -0
- package/dist/init.d.ts +18 -0
- package/dist/init.js +124 -59
- package/kits/basic-2d/published-deck.tgz +0 -0
- package/kits/basic-3d/published-deck.tgz +0 -0
- package/package.json +1 -1
package/dist/agent-prompts.js
CHANGED
|
@@ -20,7 +20,6 @@ Hard rules:
|
|
|
20
20
|
\`\`\`castle-task
|
|
21
21
|
short imperative title on the first line
|
|
22
22
|
after: comma-separated titles or ids this task must wait for (optional line)
|
|
23
|
-
supersedes: comma-separated titles or ids this task replaces (optional line)
|
|
24
23
|
Then a SHORT self-contained prompt -- one tight paragraph (aim under 100
|
|
25
24
|
words): what to build or fix and what "done" looks like. Task agents read
|
|
26
25
|
the deck's own docs for framework/API detail, so never restate recipes,
|
|
@@ -28,23 +27,22 @@ file layouts, or implementation steps.
|
|
|
28
27
|
\`\`\`
|
|
29
28
|
|
|
30
29
|
- Use \`after:\` only when a task truly builds on or would conflict with another (it may reference tasks spawned in this same reply, by title). Independent tasks must NOT wait on each other.
|
|
31
|
-
|
|
32
|
-
-
|
|
30
|
+
|
|
31
|
+
Keeping the board clean. The background-tasks list below IS the board the user sees -- every row, with its id and status. Be diligent about removing rows that no longer belong, using these two fences:
|
|
33
32
|
|
|
34
33
|
\`\`\`castle-done
|
|
35
|
-
comma-separated titles or ids
|
|
34
|
+
comma-separated finished-task titles or ids, or \`all\`
|
|
36
35
|
\`\`\`
|
|
37
|
-
|
|
38
|
-
- NEVER check a task off on your own judgment -- only a clear user statement that it works (or an explicit ask to clear it) counts. When in doubt, leave the row on the board.
|
|
39
|
-
- The background-tasks list below IS the board the user sees -- every row, with its id. To clear the WHOLE board at once (e.g. "clear all the tasks"), use \`all\` instead of listing ids: a \`castle-done\` with body \`all\` checks off every finished row, and a \`castle-stop\` with body \`all\` stops everything still running. Never claim the board is cleared without actually emitting the fence.
|
|
40
|
-
- To STOP tasks (running or waiting) when the user asks or their work is clearly no longer wanted, include:
|
|
41
|
-
|
|
42
36
|
\`\`\`castle-stop
|
|
43
|
-
comma-separated titles or ids
|
|
37
|
+
comma-separated active-task titles or ids, or \`all\`
|
|
44
38
|
\`\`\`
|
|
45
39
|
|
|
46
|
-
|
|
47
|
-
-
|
|
40
|
+
- \`castle-done\` removes FINISHED rows (done/failed). Use it when the user confirms a task works, or to clear a finished task that has become obsolete (replaced by newer work). \`all\` clears every finished row.
|
|
41
|
+
- \`castle-stop\` stops AND removes ACTIVE rows (running/waiting) -- a running task's agent is killed. Use it when the user asks to stop something, or when you spawn a fix/replacement that makes an in-flight task obsolete. \`all\` stops everything active.
|
|
42
|
+
- When you spawn a task that fixes, redoes, or replaces an earlier one, remove the earlier one in the SAME reply: \`castle-done\` if it already finished, \`castle-stop\` if it is still running or waiting. Keep the board meaning "what to look at right now".
|
|
43
|
+
- Do not mark a task done to imply YOU verified its quality -- the user playtests and confirms that. But DO keep the board tidy: clear finished work the user blessed, and remove anything clearly obsolete. When genuinely unsure, leave the row.
|
|
44
|
+
- Never claim the board is cleared without actually emitting the fence.
|
|
45
|
+
- Tasks are one-and-done -- when the user gives feedback on a finished task, spawn a new fix task (and \`castle-done\` the old row) rather than reopening it.
|
|
48
46
|
- Task agents are capable coding agents working in this same deck directory, but they know nothing about this conversation beyond your prompt.
|
|
49
47
|
|
|
50
48
|
Conversation style:
|
package/dist/agent.js
CHANGED
|
@@ -113,8 +113,7 @@ function visibleLength(raw) {
|
|
|
113
113
|
return raw.length;
|
|
114
114
|
}
|
|
115
115
|
// Pull ```castle-task fenced directives out of a finished router reply.
|
|
116
|
-
// Block format: title line, then optional "after:"
|
|
117
|
-
// either order), then the task prompt.
|
|
116
|
+
// Block format: title line, then an optional "after:" line, then the prompt.
|
|
118
117
|
function extractDirectives(full) {
|
|
119
118
|
const directives = [];
|
|
120
119
|
const checkoffs = [];
|
|
@@ -135,9 +134,9 @@ function extractDirectives(full) {
|
|
|
135
134
|
const cleaned = withoutDone.replace(fenceRe, (_match, body) => {
|
|
136
135
|
const lines = String(body).replace(/\r/g, '').split('\n');
|
|
137
136
|
const title = (lines.shift() ?? '').trim();
|
|
138
|
-
const headers = { after: []
|
|
137
|
+
const headers = { after: [] };
|
|
139
138
|
while (lines.length > 0) {
|
|
140
|
-
const headerMatch = /^(after
|
|
139
|
+
const headerMatch = /^(after):\s*(.*)$/i.exec((lines[0] ?? '').trim());
|
|
141
140
|
if (!headerMatch)
|
|
142
141
|
break;
|
|
143
142
|
lines.shift();
|
|
@@ -148,7 +147,7 @@ function extractDirectives(full) {
|
|
|
148
147
|
}
|
|
149
148
|
const prompt = lines.join('\n').trim();
|
|
150
149
|
if (title) {
|
|
151
|
-
directives.push({ title, after: headers.after,
|
|
150
|
+
directives.push({ title, after: headers.after, prompt });
|
|
152
151
|
}
|
|
153
152
|
return '';
|
|
154
153
|
});
|
|
@@ -528,6 +527,9 @@ function createTaskStore(opts) {
|
|
|
528
527
|
refreshTaskFiles(tasksDir, task);
|
|
529
528
|
const wasStopped = stopRequested.delete(task.id);
|
|
530
529
|
task.status = wasStopped ? 'interrupted' : result.ok ? 'done' : 'failed';
|
|
530
|
+
// A stopped task is cleared off the board (castle-stop = halt + remove).
|
|
531
|
+
if (wasStopped)
|
|
532
|
+
task.acknowledged = true;
|
|
531
533
|
if (result.ok && !wasStopped)
|
|
532
534
|
task.progress = 100;
|
|
533
535
|
task.finishedAt = nowIso();
|
|
@@ -543,19 +545,6 @@ function createTaskStore(opts) {
|
|
|
543
545
|
});
|
|
544
546
|
}
|
|
545
547
|
function spawnFromDirective(directive, originMessageId) {
|
|
546
|
-
// A fix/redo task sweeps the rows it obsoletes off the user's board. Halt
|
|
547
|
-
// first -- a superseded task that is still running must have its process
|
|
548
|
-
// killed (not just hidden, which left an invisible zombie the rest of the
|
|
549
|
-
// board could depend on), and a waiting one must not start later.
|
|
550
|
-
for (const id of resolveDeps(tasks, directive.supersedes)) {
|
|
551
|
-
const old = tasks.get(id);
|
|
552
|
-
if (old && !old.acknowledged) {
|
|
553
|
-
haltTask(old);
|
|
554
|
-
old.acknowledged = true;
|
|
555
|
-
touch(old);
|
|
556
|
-
opts.onSuperseded(old);
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
548
|
const task = {
|
|
560
549
|
id: nanoid(8),
|
|
561
550
|
title: directive.title,
|
|
@@ -621,12 +610,14 @@ function createTaskStore(opts) {
|
|
|
621
610
|
// stop everything still active. Waiting tasks are cancelled outright;
|
|
622
611
|
// running ones get their agent process killed and finalize as interrupted
|
|
623
612
|
// via the stopRequested path.
|
|
624
|
-
// Halt an active task: a waiting one is cancelled
|
|
625
|
-
// running one gets its agent process
|
|
626
|
-
//
|
|
613
|
+
// Halt + remove an active task (castle-stop): a waiting one is cancelled and
|
|
614
|
+
// cleared off the board immediately; a running one gets its agent process
|
|
615
|
+
// killed and is cleared when it finalizes (the stopRequested path acks it).
|
|
616
|
+
// No-op on terminal tasks.
|
|
627
617
|
function haltTask(task) {
|
|
628
618
|
if (task.status === 'waiting') {
|
|
629
619
|
task.status = 'interrupted';
|
|
620
|
+
task.acknowledged = true;
|
|
630
621
|
touch(task);
|
|
631
622
|
}
|
|
632
623
|
else if (task.status === 'running') {
|
|
@@ -992,7 +983,6 @@ export function createAgentServer(opts) {
|
|
|
992
983
|
onStarted: () => undefined,
|
|
993
984
|
onRetry: (task, attempt) => addLog(`agent died, retrying (${attempt}/${MAX_TASK_ATTEMPTS}): ${task.title}`),
|
|
994
985
|
onFinished: (task) => taskFeeds.map.delete(task.id),
|
|
995
|
-
onSuperseded: () => undefined,
|
|
996
986
|
onFeed: (task, entry) => taskFeeds.push(task, entry),
|
|
997
987
|
});
|
|
998
988
|
// A new user message interrupts the in-flight router reply: its partial
|
|
@@ -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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
|
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
|
-
|
|
205
|
-
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
-
|
|
241
|
-
console.
|
|
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
|
-
|
|
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
|