@nerviq/cli 0.0.1 → 0.9.0-beta.2

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 (148) hide show
  1. package/CHANGELOG.md +181 -0
  2. package/LICENSE +21 -0
  3. package/README.md +447 -0
  4. package/bin/cli.js +749 -0
  5. package/content/case-study-template.md +91 -0
  6. package/content/claims-governance.md +37 -0
  7. package/content/claude-code/audit-repo/SKILL.md +20 -0
  8. package/content/claude-native-integration.md +60 -0
  9. package/content/devto-article.json +9 -0
  10. package/content/launch-posts.md +226 -0
  11. package/content/pilot-rollout-kit.md +30 -0
  12. package/content/release-checklist.md +31 -0
  13. package/package.json +53 -4
  14. package/src/activity.js +529 -0
  15. package/src/aider/activity.js +226 -0
  16. package/src/aider/config-parser.js +166 -0
  17. package/src/aider/context.js +158 -0
  18. package/src/aider/deep-review.js +316 -0
  19. package/src/aider/domain-packs.js +278 -0
  20. package/src/aider/freshness.js +168 -0
  21. package/src/aider/governance.js +253 -0
  22. package/src/aider/interactive.js +334 -0
  23. package/src/aider/mcp-packs.js +98 -0
  24. package/src/aider/patch.js +214 -0
  25. package/src/aider/plans.js +186 -0
  26. package/src/aider/premium.js +360 -0
  27. package/src/aider/setup.js +404 -0
  28. package/src/aider/techniques.js +1323 -0
  29. package/src/analyze.js +821 -0
  30. package/src/audit.js +1003 -0
  31. package/src/badge.js +13 -0
  32. package/src/benchmark.js +339 -0
  33. package/src/claudex-sync.json +7 -0
  34. package/src/codex/activity.js +324 -0
  35. package/src/codex/config-parser.js +183 -0
  36. package/src/codex/context.js +221 -0
  37. package/src/codex/deep-review.js +493 -0
  38. package/src/codex/domain-packs.js +372 -0
  39. package/src/codex/freshness.js +167 -0
  40. package/src/codex/governance.js +192 -0
  41. package/src/codex/interactive.js +618 -0
  42. package/src/codex/mcp-packs.js +660 -0
  43. package/src/codex/patch.js +209 -0
  44. package/src/codex/plans.js +251 -0
  45. package/src/codex/premium.js +614 -0
  46. package/src/codex/setup.js +603 -0
  47. package/src/codex/techniques.js +2649 -0
  48. package/src/context.js +272 -0
  49. package/src/copilot/activity.js +309 -0
  50. package/src/copilot/config-parser.js +226 -0
  51. package/src/copilot/context.js +197 -0
  52. package/src/copilot/deep-review.js +346 -0
  53. package/src/copilot/domain-packs.js +350 -0
  54. package/src/copilot/freshness.js +197 -0
  55. package/src/copilot/governance.js +222 -0
  56. package/src/copilot/interactive.js +406 -0
  57. package/src/copilot/mcp-packs.js +572 -0
  58. package/src/copilot/patch.js +238 -0
  59. package/src/copilot/plans.js +253 -0
  60. package/src/copilot/premium.js +450 -0
  61. package/src/copilot/setup.js +488 -0
  62. package/src/copilot/techniques.js +1822 -0
  63. package/src/cursor/activity.js +301 -0
  64. package/src/cursor/config-parser.js +265 -0
  65. package/src/cursor/context.js +236 -0
  66. package/src/cursor/deep-review.js +334 -0
  67. package/src/cursor/domain-packs.js +346 -0
  68. package/src/cursor/freshness.js +214 -0
  69. package/src/cursor/governance.js +229 -0
  70. package/src/cursor/interactive.js +391 -0
  71. package/src/cursor/mcp-packs.js +571 -0
  72. package/src/cursor/patch.js +243 -0
  73. package/src/cursor/plans.js +254 -0
  74. package/src/cursor/premium.js +468 -0
  75. package/src/cursor/setup.js +488 -0
  76. package/src/cursor/techniques.js +1786 -0
  77. package/src/deep-review.js +345 -0
  78. package/src/domain-packs.js +364 -0
  79. package/src/formatters/sarif.js +115 -0
  80. package/src/gemini/activity.js +402 -0
  81. package/src/gemini/config-parser.js +275 -0
  82. package/src/gemini/context.js +221 -0
  83. package/src/gemini/deep-review.js +559 -0
  84. package/src/gemini/domain-packs.js +371 -0
  85. package/src/gemini/freshness.js +204 -0
  86. package/src/gemini/governance.js +201 -0
  87. package/src/gemini/interactive.js +860 -0
  88. package/src/gemini/mcp-packs.js +658 -0
  89. package/src/gemini/patch.js +229 -0
  90. package/src/gemini/plans.js +269 -0
  91. package/src/gemini/premium.js +759 -0
  92. package/src/gemini/setup.js +692 -0
  93. package/src/gemini/techniques.js +2084 -0
  94. package/src/governance.js +523 -0
  95. package/src/harmony/advisor.js +383 -0
  96. package/src/harmony/audit.js +303 -0
  97. package/src/harmony/canon.js +444 -0
  98. package/src/harmony/cli.js +331 -0
  99. package/src/harmony/drift.js +401 -0
  100. package/src/harmony/governance.js +313 -0
  101. package/src/harmony/memory.js +238 -0
  102. package/src/harmony/sync.js +458 -0
  103. package/src/harmony/watch.js +336 -0
  104. package/src/index.js +256 -0
  105. package/src/insights.js +119 -0
  106. package/src/interactive.js +118 -0
  107. package/src/mcp-packs.js +597 -0
  108. package/src/opencode/activity.js +286 -0
  109. package/src/opencode/config-parser.js +109 -0
  110. package/src/opencode/context.js +247 -0
  111. package/src/opencode/deep-review.js +313 -0
  112. package/src/opencode/domain-packs.js +240 -0
  113. package/src/opencode/freshness.js +158 -0
  114. package/src/opencode/governance.js +159 -0
  115. package/src/opencode/interactive.js +392 -0
  116. package/src/opencode/mcp-packs.js +474 -0
  117. package/src/opencode/patch.js +184 -0
  118. package/src/opencode/plans.js +231 -0
  119. package/src/opencode/premium.js +413 -0
  120. package/src/opencode/setup.js +449 -0
  121. package/src/opencode/techniques.js +1713 -0
  122. package/src/plans.js +655 -0
  123. package/src/secret-patterns.js +30 -0
  124. package/src/setup.js +1274 -0
  125. package/src/synergy/adaptive.js +261 -0
  126. package/src/synergy/compensation.js +156 -0
  127. package/src/synergy/evidence.js +193 -0
  128. package/src/synergy/learning.js +184 -0
  129. package/src/synergy/patterns.js +227 -0
  130. package/src/synergy/ranking.js +83 -0
  131. package/src/synergy/report.js +163 -0
  132. package/src/synergy/routing.js +152 -0
  133. package/src/techniques.js +1354 -0
  134. package/src/watch.js +229 -0
  135. package/src/windsurf/activity.js +302 -0
  136. package/src/windsurf/config-parser.js +267 -0
  137. package/src/windsurf/context.js +249 -0
  138. package/src/windsurf/deep-review.js +337 -0
  139. package/src/windsurf/domain-packs.js +348 -0
  140. package/src/windsurf/freshness.js +215 -0
  141. package/src/windsurf/governance.js +231 -0
  142. package/src/windsurf/interactive.js +388 -0
  143. package/src/windsurf/mcp-packs.js +535 -0
  144. package/src/windsurf/patch.js +231 -0
  145. package/src/windsurf/plans.js +247 -0
  146. package/src/windsurf/premium.js +467 -0
  147. package/src/windsurf/setup.js +471 -0
  148. package/src/windsurf/techniques.js +1758 -0
