@sdd-method/maintain-cli 0.1.0 → 0.2.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/README.md +1 -1
- package/dist/lib/agent-harness.js +17 -8
- package/dist/lib/briefing/index.js +1 -1
- package/dist/lib/briefing/render.js +17 -8
- package/dist/lib/catalogue/aggregation-manifest-loader.js +4 -4
- package/dist/lib/catalogue/freshness.js +4 -3
- package/dist/lib/context-pull/index.js +79 -39
- package/dist/lib/context-pull/retired-mcp-setup.js +7 -0
- package/dist/lib/hooks-seed/entries.js +30 -1
- package/dist/lib/hooks-seed/index.js +4 -7
- package/dist/lib/hooks-seed/merge.js +31 -0
- package/dist/lib/sync/briefing-merge.js +50 -34
- package/dist/lib/sync/diff.js +1 -1
- package/dist/lib/sync/index.js +20 -70
- package/dist/lib/sync/settings-merge.js +70 -162
- package/dist/maintain-program.js +5 -2
- package/dist/maintain-version.json +2 -2
- package/dist/verbs/catalogue-check.js +28 -0
- package/dist/verbs/context.js +1 -1
- package/dist/verify-program.js +1 -1
- package/dist/verify-version.json +2 -2
- package/package.json +2 -2
- package/dist/lib/hooks-seed/copilot.js +0 -167
- package/dist/lib/hooks-seed/lifecycle-mapping.js +0 -28
- package/dist/lib/mcp-seed/entries.js +0 -14
- package/dist/lib/mcp-seed/index.js +0 -28
- package/dist/lib/mcp-seed/merge.js +0 -99
- package/dist/lib/mcp-seed/types.js +0 -13
- package/dist/lib/sync/copilot-hooks-merge.js +0 -108
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Builders + drift detection for the method-managed GitHub Copilot CLI
|
|
3
|
-
* hooks, per `docs/method/methods/hook-contracts.md` (Hook 1, Hook 2)
|
|
4
|
-
* and the portable contracts in `hook-portable-contract-schema.yaml`.
|
|
5
|
-
*
|
|
6
|
-
* The Copilot CLI surface differs from Claude Code in three ways
|
|
7
|
-
* (per ADR 0141 §4.3 + hook-contracts.md):
|
|
8
|
-
*
|
|
9
|
-
* 1. Hook configs live in per-event JSON files (`.github/hooks/<event>.json`),
|
|
10
|
-
* not a single settings.json.
|
|
11
|
-
* 2. No per-tool matcher — every `preToolUse` hook fires for every
|
|
12
|
-
* tool, with in-script filtering instead.
|
|
13
|
-
* 3. Approval semantic is declarative (`approval: deny-on-nonzero`)
|
|
14
|
-
* rather than exit-code based.
|
|
15
|
-
*
|
|
16
|
-
* Ownership is sentinel-based, same as Claude Code: the `bash` and
|
|
17
|
-
* `powershell` command strings carry `# sdd-method-managed:true`.
|
|
18
|
-
* Adopter-added events in the same `events[]` array lack the sentinel
|
|
19
|
-
* and survive sync.
|
|
20
|
-
*/
|
|
21
|
-
import { METHOD_MANAGED_SENTINEL } from "./types.js";
|
|
22
|
-
import { nativeEventName } from "./lifecycle-mapping.js";
|
|
23
|
-
const HARNESS = "github-copilot-cli";
|
|
24
|
-
/** Filename per portable lifecycle class. */
|
|
25
|
-
const HOOK_FILE_BY_PORTABLE = {
|
|
26
|
-
pre_tool_use: "pre-tool-use.json",
|
|
27
|
-
session_start: "session-start.json",
|
|
28
|
-
};
|
|
29
|
-
export function copilotHookFilePath(portable) {
|
|
30
|
-
const name = HOOK_FILE_BY_PORTABLE[portable];
|
|
31
|
-
if (!name) {
|
|
32
|
-
throw new Error(`No Copilot CLI hook file mapped for portable class "${portable}"`);
|
|
33
|
-
}
|
|
34
|
-
return `.github/hooks/${name}`;
|
|
35
|
-
}
|
|
36
|
-
function withSentinel(command) {
|
|
37
|
-
if (command.includes(METHOD_MANAGED_SENTINEL))
|
|
38
|
-
return command;
|
|
39
|
-
return `${command} ${METHOD_MANAGED_SENTINEL}`;
|
|
40
|
-
}
|
|
41
|
-
/**
|
|
42
|
-
* Hook 1 — `block-edits-to-method-managed-paths` (pre_tool_use).
|
|
43
|
-
* Copilot CLI has no per-tool matcher, so the script
|
|
44
|
-
* (orchestration/scripts/copilot-method-guard.{sh,ps1}) does the
|
|
45
|
-
* filtering on tool name in addition to the path check. Method-side
|
|
46
|
-
* authors the scripts; we just emit the wiring.
|
|
47
|
-
*/
|
|
48
|
-
export function buildCopilotPreToolUseEvent() {
|
|
49
|
-
return {
|
|
50
|
-
type: nativeEventName(HARNESS, "pre_tool_use"),
|
|
51
|
-
command: {
|
|
52
|
-
bash: withSentinel("./orchestration/scripts/copilot-method-guard.sh"),
|
|
53
|
-
powershell: withSentinel("./orchestration/scripts/copilot-method-guard.ps1"),
|
|
54
|
-
},
|
|
55
|
-
timeout_ms: 5000,
|
|
56
|
-
approval: "deny-on-nonzero",
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
/**
|
|
60
|
-
* Hook 2 — `validate-adr-structure-on-session-start` (session_start).
|
|
61
|
-
* Output is surfaced as session-baseline context; non-blocking
|
|
62
|
-
* (`approval: always-allow`) to match the portable contract's `warn`
|
|
63
|
-
* exit-code semantic.
|
|
64
|
-
*/
|
|
65
|
-
export function buildCopilotSessionStartEvent() {
|
|
66
|
-
return {
|
|
67
|
-
type: nativeEventName(HARNESS, "session_start"),
|
|
68
|
-
command: {
|
|
69
|
-
bash: withSentinel("sdd-cli validate --validator adr-structure 2>&1 | head -20"),
|
|
70
|
-
powershell: withSentinel("sdd-cli validate --validator adr-structure 2>&1 | Select-Object -First 20"),
|
|
71
|
-
},
|
|
72
|
-
timeout_ms: 30000,
|
|
73
|
-
approval: "always-allow",
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
export function buildMethodManagedCopilotHooks() {
|
|
77
|
-
return {
|
|
78
|
-
pre_tool_use: {
|
|
79
|
-
filePath: copilotHookFilePath("pre_tool_use"),
|
|
80
|
-
events: [buildCopilotPreToolUseEvent()],
|
|
81
|
-
},
|
|
82
|
-
session_start: {
|
|
83
|
-
filePath: copilotHookFilePath("session_start"),
|
|
84
|
-
events: [buildCopilotSessionStartEvent()],
|
|
85
|
-
},
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
/**
|
|
89
|
-
* An event entry is method-managed iff its `command.bash` (or
|
|
90
|
-
* `command.powershell`) carries the sentinel.
|
|
91
|
-
*/
|
|
92
|
-
export function isMethodManagedEvent(event) {
|
|
93
|
-
const bash = typeof event.command?.bash === "string" ? event.command.bash : "";
|
|
94
|
-
const ps = typeof event.command?.powershell === "string"
|
|
95
|
-
? event.command.powershell
|
|
96
|
-
: "";
|
|
97
|
-
return (bash.includes(METHOD_MANAGED_SENTINEL) ||
|
|
98
|
-
ps.includes(METHOD_MANAGED_SENTINEL));
|
|
99
|
-
}
|
|
100
|
-
/**
|
|
101
|
-
* Strip the sentinel and trailing whitespace from a command string so
|
|
102
|
-
* two commands can be compared modulo ownership marker. Symmetric to
|
|
103
|
-
* the Claude Code merge logic.
|
|
104
|
-
*/
|
|
105
|
-
function normaliseCommandString(command) {
|
|
106
|
-
return command.replace(METHOD_MANAGED_SENTINEL, "").trimEnd();
|
|
107
|
-
}
|
|
108
|
-
function eventsEquivalentExcludingSentinel(a, b) {
|
|
109
|
-
if (a.type !== b.type)
|
|
110
|
-
return false;
|
|
111
|
-
if (a.approval !== b.approval)
|
|
112
|
-
return false;
|
|
113
|
-
if (a.timeout_ms !== b.timeout_ms)
|
|
114
|
-
return false;
|
|
115
|
-
const aBash = a.command?.bash ?? "";
|
|
116
|
-
const bBash = b.command?.bash ?? "";
|
|
117
|
-
const aPs = a.command?.powershell ?? "";
|
|
118
|
-
const bPs = b.command?.powershell ?? "";
|
|
119
|
-
return (normaliseCommandString(aBash) === normaliseCommandString(bBash) &&
|
|
120
|
-
normaliseCommandString(aPs) === normaliseCommandString(bPs));
|
|
121
|
-
}
|
|
122
|
-
/**
|
|
123
|
-
* Upsert method-managed events into an existing hook file. Adopter-
|
|
124
|
-
* added events (no sentinel) survive. An unmarked adopter event that
|
|
125
|
-
* is equivalent (modulo sentinel) to a method-managed event we're
|
|
126
|
-
* adding gets de-duped — same safety net as the Claude Code merge
|
|
127
|
-
* post-v0.12.2.
|
|
128
|
-
*/
|
|
129
|
-
export function upsertCopilotHookFile(existing, owned) {
|
|
130
|
-
const currentEvents = existing?.events ?? [];
|
|
131
|
-
let dedupedAny = false;
|
|
132
|
-
const adopterEvents = currentEvents.filter((evt) => {
|
|
133
|
-
if (isMethodManagedEvent(evt))
|
|
134
|
-
return false;
|
|
135
|
-
const equivalent = owned.some((o) => eventsEquivalentExcludingSentinel(evt, o));
|
|
136
|
-
if (equivalent) {
|
|
137
|
-
dedupedAny = true;
|
|
138
|
-
return false;
|
|
139
|
-
}
|
|
140
|
-
return true;
|
|
141
|
-
});
|
|
142
|
-
const before = currentEvents.filter((e) => isMethodManagedEvent(e));
|
|
143
|
-
const after = owned;
|
|
144
|
-
const methodChanged = JSON.stringify(before) !== JSON.stringify(after);
|
|
145
|
-
const nextFile = {
|
|
146
|
-
version: 1,
|
|
147
|
-
events: [...adopterEvents, ...owned],
|
|
148
|
-
};
|
|
149
|
-
return {
|
|
150
|
-
file: nextFile,
|
|
151
|
-
changed: methodChanged || dedupedAny,
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
/**
|
|
155
|
-
* Detect adopter drift: an event still carrying the sentinel but whose
|
|
156
|
-
* content no longer matches the canonical version we'd write. Returns
|
|
157
|
-
* `true` iff drift is present.
|
|
158
|
-
*/
|
|
159
|
-
export function detectCopilotHookDrift(existing, owned) {
|
|
160
|
-
if (!existing)
|
|
161
|
-
return false;
|
|
162
|
-
const existingMethod = existing.events.filter((e) => isMethodManagedEvent(e));
|
|
163
|
-
if (existingMethod.length === 0)
|
|
164
|
-
return false;
|
|
165
|
-
return JSON.stringify(existingMethod) !== JSON.stringify(owned);
|
|
166
|
-
}
|
|
167
|
-
//# sourceMappingURL=copilot.js.map
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Portable lifecycle event class → harness-native event mapping per
|
|
3
|
-
* ADR 0141 §4.3 and the table in `docs/method/foundations/lifecycle-event-classes.yaml`.
|
|
4
|
-
*
|
|
5
|
-
* Adding a new harness binding is mechanical: extend `AgentHarness`
|
|
6
|
-
* in src/lib/agent-harness.ts and add a row here. Hook builders
|
|
7
|
-
* consume this mapping rather than hand-coding native event names.
|
|
8
|
-
*/
|
|
9
|
-
const MAPPINGS = {
|
|
10
|
-
"claude-code": {
|
|
11
|
-
pre_tool_use: "PreToolUse",
|
|
12
|
-
post_tool_use: "PostToolUse",
|
|
13
|
-
user_prompt_submit: "UserPromptSubmit",
|
|
14
|
-
session_start: "SessionStart",
|
|
15
|
-
session_stop: "Stop",
|
|
16
|
-
},
|
|
17
|
-
"github-copilot-cli": {
|
|
18
|
-
pre_tool_use: "preToolUse",
|
|
19
|
-
post_tool_use: "postToolUse",
|
|
20
|
-
user_prompt_submit: "userPromptSubmitted",
|
|
21
|
-
session_start: "sessionStart",
|
|
22
|
-
session_stop: "agentStop",
|
|
23
|
-
},
|
|
24
|
-
};
|
|
25
|
-
export function nativeEventName(harness, portable) {
|
|
26
|
-
return MAPPINGS[harness][portable];
|
|
27
|
-
}
|
|
28
|
-
//# sourceMappingURL=lifecycle-mapping.js.map
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Entry builders — produce the McpServerEntry blocks this plan owns.
|
|
3
|
-
*/
|
|
4
|
-
export function buildSddMcpEntry(opts = {}) {
|
|
5
|
-
const entry = {
|
|
6
|
-
command: "sdd-cli",
|
|
7
|
-
args: ["mcp", "serve"],
|
|
8
|
-
...(opts.sddRepoPath !== undefined
|
|
9
|
-
? { env: { SDD_REPO_PATH: opts.sddRepoPath } }
|
|
10
|
-
: {}),
|
|
11
|
-
};
|
|
12
|
-
return entry;
|
|
13
|
-
}
|
|
14
|
-
//# sourceMappingURL=entries.js.map
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Public facade for the MCP settings-seed library. Consumers:
|
|
3
|
-
*
|
|
4
|
-
* - `sdd-cli init` scaffolds (platform/app/integration) call
|
|
5
|
-
* `buildMethodManagedServers()` + `upsertMethodManagedServers()`
|
|
6
|
-
* to produce a correctly-seeded `.claude/settings.json`.
|
|
7
|
-
* - `sdd-cli sync` routes the same file through the merge path to
|
|
8
|
-
* upsert method-owned entries without clobbering adopter content.
|
|
9
|
-
*
|
|
10
|
-
* Scope: this module owns `mcpServers.<method-id>`. Hooks and other
|
|
11
|
-
* top-level keys belong to the enforcement-automation plan; we pass
|
|
12
|
-
* them through untouched.
|
|
13
|
-
*/
|
|
14
|
-
export { METHOD_MANAGED_SERVER_IDS } from "./types.js";
|
|
15
|
-
export { buildSddMcpEntry } from "./entries.js";
|
|
16
|
-
export { detectManagedDrift, upsertMethodManagedServers, } from "./merge.js";
|
|
17
|
-
import { buildSddMcpEntry } from "./entries.js";
|
|
18
|
-
/**
|
|
19
|
-
* Produce the full owned-servers map. Adding a new method-managed
|
|
20
|
-
* server id is a one-place change: add it here and to
|
|
21
|
-
* METHOD_MANAGED_SERVER_IDS.
|
|
22
|
-
*/
|
|
23
|
-
export function buildMethodManagedServers(opts = {}) {
|
|
24
|
-
return {
|
|
25
|
-
"sdd-mcp": buildSddMcpEntry(opts.sddRepoPath !== undefined ? { sddRepoPath: opts.sddRepoPath } : {}),
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
//# sourceMappingURL=index.js.map
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Merge logic for the method-managed portion of `.claude/settings.json`.
|
|
3
|
-
*
|
|
4
|
-
* Ownership model: the method owns specific server ids (listed in
|
|
5
|
-
* `METHOD_MANAGED_SERVER_IDS`). On upsert, those ids are rewritten
|
|
6
|
-
* with the method-supplied entry; any other server ids the adopter
|
|
7
|
-
* has added under `mcpServers` survive untouched, as do all other
|
|
8
|
-
* top-level keys (hooks, permissions, env, …).
|
|
9
|
-
*
|
|
10
|
-
* The merge is conservative: if a method id exists with a value that
|
|
11
|
-
* is byte-identical to the method's current seed, we return the
|
|
12
|
-
* original object unmodified (stable output → no spurious writes).
|
|
13
|
-
*/
|
|
14
|
-
export function upsertMethodManagedServers(existing, owned) {
|
|
15
|
-
const added = [];
|
|
16
|
-
const updated = [];
|
|
17
|
-
const unchanged = [];
|
|
18
|
-
const currentServers = isPlainObject(existing["mcpServers"])
|
|
19
|
-
? existing["mcpServers"]
|
|
20
|
-
: {};
|
|
21
|
-
const nextServers = { ...currentServers };
|
|
22
|
-
for (const [id, entry] of Object.entries(owned)) {
|
|
23
|
-
const prior = currentServers[id];
|
|
24
|
-
if (prior === undefined) {
|
|
25
|
-
nextServers[id] = entry;
|
|
26
|
-
added.push(id);
|
|
27
|
-
}
|
|
28
|
-
else if (!entriesEqual(prior, entry)) {
|
|
29
|
-
nextServers[id] = entry;
|
|
30
|
-
updated.push(id);
|
|
31
|
-
}
|
|
32
|
-
else {
|
|
33
|
-
unchanged.push(id);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
const changed = added.length > 0 || updated.length > 0;
|
|
37
|
-
if (!changed) {
|
|
38
|
-
return {
|
|
39
|
-
settings: existing,
|
|
40
|
-
added,
|
|
41
|
-
updated,
|
|
42
|
-
unchanged,
|
|
43
|
-
changed: false,
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
const nextSettings = {
|
|
47
|
-
...existing,
|
|
48
|
-
mcpServers: nextServers,
|
|
49
|
-
};
|
|
50
|
-
return {
|
|
51
|
-
settings: nextSettings,
|
|
52
|
-
added,
|
|
53
|
-
updated,
|
|
54
|
-
unchanged,
|
|
55
|
-
changed: true,
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
/**
|
|
59
|
-
* Detect adopter-edited method-managed entries. Returns the ids whose
|
|
60
|
-
* current value differs from the method-supplied seed — these are the
|
|
61
|
-
* ones that a refuse-or-overwrite sync should flag unless
|
|
62
|
-
* `--allow-overwrite` is passed.
|
|
63
|
-
*/
|
|
64
|
-
export function detectManagedDrift(existing, owned) {
|
|
65
|
-
const currentServers = isPlainObject(existing["mcpServers"])
|
|
66
|
-
? existing["mcpServers"]
|
|
67
|
-
: {};
|
|
68
|
-
const drifted = [];
|
|
69
|
-
for (const [id, entry] of Object.entries(owned)) {
|
|
70
|
-
const prior = currentServers[id];
|
|
71
|
-
if (prior !== undefined && !entriesEqual(prior, entry)) {
|
|
72
|
-
drifted.push(id);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
return drifted;
|
|
76
|
-
}
|
|
77
|
-
function entriesEqual(a, b) {
|
|
78
|
-
// Stable-order JSON comparison. The entry shape is small and flat
|
|
79
|
-
// enough that stringifying is cheaper than writing a structural
|
|
80
|
-
// walker.
|
|
81
|
-
return stableStringify(a) === stableStringify(b);
|
|
82
|
-
}
|
|
83
|
-
function stableStringify(value) {
|
|
84
|
-
if (value === null || typeof value !== "object")
|
|
85
|
-
return JSON.stringify(value);
|
|
86
|
-
if (Array.isArray(value)) {
|
|
87
|
-
return "[" + value.map((v) => stableStringify(v)).join(",") + "]";
|
|
88
|
-
}
|
|
89
|
-
const keys = Object.keys(value).sort();
|
|
90
|
-
return ("{" +
|
|
91
|
-
keys
|
|
92
|
-
.map((k) => JSON.stringify(k) + ":" + stableStringify(value[k]))
|
|
93
|
-
.join(",") +
|
|
94
|
-
"}");
|
|
95
|
-
}
|
|
96
|
-
function isPlainObject(v) {
|
|
97
|
-
return v !== null && typeof v === "object" && !Array.isArray(v);
|
|
98
|
-
}
|
|
99
|
-
//# sourceMappingURL=merge.js.map
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Types for the MCP settings-seed library. Mirrors the shape Claude
|
|
3
|
-
* Code expects at `.claude/settings.json`, restricted to the fields
|
|
4
|
-
* we read or write.
|
|
5
|
-
*/
|
|
6
|
-
/**
|
|
7
|
-
* Server ids owned by this method plan. Sync upserts these and leaves
|
|
8
|
-
* everything else alone. When Phases 3+4 ship adopter servers, extend
|
|
9
|
-
* the list — or better, let each plan contribute its own list and
|
|
10
|
-
* combine at merge time.
|
|
11
|
-
*/
|
|
12
|
-
export const METHOD_MANAGED_SERVER_IDS = ["sdd-mcp"];
|
|
13
|
-
//# sourceMappingURL=types.js.map
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `.github/hooks/*.json` merge pass during `sdd-cli sync` when the
|
|
3
|
-
* adopter opted into `--agent-harness github-copilot-cli`.
|
|
4
|
-
*
|
|
5
|
-
* Method-managed event entries are sentinel-tagged via their
|
|
6
|
-
* `command.bash` / `command.powershell` strings. Adopter-added events
|
|
7
|
-
* in the same `events[]` array survive sync. Drift inside a method-
|
|
8
|
-
* managed event refuses without `--allow-overwrite`, symmetric to the
|
|
9
|
-
* Claude Code settings-merge path.
|
|
10
|
-
*/
|
|
11
|
-
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
12
|
-
import { dirname, join } from "node:path";
|
|
13
|
-
import { buildMethodManagedCopilotHooks, detectCopilotHookDrift, upsertCopilotHookFile, } from "../hooks-seed/copilot.js";
|
|
14
|
-
export async function syncMethodManagedCopilotHooks(opts) {
|
|
15
|
-
const surface = buildMethodManagedCopilotHooks();
|
|
16
|
-
const entries = Object.entries(surface);
|
|
17
|
-
// First pass: detect drift across all files. If any drifted file
|
|
18
|
-
// would be refused (no --allow-overwrite), we refuse the whole
|
|
19
|
-
// merge without writing — same all-or-nothing posture as the
|
|
20
|
-
// Claude Code settings merge.
|
|
21
|
-
const reads = [];
|
|
22
|
-
for (const [portableClass, { filePath, events }] of entries) {
|
|
23
|
-
const absPath = join(opts.targetRepo, filePath);
|
|
24
|
-
const existing = await readHookFile(absPath);
|
|
25
|
-
const drifted = detectCopilotHookDrift(existing, events);
|
|
26
|
-
reads.push({
|
|
27
|
-
portableClass,
|
|
28
|
-
relPath: filePath,
|
|
29
|
-
absPath,
|
|
30
|
-
existing,
|
|
31
|
-
owned: events,
|
|
32
|
-
drifted,
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
const driftedFiles = reads.filter((r) => r.drifted).map((r) => r.relPath);
|
|
36
|
-
if (driftedFiles.length > 0 && !opts.allowOverwrite) {
|
|
37
|
-
return {
|
|
38
|
-
files: reads.map((r) => ({
|
|
39
|
-
relPath: r.relPath,
|
|
40
|
-
portableClass: r.portableClass,
|
|
41
|
-
drifted: r.drifted,
|
|
42
|
-
wrote: false,
|
|
43
|
-
})),
|
|
44
|
-
driftedRefused: driftedFiles,
|
|
45
|
-
refused: true,
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
if (opts.checkOnly) {
|
|
49
|
-
return {
|
|
50
|
-
files: reads.map((r) => ({
|
|
51
|
-
relPath: r.relPath,
|
|
52
|
-
portableClass: r.portableClass,
|
|
53
|
-
drifted: r.drifted,
|
|
54
|
-
wrote: false,
|
|
55
|
-
})),
|
|
56
|
-
driftedRefused: [],
|
|
57
|
-
refused: false,
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
// Second pass: upsert + write.
|
|
61
|
-
const fileResults = [];
|
|
62
|
-
for (const r of reads) {
|
|
63
|
-
const merge = upsertCopilotHookFile(r.existing, r.owned);
|
|
64
|
-
let wrote = false;
|
|
65
|
-
if (merge.changed || r.existing === null) {
|
|
66
|
-
await mkdir(dirname(r.absPath), { recursive: true });
|
|
67
|
-
await writeFile(r.absPath, JSON.stringify(merge.file, null, 2) + "\n", "utf8");
|
|
68
|
-
wrote = true;
|
|
69
|
-
}
|
|
70
|
-
fileResults.push({
|
|
71
|
-
relPath: r.relPath,
|
|
72
|
-
portableClass: r.portableClass,
|
|
73
|
-
drifted: r.drifted,
|
|
74
|
-
wrote,
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
return { files: fileResults, driftedRefused: [], refused: false };
|
|
78
|
-
}
|
|
79
|
-
async function readHookFile(absPath) {
|
|
80
|
-
if (!(await fileExists(absPath)))
|
|
81
|
-
return null;
|
|
82
|
-
const raw = await readFile(absPath, "utf8");
|
|
83
|
-
if (raw.trim().length === 0)
|
|
84
|
-
return null;
|
|
85
|
-
try {
|
|
86
|
-
const parsed = JSON.parse(raw);
|
|
87
|
-
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
88
|
-
const obj = parsed;
|
|
89
|
-
if (Array.isArray(obj["events"])) {
|
|
90
|
-
return obj;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
catch {
|
|
95
|
-
/* fall through — treat malformed as empty so the next write reseats it */
|
|
96
|
-
}
|
|
97
|
-
return null;
|
|
98
|
-
}
|
|
99
|
-
async function fileExists(path) {
|
|
100
|
-
try {
|
|
101
|
-
await access(path);
|
|
102
|
-
return true;
|
|
103
|
-
}
|
|
104
|
-
catch {
|
|
105
|
-
return false;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
//# sourceMappingURL=copilot-hooks-merge.js.map
|