Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/setup/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ for that client.
| `.claude/hookify.sysknife-schema-fetch.local.md` | Deferred-schema reminder |
| `.claude/hookify.sysknife-bash-guard.local.md` | VM query guard |

On rerun, existing hookify rules are left intact and reported as skipped. Edit
them manually if you want to update them to the packaged defaults.

### Cursor

| File | Purpose |
Expand Down
9 changes: 7 additions & 2 deletions packages/setup/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -779,8 +779,13 @@ async function main() {

for (const { file, content } of rules) {
const hookPath = path.join('.claude', file);
fs.writeFileSync(hookPath, content);
ok(`Created ${hookPath}`);
try {
fs.writeFileSync(hookPath, content, { flag: 'wx' });
ok(`Created ${hookPath}`);
} catch (error) {
if (error.code !== 'EEXIST') throw error;
warn(`Skipped ${hookPath} (already exists; edit manually to update)`);
}
}
}

Expand Down
27 changes: 27 additions & 0 deletions packages/setup/tests/setup-contract.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,33 @@ test('MCP configs are merged, not overwritten (preserves other servers)', () =>
assert.doesNotMatch(source, /const cursorMcp = \{ mcpServers \}/);
});

test('rerunning setup preserves existing hookify guards and reports what it did', () => {
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'sysknife-setup-hookify-'));
const rulesDir = path.join(cwd, '.claude');
const approvalPath = path.join(rulesDir, 'hookify.require-sysknife-approval.local.md');
const schemaPath = path.join(rulesDir, 'hookify.sysknife-schema-fetch.local.md');
const bashPath = path.join(rulesDir, 'hookify.sysknife-bash-guard.local.md');

try {
fs.mkdirSync(rulesDir);
fs.writeFileSync(approvalPath, 'project-specific approval guard\n');
fs.writeFileSync(schemaPath, 'project-specific schema reminder\n');

const result = runWizard({ cwd });
const output = `${result.stdout}${result.stderr}`;

assert.equal(result.status, 3, output);
assert.equal(fs.readFileSync(approvalPath, 'utf8'), 'project-specific approval guard\n');
assert.equal(fs.readFileSync(schemaPath, 'utf8'), 'project-specific schema reminder\n');
assert.match(output, /Skipped \.claude\/hookify\.require-sysknife-approval\.local\.md.*already exists/);
assert.match(output, /Skipped \.claude\/hookify\.sysknife-schema-fetch\.local\.md.*already exists/);
assert.match(output, /Created \.claude\/hookify\.sysknife-bash-guard\.local\.md/);
assert.ok(fs.readFileSync(bashPath, 'utf8').length > 0);
} finally {
fs.rmSync(cwd, { recursive: true, force: true });
}
});

test('all selected MCP configs are validated before any integration file is written', () => {
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'sysknife-setup-preflight-'));
const claudePath = path.join(cwd, '.mcp.json');
Expand Down
Loading