@caperjs/core 0.5.2 → 0.6.1
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/build/assetpack.mjs +6 -0
- package/build/plugins/assetTypes.mjs +7 -2
- package/build/plugins/caperConfig.mjs +3 -0
- package/cli/agent.mjs +10 -4
- package/cli/doctor.mjs +193 -0
- package/cli/doctor.test.mjs +142 -0
- package/cli/probe.mjs +368 -0
- package/cli/probe.test.mjs +74 -0
- package/cli/types.mjs +101 -0
- package/cli/types.test.mjs +58 -0
- package/cli.mjs +8 -0
- package/extras/llms.txt +21 -7
- package/extras/skills/caper/SKILL.md +14 -10
- package/lib/{CaptionsPlugin-CDbGZrN9.js → CaptionsPlugin-BtVJiQs_.js} +2 -2
- package/lib/{CaptionsPlugin-CDbGZrN9.js.map → CaptionsPlugin-BtVJiQs_.js.map} +1 -1
- package/lib/{DataAdapter-BTlNzWqg.js → DataAdapter-BtNz4yi2.js} +2 -2
- package/lib/{DataAdapter-BTlNzWqg.js.map → DataAdapter-BtNz4yi2.js.map} +1 -1
- package/lib/{DebugRenderer-eSVpon4c.js → DebugRenderer-DK8FAED1.js} +4 -4
- package/lib/{DebugRenderer-eSVpon4c.js.map → DebugRenderer-DK8FAED1.js.map} +1 -1
- package/lib/{DevToolsPlugin-Ci6r9j-m.js → DevToolsPlugin-BS9idGRi.js} +3 -3
- package/lib/{DevToolsPlugin-Ci6r9j-m.js.map → DevToolsPlugin-BS9idGRi.js.map} +1 -1
- package/lib/{GSAPPlugin-C8zo-fyt.js → GSAPPlugin-DVDbDVP8.js} +2 -2
- package/lib/{GSAPPlugin-C8zo-fyt.js.map → GSAPPlugin-DVDbDVP8.js.map} +1 -1
- package/lib/{LayoutPlugin-Cs2lcXKb.js → LayoutPlugin-qQJ5qJud.js} +26 -26
- package/lib/{LayoutPlugin-Cs2lcXKb.js.map → LayoutPlugin-qQJ5qJud.js.map} +1 -1
- package/lib/{SpinePlugin-CLgSXBQm.js → SpinePlugin-CPNRM2J1.js} +2 -2
- package/lib/{SpinePlugin-CLgSXBQm.js.map → SpinePlugin-CPNRM2J1.js.map} +1 -1
- package/lib/{StatsPlugin-D_90U3BG.js → StatsPlugin-BHihicBl.js} +2 -2
- package/lib/{StatsPlugin-D_90U3BG.js.map → StatsPlugin-BHihicBl.js.map} +1 -1
- package/lib/{VoiceOverPlugin-CAE1dtOw.js → VoiceOverPlugin-BdTHxgku.js} +2 -2
- package/lib/{VoiceOverPlugin-CAE1dtOw.js.map → VoiceOverPlugin-BdTHxgku.js.map} +1 -1
- package/lib/caper.mjs +5 -5
- package/lib/caper.mjs.map +1 -1
- package/lib/{const-BTDpIdRO.js → const-BQItSP39.js} +2 -2
- package/lib/{const-BTDpIdRO.js.map → const-BQItSP39.js.map} +1 -1
- package/lib/{registries-nN_hU44y.js → registries-DlqHRO90.js} +16 -16
- package/lib/{registries-nN_hU44y.js.map → registries-DlqHRO90.js.map} +1 -1
- package/package.json +3 -3
- package/src/version.ts +2 -2
package/build/assetpack.mjs
CHANGED
|
@@ -302,6 +302,12 @@ export function assetpackPlugin(manifestUrl = defaultManifestUrl, pixiPipesConfi
|
|
|
302
302
|
|
|
303
303
|
return {
|
|
304
304
|
name: 'vite-plugin-assetpack',
|
|
305
|
+
api: {
|
|
306
|
+
runOnce: async () => {
|
|
307
|
+
await getConfig();
|
|
308
|
+
await new AssetPack(apConfig).run();
|
|
309
|
+
},
|
|
310
|
+
},
|
|
305
311
|
async configResolved(resolvedConfig) {
|
|
306
312
|
mode = resolvedConfig.command;
|
|
307
313
|
// Captured before getConfig() below, which is what consumes it.
|
|
@@ -398,8 +398,10 @@ export function assetTypesPlugin(manifestUrl = 'assets.json') {
|
|
|
398
398
|
}
|
|
399
399
|
const manifest = JSON.parse(await fs.promises.readFile(manifestPath, 'utf8'));
|
|
400
400
|
await writeAssetTypes(manifest, path.dirname(manifestPath), path.join(publicDir, 'assets'), root);
|
|
401
|
-
|
|
402
|
-
|
|
401
|
+
if (viteServer) {
|
|
402
|
+
logger.info('Caper asset types plugin:: manifest changed, reloading browser...');
|
|
403
|
+
viteServer.ws.send({ type: 'full-reload' });
|
|
404
|
+
}
|
|
403
405
|
} catch (error) {
|
|
404
406
|
logger.error('Caper asset types plugin:: Error handling manifest change:', error);
|
|
405
407
|
}
|
|
@@ -409,6 +411,9 @@ export function assetTypesPlugin(manifestUrl = 'assets.json') {
|
|
|
409
411
|
|
|
410
412
|
return {
|
|
411
413
|
name: 'vite-plugin-asset-types',
|
|
414
|
+
api: {
|
|
415
|
+
generateTypes: () => generate(manifestUrl),
|
|
416
|
+
},
|
|
412
417
|
configResolved(config) {
|
|
413
418
|
publicDir = config.publicDir;
|
|
414
419
|
root = config.root;
|
|
@@ -418,6 +418,9 @@ declare module '@caperjs/core' {
|
|
|
418
418
|
|
|
419
419
|
return {
|
|
420
420
|
name: 'vite-plugin-caper-config',
|
|
421
|
+
api: {
|
|
422
|
+
generateTypes: () => build('Generating types from caper.config.ts'),
|
|
423
|
+
},
|
|
421
424
|
configResolved(config) {
|
|
422
425
|
publicDir = config.publicDir;
|
|
423
426
|
root = config.root;
|
package/cli/agent.mjs
CHANGED
|
@@ -3,11 +3,12 @@ import { bgRed, bold, cyan, green, white } from 'kleur/colors';
|
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { probe } from './probe.mjs';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
|
-
* `caper agent init`
|
|
9
|
-
* current app
|
|
10
|
-
*
|
|
9
|
+
* `caper agent <subcommand>` — `init` installs the shipped `caper` agent skill
|
|
10
|
+
* into the current app; `probe` drives a running Caper app through the
|
|
11
|
+
* Playwright automation bridge.
|
|
11
12
|
*/
|
|
12
13
|
|
|
13
14
|
const START_MARKER = '<!-- caper:agent-start -->';
|
|
@@ -100,7 +101,12 @@ export async function agent(args) {
|
|
|
100
101
|
return;
|
|
101
102
|
}
|
|
102
103
|
|
|
104
|
+
if (args[0] === 'probe') {
|
|
105
|
+
await probe(args.slice(1));
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
103
109
|
const subcommand = args[0] ?? '(none)';
|
|
104
|
-
console.error(bold(bgRed(white(`Unknown agent command: "${subcommand}". Please use "init".`))));
|
|
110
|
+
console.error(bold(bgRed(white(`Unknown agent command: "${subcommand}". Please use "init" or "probe".`))));
|
|
105
111
|
process.exit(1);
|
|
106
112
|
}
|
package/cli/doctor.mjs
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { dim, green, red, yellow } from 'kleur/colors';
|
|
2
|
+
|
|
3
|
+
import { execFile } from 'node:child_process';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* `caper doctor` — one-shot health report for a Caper app.
|
|
10
|
+
*
|
|
11
|
+
* Answers the recurring agent questions: which caper is active, is it current,
|
|
12
|
+
* are generated types fresh, and are the agent pointers installed.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const START_MARKER = '<!-- caper:agent-start -->';
|
|
16
|
+
const END_MARKER = '<!-- caper:agent-end -->';
|
|
17
|
+
|
|
18
|
+
const readInstalledVersion = () => {
|
|
19
|
+
const url = new URL('../package.json', import.meta.url);
|
|
20
|
+
const pkgPath = url.protocol === 'file:' ? fileURLToPath(url.href) : path.resolve(process.cwd(), 'package.json');
|
|
21
|
+
return JSON.parse(fs.readFileSync(pkgPath, 'utf-8')).version;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const rel = (cwd, p) => {
|
|
25
|
+
const r = path.relative(cwd, p);
|
|
26
|
+
return r.startsWith('..') ? r : `.${path.sep}${r}`;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const newestMtime = (dirOrFile) => {
|
|
30
|
+
if (!fs.existsSync(dirOrFile)) return null;
|
|
31
|
+
const stat = fs.statSync(dirOrFile);
|
|
32
|
+
if (stat.isFile()) return stat.mtime;
|
|
33
|
+
if (!stat.isDirectory()) return null;
|
|
34
|
+
let newest = null;
|
|
35
|
+
for (const entry of fs.readdirSync(dirOrFile)) {
|
|
36
|
+
if (entry === 'node_modules' || entry === '.git') continue;
|
|
37
|
+
const childNewest = newestMtime(path.join(dirOrFile, entry));
|
|
38
|
+
if (childNewest && (!newest || childNewest > newest)) newest = childNewest;
|
|
39
|
+
}
|
|
40
|
+
return newest;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const compareVersions = (a, b) => {
|
|
44
|
+
const ap = a.split('.').map(Number);
|
|
45
|
+
const bp = b.split('.').map(Number);
|
|
46
|
+
for (let i = 0; i < Math.max(ap.length, bp.length); i++) {
|
|
47
|
+
const av = ap[i] ?? 0;
|
|
48
|
+
const bv = bp[i] ?? 0;
|
|
49
|
+
if (av < bv) return -1;
|
|
50
|
+
if (av > bv) return 1;
|
|
51
|
+
}
|
|
52
|
+
return 0;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const npmLatestVersion = (pkg, timeoutMs = 5000) =>
|
|
56
|
+
new Promise((resolve) => {
|
|
57
|
+
const timer = setTimeout(() => resolve(null), timeoutMs);
|
|
58
|
+
execFile('npm', ['view', pkg, 'version'], { timeout: timeoutMs }, (err, stdout) => {
|
|
59
|
+
clearTimeout(timer);
|
|
60
|
+
resolve(err || !stdout ? null : stdout.trim());
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const push = (checks, id, status, label, hint) => {
|
|
65
|
+
checks.push({ id, status, label, ...(hint ? { hint } : {}) });
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export async function runChecks(cwd, { online = true } = {}) {
|
|
69
|
+
const checks = [];
|
|
70
|
+
const installedVersion = readInstalledVersion();
|
|
71
|
+
const nodeModulesCaper = path.join(cwd, 'node_modules/@caperjs/core');
|
|
72
|
+
|
|
73
|
+
push(checks, 'version', 'ok', `@caperjs/core ${installedVersion}`);
|
|
74
|
+
|
|
75
|
+
if (online) {
|
|
76
|
+
const latest = await npmLatestVersion('@caperjs/core');
|
|
77
|
+
if (!latest) push(checks, 'npm', 'warn', 'latest on npm', 'lookup failed (offline?)');
|
|
78
|
+
else if (compareVersions(installedVersion, latest) < 0) {
|
|
79
|
+
push(checks, 'npm', 'warn', `latest on npm ${latest}`, 'pnpm update @caperjs/core@latest');
|
|
80
|
+
} else push(checks, 'npm', 'ok', `latest on npm ${latest}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!fs.existsSync(nodeModulesCaper)) {
|
|
84
|
+
push(checks, 'link', 'fail', '@caperjs/core not in node_modules', 'pnpm install');
|
|
85
|
+
} else {
|
|
86
|
+
let real;
|
|
87
|
+
try {
|
|
88
|
+
real = fs.realpathSync(nodeModulesCaper);
|
|
89
|
+
} catch {
|
|
90
|
+
push(checks, 'link', 'fail', 'node_modules/@caperjs/core is broken', 'pnpm install');
|
|
91
|
+
}
|
|
92
|
+
if (real) {
|
|
93
|
+
if (real.startsWith(path.join(cwd, 'node_modules') + path.sep)) {
|
|
94
|
+
push(checks, 'link', 'ok', 'registry build');
|
|
95
|
+
} else {
|
|
96
|
+
const buildFile = path.join(real, 'lib/caper.mjs');
|
|
97
|
+
const label = `linked from ${rel(cwd, real)}`;
|
|
98
|
+
if (!fs.existsSync(buildFile)) {
|
|
99
|
+
push(checks, 'link', 'fail', label, 'pnpm build in linked package');
|
|
100
|
+
} else {
|
|
101
|
+
const srcMtime = newestMtime(path.join(real, 'src'));
|
|
102
|
+
const stale = srcMtime && srcMtime > fs.statSync(buildFile).mtime;
|
|
103
|
+
push(checks, 'link', stale ? 'warn' : 'ok', label, stale ? 'engine source edited after last build; run pnpm build there' : undefined);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const appTypesFile = path.join(cwd, 'src/types/caper-app.d.ts');
|
|
110
|
+
if (!fs.existsSync(appTypesFile)) {
|
|
111
|
+
push(checks, 'app-types', 'fail', 'generated app types missing', 'npx caper types');
|
|
112
|
+
} else {
|
|
113
|
+
const sources = ['caper.config.ts', 'src/scenes', 'src/plugins', 'src/popups', 'src/entities', 'src/ui', 'src/locales'].map((s) => path.join(cwd, s));
|
|
114
|
+
const newestSource = sources.reduce((best, s) => {
|
|
115
|
+
const m = newestMtime(s);
|
|
116
|
+
return m && (!best || m > best) ? m : best;
|
|
117
|
+
}, null);
|
|
118
|
+
const stale = newestSource && newestSource > fs.statSync(appTypesFile).mtime;
|
|
119
|
+
push(checks, 'app-types', stale ? 'warn' : 'ok', stale ? 'generated app types stale' : 'generated app types fresh', stale ? 'npx caper types (or restart dev server)' : undefined);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const assetTypesFile = path.join(cwd, 'src/types/caper-assets.d.ts');
|
|
123
|
+
const assetsManifest = path.join(cwd, 'public/assets/assets.json');
|
|
124
|
+
if (fs.existsSync(assetTypesFile)) push(checks, 'asset-types', 'ok', 'generated asset types present');
|
|
125
|
+
else push(checks, 'asset-types', 'warn', 'generated asset types missing', 'npx caper types (app may have assets: false)');
|
|
126
|
+
if (fs.existsSync(assetsManifest)) push(checks, 'asset-manifest', 'ok', 'asset manifest present');
|
|
127
|
+
else push(checks, 'asset-manifest', 'warn', 'asset manifest missing', 'asset pipeline has not run; npx caper types or pnpm dev once');
|
|
128
|
+
|
|
129
|
+
const agentContext = ['AGENTS.md', 'CLAUDE.md'].find((name) => fs.existsSync(path.join(cwd, name)));
|
|
130
|
+
if (!agentContext) {
|
|
131
|
+
push(checks, 'agent', 'warn', 'agent context missing', 'npx caper agent init');
|
|
132
|
+
} else {
|
|
133
|
+
const contents = fs.readFileSync(path.join(cwd, agentContext), 'utf-8');
|
|
134
|
+
const start = contents.indexOf(START_MARKER);
|
|
135
|
+
const end = contents.indexOf(END_MARKER);
|
|
136
|
+
if (start === -1 || end === -1 || end <= start) {
|
|
137
|
+
push(checks, 'agent', 'warn', 'agent pointers not installed', 'npx caper agent init');
|
|
138
|
+
} else {
|
|
139
|
+
const block = contents.slice(start, end + END_MARKER.length);
|
|
140
|
+
const versionMatch = block.match(/@caperjs\/core@(\d+\.\d+\.\d+)/);
|
|
141
|
+
const skillMatch = block.match(/load the `caper` skill at `([^`]+)`/);
|
|
142
|
+
const hints = [];
|
|
143
|
+
if (versionMatch && versionMatch[1] !== installedVersion) {
|
|
144
|
+
hints.push(`pointer is @caperjs/core@${versionMatch[1]}; re-run npx caper agent init`);
|
|
145
|
+
}
|
|
146
|
+
if (skillMatch) {
|
|
147
|
+
if (!fs.existsSync(path.join(cwd, skillMatch[1].replace(/\//g, path.sep)))) {
|
|
148
|
+
hints.push(`skill file missing at ${skillMatch[1]}; npx caper agent init`);
|
|
149
|
+
}
|
|
150
|
+
} else hints.push('skill path not found in pointer block; npx caper agent init');
|
|
151
|
+
push(checks, 'agent', hints.length ? 'warn' : 'ok', 'agent pointers', hints.length ? hints.join('; ') : undefined);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const peerDeps = ['pixi.js', 'gsap', '@pixi/sound', 'vite'];
|
|
156
|
+
const required = new Set(['pixi.js', 'vite']);
|
|
157
|
+
let peerStatus = 'ok';
|
|
158
|
+
// Direct lookup rather than require.resolve: pixi.js's `exports` map does not
|
|
159
|
+
// expose ./package.json, so resolve() throws even when it is installed.
|
|
160
|
+
const peerResults = peerDeps.map((pkg) => {
|
|
161
|
+
try {
|
|
162
|
+
const pkgJsonPath = path.join(cwd, 'node_modules', pkg, 'package.json');
|
|
163
|
+
return `${pkg}@${JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8')).version}`;
|
|
164
|
+
} catch {
|
|
165
|
+
if (required.has(pkg)) peerStatus = 'fail';
|
|
166
|
+
else if (peerStatus === 'ok') peerStatus = 'warn';
|
|
167
|
+
return `${pkg} missing`;
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
push(checks, 'peers', peerStatus, `peer deps: ${peerResults.join(', ')}`, peerStatus !== 'ok' ? 'pnpm install' : undefined);
|
|
171
|
+
|
|
172
|
+
const caches = ['.assetpack', '.cache', 'dist'].filter((name) => fs.existsSync(path.join(cwd, name)));
|
|
173
|
+
push(checks, 'caches', 'ok', `caches${caches.length ? `: ${caches.join(', ')}` : ' clean'}`, caches.length ? 'rm -rf them on weird asset/name mismatches' : undefined);
|
|
174
|
+
|
|
175
|
+
return checks;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export async function doctor(args) {
|
|
179
|
+
const offline = args.includes('--offline');
|
|
180
|
+
const json = args.includes('--json');
|
|
181
|
+
const checks = await runChecks(process.cwd(), { online: !offline });
|
|
182
|
+
|
|
183
|
+
if (json) {
|
|
184
|
+
console.log(JSON.stringify(checks, null, 2));
|
|
185
|
+
} else {
|
|
186
|
+
for (const check of checks) {
|
|
187
|
+
const symbol = check.status === 'ok' ? green('✓') : check.status === 'warn' ? yellow('⚠') : red('✗');
|
|
188
|
+
console.log(`${symbol} ${check.label}${check.hint ? ` ${dim(check.hint)}` : ''}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (checks.some((c) => c.status === 'fail')) process.exit(1);
|
|
193
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
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
|
+
|
|
6
|
+
import { agentInit } from './agent.mjs';
|
|
7
|
+
import { runChecks } from './doctor.mjs';
|
|
8
|
+
|
|
9
|
+
const START_MARKER = '<!-- caper:agent-start -->';
|
|
10
|
+
const END_MARKER = '<!-- caper:agent-end -->';
|
|
11
|
+
|
|
12
|
+
const installedVersion = JSON.parse(fs.readFileSync(path.resolve(process.cwd(), 'package.json'), 'utf-8')).version;
|
|
13
|
+
|
|
14
|
+
let tempDir = null;
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
if (tempDir && fs.existsSync(tempDir)) {
|
|
18
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
19
|
+
}
|
|
20
|
+
tempDir = null;
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
function makeTempDir() {
|
|
24
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'caper-doctor-'));
|
|
25
|
+
return tempDir;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function setMtime(file, secondsAgo) {
|
|
29
|
+
const t = Date.now() / 1000 - secondsAgo;
|
|
30
|
+
fs.utimesSync(file, t, t);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function find(checks, id) {
|
|
34
|
+
return checks.find((c) => c.id === id);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe('runChecks', () => {
|
|
38
|
+
it('reports failures/warnings for an empty app directory', async () => {
|
|
39
|
+
const cwd = makeTempDir();
|
|
40
|
+
|
|
41
|
+
const checks = await runChecks(cwd, { online: false });
|
|
42
|
+
|
|
43
|
+
expect(find(checks, 'version').status).toBe('ok');
|
|
44
|
+
expect(find(checks, 'link').status).toBe('fail');
|
|
45
|
+
expect(find(checks, 'app-types').status).toBe('fail');
|
|
46
|
+
expect(find(checks, 'agent').status).toBe('warn');
|
|
47
|
+
expect(find(checks, 'asset-types').status).toBe('warn');
|
|
48
|
+
expect(find(checks, 'asset-manifest').status).toBe('warn');
|
|
49
|
+
expect(find(checks, 'caches').status).toBe('ok');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('marks app types ok when fresh and warns after a newer source file', async () => {
|
|
53
|
+
const cwd = makeTempDir();
|
|
54
|
+
|
|
55
|
+
fs.mkdirSync(path.join(cwd, 'src/types'), { recursive: true });
|
|
56
|
+
fs.writeFileSync(path.join(cwd, 'caper.config.ts'), '// config', 'utf-8');
|
|
57
|
+
fs.writeFileSync(path.join(cwd, 'src/types/caper-app.d.ts'), '// types', 'utf-8');
|
|
58
|
+
setMtime(path.join(cwd, 'caper.config.ts'), 100);
|
|
59
|
+
setMtime(path.join(cwd, 'src/types/caper-app.d.ts'), 50);
|
|
60
|
+
|
|
61
|
+
const first = await runChecks(cwd, { online: false });
|
|
62
|
+
expect(find(first, 'app-types').status).toBe('ok');
|
|
63
|
+
|
|
64
|
+
fs.mkdirSync(path.join(cwd, 'src/scenes'), { recursive: true });
|
|
65
|
+
fs.writeFileSync(path.join(cwd, 'src/scenes/Example.ts'), '// scene', 'utf-8');
|
|
66
|
+
setMtime(path.join(cwd, 'src/scenes/Example.ts'), 10);
|
|
67
|
+
|
|
68
|
+
const second = await runChecks(cwd, { online: false });
|
|
69
|
+
expect(find(second, 'app-types').status).toBe('warn');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('warns when the agent pointer version does not match the installed version', async () => {
|
|
73
|
+
const cwd = makeTempDir();
|
|
74
|
+
|
|
75
|
+
const block = `${START_MARKER}
|
|
76
|
+
## Caper agent pointers
|
|
77
|
+
|
|
78
|
+
This app runs on \`@caperjs/core@0.0.1\`.
|
|
79
|
+
|
|
80
|
+
- Before engine-facing work, load the \`caper\` skill at \`.claude/skills/caper/SKILL.md\`.
|
|
81
|
+
${END_MARKER}`;
|
|
82
|
+
fs.writeFileSync(path.join(cwd, 'AGENTS.md'), block, 'utf-8');
|
|
83
|
+
fs.mkdirSync(path.join(cwd, '.claude/skills/caper'), { recursive: true });
|
|
84
|
+
fs.writeFileSync(path.join(cwd, '.claude/skills/caper/SKILL.md'), '# skill', 'utf-8');
|
|
85
|
+
|
|
86
|
+
const checks = await runChecks(cwd, { online: false });
|
|
87
|
+
const agent = find(checks, 'agent');
|
|
88
|
+
|
|
89
|
+
expect(agent.status).toBe('warn');
|
|
90
|
+
expect(agent.hint).toContain('@caperjs/core@0.0.1');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('reports agent pointers ok when the block and skill are current', async () => {
|
|
94
|
+
const cwd = makeTempDir();
|
|
95
|
+
|
|
96
|
+
await agentInit(cwd);
|
|
97
|
+
|
|
98
|
+
const checks = await runChecks(cwd, { online: false });
|
|
99
|
+
expect(find(checks, 'agent').status).toBe('ok');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('reports a linked package missing its build', async () => {
|
|
103
|
+
const cwd = makeTempDir();
|
|
104
|
+
const checkout = path.join(cwd, 'checkout');
|
|
105
|
+
fs.mkdirSync(path.join(checkout, 'src'), { recursive: true });
|
|
106
|
+
fs.writeFileSync(path.join(checkout, 'src/a.ts'), '// a', 'utf-8');
|
|
107
|
+
|
|
108
|
+
const linkPath = path.join(cwd, 'node_modules/@caperjs/core');
|
|
109
|
+
fs.mkdirSync(path.dirname(linkPath), { recursive: true });
|
|
110
|
+
fs.symlinkSync(checkout, linkPath, 'dir');
|
|
111
|
+
|
|
112
|
+
const checks = await runChecks(cwd, { online: false });
|
|
113
|
+
const link = find(checks, 'link');
|
|
114
|
+
|
|
115
|
+
expect(link.status).toBe('fail');
|
|
116
|
+
expect(link.label).toContain('linked from');
|
|
117
|
+
expect(link.hint).toContain('pnpm build');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('reports a linked package ok when the build is newer than source', async () => {
|
|
121
|
+
const cwd = makeTempDir();
|
|
122
|
+
const checkout = path.join(cwd, 'checkout');
|
|
123
|
+
|
|
124
|
+
fs.mkdirSync(path.join(checkout, 'src'), { recursive: true });
|
|
125
|
+
fs.writeFileSync(path.join(checkout, 'src/a.ts'), '// a', 'utf-8');
|
|
126
|
+
setMtime(path.join(checkout, 'src/a.ts'), 50);
|
|
127
|
+
|
|
128
|
+
fs.mkdirSync(path.join(checkout, 'lib'), { recursive: true });
|
|
129
|
+
fs.writeFileSync(path.join(checkout, 'lib/caper.mjs'), '// build', 'utf-8');
|
|
130
|
+
setMtime(path.join(checkout, 'lib/caper.mjs'), 10);
|
|
131
|
+
|
|
132
|
+
const linkPath = path.join(cwd, 'node_modules/@caperjs/core');
|
|
133
|
+
fs.mkdirSync(path.dirname(linkPath), { recursive: true });
|
|
134
|
+
fs.symlinkSync(checkout, linkPath, 'dir');
|
|
135
|
+
|
|
136
|
+
const checks = await runChecks(cwd, { online: false });
|
|
137
|
+
const link = find(checks, 'link');
|
|
138
|
+
|
|
139
|
+
expect(link.status).toBe('ok');
|
|
140
|
+
expect(link.label).toContain('linked from');
|
|
141
|
+
});
|
|
142
|
+
});
|