@caperjs/core 0.5.0 → 0.5.2
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/README.md +7 -1
- package/build/plugins/assetTypes.mjs +27 -17
- package/cli/agent.mjs +106 -0
- package/cli/agent.test.mjs +104 -0
- package/cli/create.mjs +10 -1
- package/cli.mjs +4 -0
- package/extras/llms.txt +85 -4
- package/extras/skills/caper/SKILL.md +103 -0
- package/lib/caper.mjs +1 -1
- package/lib/caper.mjs.map +1 -1
- package/package.json +2 -1
- package/src/version.ts +1 -1
- package/templates/app/default/README.md +2 -0
package/README.md
CHANGED
|
@@ -8,7 +8,13 @@ It sits on top of PixiJS — a renderer, not a game engine — and adds the part
|
|
|
8
8
|
|
|
9
9
|
## Status
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
Published on npm as `@caperjs/core`.
|
|
12
|
+
|
|
13
|
+
## For AI agents
|
|
14
|
+
|
|
15
|
+
- `npx caper agent init` installs the `caper` skill + `AGENTS.md` pointers for this project.
|
|
16
|
+
- The reference is `node_modules/@caperjs/core/extras/llms.txt` — read it by section (`grep -n '^## '`), not whole.
|
|
17
|
+
- Engine source ships in the package under `src/`.
|
|
12
18
|
|
|
13
19
|
## Highlights
|
|
14
20
|
|
|
@@ -69,29 +69,39 @@ async function generateAssetTypes(manifest, assetsDir) {
|
|
|
69
69
|
addToBundle('spritesheets', bundle.name, alias);
|
|
70
70
|
});
|
|
71
71
|
|
|
72
|
-
// Extract frame names from TPS JSON files
|
|
72
|
+
// Extract frame names from TPS JSON files, following the multipack
|
|
73
|
+
// chain. A sheet too big for one page is split, and only page 0 reaches
|
|
74
|
+
// the manifest — the rest hang off its `meta.related_multi_packs`, which
|
|
75
|
+
// is how PixiJS finds them too. Reading page 0 alone would type only the
|
|
76
|
+
// frames the packer happened to fit there, so a frame's alias would
|
|
77
|
+
// appear and vanish as unrelated art shifted the packing.
|
|
73
78
|
if (asset.data?.tags?.tps) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
+
const queue = srcs.filter((src) => src.endsWith('.json')).slice(0, 1);
|
|
80
|
+
const seen = new Set(queue);
|
|
81
|
+
while (queue.length) {
|
|
82
|
+
const jsonSrc = queue.shift();
|
|
83
|
+
try {
|
|
79
84
|
const jsonPath = path.join(assetsDir, jsonSrc);
|
|
85
|
+
const tpsData = JSON.parse(await fs.promises.readFile(jsonPath, 'utf8'));
|
|
80
86
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
87
|
+
for (const frameName of Object.keys(tpsData.frames || {})) {
|
|
88
|
+
assetsByType.tpsFrames.add(frameName);
|
|
89
|
+
addToBundle('tpsFrames', bundle.name, frameName);
|
|
90
|
+
}
|
|
84
91
|
|
|
85
|
-
//
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
92
|
+
// Related pages are named relative to the page that lists them.
|
|
93
|
+
// `seen` keeps a self- or cross-referencing chain from looping.
|
|
94
|
+
const dir = path.dirname(jsonSrc);
|
|
95
|
+
for (const related of tpsData.meta?.related_multi_packs || []) {
|
|
96
|
+
const next = path.join(dir, related);
|
|
97
|
+
if (!seen.has(next)) {
|
|
98
|
+
seen.add(next);
|
|
99
|
+
queue.push(next);
|
|
100
|
+
}
|
|
91
101
|
}
|
|
102
|
+
} catch (error) {
|
|
103
|
+
logger.warn(`Failed to load TPS frames from ${jsonSrc}:`, error.message);
|
|
92
104
|
}
|
|
93
|
-
} catch (error) {
|
|
94
|
-
logger.warn(`Failed to load TPS frames from ${firstSrc}:`, error.message);
|
|
95
105
|
}
|
|
96
106
|
}
|
|
97
107
|
} else if (ext === '.json' && !firstSrc.includes('atlas')) {
|
package/cli/agent.mjs
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { bgRed, bold, cyan, green, white } from 'kleur/colors';
|
|
2
|
+
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* `caper agent init` — installs the shipped `caper` agent skill into the
|
|
9
|
+
* current app and upserts a marker-delimited pointer block into the app's
|
|
10
|
+
* agent context file (`AGENTS.md` or `CLAUDE.md`).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const START_MARKER = '<!-- caper:agent-start -->';
|
|
14
|
+
const END_MARKER = '<!-- caper:agent-end -->';
|
|
15
|
+
|
|
16
|
+
// `.href` so this also works where the global URL is a DOM polyfill (vitest + happy-dom).
|
|
17
|
+
function resolveModuleRelative(rel) {
|
|
18
|
+
return fileURLToPath(new URL(rel, import.meta.url).href);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function readVersion() {
|
|
22
|
+
return JSON.parse(fs.readFileSync(resolveModuleRelative('../package.json'), 'utf-8')).version;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function makeBlock(skillPath, version) {
|
|
26
|
+
return `${START_MARKER}
|
|
27
|
+
## Caper (engine) — agent pointers
|
|
28
|
+
|
|
29
|
+
This app runs on the Caper engine (\`@caperjs/core@${version}\`). Generated by \`caper agent init\`; re-run it after upgrading caper. Do not edit between these markers.
|
|
30
|
+
|
|
31
|
+
- Before engine-facing work (scenes, entities, popups, UI, plugins, \`caper.config.ts\`, assets, generated types), load the \`caper\` skill at \`${skillPath}\`.
|
|
32
|
+
- Read the reference by section, not whole: \`node_modules/@caperjs/core/extras/llms.txt\` (\`grep -n '^## '\` for the outline). Engine source ships at \`node_modules/@caperjs/core/src/\`.
|
|
33
|
+
- Verify: typecheck → build → drive the running app through \`window.Caper\` / \`Caper.automation[appId]\` (llms.txt §17). Never leave a dev server running in the foreground.
|
|
34
|
+
${END_MARKER}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function upsertContext(cwd, dir) {
|
|
38
|
+
const candidates = ['AGENTS.md', 'CLAUDE.md'];
|
|
39
|
+
let contextFile = candidates.find((name) => fs.existsSync(path.join(cwd, name)));
|
|
40
|
+
|
|
41
|
+
if (!contextFile) {
|
|
42
|
+
contextFile = 'AGENTS.md';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const contextPath = path.resolve(cwd, contextFile);
|
|
46
|
+
const existing = fs.existsSync(contextPath) ? fs.readFileSync(contextPath, 'utf-8') : '';
|
|
47
|
+
const skillPath = path.posix.join(dir, 'caper/SKILL.md');
|
|
48
|
+
const version = readVersion();
|
|
49
|
+
const block = makeBlock(skillPath, version);
|
|
50
|
+
|
|
51
|
+
const startIndex = existing.indexOf(START_MARKER);
|
|
52
|
+
const endIndex = existing.indexOf(END_MARKER);
|
|
53
|
+
|
|
54
|
+
let contents;
|
|
55
|
+
if (startIndex !== -1 && endIndex !== -1 && endIndex > startIndex) {
|
|
56
|
+
const before = existing.slice(0, startIndex);
|
|
57
|
+
const after = existing.slice(endIndex + END_MARKER.length);
|
|
58
|
+
const trimmedBefore = before.replace(/\s+$/, '');
|
|
59
|
+
const trimmedAfter = after.replace(/^\s+/, '');
|
|
60
|
+
const spacerBefore = trimmedBefore ? '\n\n' : '';
|
|
61
|
+
const spacerAfter = trimmedAfter ? '\n\n' : '';
|
|
62
|
+
contents = `${trimmedBefore}${spacerBefore}${block}${spacerAfter}${trimmedAfter}`;
|
|
63
|
+
} else if (existing) {
|
|
64
|
+
const spacer = existing.endsWith('\n\n') ? '' : '\n\n';
|
|
65
|
+
contents = `${existing.replace(/\s+$/, '')}${spacer}${block}\n`;
|
|
66
|
+
} else {
|
|
67
|
+
contents = `${block}\n`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
fs.writeFileSync(contextPath, contents, 'utf-8');
|
|
71
|
+
return contextPath;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function agentInit(cwd, { dir = '.claude/skills' } = {}) {
|
|
75
|
+
const skillSource = resolveModuleRelative('../extras/skills/caper/SKILL.md');
|
|
76
|
+
const skillDir = path.resolve(cwd, dir, 'caper');
|
|
77
|
+
const skillFile = path.join(skillDir, 'SKILL.md');
|
|
78
|
+
|
|
79
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
80
|
+
fs.copyFileSync(skillSource, skillFile);
|
|
81
|
+
|
|
82
|
+
const contextFile = upsertContext(cwd, dir);
|
|
83
|
+
|
|
84
|
+
console.log(green(bold('✓ Created caper skill')) + ` ${cyan(path.relative(cwd, skillFile).replace(/\\/g, '/'))}`);
|
|
85
|
+
console.log(green(bold('✓ Updated agent context')) + ` ${cyan(path.relative(cwd, contextFile).replace(/\\/g, '/'))}`);
|
|
86
|
+
|
|
87
|
+
return { skillFile, contextFile };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function agent(args) {
|
|
91
|
+
if (args[0] === 'init') {
|
|
92
|
+
let dir = '.claude/skills';
|
|
93
|
+
for (let i = 1; i < args.length; i++) {
|
|
94
|
+
if (args[i] === '--dir' && args[i + 1]) {
|
|
95
|
+
dir = args[i + 1];
|
|
96
|
+
i++;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
await agentInit(process.cwd(), { dir });
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const subcommand = args[0] ?? '(none)';
|
|
104
|
+
console.error(bold(bgRed(white(`Unknown agent command: "${subcommand}". Please use "init".`))));
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { agentInit } from './agent.mjs';
|
|
6
|
+
|
|
7
|
+
const shippedSkill = path.resolve(process.cwd(), 'extras/skills/caper/SKILL.md');
|
|
8
|
+
|
|
9
|
+
const START_MARKER = '<!-- caper:agent-start -->';
|
|
10
|
+
|
|
11
|
+
let tempDir = null;
|
|
12
|
+
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
if (tempDir && fs.existsSync(tempDir)) {
|
|
15
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
16
|
+
}
|
|
17
|
+
tempDir = null;
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
function makeTempDir() {
|
|
21
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'caper-agent-'));
|
|
22
|
+
return tempDir;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function countMarkers(contents) {
|
|
26
|
+
return {
|
|
27
|
+
start: (contents.match(/<!-- caper:agent-start -->/g) || []).length,
|
|
28
|
+
end: (contents.match(/<!-- caper:agent-end -->/g) || []).length,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe('agentInit', () => {
|
|
33
|
+
it('creates the default skill copy and AGENTS.md when no context file exists', async () => {
|
|
34
|
+
const cwd = makeTempDir();
|
|
35
|
+
|
|
36
|
+
const { skillFile, contextFile } = await agentInit(cwd);
|
|
37
|
+
|
|
38
|
+
expect(fs.existsSync(skillFile)).toBe(true);
|
|
39
|
+
expect(fs.readFileSync(skillFile, 'utf-8')).toBe(fs.readFileSync(shippedSkill, 'utf-8'));
|
|
40
|
+
expect(path.basename(contextFile)).toBe('AGENTS.md');
|
|
41
|
+
|
|
42
|
+
const context = fs.readFileSync(contextFile, 'utf-8');
|
|
43
|
+
expect(context).toContain('<!-- caper:agent-start -->');
|
|
44
|
+
expect(context).toContain('<!-- caper:agent-end -->');
|
|
45
|
+
expect(context).toContain('.claude/skills/caper/SKILL.md');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('respects --dir and references the custom skill path', async () => {
|
|
49
|
+
const cwd = makeTempDir();
|
|
50
|
+
|
|
51
|
+
const { skillFile, contextFile } = await agentInit(cwd, { dir: 'skills' });
|
|
52
|
+
|
|
53
|
+
expect(skillFile).toBe(path.join(cwd, 'skills/caper/SKILL.md'));
|
|
54
|
+
expect(fs.existsSync(skillFile)).toBe(true);
|
|
55
|
+
|
|
56
|
+
const context = fs.readFileSync(contextFile, 'utf-8');
|
|
57
|
+
expect(context).toContain('skills/caper/SKILL.md');
|
|
58
|
+
expect(context).not.toContain('.claude/skills/caper/SKILL.md');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('appends the block to CLAUDE.md when only CLAUDE.md exists', async () => {
|
|
62
|
+
const cwd = makeTempDir();
|
|
63
|
+
const original = '# Project CLAUDE.md\n\nSome existing guidance.\n';
|
|
64
|
+
fs.writeFileSync(path.join(cwd, 'CLAUDE.md'), original, 'utf-8');
|
|
65
|
+
|
|
66
|
+
const { skillFile, contextFile } = await agentInit(cwd);
|
|
67
|
+
|
|
68
|
+
expect(path.basename(contextFile)).toBe('CLAUDE.md');
|
|
69
|
+
expect(fs.existsSync(path.join(cwd, 'AGENTS.md'))).toBe(false);
|
|
70
|
+
|
|
71
|
+
const context = fs.readFileSync(contextFile, 'utf-8');
|
|
72
|
+
expect(context).toContain('Some existing guidance.');
|
|
73
|
+
expect(context).toContain('<!-- caper:agent-start -->');
|
|
74
|
+
expect(context).toContain('<!-- caper:agent-end -->');
|
|
75
|
+
expect(context.indexOf('Some existing guidance.')).toBeLessThan(context.indexOf(START_MARKER));
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('is idempotent and preserves user edits outside the markers', async () => {
|
|
79
|
+
const cwd = makeTempDir();
|
|
80
|
+
|
|
81
|
+
await agentInit(cwd);
|
|
82
|
+
const first = fs.readFileSync(path.join(cwd, 'AGENTS.md'), 'utf-8');
|
|
83
|
+
|
|
84
|
+
// User edits outside the markers
|
|
85
|
+
const edited = `# App pointers\n\n${first}\n\n## Custom section\n\nKeep me.\n`;
|
|
86
|
+
fs.writeFileSync(path.join(cwd, 'AGENTS.md'), edited, 'utf-8');
|
|
87
|
+
|
|
88
|
+
await agentInit(cwd);
|
|
89
|
+
const second = fs.readFileSync(path.join(cwd, 'AGENTS.md'), 'utf-8');
|
|
90
|
+
|
|
91
|
+
const markers = countMarkers(second);
|
|
92
|
+
expect(markers.start).toBe(1);
|
|
93
|
+
expect(markers.end).toBe(1);
|
|
94
|
+
expect(second).toContain('# App pointers');
|
|
95
|
+
expect(second).toContain('## Custom section');
|
|
96
|
+
expect(second).toContain('Keep me.');
|
|
97
|
+
|
|
98
|
+
// Replace the block with the freshly generated one so we can compare the rest
|
|
99
|
+
const start = second.indexOf('<!-- caper:agent-start -->');
|
|
100
|
+
const end = second.indexOf('<!-- caper:agent-end -->') + '<!-- caper:agent-end -->'.length;
|
|
101
|
+
const regenerated = second.slice(start, end);
|
|
102
|
+
expect(regenerated).toBe(first.trimEnd());
|
|
103
|
+
});
|
|
104
|
+
});
|
package/cli/create.mjs
CHANGED
|
@@ -6,6 +6,7 @@ import path from 'node:path';
|
|
|
6
6
|
import process from 'node:process';
|
|
7
7
|
import { promisify } from 'node:util';
|
|
8
8
|
import shell from 'shelljs';
|
|
9
|
+
import { agentInit } from './agent.mjs';
|
|
9
10
|
import { copy, dist, mkdirp, package_manager } from './utils.mjs';
|
|
10
11
|
|
|
11
12
|
let packageManager = package_manager;
|
|
@@ -150,7 +151,7 @@ export function injectPluginsIntoConfig(configContents, pluginConfigs) {
|
|
|
150
151
|
* @param {string} cwd
|
|
151
152
|
* @param {string[]} plugins
|
|
152
153
|
*/
|
|
153
|
-
function write_template_files(cwd, template, applicationNameForPkg, applicationName, defaultName, plugins) {
|
|
154
|
+
async function write_template_files(cwd, template, applicationNameForPkg, applicationName, defaultName, plugins) {
|
|
154
155
|
const dir = dist(`templates/${template}`);
|
|
155
156
|
copy(`${dir}/package.template.json`, `${cwd}/package.json`);
|
|
156
157
|
|
|
@@ -209,6 +210,14 @@ function write_template_files(cwd, template, applicationNameForPkg, applicationN
|
|
|
209
210
|
readme_contents = readme_contents.replace(/~PACKAGE_MANAGER~/g, mgr);
|
|
210
211
|
fs.writeFileSync(readme_file, readme_contents, 'utf-8');
|
|
211
212
|
|
|
213
|
+
// Install the shipped caper agent skill and upsert pointers into AGENTS.md/CLAUDE.md.
|
|
214
|
+
// This is best-effort: scaffolding must not fail because the agent init step failed.
|
|
215
|
+
try {
|
|
216
|
+
await agentInit(cwd);
|
|
217
|
+
} catch (e) {
|
|
218
|
+
console.warn('Warning: failed to install the caper agent skill:', e.message);
|
|
219
|
+
}
|
|
220
|
+
|
|
212
221
|
// find __APPLICATION_NAME__.ts and replace it with the application name, then delete ~Application.ts
|
|
213
222
|
const app_file = `${cwd}/src/__APPLICATION_NAME__.ts`;
|
|
214
223
|
const app_contents = fs.readFileSync(app_file, 'utf-8');
|
package/cli.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import { bgRed, bold, green, red, white } from 'kleur/colors';
|
|
|
5
5
|
import fs from 'node:fs';
|
|
6
6
|
import process from 'node:process';
|
|
7
7
|
import { add } from './cli/add.mjs';
|
|
8
|
+
import { agent } from './cli/agent.mjs';
|
|
8
9
|
import { generateCaptions } from './cli/audio/cc.mjs';
|
|
9
10
|
import { compress } from './cli/audio/index.mjs';
|
|
10
11
|
import { create } from './cli/create.mjs';
|
|
@@ -64,6 +65,9 @@ switch (args[0]) {
|
|
|
64
65
|
case 'add':
|
|
65
66
|
await add(args.slice(1));
|
|
66
67
|
break;
|
|
68
|
+
case 'agent':
|
|
69
|
+
await agent(args.slice(1));
|
|
70
|
+
break;
|
|
67
71
|
case 'create': {
|
|
68
72
|
let packageManager = 'npm'; // Default to npm
|
|
69
73
|
let projectPath = '.'; // Default to current directory
|
package/extras/llms.txt
CHANGED
|
@@ -7,7 +7,7 @@ on every `Container`, build-time auto-discovery of scenes/plugins/popups/entitie
|
|
|
7
7
|
and generated TypeScript types for app IDs and assets. Caper is a personal fork
|
|
8
8
|
of `dill-pixel` by Relish Studios.
|
|
9
9
|
|
|
10
|
-
**Package version:**
|
|
10
|
+
**Package version:** see `../package.json` (this file ships inside the package) · **Peer deps:** `pixi.js@8.x`, `@pixi/sound@^6`, `gsap@^3.13`.
|
|
11
11
|
|
|
12
12
|
**File citations in this guide** point at the `@caperjs/core` package source
|
|
13
13
|
(paths relative to this file, e.g. `../src/core/Application.ts`) — those ship
|
|
@@ -39,7 +39,8 @@ produce working code.
|
|
|
39
39
|
14. [Generated types & virtual modules](#14-generated-types--virtual-modules)
|
|
40
40
|
15. [Common recipes](#15-common-recipes)
|
|
41
41
|
16. [Gotchas & verification loop](#16-gotchas--verification-loop)
|
|
42
|
-
17. [
|
|
42
|
+
17. [Automation bridge — drive the running app](#17-automation-bridge--drive-the-running-app)
|
|
43
|
+
18. [API reference index](#18-api-reference-index)
|
|
43
44
|
|
|
44
45
|
---
|
|
45
46
|
|
|
@@ -1543,7 +1544,87 @@ cache produces confusing texture/name mismatches.
|
|
|
1543
1544
|
|
|
1544
1545
|
---
|
|
1545
1546
|
|
|
1546
|
-
## 17.
|
|
1547
|
+
## 17. Automation bridge — drive the running app
|
|
1548
|
+
|
|
1549
|
+
Every app registers itself on `window.Caper` at boot (source:
|
|
1550
|
+
[`../src/core/globals.ts`](../src/core/globals.ts)). This is how an agent,
|
|
1551
|
+
Playwright script, or smoke test drives a Caper app without clicking around.
|
|
1552
|
+
|
|
1553
|
+
### 17.1 What is always there
|
|
1554
|
+
|
|
1555
|
+
```ts
|
|
1556
|
+
Caper.apps // Map<appId, app> (appId = caper.config.ts `id`, else 'CaperApplication')
|
|
1557
|
+
Caper.app // last created app
|
|
1558
|
+
await Caper.ready() // resolves when the first app has fully booted (safe to call early)
|
|
1559
|
+
await Caper.ready('my-app-id')
|
|
1560
|
+
```
|
|
1561
|
+
|
|
1562
|
+
### 17.2 The automation facade (gated)
|
|
1563
|
+
|
|
1564
|
+
Enabled when **any** of: dev server (`import.meta.env.DEV`), `automation: true`
|
|
1565
|
+
in `caper.config.ts`, or env `VITE_CAPER_AUTOMATION=true` at build time. When on,
|
|
1566
|
+
`Caper.automation[appId]` (also `app.automation`) exposes:
|
|
1567
|
+
|
|
1568
|
+
| Member | What it does |
|
|
1569
|
+
| --- | --- |
|
|
1570
|
+
| `action(name, data?)` | `app.sendAction(name, data)`. Subject to the ActionsPlugin context rules — out-of-context actions are dropped, same as a real input. |
|
|
1571
|
+
| `getContext()` | Current action context as a string (e.g. `'game'`, `'popup'`). |
|
|
1572
|
+
| `getState()` | Whatever the app's registered state getter returns (`undefined` until registered). |
|
|
1573
|
+
| `registerStateGetter(fn)` | App code calls this once to expose a snapshot of game state. |
|
|
1574
|
+
| `notifyStateChanged(state)` | App code calls this on each meaningful change; re-runs pending `waitFor` predicates and logs a `state` entry. |
|
|
1575
|
+
| `waitFor(predicate, { timeoutMs? })` | Resolves with the state the first time `predicate(state)` is true (checked immediately, then on every `notifyStateChanged`). Rejects on timeout. |
|
|
1576
|
+
| `log` | Ring buffer of the last 200 entries: `{ t, kind: 'action' \| 'state' \| 'context', name?, data? }`. Only **dispatched** actions are logged (dropped ones are not). |
|
|
1577
|
+
|
|
1578
|
+
### 17.3 App-side wiring (once per game)
|
|
1579
|
+
|
|
1580
|
+
```ts
|
|
1581
|
+
// in the main scene's initialize(), or wherever the game state lives
|
|
1582
|
+
this.app.automation?.registerStateGetter(() => this.store.snapshot());
|
|
1583
|
+
this.store.onChanged.connect((s) => this.app.automation?.notifyStateChanged(s));
|
|
1584
|
+
```
|
|
1585
|
+
|
|
1586
|
+
Keep the snapshot plain data (no display objects) so predicates stay cheap and
|
|
1587
|
+
serializable for drivers outside the page.
|
|
1588
|
+
|
|
1589
|
+
### 17.4 Driving it
|
|
1590
|
+
|
|
1591
|
+
Use a browser you own. Launch a separate Chromium with Playwright (isolated,
|
|
1592
|
+
throwaway profile) and point it at the dev server; do **not** attach to or drive the
|
|
1593
|
+
human's own browser session unless they ask you to look at one of their tabs.
|
|
1594
|
+
|
|
1595
|
+
```ts
|
|
1596
|
+
import { chromium } from 'playwright';
|
|
1597
|
+
const browser = await chromium.launch(); // headless; { headless: false } only when a human watches
|
|
1598
|
+
const page = await browser.newPage({ viewport: { width: 1280, height: 720 } });
|
|
1599
|
+
page.on('pageerror', (e) => console.error('pageerror', e));
|
|
1600
|
+
await page.goto('http://localhost:3000/');
|
|
1601
|
+
await page.waitForFunction(() => window.Caper?.__readyApps?.size > 0);
|
|
1602
|
+
const result = await page.evaluate(async () => {
|
|
1603
|
+
const app = await Caper.ready();
|
|
1604
|
+
const a = Caper.automation[app.config.id];
|
|
1605
|
+
a.action('fire');
|
|
1606
|
+
const s = await a.waitFor((st) => st?.phase === 'resolved', { timeoutMs: 10_000 });
|
|
1607
|
+
return { context: a.getContext(), state: s, actions: a.log.filter((e) => e.kind === 'action').map((e) => e.name) };
|
|
1608
|
+
});
|
|
1609
|
+
await page.screenshot({ path: 'scratch/after-fire.png' });
|
|
1610
|
+
await browser.close();
|
|
1611
|
+
```
|
|
1612
|
+
|
|
1613
|
+
Rules of thumb:
|
|
1614
|
+
|
|
1615
|
+
- Prefer `action()` over synthesising pointer events — it goes through the same
|
|
1616
|
+
ActionsPlugin path as a real button, so context gating and action signals behave
|
|
1617
|
+
identically.
|
|
1618
|
+
- If an action seems ignored, check `getContext()` first: it was probably dropped
|
|
1619
|
+
as out-of-context. That is by design (see §1 / gotchas), not a bug.
|
|
1620
|
+
- `waitFor` only re-evaluates on `notifyStateChanged`; if it never resolves, the
|
|
1621
|
+
app is not calling `notifyStateChanged` for that transition.
|
|
1622
|
+
- For a boot-only smoke test you need none of the facade: assert
|
|
1623
|
+
`Caper.apps.size > 0` after `Caper.ready()` and that the page logged no errors.
|
|
1624
|
+
|
|
1625
|
+
---
|
|
1626
|
+
|
|
1627
|
+
## 18. API reference index
|
|
1547
1628
|
|
|
1548
1629
|
Flat jump table. Name → section → one-line.
|
|
1549
1630
|
|
|
@@ -1604,5 +1685,5 @@ Flat jump table. Name → section → one-line.
|
|
|
1604
1685
|
|
|
1605
1686
|
---
|
|
1606
1687
|
|
|
1607
|
-
**Last updated:** `@caperjs/core@
|
|
1688
|
+
**Last updated:** with `@caperjs/core@0.5.x` (PixiJS 8.x); version in `../package.json`. When in doubt, read the
|
|
1608
1689
|
cited framework source file — signatures and defaults are authoritative there.
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: caper
|
|
3
|
+
description: Work on a game built with the Caper engine (@caperjs/core, PixiJS v8). Use before touching scenes, entities, popups, UI, plugins, caper.config.ts, assets, generated types, or the dev/verify loop in any app that depends on @caperjs/core. Routes you to the exact section of the shipped reference (llms.txt) instead of the whole engine, and gives the verify loop (typecheck, build, headless automation bridge). Trigger for "caper", "add a scene/entity/popup/plugin", "factory methods", "add.sprite", "defineScene", "actions", "UICanvas", "FlexContainer", "caper.config", "caper-app.d.ts", "assetpack", "window.Caper", or any error mentioning @caperjs.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Caper — how to work in a Caper app
|
|
7
|
+
|
|
8
|
+
This skill is shipped by `@caperjs/core` and copied here by `caper agent init`.
|
|
9
|
+
Do not edit it; re-run `npx caper agent init` after upgrading caper.
|
|
10
|
+
|
|
11
|
+
## 1. Read the reference by section, never whole
|
|
12
|
+
|
|
13
|
+
The full consumer reference is `node_modules/@caperjs/core/extras/llms.txt`
|
|
14
|
+
(~1,600 lines). Load only the section you need:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
grep -n '^## ' node_modules/@caperjs/core/extras/llms.txt # section line numbers
|
|
18
|
+
sed -n '<start>,<end>p' node_modules/@caperjs/core/extras/llms.txt
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
| You need to… | Read section |
|
|
22
|
+
| --------------------------------------------- | ---------------------------------- |
|
|
23
|
+
| know the rules before writing any code | §1 Rules of engagement |
|
|
24
|
+
| scaffold a scene / entity / popup / plugin | §2.2 CLI, §15 Recipes |
|
|
25
|
+
| boot, `create()`, custom `Application` | §3 |
|
|
26
|
+
| `caper.config.ts`, actions, contexts, data | §4 |
|
|
27
|
+
| scene lifecycle, assets per scene, transitions| §5 |
|
|
28
|
+
| build a display tree (`add.*` / `make.*`) | §6 (full catalog at §6.1) |
|
|
29
|
+
| buttons, flex layout, popups, toasts, HUD | §7 |
|
|
30
|
+
| `app.scenes / actions / popups / audio / …` | §8 |
|
|
31
|
+
| write or fix a plugin | §9 |
|
|
32
|
+
| assets, bundles, AssetPack tags | §11 |
|
|
33
|
+
| signals, store, mixins | §12 |
|
|
34
|
+
| generated types, virtual modules | §14 |
|
|
35
|
+
| drive the running app headlessly | §17 Automation bridge |
|
|
36
|
+
| look up one symbol | §18 API index (name → section) |
|
|
37
|
+
|
|
38
|
+
Engine source also ships: `node_modules/@caperjs/core/src/`. When the doc is
|
|
39
|
+
not enough, open the one file the doc cites, not the whole tree.
|
|
40
|
+
|
|
41
|
+
## 2. The rules (llms.txt §1, condensed)
|
|
42
|
+
|
|
43
|
+
1. `caper.config.ts` `plugins: [...]` is authoritative; list every plugin,
|
|
44
|
+
including ones other plugins `require`.
|
|
45
|
+
2. Fail loud. No silent fallbacks in plugins or config.
|
|
46
|
+
3. Metadata goes on `defineScene / defineEntity / definePopup / definePlugin`
|
|
47
|
+
wrappers, not class statics. Default-export the class so discovery finds it.
|
|
48
|
+
4. `this.add.*` / `this.make.*` over `new Sprite()` / `new Text()` /
|
|
49
|
+
`new Graphics()` / `new Container()`. Extend Caper's `Container`, not Pixi's.
|
|
50
|
+
5. `npx caper add scene|entity|popup|plugin <Name>` before hand-authoring.
|
|
51
|
+
6. Verify in the running app, not in isolation (see §3 below).
|
|
52
|
+
|
|
53
|
+
## 3. Verify loop
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pnpm typecheck # or the app's tsc script; first gate after any .ts change
|
|
57
|
+
pnpm build # vite build; catches asset and discovery errors
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Then check behaviour live, cheapest first:
|
|
61
|
+
|
|
62
|
+
- **Use your own browser, never the human's.** Launch a separate Chromium with
|
|
63
|
+
Playwright (a script via `npx tsx`, or the Playwright MCP tools) pointed at the
|
|
64
|
+
dev-server URL. Do **not** attach to or drive the human's own Chrome (for
|
|
65
|
+
example claude-in-chrome tabs) unless they ask you to look at a tab of theirs.
|
|
66
|
+
Headless for checks; headed (`headless: false`) only when a human will watch.
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
import { chromium } from 'playwright';
|
|
70
|
+
const browser = await chromium.launch(); // isolated, throwaway profile
|
|
71
|
+
const page = await browser.newPage({ viewport: { width: 1280, height: 720 } });
|
|
72
|
+
page.on('pageerror', (e) => console.error('pageerror', e));
|
|
73
|
+
await page.goto('http://localhost:3000/');
|
|
74
|
+
await page.waitForFunction(() => window.Caper?.__readyApps?.size > 0);
|
|
75
|
+
await page.screenshot({ path: 'scratch/after-change.png' });
|
|
76
|
+
await browser.close();
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
- **Headless via the automation bridge** (dev server, or `automation: true`
|
|
80
|
+
in config, or `VITE_CAPER_AUTOMATION=true`): from Playwright or a browser
|
|
81
|
+
tool, `const app = await Caper.ready(); const a = Caper.automation[app.config.id];`
|
|
82
|
+
then `a.action('name', data)`, `a.getContext()`, `a.getState()`,
|
|
83
|
+
`await a.waitFor(s => …, { timeoutMs })`, and inspect `a.log` (last 200
|
|
84
|
+
actions / context changes / state pushes). Details: llms.txt §17.
|
|
85
|
+
- **Do not start the dev server yourself** and leave it running in the
|
|
86
|
+
foreground; it never exits. If the human has it running, use it. Otherwise
|
|
87
|
+
ask them to run `pnpm dev` and report.
|
|
88
|
+
|
|
89
|
+
## 4. Gotchas that cost agents the most time
|
|
90
|
+
|
|
91
|
+
- Out-of-context actions are **dropped by the ActionsPlugin**. That is the
|
|
92
|
+
phase guard; do not add `if (phase !== …)` checks around `sendAction`.
|
|
93
|
+
- `src/types/caper-app.d.ts` and `caper-assets.d.ts` are **generated** on dev
|
|
94
|
+
server start. Never hand-edit. Missing or stale → delete them and have the
|
|
95
|
+
dev server run once. They are usually gitignored, so a fresh clone has no
|
|
96
|
+
typed ids until then.
|
|
97
|
+
- Weird asset / name mismatches → `rm -rf .assetpack .cache dist` and rebuild.
|
|
98
|
+
- Renamed a scene / popup / entity id? Regenerate types (above) or old ids
|
|
99
|
+
linger in the unions and cause confusing downstream errors.
|
|
100
|
+
- If the engine is **linked from a local checkout**, consumer apps resolve the
|
|
101
|
+
built `lib/`; rebuild the engine (`pnpm framework:build` in the caper repo)
|
|
102
|
+
after editing engine source, unless the app's vite config aliases
|
|
103
|
+
`@caperjs/core` to the engine `src/`.
|
package/lib/caper.mjs
CHANGED
|
@@ -204,7 +204,7 @@ function B(e, t) {
|
|
|
204
204
|
}
|
|
205
205
|
//#endregion
|
|
206
206
|
//#region src/version.ts
|
|
207
|
-
var V = "0.5.
|
|
207
|
+
var V = "0.5.2", H = "8.19.0", Mn = " ........ ..... ............ ............. ........... \n ............. ....... ................ ............. .............. \n .............. ......... ............... ............. .............. \n ..... ........... .............. ............ .............. \n ..... ...... ...... .............. ............. ............. \n .............. ...... ..... ............ ............. ..... ...... \n ................... ..... ..... ............. ..... ....... \n ................ .......... ............. ..... ..... ";
|
|
208
208
|
function U() {
|
|
209
209
|
let e = `\n${Mn}\n\n v${V} | %cPixi.js v${H} %c| %chttps://github.com/anthonysapp/caper\n\n`;
|
|
210
210
|
console.log(e, "color: #E91E63; font-weight: 600;", "color: inherit;", "color: #00BCD4; text-decoration: underline;");
|