@sdd-method/maintain-cli 0.1.0-rc.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/ci-binding/index.js +18 -7
- 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/init/ci-workflow.js +15 -6
- package/dist/lib/lock-file.js +5 -0
- package/dist/lib/sync/briefing-merge.js +50 -34
- package/dist/lib/sync/diff.js +1 -1
- package/dist/lib/sync/index.js +31 -72
- 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/verbs/sync.js +13 -1
- package/dist/verify-program.js +1 -1
- package/dist/verify-version.json +2 -2
- package/package.json +3 -3
- 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
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { checkCataloguesFresh } from "../lib/catalogue/freshness.js";
|
|
3
|
+
/**
|
|
4
|
+
* `catalogue check` (M-ADR 0220 §4.2, ruling 3): whether the committed catalogues are what the
|
|
5
|
+
* committed specifications build to. It builds into temporary space and writes nothing to the
|
|
6
|
+
* repository: `catalogue build --check` under its own name, so the author's surface holds no verb
|
|
7
|
+
* that writes the model. Its own registrar, so an artefact can carry it without the build,
|
|
8
|
+
* aggregate and graph registrations in catalogue-maintain.ts.
|
|
9
|
+
*/
|
|
10
|
+
export function registerCatalogueCheck(catalogue) {
|
|
11
|
+
catalogue
|
|
12
|
+
.command("check")
|
|
13
|
+
.description("Report whether the committed catalogues/ are what the committed specifications build to; builds in a temporary directory and writes nothing (exit 1 lists every stale file)")
|
|
14
|
+
.option("--sdd-path <path>", "Path to the SDD repo root (defaults to cwd)")
|
|
15
|
+
.option("--strict", "Treat build findings as failures")
|
|
16
|
+
.option("--snapshot-store <dir>", "Snapshot store root the binding's snapshot_area is relative to (default: $SDD_SNAPSHOT_STORE; M-ADR 0214)")
|
|
17
|
+
.action(async (opts) => {
|
|
18
|
+
const snapshotStore = opts.snapshotStore ?? process.env.SDD_SNAPSHOT_STORE;
|
|
19
|
+
const r = await checkCataloguesFresh(resolve(opts.sddPath ?? process.cwd()), {
|
|
20
|
+
label: "catalogue check",
|
|
21
|
+
...(opts.strict !== undefined ? { strict: opts.strict } : {}),
|
|
22
|
+
...(snapshotStore !== undefined ? { snapshotStore } : {}),
|
|
23
|
+
}, (s) => process.stdout.write(s));
|
|
24
|
+
if (r.exitCode !== 0)
|
|
25
|
+
process.exit(r.exitCode);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=catalogue-check.js.map
|
package/dist/verbs/context.js
CHANGED
|
@@ -8,7 +8,7 @@ export function registerContext(program) {
|
|
|
8
8
|
.description("Briefing for a code repository an SDD specifies (M-ADR 0128 §1, M-ADR 0150 §4.4)");
|
|
9
9
|
context
|
|
10
10
|
.command("pull")
|
|
11
|
-
.description("Write agent-instructions.md
|
|
11
|
+
.description("Write agent-instructions.md and the harness wrapper's method-managed block into this code repository, rendered from the templates its upstream SDD carries. The SDD is found by looking this repository up in the catalogues/repositories.csv of the SDDs beside it. Writes nothing else.")
|
|
12
12
|
.option("--target-repo <dir>", "the code repository to write into", resolvePathOption, process.cwd())
|
|
13
13
|
.option("--sdd <dir>", "the upstream SDD, skipping discovery", resolvePathOption)
|
|
14
14
|
.option("--repo <id>", "this repository's id in the SDD's catalogue (default: its git top-level directory name)")
|
package/dist/verbs/sync.js
CHANGED
|
@@ -2,7 +2,7 @@ import { AGENT_HARNESSES, resolveAgentHarness, } from "../lib/agent-harness.js";
|
|
|
2
2
|
import { loadSyncConfig } from "../lib/sync/config.js";
|
|
3
3
|
import { materialiseBundle } from "../lib/sync/bundle-source.js";
|
|
4
4
|
import { runSync } from "../lib/sync/index.js";
|
|
5
|
-
import { CI_BINDINGS, isCiBinding } from "../lib/ci-binding/index.js";
|
|
5
|
+
import { CI_BINDINGS, CI_RUNNER_DEFAULT, isCiBinding, parseCiRunner } from "../lib/ci-binding/index.js";
|
|
6
6
|
import { resolvePathOption } from "./path-option.js";
|
|
7
7
|
export function registerSync(program) {
|
|
8
8
|
program
|
|
@@ -18,12 +18,22 @@ export function registerSync(program) {
|
|
|
18
18
|
.option("--no-delete", "preserve files removed upstream")
|
|
19
19
|
.option("--agent-harness <name>", `target agent harness for hooks + briefing. Default: claude-code. Supported: ${AGENT_HARNESSES.join(", ")}`)
|
|
20
20
|
.option("--ci-binding <host>", `CI host whose wrapper calls the Method Validation module (M-ADR 0219): ${CI_BINDINGS.join(", ")}. Default: the binding the lock records, else github; a new choice is recorded in the lock`)
|
|
21
|
+
.option("--ci-runner <labels>", `runner label(s) the CI wrapper targets, comma-separated (GitHub runs-on, GitLab tags, Jenkins agent label), e.g. a self-hosted runner. '${CI_RUNNER_DEFAULT}' returns to the host's default runner. Default: what the lock records; a new choice is recorded`)
|
|
21
22
|
.action(async (opts) => {
|
|
22
23
|
if (opts.ciBinding !== undefined && !isCiBinding(opts.ciBinding)) {
|
|
23
24
|
process.stderr.write(`[FAIL] --ci-binding must be one of ${CI_BINDINGS.join(", ")}; got '${opts.ciBinding}'\n`);
|
|
24
25
|
process.exit(1);
|
|
25
26
|
}
|
|
26
27
|
const ciBinding = opts.ciBinding;
|
|
28
|
+
let ciRunner;
|
|
29
|
+
if (opts.ciRunner !== undefined) {
|
|
30
|
+
const parsed = opts.ciRunner === CI_RUNNER_DEFAULT ? [] : parseCiRunner(opts.ciRunner);
|
|
31
|
+
if (parsed === null) {
|
|
32
|
+
process.stderr.write(`[FAIL] --ci-runner takes comma-separated labels of letters, digits, '.', '_' or '-', or '${CI_RUNNER_DEFAULT}'; got '${opts.ciRunner}'\n`);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
ciRunner = parsed;
|
|
36
|
+
}
|
|
27
37
|
let agentHarness;
|
|
28
38
|
try {
|
|
29
39
|
agentHarness = resolveAgentHarness(opts.agentHarness);
|
|
@@ -50,6 +60,7 @@ export function registerSync(program) {
|
|
|
50
60
|
delete: opts.delete,
|
|
51
61
|
agentHarness,
|
|
52
62
|
...(ciBinding ? { ciBinding } : {}),
|
|
63
|
+
...(ciRunner ? { ciRunner } : {}),
|
|
53
64
|
...(opts.replaceBundle ? { replaceBundle: opts.replaceBundle } : {}),
|
|
54
65
|
...(opts.yes === true ? { yes: true } : {}),
|
|
55
66
|
});
|
|
@@ -78,6 +89,7 @@ export function registerSync(program) {
|
|
|
78
89
|
delete: opts.delete,
|
|
79
90
|
agentHarness,
|
|
80
91
|
...(ciBinding ? { ciBinding } : {}),
|
|
92
|
+
...(ciRunner ? { ciRunner } : {}),
|
|
81
93
|
});
|
|
82
94
|
if (exitCode !== 0) {
|
|
83
95
|
await resolved.cleanup();
|
package/dist/verify-program.js
CHANGED
|
@@ -91,7 +91,7 @@ export function buildVerifyProgram() {
|
|
|
91
91
|
.name("sdd-verify")
|
|
92
92
|
.description("Verify an SDD repository without changing it — validate, list, and read-only catalogue and pack checks. Cannot write to the repository it is pointed at.")
|
|
93
93
|
.version(getPackageVersion(), "-v, --version")
|
|
94
|
-
.addHelpText("after", `\nSpec: ${SPEC_URL}\nAuthoring and maintenance verbs (sync, catalogue build, init, scaffold,
|
|
94
|
+
.addHelpText("after", `\nSpec: ${SPEC_URL}\nAuthoring and maintenance verbs (sync, catalogue build, init, scaffold, …) are in @sdd-method/sdd-cli.`)
|
|
95
95
|
.exitOverride()
|
|
96
96
|
.configureOutput({
|
|
97
97
|
writeOut: (str) => process.stdout.write(str),
|
package/dist/verify-version.json
CHANGED
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sdd-method/maintain-cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"sddCliVersion": "1.
|
|
5
|
-
"description": "Bring an SDD repository current and rebuild its derived model
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"sddCliVersion": "1.43.0",
|
|
5
|
+
"description": "Bring an SDD repository current and rebuild its derived model — sync, briefing, catalogue build and aggregate, pack build, context pull — plus every read-only verify verb. Ships only the code those verbs reach; it cannot author specification content. Authoring is in @sdd-method/sdd-cli.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"bin": {
|
|
@@ -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
|