@nerviq/cli 1.29.0 → 1.29.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1527 -1493
- package/README.md +550 -538
- package/SECURITY.md +82 -82
- package/bin/cli.js +2562 -2558
- package/docs/api-reference.md +356 -356
- package/docs/audit-fix.md +109 -0
- package/docs/autofix.md +3 -62
- package/docs/getting-started.md +1 -1
- package/docs/index.html +592 -592
- package/docs/integration-contracts.md +287 -287
- package/docs/maintenance.md +128 -128
- package/docs/new-platform-guide.md +202 -202
- package/docs/release-process.md +63 -0
- package/docs/shallow-risk.md +244 -244
- package/docs/why-nerviq.md +82 -82
- package/package.json +67 -67
- package/src/aider/activity.js +226 -226
- package/src/aider/context.js +162 -162
- package/src/aider/freshness.js +123 -123
- package/src/aider/techniques.js +3465 -3465
- package/src/audit/layers.js +180 -180
- package/src/audit.js +1032 -1032
- package/src/benchmark.js +299 -299
- package/src/codex/activity.js +324 -324
- package/src/codex/freshness.js +142 -142
- package/src/codex/techniques.js +4895 -4895
- package/src/context.js +326 -326
- package/src/continuous-ops.js +11 -1
- package/src/convert.js +340 -340
- package/src/copilot/config-parser.js +280 -280
- package/src/copilot/context.js +218 -218
- package/src/copilot/freshness.js +177 -177
- package/src/copilot/patch.js +238 -238
- package/src/copilot/techniques.js +3578 -3578
- package/src/cursor/freshness.js +194 -194
- package/src/cursor/patch.js +243 -243
- package/src/cursor/techniques.js +3735 -3735
- package/src/doctor.js +201 -201
- package/src/fix-engine.js +511 -8
- package/src/formatters/csv.js +86 -86
- package/src/formatters/junit.js +123 -123
- package/src/formatters/markdown.js +164 -164
- package/src/formatters/otel.js +151 -151
- package/src/freshness.js +156 -156
- package/src/gemini/activity.js +402 -402
- package/src/gemini/context.js +290 -290
- package/src/gemini/freshness.js +183 -183
- package/src/gemini/patch.js +229 -229
- package/src/gemini/techniques.js +3811 -3811
- package/src/governance.js +533 -533
- package/src/harmony/audit.js +306 -306
- package/src/i18n.js +63 -63
- package/src/insights.js +119 -119
- package/src/integrations.js +134 -134
- package/src/locales/en.json +33 -33
- package/src/locales/es.json +33 -33
- package/src/migrate.js +354 -354
- package/src/opencode/activity.js +286 -286
- package/src/opencode/freshness.js +137 -137
- package/src/opencode/techniques.js +3450 -3450
- package/src/setup/analysis.js +12 -12
- package/src/setup.js +7 -6
- package/src/shallow-risk/index.js +56 -56
- package/src/shallow-risk/patterns/agent-config-cross-platform-drift.js +50 -50
- package/src/shallow-risk/patterns/agent-config-dangerous-autoapprove.js +46 -46
- package/src/shallow-risk/patterns/agent-config-deprecated-keys.js +46 -46
- package/src/shallow-risk/patterns/agent-config-missing-file.js +317 -317
- package/src/shallow-risk/patterns/agent-config-secret-literal.js +49 -49
- package/src/shallow-risk/patterns/agent-config-stack-contradiction.js +34 -34
- package/src/shallow-risk/patterns/hook-script-missing.js +70 -70
- package/src/shallow-risk/patterns/mcp-server-no-allowlist.js +52 -52
- package/src/shallow-risk/shared.js +648 -648
- package/src/source-urls.js +295 -295
- package/src/state-paths.js +85 -85
- package/src/supplemental-checks.js +805 -805
- package/src/telemetry.js +160 -160
- package/src/windsurf/context.js +359 -359
- package/src/windsurf/freshness.js +194 -194
- package/src/windsurf/patch.js +231 -231
- package/src/windsurf/techniques.js +3779 -3779
package/src/gemini/patch.js
CHANGED
|
@@ -1,229 +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 = '_nerviq_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 _nerviq_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 + '.nerviq-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
|
-
};
|
|
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 = '_nerviq_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 _nerviq_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 + '.nerviq-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
|
+
};
|