@@ -0,0 +1,229 @@
1
+ /**
2
+ * Gemini Patch Intelligence
3
+ *
4
+ * Safe patching of existing Gemini CLI files using managed blocks.
5
+ * Supports GEMINI.md (HTML comment blocks) and settings.json (JSON merge).
6
+ *
7
+ * Managed blocks are sections that nerviq controls.
8
+ * Hand-authored content outside managed blocks is preserved.
9
+ */
10
+
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+ const { writeRollbackArtifact, writeActivityArtifact } = require('../activity');
14
+
15
+ // Managed block markers
16
+ const MANAGED_START_MD = '<!-- nerviq:managed:start -->';
17
+ const MANAGED_END_MD = '<!-- nerviq:managed:end -->';
18
+ const MANAGED_JSON_KEY = '_claudex_managed';
19
+
20
+ /**
21
+ * Extract managed blocks from a file.
22
+ * Returns { before, managed, after } where managed is the content between markers.
23
+ */
24
+ function extractManagedBlock(content, startMarker, endMarker) {
25
+ const startIdx = content.indexOf(startMarker);
26
+ const endIdx = content.indexOf(endMarker);
27
+
28
+ if (startIdx === -1 || endIdx === -1 || endIdx <= startIdx) {
29
+ return { before: content, managed: null, after: '' };
30
+ }
31
+
32
+ return {
33
+ before: content.substring(0, startIdx),
34
+ managed: content.substring(startIdx + startMarker.length, endIdx).trim(),
35
+ after: content.substring(endIdx + endMarker.length),
36
+ };
37
+ }
38
+
39
+ /**
40
+ * Replace or insert a managed block in a file.
41
+ * If the file already has managed markers, replace the content between them.
42
+ * If not, append the managed block at the end.
43
+ */
44
+ function upsertManagedBlock(content, newManaged, startMarker, endMarker) {
45
+ const { before, managed, after } = extractManagedBlock(content, startMarker, endMarker);
46
+
47
+ if (managed !== null) {
48
+ // Replace existing managed block
49
+ return `${before}${startMarker}\n${newManaged}\n${endMarker}${after}`;
50
+ }
51
+
52
+ // Append new managed block
53
+ const separator = content.endsWith('\n') ? '\n' : '\n\n';
54
+ return `${content}${separator}${startMarker}\n${newManaged}\n${endMarker}\n`;
55
+ }
56
+
57
+ /**
58
+ * Patch GEMINI.md with managed sections.
59
+ * Preserves all hand-authored content.
60
+ */
61
+ function patchGeminiMd(existingContent, managedSections) {
62
+ const newManaged = Object.entries(managedSections)
63
+ .map(([section, content]) => `## ${section}\n${content}`)
64
+ .join('\n\n');
65
+
66
+ return upsertManagedBlock(existingContent, newManaged, MANAGED_START_MD, MANAGED_END_MD);
67
+ }
68
+
69
+ /**
70
+ * Patch settings.json by safely merging new keys.
71
+ * Preserves all existing keys. Only adds new keys or updates
72
+ * the _claudex_managed namespace without breaking existing config.
73
+ */
74
+ function patchSettingsJson(existingContent, newKeys) {
75
+ let existing;
76
+ try {
77
+ existing = JSON.parse(existingContent);
78
+ } catch {
79
+ existing = {};
80
+ }
81
+
82
+ // Merge new keys without overwriting existing non-managed keys
83
+ const merged = { ...existing };
84
+
85
+ for (const [key, value] of Object.entries(newKeys)) {
86
+ if (key === MANAGED_JSON_KEY) {
87
+ // Managed namespace: always overwrite with latest
88
+ merged[MANAGED_JSON_KEY] = {
89
+ ...(existing[MANAGED_JSON_KEY] || {}),
90
+ ...value,
91
+ };
92
+ } else if (!(key in existing)) {
93
+ // Only add keys that don't already exist
94
+ merged[key] = value;
95
+ }
96
+ }
97
+
98
+ // Ensure managed key has metadata
99
+ if (!merged[MANAGED_JSON_KEY]) {
100
+ merged[MANAGED_JSON_KEY] = {};
101
+ }
102
+ merged[MANAGED_JSON_KEY]._updatedAt = new Date().toISOString();
103
+ merged[MANAGED_JSON_KEY]._generator = nerviq;
104
+
105
+ return JSON.stringify(merged, null, 2) + '\n';
106
+ }
107
+
108
+ /**
109
+ * Detect if a repo has multiple agent surfaces (Gemini + Claude + Codex coexistence).
110
+ */
111
+ function detectMixedAgentRepo(dir) {
112
+ const hasClaude = fs.existsSync(path.join(dir, 'CLAUDE.md')) ||
113
+ fs.existsSync(path.join(dir, '.claude'));
114
+ const hasCodex = fs.existsSync(path.join(dir, 'AGENTS.md')) ||
115
+ fs.existsSync(path.join(dir, '.codex'));
116
+ const hasGemini = fs.existsSync(path.join(dir, 'GEMINI.md')) ||
117
+ fs.existsSync(path.join(dir, '.gemini'));
118
+
119
+ const platforms = [];
120
+ if (hasClaude) platforms.push('claude');
121
+ if (hasCodex) platforms.push('codex');
122
+ if (hasGemini) platforms.push('gemini');
123
+
124
+ return {
125
+ isMixed: platforms.length >= 2,
126
+ hasClaude,
127
+ hasCodex,
128
+ hasGemini,
129
+ platforms,
130
+ guidance: platforms.length >= 2
131
+ ? `This is a mixed-agent repo (${platforms.join(', ')}). Keep each platform's instructions in its own file (CLAUDE.md, AGENTS.md, GEMINI.md). Do not merge them.`
132
+ : null,
133
+ };
134
+ }
135
+
136
+ /**
137
+ * Generate a diff preview for a patch operation.
138
+ */
139
+ function generatePatchPreview(originalContent, patchedContent, filePath) {
140
+ const origLines = originalContent.split('\n');
141
+ const patchLines = patchedContent.split('\n');
142
+
143
+ const lines = [`--- ${filePath} (original)`, `+++ ${filePath} (patched)`];
144
+
145
+ // Simple line-by-line diff showing only changed sections
146
+ let inChange = false;
147
+ for (let i = 0; i < Math.max(origLines.length, patchLines.length); i++) {
148
+ const orig = origLines[i] || '';
149
+ const patched = patchLines[i] || '';
150
+ if (orig !== patched) {
151
+ if (!inChange) {
152
+ lines.push(`@@ line ${i + 1} @@`);
153
+ inChange = true;
154
+ }
155
+ if (i < origLines.length) lines.push(`-${orig}`);
156
+ if (i < patchLines.length) lines.push(`+${patched}`);
157
+ } else {
158
+ inChange = false;
159
+ }
160
+ }
161
+
162
+ return lines.join('\n');
163
+ }
164
+
165
+ /**
166
+ * Apply a patch to a file with backup and rollback support.
167
+ */
168
+ function applyPatch(dir, filePath, patchFn, options = {}) {
169
+ const fullPath = path.join(dir, filePath);
170
+ const dryRun = options.dryRun === true;
171
+
172
+ if (!fs.existsSync(fullPath)) {
173
+ return { success: false, reason: `${filePath} does not exist`, preview: null };
174
+ }
175
+
176
+ const original = fs.readFileSync(fullPath, 'utf8');
177
+ const patched = patchFn(original);
178
+
179
+ if (patched === original) {
180
+ return { success: true, reason: 'no changes needed', preview: null, unchanged: true };
181
+ }
182
+
183
+ const preview = generatePatchPreview(original, patched, filePath);
184
+
185
+ if (dryRun) {
186
+ return { success: true, reason: 'dry run', preview, unchanged: false };
187
+ }
188
+
189
+ // Backup + write
190
+ const backupPath = fullPath + '.claudex-backup';
191
+ fs.writeFileSync(backupPath, original, 'utf8');
192
+ fs.writeFileSync(fullPath, patched, 'utf8');
193
+
194
+ // Rollback artifact
195
+ const rollback = writeRollbackArtifact(dir, {
196
+ sourcePlan: 'gemini-patch',
197
+ patchedFiles: [filePath],
198
+ backupFiles: [{ original: filePath, backup: path.relative(dir, backupPath) }],
199
+ rollbackInstructions: [`Restore ${filePath} from ${path.relative(dir, backupPath)}`],
200
+ });
201
+
202
+ const activity = writeActivityArtifact(dir, 'gemini-patch', {
203
+ platform: 'gemini',
204
+ patchedFiles: [filePath],
205
+ rollbackArtifact: rollback.relativePath,
206
+ });
207
+
208
+ return {
209
+ success: true,
210
+ reason: 'patched',
211
+ preview,
212
+ unchanged: false,
213
+ rollbackArtifact: rollback.relativePath,
214
+ activityArtifact: activity.relativePath,
215
+ };
216
+ }
217
+
218
+ module.exports = {
219
+ MANAGED_START_MD,
220
+ MANAGED_END_MD,
221
+ MANAGED_JSON_KEY,
222
+ extractManagedBlock,
223
+ upsertManagedBlock,
224
+ patchGeminiMd,
225
+ patchSettingsJson,
226
+ detectMixedAgentRepo,
227
+ generatePatchPreview,
228
+ applyPatch,
229
+ };
@@ -0,0 +1,269 @@
1
+ const path = require('path');
2
+ const { version } = require('../../package.json');
3
+ const { audit } = require('../audit');
4
+ const { analyzeProject } = require('../analyze');
5
+ const { buildGeminiSetupFiles } = require('./setup');
6
+ const { getGeminiMcpPreflight } = require('./mcp-packs');
7
+
8
+ function maturityFromScore(score) {
9
+ if (score >= 81) return 'mature';
10
+ if (score >= 61) return 'solid';
11
+ if (score >= 41) return 'developing';
12
+ if (score >= 21) return 'weak';
13
+ return 'raw';
14
+ }
15
+
16
+ function triggerMatchesFile(result, filePath) {
17
+ if (filePath === 'GEMINI.md') {
18
+ return result.file === 'GEMINI.md' || result.category === 'instructions' || result.category === 'review' || result.category === 'quality-deep';
19
+ }
20
+ if (filePath === '.gemini/settings.json') {
21
+ return result.file === '.gemini/settings.json' || ['config', 'trust', 'hooks', 'mcp', 'agents', 'automation', 'local'].includes(result.category);
22
+ }
23
+ if (filePath === '.gemini/settings.json (hooks append)') {
24
+ return result.category === 'hooks';
25
+ }
26
+ if (filePath === '.gemini/commands/README.md') {
27
+ return result.category === 'commands';
28
+ }
29
+ if (filePath === '.gemini/agents/README.md') {
30
+ return result.category === 'agents';
31
+ }
32
+ if (filePath === '.gemini/skills/README.md') {
33
+ return result.category === 'skills';
34
+ }
35
+ if (filePath === '.gemini/policy/README.md') {
36
+ return result.category === 'policy';
37
+ }
38
+ if (filePath === '.gemini/settings.json (MCP append)') {
39
+ return result.category === 'mcp';
40
+ }
41
+ if (filePath === '.github/workflows/gemini-review.yml') {
42
+ return result.category === 'automation' || result.category === 'review';
43
+ }
44
+ return result.file === filePath;
45
+ }
46
+
47
+ function uniqueValues(items = []) {
48
+ return [...new Set(items.filter(Boolean))];
49
+ }
50
+
51
+ function buildDomainPackGuidance(report) {
52
+ return (report.recommendedDomainPacks || []).map((pack) => ({
53
+ key: pack.key,
54
+ label: pack.label,
55
+ useWhen: pack.useWhen,
56
+ matchReasons: pack.matchReasons || [],
57
+ recommendedModules: pack.recommendedModules || [],
58
+ recommendedProposalFamilies: pack.recommendedProposalFamilies || [],
59
+ recommendedSurfaces: pack.recommendedSurfaces || [],
60
+ benchmarkFocus: pack.benchmarkFocus || [],
61
+ }));
62
+ }
63
+
64
+ function selectPackContext(filePath, domainPackGuidance = []) {
65
+ return domainPackGuidance
66
+ .filter((pack) => {
67
+ if (!Array.isArray(pack.recommendedSurfaces) || pack.recommendedSurfaces.length === 0) {
68
+ return true;
69
+ }
70
+ return pack.recommendedSurfaces.some((surface) => filePath === surface || filePath.startsWith(surface));
71
+ })
72
+ .map((pack) => ({
73
+ key: pack.key,
74
+ label: pack.label,
75
+ why: pack.matchReasons[0] || pack.useWhen,
76
+ recommendedModules: (pack.recommendedModules || []).slice(0, 3),
77
+ recommendedProposalFamilies: (pack.recommendedProposalFamilies || []).slice(0, 3),
78
+ benchmarkFocus: (pack.benchmarkFocus || []).slice(0, 2),
79
+ }));
80
+ }
81
+
82
+ const PROPOSAL_FAMILIES = {
83
+ 'GEMINI.md': {
84
+ id: 'gemini-md',
85
+ title: 'Create Gemini CLI GEMINI.md baseline',
86
+ module: 'instructions',
87
+ risk: 'low',
88
+ confidence: 'high',
89
+ },
90
+ '.gemini/settings.json': {
91
+ id: 'gemini-settings',
92
+ title: 'Create Gemini CLI settings.json baseline',
93
+ module: 'config',
94
+ risk: 'medium',
95
+ confidence: 'high',
96
+ },
97
+ '.gemini/settings.json (hooks append)': {
98
+ id: 'gemini-hooks',
99
+ title: 'Add Gemini CLI hooks scaffold to settings.json',
100
+ module: 'hooks',
101
+ risk: 'medium',
102
+ confidence: 'medium',
103
+ },
104
+ '.gemini/commands/README.md': {
105
+ id: 'gemini-commands',
106
+ title: 'Create Gemini CLI commands starter',
107
+ module: 'commands',
108
+ risk: 'low',
109
+ confidence: 'high',
110
+ },
111
+ '.gemini/agents/README.md': {
112
+ id: 'gemini-agents',
113
+ title: 'Create Gemini CLI agents starter',
114
+ module: 'agents',
115
+ risk: 'low',
116
+ confidence: 'medium',
117
+ },
118
+ '.gemini/skills/README.md': {
119
+ id: 'gemini-skills',
120
+ title: 'Create Gemini CLI skills starter',
121
+ module: 'skills',
122
+ risk: 'low',
123
+ confidence: 'high',
124
+ },
125
+ '.gemini/policy/README.md': {
126
+ id: 'gemini-policy',
127
+ title: 'Create Gemini CLI policy starter',
128
+ module: 'policy',
129
+ risk: 'low',
130
+ confidence: 'high',
131
+ },
132
+ '.gemini/settings.json (MCP append)': {
133
+ id: 'gemini-mcp',
134
+ title: 'Add recommended MCP packs to Gemini CLI settings',
135
+ module: 'mcp',
136
+ risk: 'medium',
137
+ confidence: 'high',
138
+ },
139
+ '.github/workflows/gemini-review.yml': {
140
+ id: 'gemini-ci-review',
141
+ title: 'Create Gemini CLI CI review workflow',
142
+ module: 'ci',
143
+ risk: 'medium',
144
+ confidence: 'medium',
145
+ },
146
+ };
147
+
148
+ function resolveProposalFamily(file) {
149
+ const familyFromFile = file.family
150
+ ? Object.values(PROPOSAL_FAMILIES).find(f => f.id === file.family)
151
+ : null;
152
+ return familyFromFile || PROPOSAL_FAMILIES[file.path] || {
153
+ id: 'gemini-unknown',
154
+ title: `Create ${file.path}`,
155
+ module: 'unknown',
156
+ risk: 'medium',
157
+ confidence: 'low',
158
+ };
159
+ }
160
+
161
+ function proposalForFile(file, auditResult, domainPackGuidance = []) {
162
+ const triggers = auditResult.results
163
+ .filter((result) => result.passed === false && triggerMatchesFile(result, file.path))
164
+ .sort((a, b) => {
165
+ const weight = { critical: 3, high: 2, medium: 1, low: 0 };
166
+ return (weight[b.impact] || 0) - (weight[a.impact] || 0);
167
+ })
168
+ .slice(0, 6)
169
+ .map((result) => ({
170
+ key: result.key,
171
+ name: result.name,
172
+ impact: result.impact,
173
+ fix: result.fix,
174
+ }));
175
+ const packContext = selectPackContext(file.path, domainPackGuidance);
176
+
177
+ const familyMeta = resolveProposalFamily(file);
178
+
179
+ return {
180
+ id: familyMeta.id,
181
+ title: familyMeta.title,
182
+ module: familyMeta.module,
183
+ risk: familyMeta.risk,
184
+ confidence: familyMeta.confidence,
185
+ triggers,
186
+ rationale: uniqueValues([
187
+ ...triggers.map((item) => item.fix),
188
+ ...packContext.map((item) => `Supports ${item.label} rollout guidance: ${item.why}`),
189
+ ]),
190
+ packContext,
191
+ files: [{
192
+ path: file.path,
193
+ action: file.action,
194
+ currentState: file.currentState,
195
+ proposedState: file.proposedState,
196
+ content: file.content,
197
+ preview: file.content.split('\n').slice(0, 12).join('\n'),
198
+ diffPreview: [
199
+ `--- missing`,
200
+ `+++ ${file.path}`,
201
+ ...file.content.split('\n').slice(0, 12).map((line) => `+${line}`),
202
+ ].join('\n'),
203
+ }],
204
+ readyToApply: true,
205
+ };
206
+ }
207
+
208
+ async function buildGeminiProposalBundle(options) {
209
+ const auditResult = await audit({ ...options, platform: 'gemini', silent: true });
210
+ const analysisReport = await analyzeProject({ ...options, platform: 'gemini', mode: 'suggest-only' });
211
+ const domainPackGuidance = buildDomainPackGuidance(analysisReport);
212
+ const { files } = buildGeminiSetupFiles(options);
213
+ const proposals = files.map((file) => proposalForFile(file, auditResult, domainPackGuidance));
214
+
215
+ // MCP preflight warnings for any MCP proposals
216
+ const mcpProposal = proposals.find(p => p.id === 'gemini-mcp');
217
+ let mcpPreflightWarnings = [];
218
+ if (mcpProposal) {
219
+ const mcpFile = files.find(f => f.family === 'gemini-mcp');
220
+ if (mcpFile && mcpFile.content) {
221
+ // Parse JSON content to extract server keys
222
+ let detectedKeys = [];
223
+ try {
224
+ const parsed = JSON.parse(mcpFile.content);
225
+ detectedKeys = Object.keys(parsed);
226
+ } catch {
227
+ // Fallback: try regex for serverName patterns
228
+ const keyMatches = mcpFile.content.match(/"([^"]+)"\s*:/g) || [];
229
+ detectedKeys = keyMatches.map(m => m.replace(/[":]/g, '').trim());
230
+ }
231
+ mcpPreflightWarnings = getGeminiMcpPreflight(detectedKeys)
232
+ .filter(p => !p.safe)
233
+ .map(p => ({ key: p.key, label: p.label, warning: p.warning }));
234
+ }
235
+ }
236
+
237
+ return {
238
+ schemaVersion: 2,
239
+ generatedBy: `nerviq@${version}`,
240
+ createdAt: new Date().toISOString(),
241
+ platform: 'gemini',
242
+ directory: options.dir,
243
+ projectSummary: {
244
+ name: path.basename(options.dir),
245
+ score: auditResult.score,
246
+ organicScore: auditResult.organicScore,
247
+ maturity: maturityFromScore(auditResult.score),
248
+ domains: analysisReport.projectSummary.domains || [],
249
+ },
250
+ strengthsPreserved: auditResult.results
251
+ .filter((item) => item.passed === true)
252
+ .slice(0, 5)
253
+ .map((item) => item.name),
254
+ topNextActions: auditResult.topNextActions,
255
+ recommendedDomainPacks: domainPackGuidance,
256
+ proposalFamilies: [...new Set(proposals.map(p => p.id))],
257
+ optionalModules: analysisReport.optionalModules || [],
258
+ riskNotes: uniqueValues([
259
+ ...(analysisReport.riskNotes || []),
260
+ ...((auditResult.platformCaveats || []).map((item) => item.message)),
261
+ ]),
262
+ mcpPreflightWarnings,
263
+ proposals,
264
+ };
265
+ }
266
+
267
+ module.exports = {
268
+ buildGeminiProposalBundle,
269
+ };