@nerviq/cli 0.0.1 → 0.9.0-beta.1
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/CHANGELOG.md +181 -0
- package/LICENSE +21 -0
- package/README.md +447 -0
- package/bin/cli.js +749 -0
- package/content/case-study-template.md +91 -0
- package/content/claims-governance.md +37 -0
- package/content/claude-code/audit-repo/SKILL.md +20 -0
- package/content/claude-native-integration.md +60 -0
- package/content/devto-article.json +9 -0
- package/content/launch-posts.md +226 -0
- package/content/pilot-rollout-kit.md +30 -0
- package/content/release-checklist.md +31 -0
- package/package.json +53 -4
- package/src/activity.js +529 -0
- package/src/aider/activity.js +226 -0
- package/src/aider/config-parser.js +166 -0
- package/src/aider/context.js +158 -0
- package/src/aider/deep-review.js +316 -0
- package/src/aider/domain-packs.js +278 -0
- package/src/aider/freshness.js +168 -0
- package/src/aider/governance.js +253 -0
- package/src/aider/interactive.js +334 -0
- package/src/aider/mcp-packs.js +98 -0
- package/src/aider/patch.js +214 -0
- package/src/aider/plans.js +186 -0
- package/src/aider/premium.js +360 -0
- package/src/aider/setup.js +404 -0
- package/src/aider/techniques.js +1323 -0
- package/src/analyze.js +821 -0
- package/src/audit.js +1003 -0
- package/src/badge.js +13 -0
- package/src/benchmark.js +339 -0
- package/src/claudex-sync.json +7 -0
- package/src/codex/activity.js +324 -0
- package/src/codex/config-parser.js +183 -0
- package/src/codex/context.js +221 -0
- package/src/codex/deep-review.js +493 -0
- package/src/codex/domain-packs.js +372 -0
- package/src/codex/freshness.js +167 -0
- package/src/codex/governance.js +192 -0
- package/src/codex/interactive.js +618 -0
- package/src/codex/mcp-packs.js +660 -0
- package/src/codex/patch.js +209 -0
- package/src/codex/plans.js +251 -0
- package/src/codex/premium.js +614 -0
- package/src/codex/setup.js +603 -0
- package/src/codex/techniques.js +2649 -0
- package/src/context.js +272 -0
- package/src/copilot/activity.js +309 -0
- package/src/copilot/config-parser.js +226 -0
- package/src/copilot/context.js +197 -0
- package/src/copilot/deep-review.js +346 -0
- package/src/copilot/domain-packs.js +350 -0
- package/src/copilot/freshness.js +197 -0
- package/src/copilot/governance.js +222 -0
- package/src/copilot/interactive.js +406 -0
- package/src/copilot/mcp-packs.js +572 -0
- package/src/copilot/patch.js +238 -0
- package/src/copilot/plans.js +253 -0
- package/src/copilot/premium.js +450 -0
- package/src/copilot/setup.js +488 -0
- package/src/copilot/techniques.js +1822 -0
- package/src/cursor/activity.js +301 -0
- package/src/cursor/config-parser.js +265 -0
- package/src/cursor/context.js +236 -0
- package/src/cursor/deep-review.js +334 -0
- package/src/cursor/domain-packs.js +346 -0
- package/src/cursor/freshness.js +214 -0
- package/src/cursor/governance.js +229 -0
- package/src/cursor/interactive.js +391 -0
- package/src/cursor/mcp-packs.js +571 -0
- package/src/cursor/patch.js +243 -0
- package/src/cursor/plans.js +254 -0
- package/src/cursor/premium.js +468 -0
- package/src/cursor/setup.js +488 -0
- package/src/cursor/techniques.js +1786 -0
- package/src/deep-review.js +345 -0
- package/src/domain-packs.js +364 -0
- package/src/formatters/sarif.js +115 -0
- package/src/gemini/activity.js +402 -0
- package/src/gemini/config-parser.js +275 -0
- package/src/gemini/context.js +221 -0
- package/src/gemini/deep-review.js +559 -0
- package/src/gemini/domain-packs.js +371 -0
- package/src/gemini/freshness.js +204 -0
- package/src/gemini/governance.js +201 -0
- package/src/gemini/interactive.js +860 -0
- package/src/gemini/mcp-packs.js +658 -0
- package/src/gemini/patch.js +229 -0
- package/src/gemini/plans.js +269 -0
- package/src/gemini/premium.js +759 -0
- package/src/gemini/setup.js +692 -0
- package/src/gemini/techniques.js +2084 -0
- package/src/governance.js +523 -0
- package/src/harmony/advisor.js +383 -0
- package/src/harmony/audit.js +303 -0
- package/src/harmony/canon.js +444 -0
- package/src/harmony/cli.js +331 -0
- package/src/harmony/drift.js +401 -0
- package/src/harmony/governance.js +313 -0
- package/src/harmony/memory.js +238 -0
- package/src/harmony/sync.js +458 -0
- package/src/harmony/watch.js +336 -0
- package/src/index.js +256 -0
- package/src/insights.js +119 -0
- package/src/interactive.js +118 -0
- package/src/mcp-packs.js +597 -0
- package/src/opencode/activity.js +286 -0
- package/src/opencode/config-parser.js +109 -0
- package/src/opencode/context.js +247 -0
- package/src/opencode/deep-review.js +313 -0
- package/src/opencode/domain-packs.js +240 -0
- package/src/opencode/freshness.js +158 -0
- package/src/opencode/governance.js +159 -0
- package/src/opencode/interactive.js +392 -0
- package/src/opencode/mcp-packs.js +474 -0
- package/src/opencode/patch.js +184 -0
- package/src/opencode/plans.js +231 -0
- package/src/opencode/premium.js +413 -0
- package/src/opencode/setup.js +449 -0
- package/src/opencode/techniques.js +1713 -0
- package/src/plans.js +655 -0
- package/src/secret-patterns.js +30 -0
- package/src/setup.js +1274 -0
- package/src/synergy/adaptive.js +261 -0
- package/src/synergy/compensation.js +156 -0
- package/src/synergy/evidence.js +193 -0
- package/src/synergy/learning.js +184 -0
- package/src/synergy/patterns.js +227 -0
- package/src/synergy/ranking.js +83 -0
- package/src/synergy/report.js +163 -0
- package/src/synergy/routing.js +152 -0
- package/src/techniques.js +1354 -0
- package/src/watch.js +229 -0
- package/src/windsurf/activity.js +302 -0
- package/src/windsurf/config-parser.js +267 -0
- package/src/windsurf/context.js +249 -0
- package/src/windsurf/deep-review.js +337 -0
- package/src/windsurf/domain-packs.js +348 -0
- package/src/windsurf/freshness.js +215 -0
- package/src/windsurf/governance.js +231 -0
- package/src/windsurf/interactive.js +388 -0
- package/src/windsurf/mcp-packs.js +535 -0
- package/src/windsurf/patch.js +231 -0
- package/src/windsurf/plans.js +247 -0
- package/src/windsurf/premium.js +467 -0
- package/src/windsurf/setup.js +471 -0
- package/src/windsurf/techniques.js +1758 -0
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Harmony Sync — Unified Setup / Sync Engine
|
|
3
|
+
*
|
|
4
|
+
* Generates aligned configs for ALL active platforms from a shared canonical
|
|
5
|
+
* understanding. Ensures instructions, MCP servers, and trust posture are
|
|
6
|
+
* consistent across Claude, Codex, Gemini, Copilot, and Cursor.
|
|
7
|
+
*
|
|
8
|
+
* Uses managed blocks from each platform's patch module so hand-authored
|
|
9
|
+
* content is always preserved.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const { buildCanonicalModel, PLATFORM_SIGNATURES } = require('./canon');
|
|
15
|
+
|
|
16
|
+
// ─── Managed block markers (imported from platform patch modules) ───────────
|
|
17
|
+
|
|
18
|
+
const MANAGED_MARKERS = {
|
|
19
|
+
claude: {
|
|
20
|
+
start: '<!-- nerviq:managed:start -->',
|
|
21
|
+
end: '<!-- nerviq:managed:end -->',
|
|
22
|
+
},
|
|
23
|
+
codex: {
|
|
24
|
+
start: '<!-- nerviq:managed:start -->',
|
|
25
|
+
end: '<!-- nerviq:managed:end -->',
|
|
26
|
+
},
|
|
27
|
+
gemini: {
|
|
28
|
+
start: '<!-- nerviq:managed:start -->',
|
|
29
|
+
end: '<!-- nerviq:managed:end -->',
|
|
30
|
+
},
|
|
31
|
+
copilot: {
|
|
32
|
+
start: '<!-- nerviq:managed:start -->',
|
|
33
|
+
end: '<!-- nerviq:managed:end -->',
|
|
34
|
+
},
|
|
35
|
+
cursor: {
|
|
36
|
+
// Cursor uses MDC format but managed blocks are still HTML-comment-based for .mdc
|
|
37
|
+
start: '<!-- nerviq:managed:start -->',
|
|
38
|
+
end: '<!-- nerviq:managed:end -->',
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
function safeReadFile(filePath) {
|
|
45
|
+
try {
|
|
46
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
47
|
+
} catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function ensureDir(dirPath) {
|
|
53
|
+
try {
|
|
54
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
55
|
+
} catch {
|
|
56
|
+
// Already exists or not writable
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Extract managed block from content.
|
|
62
|
+
*/
|
|
63
|
+
function extractManagedBlock(content, startMarker, endMarker) {
|
|
64
|
+
const startIdx = content.indexOf(startMarker);
|
|
65
|
+
const endIdx = content.indexOf(endMarker);
|
|
66
|
+
|
|
67
|
+
if (startIdx === -1 || endIdx === -1 || endIdx <= startIdx) {
|
|
68
|
+
return { before: content, managed: null, after: '' };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
before: content.substring(0, startIdx),
|
|
73
|
+
managed: content.substring(startIdx + startMarker.length, endIdx).trim(),
|
|
74
|
+
after: content.substring(endIdx + endMarker.length),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Upsert a managed block within content.
|
|
80
|
+
*/
|
|
81
|
+
function upsertManagedBlock(content, newManaged, startMarker, endMarker) {
|
|
82
|
+
const { before, managed, after } = extractManagedBlock(content, startMarker, endMarker);
|
|
83
|
+
|
|
84
|
+
if (managed !== null) {
|
|
85
|
+
return `${before}${startMarker}\n${newManaged}\n${endMarker}${after}`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const separator = content.endsWith('\n') ? '\n' : '\n\n';
|
|
89
|
+
return `${content}${separator}${startMarker}\n${newManaged}\n${endMarker}\n`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ─── Instruction content builders ───────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Build a shared instruction block that should appear on every platform.
|
|
96
|
+
* Derived from the canonical model's shared understanding.
|
|
97
|
+
*/
|
|
98
|
+
function buildSharedInstructionBlock(model) {
|
|
99
|
+
const lines = [];
|
|
100
|
+
|
|
101
|
+
lines.push(`## Harmony-Managed Instructions`);
|
|
102
|
+
lines.push(`<!-- Synced by nerviq harmony. Do not edit this block manually. -->`);
|
|
103
|
+
lines.push('');
|
|
104
|
+
|
|
105
|
+
// Project identity
|
|
106
|
+
lines.push(`Project: ${model.projectName}`);
|
|
107
|
+
if (model.stacks.length > 0) {
|
|
108
|
+
lines.push(`Stacks: ${model.stacks.join(', ')}`);
|
|
109
|
+
}
|
|
110
|
+
lines.push('');
|
|
111
|
+
|
|
112
|
+
// Shared instructions (if any were found across platforms)
|
|
113
|
+
if (model.sharedInstructions.length > 0) {
|
|
114
|
+
lines.push('### Shared Guidelines');
|
|
115
|
+
for (const instruction of model.sharedInstructions.slice(0, 20)) {
|
|
116
|
+
lines.push(instruction);
|
|
117
|
+
}
|
|
118
|
+
lines.push('');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// MCP servers (unified list)
|
|
122
|
+
const mcpNames = Object.keys(model.mcpServers);
|
|
123
|
+
if (mcpNames.length > 0) {
|
|
124
|
+
lines.push('### Available MCP Servers');
|
|
125
|
+
for (const name of mcpNames) {
|
|
126
|
+
const server = model.mcpServers[name];
|
|
127
|
+
lines.push(`- ${name} (on: ${server.platforms.join(', ')})`);
|
|
128
|
+
}
|
|
129
|
+
lines.push('');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return lines.join('\n');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ─── Platform-specific file generators ──────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Generate the instruction file path for a platform.
|
|
139
|
+
*/
|
|
140
|
+
function getInstructionPath(platform) {
|
|
141
|
+
switch (platform) {
|
|
142
|
+
case 'claude': return 'CLAUDE.md';
|
|
143
|
+
case 'codex': return 'AGENTS.md';
|
|
144
|
+
case 'gemini': return 'GEMINI.md';
|
|
145
|
+
case 'copilot': return '.github/copilot-instructions.md';
|
|
146
|
+
case 'cursor': return '.cursorrules';
|
|
147
|
+
default: return null;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Generate MCP config content for a platform based on the unified MCP server list.
|
|
153
|
+
*/
|
|
154
|
+
function buildMcpConfig(platform, mcpServers) {
|
|
155
|
+
// Filter to servers that should be on this platform
|
|
156
|
+
const servers = {};
|
|
157
|
+
for (const [name, server] of Object.entries(mcpServers)) {
|
|
158
|
+
servers[name] = {
|
|
159
|
+
command: server.command || 'npx',
|
|
160
|
+
args: server.args || [],
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (Object.keys(servers).length === 0) return null;
|
|
165
|
+
|
|
166
|
+
if (platform === 'claude') {
|
|
167
|
+
// Claude settings.json format
|
|
168
|
+
return { mcpServers: servers };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (platform === 'cursor' || platform === 'copilot') {
|
|
172
|
+
// mcp.json format
|
|
173
|
+
return { mcpServers: servers };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (platform === 'gemini') {
|
|
177
|
+
// Gemini settings.json format
|
|
178
|
+
return { mcpServers: servers };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Get the MCP config file path for a platform.
|
|
186
|
+
*/
|
|
187
|
+
function getMcpConfigPath(platform) {
|
|
188
|
+
switch (platform) {
|
|
189
|
+
case 'claude': return '.claude/settings.json';
|
|
190
|
+
case 'gemini': return '.gemini/settings.json';
|
|
191
|
+
case 'copilot': return '.vscode/mcp.json';
|
|
192
|
+
case 'cursor': return '.cursor/mcp.json';
|
|
193
|
+
default: return null;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ─── Trust alignment recommendations ────────────────────────────────────────
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Recommend a unified trust posture across all platforms.
|
|
201
|
+
* Strategy: use the most restrictive trust level present.
|
|
202
|
+
*/
|
|
203
|
+
function recommendTrustPosture(model) {
|
|
204
|
+
const TRUST_LEVELS = {
|
|
205
|
+
'locked-down': 0,
|
|
206
|
+
'default': 1,
|
|
207
|
+
'unknown': 1,
|
|
208
|
+
'safe-write': 2,
|
|
209
|
+
'standard': 2,
|
|
210
|
+
'no-sandbox': 3,
|
|
211
|
+
'full-auto': 4,
|
|
212
|
+
'bypass': 4,
|
|
213
|
+
'unrestricted': 5,
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const postures = Object.values(model.trustPosture);
|
|
217
|
+
if (postures.length === 0) return 'safe-write';
|
|
218
|
+
|
|
219
|
+
// Find the most restrictive (lowest) trust level
|
|
220
|
+
let minLevel = Infinity;
|
|
221
|
+
let minPosture = 'default';
|
|
222
|
+
for (const posture of postures) {
|
|
223
|
+
const level = TRUST_LEVELS[posture] ?? 1;
|
|
224
|
+
if (level < minLevel) {
|
|
225
|
+
minLevel = level;
|
|
226
|
+
minPosture = posture;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return minPosture;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ─── Main sync functions ────────────────────────────────────────────────────
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Generate harmony sync operations from a canonical model.
|
|
237
|
+
*
|
|
238
|
+
* For each active platform:
|
|
239
|
+
* - Generate/update instruction file with shared managed block
|
|
240
|
+
* - Ensure shared MCP servers exist on all platforms that support MCP
|
|
241
|
+
* - Report trust posture alignment recommendations
|
|
242
|
+
*
|
|
243
|
+
* @param {object} canonicalModel - Output of buildCanonicalModel()
|
|
244
|
+
* @param {object} [options] - { syncMcp: true, syncInstructions: true, dryRun: false }
|
|
245
|
+
* @returns {object} { files, summary, warnings }
|
|
246
|
+
*/
|
|
247
|
+
function generateHarmonySync(canonicalModel, options = {}) {
|
|
248
|
+
const {
|
|
249
|
+
syncMcp = true,
|
|
250
|
+
syncInstructions = true,
|
|
251
|
+
} = options;
|
|
252
|
+
|
|
253
|
+
const model = canonicalModel;
|
|
254
|
+
const files = [];
|
|
255
|
+
const warnings = [];
|
|
256
|
+
|
|
257
|
+
const sharedBlock = buildSharedInstructionBlock(model);
|
|
258
|
+
const recommendedTrust = recommendTrustPosture(model);
|
|
259
|
+
|
|
260
|
+
for (const ap of model.activePlatforms) {
|
|
261
|
+
const platform = ap.platform;
|
|
262
|
+
|
|
263
|
+
// ── Instruction file sync ──
|
|
264
|
+
if (syncInstructions) {
|
|
265
|
+
const instrPath = getInstructionPath(platform);
|
|
266
|
+
if (instrPath) {
|
|
267
|
+
const fullPath = path.join(model.dir, instrPath);
|
|
268
|
+
const existingContent = safeReadFile(fullPath);
|
|
269
|
+
const markers = MANAGED_MARKERS[platform];
|
|
270
|
+
|
|
271
|
+
if (existingContent) {
|
|
272
|
+
// Patch existing file with managed block
|
|
273
|
+
const updated = upsertManagedBlock(
|
|
274
|
+
existingContent,
|
|
275
|
+
sharedBlock,
|
|
276
|
+
markers.start,
|
|
277
|
+
markers.end,
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
if (updated !== existingContent) {
|
|
281
|
+
files.push({
|
|
282
|
+
platform,
|
|
283
|
+
path: instrPath,
|
|
284
|
+
action: 'patch',
|
|
285
|
+
content: updated,
|
|
286
|
+
preview: `Update managed block in ${instrPath}`,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
} else {
|
|
290
|
+
// Create new instruction file with managed block
|
|
291
|
+
const newContent = `# ${platform === 'codex' ? 'AGENTS' : platform.charAt(0).toUpperCase() + platform.slice(1)} Instructions\n\n` +
|
|
292
|
+
`${markers.start}\n${sharedBlock}\n${markers.end}\n`;
|
|
293
|
+
|
|
294
|
+
files.push({
|
|
295
|
+
platform,
|
|
296
|
+
path: instrPath,
|
|
297
|
+
action: 'create',
|
|
298
|
+
content: newContent,
|
|
299
|
+
preview: `Create ${instrPath} with harmony-managed content`,
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// ── MCP server sync ──
|
|
306
|
+
if (syncMcp && Object.keys(model.mcpServers).length > 0) {
|
|
307
|
+
const mcpPath = getMcpConfigPath(platform);
|
|
308
|
+
if (!mcpPath) continue; // Codex doesn't support MCP config
|
|
309
|
+
|
|
310
|
+
const fullMcpPath = path.join(model.dir, mcpPath);
|
|
311
|
+
const existingMcp = safeReadFile(fullMcpPath);
|
|
312
|
+
const mcpConfig = buildMcpConfig(platform, model.mcpServers);
|
|
313
|
+
|
|
314
|
+
if (!mcpConfig) continue;
|
|
315
|
+
|
|
316
|
+
if (existingMcp) {
|
|
317
|
+
// Merge: add missing servers, don't overwrite existing
|
|
318
|
+
let existingJson;
|
|
319
|
+
try {
|
|
320
|
+
existingJson = JSON.parse(existingMcp);
|
|
321
|
+
} catch {
|
|
322
|
+
warnings.push(`Cannot parse ${mcpPath} — skipping MCP sync for ${platform}`);
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const existingServers = existingJson.mcpServers || existingJson.servers || {};
|
|
327
|
+
const newServers = mcpConfig.mcpServers || {};
|
|
328
|
+
let added = 0;
|
|
329
|
+
|
|
330
|
+
for (const [name, config] of Object.entries(newServers)) {
|
|
331
|
+
if (!existingServers[name]) {
|
|
332
|
+
existingServers[name] = config;
|
|
333
|
+
added++;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (added > 0) {
|
|
338
|
+
// Preserve the original key name (mcpServers or servers)
|
|
339
|
+
const serverKey = existingJson.servers ? 'servers' : 'mcpServers';
|
|
340
|
+
existingJson[serverKey] = existingServers;
|
|
341
|
+
|
|
342
|
+
files.push({
|
|
343
|
+
platform,
|
|
344
|
+
path: mcpPath,
|
|
345
|
+
action: 'patch',
|
|
346
|
+
content: JSON.stringify(existingJson, null, 2) + '\n',
|
|
347
|
+
preview: `Add ${added} MCP server(s) to ${mcpPath}`,
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
} else {
|
|
351
|
+
// Create new MCP config
|
|
352
|
+
files.push({
|
|
353
|
+
platform,
|
|
354
|
+
path: mcpPath,
|
|
355
|
+
action: 'create',
|
|
356
|
+
content: JSON.stringify(mcpConfig, null, 2) + '\n',
|
|
357
|
+
preview: `Create ${mcpPath} with ${Object.keys(mcpConfig.mcpServers || {}).length} MCP server(s)`,
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Trust posture warnings
|
|
364
|
+
const trustValues = Object.values(model.trustPosture);
|
|
365
|
+
const uniqueTrust = new Set(trustValues);
|
|
366
|
+
if (uniqueTrust.size > 1) {
|
|
367
|
+
warnings.push(
|
|
368
|
+
`Trust posture varies across platforms. Recommended baseline: "${recommendedTrust}". ` +
|
|
369
|
+
`Current: ${Object.entries(model.trustPosture).map(([p, t]) => `${p}=${t}`).join(', ')}. ` +
|
|
370
|
+
`Trust alignment must be applied manually per platform.`
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Summary
|
|
375
|
+
const creates = files.filter(f => f.action === 'create').length;
|
|
376
|
+
const patches = files.filter(f => f.action === 'patch').length;
|
|
377
|
+
const summary = {
|
|
378
|
+
totalFiles: files.length,
|
|
379
|
+
creates,
|
|
380
|
+
patches,
|
|
381
|
+
platforms: [...new Set(files.map(f => f.platform))],
|
|
382
|
+
recommendedTrust,
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
return { files, summary, warnings };
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Preview sync operations without writing.
|
|
390
|
+
*
|
|
391
|
+
* @param {string} dir - Project root directory
|
|
392
|
+
* @param {object} [options] - Same as generateHarmonySync options
|
|
393
|
+
* @returns {object} Sync plan (files, summary, warnings)
|
|
394
|
+
*/
|
|
395
|
+
function previewHarmonySync(dir, options = {}) {
|
|
396
|
+
const model = buildCanonicalModel(dir);
|
|
397
|
+
return generateHarmonySync(model, options);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Apply harmony sync — write all generated files to disk.
|
|
402
|
+
*
|
|
403
|
+
* @param {string} dir - Project root directory
|
|
404
|
+
* @param {object} [options] - { syncMcp, syncInstructions, dryRun }
|
|
405
|
+
* @returns {object} { applied, skipped, warnings }
|
|
406
|
+
*/
|
|
407
|
+
function applyHarmonySync(dir, options = {}) {
|
|
408
|
+
const { dryRun = false } = options;
|
|
409
|
+
const model = buildCanonicalModel(dir);
|
|
410
|
+
const sync = generateHarmonySync(model, options);
|
|
411
|
+
|
|
412
|
+
if (dryRun) {
|
|
413
|
+
return {
|
|
414
|
+
applied: [],
|
|
415
|
+
skipped: sync.files.map(f => f.path),
|
|
416
|
+
warnings: sync.warnings,
|
|
417
|
+
plan: sync,
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const applied = [];
|
|
422
|
+
const skipped = [];
|
|
423
|
+
|
|
424
|
+
for (const file of sync.files) {
|
|
425
|
+
const fullPath = path.join(dir, file.path);
|
|
426
|
+
|
|
427
|
+
try {
|
|
428
|
+
// Ensure parent directory exists
|
|
429
|
+
ensureDir(path.dirname(fullPath));
|
|
430
|
+
|
|
431
|
+
fs.writeFileSync(fullPath, file.content, 'utf8');
|
|
432
|
+
applied.push({
|
|
433
|
+
platform: file.platform,
|
|
434
|
+
path: file.path,
|
|
435
|
+
action: file.action,
|
|
436
|
+
});
|
|
437
|
+
} catch (err) {
|
|
438
|
+
skipped.push({
|
|
439
|
+
platform: file.platform,
|
|
440
|
+
path: file.path,
|
|
441
|
+
reason: err.message,
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
return {
|
|
447
|
+
applied,
|
|
448
|
+
skipped,
|
|
449
|
+
warnings: sync.warnings,
|
|
450
|
+
summary: sync.summary,
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
module.exports = {
|
|
455
|
+
generateHarmonySync,
|
|
456
|
+
applyHarmonySync,
|
|
457
|
+
previewHarmonySync,
|
|
458
|
+
};
|