@ulysses-ai/create-workspace 0.23.3-beta.0 → 0.24.1-beta.0
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/package.json +2 -1
- package/template/CLAUDE.md.tmpl +2 -2
- package/template/_claude/hooks/_utils.mjs +70 -0
- package/template/_claude/hooks/session-start.mjs +21 -1
- package/template/_claude/rules/work-item-tracking.md +6 -3
- package/template/_claude/rules/workspace-structure.md +1 -1
- package/template/_claude/scripts/chat-record.mjs +43 -26
- package/template/_claude/scripts/classify-update.mjs +194 -32
- package/template/_claude/scripts/forges/github.mjs +51 -1
- package/template/_claude/scripts/forges/gitlab.mjs +69 -0
- package/template/_claude/scripts/forges/interface.mjs +19 -2
- package/template/_claude/scripts/maintenance-audit.mjs +325 -3
- package/template/_claude/scripts/merge-mode.mjs +22 -1
- package/template/_claude/scripts/release-leak-audit.mjs +480 -0
- package/template/_claude/scripts/task-pr.mjs +316 -72
- package/template/_claude/scripts/task-worktree.mjs +9 -1
- package/template/_claude/scripts/template-merge.mjs +21 -4
- package/template/_claude/scripts/template-modifications.mjs +152 -0
- package/template/_claude/scripts/trackers/github-issues.mjs +110 -1
- package/template/_claude/scripts/trackers/gitlab-issues.mjs +221 -2
- package/template/_claude/scripts/trackers/interface.mjs +47 -0
- package/template/_claude/skills/braindump/SKILL.md +1 -1
- package/template/_claude/skills/complete-work/SKILL.md +22 -12
- package/template/_claude/skills/handoff/SKILL.md +1 -1
- package/template/_claude/skills/maintenance/SKILL.md +8 -3
- package/template/_claude/skills/release/SKILL.md +20 -6
- package/template/_claude/skills/setup-tracker/SKILL.md +24 -1
- package/template/_claude/skills/start-work/SKILL.md +23 -5
- package/template/_claude/skills/workspace-init/SKILL.md +51 -9
- package/template/_claude/skills/workspace-update/SKILL.md +23 -15
- package/template/workspace.json.tmpl +2 -1
- package/lib/init.test.mjs +0 -75
- package/lib/payload.test.mjs +0 -210
- package/lib/scaffold.test.mjs +0 -79
- package/lib/upgrade.test.mjs +0 -547
package/lib/init.test.mjs
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Test for init.mjs (--init scaffolding)
|
|
3
|
-
// Run: node lib/init.test.mjs
|
|
4
|
-
//
|
|
5
|
-
// --init stages the template payload, installs the bootstrap skills plus
|
|
6
|
-
// hooks/scripts/lib, and must leave a template baseline behind: the sha256 of
|
|
7
|
-
// every verbatim payload file, so the first /workspace-update classifies
|
|
8
|
-
// three ways instead of treating every template change as a local edit
|
|
9
|
-
// (gh:183).
|
|
10
|
-
import { initWorkspace } from './init.mjs';
|
|
11
|
-
import { createHash } from 'node:crypto';
|
|
12
|
-
import { mkdtempSync, rmSync, existsSync, readFileSync } from 'fs';
|
|
13
|
-
import { join } from 'path';
|
|
14
|
-
import { tmpdir } from 'os';
|
|
15
|
-
import { fileURLToPath } from 'url';
|
|
16
|
-
import { dirname } from 'path';
|
|
17
|
-
|
|
18
|
-
let failed = 0;
|
|
19
|
-
let passed = 0;
|
|
20
|
-
function check(label, ok) {
|
|
21
|
-
if (ok) { passed++; } else {
|
|
22
|
-
failed++;
|
|
23
|
-
console.error(` FAIL: ${label}`);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const here = dirname(fileURLToPath(import.meta.url));
|
|
28
|
-
const pkgVersion = JSON.parse(readFileSync(join(here, '..', 'package.json'), 'utf8')).version;
|
|
29
|
-
const sha = (p) => createHash('sha256').update(readFileSync(p)).digest('hex');
|
|
30
|
-
|
|
31
|
-
const root = mkdtempSync(join(tmpdir(), 'init-test-'));
|
|
32
|
-
|
|
33
|
-
try {
|
|
34
|
-
await initWorkspace(root);
|
|
35
|
-
|
|
36
|
-
const baselinePath = join(root, '.claude', '.template-baseline.json');
|
|
37
|
-
check('baseline written at .claude/.template-baseline.json', existsSync(baselinePath));
|
|
38
|
-
const baseline = JSON.parse(readFileSync(baselinePath, 'utf8'));
|
|
39
|
-
|
|
40
|
-
check(`baseline records the payload version (${pkgVersion})`, baseline.templateVersion === pkgVersion);
|
|
41
|
-
check(
|
|
42
|
-
'baseline covers the bootstrap skills --init installs',
|
|
43
|
-
typeof baseline.files['.claude/skills/workspace-update/SKILL.md'] === 'string'
|
|
44
|
-
&& typeof baseline.files['.claude/skills/workspace-init/SKILL.md'] === 'string',
|
|
45
|
-
);
|
|
46
|
-
check(
|
|
47
|
-
'baseline covers hooks and scripts',
|
|
48
|
-
typeof baseline.files['.claude/hooks/session-start.mjs'] === 'string'
|
|
49
|
-
&& typeof baseline.files['.claude/scripts/classify-update.mjs'] === 'string',
|
|
50
|
-
);
|
|
51
|
-
check(
|
|
52
|
-
'baseline covers the standalone verbatim files',
|
|
53
|
-
typeof baseline.files['.mcp.json'] === 'string' && typeof baseline.files['.claudeignore'] === 'string',
|
|
54
|
-
);
|
|
55
|
-
check(
|
|
56
|
-
'entry hash matches the installed file byte-for-byte',
|
|
57
|
-
baseline.files['.claude/scripts/classify-update.mjs'] === sha(join(root, '.claude', 'scripts', 'classify-update.mjs')),
|
|
58
|
-
);
|
|
59
|
-
check(
|
|
60
|
-
'baseline never covers tests (the tarball ships none)',
|
|
61
|
-
Object.keys(baseline.files).every((k) => !k.endsWith('.test.mjs')),
|
|
62
|
-
);
|
|
63
|
-
check(
|
|
64
|
-
'baseline never covers machine-local files',
|
|
65
|
-
!('.claude/settings.local.json' in baseline.files) && !('.claude/.active-session.json' in baseline.files),
|
|
66
|
-
);
|
|
67
|
-
} finally {
|
|
68
|
-
rmSync(root, { recursive: true, force: true });
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
if (failed > 0) {
|
|
72
|
-
console.error(`${failed} check(s) failed, ${passed} passed`);
|
|
73
|
-
process.exit(1);
|
|
74
|
-
}
|
|
75
|
-
console.log(`${passed} checks passed`);
|
package/lib/payload.test.mjs
DELETED
|
@@ -1,210 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Unit tests for payload.mjs
|
|
3
|
-
// Run: node lib/payload.test.mjs
|
|
4
|
-
//
|
|
5
|
-
// The staged .workspace-update/ payload is an on-disk format shared with
|
|
6
|
-
// the /workspace-update skill already installed in existing workspaces,
|
|
7
|
-
// which reads .workspace-update/.claude/... and .workspace-update/.mcp.json.
|
|
8
|
-
// These tests pin that layout even though template/ itself stores the
|
|
9
|
-
// protected paths under the inert names _claude/ and _mcp.json. The
|
|
10
|
-
// published-tarball reader (baseline reconstruction's input) is covered
|
|
11
|
-
// here too, against fixture tarballs built in-memory — no network.
|
|
12
|
-
import { stagePayload, cleanPayload, extractTarballEntries, fetchPackageTarball } from './payload.mjs';
|
|
13
|
-
import { mkdtempSync, rmSync, existsSync, readFileSync, writeFileSync } from 'fs';
|
|
14
|
-
import { join } from 'path';
|
|
15
|
-
import { tmpdir } from 'os';
|
|
16
|
-
import { gzipSync } from 'zlib';
|
|
17
|
-
|
|
18
|
-
let failed = 0;
|
|
19
|
-
let passed = 0;
|
|
20
|
-
function check(label, ok) {
|
|
21
|
-
if (ok) { passed++; } else {
|
|
22
|
-
failed++;
|
|
23
|
-
console.error(` FAIL: ${label}`);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const root = mkdtempSync(join(tmpdir(), 'payload-test-'));
|
|
28
|
-
|
|
29
|
-
try {
|
|
30
|
-
const { payloadDir, toVersion } = stagePayload(root, { action: 'init' });
|
|
31
|
-
|
|
32
|
-
check('payload staged at .workspace-update/', existsSync(payloadDir));
|
|
33
|
-
check('manifest written', existsSync(join(payloadDir, '.manifest.json')));
|
|
34
|
-
check('returns the package version', typeof toVersion === 'string' && toVersion.length > 0);
|
|
35
|
-
|
|
36
|
-
// The payload keeps the live names older workspaces' skills expect
|
|
37
|
-
check('.claude/ staged under live name', existsSync(join(payloadDir, '.claude')));
|
|
38
|
-
check('.claude/skills/workspace-update/SKILL.md staged', existsSync(join(payloadDir, '.claude', 'skills', 'workspace-update', 'SKILL.md')));
|
|
39
|
-
check('.claude/settings.json staged', existsSync(join(payloadDir, '.claude', 'settings.json')));
|
|
40
|
-
check('.mcp.json staged under live name', existsSync(join(payloadDir, '.mcp.json')));
|
|
41
|
-
check('_gitignore stays inert (skills merge it under that name)', existsSync(join(payloadDir, '_gitignore')));
|
|
42
|
-
|
|
43
|
-
// The template's inert names must not leak into the payload
|
|
44
|
-
check('no _claude/ in payload', !existsSync(join(payloadDir, '_claude')));
|
|
45
|
-
check('no _mcp.json in payload', !existsSync(join(payloadDir, '_mcp.json')));
|
|
46
|
-
|
|
47
|
-
// cleanPayload removes the staging directory
|
|
48
|
-
cleanPayload(root);
|
|
49
|
-
check('cleanPayload removes .workspace-update/', !existsSync(payloadDir));
|
|
50
|
-
} finally {
|
|
51
|
-
rmSync(root, { recursive: true, force: true });
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// ---- extractTarballEntries ----
|
|
55
|
-
|
|
56
|
-
// Minimal ustar writer: 512-byte header + padded body, matching what npm's
|
|
57
|
-
// tarballs carry for paths under 100 chars.
|
|
58
|
-
function tarEntry(name, content, type = '0') {
|
|
59
|
-
const body = Buffer.from(content, 'utf8');
|
|
60
|
-
const header = Buffer.alloc(512);
|
|
61
|
-
header.write(name, 0, 100, 'utf8');
|
|
62
|
-
header.write(body.length.toString(8).padStart(11, '0') + '\0', 124, 'ascii');
|
|
63
|
-
header.write(type, 156, 'ascii');
|
|
64
|
-
header.write('ustar', 257, 'ascii');
|
|
65
|
-
header.write('00', 263, 'ascii');
|
|
66
|
-
let sum = 0;
|
|
67
|
-
for (const byte of header) sum += byte;
|
|
68
|
-
header.write(sum.toString(8).padStart(6, '0') + '\0 ', 148, 'ascii');
|
|
69
|
-
const pad = Buffer.alloc((512 - (body.length % 512)) % 512);
|
|
70
|
-
return Buffer.concat([header, body, pad]);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function makeTarball(entries) {
|
|
74
|
-
return gzipSync(Buffer.concat([
|
|
75
|
-
...entries.map(([name, content, type]) => tarEntry(name, content, type)),
|
|
76
|
-
Buffer.alloc(1024),
|
|
77
|
-
]));
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// A GNU long-name record ('L') followed by the entry it names.
|
|
81
|
-
function makeTarballWithLongName(longPath, content) {
|
|
82
|
-
const blocks = [
|
|
83
|
-
tarEntry('././@LongLink', longPath, 'L'),
|
|
84
|
-
tarEntry(longPath.slice(0, 20), content),
|
|
85
|
-
Buffer.alloc(1024),
|
|
86
|
-
];
|
|
87
|
-
return gzipSync(Buffer.concat(blocks));
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// A PAX extended header ('x') carrying a path override, then the entry.
|
|
91
|
-
function makeTarballWithPaxPath(path, content) {
|
|
92
|
-
const record = `${path.length + 20} path=${path}\n`;
|
|
93
|
-
const blocks = [
|
|
94
|
-
tarEntry('PaxHeaders/x', record, 'x'),
|
|
95
|
-
tarEntry('short-name', content),
|
|
96
|
-
Buffer.alloc(1024),
|
|
97
|
-
];
|
|
98
|
-
return gzipSync(Buffer.concat(blocks));
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
{
|
|
102
|
-
const dest = mkdtempSync(join(tmpdir(), 'payload-tar-'));
|
|
103
|
-
try {
|
|
104
|
-
const files = extractTarballEntries(makeTarball([
|
|
105
|
-
['template/_claude/hooks/a.mjs', '// a\n'],
|
|
106
|
-
['template/_claude/rules', '', '5'],
|
|
107
|
-
['template/_mcp.json', '{"mcpServers":{}}\n'],
|
|
108
|
-
['template/.claudeignore', 'scratch/\n'],
|
|
109
|
-
['package/package.json', '{"version":"9.9.9"}\n'],
|
|
110
|
-
['README.md', 'not under the prefix\n'],
|
|
111
|
-
]), dest, { prefix: 'template/' });
|
|
112
|
-
check('extracts the three prefixed files', files === 3);
|
|
113
|
-
check('nested file content round-trips',
|
|
114
|
-
readFileSync(join(dest, '_claude', 'hooks', 'a.mjs'), 'utf8') === '// a\n');
|
|
115
|
-
check('directory entry materializes', existsSync(join(dest, '_claude', 'rules')));
|
|
116
|
-
check('entries outside the prefix are not extracted',
|
|
117
|
-
!existsSync(join(dest, '..', 'package')) && !existsSync(join(dest, 'package')));
|
|
118
|
-
check('unprefixed top-level files are not extracted', !existsSync(join(dest, 'README.md')));
|
|
119
|
-
|
|
120
|
-
// A traversal attempt inside the prefix is skipped, not written.
|
|
121
|
-
const guardDest = mkdtempSync(join(tmpdir(), 'payload-tar-'));
|
|
122
|
-
extractTarballEntries(makeTarball([
|
|
123
|
-
['template/../escape.txt', 'nope\n'],
|
|
124
|
-
['template/ok.txt', 'fine\n'],
|
|
125
|
-
]), guardDest, { prefix: 'template/' });
|
|
126
|
-
check('.. paths are refused', !existsSync(join(guardDest, '..', 'escape.txt')));
|
|
127
|
-
check('sibling of .. entry still extracted', existsSync(join(guardDest, 'ok.txt')));
|
|
128
|
-
rmSync(guardDest, { recursive: true, force: true });
|
|
129
|
-
|
|
130
|
-
// GNU long names and PAX path overrides both resolve to the real name.
|
|
131
|
-
const longPath = `template/_claude/scripts/${'very-long-name-'.repeat(8)}.mjs`;
|
|
132
|
-
const longDest = mkdtempSync(join(tmpdir(), 'payload-tar-'));
|
|
133
|
-
extractTarballEntries(makeTarballWithLongName(longPath, '// long\n'), longDest, { prefix: 'template/' });
|
|
134
|
-
check('GNU long-name entries extract under their full path',
|
|
135
|
-
readFileSync(join(longDest, '_claude', 'scripts', `${'very-long-name-'.repeat(8)}.mjs`), 'utf8') === '// long\n');
|
|
136
|
-
rmSync(longDest, { recursive: true, force: true });
|
|
137
|
-
|
|
138
|
-
const paxDest = mkdtempSync(join(tmpdir(), 'payload-tar-'));
|
|
139
|
-
extractTarballEntries(makeTarballWithPaxPath('template/_claude/pax.mjs', '// pax\n'), paxDest, { prefix: 'template/' });
|
|
140
|
-
check('PAX path overrides extract under the overridden path',
|
|
141
|
-
readFileSync(join(paxDest, '_claude', 'pax.mjs'), 'utf8') === '// pax\n');
|
|
142
|
-
rmSync(paxDest, { recursive: true, force: true });
|
|
143
|
-
|
|
144
|
-
// A PAX override that belongs to a NON-file entry (a symlink) dies with
|
|
145
|
-
// it: the file after the link extracts under its own header name, never
|
|
146
|
-
// under the leaked override.
|
|
147
|
-
const leakDest = mkdtempSync(join(tmpdir(), 'payload-tar-'));
|
|
148
|
-
const record = `${'template/_claude/leaked.mjs'.length + 20} path=template/_claude/leaked.mjs\n`;
|
|
149
|
-
extractTarballEntries(makeTarball([
|
|
150
|
-
['PaxHeaders/x', record, 'x'],
|
|
151
|
-
['target', 'template/elsewhere.mjs', '2'],
|
|
152
|
-
['template/_claude/own-name.mjs', '// own\n'],
|
|
153
|
-
]), leakDest, { prefix: 'template/' });
|
|
154
|
-
check('a PAX name followed by a non-file entry does not leak',
|
|
155
|
-
!existsSync(join(leakDest, '_claude', 'leaked.mjs'))
|
|
156
|
-
&& !existsSync(join(leakDest, '_claude', 'elsewhere.mjs')));
|
|
157
|
-
check('the entry after a discarded override uses its own name',
|
|
158
|
-
readFileSync(join(leakDest, '_claude', 'own-name.mjs'), 'utf8') === '// own\n');
|
|
159
|
-
rmSync(leakDest, { recursive: true, force: true });
|
|
160
|
-
|
|
161
|
-
// Tar names written on Windows carry '\' separators — treated as
|
|
162
|
-
// directory separators like '/'.
|
|
163
|
-
const bsDest = mkdtempSync(join(tmpdir(), 'payload-tar-'));
|
|
164
|
-
extractTarballEntries(makeTarball([
|
|
165
|
-
['template\\_claude\\hooks\\win.mjs', '// win\n'],
|
|
166
|
-
]), bsDest, { prefix: 'template/' });
|
|
167
|
-
check('backslash-separated entry names extract as directories',
|
|
168
|
-
readFileSync(join(bsDest, '_claude', 'hooks', 'win.mjs'), 'utf8') === '// win\n');
|
|
169
|
-
rmSync(bsDest, { recursive: true, force: true });
|
|
170
|
-
} finally {
|
|
171
|
-
rmSync(dest, { recursive: true, force: true });
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// fetchPackageTarball rejects non-semver versions before any network or
|
|
176
|
-
// npm call — a workspace.json value is user input.
|
|
177
|
-
{
|
|
178
|
-
const bogus = await fetchPackageTarball('0.15.0; rm -rf /');
|
|
179
|
-
check('non-semver version fetches nothing', bogus === null);
|
|
180
|
-
const alsoBogus = await fetchPackageTarball('');
|
|
181
|
-
check('empty version fetches nothing', alsoBogus === null);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// The registry fetch is bounded: a hung connection must not stall an
|
|
185
|
-
// upgrade indefinitely (reconstruction falls back to asking per file).
|
|
186
|
-
{
|
|
187
|
-
const realFetch = globalThis.fetch;
|
|
188
|
-
const tarball = makeTarball([['package/template/.claudeignore', 'x/\n']]);
|
|
189
|
-
let seenInit = null;
|
|
190
|
-
globalThis.fetch = async (url, init) => {
|
|
191
|
-
seenInit = { url, init };
|
|
192
|
-
return { ok: true, arrayBuffer: async () => tarball };
|
|
193
|
-
};
|
|
194
|
-
try {
|
|
195
|
-
const bytes = await fetchPackageTarball('0.15.0');
|
|
196
|
-
check('registry fetch returns the tarball bytes', bytes !== null && bytes.equals(tarball));
|
|
197
|
-
check('registry fetch targets the versioned tarball URL',
|
|
198
|
-
seenInit !== null && seenInit.url.endsWith('/create-workspace-0.15.0.tgz'));
|
|
199
|
-
check('registry fetch carries an abort timeout signal',
|
|
200
|
-
seenInit !== null && seenInit.init?.signal instanceof AbortSignal);
|
|
201
|
-
} finally {
|
|
202
|
-
globalThis.fetch = realFetch;
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
if (failed > 0) {
|
|
207
|
-
console.error(`${failed} check(s) failed, ${passed} passed`);
|
|
208
|
-
process.exit(1);
|
|
209
|
-
}
|
|
210
|
-
console.log(`${passed} checks passed`);
|
package/lib/scaffold.test.mjs
DELETED
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// End-to-end test for scaffold.mjs
|
|
3
|
-
// Run: node lib/scaffold.test.mjs
|
|
4
|
-
//
|
|
5
|
-
// The template stores .gitignore, .claude/, and .mcp.json under the inert
|
|
6
|
-
// names _gitignore, _claude/, and _mcp.json (Claude Code protects the live
|
|
7
|
-
// names from headless edits). scaffold() must install every one under its
|
|
8
|
-
// live name and leave no inert leftovers behind.
|
|
9
|
-
import { scaffold } from './scaffold.mjs';
|
|
10
|
-
import { mkdtempSync, rmSync, existsSync, readFileSync } from 'fs';
|
|
11
|
-
import { join } from 'path';
|
|
12
|
-
import { tmpdir } from 'os';
|
|
13
|
-
|
|
14
|
-
let failed = 0;
|
|
15
|
-
let passed = 0;
|
|
16
|
-
function check(label, ok) {
|
|
17
|
-
if (ok) { passed++; } else {
|
|
18
|
-
failed++;
|
|
19
|
-
console.error(` FAIL: ${label}`);
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const directory = mkdtempSync(join(tmpdir(), 'scaffold-test-'));
|
|
24
|
-
|
|
25
|
-
try {
|
|
26
|
-
await scaffold({
|
|
27
|
-
name: 'demo',
|
|
28
|
-
directory,
|
|
29
|
-
repos: [],
|
|
30
|
-
userName: 'tester',
|
|
31
|
-
activateRules: [],
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
// Live names installed
|
|
35
|
-
check('.claude/ directory installed', existsSync(join(directory, '.claude')));
|
|
36
|
-
check('.claude/settings.json installed', existsSync(join(directory, '.claude', 'settings.json')));
|
|
37
|
-
check('.claude/settings.local.json written', existsSync(join(directory, '.claude', 'settings.local.json')));
|
|
38
|
-
check('.claude/rules/ installed', existsSync(join(directory, '.claude', 'rules')));
|
|
39
|
-
check('.claude/rules/coherent-revisions.md installed', existsSync(join(directory, '.claude', 'rules', 'coherent-revisions.md')));
|
|
40
|
-
check('.mcp.json installed', existsSync(join(directory, '.mcp.json')));
|
|
41
|
-
check('.gitignore installed', existsSync(join(directory, '.gitignore')));
|
|
42
|
-
|
|
43
|
-
// .mcp.json is the template's payload, unmodified
|
|
44
|
-
const mcp = JSON.parse(readFileSync(join(directory, '.mcp.json'), 'utf8'));
|
|
45
|
-
check('.mcp.json parses with mcpServers object', typeof mcp.mcpServers === 'object' && mcp.mcpServers !== null);
|
|
46
|
-
|
|
47
|
-
// Inert names are gone
|
|
48
|
-
check('no _claude/ leftover', !existsSync(join(directory, '_claude')));
|
|
49
|
-
check('no _mcp.json leftover', !existsSync(join(directory, '_mcp.json')));
|
|
50
|
-
check('no _gitignore leftover', !existsSync(join(directory, '_gitignore')));
|
|
51
|
-
|
|
52
|
-
// Template baseline: hashes of the verbatim-installed files, for three-way
|
|
53
|
-
// update classification (gh:183)
|
|
54
|
-
const baselinePath = join(directory, '.claude', '.template-baseline.json');
|
|
55
|
-
check('template baseline written', existsSync(baselinePath));
|
|
56
|
-
const baseline = JSON.parse(readFileSync(baselinePath, 'utf8'));
|
|
57
|
-
const pkgVersion = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
|
|
58
|
-
check(`baseline records the package version (${pkgVersion})`, baseline.templateVersion === pkgVersion);
|
|
59
|
-
check(
|
|
60
|
-
'baseline covers rules installed verbatim from the template',
|
|
61
|
-
typeof baseline.files['.claude/rules/coherent-revisions.md'] === 'string',
|
|
62
|
-
);
|
|
63
|
-
check(
|
|
64
|
-
'baseline covers .mcp.json via its inert source name',
|
|
65
|
-
typeof baseline.files['.mcp.json'] === 'string' && typeof baseline.files['.claudeignore'] === 'string',
|
|
66
|
-
);
|
|
67
|
-
check(
|
|
68
|
-
'baseline never covers tests or machine-local files',
|
|
69
|
-
Object.keys(baseline.files).every((k) => !k.endsWith('.test.mjs') && k !== '.claude/settings.local.json'),
|
|
70
|
-
);
|
|
71
|
-
} finally {
|
|
72
|
-
rmSync(directory, { recursive: true, force: true });
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
if (failed > 0) {
|
|
76
|
-
console.error(`${failed} check(s) failed, ${passed} passed`);
|
|
77
|
-
process.exit(1);
|
|
78
|
-
}
|
|
79
|
-
console.log(`${passed} checks passed`);
|