@nerviq/cli 1.20.1 → 1.21.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.
Files changed (181) hide show
  1. package/LICENSE +23 -23
  2. package/README.md +2 -2
  3. package/package.json +1 -1
  4. package/src/activity.js +1039 -1039
  5. package/src/adoption-advisor.js +299 -299
  6. package/src/aider/config-parser.js +166 -166
  7. package/src/aider/context.js +4 -1
  8. package/src/aider/deep-review.js +316 -316
  9. package/src/aider/domain-packs.js +303 -303
  10. package/src/aider/freshness.js +93 -93
  11. package/src/aider/governance.js +253 -253
  12. package/src/aider/interactive.js +334 -334
  13. package/src/aider/mcp-packs.js +329 -329
  14. package/src/aider/patch.js +214 -214
  15. package/src/aider/plans.js +186 -186
  16. package/src/aider/premium.js +360 -360
  17. package/src/aider/setup.js +404 -404
  18. package/src/aider/techniques.js +312 -67
  19. package/src/analyze.js +951 -951
  20. package/src/anti-patterns.js +485 -485
  21. package/src/audit/instruction-files.js +180 -180
  22. package/src/audit/recommendations.js +577 -577
  23. package/src/auto-suggest.js +154 -154
  24. package/src/badge.js +13 -13
  25. package/src/behavioral-drift.js +801 -801
  26. package/src/benchmark.js +67 -67
  27. package/src/catalog.js +103 -103
  28. package/src/certification.js +128 -128
  29. package/src/codex/config-parser.js +183 -183
  30. package/src/codex/context.js +223 -223
  31. package/src/codex/deep-review.js +493 -493
  32. package/src/codex/domain-packs.js +394 -394
  33. package/src/codex/freshness.js +84 -84
  34. package/src/codex/governance.js +192 -192
  35. package/src/codex/interactive.js +618 -618
  36. package/src/codex/mcp-packs.js +914 -914
  37. package/src/codex/patch.js +209 -209
  38. package/src/codex/plans.js +251 -251
  39. package/src/codex/premium.js +614 -614
  40. package/src/codex/setup.js +591 -591
  41. package/src/continuous-ops.js +681 -681
  42. package/src/copilot/activity.js +309 -309
  43. package/src/copilot/deep-review.js +346 -346
  44. package/src/copilot/domain-packs.js +372 -372
  45. package/src/copilot/freshness.js +57 -57
  46. package/src/copilot/governance.js +222 -222
  47. package/src/copilot/interactive.js +406 -406
  48. package/src/copilot/mcp-packs.js +826 -826
  49. package/src/copilot/plans.js +253 -253
  50. package/src/copilot/premium.js +451 -451
  51. package/src/copilot/setup.js +488 -488
  52. package/src/cost-tracking.js +61 -61
  53. package/src/cursor/activity.js +301 -301
  54. package/src/cursor/config-parser.js +265 -265
  55. package/src/cursor/context.js +256 -256
  56. package/src/cursor/deep-review.js +334 -334
  57. package/src/cursor/domain-packs.js +368 -368
  58. package/src/cursor/freshness.js +65 -65
  59. package/src/cursor/governance.js +229 -229
  60. package/src/cursor/interactive.js +391 -391
  61. package/src/cursor/mcp-packs.js +828 -828
  62. package/src/cursor/plans.js +254 -254
  63. package/src/cursor/premium.js +469 -469
  64. package/src/cursor/setup.js +488 -488
  65. package/src/dashboard.js +493 -493
  66. package/src/deep-review.js +428 -428
  67. package/src/deprecation.js +98 -98
  68. package/src/diff-only.js +280 -280
  69. package/src/doctor.js +119 -119
  70. package/src/domain-pack-expansion.js +1033 -1033
  71. package/src/domain-packs.js +387 -387
  72. package/src/feedback.js +178 -178
  73. package/src/fix-engine.js +783 -783
  74. package/src/fix-prompts.js +122 -122
  75. package/src/formatters/sarif.js +115 -115
  76. package/src/freshness.js +74 -74
  77. package/src/gemini/config-parser.js +275 -275
  78. package/src/gemini/deep-review.js +559 -559
  79. package/src/gemini/domain-packs.js +393 -393
  80. package/src/gemini/freshness.js +66 -66
  81. package/src/gemini/governance.js +201 -201
  82. package/src/gemini/interactive.js +860 -860
  83. package/src/gemini/mcp-packs.js +915 -915
  84. package/src/gemini/plans.js +269 -269
  85. package/src/gemini/premium.js +760 -760
  86. package/src/gemini/setup.js +692 -692
  87. package/src/governance.js +72 -72
  88. package/src/harmony/add.js +68 -68
  89. package/src/harmony/advisor.js +333 -333
  90. package/src/harmony/canon.js +565 -565
  91. package/src/harmony/cli.js +591 -591
  92. package/src/harmony/drift.js +401 -401
  93. package/src/harmony/governance.js +313 -313
  94. package/src/harmony/memory.js +239 -239
  95. package/src/harmony/sync.js +475 -475
  96. package/src/harmony/watch.js +370 -370
  97. package/src/hook-validation.js +342 -342
  98. package/src/index.js +271 -271
  99. package/src/init.js +184 -184
  100. package/src/instruction-surfaces.js +185 -185
  101. package/src/integrations.js +144 -144
  102. package/src/interactive.js +118 -118
  103. package/src/locales/en.json +1 -1
  104. package/src/locales/es.json +1 -1
  105. package/src/mcp-packs.js +830 -830
  106. package/src/mcp-server.js +726 -726
  107. package/src/mcp-validation.js +337 -337
  108. package/src/nerviq-sync.json +7 -7
  109. package/src/opencode/config-parser.js +109 -109
  110. package/src/opencode/context.js +247 -247
  111. package/src/opencode/deep-review.js +313 -313
  112. package/src/opencode/domain-packs.js +262 -262
  113. package/src/opencode/freshness.js +66 -66
  114. package/src/opencode/governance.js +159 -159
  115. package/src/opencode/interactive.js +392 -392
  116. package/src/opencode/mcp-packs.js +705 -705
  117. package/src/opencode/patch.js +184 -184
  118. package/src/opencode/plans.js +231 -231
  119. package/src/opencode/premium.js +413 -413
  120. package/src/opencode/setup.js +449 -449
  121. package/src/opencode/techniques.js +27 -27
  122. package/src/operating-profile.js +574 -574
  123. package/src/org.js +152 -152
  124. package/src/permission-rules.js +218 -218
  125. package/src/plans.js +839 -839
  126. package/src/platform-change-manifest.js +86 -86
  127. package/src/plugins.js +110 -110
  128. package/src/policy-layers.js +210 -210
  129. package/src/profiles.js +124 -124
  130. package/src/prompt-injection.js +74 -74
  131. package/src/public-api.js +173 -173
  132. package/src/recommendation-rules.js +84 -84
  133. package/src/repo-archetype.js +386 -386
  134. package/src/secret-patterns.js +39 -39
  135. package/src/server.js +527 -527
  136. package/src/setup/analysis.js +607 -607
  137. package/src/setup/runtime.js +172 -172
  138. package/src/setup.js +677 -677
  139. package/src/shared/capabilities.js +194 -194
  140. package/src/source-urls.js +132 -132
  141. package/src/stack-checks.js +565 -565
  142. package/src/supplemental-checks.js +13 -13
  143. package/src/synergy/adaptive.js +261 -261
  144. package/src/synergy/compensation.js +137 -137
  145. package/src/synergy/evidence.js +193 -193
  146. package/src/synergy/learning.js +199 -199
  147. package/src/synergy/patterns.js +227 -227
  148. package/src/synergy/ranking.js +83 -83
  149. package/src/synergy/report.js +165 -165
  150. package/src/synergy/routing.js +146 -146
  151. package/src/techniques/api.js +407 -407
  152. package/src/techniques/automation.js +316 -316
  153. package/src/techniques/compliance.js +257 -257
  154. package/src/techniques/hygiene.js +294 -294
  155. package/src/techniques/instructions.js +243 -243
  156. package/src/techniques/observability.js +226 -226
  157. package/src/techniques/optimization.js +142 -142
  158. package/src/techniques/quality.js +318 -318
  159. package/src/techniques/security.js +237 -237
  160. package/src/techniques/shared.js +443 -443
  161. package/src/techniques/stacks.js +2294 -2294
  162. package/src/techniques/tools.js +106 -106
  163. package/src/techniques/workflow.js +413 -413
  164. package/src/techniques.js +81 -81
  165. package/src/terminology.js +73 -73
  166. package/src/token-estimate.js +35 -35
  167. package/src/usage-patterns.js +99 -99
  168. package/src/verification-metadata.js +145 -145
  169. package/src/watch.js +247 -247
  170. package/src/windsurf/activity.js +302 -302
  171. package/src/windsurf/config-parser.js +267 -267
  172. package/src/windsurf/deep-review.js +337 -337
  173. package/src/windsurf/domain-packs.js +370 -370
  174. package/src/windsurf/freshness.js +36 -36
  175. package/src/windsurf/governance.js +231 -231
  176. package/src/windsurf/interactive.js +388 -388
  177. package/src/windsurf/mcp-packs.js +792 -792
  178. package/src/windsurf/plans.js +247 -247
  179. package/src/windsurf/premium.js +468 -468
  180. package/src/windsurf/setup.js +471 -471
  181. package/src/workspace.js +375 -375
