@haposoft/cafekit 0.18.1 → 0.18.3
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 +12 -1
- package/bin/__tests__/adv-skill.test.js +371 -0
- package/bin/__tests__/agent-authority.test.js +38 -0
- package/bin/__tests__/agent-instructions.test.js +54 -0
- package/bin/__tests__/claude-plugin-build.test.js +151 -0
- package/bin/__tests__/claude-settings.test.js +123 -0
- package/bin/__tests__/codex-ask-guard.test.js +123 -0
- package/bin/__tests__/codex-ask-projection.test.js +51 -0
- package/bin/__tests__/codex-hooks-ownership.test.js +80 -1
- package/bin/__tests__/codex-hooks.test.js +812 -12
- package/bin/__tests__/codex-implicit-policy.test.js +160 -0
- package/bin/__tests__/codex-native-hook-boundaries.test.js +136 -0
- package/bin/__tests__/codex-native.test.js +198 -58
- package/bin/__tests__/codex-plugin-build.test.js +230 -0
- package/bin/__tests__/codex-plugin-context.test.js +135 -0
- package/bin/__tests__/codex-plugin-hooks.test.js +220 -0
- package/bin/__tests__/codex-projection-parity.test.js +314 -0
- package/bin/__tests__/codex-quiet-hooks.test.js +170 -0
- package/bin/__tests__/codex-research-projection.test.js +70 -0
- package/bin/__tests__/codex-runtime-layout.test.js +81 -0
- package/bin/__tests__/codex-setup-agents.test.js +216 -0
- package/bin/__tests__/codex-specs-projection.test.js +76 -0
- package/bin/__tests__/develop-contract.test.js +1 -1
- package/bin/__tests__/docs-sync-on-demand.test.js +25 -20
- package/bin/__tests__/inspect-paths.test.js +183 -0
- package/bin/__tests__/installer-safety.test.js +335 -0
- package/bin/__tests__/legacy-hooks-retired.test.js +81 -21
- package/bin/__tests__/migrate-apply.test.js +373 -0
- package/bin/__tests__/migrate-cli.test.js +152 -0
- package/bin/__tests__/migrate-merged.test.js +269 -0
- package/bin/__tests__/migrate-plan.test.js +271 -0
- package/bin/__tests__/omp-bridge.test.js +147 -17
- package/bin/__tests__/omp-hooks.test.js +35 -5
- package/bin/__tests__/orca-session.test.js +7 -0
- package/bin/__tests__/plugin-safety-keys.test.js +121 -0
- package/bin/__tests__/privacy-paths.test.js +501 -0
- package/bin/__tests__/usage-hook-retired.test.js +29 -9
- package/bin/__tests__/vibe-run.test.js +215 -0
- package/bin/__tests__/vibe-skill.test.js +549 -0
- package/bin/install.js +124 -1
- package/bin/lib/backup.js +43 -24
- package/bin/lib/codex-install.js +141 -12
- package/bin/lib/context.js +6 -1
- package/bin/lib/lock.js +98 -18
- package/bin/lib/managed-writer.js +16 -10
- package/bin/lib/migrate-apply.js +202 -0
- package/bin/lib/migrate-merged.js +342 -0
- package/bin/lib/migrate-plan.js +419 -0
- package/bin/lib/plugin-inventory.js +33 -0
- package/bin/phases/claude-runtime.js +93 -25
- package/bin/phases/claude-settings.js +22 -1
- package/bin/phases/codex-hooks.js +134 -33
- package/bin/phases/codex-runtime.js +110 -85
- package/bin/phases/copy-payload.js +50 -4
- package/bin/phases/omp-runtime.js +4 -26
- package/bin/phases/root-config.js +5 -2
- package/package.json +1 -1
- package/src/claude/CLAUDE.md +5 -1
- package/src/claude/agents/advisor.md +55 -0
- package/src/claude/agents/debugger.md +2 -3
- package/src/claude/agents/deployer.md +2 -1
- package/src/claude/agents/docs-keeper.md +12 -0
- package/src/claude/agents/inspector.md +1 -1
- package/src/claude/agents/project-manager.md +2 -2
- package/src/claude/agents/researcher.md +0 -1
- package/src/claude/agents/spec-maker.md +4 -1
- package/src/claude/gitignore +3 -0
- package/src/claude/hooks/__tests__/installed-root.test.js +111 -0
- package/src/claude/hooks/__tests__/plugin-context.test.js +96 -0
- package/src/claude/hooks/__tests__/plugin-options.test.js +141 -0
- package/src/claude/hooks/__tests__/plugin-state-isolation.test.js +125 -0
- package/src/claude/hooks/__tests__/precompact.test.js +213 -11
- package/src/claude/hooks/__tests__/privacy-block.test.js +60 -9
- package/src/claude/hooks/__tests__/session-writeenv.test.js +25 -0
- package/src/claude/hooks/__tests__/setup-statusline.test.js +179 -0
- package/src/claude/hooks/__tests__/spec-gate.test.js +227 -11
- package/src/claude/hooks/__tests__/spec-state-touched.test.js +204 -8
- package/src/claude/hooks/__tests__/state.test.js +207 -7
- package/src/claude/hooks/__tests__/update-check.test.js +88 -0
- package/src/claude/hooks/agent.cjs +11 -7
- package/src/claude/hooks/inspect-block.cjs +20 -66
- package/src/claude/hooks/lib/hook-state-dir.cjs +19 -1
- package/src/claude/hooks/lib/inspect-paths.cjs +304 -0
- package/src/claude/hooks/lib/parser.cjs +1 -1
- package/src/claude/hooks/lib/plugin-options.cjs +47 -0
- package/src/claude/hooks/lib/privacy-command-analysis.cjs +317 -24
- package/src/claude/hooks/lib/privacy-paths.cjs +673 -0
- package/src/claude/hooks/lib/project-root.cjs +15 -0
- package/src/claude/hooks/lib/rules-reservation.cjs +36 -0
- package/src/claude/hooks/lib/spec-touch.cjs +80 -0
- package/src/claude/hooks/lib/update-check.cjs +77 -0
- package/src/claude/hooks/plugin-context.cjs +80 -0
- package/src/claude/hooks/precompact.cjs +8 -15
- package/src/claude/hooks/privacy-block.cjs +25 -206
- package/src/claude/hooks/rules.cjs +10 -29
- package/src/claude/hooks/session.cjs +37 -117
- package/src/claude/hooks/spec-gate.cjs +9 -26
- package/src/claude/hooks/spec-state.cjs +44 -59
- package/src/claude/hooks/state.cjs +101 -52
- package/src/claude/migration-manifest.json +14 -2
- package/src/claude/plugin/commands/setup-statusline.md +12 -0
- package/src/claude/plugin/core.md +71 -0
- package/src/claude/rules/hook-protocols.md +1 -1
- package/src/claude/rules/manage-docs.md +1 -2
- package/src/claude/rules/review-audit-self-decision.md +1 -1
- package/src/claude/rules/skill-workflow-routing.md +1 -1
- package/src/claude/rules/state-sync.md +14 -0
- package/src/claude/rules/workflow.md +1 -1
- package/src/claude/runtime.json +0 -3
- package/src/claude/scripts/generate-skill-catalog.cjs +7 -2
- package/src/claude/scripts/provenance.cjs +54 -16
- package/src/claude/scripts/setup-statusline.cjs +88 -0
- package/src/claude/settings/settings.json +1 -1
- package/src/claude/skills/adv/SKILL.md +139 -0
- package/src/claude/skills/adv/references/optional-modes.md +41 -0
- package/src/claude/skills/adv/references/ultra-mode.md +29 -0
- package/src/claude/skills/ai-multimodal/scripts/check_setup.py +8 -68
- package/src/claude/skills/ai-multimodal/scripts/gemini_batch_process.py +6 -28
- package/src/claude/skills/brainstorm/SKILL.md +1 -1
- package/src/claude/skills/code-review/SKILL.md +4 -1
- package/src/claude/skills/code-review/references/pr-fix-reply.md +42 -0
- package/src/claude/skills/develop/SKILL.md +5 -3
- package/src/claude/skills/develop/references/quality-gate.md +1 -1
- package/src/claude/skills/fix/SKILL.md +2 -2
- package/src/claude/skills/git/SKILL.md +5 -3
- package/src/claude/skills/git/references/merge-pr.md +125 -0
- package/src/claude/skills/git/references/worktree-blueprint.md +1 -1
- package/src/claude/skills/specs/SKILL.md +2 -2
- package/src/claude/skills/specs/references/review.md +1 -1
- package/src/claude/skills/specs/references/templates.md +2 -0
- package/src/claude/skills/vibe/SKILL.md +69 -0
- package/src/claude/skills/vibe/references/pipeline-skill-integration.md +34 -0
- package/src/claude/skills/vibe/references/report-templates.md +84 -0
- package/src/claude/skills/vibe/references/subagent-orchestration.md +23 -0
- package/src/codex/AGENTS.md +9 -1
- package/src/codex/hooks/ask-guard.cjs +54 -0
- package/src/codex/hooks/inspect-block.cjs +15 -8
- package/src/codex/hooks/lib/hook-context.cjs +118 -4
- package/src/codex/hooks/lib/inspect-paths.cjs +1 -199
- package/src/codex/hooks/lib/plugin-options.cjs +2 -0
- package/src/codex/hooks/lib/privacy-paths.cjs +1 -0
- package/src/codex/hooks/lib/privacy-state.cjs +116 -17
- package/src/codex/hooks/lib/rules-reservation.cjs +38 -0
- package/src/codex/hooks/lib/secret-keywords.cjs +1 -0
- package/src/codex/hooks/lib/spec-session-touch.cjs +100 -0
- package/src/codex/hooks/lib/spec-touch.cjs +3 -0
- package/src/codex/hooks/lib/state-store.cjs +5 -4
- package/src/codex/hooks/lib/update-check.cjs +1 -0
- package/src/codex/hooks/precompact.cjs +40 -0
- package/src/codex/hooks/privacy-approval.cjs +28 -17
- package/src/codex/hooks/privacy-block.cjs +11 -216
- package/src/codex/hooks/rules.cjs +14 -34
- package/src/codex/hooks/secret-output-guardrail.cjs +1 -1
- package/src/codex/hooks/session.cjs +59 -11
- package/src/codex/hooks/spec-gate.cjs +21 -6
- package/src/codex/hooks/spec-state.cjs +32 -6
- package/src/codex/hooks/state.cjs +25 -4
- package/src/codex/hooks.json +39 -7
- package/src/codex/plugin/core.md +76 -0
- package/src/codex/plugin/hooks/plugin-context.cjs +63 -0
- package/src/codex/plugin/skills/setup-agents/SKILL.md +17 -0
- package/src/codex/rules/hook-protocols.md +13 -6
- package/src/codex/scripts/setup-agents.cjs +124 -0
- package/src/common/AGENTS.md +1 -1
- package/src/omp/extensions/{cafekit-bridge.mjs → cafekit-bridge.js} +85 -9
- package/src/omp/hooks/privacy-block.cjs +26 -207
- package/src/codex/rules/state-sync.md +0 -60
|
@@ -97,6 +97,341 @@ function inTempProject(run) {
|
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
function runCodexInstaller(root) {
|
|
101
|
+
return spawnSync(process.execPath, [INSTALLER, '--platform', 'codex', '--yes'], {
|
|
102
|
+
cwd: root, encoding: 'utf8', env: { ...process.env, PATH: '/usr/bin:/bin' }
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function rootFileGit(root, args) {
|
|
107
|
+
const result = spawnSync('git', args, { cwd: root, encoding: 'utf8' });
|
|
108
|
+
assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
|
|
109
|
+
return result.stdout;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function commitRootFile(root, file) {
|
|
113
|
+
rootFileGit(root, ['init', '-q']);
|
|
114
|
+
const content = file === 'AGENTS.md' ? upsertManagedCoreBlock('', SHARED_TEMPLATE) : TEMPLATE;
|
|
115
|
+
fs.writeFileSync(path.join(root, file), `${content}\n## Project notes\nKeep these project notes.\n`);
|
|
116
|
+
rootFileGit(root, ['add', '--', file]);
|
|
117
|
+
rootFileGit(root, ['-c', 'user.name=CafeKit Fixture', '-c', 'user.email=cafekit-fixture@example.invalid',
|
|
118
|
+
'commit', '--no-gpg-sign', '-qm', 'root file fixture']);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function runRootFileInstaller(root, platform, args = [], env = {}) {
|
|
122
|
+
return spawnSync(process.execPath,
|
|
123
|
+
[INSTALLER, '--platform', platform, '--yes', '--without-document-skills', ...args],
|
|
124
|
+
{ cwd: root, encoding: 'utf8', env: { ...process.env, PATH: '/usr/bin:/bin', ...env } });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
for (const [file, platform, args] of [
|
|
128
|
+
['AGENTS.md', 'claude', []], ['CLAUDE.md', 'claude', []],
|
|
129
|
+
['AGENTS.md', 'codex', []], ['AGENTS.md', 'omp', []],
|
|
130
|
+
['AGENTS.md', 'claude', ['--dry-run']],
|
|
131
|
+
]) {
|
|
132
|
+
test(`missing tracked ${file} stops ${platform} installation${args.length ? ' in dry-run' : ''} before writes`, () => {
|
|
133
|
+
inTempProject(root => {
|
|
134
|
+
commitRootFile(root, file);
|
|
135
|
+
fs.unlinkSync(path.join(root, file));
|
|
136
|
+
const before = fs.readdirSync(root).filter(name => name !== '.cafekit.lock');
|
|
137
|
+
const result = runRootFileInstaller(root, platform, args);
|
|
138
|
+
const output = `${result.stdout}\n${result.stderr}`;
|
|
139
|
+
assert.notEqual(result.status, 0, output);
|
|
140
|
+
assert.ok(output.includes(`git checkout -- ${file}`), output);
|
|
141
|
+
assert.ok(output.includes(`git rm ${file}`), output);
|
|
142
|
+
assert.deepEqual(fs.readdirSync(root).filter(name => name !== '.cafekit.lock'), before);
|
|
143
|
+
for (const name of ['AGENTS.md', 'CLAUDE.md', '.claude', '.codex', '.agents', '.omp', '.cafekit-backup']) {
|
|
144
|
+
assert.equal(fs.existsSync(path.join(root, name)), false, name);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
for (const assumeUnchanged of [false, true]) {
|
|
151
|
+
test(`missing skip-worktree AGENTS.md permits installation${assumeUnchanged ? ' with assume-unchanged' : ''}`, () => {
|
|
152
|
+
inTempProject(root => {
|
|
153
|
+
commitRootFile(root, 'AGENTS.md');
|
|
154
|
+
rootFileGit(root, ['update-index', '--skip-worktree', '--', 'AGENTS.md']);
|
|
155
|
+
if (assumeUnchanged) rootFileGit(root, ['update-index', '--assume-unchanged', '--', 'AGENTS.md']);
|
|
156
|
+
assert.match(rootFileGit(root, ['ls-files', '-v', '--', 'AGENTS.md']), assumeUnchanged ? /^s / : /^S /);
|
|
157
|
+
fs.unlinkSync(path.join(root, 'AGENTS.md'));
|
|
158
|
+
const result = runRootFileInstaller(root, 'claude');
|
|
159
|
+
assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
|
|
160
|
+
assert.ok(fs.existsSync(path.join(root, 'AGENTS.md')));
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
test('a tracked dangling CLAUDE.md symlink is present for the root preflight', () => {
|
|
166
|
+
inTempProject(root => {
|
|
167
|
+
rootFileGit(root, ['init', '-q']);
|
|
168
|
+
fs.mkdirSync(path.join(root, 'notes'));
|
|
169
|
+
fs.symlinkSync('notes/claude.md', path.join(root, 'CLAUDE.md'));
|
|
170
|
+
rootFileGit(root, ['add', '--', 'CLAUDE.md']);
|
|
171
|
+
rootFileGit(root, ['-c', 'user.name=CafeKit Fixture', '-c', 'user.email=cafekit-fixture@example.invalid',
|
|
172
|
+
'commit', '--no-gpg-sign', '-qm', 'root symlink fixture']);
|
|
173
|
+
const result = runRootFileInstaller(root, 'claude');
|
|
174
|
+
const output = `${result.stdout}\n${result.stderr}`;
|
|
175
|
+
assert.notEqual(result.status, 0, output);
|
|
176
|
+
assert.doesNotMatch(output, /tracked by git but missing/);
|
|
177
|
+
assert.match(output, /Refusing to follow symlinked managed path/);
|
|
178
|
+
assert.equal(fs.readlinkSync(path.join(root, 'CLAUDE.md')), 'notes/claude.md');
|
|
179
|
+
assert.equal(fs.existsSync(path.join(root, 'notes/claude.md')), false);
|
|
180
|
+
assert.deepEqual(fs.readdirSync(path.join(root, 'notes')), []);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test('uncertain git ownership warns and permits a fresh root file', () => {
|
|
185
|
+
inTempProject(root => {
|
|
186
|
+
commitRootFile(root, 'AGENTS.md');
|
|
187
|
+
fs.unlinkSync(path.join(root, 'AGENTS.md'));
|
|
188
|
+
const result = runRootFileInstaller(root, 'claude', [], {
|
|
189
|
+
GIT_TEST_ASSUME_DIFFERENT_OWNER: '1', GIT_CONFIG_GLOBAL: '/dev/null', GIT_CONFIG_NOSYSTEM: '1',
|
|
190
|
+
});
|
|
191
|
+
assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
|
|
192
|
+
assert.match(`${result.stdout}\n${result.stderr}`, /Could not confirm git state for AGENTS\.md; installing a fresh copy\./);
|
|
193
|
+
assert.ok(fs.existsSync(path.join(root, 'AGENTS.md')));
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test('Codex installation leaves a missing tracked CLAUDE.md alone', () => {
|
|
198
|
+
inTempProject(root => {
|
|
199
|
+
commitRootFile(root, 'CLAUDE.md');
|
|
200
|
+
fs.unlinkSync(path.join(root, 'CLAUDE.md'));
|
|
201
|
+
const result = runRootFileInstaller(root, 'codex');
|
|
202
|
+
assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
|
|
203
|
+
assert.equal(fs.existsSync(path.join(root, 'CLAUDE.md')), false);
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test('a root file removed from the git index can be freshly installed', () => {
|
|
208
|
+
inTempProject(root => {
|
|
209
|
+
commitRootFile(root, 'AGENTS.md');
|
|
210
|
+
rootFileGit(root, ['rm', '--', 'AGENTS.md']);
|
|
211
|
+
const result = runRootFileInstaller(root, 'claude');
|
|
212
|
+
assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
|
|
213
|
+
assert.ok(fs.existsSync(path.join(root, 'AGENTS.md')));
|
|
214
|
+
assert.doesNotMatch(`${result.stdout}\n${result.stderr}`, /Could not confirm git state/);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test('a project outside git installs root files without a git-state warning', () => {
|
|
219
|
+
inTempProject(root => {
|
|
220
|
+
const result = runRootFileInstaller(root, 'claude');
|
|
221
|
+
assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
|
|
222
|
+
assert.ok(fs.existsSync(path.join(root, 'AGENTS.md')));
|
|
223
|
+
assert.doesNotMatch(`${result.stdout}\n${result.stderr}`, /Could not confirm git state/);
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test('an existing tracked root file retains notes after its managed marker', () => {
|
|
228
|
+
inTempProject(root => {
|
|
229
|
+
commitRootFile(root, 'AGENTS.md');
|
|
230
|
+
const result = runRootFileInstaller(root, 'claude');
|
|
231
|
+
assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
|
|
232
|
+
assert.match(fs.readFileSync(path.join(root, 'AGENTS.md'), 'utf8'), /CAFEKIT CORE END -->[\s\S]*Keep these project notes\./);
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
const NOTES_HOME = 'Project notes (commands, files to leave unchanged, slow or expensive operations) belong in the root AGENTS.md after the CAFEKIT CORE END marker';
|
|
237
|
+
const normalizeWhitespace = text => text.replace(/\s+/g, ' ').trim();
|
|
238
|
+
|
|
239
|
+
test('the installed Claude block points project notes to the shared root with a quoted import', () => {
|
|
240
|
+
inTempProject(root => {
|
|
241
|
+
const result = runRootFileInstaller(root, 'claude');
|
|
242
|
+
assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
|
|
243
|
+
const body = normalizeWhitespace(managedBody(fs.readFileSync(path.join(root, 'CLAUDE.md'), 'utf8'), START, END));
|
|
244
|
+
assert.ok(body.includes(`${NOTES_HOME}; Claude Code reads them through \`@AGENTS.md\`.`), body);
|
|
245
|
+
assert.doesNotMatch(body, /@AGENTS\.md\./);
|
|
246
|
+
assert.doesNotMatch(body, /Codex|\.codex|\$cf-/i);
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test('the installed Codex block points project notes to the shared root directly', () => {
|
|
251
|
+
inTempProject(root => {
|
|
252
|
+
const result = runRootFileInstaller(root, 'codex');
|
|
253
|
+
assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
|
|
254
|
+
const body = normalizeWhitespace(managedBody(fs.readFileSync(path.join(root, 'AGENTS.md'), 'utf8'), CODEX_START, CODEX_END));
|
|
255
|
+
assert.ok(body.includes(`${NOTES_HOME}; Codex reads AGENTS.md directly.`), body);
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test('the package README preserves root ownership and names one project-notes home for both hosts', () => {
|
|
260
|
+
const readme = normalizeWhitespace(fs.readFileSync(path.join(__dirname, '../../README.md'), 'utf8'));
|
|
261
|
+
const note = `${NOTES_HOME}; Claude Code reads them through \`@AGENTS.md\` and Codex reads AGENTS.md directly.`;
|
|
262
|
+
const ownership = 'CafeKit owns only its marked block inside the project-root `CLAUDE.md`; project instructions outside that block are preserved across installs and upgrades.';
|
|
263
|
+
assert.ok(readme.includes(`${ownership} ${note}`));
|
|
264
|
+
assert.ok(readme.includes(`Claude statusline. ${note}`));
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
for (const host of ['claude', 'codex']) {
|
|
268
|
+
for (const state of ['pristine file', 'modified file', 'untracked file', 'external symlink', 'dangling symlink', 'pristine directory', 'directory with an extra file', 'directory with a modified file', 'directory with a symlink']) {
|
|
269
|
+
test(`${host} obsolete cleanup safely handles a ${state}`, () => {
|
|
270
|
+
const outside = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-obsolete-outside-'));
|
|
271
|
+
try {
|
|
272
|
+
inTempProject(root => {
|
|
273
|
+
const run = () => spawnSync(process.execPath, [INSTALLER, '--platform', host, '--yes', '--force-overwrite'], {
|
|
274
|
+
cwd: root, encoding: 'utf8', env: { ...process.env, PATH: '/usr/bin:/bin' }
|
|
275
|
+
});
|
|
276
|
+
const first = run();
|
|
277
|
+
assert.equal(first.status, 0, first.stderr);
|
|
278
|
+
const folder = `.${host}`;
|
|
279
|
+
const manifestPath = path.join(root, folder, 'cafekit-manifest.json');
|
|
280
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
281
|
+
const directory = state.includes('directory');
|
|
282
|
+
const relative = directory
|
|
283
|
+
? (host === 'codex' ? '.agents/skills/generate-graph' : '.claude/skills/generate-graph')
|
|
284
|
+
: `${folder}/hooks/usage.cjs`;
|
|
285
|
+
const target = path.join(root, relative);
|
|
286
|
+
const keyFor = relative => host === 'codex' ? relative : relative.slice('.claude/'.length);
|
|
287
|
+
const records = {};
|
|
288
|
+
const record = (relative, bytes) => {
|
|
289
|
+
const key = keyFor(relative);
|
|
290
|
+
records[key] = { sha256: sha256(bytes) };
|
|
291
|
+
manifest.files[key] = records[key];
|
|
292
|
+
};
|
|
293
|
+
const external = path.join(outside, 'target.txt');
|
|
294
|
+
if (state !== 'dangling symlink') fs.writeFileSync(external, 'outside bytes\n');
|
|
295
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
296
|
+
if (directory) {
|
|
297
|
+
fs.mkdirSync(path.join(target, 'nested'), { recursive: true });
|
|
298
|
+
fs.writeFileSync(path.join(target, 'tracked.txt'), state.includes('modified') ? 'changed\n' : 'installed\n');
|
|
299
|
+
fs.writeFileSync(path.join(target, 'nested/second.txt'), 'second\n');
|
|
300
|
+
record(`${relative}/tracked.txt`, 'installed\n');
|
|
301
|
+
record(`${relative}/nested/second.txt`, 'second\n');
|
|
302
|
+
if (state.includes('extra')) fs.writeFileSync(path.join(target, 'foreign.txt'), 'user bytes\n');
|
|
303
|
+
if (state.includes('symlink')) {
|
|
304
|
+
fs.symlinkSync(external, path.join(target, 'linked.txt'));
|
|
305
|
+
record(`${relative}/linked.txt`, 'outside bytes\n');
|
|
306
|
+
}
|
|
307
|
+
} else if (state.includes('symlink')) {
|
|
308
|
+
fs.symlinkSync(external, target);
|
|
309
|
+
record(relative, 'outside bytes\n');
|
|
310
|
+
} else {
|
|
311
|
+
fs.writeFileSync(target, state === 'modified file' ? 'changed\n' : 'installed\n');
|
|
312
|
+
if (state !== 'untracked file') record(relative, 'installed\n');
|
|
313
|
+
}
|
|
314
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifest));
|
|
315
|
+
const again = run();
|
|
316
|
+
assert.equal(again.status, 0, `${again.stdout}\n${again.stderr}`);
|
|
317
|
+
const preserved = !state.startsWith('pristine');
|
|
318
|
+
assert.equal(fs.existsSync(target) || state === 'dangling symlink' && fs.lstatSync(target).isSymbolicLink(), preserved);
|
|
319
|
+
const after = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
320
|
+
for (const [key, value] of Object.entries(records)) {
|
|
321
|
+
if (preserved) assert.deepEqual(after.files[key], value, key);
|
|
322
|
+
else assert.equal(after.files[key], undefined, key);
|
|
323
|
+
}
|
|
324
|
+
if (state.includes('symlink')) {
|
|
325
|
+
const link = directory ? path.join(target, 'linked.txt') : target;
|
|
326
|
+
assert.ok(fs.lstatSync(link).isSymbolicLink());
|
|
327
|
+
assert.equal(fs.readlinkSync(link), external);
|
|
328
|
+
}
|
|
329
|
+
if (state !== 'dangling symlink') assert.equal(fs.readFileSync(external, 'utf8'), 'outside bytes\n');
|
|
330
|
+
else assert.equal(fs.existsSync(external), false);
|
|
331
|
+
const warnings = `${again.stdout}\n${again.stderr}`.split('\n').filter(line => /Preserved \d+ obsolete runtime/.test(line));
|
|
332
|
+
assert.equal(warnings.length, preserved ? 1 : 0);
|
|
333
|
+
if (preserved) {
|
|
334
|
+
assert.match(warnings[0], /Preserved 1 obsolete runtime/);
|
|
335
|
+
assert.match(warnings[0], /manually/i);
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
} finally {
|
|
339
|
+
fs.rmSync(outside, { recursive: true, force: true });
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
test('combined install emits one aggregate warning for preserved obsolete runtime items', () => {
|
|
346
|
+
inTempProject(root => {
|
|
347
|
+
assert.equal(runCombinedInstaller(root).status, 0);
|
|
348
|
+
for (const host of ['claude', 'codex']) {
|
|
349
|
+
fs.writeFileSync(path.join(root, `.${host}/hooks/usage.cjs`), 'user bytes\n');
|
|
350
|
+
}
|
|
351
|
+
const again = runCombinedInstaller(root);
|
|
352
|
+
assert.equal(again.status, 0, again.stderr);
|
|
353
|
+
const warnings = `${again.stdout}\n${again.stderr}`.split('\n').filter(line => /Preserved \d+ obsolete runtime/.test(line));
|
|
354
|
+
assert.equal(warnings.length, 1);
|
|
355
|
+
assert.match(warnings[0], /Preserved 2 obsolete runtime/);
|
|
356
|
+
assert.match(warnings[0], /manually/i);
|
|
357
|
+
for (const host of ['claude', 'codex']) assert.equal(fs.readFileSync(path.join(root, `.${host}/hooks/usage.cjs`), 'utf8'), 'user bytes\n');
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
test('combined install removes pristine obsolete files after each host tracker is available', () => {
|
|
362
|
+
inTempProject(root => {
|
|
363
|
+
assert.equal(runCombinedInstaller(root).status, 0);
|
|
364
|
+
for (const host of ['claude', 'codex']) {
|
|
365
|
+
const target = path.join(root, `.${host}/hooks/usage.cjs`);
|
|
366
|
+
fs.writeFileSync(target, 'installed bytes\n');
|
|
367
|
+
const manifestPath = path.join(root, `.${host}/cafekit-manifest.json`);
|
|
368
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
369
|
+
const key = host === 'codex' ? '.codex/hooks/usage.cjs' : 'hooks/usage.cjs';
|
|
370
|
+
manifest.files[key] = { sha256: sha256('installed bytes\n') };
|
|
371
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifest));
|
|
372
|
+
}
|
|
373
|
+
const again = runCombinedInstaller(root);
|
|
374
|
+
assert.equal(again.status, 0, again.stderr);
|
|
375
|
+
for (const host of ['claude', 'codex']) {
|
|
376
|
+
assert.equal(fs.existsSync(path.join(root, `.${host}/hooks/usage.cjs`)), false, host);
|
|
377
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(root, `.${host}/cafekit-manifest.json`), 'utf8'));
|
|
378
|
+
assert.equal(manifest.files[host === 'codex' ? '.codex/hooks/usage.cjs' : 'hooks/usage.cjs'], undefined, host);
|
|
379
|
+
}
|
|
380
|
+
assert.doesNotMatch(again.stdout + again.stderr, /Preserved \d+ obsolete runtime/);
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
test('identical Codex reinstall does not report hook helpers or rules updated', () => {
|
|
385
|
+
inTempProject(root => {
|
|
386
|
+
const first = runCodexInstaller(root);
|
|
387
|
+
assert.equal(first.status, 0, first.stderr);
|
|
388
|
+
const second = runCodexInstaller(root);
|
|
389
|
+
assert.equal(second.status, 0, second.stderr);
|
|
390
|
+
const labels = ['Codex native hooks', 'Codex privacy command analyzer', 'Codex privacy path analyzer',
|
|
391
|
+
'Codex secret keyword matcher', 'Codex runtime path safety', 'Codex inspection path analyzer',
|
|
392
|
+
'Codex spec touch helpers', 'Codex update check', 'Codex native rule overrides', 'rules/ directory'];
|
|
393
|
+
const updates = `${second.stdout}\n${second.stderr}`.split('\n')
|
|
394
|
+
.filter(line => line.includes('↻') && labels.some(label => line.includes(label)));
|
|
395
|
+
assert.deepEqual(updates, []);
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
test('installed Codex hook libraries match canonical bytes and contain no source shims', () => {
|
|
400
|
+
inTempProject(root => {
|
|
401
|
+
const result = runCodexInstaller(root);
|
|
402
|
+
assert.equal(result.status, 0, result.stderr);
|
|
403
|
+
const sourceRoot = path.join(__dirname, '../../src');
|
|
404
|
+
const libs = path.join(sourceRoot, 'codex/hooks/lib');
|
|
405
|
+
const shim = /^(?:['"]use strict['"];?\s*)?module\.exports\s*=\s*require\(['"]\.\.\/\.\.\/\.\.\/claude\/hooks\/lib\/([^'"]+)['"]\);?\s*$/;
|
|
406
|
+
for (const name of fs.readdirSync(libs)) {
|
|
407
|
+
const source = fs.readFileSync(path.join(libs, name), 'utf8');
|
|
408
|
+
const match = source.match(shim);
|
|
409
|
+
if (match) assert.equal(match[1], name);
|
|
410
|
+
const expected = path.join(sourceRoot, match ? 'claude/hooks/lib' : 'codex/hooks/lib', name);
|
|
411
|
+
const installed = fs.readFileSync(path.join(root, '.codex/hooks/lib', name));
|
|
412
|
+
assert.doesNotMatch(installed.toString(), shim, name);
|
|
413
|
+
assert.deepEqual(installed, fs.readFileSync(expected), name);
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
test('installed Claude runtime omits retired usage configuration', () => {
|
|
419
|
+
inTempProject(root => {
|
|
420
|
+
const result = runCombinedInstaller(root);
|
|
421
|
+
assert.equal(result.status, 0, result.stderr);
|
|
422
|
+
const runtime = JSON.parse(fs.readFileSync(path.join(root, '.claude/runtime.json'), 'utf8'));
|
|
423
|
+
assert.equal(Object.hasOwn(runtime, 'usage'), false);
|
|
424
|
+
});
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
test('multimodal setup scripts keep local resolution without the missing resolver', () => {
|
|
428
|
+
for (const name of ['check_setup.py', 'gemini_batch_process.py']) {
|
|
429
|
+
const content = fs.readFileSync(path.join(__dirname, '../../src/claude/skills/ai-multimodal/scripts', name), 'utf8');
|
|
430
|
+
assert.doesNotMatch(content, /resolve_env|CENTRALIZED_RESOLVER_AVAILABLE/, name);
|
|
431
|
+
assert.ok(content.includes("os.getenv('GEMINI_API_KEY')"), name);
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
|
|
100
435
|
test('legacy managed root gitignore migrates A13 plan patterns and preserves user content', () => {
|
|
101
436
|
inTempProject((root) => {
|
|
102
437
|
fs.writeFileSync('.gitignore', [
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// review authority and the scaffold guard) are gone from every runtime. Each case pins one way
|
|
5
5
|
// they could survive: a fresh install, an upgrade on Claude (settings prune matches by substring,
|
|
6
6
|
// so a user's own hook with the same file name must stay), an upgrade on Codex (whose tracker
|
|
7
|
-
// keys are `.codex/…` and whose handlers come in three shapes), an upgrade on omp, an omp bridge
|
|
7
|
+
// keys are `.codex/…` and whose handlers come in three shapes), an upgrade on omp (hash-checked like Codex), an omp bridge
|
|
8
8
|
// that still lists the removed gates, and the source lists themselves.
|
|
9
9
|
|
|
10
10
|
const assert = require('node:assert/strict');
|
|
@@ -21,7 +21,7 @@ const SRC = path.join(PACKAGE_ROOT, 'src');
|
|
|
21
21
|
const readJson = (file) => JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
22
22
|
const MANIFEST = readJson(path.join(SRC, 'claude/migration-manifest.json'));
|
|
23
23
|
const SOURCE_SETTINGS = readJson(path.join(SRC, 'claude/settings/settings.json'));
|
|
24
|
-
const SOURCE_BRIDGE_PATH = path.join(SRC, 'omp/extensions/cafekit-bridge.
|
|
24
|
+
const SOURCE_BRIDGE_PATH = path.join(SRC, 'omp/extensions/cafekit-bridge.js');
|
|
25
25
|
|
|
26
26
|
const NAMES = [
|
|
27
27
|
'completion-authority.cjs',
|
|
@@ -48,6 +48,7 @@ function install(root, platforms) {
|
|
|
48
48
|
const result = spawnSync(process.execPath, [INSTALLER, '--platform', platforms.join(','), '--yes'],
|
|
49
49
|
{ cwd: root, encoding: 'utf8', env: { ...process.env, PATH: '/usr/bin:/bin' } });
|
|
50
50
|
assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
|
|
51
|
+
return result;
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
function commands(settings) {
|
|
@@ -69,12 +70,12 @@ test('fresh installs ship and register none of the five legacy hooks', () => {
|
|
|
69
70
|
const claude = commands(readJson(path.join(root, '.claude', 'settings.json')));
|
|
70
71
|
const codex = commands(readJson(path.join(root, '.codex', 'hooks.json')));
|
|
71
72
|
for (const command of [...claude, ...codex]) assert.deepEqual(mentionsName(command), [], command);
|
|
72
|
-
const bridge = fs.readFileSync(path.join(root, '.omp', 'extensions', 'cafekit-bridge.
|
|
73
|
+
const bridge = fs.readFileSync(path.join(root, '.omp', 'extensions', 'cafekit-bridge.js'), 'utf8');
|
|
73
74
|
assert.deepEqual(mentionsName(bridge), []);
|
|
74
75
|
});
|
|
75
76
|
});
|
|
76
77
|
|
|
77
|
-
test('claude upgrade
|
|
78
|
+
test('claude upgrade preserves edited legacy hooks while removing their CafeKit entries', () => {
|
|
78
79
|
inTempProject((root) => {
|
|
79
80
|
install(root, ['claude']);
|
|
80
81
|
const settingsPath = path.join(root, '.claude', 'settings.json');
|
|
@@ -99,10 +100,11 @@ test('claude upgrade removes the legacy hooks and their CafeKit entries but keep
|
|
|
99
100
|
settings.hooks.SubagentStop[0].hooks.push({ type: 'command', command: cmd('semantic-review-authority.cjs') });
|
|
100
101
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
101
102
|
|
|
102
|
-
install(root, ['claude']);
|
|
103
|
-
|
|
103
|
+
const upgrade = install(root, ['claude']);
|
|
104
|
+
assert.match(upgrade.stdout + upgrade.stderr, /Preserved 5 obsolete runtime/);
|
|
105
|
+
for (const name of NAMES) assert.equal(fs.readFileSync(path.join(root, '.claude', 'hooks', name), 'utf8'), '// user edit\n', name);
|
|
104
106
|
const after = readJson(manifestPath);
|
|
105
|
-
for (const name of NAMES) assert.equal(
|
|
107
|
+
for (const name of NAMES) assert.equal(after.files[`hooks/${name}`].sha256, '0'.repeat(64), `record ${name}`);
|
|
106
108
|
const afterCommands = commands(readJson(settingsPath));
|
|
107
109
|
for (const name of REGISTERED) {
|
|
108
110
|
assert.equal(afterCommands.some((command) => command.includes(`.claude/hooks/${name}`)), false, name);
|
|
@@ -112,7 +114,7 @@ test('claude upgrade removes the legacy hooks and their CafeKit entries but keep
|
|
|
112
114
|
});
|
|
113
115
|
});
|
|
114
116
|
|
|
115
|
-
test('codex upgrade
|
|
117
|
+
test('codex upgrade preserves edited legacy hooks and prunes only absent records', () => {
|
|
116
118
|
inTempProject((root) => {
|
|
117
119
|
install(root, ['codex']);
|
|
118
120
|
const hooksPath = path.join(root, '.codex', 'hooks.json');
|
|
@@ -137,10 +139,18 @@ test('codex upgrade removes the legacy hooks, their records and their CafeKit en
|
|
|
137
139
|
add('UserPromptSubmit', FOREIGN[0]);
|
|
138
140
|
fs.writeFileSync(hooksPath, JSON.stringify(config, null, 2));
|
|
139
141
|
|
|
140
|
-
install(root, ['codex']);
|
|
141
|
-
|
|
142
|
+
const upgrade = install(root, ['codex']);
|
|
143
|
+
assert.match(upgrade.stdout + upgrade.stderr, /Preserved 4 obsolete runtime/);
|
|
144
|
+
for (const name of NAMES) {
|
|
145
|
+
const file = path.join(root, '.codex', 'hooks', name);
|
|
146
|
+
if (name === 'completion-authority-state.cjs') assert.equal(fs.existsSync(file), false, name);
|
|
147
|
+
else assert.equal(fs.readFileSync(file, 'utf8'), '// user edit\n', name);
|
|
148
|
+
}
|
|
142
149
|
const after = readJson(manifestPath);
|
|
143
|
-
for (const name of NAMES)
|
|
150
|
+
for (const name of NAMES) {
|
|
151
|
+
if (name === 'completion-authority-state.cjs') assert.equal(after.files[`.codex/hooks/${name}`], undefined, `record ${name}`);
|
|
152
|
+
else assert.equal(after.files[`.codex/hooks/${name}`].sha256, '0'.repeat(64), `record ${name}`);
|
|
153
|
+
}
|
|
144
154
|
const afterCommands = commands(readJson(hooksPath));
|
|
145
155
|
for (const name of REGISTERED) {
|
|
146
156
|
assert.equal(afterCommands.some((command) => command.includes(`.codex/hooks/${name}`) || command.includes(`.codex\\hooks\\${name}`)), false, name);
|
|
@@ -156,9 +166,10 @@ test('omp upgrade removes the legacy hooks and their records', () => {
|
|
|
156
166
|
const manifestPath = path.join(root, '.omp', 'cafekit-manifest.json');
|
|
157
167
|
const manifest = readJson(manifestPath);
|
|
158
168
|
// omp records keys relative to the project root (recordRoot '.').
|
|
169
|
+
const shipped = '// shipped by an older CafeKit\n';
|
|
159
170
|
for (const name of NAMES) {
|
|
160
|
-
fs.writeFileSync(path.join(root, '.omp', 'hooks', name),
|
|
161
|
-
manifest.files[`.omp/hooks/${name}`] = { sha256: '
|
|
171
|
+
fs.writeFileSync(path.join(root, '.omp', 'hooks', name), shipped);
|
|
172
|
+
manifest.files[`.omp/hooks/${name}`] = { sha256: crypto.createHash('sha256').update(shipped).digest('hex') };
|
|
162
173
|
}
|
|
163
174
|
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
164
175
|
|
|
@@ -171,28 +182,77 @@ test('omp upgrade removes the legacy hooks and their records', () => {
|
|
|
171
182
|
});
|
|
172
183
|
});
|
|
173
184
|
|
|
174
|
-
test('omp upgrade
|
|
185
|
+
test('omp upgrade preserves edited legacy hooks and prunes only absent records', () => {
|
|
175
186
|
inTempProject((root) => {
|
|
176
187
|
install(root, ['omp']);
|
|
177
|
-
const
|
|
188
|
+
const manifestPath = path.join(root, '.omp', 'cafekit-manifest.json');
|
|
189
|
+
const manifest = readJson(manifestPath);
|
|
190
|
+
// A record whose file is already gone must leave the manifest too.
|
|
191
|
+
for (const name of NAMES) {
|
|
192
|
+
if (name !== 'completion-authority-state.cjs') fs.writeFileSync(path.join(root, '.omp', 'hooks', name), '// user edit\n');
|
|
193
|
+
manifest.files[`.omp/hooks/${name}`] = { sha256: '0'.repeat(64) };
|
|
194
|
+
}
|
|
195
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
196
|
+
|
|
197
|
+
const upgrade = install(root, ['omp']);
|
|
198
|
+
assert.match(`${upgrade.stdout}\n${upgrade.stderr}`, new RegExp(`Preserved ${NAMES.length - 1} obsolete runtime item`));
|
|
199
|
+
const after = readJson(manifestPath);
|
|
200
|
+
for (const name of NAMES) {
|
|
201
|
+
if (name === 'completion-authority-state.cjs') {
|
|
202
|
+
assert.equal(after.files[`.omp/hooks/${name}`], undefined, `record ${name}`);
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
assert.equal(fs.readFileSync(path.join(root, '.omp', 'hooks', name), 'utf8'), '// user edit\n', name);
|
|
206
|
+
assert.equal(after.files[`.omp/hooks/${name}`].sha256, '0'.repeat(64), `record ${name}`);
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test('omp upgrade removes a pristine old .mjs bridge, which omp never loads', () => {
|
|
212
|
+
inTempProject((root) => {
|
|
213
|
+
install(root, ['omp']);
|
|
214
|
+
const oldPath = path.join(root, '.omp', 'extensions', 'cafekit-bridge.mjs');
|
|
178
215
|
const manifestPath = path.join(root, '.omp', 'cafekit-manifest.json');
|
|
179
216
|
const old = fs.readFileSync(SOURCE_BRIDGE_PATH, 'utf8')
|
|
180
|
-
.replace("'inspect-block.cjs']", "'inspect-block.cjs', 'task-scaffold-guard.cjs']")
|
|
181
|
-
|
|
182
|
-
.replace("'spec-gate.cjs', 'state.cjs'", "'spec-gate.cjs', 'completion-authority.cjs', 'state.cjs'");
|
|
183
|
-
assert.deepEqual(mentionsName(old).sort(), ['completion-authority.cjs', 'task-scaffold-guard.cjs'], 'the fixture lists the removed gates');
|
|
184
|
-
fs.writeFileSync(bridgePath, old);
|
|
217
|
+
.replace("'inspect-block.cjs']", "'inspect-block.cjs', 'task-scaffold-guard.cjs']");
|
|
218
|
+
fs.writeFileSync(oldPath, old);
|
|
185
219
|
const manifest = readJson(manifestPath);
|
|
186
220
|
manifest.files['.omp/extensions/cafekit-bridge.mjs'] = { sha256: crypto.createHash('sha256').update(old).digest('hex') };
|
|
187
221
|
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
188
222
|
|
|
189
223
|
install(root, ['omp']);
|
|
190
|
-
|
|
224
|
+
assert.equal(fs.existsSync(oldPath), false, 'the pristine .mjs bridge is removed');
|
|
225
|
+
assert.equal(readJson(manifestPath).files['.omp/extensions/cafekit-bridge.mjs'], undefined, 'and its record');
|
|
226
|
+
const installed = fs.readFileSync(path.join(root, '.omp', 'extensions', 'cafekit-bridge.js'), 'utf8');
|
|
191
227
|
assert.deepEqual(mentionsName(installed), []);
|
|
192
228
|
assert.equal(installed, fs.readFileSync(SOURCE_BRIDGE_PATH, 'utf8'));
|
|
193
229
|
});
|
|
194
230
|
});
|
|
195
231
|
|
|
232
|
+
test('omp upgrade keeps an edited, untracked or symlinked old .mjs bridge and says so', () => {
|
|
233
|
+
for (const shape of ['edited', 'untracked', 'symlink']) {
|
|
234
|
+
inTempProject((root) => {
|
|
235
|
+
install(root, ['omp']);
|
|
236
|
+
const oldPath = path.join(root, '.omp', 'extensions', 'cafekit-bridge.mjs');
|
|
237
|
+
const manifestPath = path.join(root, '.omp', 'cafekit-manifest.json');
|
|
238
|
+
const manifest = readJson(manifestPath);
|
|
239
|
+
if (shape === 'symlink') {
|
|
240
|
+
const target = path.join(root, 'my-bridge.mjs');
|
|
241
|
+
fs.writeFileSync(target, '// mine\n');
|
|
242
|
+
fs.symlinkSync(target, oldPath);
|
|
243
|
+
} else {
|
|
244
|
+
fs.writeFileSync(oldPath, '// edited by the user\n');
|
|
245
|
+
if (shape === 'edited') manifest.files['.omp/extensions/cafekit-bridge.mjs'] = { sha256: '0'.repeat(64) };
|
|
246
|
+
}
|
|
247
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
248
|
+
|
|
249
|
+
const result = install(root, ['omp']);
|
|
250
|
+
assert.equal(fs.lstatSync(oldPath).isFile() || fs.lstatSync(oldPath).isSymbolicLink(), true, `${shape} .mjs is kept`);
|
|
251
|
+
assert.match(`${result.stdout}\n${result.stderr}`, /Preserved \d+ obsolete runtime item/, `${shape} is reported`);
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
|
|
196
256
|
test('no source ships, registers or bridges the five legacy hooks', () => {
|
|
197
257
|
for (const name of NAMES) {
|
|
198
258
|
assert.equal(fs.existsSync(path.join(SRC, 'claude/hooks', name)), false, `claude ${name}`);
|