buddy-reroll 0.3.2 → 0.3.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/index.js +16 -1
- package/lib/doctor.js +8 -0
- package/lib/hooks.js +36 -32
- package/lib/hooks.test.js +142 -144
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -490,7 +490,22 @@ async function main() {
|
|
|
490
490
|
}
|
|
491
491
|
|
|
492
492
|
if (args.doctor) {
|
|
493
|
-
|
|
493
|
+
const report = getDoctorReport();
|
|
494
|
+
if (report.status === "not-executable" && report.binaryPath) {
|
|
495
|
+
try {
|
|
496
|
+
const { chmodSync, statSync } = await import("fs");
|
|
497
|
+
const mode = statSync(report.binaryPath).mode | 0o111;
|
|
498
|
+
chmodSync(report.binaryPath, mode);
|
|
499
|
+
console.log(`\n ✓ Fixed: restored execute permission on ${report.binaryPath}`);
|
|
500
|
+
const fixed = getDoctorReport();
|
|
501
|
+
console.log(`\n${formatDoctorReport(fixed, "buddy-reroll doctor")}\n`);
|
|
502
|
+
} catch (err) {
|
|
503
|
+
console.log(`\n${formatDoctorReport(report, "buddy-reroll doctor")}\n`);
|
|
504
|
+
console.log(` ⚠ Auto-fix failed: ${err.message}\n Run manually: chmod +x "${report.binaryPath}"\n`);
|
|
505
|
+
}
|
|
506
|
+
} else {
|
|
507
|
+
console.log(`\n${formatDoctorReport(report, "buddy-reroll doctor")}\n`);
|
|
508
|
+
}
|
|
494
509
|
return;
|
|
495
510
|
}
|
|
496
511
|
|
package/lib/doctor.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { accessSync, constants, chmodSync, statSync } from "fs";
|
|
1
2
|
import { getClaudeBinaryOverride, getClaudeConfigDir, findBinaryPath, findConfigPath, getPatchability } from "./runtime.js";
|
|
2
3
|
|
|
3
4
|
function buildStatus(binaryPath, configPath, patchability) {
|
|
@@ -5,6 +6,11 @@ function buildStatus(binaryPath, configPath, patchability) {
|
|
|
5
6
|
if (!binaryPath) return "missing-binary";
|
|
6
7
|
if (!configPath) return "missing-config";
|
|
7
8
|
if (!patchability.ok) return "read-only";
|
|
9
|
+
try {
|
|
10
|
+
accessSync(binaryPath, constants.X_OK);
|
|
11
|
+
} catch {
|
|
12
|
+
return "not-executable";
|
|
13
|
+
}
|
|
8
14
|
return "ready";
|
|
9
15
|
}
|
|
10
16
|
|
|
@@ -18,6 +24,8 @@ function buildNextStep(status) {
|
|
|
18
24
|
return "Open Claude Code once so it writes config, or point `CLAUDE_CONFIG_DIR` to the correct config directory.";
|
|
19
25
|
case "read-only":
|
|
20
26
|
return "Use a user-writable Claude install, or point `CLAUDE_BINARY_PATH` to a writable Claude binary copy; `--current` can still work with a read-only install.";
|
|
27
|
+
case "not-executable":
|
|
28
|
+
return "Claude binary lost its execute permission (likely from a patch). Run: chmod +x <binary-path>";
|
|
21
29
|
default:
|
|
22
30
|
return "`--current` and reroll commands are ready to run on this machine.";
|
|
23
31
|
}
|
package/lib/hooks.js
CHANGED
|
@@ -4,14 +4,23 @@ import { homedir } from "os";
|
|
|
4
4
|
|
|
5
5
|
const HOOK_COMMAND = "npx buddy-reroll --apply-hook";
|
|
6
6
|
|
|
7
|
+
const HOOK_ENTRY = {
|
|
8
|
+
matcher: "",
|
|
9
|
+
hooks: [{ type: "command", command: HOOK_COMMAND }],
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
function isOurHook(entry) {
|
|
13
|
+
if (typeof entry === "string") return entry === HOOK_COMMAND;
|
|
14
|
+
if (entry?.hooks) return entry.hooks.some((h) => h.command === HOOK_COMMAND);
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
|
|
7
18
|
export function getSettingsPath() {
|
|
8
19
|
return join(process.env.CLAUDE_CONFIG_DIR ?? join(homedir(), ".claude"), "settings.json");
|
|
9
20
|
}
|
|
10
21
|
|
|
11
22
|
export function readSettings(settingsPath) {
|
|
12
|
-
if (!existsSync(settingsPath)) {
|
|
13
|
-
return {};
|
|
14
|
-
}
|
|
23
|
+
if (!existsSync(settingsPath)) return {};
|
|
15
24
|
try {
|
|
16
25
|
return JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
17
26
|
} catch {
|
|
@@ -27,49 +36,46 @@ export function writeSettings(settingsPath, obj) {
|
|
|
27
36
|
|
|
28
37
|
export function installHook(settingsPath = getSettingsPath()) {
|
|
29
38
|
const settings = readSettings(settingsPath);
|
|
30
|
-
|
|
31
|
-
if (!settings.hooks) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
if (settings.hooks.SessionStart.includes(HOOK_COMMAND)) {
|
|
39
|
+
|
|
40
|
+
if (!settings.hooks) settings.hooks = {};
|
|
41
|
+
if (!Array.isArray(settings.hooks.SessionStart)) settings.hooks.SessionStart = [];
|
|
42
|
+
|
|
43
|
+
const existing = settings.hooks.SessionStart.find(isOurHook);
|
|
44
|
+
if (existing && typeof existing === "object") {
|
|
39
45
|
return { installed: false, reason: "already installed" };
|
|
40
46
|
}
|
|
41
|
-
|
|
42
|
-
settings.hooks.SessionStart.
|
|
47
|
+
|
|
48
|
+
settings.hooks.SessionStart = settings.hooks.SessionStart.filter((e) => !isOurHook(e));
|
|
49
|
+
settings.hooks.SessionStart.push(HOOK_ENTRY);
|
|
43
50
|
writeSettings(settingsPath, settings);
|
|
44
|
-
|
|
51
|
+
|
|
45
52
|
return { installed: true, path: settingsPath };
|
|
46
53
|
}
|
|
47
54
|
|
|
48
55
|
export function removeHook(settingsPath = getSettingsPath()) {
|
|
49
56
|
const settings = readSettings(settingsPath);
|
|
50
|
-
|
|
51
|
-
if (!settings.hooks || !settings.hooks.SessionStart) {
|
|
57
|
+
|
|
58
|
+
if (!settings.hooks || !Array.isArray(settings.hooks.SessionStart)) {
|
|
52
59
|
return { removed: false, reason: "not installed" };
|
|
53
60
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if (Object.keys(settings.hooks).length === 0) {
|
|
62
|
-
delete settings.hooks;
|
|
61
|
+
|
|
62
|
+
const before = settings.hooks.SessionStart.length;
|
|
63
|
+
settings.hooks.SessionStart = settings.hooks.SessionStart.filter((e) => !isOurHook(e));
|
|
64
|
+
|
|
65
|
+
if (settings.hooks.SessionStart.length === before) {
|
|
66
|
+
return { removed: false, reason: "not installed" };
|
|
63
67
|
}
|
|
64
|
-
|
|
68
|
+
|
|
69
|
+
if (settings.hooks.SessionStart.length === 0) delete settings.hooks.SessionStart;
|
|
70
|
+
if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
|
|
71
|
+
|
|
65
72
|
writeSettings(settingsPath, settings);
|
|
66
|
-
|
|
67
73
|
return { removed: true, path: settingsPath };
|
|
68
74
|
}
|
|
69
75
|
|
|
70
76
|
export function isHookInstalled(settingsPath = getSettingsPath()) {
|
|
71
77
|
const settings = readSettings(settingsPath);
|
|
72
|
-
return settings.hooks?.SessionStart
|
|
78
|
+
return Array.isArray(settings.hooks?.SessionStart) && settings.hooks.SessionStart.some(isOurHook);
|
|
73
79
|
}
|
|
74
80
|
|
|
75
81
|
export function getSaltStorePath() {
|
|
@@ -85,9 +91,7 @@ export function storeSalt(salt) {
|
|
|
85
91
|
|
|
86
92
|
export function readStoredSalt() {
|
|
87
93
|
const path = getSaltStorePath();
|
|
88
|
-
if (!existsSync(path))
|
|
89
|
-
return null;
|
|
90
|
-
}
|
|
94
|
+
if (!existsSync(path)) return null;
|
|
91
95
|
try {
|
|
92
96
|
return JSON.parse(readFileSync(path, "utf-8"));
|
|
93
97
|
} catch {
|
package/lib/hooks.test.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, afterEach } from "bun:test";
|
|
2
2
|
import { mkdtempSync, rmSync, writeFileSync, readFileSync } from "fs";
|
|
3
3
|
import { join } from "path";
|
|
4
|
-
import { tmpdir } from "os";
|
|
4
|
+
import { tmpdir, homedir } from "os";
|
|
5
|
+
import { realpathSync } from "fs";
|
|
5
6
|
import {
|
|
6
7
|
readSettings,
|
|
7
8
|
writeSettings,
|
|
@@ -12,10 +13,19 @@ import {
|
|
|
12
13
|
readStoredSalt,
|
|
13
14
|
} from "./hooks.js";
|
|
14
15
|
|
|
16
|
+
const HOOK_COMMAND = "npx buddy-reroll --apply-hook";
|
|
17
|
+
|
|
18
|
+
function findOurHook(settings) {
|
|
19
|
+
const entries = settings.hooks?.SessionStart ?? [];
|
|
20
|
+
return entries.find(
|
|
21
|
+
(e) => typeof e === "object" && e.hooks?.some((h) => h.command === HOOK_COMMAND)
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
15
25
|
let tempDir;
|
|
16
26
|
|
|
17
27
|
beforeEach(() => {
|
|
18
|
-
tempDir = mkdtempSync(join(tmpdir(), "buddy-reroll-test-"));
|
|
28
|
+
tempDir = mkdtempSync(join(realpathSync(tmpdir()), "buddy-reroll-test-"));
|
|
19
29
|
process.env.CLAUDE_CONFIG_DIR = tempDir;
|
|
20
30
|
});
|
|
21
31
|
|
|
@@ -26,215 +36,203 @@ afterEach(() => {
|
|
|
26
36
|
|
|
27
37
|
describe("readSettings", () => {
|
|
28
38
|
it("returns empty object when file doesn't exist", () => {
|
|
29
|
-
|
|
30
|
-
expect(readSettings(settingsPath)).toEqual({});
|
|
39
|
+
expect(readSettings(join(tempDir, "settings.json"))).toEqual({});
|
|
31
40
|
});
|
|
32
41
|
|
|
33
42
|
it("parses valid JSON file", () => {
|
|
34
|
-
const
|
|
35
|
-
writeSettings(
|
|
36
|
-
expect(readSettings(
|
|
43
|
+
const p = join(tempDir, "settings.json");
|
|
44
|
+
writeSettings(p, { permissions: { allow: [] } });
|
|
45
|
+
expect(readSettings(p)).toEqual({ permissions: { allow: [] } });
|
|
37
46
|
});
|
|
38
47
|
|
|
39
48
|
it("returns empty object for corrupted JSON", () => {
|
|
40
|
-
const
|
|
41
|
-
writeFileSync(
|
|
42
|
-
expect(readSettings(
|
|
49
|
+
const p = join(tempDir, "settings.json");
|
|
50
|
+
writeFileSync(p, "{ invalid json");
|
|
51
|
+
expect(readSettings(p)).toEqual({});
|
|
43
52
|
});
|
|
44
53
|
});
|
|
45
54
|
|
|
46
55
|
describe("writeSettings", () => {
|
|
47
56
|
it("creates parent directories if needed", () => {
|
|
48
|
-
const
|
|
49
|
-
writeSettings(
|
|
50
|
-
expect(readSettings(
|
|
57
|
+
const p = join(tempDir, "nested", "dir", "settings.json");
|
|
58
|
+
writeSettings(p, { test: true });
|
|
59
|
+
expect(readSettings(p)).toEqual({ test: true });
|
|
51
60
|
});
|
|
52
61
|
|
|
53
|
-
it("writes JSON with 2-space indent and newline", () => {
|
|
54
|
-
const
|
|
55
|
-
writeSettings(
|
|
56
|
-
const content = readFileSync(
|
|
62
|
+
it("writes JSON with 2-space indent and trailing newline", () => {
|
|
63
|
+
const p = join(tempDir, "settings.json");
|
|
64
|
+
writeSettings(p, { a: 1 });
|
|
65
|
+
const content = readFileSync(p, "utf-8");
|
|
57
66
|
expect(content).toContain(" ");
|
|
58
67
|
expect(content.endsWith("\n")).toBe(true);
|
|
59
68
|
});
|
|
60
69
|
});
|
|
61
70
|
|
|
62
71
|
describe("installHook", () => {
|
|
63
|
-
it("creates settings.json
|
|
64
|
-
const
|
|
65
|
-
const result = installHook(
|
|
72
|
+
it("creates settings.json with correct hook format", () => {
|
|
73
|
+
const p = join(tempDir, "settings.json");
|
|
74
|
+
const result = installHook(p);
|
|
66
75
|
expect(result.installed).toBe(true);
|
|
67
|
-
const settings = readSettings(
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
76
|
+
const settings = readSettings(p);
|
|
77
|
+
const hook = findOurHook(settings);
|
|
78
|
+
expect(hook).toBeDefined();
|
|
79
|
+
expect(hook.matcher).toBe("");
|
|
80
|
+
expect(hook.hooks[0].type).toBe("command");
|
|
81
|
+
expect(hook.hooks[0].command).toBe(HOOK_COMMAND);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("adds hook to existing settings without clobbering", () => {
|
|
85
|
+
const p = join(tempDir, "settings.json");
|
|
86
|
+
writeSettings(p, { permissions: { allow: ["test"] } });
|
|
87
|
+
installHook(p);
|
|
88
|
+
const settings = readSettings(p);
|
|
89
|
+
expect(settings.permissions).toEqual({ allow: ["test"] });
|
|
90
|
+
expect(findOurHook(settings)).toBeDefined();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("is idempotent", () => {
|
|
94
|
+
const p = join(tempDir, "settings.json");
|
|
95
|
+
installHook(p);
|
|
96
|
+
const result = installHook(p);
|
|
84
97
|
expect(result.installed).toBe(false);
|
|
85
98
|
expect(result.reason).toBe("already installed");
|
|
86
|
-
const settings = readSettings(
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
});
|
|
96
|
-
installHook(
|
|
97
|
-
const settings = readSettings(
|
|
98
|
-
|
|
99
|
-
expect(
|
|
99
|
+
const settings = readSettings(p);
|
|
100
|
+
const hooks = settings.hooks.SessionStart.filter(
|
|
101
|
+
(e) => typeof e === "object" && e.hooks?.some((h) => h.command === HOOK_COMMAND)
|
|
102
|
+
);
|
|
103
|
+
expect(hooks.length).toBe(1);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("migrates old string-format hook to new format", () => {
|
|
107
|
+
const p = join(tempDir, "settings.json");
|
|
108
|
+
writeSettings(p, { hooks: { SessionStart: [HOOK_COMMAND] } });
|
|
109
|
+
installHook(p);
|
|
110
|
+
const settings = readSettings(p);
|
|
111
|
+
const strings = settings.hooks.SessionStart.filter((e) => typeof e === "string");
|
|
112
|
+
expect(strings.length).toBe(0);
|
|
113
|
+
expect(findOurHook(settings)).toBeDefined();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("preserves other hooks in SessionStart array", () => {
|
|
117
|
+
const p = join(tempDir, "settings.json");
|
|
118
|
+
const otherHook = { matcher: "Bash", hooks: [{ type: "command", command: "echo hi" }] };
|
|
119
|
+
writeSettings(p, { hooks: { SessionStart: [otherHook] } });
|
|
120
|
+
installHook(p);
|
|
121
|
+
const settings = readSettings(p);
|
|
122
|
+
expect(settings.hooks.SessionStart.length).toBe(2);
|
|
123
|
+
expect(settings.hooks.SessionStart[0]).toEqual(otherHook);
|
|
100
124
|
});
|
|
101
125
|
});
|
|
102
126
|
|
|
103
127
|
describe("removeHook", () => {
|
|
104
|
-
it("removes hook
|
|
105
|
-
const
|
|
106
|
-
installHook(
|
|
107
|
-
const result = removeHook(
|
|
128
|
+
it("removes hook", () => {
|
|
129
|
+
const p = join(tempDir, "settings.json");
|
|
130
|
+
installHook(p);
|
|
131
|
+
const result = removeHook(p);
|
|
108
132
|
expect(result.removed).toBe(true);
|
|
109
|
-
|
|
110
|
-
expect(settings.hooks?.SessionStart?.includes("npx buddy-reroll --apply-hook") ?? false).toBe(false);
|
|
133
|
+
expect(findOurHook(readSettings(p))).toBeUndefined();
|
|
111
134
|
});
|
|
112
135
|
|
|
113
|
-
it("
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
removeHook(
|
|
117
|
-
|
|
118
|
-
expect(settings.hooks?.SessionStart).toBeUndefined();
|
|
136
|
+
it("removes old string-format hook too", () => {
|
|
137
|
+
const p = join(tempDir, "settings.json");
|
|
138
|
+
writeSettings(p, { hooks: { SessionStart: [HOOK_COMMAND] } });
|
|
139
|
+
const result = removeHook(p);
|
|
140
|
+
expect(result.removed).toBe(true);
|
|
119
141
|
});
|
|
120
142
|
|
|
121
|
-
it("deletes empty hooks
|
|
122
|
-
const
|
|
123
|
-
installHook(
|
|
124
|
-
removeHook(
|
|
125
|
-
const settings = readSettings(
|
|
143
|
+
it("deletes empty SessionStart and hooks", () => {
|
|
144
|
+
const p = join(tempDir, "settings.json");
|
|
145
|
+
installHook(p);
|
|
146
|
+
removeHook(p);
|
|
147
|
+
const settings = readSettings(p);
|
|
126
148
|
expect(settings.hooks).toBeUndefined();
|
|
127
149
|
});
|
|
128
150
|
|
|
129
|
-
it("returns false when
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
expect(result.removed).toBe(false);
|
|
133
|
-
expect(result.reason).toBe("not installed");
|
|
151
|
+
it("returns false when not installed", () => {
|
|
152
|
+
const p = join(tempDir, "settings.json");
|
|
153
|
+
expect(removeHook(p).removed).toBe(false);
|
|
134
154
|
});
|
|
135
155
|
|
|
136
|
-
it("preserves other hooks
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
expect(settings.hooks.SessionStart).toContain("other-hook");
|
|
144
|
-
expect(settings.hooks.SessionStart).not.toContain("npx buddy-reroll --apply-hook");
|
|
156
|
+
it("preserves other hooks", () => {
|
|
157
|
+
const p = join(tempDir, "settings.json");
|
|
158
|
+
const otherHook = { matcher: "Bash", hooks: [{ type: "command", command: "echo hi" }] };
|
|
159
|
+
writeSettings(p, { hooks: { SessionStart: [otherHook, HOOK_COMMAND] } });
|
|
160
|
+
removeHook(p);
|
|
161
|
+
const settings = readSettings(p);
|
|
162
|
+
expect(settings.hooks.SessionStart).toEqual([otherHook]);
|
|
145
163
|
});
|
|
146
164
|
|
|
147
165
|
it("preserves other hook types", () => {
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
const settings = readSettings(settingsPath);
|
|
157
|
-
expect(settings.hooks.OnExit).toContain("some-command");
|
|
166
|
+
const p = join(tempDir, "settings.json");
|
|
167
|
+
installHook(p);
|
|
168
|
+
const settings = readSettings(p);
|
|
169
|
+
settings.hooks.PostToolUse = [{ matcher: "Edit", hooks: [{ type: "command", command: "lint" }] }];
|
|
170
|
+
writeSettings(p, settings);
|
|
171
|
+
removeHook(p);
|
|
172
|
+
const after = readSettings(p);
|
|
173
|
+
expect(after.hooks.PostToolUse).toBeDefined();
|
|
158
174
|
});
|
|
159
175
|
});
|
|
160
176
|
|
|
161
177
|
describe("isHookInstalled", () => {
|
|
162
|
-
it("
|
|
163
|
-
const
|
|
164
|
-
installHook(
|
|
165
|
-
expect(isHookInstalled(
|
|
178
|
+
it("true after install", () => {
|
|
179
|
+
const p = join(tempDir, "settings.json");
|
|
180
|
+
installHook(p);
|
|
181
|
+
expect(isHookInstalled(p)).toBe(true);
|
|
166
182
|
});
|
|
167
183
|
|
|
168
|
-
it("
|
|
169
|
-
const
|
|
170
|
-
installHook(
|
|
171
|
-
removeHook(
|
|
172
|
-
expect(isHookInstalled(
|
|
184
|
+
it("false after remove", () => {
|
|
185
|
+
const p = join(tempDir, "settings.json");
|
|
186
|
+
installHook(p);
|
|
187
|
+
removeHook(p);
|
|
188
|
+
expect(isHookInstalled(p)).toBe(false);
|
|
173
189
|
});
|
|
174
190
|
|
|
175
|
-
it("
|
|
176
|
-
|
|
177
|
-
expect(isHookInstalled(settingsPath)).toBe(false);
|
|
191
|
+
it("false when no file", () => {
|
|
192
|
+
expect(isHookInstalled(join(tempDir, "settings.json"))).toBe(false);
|
|
178
193
|
});
|
|
179
194
|
|
|
180
|
-
it("
|
|
181
|
-
const
|
|
182
|
-
writeSettings(
|
|
183
|
-
expect(isHookInstalled(
|
|
195
|
+
it("detects old string-format as installed", () => {
|
|
196
|
+
const p = join(tempDir, "settings.json");
|
|
197
|
+
writeSettings(p, { hooks: { SessionStart: [HOOK_COMMAND] } });
|
|
198
|
+
expect(isHookInstalled(p)).toBe(true);
|
|
184
199
|
});
|
|
185
200
|
});
|
|
186
201
|
|
|
187
202
|
describe("storeSalt and readStoredSalt", () => {
|
|
188
|
-
it("roundtrips salt
|
|
189
|
-
|
|
190
|
-
storeSalt(salt);
|
|
203
|
+
it("roundtrips salt", () => {
|
|
204
|
+
storeSalt("test-salt");
|
|
191
205
|
const stored = readStoredSalt();
|
|
192
|
-
expect(stored.salt).toBe(salt);
|
|
193
|
-
expect(typeof stored.timestamp).toBe("number");
|
|
206
|
+
expect(stored.salt).toBe("test-salt");
|
|
194
207
|
expect(stored.timestamp).toBeGreaterThan(0);
|
|
195
208
|
});
|
|
196
209
|
|
|
197
|
-
it("returns null when
|
|
210
|
+
it("returns null when missing", () => {
|
|
198
211
|
expect(readStoredSalt()).toBeNull();
|
|
199
212
|
});
|
|
200
213
|
|
|
201
|
-
it("returns null for corrupted
|
|
202
|
-
|
|
203
|
-
writeFileSync(saltPath, "{ invalid json");
|
|
214
|
+
it("returns null for corrupted file", () => {
|
|
215
|
+
writeFileSync(join(tempDir, ".buddy-reroll.json"), "broken");
|
|
204
216
|
expect(readStoredSalt()).toBeNull();
|
|
205
217
|
});
|
|
206
|
-
|
|
207
|
-
it("creates parent directories for salt file", () => {
|
|
208
|
-
const salt = "another-test-salt";
|
|
209
|
-
storeSalt(salt);
|
|
210
|
-
const stored = readStoredSalt();
|
|
211
|
-
expect(stored.salt).toBe(salt);
|
|
212
|
-
});
|
|
213
218
|
});
|
|
214
219
|
|
|
215
220
|
describe("integration", () => {
|
|
216
|
-
it("preserves
|
|
217
|
-
const
|
|
218
|
-
writeSettings(
|
|
221
|
+
it("preserves complex settings through install and remove", () => {
|
|
222
|
+
const p = join(tempDir, "settings.json");
|
|
223
|
+
writeSettings(p, {
|
|
219
224
|
permissions: { allow: ["some-permission"] },
|
|
220
225
|
other: { nested: { value: 42 } },
|
|
221
226
|
});
|
|
222
|
-
installHook(
|
|
223
|
-
const
|
|
224
|
-
expect(
|
|
225
|
-
expect(
|
|
226
|
-
expect(
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
permissions: { allow: ["some-permission"] },
|
|
233
|
-
hooks: { SessionStart: ["npx buddy-reroll --apply-hook"] },
|
|
234
|
-
});
|
|
235
|
-
removeHook(settingsPath);
|
|
236
|
-
const settings = readSettings(settingsPath);
|
|
237
|
-
expect(settings.permissions).toEqual({ allow: ["some-permission"] });
|
|
238
|
-
expect(settings.hooks).toBeUndefined();
|
|
227
|
+
installHook(p);
|
|
228
|
+
const after = readSettings(p);
|
|
229
|
+
expect(after.permissions).toEqual({ allow: ["some-permission"] });
|
|
230
|
+
expect(after.other).toEqual({ nested: { value: 42 } });
|
|
231
|
+
expect(findOurHook(after)).toBeDefined();
|
|
232
|
+
|
|
233
|
+
removeHook(p);
|
|
234
|
+
const final = readSettings(p);
|
|
235
|
+
expect(final.permissions).toEqual({ allow: ["some-permission"] });
|
|
236
|
+
expect(final.hooks).toBeUndefined();
|
|
239
237
|
});
|
|
240
238
|
});
|