@@ -1,591 +1,591 @@
1
- /**
2
- * H9. Harmony CLI Commands
3
- *
4
- * Command handlers for harmony operations, to be called from bin/cli.js.
5
- * Each function returns a formatted output string (or prints to console).
6
- *
7
- * Zero external dependencies - imports from sibling harmony modules and parent platform modules.
8
- */
9
-
10
- const path = require('path');
11
- const { generateStrategicAdvice, PLATFORM_STRENGTHS } = require('./advisor');
12
- const { startHarmonyWatch, buildHarmonyWatchPlan } = require('./watch');
13
- const { saveHarmonyState, loadHarmonyState, getHarmonyHistory, recordPlatformScore, recordDrift } = require('./memory');
14
- const { getHarmonyGovernanceSummary, formatHarmonyGovernanceReport } = require('./governance');
15
-
16
- const COLORS = {
17
- reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
18
- green: '\x1b[32m', yellow: '\x1b[33m', red: '\x1b[31m', blue: '\x1b[36m',
19
- magenta: '\x1b[35m',
20
- };
21
- const c = (text, color) => `${COLORS[color] || ''}${text}${COLORS.reset}`;
22
-
23
- // ─── Shared helpers ───────────────────────────────────────────────────────────
24
-
25
- function resolveDir(options) {
26
- return path.resolve(options.dir || options.d || '.');
27
- }
28
-
29
- /**
30
- * Collect audit results from all detectable platforms.
31
- * Runs audit() for each platform in sequence (audit is async).
32
- */
33
- /**
34
- * Detect which platforms have config files present in the directory.
35
- * Only these platforms will be audited in harmony commands.
36
- */
37
- function detectPresentPlatforms(dir) {
38
- const fs = require('fs');
39
- const pathMod = require('path');
40
- const exists = (f) => fs.existsSync(pathMod.join(dir, f));
41
-
42
- const detected = [];
43
- if (exists('CLAUDE.md') || exists('.claude/settings.json') || exists('.claude/CLAUDE.md')) detected.push('claude');
44
- if (exists('AGENTS.md') || exists('.codex/config.toml')) detected.push('codex');
45
- if (exists('GEMINI.md') || exists('.gemini/settings.json')) detected.push('gemini');
46
- if (exists('.github/copilot-instructions.md')) detected.push('copilot');
47
- if (exists('.cursorrules') || exists('.cursor/rules')) detected.push('cursor');
48
- if (exists('.windsurfrules') || exists('.windsurf/rules')) detected.push('windsurf');
49
- if (exists('.aider.conf.yml') || exists('.aiderignore')) detected.push('aider');
50
- if (exists('opencode.json') || exists('.opencode')) detected.push('opencode');
51
-
52
- // AGENTS.md is shared by codex, copilot, and opencode — only add if not already detected via platform-specific file
53
- if (exists('AGENTS.md') && !detected.includes('codex')) detected.push('codex');
54
-
55
- return detected.length > 0 ? detected : ['claude']; // default to claude if nothing found
56
- }
57
-
58
- async function collectPlatformAudits(dir) {
59
- const { audit } = require('../audit');
60
- const platforms = detectPresentPlatforms(dir);
61
- const results = [];
62
-
63
- for (const platform of platforms) {
64
- try {
65
- const result = await audit({ dir, silent: true, platform });
66
- if (result && typeof result.score === 'number') {
67
- results.push({ platform, ...result });
68
- }
69
- } catch (_e) { /* platform not available */ }
70
- }
71
-
72
- return results;
73
- }
74
-
75
- // ─── Command: harmony audit ───────────────────────────────────────────────────
76
-
77
- /**
78
- * Run a cross-platform audit and display per-platform scores.
79
- */
80
- async function runHarmonyAudit(options) {
81
- const dir = resolveDir(options);
82
- const platformAudits = await collectPlatformAudits(dir);
83
-
84
- if (options.json) {
85
- console.log(JSON.stringify({ dir, platforms: platformAudits }, null, 2));
86
- return { dir, platforms: platformAudits };
87
- }
88
-
89
- console.log('');
90
- console.log(c(' Harmony Cross-Platform Audit', 'bold'));
91
- console.log(c(' ═══════════════════════════════════════', 'dim'));
92
- console.log(c(` Directory: ${dir}`, 'dim'));
93
- console.log('');
94
-
95
- if (platformAudits.length === 0) {
96
- console.log(c(' No platform configurations detected.', 'yellow'));
97
- console.log(c(' Run "nerviq setup" to bootstrap a platform.', 'dim'));
98
- console.log('');
99
- return { dir, platforms: [] };
100
- }
101
-
102
- for (const audit of platformAudits) {
103
- const scoreColor = audit.score >= 70 ? 'green' : audit.score >= 40 ? 'yellow' : 'red';
104
- console.log(` ${c(audit.platform.padEnd(12), 'bold')} ${c(`${audit.score}/100`, scoreColor)} (${audit.passed || 0}/${(audit.passed || 0) + (audit.failed || 0)} checks)`);
105
-
106
- // Record score to memory
107
- try {
108
- recordPlatformScore(dir, audit.platform, audit.score, { passed: audit.passed, failed: audit.failed });
109
- } catch (_e) { /* memory write optional */ }
110
- }
111
-
112
- // Detect drift: compare current scores to last recorded scores
113
- try {
114
- const state = loadHarmonyState(dir);
115
- const prevScores = state.platformScores || [];
116
- for (const audit of platformAudits) {
117
- const prevEntries = (prevScores || []).filter(e => e.platform === audit.platform);
118
- if (prevEntries.length >= 2) {
119
- // Compare to the second-to-last entry (last is what we just recorded)
120
- const prev = prevEntries[prevEntries.length - 2];
121
- const delta = audit.score - (prev.score || 0);
122
- if (Math.abs(delta) >= 5) {
123
- recordDrift(dir, {
124
- platform: audit.platform,
125
- driftScore: delta,
126
- driftedFields: [`score: ${prev.score} → ${audit.score}`],
127
- });
128
- const sign = delta > 0 ? '+' : '';
129
- console.log(c(` ${audit.platform}: ${sign}${delta} since last audit`, delta > 0 ? 'green' : 'red'));
130
- }
131
- }
132
- }
133
- } catch (_e) { /* drift recording is optional */ }
134
-
135
- // Average score
136
- const avgScore = Math.round(platformAudits.reduce((sum, a) => sum + (a.score || 0), 0) / platformAudits.length);
137
- const avgColor = avgScore >= 70 ? 'green' : avgScore >= 40 ? 'yellow' : 'red';
138
- console.log('');
139
- console.log(` ${c('Average:', 'bold')} ${c(`${avgScore}/100`, avgColor)}`);
140
- console.log(` ${c('Platforms:', 'bold')} ${platformAudits.length}`);
141
- console.log('');
142
-
143
- return { dir, platforms: platformAudits, averageScore: avgScore };
144
- }
145
-
146
- // ─── Command: harmony sync ────────────────────────────────────────────────────
147
-
148
- /**
149
- * Sync canonical model across platforms (detect drift and suggest fixes).
150
- */
151
- async function runHarmonySync(options) {
152
- const dir = resolveDir(options);
153
- const platformAudits = await collectPlatformAudits(dir);
154
-
155
- // Load or build canonical model from memory
156
- const state = loadHarmonyState(dir);
157
- const canonicalModel = state.canon || null;
158
-
159
- const advice = generateStrategicAdvice(canonicalModel, platformAudits);
160
-
161
- if (options.json) {
162
- console.log(JSON.stringify({ dir, sync: advice.crossPlatformActions }, null, 2));
163
- return advice.crossPlatformActions;
164
- }
165
-
166
- console.log('');
167
- console.log(c(' Harmony Sync', 'bold'));
168
- console.log(c(' ═══════════════════════════════════════', 'dim'));
169
- console.log('');
170
-
171
- if (advice.crossPlatformActions.length === 0) {
172
- console.log(c(' All platforms are in sync. No actions needed.', 'green'));
173
- console.log('');
174
- return [];
175
- }
176
-
177
- for (const action of advice.crossPlatformActions) {
178
- const prioColor = action.priority === 'high' ? 'red' : action.priority === 'medium' ? 'yellow' : 'dim';
179
- console.log(` ${c(`[${action.priority.toUpperCase()}]`, prioColor)} ${action.action}`);
180
- console.log(` ${c('Affected:', 'dim')} ${action.affectedPlatforms.join(', ')}`);
181
- if (action.sourcePlatforms) {
182
- console.log(` ${c('Source:', 'dim')} ${action.sourcePlatforms.join(', ')}`);
183
- }
184
- console.log('');
185
- }
186
-
187
- return advice.crossPlatformActions;
188
- }
189
-
190
- // ─── Command: harmony drift ──────────────────────────────────────────────────
191
-
192
- /**
193
- * Detect and display drift between platforms.
194
- */
195
- async function runHarmonyDrift(options) {
196
- const dir = resolveDir(options);
197
- const state = loadHarmonyState(dir);
198
- const history = getHarmonyHistory(dir, options.platform ? { platform: options.platform } : undefined);
199
-
200
- if (options.json) {
201
- console.log(JSON.stringify({ dir, drift: history.driftHistory }, null, 2));
202
- return history.driftHistory;
203
- }
204
-
205
- console.log('');
206
- console.log(c(' Harmony Drift History', 'bold'));
207
- console.log(c(' ═══════════════════════════════════════', 'dim'));
208
- console.log('');
209
-
210
- const driftEntries = history.driftHistory;
211
-
212
- if (driftEntries.length === 0) {
213
- console.log(c(' No drift history recorded yet.', 'dim'));
214
- console.log(c(' Run "nerviq harmony audit" to start tracking.', 'dim'));
215
- console.log('');
216
- return [];
217
- }
218
-
219
- // Show recent drift entries (last 20)
220
- const recent = driftEntries.slice(-20);
221
- for (const entry of recent) {
222
- const driftColor = entry.driftScore > 20 ? 'red' : entry.driftScore > 10 ? 'yellow' : 'green';
223
- console.log(` ${c(entry.timestamp || 'unknown', 'dim')} ${entry.platform.padEnd(12)} drift: ${c(String(entry.driftScore), driftColor)}`);
224
- if (entry.driftedFields && entry.driftedFields.length > 0) {
225
- console.log(` ${c('Fields:', 'dim')} ${entry.driftedFields.join(', ')}`);
226
- }
227
- }
228
- console.log('');
229
-
230
- return driftEntries;
231
- }
232
-
233
- // ─── Command: harmony advise ──────────────────────────────────────────────────
234
-
235
- /**
236
- * Generate and display strategic advice.
237
- */
238
- async function runHarmonyAdvise(options) {
239
- const dir = resolveDir(options);
240
- const platformAudits = await collectPlatformAudits(dir);
241
- const state = loadHarmonyState(dir);
242
- const canonicalModel = state.canon || null;
243
-
244
- const advice = generateStrategicAdvice(canonicalModel, platformAudits);
245
-
246
- if (options.json) {
247
- console.log(JSON.stringify(advice, null, 2));
248
- return advice;
249
- }
250
-
251
- console.log('');
252
- console.log(c(' Harmony Strategic Advisor', 'bold'));
253
- console.log(c(' ═══════════════════════════════════════', 'dim'));
254
- console.log('');
255
-
256
- // Task routing
257
- console.log(c(' Task Routing Recommendations', 'bold'));
258
- console.log('');
259
- for (const route of advice.taskRouting) {
260
- const confColor = route.confidence === 'high' ? 'green' : route.confidence === 'medium' ? 'yellow' : 'dim';
261
- console.log(` ${c(route.taskLabel.padEnd(22), 'bold')} → ${c(route.recommendedLabel, 'blue')} ${c(`[${route.confidence}]`, confColor)}`);
262
- console.log(` ${c(route.reasoning, 'dim')}`);
263
- if (route.alternatives.length > 0) {
264
- const altLabels = route.alternatives.map(a => `${a.label} (${a.score})`).join(', ');
265
- console.log(` ${c('Alternatives:', 'dim')} ${altLabels}`);
266
- }
267
- console.log('');
268
- }
269
-
270
- // Config recommendations
271
- if (advice.configRecommendations.length > 0) {
272
- console.log(c(' Configuration Recommendations', 'bold'));
273
- console.log('');
274
- for (const rec of advice.configRecommendations) {
275
- const impactColor = rec.impact === 'high' ? 'red' : rec.impact === 'medium' ? 'yellow' : 'dim';
276
- console.log(` ${c(`[${rec.impact.toUpperCase()}]`, impactColor)} ${rec.platform}: ${rec.recommendation}`);
277
- }
278
- console.log('');
279
- }
280
-
281
- // Cross-platform actions
282
- if (advice.crossPlatformActions.length > 0) {
283
- console.log(c(' Cross-Platform Actions', 'bold'));
284
- console.log('');
285
- for (const action of advice.crossPlatformActions) {
286
- const prioColor = action.priority === 'high' ? 'red' : action.priority === 'medium' ? 'yellow' : 'dim';
287
- console.log(` ${c(`[${action.priority.toUpperCase()}]`, prioColor)} ${action.action}`);
288
- console.log(` ${c('Platforms:', 'dim')} ${action.affectedPlatforms.join(', ')}`);
289
- }
290
- console.log('');
291
- }
292
-
293
- return advice;
294
- }
295
-
296
- // ─── Command: harmony watch ───────────────────────────────────────────────────
297
-
298
- /**
299
- * Start the harmony watch loop.
300
- */
301
- async function runHarmonyWatch(options) {
302
- const dir = resolveDir(options);
303
-
304
- await startHarmonyWatch({
305
- dir,
306
- autoSync: !!options.autoSync,
307
- debounceMs: options.debounce || 800,
308
- onDriftDetected: (platform, details) => {
309
- console.log(c(` DRIFT ALERT: ${platform} score dropped by ${Math.abs(details.delta)}`, 'red'));
310
- },
311
- onPlatformChange: (platform, file) => {
312
- // Logged by watch module itself
313
- },
314
- runAudit: options.noAudit ? null : async (auditDir) => {
315
- const audits = await collectPlatformAudits(auditDir);
316
- const result = {};
317
- for (const audit of audits) {
318
- result[audit.platform] = audit;
319
- }
320
- return result;
321
- },
322
- });
323
- }
324
-
325
- // ─── Command: harmony governance ──────────────────────────────────────────────
326
-
327
- /**
328
- * Display the cross-platform governance summary.
329
- */
330
- async function runHarmonyGovernance(options) {
331
- const dir = resolveDir(options);
332
- const platformAudits = await collectPlatformAudits(dir);
333
- const state = loadHarmonyState(dir);
334
- const canonicalModel = state.canon || null;
335
-
336
- const summary = getHarmonyGovernanceSummary(canonicalModel, platformAudits);
337
-
338
- if (options.json) {
339
- console.log(JSON.stringify(summary, null, 2));
340
- return summary;
341
- }
342
-
343
- const report = formatHarmonyGovernanceReport(summary, options);
344
- console.log(report);
345
-
346
- return summary;
347
- }
348
-
349
- // ─── Command: harmony score ──────────────────────────────────────────────────
350
-
351
- /**
352
- * Output a standalone Harmony Score (0-100) with optional badge and CI threshold.
353
- *
354
- * Options:
355
- * --json JSON output
356
- * --badge Print shields.io badge markdown
357
- * --threshold N Exit with code 1 if score < N (for CI gates)
358
- * --quiet Score number only (for piping)
359
- */
360
- async function runHarmonyScore(options) {
361
- const dir = resolveDir(options);
362
- const { harmonyAudit } = require('./audit');
363
- const result = await harmonyAudit({ dir, silent: true });
364
-
365
- const score = result.harmonyScore;
366
- const threshold = parseInt(options.threshold, 10) || 0;
367
- const pass = score >= threshold;
368
-
369
- if (options.json) {
370
- const output = {
371
- harmonyScore: score,
372
- platforms: result.platformScores,
373
- activePlatforms: result.activePlatforms.map(p => p.platform),
374
- driftCount: result.drift.drifts.length,
375
- threshold: threshold || null,
376
- pass,
377
- };
378
- if (options.badge) {
379
- output.badge = getHarmonyBadgeMarkdown(score);
380
- output.badgeUrl = getHarmonyBadgeUrl(score);
381
- }
382
- console.log(JSON.stringify(output, null, 2));
383
- return output;
384
- }
385
-
386
- if (options.quiet) {
387
- console.log(score);
388
- return { score, pass };
389
- }
390
-
391
- console.log('');
392
- console.log(c(' Harmony Score', 'bold'));
393
- console.log(c(' ═══════════════════════════════════════', 'dim'));
394
- console.log('');
395
-
396
- // Score with color bar
397
- const barWidth = 30;
398
- const filled = Math.round((score / 100) * barWidth);
399
- const empty = barWidth - filled;
400
- const scoreColor = score >= 80 ? 'green' : score >= 50 ? 'yellow' : 'red';
401
- const bar = c('\u2588'.repeat(filled), scoreColor) + c('\u2591'.repeat(empty), 'dim');
402
- console.log(` ${bar} ${c(`${score}/100`, scoreColor)}`);
403
- console.log('');
404
-
405
- // Per-platform breakdown
406
- for (const ap of result.activePlatforms) {
407
- const ps = result.platformScores[ap.platform];
408
- const psColor = ps >= 70 ? 'green' : ps >= 40 ? 'yellow' : 'red';
409
- console.log(` ${ap.platform.padEnd(12)} ${ps !== null ? c(`${ps}/100`, psColor) : c('n/a', 'dim')}`);
410
- }
411
- console.log('');
412
-
413
- // Drift summary
414
- const driftCount = result.drift.drifts.length;
415
- if (driftCount > 0) {
416
- const critical = result.drift.drifts.filter(d => d.severity === 'critical').length;
417
- const high = result.drift.drifts.filter(d => d.severity === 'high').length;
418
- let driftMsg = ` ${driftCount} drift issue${driftCount !== 1 ? 's' : ''}`;
419
- if (critical > 0) driftMsg += c(` (${critical} critical)`, 'red');
420
- else if (high > 0) driftMsg += c(` (${high} high)`, 'yellow');
421
- console.log(driftMsg);
422
- console.log(c(' Run "nerviq harmony-audit" for details.', 'dim'));
423
- console.log('');
424
- }
425
-
426
- // Badge output
427
- if (options.badge) {
428
- console.log(c(' Badge:', 'bold'));
429
- console.log(` ${getHarmonyBadgeMarkdown(score)}`);
430
- console.log('');
431
- }
432
-
433
- // Threshold check
434
- if (threshold > 0) {
435
- if (pass) {
436
- console.log(c(` Threshold: ${score} >= ${threshold} PASS`, 'green'));
437
- } else {
438
- console.log(c(` Threshold: ${score} < ${threshold} FAIL`, 'red'));
439
- }
440
- console.log('');
441
- }
442
-
443
- return { score, pass, platforms: result.platformScores };
444
- }
445
-
446
- // ─── Harmony Badge helpers ───────────────────────────────────────────────────
447
-
448
- function getHarmonyBadgeUrl(score) {
449
- const color = score >= 80 ? 'brightgreen' : score >= 60 ? 'yellow' : score >= 40 ? 'orange' : 'red';
450
- const label = encodeURIComponent('Harmony Score');
451
- const message = encodeURIComponent(`${score}/100`);
452
- return `https://img.shields.io/badge/${label}-${message}-${color}`;
453
- }
454
-
455
- function getHarmonyBadgeMarkdown(score) {
456
- const url = getHarmonyBadgeUrl(score);
457
- return `[![Harmony Score](${url})](https://github.com/nerviq/nerviq)`;
458
- }
459
-
460
- // ─── Command: harmony demo ──────────────────────────────────────────────────
461
-
462
- /**
463
- * Zero-setup demo: creates a temporary multi-platform project, runs harmony
464
- * audit on it, and shows how Nerviq detects cross-platform drift.
465
- *
466
- * This lets new users see Harmony's value instantly without configuring anything.
467
- */
468
- async function runHarmonyDemo(options) {
469
- const fs = require('fs');
470
- const os = require('os');
471
- const { harmonyAudit } = require('./audit');
472
-
473
- console.log('');
474
- console.log(c(' Harmony Demo — Zero-Setup Cross-Platform Drift Detection', 'bold'));
475
- console.log(c(' ═══════════════════════════════════════════════════════', 'dim'));
476
- console.log('');
477
- console.log(c(' Creating a sample multi-platform project...', 'dim'));
478
- console.log('');
479
-
480
- // Create temp directory with realistic multi-platform configs
481
- const demoDir = path.join(os.tmpdir(), `nerviq-harmony-demo-${Date.now()}`);
482
- fs.mkdirSync(demoDir, { recursive: true });
483
- fs.mkdirSync(path.join(demoDir, '.claude'), { recursive: true });
484
- fs.mkdirSync(path.join(demoDir, '.cursor'), { recursive: true });
485
- fs.mkdirSync(path.join(demoDir, '.github'), { recursive: true });
486
-
487
- // Claude config — well-configured
488
- fs.writeFileSync(path.join(demoDir, 'CLAUDE.md'), [
489
- '# Project Instructions',
490
- '',
491
- '## Architecture',
492
- 'This is a Node.js API with PostgreSQL. Use Express for routing.',
493
- '',
494
- '## Testing',
495
- 'Run tests with `npm test`. All PRs require passing tests.',
496
- '',
497
- '## Security',
498
- '- Never commit .env files',
499
- '- Use parameterized queries for all database access',
500
- '- Validate all user input',
501
- '',
502
- '## Code Style',
503
- '- Use ESLint with the project config',
504
- '- Prefer async/await over callbacks',
505
- '- Add JSDoc comments for public functions',
506
- ].join('\n'));
507
-
508
- fs.writeFileSync(path.join(demoDir, '.claude', 'settings.json'), JSON.stringify({
509
- permissions: {
510
- allow: ['Read', 'Glob', 'Grep'],
511
- deny: ['Bash(rm -rf *)'],
512
- },
513
- model: 'claude-sonnet-4-6',
514
- }, null, 2));
515
-
516
- // Cursor config — intentionally drifted (different rules, less security)
517
- fs.writeFileSync(path.join(demoDir, '.cursorrules'), [
518
- 'You are a helpful coding assistant.',
519
- 'This is a Node.js project using Express.',
520
- 'Write clean, readable code.',
521
- // Missing: security rules, testing rules, architecture details
522
- ].join('\n'));
523
-
524
- // Copilot config — partial coverage
525
- fs.writeFileSync(path.join(demoDir, '.github', 'copilot-instructions.md'), [
526
- '# Copilot Instructions',
527
- '',
528
- 'This is a Node.js Express API project.',
529
- 'Use TypeScript-style JSDoc annotations.',
530
- 'Follow RESTful conventions for API endpoints.',
531
- // Missing: security, testing, architecture details
532
- ].join('\n'));
533
-
534
- // Add a package.json for realism
535
- fs.writeFileSync(path.join(demoDir, 'package.json'), JSON.stringify({
536
- name: 'harmony-demo-project',
537
- version: '1.0.0',
538
- scripts: { test: 'jest' },
539
- }, null, 2));
540
-
541
- console.log(c(' Demo project created with 3 platforms:', 'bold'));
542
- console.log(` ${c('Claude', 'green')} — Well-configured (CLAUDE.md + settings.json)`);
543
- console.log(` ${c('Cursor', 'yellow')} — Basic rules only (.cursorrules)`);
544
- console.log(` ${c('Copilot', 'yellow')} — Partial coverage (copilot-instructions.md)`);
545
- console.log('');
546
- console.log(c(' Intentional drift injected:', 'bold'));
547
- console.log(` ${c('\u2718', 'red')} Security rules only in Claude, missing from Cursor & Copilot`);
548
- console.log(` ${c('\u2718', 'red')} Testing instructions only in Claude`);
549
- console.log(` ${c('\u2718', 'red')} Architecture details inconsistent across platforms`);
550
- console.log(` ${c('\u2718', 'red')} Trust posture differs (Claude has explicit permissions)`);
551
- console.log('');
552
- console.log(c(' Running Harmony Audit...', 'dim'));
553
- console.log('');
554
-
555
- // Run the actual harmony audit on the demo project
556
- const result = await harmonyAudit({ dir: demoDir, silent: false, verbose: !!options.verbose });
557
-
558
- console.log('');
559
- console.log(c(' ═══════════════════════════════════════════════════════', 'dim'));
560
- console.log(c(' What you just saw:', 'bold'));
561
- console.log('');
562
- console.log(' Nerviq Harmony detected real configuration drift between');
563
- console.log(' 3 AI coding platforms in your project — differences in');
564
- console.log(' instructions, security posture, and tool coverage that');
565
- console.log(' cause inconsistent AI behavior.');
566
- console.log('');
567
- console.log(c(' Try it on your own project:', 'bold'));
568
- console.log(` ${c('npx @nerviq/cli harmony-audit', 'blue')}`);
569
- console.log(` ${c('npx @nerviq/cli harmony-score --threshold 70', 'blue')}`);
570
- console.log('');
571
-
572
- // Clean up
573
- try {
574
- fs.rmSync(demoDir, { recursive: true, force: true });
575
- } catch (_e) { /* cleanup optional */ }
576
-
577
- return result;
578
- }
579
-
580
- module.exports = {
581
- runHarmonyAudit,
582
- runHarmonySync,
583
- runHarmonyDrift,
584
- runHarmonyAdvise,
585
- runHarmonyWatch,
586
- runHarmonyGovernance,
587
- runHarmonyScore,
588
- runHarmonyDemo,
589
- getHarmonyBadgeUrl,
590
- getHarmonyBadgeMarkdown,
591
- };
1
+ /**
2
+ * H9. Harmony CLI Commands
3
+ *
4
+ * Command handlers for harmony operations, to be called from bin/cli.js.
5
+ * Each function returns a formatted output string (or prints to console).
6
+ *
7
+ * Zero external dependencies - imports from sibling harmony modules and parent platform modules.
8
+ */
9
+
10
+ const path = require('path');
11
+ const { generateStrategicAdvice, PLATFORM_STRENGTHS } = require('./advisor');
12
+ const { startHarmonyWatch, buildHarmonyWatchPlan } = require('./watch');
13
+ const { saveHarmonyState, loadHarmonyState, getHarmonyHistory, recordPlatformScore, recordDrift } = require('./memory');
14
+ const { getHarmonyGovernanceSummary, formatHarmonyGovernanceReport } = require('./governance');
15
+
16
+ const COLORS = {
17
+ reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
18
+ green: '\x1b[32m', yellow: '\x1b[33m', red: '\x1b[31m', blue: '\x1b[36m',
19
+ magenta: '\x1b[35m',
20
+ };
21
+ const c = (text, color) => `${COLORS[color] || ''}${text}${COLORS.reset}`;
22
+
23
+ // ─── Shared helpers ───────────────────────────────────────────────────────────
24
+
25
+ function resolveDir(options) {
26
+ return path.resolve(options.dir || options.d || '.');
27
+ }
28
+
29
+ /**
30
+ * Collect audit results from all detectable platforms.
31
+ * Runs audit() for each platform in sequence (audit is async).
32
+ */
33
+ /**
34
+ * Detect which platforms have config files present in the directory.
35
+ * Only these platforms will be audited in harmony commands.
36
+ */
37
+ function detectPresentPlatforms(dir) {
38
+ const fs = require('fs');
39
+ const pathMod = require('path');
40
+ const exists = (f) => fs.existsSync(pathMod.join(dir, f));
41
+
42
+ const detected = [];
43
+ if (exists('CLAUDE.md') || exists('.claude/settings.json') || exists('.claude/CLAUDE.md')) detected.push('claude');
44
+ if (exists('AGENTS.md') || exists('.codex/config.toml')) detected.push('codex');
45
+ if (exists('GEMINI.md') || exists('.gemini/settings.json')) detected.push('gemini');
46
+ if (exists('.github/copilot-instructions.md')) detected.push('copilot');
47
+ if (exists('.cursorrules') || exists('.cursor/rules')) detected.push('cursor');
48
+ if (exists('.windsurfrules') || exists('.windsurf/rules')) detected.push('windsurf');
49
+ if (exists('.aider.conf.yml') || exists('.aiderignore')) detected.push('aider');
50
+ if (exists('opencode.json') || exists('.opencode')) detected.push('opencode');
51
+
52
+ // AGENTS.md is shared by codex, copilot, and opencode — only add if not already detected via platform-specific file
53
+ if (exists('AGENTS.md') && !detected.includes('codex')) detected.push('codex');
54
+
55
+ return detected.length > 0 ? detected : ['claude']; // default to claude if nothing found
56
+ }
57
+
58
+ async function collectPlatformAudits(dir) {
59
+ const { audit } = require('../audit');
60
+ const platforms = detectPresentPlatforms(dir);
61
+ const results = [];
62
+
63
+ for (const platform of platforms) {
64
+ try {
65
+ const result = await audit({ dir, silent: true, platform });
66
+ if (result && typeof result.score === 'number') {
67
+ results.push({ platform, ...result });
68
+ }
69
+ } catch (_e) { /* platform not available */ }
70
+ }
71
+
72
+ return results;
73
+ }
74
+
75
+ // ─── Command: harmony audit ───────────────────────────────────────────────────
76
+
77
+ /**
78
+ * Run a cross-platform audit and display per-platform scores.
79
+ */
80
+ async function runHarmonyAudit(options) {
81
+ const dir = resolveDir(options);
82
+ const platformAudits = await collectPlatformAudits(dir);
83
+
84
+ if (options.json) {
85
+ console.log(JSON.stringify({ dir, platforms: platformAudits }, null, 2));
86
+ return { dir, platforms: platformAudits };
87
+ }
88
+
89
+ console.log('');
90
+ console.log(c(' Harmony Cross-Platform Audit', 'bold'));
91
+ console.log(c(' ═══════════════════════════════════════', 'dim'));
92
+ console.log(c(` Directory: ${dir}`, 'dim'));
93
+ console.log('');
94
+
95
+ if (platformAudits.length === 0) {
96
+ console.log(c(' No platform configurations detected.', 'yellow'));
97
+ console.log(c(' Run "nerviq setup" to bootstrap a platform.', 'dim'));
98
+ console.log('');
99
+ return { dir, platforms: [] };
100
+ }
101
+
102
+ for (const audit of platformAudits) {
103
+ const scoreColor = audit.score >= 70 ? 'green' : audit.score >= 40 ? 'yellow' : 'red';
104
+ console.log(` ${c(audit.platform.padEnd(12), 'bold')} ${c(`${audit.score}/100`, scoreColor)} (${audit.passed || 0}/${(audit.passed || 0) + (audit.failed || 0)} checks)`);
105
+
106
+ // Record score to memory
107
+ try {
108
+ recordPlatformScore(dir, audit.platform, audit.score, { passed: audit.passed, failed: audit.failed });
109
+ } catch (_e) { /* memory write optional */ }
110
+ }
111
+
112
+ // Detect drift: compare current scores to last recorded scores
113
+ try {
114
+ const state = loadHarmonyState(dir);
115
+ const prevScores = state.platformScores || [];
116
+ for (const audit of platformAudits) {
117
+ const prevEntries = (prevScores || []).filter(e => e.platform === audit.platform);
118
+ if (prevEntries.length >= 2) {
119
+ // Compare to the second-to-last entry (last is what we just recorded)
120
+ const prev = prevEntries[prevEntries.length - 2];
121
+ const delta = audit.score - (prev.score || 0);
122
+ if (Math.abs(delta) >= 5) {
123
+ recordDrift(dir, {
124
+ platform: audit.platform,
125
+ driftScore: delta,
126
+ driftedFields: [`score: ${prev.score} → ${audit.score}`],
127
+ });
128
+ const sign = delta > 0 ? '+' : '';
129
+ console.log(c(` ${audit.platform}: ${sign}${delta} since last audit`, delta > 0 ? 'green' : 'red'));
130
+ }
131
+ }
132
+ }
133
+ } catch (_e) { /* drift recording is optional */ }
134
+
135
+ // Average score
136
+ const avgScore = Math.round(platformAudits.reduce((sum, a) => sum + (a.score || 0), 0) / platformAudits.length);
137
+ const avgColor = avgScore >= 70 ? 'green' : avgScore >= 40 ? 'yellow' : 'red';
138
+ console.log('');
139
+ console.log(` ${c('Average:', 'bold')} ${c(`${avgScore}/100`, avgColor)}`);
140
+ console.log(` ${c('Platforms:', 'bold')} ${platformAudits.length}`);
141
+ console.log('');
142
+
143
+ return { dir, platforms: platformAudits, averageScore: avgScore };
144
+ }
145
+
146
+ // ─── Command: harmony sync ────────────────────────────────────────────────────
147
+
148
+ /**
149
+ * Sync canonical model across platforms (detect drift and suggest fixes).
150
+ */
151
+ async function runHarmonySync(options) {
152
+ const dir = resolveDir(options);
153
+ const platformAudits = await collectPlatformAudits(dir);
154
+
155
+ // Load or build canonical model from memory
156
+ const state = loadHarmonyState(dir);
157
+ const canonicalModel = state.canon || null;
158
+
159
+ const advice = generateStrategicAdvice(canonicalModel, platformAudits);
160
+
161
+ if (options.json) {
162
+ console.log(JSON.stringify({ dir, sync: advice.crossPlatformActions }, null, 2));
163
+ return advice.crossPlatformActions;
164
+ }
165
+
166
+ console.log('');
167
+ console.log(c(' Harmony Sync', 'bold'));
168
+ console.log(c(' ═══════════════════════════════════════', 'dim'));
169
+ console.log('');
170
+
171
+ if (advice.crossPlatformActions.length === 0) {
172
+ console.log(c(' All platforms are in sync. No actions needed.', 'green'));
173
+ console.log('');
174
+ return [];
175
+ }
176
+
177
+ for (const action of advice.crossPlatformActions) {
178
+ const prioColor = action.priority === 'high' ? 'red' : action.priority === 'medium' ? 'yellow' : 'dim';
179
+ console.log(` ${c(`[${action.priority.toUpperCase()}]`, prioColor)} ${action.action}`);
180
+ console.log(` ${c('Affected:', 'dim')} ${action.affectedPlatforms.join(', ')}`);
181
+ if (action.sourcePlatforms) {
182
+ console.log(` ${c('Source:', 'dim')} ${action.sourcePlatforms.join(', ')}`);
183
+ }
184
+ console.log('');
185
+ }
186
+
187
+ return advice.crossPlatformActions;
188
+ }
189
+
190
+ // ─── Command: harmony drift ──────────────────────────────────────────────────
191
+
192
+ /**
193
+ * Detect and display drift between platforms.
194
+ */
195
+ async function runHarmonyDrift(options) {
196
+ const dir = resolveDir(options);
197
+ const state = loadHarmonyState(dir);
198
+ const history = getHarmonyHistory(dir, options.platform ? { platform: options.platform } : undefined);
199
+
200
+ if (options.json) {
201
+ console.log(JSON.stringify({ dir, drift: history.driftHistory }, null, 2));
202
+ return history.driftHistory;
203
+ }
204
+
205
+ console.log('');
206
+ console.log(c(' Harmony Drift History', 'bold'));
207
+ console.log(c(' ═══════════════════════════════════════', 'dim'));
208
+ console.log('');
209
+
210
+ const driftEntries = history.driftHistory;
211
+
212
+ if (driftEntries.length === 0) {
213
+ console.log(c(' No drift history recorded yet.', 'dim'));
214
+ console.log(c(' Run "nerviq harmony audit" to start tracking.', 'dim'));
215
+ console.log('');
216
+ return [];
217
+ }
218
+
219
+ // Show recent drift entries (last 20)
220
+ const recent = driftEntries.slice(-20);
221
+ for (const entry of recent) {
222
+ const driftColor = entry.driftScore > 20 ? 'red' : entry.driftScore > 10 ? 'yellow' : 'green';
223
+ console.log(` ${c(entry.timestamp || 'unknown', 'dim')} ${entry.platform.padEnd(12)} drift: ${c(String(entry.driftScore), driftColor)}`);
224
+ if (entry.driftedFields && entry.driftedFields.length > 0) {
225
+ console.log(` ${c('Fields:', 'dim')} ${entry.driftedFields.join(', ')}`);
226
+ }
227
+ }
228
+ console.log('');
229
+
230
+ return driftEntries;
231
+ }
232
+
233
+ // ─── Command: harmony advise ──────────────────────────────────────────────────
234
+
235
+ /**
236
+ * Generate and display strategic advice.
237
+ */
238
+ async function runHarmonyAdvise(options) {
239
+ const dir = resolveDir(options);
240
+ const platformAudits = await collectPlatformAudits(dir);
241
+ const state = loadHarmonyState(dir);
242
+ const canonicalModel = state.canon || null;
243
+
244
+ const advice = generateStrategicAdvice(canonicalModel, platformAudits);
245
+
246
+ if (options.json) {
247
+ console.log(JSON.stringify(advice, null, 2));
248
+ return advice;
249
+ }
250
+
251
+ console.log('');
252
+ console.log(c(' Harmony Strategic Advisor', 'bold'));
253
+ console.log(c(' ═══════════════════════════════════════', 'dim'));
254
+ console.log('');
255
+
256
+ // Task routing
257
+ console.log(c(' Task Routing Recommendations', 'bold'));
258
+ console.log('');
259
+ for (const route of advice.taskRouting) {
260
+ const confColor = route.confidence === 'high' ? 'green' : route.confidence === 'medium' ? 'yellow' : 'dim';
261
+ console.log(` ${c(route.taskLabel.padEnd(22), 'bold')} → ${c(route.recommendedLabel, 'blue')} ${c(`[${route.confidence}]`, confColor)}`);
262
+ console.log(` ${c(route.reasoning, 'dim')}`);
263
+ if (route.alternatives.length > 0) {
264
+ const altLabels = route.alternatives.map(a => `${a.label} (${a.score})`).join(', ');
265
+ console.log(` ${c('Alternatives:', 'dim')} ${altLabels}`);
266
+ }
267
+ console.log('');
268
+ }
269
+
270
+ // Config recommendations
271
+ if (advice.configRecommendations.length > 0) {
272
+ console.log(c(' Configuration Recommendations', 'bold'));
273
+ console.log('');
274
+ for (const rec of advice.configRecommendations) {
275
+ const impactColor = rec.impact === 'high' ? 'red' : rec.impact === 'medium' ? 'yellow' : 'dim';
276
+ console.log(` ${c(`[${rec.impact.toUpperCase()}]`, impactColor)} ${rec.platform}: ${rec.recommendation}`);
277
+ }
278
+ console.log('');
279
+ }
280
+
281
+ // Cross-platform actions
282
+ if (advice.crossPlatformActions.length > 0) {
283
+ console.log(c(' Cross-Platform Actions', 'bold'));
284
+ console.log('');
285
+ for (const action of advice.crossPlatformActions) {
286
+ const prioColor = action.priority === 'high' ? 'red' : action.priority === 'medium' ? 'yellow' : 'dim';
287
+ console.log(` ${c(`[${action.priority.toUpperCase()}]`, prioColor)} ${action.action}`);
288
+ console.log(` ${c('Platforms:', 'dim')} ${action.affectedPlatforms.join(', ')}`);
289
+ }
290
+ console.log('');
291
+ }
292
+
293
+ return advice;
294
+ }
295
+
296
+ // ─── Command: harmony watch ───────────────────────────────────────────────────
297
+
298
+ /**
299
+ * Start the harmony watch loop.
300
+ */
301
+ async function runHarmonyWatch(options) {
302
+ const dir = resolveDir(options);
303
+
304
+ await startHarmonyWatch({
305
+ dir,
306
+ autoSync: !!options.autoSync,
307
+ debounceMs: options.debounce || 800,
308
+ onDriftDetected: (platform, details) => {
309
+ console.log(c(` DRIFT ALERT: ${platform} score dropped by ${Math.abs(details.delta)}`, 'red'));
310
+ },
311
+ onPlatformChange: (platform, file) => {
312
+ // Logged by watch module itself
313
+ },
314
+ runAudit: options.noAudit ? null : async (auditDir) => {
315
+ const audits = await collectPlatformAudits(auditDir);
316
+ const result = {};
317
+ for (const audit of audits) {
318
+ result[audit.platform] = audit;
319
+ }
320
+ return result;
321
+ },
322
+ });
323
+ }
324
+
325
+ // ─── Command: harmony governance ──────────────────────────────────────────────
326
+
327
+ /**
328
+ * Display the cross-platform governance summary.
329
+ */
330
+ async function runHarmonyGovernance(options) {
331
+ const dir = resolveDir(options);
332
+ const platformAudits = await collectPlatformAudits(dir);
333
+ const state = loadHarmonyState(dir);
334
+ const canonicalModel = state.canon || null;
335
+
336
+ const summary = getHarmonyGovernanceSummary(canonicalModel, platformAudits);
337
+
338
+ if (options.json) {
339
+ console.log(JSON.stringify(summary, null, 2));
340
+ return summary;
341
+ }
342
+
343
+ const report = formatHarmonyGovernanceReport(summary, options);
344
+ console.log(report);
345
+
346
+ return summary;
347
+ }
348
+
349
+ // ─── Command: harmony score ──────────────────────────────────────────────────
350
+
351
+ /**
352
+ * Output a standalone Harmony Score (0-100) with optional badge and CI threshold.
353
+ *
354
+ * Options:
355
+ * --json JSON output
356
+ * --badge Print shields.io badge markdown
357
+ * --threshold N Exit with code 1 if score < N (for CI gates)
358
+ * --quiet Score number only (for piping)
359
+ */
360
+ async function runHarmonyScore(options) {
361
+ const dir = resolveDir(options);
362
+ const { harmonyAudit } = require('./audit');
363
+ const result = await harmonyAudit({ dir, silent: true });
364
+
365
+ const score = result.harmonyScore;
366
+ const threshold = parseInt(options.threshold, 10) || 0;
367
+ const pass = score >= threshold;
368
+
369
+ if (options.json) {
370
+ const output = {
371
+ harmonyScore: score,
372
+ platforms: result.platformScores,
373
+ activePlatforms: result.activePlatforms.map(p => p.platform),
374
+ driftCount: result.drift.drifts.length,
375
+ threshold: threshold || null,
376
+ pass,
377
+ };
378
+ if (options.badge) {
379
+ output.badge = getHarmonyBadgeMarkdown(score);
380
+ output.badgeUrl = getHarmonyBadgeUrl(score);
381
+ }
382
+ console.log(JSON.stringify(output, null, 2));
383
+ return output;
384
+ }
385
+
386
+ if (options.quiet) {
387
+ console.log(score);
388
+ return { score, pass };
389
+ }
390
+
391
+ console.log('');
392
+ console.log(c(' Harmony Score', 'bold'));
393
+ console.log(c(' ═══════════════════════════════════════', 'dim'));
394
+ console.log('');
395
+
396
+ // Score with color bar
397
+ const barWidth = 30;
398
+ const filled = Math.round((score / 100) * barWidth);
399
+ const empty = barWidth - filled;
400
+ const scoreColor = score >= 80 ? 'green' : score >= 50 ? 'yellow' : 'red';
401
+ const bar = c('\u2588'.repeat(filled), scoreColor) + c('\u2591'.repeat(empty), 'dim');
402
+ console.log(` ${bar} ${c(`${score}/100`, scoreColor)}`);
403
+ console.log('');
404
+
405
+ // Per-platform breakdown
406
+ for (const ap of result.activePlatforms) {
407
+ const ps = result.platformScores[ap.platform];
408
+ const psColor = ps >= 70 ? 'green' : ps >= 40 ? 'yellow' : 'red';
409
+ console.log(` ${ap.platform.padEnd(12)} ${ps !== null ? c(`${ps}/100`, psColor) : c('n/a', 'dim')}`);
410
+ }
411
+ console.log('');
412
+
413
+ // Drift summary
414
+ const driftCount = result.drift.drifts.length;
415
+ if (driftCount > 0) {
416
+ const critical = result.drift.drifts.filter(d => d.severity === 'critical').length;
417
+ const high = result.drift.drifts.filter(d => d.severity === 'high').length;
418
+ let driftMsg = ` ${driftCount} drift issue${driftCount !== 1 ? 's' : ''}`;
419
+ if (critical > 0) driftMsg += c(` (${critical} critical)`, 'red');
420
+ else if (high > 0) driftMsg += c(` (${high} high)`, 'yellow');
421
+ console.log(driftMsg);
422
+ console.log(c(' Run "nerviq harmony-audit" for details.', 'dim'));
423
+ console.log('');
424
+ }
425
+
426
+ // Badge output
427
+ if (options.badge) {
428
+ console.log(c(' Badge:', 'bold'));
429
+ console.log(` ${getHarmonyBadgeMarkdown(score)}`);
430
+ console.log('');
431
+ }
432
+
433
+ // Threshold check
434
+ if (threshold > 0) {
435
+ if (pass) {
436
+ console.log(c(` Threshold: ${score} >= ${threshold} PASS`, 'green'));
437
+ } else {
438
+ console.log(c(` Threshold: ${score} < ${threshold} FAIL`, 'red'));
439
+ }
440
+ console.log('');
441
+ }
442
+
443
+ return { score, pass, platforms: result.platformScores };
444
+ }
445
+
446
+ // ─── Harmony Badge helpers ───────────────────────────────────────────────────
447
+
448
+ function getHarmonyBadgeUrl(score) {
449
+ const color = score >= 80 ? 'brightgreen' : score >= 60 ? 'yellow' : score >= 40 ? 'orange' : 'red';
450
+ const label = encodeURIComponent('Harmony Score');
451
+ const message = encodeURIComponent(`${score}/100`);
452
+ return `https://img.shields.io/badge/${label}-${message}-${color}`;
453
+ }
454
+
455
+ function getHarmonyBadgeMarkdown(score) {
456
+ const url = getHarmonyBadgeUrl(score);
457
+ return `[![Harmony Score](${url})](https://github.com/nerviq/nerviq)`;
458
+ }
459
+
460
+ // ─── Command: harmony demo ──────────────────────────────────────────────────
461
+
462
+ /**
463
+ * Zero-setup demo: creates a temporary multi-platform project, runs harmony
464
+ * audit on it, and shows how Nerviq detects cross-platform drift.
465
+ *
466
+ * This lets new users see Harmony's value instantly without configuring anything.
467
+ */
468
+ async function runHarmonyDemo(options) {
469
+ const fs = require('fs');
470
+ const os = require('os');
471
+ const { harmonyAudit } = require('./audit');
472
+
473
+ console.log('');
474
+ console.log(c(' Harmony Demo — Zero-Setup Cross-Platform Drift Detection', 'bold'));
475
+ console.log(c(' ═══════════════════════════════════════════════════════', 'dim'));
476
+ console.log('');
477
+ console.log(c(' Creating a sample multi-platform project...', 'dim'));
478
+ console.log('');
479
+
480
+ // Create temp directory with realistic multi-platform configs
481
+ const demoDir = path.join(os.tmpdir(), `nerviq-harmony-demo-${Date.now()}`);
482
+ fs.mkdirSync(demoDir, { recursive: true });
483
+ fs.mkdirSync(path.join(demoDir, '.claude'), { recursive: true });
484
+ fs.mkdirSync(path.join(demoDir, '.cursor'), { recursive: true });
485
+ fs.mkdirSync(path.join(demoDir, '.github'), { recursive: true });
486
+
487
+ // Claude config — well-configured
488
+ fs.writeFileSync(path.join(demoDir, 'CLAUDE.md'), [
489
+ '# Project Instructions',
490
+ '',
491
+ '## Architecture',
492
+ 'This is a Node.js API with PostgreSQL. Use Express for routing.',
493
+ '',
494
+ '## Testing',
495
+ 'Run tests with `npm test`. All PRs require passing tests.',
496
+ '',
497
+ '## Security',
498
+ '- Never commit .env files',
499
+ '- Use parameterized queries for all database access',
500
+ '- Validate all user input',
501
+ '',
502
+ '## Code Style',
503
+ '- Use ESLint with the project config',
504
+ '- Prefer async/await over callbacks',
505
+ '- Add JSDoc comments for public functions',
506
+ ].join('\n'));
507
+
508
+ fs.writeFileSync(path.join(demoDir, '.claude', 'settings.json'), JSON.stringify({
509
+ permissions: {
510
+ allow: ['Read', 'Glob', 'Grep'],
511
+ deny: ['Bash(rm -rf *)'],
512
+ },
513
+ model: 'claude-sonnet-4-6',
514
+ }, null, 2));
515
+
516
+ // Cursor config — intentionally drifted (different rules, less security)
517
+ fs.writeFileSync(path.join(demoDir, '.cursorrules'), [
518
+ 'You are a helpful coding assistant.',
519
+ 'This is a Node.js project using Express.',
520
+ 'Write clean, readable code.',
521
+ // Missing: security rules, testing rules, architecture details
522
+ ].join('\n'));
523
+
524
+ // Copilot config — partial coverage
525
+ fs.writeFileSync(path.join(demoDir, '.github', 'copilot-instructions.md'), [
526
+ '# Copilot Instructions',
527
+ '',
528
+ 'This is a Node.js Express API project.',
529
+ 'Use TypeScript-style JSDoc annotations.',
530
+ 'Follow RESTful conventions for API endpoints.',
531
+ // Missing: security, testing, architecture details
532
+ ].join('\n'));
533
+
534
+ // Add a package.json for realism
535
+ fs.writeFileSync(path.join(demoDir, 'package.json'), JSON.stringify({
536
+ name: 'harmony-demo-project',
537
+ version: '1.0.0',
538
+ scripts: { test: 'jest' },
539
+ }, null, 2));
540
+
541
+ console.log(c(' Demo project created with 3 platforms:', 'bold'));
542
+ console.log(` ${c('Claude', 'green')} — Well-configured (CLAUDE.md + settings.json)`);
543
+ console.log(` ${c('Cursor', 'yellow')} — Basic rules only (.cursorrules)`);
544
+ console.log(` ${c('Copilot', 'yellow')} — Partial coverage (copilot-instructions.md)`);
545
+ console.log('');
546
+ console.log(c(' Intentional drift injected:', 'bold'));
547
+ console.log(` ${c('\u2718', 'red')} Security rules only in Claude, missing from Cursor & Copilot`);
548
+ console.log(` ${c('\u2718', 'red')} Testing instructions only in Claude`);
549
+ console.log(` ${c('\u2718', 'red')} Architecture details inconsistent across platforms`);
550
+ console.log(` ${c('\u2718', 'red')} Trust posture differs (Claude has explicit permissions)`);
551
+ console.log('');
552
+ console.log(c(' Running Harmony Audit...', 'dim'));
553
+ console.log('');
554
+
555
+ // Run the actual harmony audit on the demo project
556
+ const result = await harmonyAudit({ dir: demoDir, silent: false, verbose: !!options.verbose });
557
+
558
+ console.log('');
559
+ console.log(c(' ═══════════════════════════════════════════════════════', 'dim'));
560
+ console.log(c(' What you just saw:', 'bold'));
561
+ console.log('');
562
+ console.log(' Nerviq Harmony detected real configuration drift between');
563
+ console.log(' 3 AI coding platforms in your project — differences in');
564
+ console.log(' instructions, security posture, and tool coverage that');
565
+ console.log(' cause inconsistent AI behavior.');
566
+ console.log('');
567
+ console.log(c(' Try it on your own project:', 'bold'));
568
+ console.log(` ${c('npx @nerviq/cli harmony-audit', 'blue')}`);
569
+ console.log(` ${c('npx @nerviq/cli harmony-score --threshold 70', 'blue')}`);
570
+ console.log('');
571
+
572
+ // Clean up
573
+ try {
574
+ fs.rmSync(demoDir, { recursive: true, force: true });
575
+ } catch (_e) { /* cleanup optional */ }
576
+
577
+ return result;
578
+ }
579
+
580
+ module.exports = {
581
+ runHarmonyAudit,
582
+ runHarmonySync,
583
+ runHarmonyDrift,
584
+ runHarmonyAdvise,
585
+ runHarmonyWatch,
586
+ runHarmonyGovernance,
587
+ runHarmonyScore,
588
+ runHarmonyDemo,
589
+ getHarmonyBadgeUrl,
590
+ getHarmonyBadgeMarkdown,
591
+ };