@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.
- 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,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
|
+
};
|