create-religion 0.3.0 → 0.5.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/dist/bin/religion.js
CHANGED
|
@@ -86,16 +86,33 @@ async function runInstall(options) {
|
|
|
86
86
|
create: plan.filter((p) => p.action === "create").length,
|
|
87
87
|
update: plan.filter((p) => p.action === "update").length,
|
|
88
88
|
conflict: plan.filter((p) => p.action === "conflict").length,
|
|
89
|
-
kept: plan.filter((p) => p.action === "seed-skip").length
|
|
89
|
+
kept: plan.filter((p) => p.action === "seed-skip").length,
|
|
90
|
+
merge: plan.filter((p) => p.action === "merge").length,
|
|
91
|
+
remerge: plan.filter((p) => p.action === "remerge").length
|
|
90
92
|
};
|
|
91
93
|
console.log(`\nAdapters: ${adapters.map((a) => ADAPTERS[a].label).join(", ")}`);
|
|
92
94
|
console.log(` create ${counts.create}`);
|
|
93
95
|
console.log(` update ${counts.update}`);
|
|
94
96
|
console.log(` keep ${counts.kept} (your files, never overwritten)`);
|
|
95
97
|
console.log(` conflict ${counts.conflict}`);
|
|
98
|
+
if (counts.merge > 0)
|
|
99
|
+
console.log(` merge ${counts.merge} (your file, awaiting a decision)`);
|
|
100
|
+
if (counts.remerge > 0)
|
|
101
|
+
console.log(` merged ${counts.remerge} (your sections kept)`);
|
|
96
102
|
for (const entry of plan.filter((p) => p.action === "conflict")) {
|
|
97
103
|
console.log(` ${entry.relative}`);
|
|
98
104
|
}
|
|
105
|
+
const mergeable = plan.filter((p) => p.action === "merge").map((p) => p.relative);
|
|
106
|
+
let merge = false;
|
|
107
|
+
if (mergeable.length > 0) {
|
|
108
|
+
console.log(`\nThese already exist and were written by you:`);
|
|
109
|
+
for (const relative of mergeable)
|
|
110
|
+
console.log(` ${relative}`);
|
|
111
|
+
console.log("\nReligion can append its own sections to them inside markers, keeping everything" +
|
|
112
|
+
"\nyou wrote exactly where it is. Later updates then replace only what is between" +
|
|
113
|
+
"\nthose markers. The originals are backed up either way.");
|
|
114
|
+
merge = await confirmMerge(options.yes);
|
|
115
|
+
}
|
|
99
116
|
if (options.dryRun) {
|
|
100
117
|
console.log("\nDry run. Nothing written.");
|
|
101
118
|
return;
|
|
@@ -103,9 +120,12 @@ async function runInstall(options) {
|
|
|
103
120
|
if (plan.length === 0) {
|
|
104
121
|
throw new Error(`No template files found under ${templateRoot}. Nothing would be installed, which is never correct.`);
|
|
105
122
|
}
|
|
106
|
-
const result = await applyInstall(templateRoot, target, plan, { force: options.force });
|
|
123
|
+
const result = await applyInstall(templateRoot, target, plan, { force: options.force, merge });
|
|
107
124
|
await writeManifest(target, await version(), adapters, templateRoot, previous, result.conflicts);
|
|
108
125
|
console.log(`\nWrote ${result.written.length} file(s).`);
|
|
126
|
+
if (result.merged.length > 0) {
|
|
127
|
+
console.log(`Merged ${result.merged.length} entry file(s), keeping what you wrote.`);
|
|
128
|
+
}
|
|
109
129
|
if (result.backups.length > 0)
|
|
110
130
|
console.log(`Backed up ${result.backups.length} conflicting file(s).`);
|
|
111
131
|
if (result.conflicts.length > 0) {
|
|
@@ -126,6 +146,21 @@ async function runInstall(options) {
|
|
|
126
146
|
console.log("\nNext: run the setup skill in your AI tool, then fill in the two plans.");
|
|
127
147
|
}
|
|
128
148
|
}
|
|
149
|
+
/**
|
|
150
|
+
* Ask before rewriting a file the user wrote.
|
|
151
|
+
*
|
|
152
|
+
* Defaults to yes because the merge is additive and reversible: nothing of theirs moves and
|
|
153
|
+
* the original is backed up first. Declining leaves the file untouched and reported as a
|
|
154
|
+
* conflict, which is the old behaviour.
|
|
155
|
+
*/
|
|
156
|
+
async function confirmMerge(assumeYes) {
|
|
157
|
+
if (assumeYes || !process.stdin.isTTY)
|
|
158
|
+
return assumeYes;
|
|
159
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
160
|
+
const answer = (await rl.question("\nMerge them? [Y/n] ")).trim().toLowerCase();
|
|
161
|
+
rl.close();
|
|
162
|
+
return answer === "" || answer === "y" || answer === "yes";
|
|
163
|
+
}
|
|
129
164
|
async function chooseAdapters(assumeYes) {
|
|
130
165
|
const all = Object.keys(ADAPTERS);
|
|
131
166
|
if (assumeYes || !process.stdin.isTTY)
|
package/dist/lib/install.js
CHANGED
|
@@ -10,6 +10,7 @@ import crypto from "node:crypto";
|
|
|
10
10
|
import fs from "node:fs/promises";
|
|
11
11
|
import path from "node:path";
|
|
12
12
|
import { exists, STATE_DIR } from "./paths.js";
|
|
13
|
+
import { hasDamagedMarkers, hasMarkers, replaceManagedBlock, spliceEntry } from "./merge.js";
|
|
13
14
|
export const ADAPTERS = {
|
|
14
15
|
claude: { label: "Claude Code", trees: [".claude/skills", ".claude/hooks"], entry: ["CLAUDE.md"] },
|
|
15
16
|
codex: { label: "Codex", trees: [".agents/skills"], entry: ["AGENTS.md"] },
|
|
@@ -24,8 +25,13 @@ export function hash(contents) {
|
|
|
24
25
|
export function isManaged(relative) {
|
|
25
26
|
return !relative.startsWith(`${STATE_DIR}/`);
|
|
26
27
|
}
|
|
28
|
+
/** The entry files for the chosen adapters, which are the only files merging applies to. */
|
|
29
|
+
function entryFiles(adapters) {
|
|
30
|
+
return new Set(adapters.flatMap((a) => ADAPTERS[a].entry));
|
|
31
|
+
}
|
|
27
32
|
export async function planInstall(templateRoot, target, adapters, previous) {
|
|
28
33
|
const wanted = wantedPaths(adapters);
|
|
34
|
+
const entries = entryFiles(adapters);
|
|
29
35
|
const plan = [];
|
|
30
36
|
for (const relative of await walk(templateRoot)) {
|
|
31
37
|
if (!wanted(relative))
|
|
@@ -50,7 +56,22 @@ export async function planInstall(templateRoot, target, adapters, previous) {
|
|
|
50
56
|
// installed. Anything else is a local edit, and overwriting it silently is the failure
|
|
51
57
|
// this manifest exists to prevent.
|
|
52
58
|
const recorded = previous?.managed[relative];
|
|
53
|
-
|
|
59
|
+
if (recorded && recorded === hash(current)) {
|
|
60
|
+
plan.push({ relative, action: "update" });
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
// An entry file is the one place where "differs from the template" is the normal case
|
|
64
|
+
// rather than a problem: a repository that already tells agents how to work has one, and
|
|
65
|
+
// it is usually the most carefully written file in the project.
|
|
66
|
+
if (entries.has(relative)) {
|
|
67
|
+
const text = current.toString("utf8");
|
|
68
|
+
// A damaged marker pair is a conflict, never a fresh merge: splicing a second block
|
|
69
|
+
// into a file that already has one is worse than the damage.
|
|
70
|
+
const action = hasMarkers(text) ? "remerge" : hasDamagedMarkers(text) ? "conflict" : "merge";
|
|
71
|
+
plan.push({ relative, action });
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
plan.push({ relative, action: "conflict" });
|
|
54
75
|
}
|
|
55
76
|
return plan;
|
|
56
77
|
}
|
|
@@ -58,6 +79,7 @@ export async function applyInstall(templateRoot, target, plan, options) {
|
|
|
58
79
|
const written = [];
|
|
59
80
|
const conflicts = [];
|
|
60
81
|
const backups = [];
|
|
82
|
+
const merged = [];
|
|
61
83
|
for (const entry of plan) {
|
|
62
84
|
if (entry.action === "unchanged" || entry.action === "seed-skip")
|
|
63
85
|
continue;
|
|
@@ -65,6 +87,33 @@ export async function applyInstall(templateRoot, target, plan, options) {
|
|
|
65
87
|
conflicts.push(entry.relative);
|
|
66
88
|
continue;
|
|
67
89
|
}
|
|
90
|
+
// Replacing only what is between the markers is the whole point of having merged: the
|
|
91
|
+
// user's own prose sits outside them and is never read, let alone rewritten.
|
|
92
|
+
if (entry.action === "remerge" || (entry.action === "merge" && options.merge)) {
|
|
93
|
+
const destination = path.join(target, entry.relative);
|
|
94
|
+
const template = await fs.readFile(path.join(templateRoot, entry.relative), "utf8");
|
|
95
|
+
const current = await fs.readFile(destination, "utf8");
|
|
96
|
+
if (entry.action === "merge") {
|
|
97
|
+
const backup = path.join(target, STATE_DIR, ".state", "backups", entry.relative);
|
|
98
|
+
await fs.mkdir(path.dirname(backup), { recursive: true });
|
|
99
|
+
await fs.copyFile(destination, backup);
|
|
100
|
+
backups.push(entry.relative);
|
|
101
|
+
}
|
|
102
|
+
const next = entry.action === "merge" ? spliceEntry(current, template) : replaceManagedBlock(current, template);
|
|
103
|
+
if (next === null) {
|
|
104
|
+
// Markers went missing between planning and writing, or were edited into an order
|
|
105
|
+
// this cannot reason about. Leaving it alone is the only safe answer.
|
|
106
|
+
conflicts.push(entry.relative);
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
await fs.writeFile(destination, next, "utf8");
|
|
110
|
+
merged.push(entry.relative);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (entry.action === "merge") {
|
|
114
|
+
conflicts.push(entry.relative);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
68
117
|
const destination = path.join(target, entry.relative);
|
|
69
118
|
if (entry.action === "conflict") {
|
|
70
119
|
const backup = path.join(target, STATE_DIR, ".state", "backups", entry.relative);
|
|
@@ -76,7 +125,7 @@ export async function applyInstall(templateRoot, target, plan, options) {
|
|
|
76
125
|
await fs.copyFile(path.join(templateRoot, entry.relative), destination);
|
|
77
126
|
written.push(entry.relative);
|
|
78
127
|
}
|
|
79
|
-
return { written, conflicts, backups };
|
|
128
|
+
return { written, conflicts, backups, merged };
|
|
80
129
|
}
|
|
81
130
|
/**
|
|
82
131
|
* Records what was *installed*, not what is currently on disk.
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Merging Religion's entry-file sections into a file the project already had.
|
|
3
|
+
*
|
|
4
|
+
* A repository that already carries agent instructions is the normal case, not the odd one,
|
|
5
|
+
* and the previous behaviour left those installs with the workflow on disk but nothing
|
|
6
|
+
* telling the agent it existed. Overwriting was never an option: those files are often the
|
|
7
|
+
* most carefully written thing in the repository.
|
|
8
|
+
*
|
|
9
|
+
* So the user's file stays exactly where it is and Religion's sections are appended inside
|
|
10
|
+
* markers. The markers are what make the result maintainable rather than a one-time paste:
|
|
11
|
+
* a later update replaces what is between them and never reads what is outside.
|
|
12
|
+
*
|
|
13
|
+
* Two sections are deliberately left outside the block. "What this is" is what the user's own
|
|
14
|
+
* file already says, and Commands is filled in by the setup skill with the project's real
|
|
15
|
+
* commands. Both would be destroyed by an update that replaced them.
|
|
16
|
+
*/
|
|
17
|
+
export const MANAGED_START = "<!-- religion:managed:start -->";
|
|
18
|
+
export const MANAGED_END = "<!-- religion:managed:end -->";
|
|
19
|
+
/** Sections the user owns. They never enter the managed block. */
|
|
20
|
+
const USER_OWNED = new Set(["What this is", "Commands"]);
|
|
21
|
+
const BLOCK_NOTE = "Everything between these markers is generated by Religion and replaced on update.\n" +
|
|
22
|
+
"Edit the files it points at, or the sections outside this block, rather than editing here.";
|
|
23
|
+
/** Split a rendered entry file into its leading lines and its `## ` sections. */
|
|
24
|
+
function parse(entry) {
|
|
25
|
+
const lines = entry.split(/\r?\n/);
|
|
26
|
+
const lead = [];
|
|
27
|
+
const sections = [];
|
|
28
|
+
let current = null;
|
|
29
|
+
for (const line of lines) {
|
|
30
|
+
const heading = /^## (.+)$/.exec(line);
|
|
31
|
+
if (heading?.[1]) {
|
|
32
|
+
current = { heading: heading[1].trim(), body: "" };
|
|
33
|
+
sections.push(current);
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (current)
|
|
37
|
+
current.body += line + "\n";
|
|
38
|
+
else
|
|
39
|
+
lead.push(line);
|
|
40
|
+
}
|
|
41
|
+
return { lead, sections };
|
|
42
|
+
}
|
|
43
|
+
function render(sections) {
|
|
44
|
+
return sections.map((s) => `## ${s.heading}\n${s.body.replace(/\n+$/, "")}\n`).join("\n");
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The part of a rendered entry file that Religion owns.
|
|
48
|
+
*
|
|
49
|
+
* Includes the import lines, which is what makes a merged install keep receiving new context
|
|
50
|
+
* files: leaving them outside the block would freeze the set of loaded files at whatever
|
|
51
|
+
* shipped on the day of the merge.
|
|
52
|
+
*/
|
|
53
|
+
export function managedBlock(templateEntry) {
|
|
54
|
+
const { lead, sections } = parse(templateEntry);
|
|
55
|
+
const imports = lead.filter((line) => line.startsWith("@")).join("\n");
|
|
56
|
+
const owned = sections.filter((s) => !USER_OWNED.has(s.heading));
|
|
57
|
+
return [
|
|
58
|
+
MANAGED_START,
|
|
59
|
+
"",
|
|
60
|
+
BLOCK_NOTE,
|
|
61
|
+
"",
|
|
62
|
+
...(imports ? [imports, ""] : []),
|
|
63
|
+
render(owned).replace(/\n+$/, ""),
|
|
64
|
+
"",
|
|
65
|
+
MANAGED_END
|
|
66
|
+
].join("\n");
|
|
67
|
+
}
|
|
68
|
+
/** The sections the user owns, taken from the template as a starting point. */
|
|
69
|
+
export function userOwnedSections(templateEntry) {
|
|
70
|
+
const { sections } = parse(templateEntry);
|
|
71
|
+
const owned = sections.filter((s) => USER_OWNED.has(s.heading) && s.heading !== "What this is");
|
|
72
|
+
return render(owned).replace(/\n+$/, "");
|
|
73
|
+
}
|
|
74
|
+
export function hasMarkers(contents) {
|
|
75
|
+
return contents.includes(MANAGED_START) && contents.includes(MANAGED_END);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* A file carrying one marker but not both, or them in the wrong order.
|
|
79
|
+
*
|
|
80
|
+
* This must be told apart from a file that was never merged. Treating it as unmerged splices
|
|
81
|
+
* a second block into a file that already had one, which is a worse state than the damage it
|
|
82
|
+
* was trying to repair.
|
|
83
|
+
*/
|
|
84
|
+
export function hasDamagedMarkers(contents) {
|
|
85
|
+
const start = contents.indexOf(MANAGED_START);
|
|
86
|
+
const end = contents.indexOf(MANAGED_END);
|
|
87
|
+
if (start < 0 && end < 0)
|
|
88
|
+
return false;
|
|
89
|
+
return start < 0 || end < 0 || end < start;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Build the merged file: their content, then Religion's block, then the sections they own.
|
|
93
|
+
*
|
|
94
|
+
* Nothing of theirs moves. That is the property that makes this safe to accept without
|
|
95
|
+
* reading the diff, and the reason the block is appended rather than woven in.
|
|
96
|
+
*/
|
|
97
|
+
export function spliceEntry(existing, templateEntry) {
|
|
98
|
+
const body = existing.replace(/\n+$/, "");
|
|
99
|
+
const commands = userOwnedSections(templateEntry);
|
|
100
|
+
const parts = [body, "", managedBlock(templateEntry)];
|
|
101
|
+
// Only add Commands when their file has no section by that name; theirs is more accurate
|
|
102
|
+
// than the placeholder, and setup will find either one.
|
|
103
|
+
if (commands && !/^## Commands\s*$/m.test(existing))
|
|
104
|
+
parts.push("", commands);
|
|
105
|
+
return parts.join("\n") + "\n";
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Replace the managed block in a merged file, leaving everything outside it untouched.
|
|
109
|
+
*
|
|
110
|
+
* Returns null when the markers are missing or out of order, which is treated as a conflict
|
|
111
|
+
* rather than repaired: a file whose markers were edited is one whose owner had a reason.
|
|
112
|
+
*/
|
|
113
|
+
export function replaceManagedBlock(existing, templateEntry) {
|
|
114
|
+
const start = existing.indexOf(MANAGED_START);
|
|
115
|
+
const end = existing.indexOf(MANAGED_END);
|
|
116
|
+
if (start < 0 || end < 0 || end < start)
|
|
117
|
+
return null;
|
|
118
|
+
return existing.slice(0, start) + managedBlock(templateEntry) + existing.slice(end + MANAGED_END.length);
|
|
119
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-religion",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "A file-backed, spec-driven AI development workflow. Spec before code, small reviewed diffs, and project state in markdown so a cleared context costs nothing.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Nate",
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Blocks first-tier commands that were not approved in the conversation.
|
|
4
|
-
*
|
|
5
|
-
* This enforces nothing new. The Authority section of the project entry file already says
|
|
6
|
-
* merging, pushing, deploying, publishing, deleting data, and rewriting history each need
|
|
7
|
-
* a fresh yes. The hook exists because that rule is prose, and prose can be forgotten
|
|
8
|
-
* mid-task. It is a safety net for one tool, never a rule of its own.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { readFileSync } from "node:fs";
|
|
12
|
-
|
|
13
|
-
const PATTERNS = [
|
|
14
|
-
{ re: /\bgit\s+push\b/, what: "pushing to a remote" },
|
|
15
|
-
{ re: /\bgit\s+merge\b/, what: "merging" },
|
|
16
|
-
{ re: /\bgit\s+(?:rebase|reset\s+--hard|filter-branch)\b/, what: "rewriting history" },
|
|
17
|
-
{ re: /\bgit\s+commit\b.*--amend\b/, what: "rewriting a commit" },
|
|
18
|
-
{ re: /\bnpm\s+publish\b|\bpnpm\s+publish\b|\byarn\s+publish\b/, what: "publishing" },
|
|
19
|
-
{ re: /\b(?:vercel|railway|render)\s+(?:deploy|up)\b/, what: "deploying" },
|
|
20
|
-
{ re: /\brm\s+-[a-z]*r[a-z]*f?\b/, what: "recursive deletion" }
|
|
21
|
-
];
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Degrade to a prompt when this guard cannot do its job.
|
|
25
|
-
*
|
|
26
|
-
* A guard that fails open becomes the exact outcome it exists to prevent: the command runs
|
|
27
|
-
* and nobody was asked. Failing closed is no better, because one malformed payload from a
|
|
28
|
-
* runtime change would block every push, merge, publish or deploy until somebody edits a hook. Asking is the
|
|
29
|
-
* honest middle: the user sees the command and decides, and the cost of a bug here is one
|
|
30
|
-
* extra prompt rather than a silent pass.
|
|
31
|
-
*/
|
|
32
|
-
function askBecauseGuardFailed(error) {
|
|
33
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
34
|
-
process.stdout.write(JSON.stringify({
|
|
35
|
-
hookSpecificOutput: {
|
|
36
|
-
hookEventName: "PreToolUse",
|
|
37
|
-
permissionDecision: "ask",
|
|
38
|
-
permissionDecisionReason:
|
|
39
|
-
`The first-tier guard hook could not run, so this command was not checked.\n\n` +
|
|
40
|
-
`Reason: ${detail}\n\n` +
|
|
41
|
-
`Approve it only if you would have approved it anyway. The guard is not covering ` +
|
|
42
|
-
`you right now, and that is worth reporting.`
|
|
43
|
-
}
|
|
44
|
-
}));
|
|
45
|
-
process.exit(0);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Covers the whole hook, not only the reads below: any throw reaches this and still asks.
|
|
49
|
-
process.on("uncaughtException", askBecauseGuardFailed);
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
let input = "";
|
|
53
|
-
try {
|
|
54
|
-
input = readFileSync(0, "utf8");
|
|
55
|
-
} catch (error) {
|
|
56
|
-
askBecauseGuardFailed(error);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
let command = "";
|
|
60
|
-
try {
|
|
61
|
-
const payload = JSON.parse(input);
|
|
62
|
-
command = payload?.tool_input?.command ?? "";
|
|
63
|
-
} catch (error) {
|
|
64
|
-
askBecauseGuardFailed(error);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (!command) process.exit(0);
|
|
68
|
-
|
|
69
|
-
// A force flag makes an otherwise ordinary command destructive, so it is always first tier.
|
|
70
|
-
const forced = /--force\b|(?:^|\s)-f(?:\s|$)/.test(command) && /\bgit\s+push\b/.test(command);
|
|
71
|
-
|
|
72
|
-
for (const { re, what } of PATTERNS) {
|
|
73
|
-
if (!re.test(command)) continue;
|
|
74
|
-
|
|
75
|
-
const reason =
|
|
76
|
-
`Blocked: ${what}${forced ? " (forced)" : ""} is a first-tier action.\n\n` +
|
|
77
|
-
`The Authority section of this project's entry file requires an explicit yes for it, ` +
|
|
78
|
-
`in this conversation. The one narrow exception is an automated run under ` +
|
|
79
|
-
`git.mode pull-request, which may push branches it created after enumerating exactly ` +
|
|
80
|
-
`that at invocation. This prompt is a net, not a second opinion: if that grant was ` +
|
|
81
|
-
`given, confirm and continue.\n\n` +
|
|
82
|
-
`If the user has already approved this exact action, say so and ask them to confirm ` +
|
|
83
|
-
`once more, then proceed. Do not edit or disable this hook to get past it.`;
|
|
84
|
-
|
|
85
|
-
process.stdout.write(JSON.stringify({
|
|
86
|
-
hookSpecificOutput: {
|
|
87
|
-
hookEventName: "PreToolUse",
|
|
88
|
-
permissionDecision: "ask",
|
|
89
|
-
permissionDecisionReason: reason
|
|
90
|
-
}
|
|
91
|
-
}));
|
|
92
|
-
process.exit(0);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
process.exit(0);
|