@nerviq/cli 0.0.1 → 0.9.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
package/src/plans.js ADDED
@@ -0,0 +1,655 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ const { version } = require('../package.json');
5
+ const { analyzeProject } = require('./analyze');
6
+ const { ProjectContext } = require('./context');
7
+ const { TECHNIQUES, STACKS } = require('./techniques');
8
+ const { TEMPLATES } = require('./setup');
9
+ const { buildSettingsForProfile } = require('./governance');
10
+ const { getMcpPackPreflight } = require('./mcp-packs');
11
+ const { writeActivityArtifact, writeRollbackArtifact } = require('./activity');
12
+ const { buildCodexProposalBundle } = require('./codex/plans');
13
+
14
+ const TEMPLATE_DIR_MAP = {
15
+ hooks: '.claude/hooks',
16
+ commands: '.claude/commands',
17
+ skills: '.claude/skills',
18
+ rules: '.claude/rules',
19
+ agents: '.claude/agents',
20
+ };
21
+
22
+ const TEMPLATE_LABELS = {
23
+ 'claude-md': 'CLAUDE.md baseline',
24
+ hooks: 'Hooks bundle',
25
+ commands: 'Slash commands',
26
+ skills: 'Skills pack',
27
+ rules: 'Rules pack',
28
+ agents: 'Specialized agents',
29
+ };
30
+
31
+ const TEMPLATE_MODULES = {
32
+ 'claude-md': 'CLAUDE.md',
33
+ hooks: 'hooks',
34
+ commands: 'commands',
35
+ skills: 'skills',
36
+ rules: 'rules',
37
+ agents: 'agents',
38
+ };
39
+
40
+ const IMPACT_ORDER = { critical: 3, high: 2, medium: 1, low: 0 };
41
+ const FALLBACK_TEMPLATE_BY_KEY = {
42
+ importSyntax: 'claude-md',
43
+ verificationLoop: 'claude-md',
44
+ testCommand: 'claude-md',
45
+ lintCommand: 'claude-md',
46
+ buildCommand: 'claude-md',
47
+ securityReview: 'claude-md',
48
+ compactionAwareness: 'claude-md',
49
+ contextManagement: 'claude-md',
50
+ xmlTags: 'claude-md',
51
+ roleDefinition: 'claude-md',
52
+ constraintBlocks: 'claude-md',
53
+ claudeMdFreshness: 'claude-md',
54
+ permissionDeny: 'hooks',
55
+ secretsProtection: 'hooks',
56
+ preToolUseHook: 'hooks',
57
+ postToolUseHook: 'hooks',
58
+ sessionStartHook: 'hooks',
59
+ agentsHaveMaxTurns: 'agents',
60
+ };
61
+
62
+ function previewContent(content) {
63
+ return content.split('\n').slice(0, 12).join('\n');
64
+ }
65
+
66
+ function riskFromImpact(impact) {
67
+ if (impact === 'critical') return 'high';
68
+ if (impact === 'high') return 'medium';
69
+ return 'low';
70
+ }
71
+
72
+ function normalizeNewlines(content) {
73
+ return (content || '').replace(/\r\n/g, '\n');
74
+ }
75
+
76
+ function ensureTrailingNewline(content) {
77
+ const normalized = normalizeNewlines(content);
78
+ return normalized.endsWith('\n') ? normalized : `${normalized}\n`;
79
+ }
80
+
81
+ function upsertManagedBlock(content, id, block) {
82
+ const start = `<!-- nerviq:${id}:start -->`;
83
+ const end = `<!-- nerviq:${id}:end -->`;
84
+ const wrapped = `${start}\n${block.trim()}\n${end}`;
85
+ const pattern = new RegExp(`${start}[\\s\\S]*?${end}`);
86
+
87
+ if (pattern.test(content)) {
88
+ return {
89
+ changed: true,
90
+ content: content.replace(pattern, wrapped),
91
+ };
92
+ }
93
+
94
+ return {
95
+ changed: true,
96
+ content: `${content.trimEnd()}\n\n${wrapped}\n`,
97
+ };
98
+ }
99
+
100
+ function extractGeneratedBuildSection(content) {
101
+ const match = content.match(/## Build & Test\n([\s\S]*?)\n\n## Working Notes/);
102
+ return match ? `## Build & Test\n${match[1].trim()}` : null;
103
+ }
104
+
105
+ function extractGeneratedVerificationBlock(content) {
106
+ const match = content.match(/<verification>\n([\s\S]*?)\n<\/verification>/);
107
+ return match ? `<verification>\n${match[1].trim()}\n</verification>` : null;
108
+ }
109
+
110
+ function extractGeneratedWorkingNotes(content) {
111
+ const match = content.match(/## Working Notes\n([\s\S]*?)\n\n<constraints>/);
112
+ return match ? `## Working Notes\n${match[1].trim()}` : null;
113
+ }
114
+
115
+ function extractGeneratedConstraintsBlock(content) {
116
+ const match = content.match(/<constraints>\n([\s\S]*?)\n<\/constraints>/);
117
+ return match ? `<constraints>\n${match[1].trim()}\n</constraints>` : null;
118
+ }
119
+
120
+ function extractGeneratedContextSection(content) {
121
+ const match = content.match(/## Context Management\n([\s\S]*?)\n\n---/);
122
+ return match ? `## Context Management\n${match[1].trim()}` : null;
123
+ }
124
+
125
+ function getFailedTemplateGroups(ctx, only = []) {
126
+ const groups = new Map();
127
+ for (const [key, technique] of Object.entries(TECHNIQUES)) {
128
+ const passed = technique.check(ctx);
129
+ const templateKey = technique.template || FALLBACK_TEMPLATE_BY_KEY[key];
130
+ if (passed !== false || !templateKey) continue;
131
+ if (templateKey === 'mermaid') continue;
132
+ if (only.length > 0 && !only.includes(key) && !only.includes(templateKey)) continue;
133
+ if (!groups.has(templateKey)) {
134
+ groups.set(templateKey, []);
135
+ }
136
+ groups.get(templateKey).push({ key, ...technique });
137
+ }
138
+ return groups;
139
+ }
140
+
141
+ function buildClaudeMdPatchFile(ctx, stacks) {
142
+ const claudePath = ctx.fileContent('CLAUDE.md') !== null
143
+ ? 'CLAUDE.md'
144
+ : (ctx.fileContent('.claude/CLAUDE.md') !== null ? '.claude/CLAUDE.md' : null);
145
+ if (!claudePath) return null;
146
+
147
+ const existing = normalizeNewlines(ctx.fileContent(claudePath));
148
+ const generated = TEMPLATES['claude-md'](stacks, ctx);
149
+ const buildSection = extractGeneratedBuildSection(generated);
150
+ const verificationBlock = extractGeneratedVerificationBlock(generated);
151
+ const workingNotes = extractGeneratedWorkingNotes(generated);
152
+ const constraintsBlock = extractGeneratedConstraintsBlock(generated);
153
+ const contextSection = extractGeneratedContextSection(generated);
154
+ let merged = existing;
155
+ let changed = false;
156
+
157
+ const hasTest = /npm test|pytest|jest|vitest|cargo test|go test|mix test|rspec/.test(merged);
158
+ const hasLint = /eslint|prettier|ruff|black|clippy|golangci-lint|rubocop/.test(merged);
159
+ const hasBuild = /npm run build|cargo build|go build|make|tsc|gradle build|mvn compile/.test(merged);
160
+ const hasVerification = merged.includes('<verification>');
161
+ const hasSecurityWorkflow = merged.toLowerCase().includes('security') || merged.includes('/security-review');
162
+ const hasImportGuidance = merged.includes('@import');
163
+ const hasRoleDefinition = /you are|your role|act as|persona|behave as/i.test(merged);
164
+ const hasConstraintBlock = /<constraints|<rules|<requirements|<boundaries/i.test(merged);
165
+ const hasCompaction = /\/compact|compaction/i.test(merged);
166
+ const hasContextManagement = /context.*(manage|window|limit|budget|token)/i.test(merged);
167
+ const modernFeatures = ['hook', 'skill', 'agent', 'subagent', 'mcp', 'worktree'];
168
+ const hasFreshness = modernFeatures.filter(feature => merged.toLowerCase().includes(feature)).length >= 2;
169
+
170
+ if ((!hasTest || !hasLint || !hasBuild) && buildSection) {
171
+ const result = upsertManagedBlock(merged, 'build-test', buildSection);
172
+ merged = result.content;
173
+ changed = true;
174
+ }
175
+
176
+ if (!hasRoleDefinition && workingNotes) {
177
+ const result = upsertManagedBlock(merged, 'working-style', workingNotes);
178
+ merged = result.content;
179
+ changed = true;
180
+ }
181
+
182
+ if (!hasConstraintBlock && constraintsBlock) {
183
+ const result = upsertManagedBlock(merged, 'constraints', constraintsBlock);
184
+ merged = result.content;
185
+ changed = true;
186
+ }
187
+
188
+ if (!hasVerification && verificationBlock) {
189
+ const result = upsertManagedBlock(merged, 'verification', verificationBlock);
190
+ merged = result.content;
191
+ changed = true;
192
+ }
193
+
194
+ if (!hasSecurityWorkflow) {
195
+ const result = upsertManagedBlock(merged, 'security-workflow', [
196
+ '## Security Workflow',
197
+ '- Run `/security-review` when touching authentication, permissions, secrets, or customer data.',
198
+ '- Treat secret access, shell commands, and risky file operations as review-worthy changes.',
199
+ ].join('\n'));
200
+ merged = result.content;
201
+ changed = true;
202
+ }
203
+
204
+ if (!hasImportGuidance) {
205
+ const result = upsertManagedBlock(merged, 'modularity', [
206
+ '## Modularity',
207
+ '- If this file grows, split it with `@import ./docs/...` so the base instructions stay concise.',
208
+ ].join('\n'));
209
+ merged = result.content;
210
+ changed = true;
211
+ }
212
+
213
+ if ((!hasCompaction || !hasContextManagement || !hasFreshness) && contextSection) {
214
+ const result = upsertManagedBlock(merged, 'context-management', contextSection);
215
+ merged = result.content;
216
+ changed = true;
217
+ }
218
+
219
+ if (!changed) {
220
+ return null;
221
+ }
222
+
223
+ return {
224
+ path: claudePath,
225
+ action: 'patch',
226
+ currentState: 'existing CLAUDE.md is missing recommended verification or security sections',
227
+ proposedState: 'append managed sections for verification, security workflow, and modularity',
228
+ content: ensureTrailingNewline(merged),
229
+ };
230
+ }
231
+
232
+ function buildAgentPatchFiles(ctx) {
233
+ if (!ctx.hasDir('.claude/agents')) {
234
+ return [];
235
+ }
236
+
237
+ return ctx.dirFiles('.claude/agents')
238
+ .filter(file => file.endsWith('.md'))
239
+ .map((file) => {
240
+ const relativePath = `.claude/agents/${file}`;
241
+ const content = normalizeNewlines(ctx.fileContent(relativePath) || '');
242
+ if (!content.startsWith('---\n') || content.includes('\nmaxTurns:')) {
243
+ return null;
244
+ }
245
+
246
+ const updated = content.replace(/^---\n([\s\S]*?)\n---/, (match, frontmatter) => `---\n${frontmatter}\nmaxTurns: 50\n---`);
247
+ if (updated === content) {
248
+ return null;
249
+ }
250
+
251
+ return {
252
+ path: relativePath,
253
+ action: 'patch',
254
+ currentState: 'existing agent is missing a maxTurns safety limit',
255
+ proposedState: 'add maxTurns: 50 to the agent frontmatter',
256
+ content: ensureTrailingNewline(updated),
257
+ };
258
+ })
259
+ .filter(Boolean);
260
+ }
261
+
262
+ function buildHookSettings(ctx, plannedHookFiles, options = {}) {
263
+ const existing = ctx.hasDir('.claude/hooks')
264
+ ? ctx.dirFiles('.claude/hooks').filter(file => file.endsWith('.sh') || file.endsWith('.js'))
265
+ : [];
266
+ const hookFiles = [...new Set([...existing, ...plannedHookFiles])].sort();
267
+ if (hookFiles.length === 0) {
268
+ return null;
269
+ }
270
+ const settingsPath = '.claude/settings.json';
271
+ const existingSettings = ctx.jsonFile(settingsPath);
272
+ const settings = buildSettingsForProfile({
273
+ profileKey: options.profile || 'safe-write',
274
+ hookFiles,
275
+ existingSettings,
276
+ mcpPackKeys: options.mcpPacks || [],
277
+ });
278
+ const content = `${JSON.stringify(settings, null, 2)}\n`;
279
+ const existingContent = existingSettings ? `${JSON.stringify(existingSettings, null, 2)}\n` : null;
280
+
281
+ if (existingContent === content) {
282
+ return null;
283
+ }
284
+
285
+ return {
286
+ path: settingsPath,
287
+ action: existingSettings ? 'patch' : 'create',
288
+ currentState: existingSettings
289
+ ? 'existing settings are missing selected profile protections or hook registrations'
290
+ : 'settings file is missing',
291
+ proposedState: existingSettings
292
+ ? `merge ${options.profile || 'safe-write'} profile protections into existing settings`
293
+ : `create settings for ${options.profile || 'safe-write'} profile and register hooks`,
294
+ content,
295
+ };
296
+ }
297
+
298
+ function buildTemplateFiles(templateKey, stacks, ctx, triggers, options = {}) {
299
+ const patchFiles = templateKey === 'agents' ? buildAgentPatchFiles(ctx) : [];
300
+
301
+ if (templateKey === 'claude-md') {
302
+ const patchFile = buildClaudeMdPatchFile(ctx, stacks);
303
+ if (patchFile) {
304
+ return [patchFile];
305
+ }
306
+ }
307
+
308
+ const template = TEMPLATES[templateKey];
309
+ if (!template) return [];
310
+
311
+ const result = template(stacks, ctx);
312
+ if (typeof result === 'string') {
313
+ return [{ path: 'CLAUDE.md', content: result }];
314
+ }
315
+
316
+ const targetDir = TEMPLATE_DIR_MAP[templateKey];
317
+ if (!targetDir) return [];
318
+
319
+ const generatedFiles = Object.entries(result).map(([fileName, content]) => ({
320
+ path: path.posix.join(targetDir.replace(/\\/g, '/'), fileName),
321
+ content,
322
+ }));
323
+
324
+ const patchedPaths = new Set(patchFiles.map(file => file.path));
325
+ return [...patchFiles, ...generatedFiles.filter(file => !patchedPaths.has(file.path))];
326
+ }
327
+
328
+ function toProposal(templateKey, triggers, templateFiles, ctx) {
329
+ const sortedTriggers = [...triggers].sort((a, b) => {
330
+ const impactA = IMPACT_ORDER[a.impact] ?? 0;
331
+ const impactB = IMPACT_ORDER[b.impact] ?? 0;
332
+ return impactB - impactA;
333
+ });
334
+ const highestImpact = sortedTriggers[0]?.impact || 'medium';
335
+ const files = templateFiles.map(file => {
336
+ const exists = ctx.fileContent(file.path) !== null || ctx.hasDir(file.path);
337
+ const action = file.action || (exists ? 'manual-review' : 'create');
338
+ const currentState = file.currentState || (exists ? 'file already exists and will be preserved' : 'missing');
339
+ const proposedState = file.proposedState || (exists ? 'generated baseline available for manual merge' : 'create new file');
340
+ const diffPreview = [
341
+ `--- ${exists ? file.path : 'missing'}`,
342
+ `+++ ${file.path}`,
343
+ ...previewContent(file.content).split('\n').map(line => `+${line}`),
344
+ ].join('\n');
345
+ return {
346
+ path: file.path,
347
+ action,
348
+ currentState,
349
+ proposedState,
350
+ bytes: Buffer.byteLength(file.content, 'utf8'),
351
+ content: file.content,
352
+ preview: previewContent(file.content),
353
+ diffPreview,
354
+ };
355
+ });
356
+
357
+ return {
358
+ id: templateKey,
359
+ title: TEMPLATE_LABELS[templateKey] || templateKey,
360
+ module: TEMPLATE_MODULES[templateKey] || templateKey,
361
+ risk: riskFromImpact(highestImpact),
362
+ confidence: sortedTriggers.length >= 2 ? 'high' : 'medium',
363
+ triggers: sortedTriggers.map(trigger => ({
364
+ key: trigger.key,
365
+ name: trigger.name,
366
+ impact: trigger.impact,
367
+ fix: trigger.fix,
368
+ })),
369
+ rationale: sortedTriggers.map(trigger => trigger.fix),
370
+ files,
371
+ readyToApply: files.some(file => ['create', 'patch'].includes(file.action)),
372
+ };
373
+ }
374
+
375
+ async function buildProposalBundle(options) {
376
+ if (options.platform === 'codex') {
377
+ return buildCodexProposalBundle(options);
378
+ }
379
+
380
+ const ctx = new ProjectContext(options.dir);
381
+ const stacks = ctx.detectStacks(STACKS);
382
+ const report = await analyzeProject({ ...options, mode: 'augment' });
383
+ const mcpPreflightWarnings = getMcpPackPreflight(options.mcpPacks || [])
384
+ .filter(item => item.missingEnvVars.length > 0);
385
+ const groups = getFailedTemplateGroups(ctx, options.only || []);
386
+ const proposals = [];
387
+
388
+ for (const [templateKey, triggers] of groups.entries()) {
389
+ const templateFiles = buildTemplateFiles(templateKey, stacks, ctx, triggers, options);
390
+ if (templateKey === 'hooks') {
391
+ const plannedHookFiles = templateFiles
392
+ .map(file => path.basename(file.path))
393
+ .filter(file => file.endsWith('.sh') || file.endsWith('.js'));
394
+ const settingsFile = buildHookSettings(ctx, plannedHookFiles, options);
395
+ if (settingsFile) {
396
+ templateFiles.push(settingsFile);
397
+ }
398
+ }
399
+ proposals.push(toProposal(templateKey, triggers, templateFiles, ctx));
400
+ }
401
+
402
+ proposals.sort((a, b) => {
403
+ const impactA = IMPACT_ORDER[a.triggers[0]?.impact] ?? 0;
404
+ const impactB = IMPACT_ORDER[b.triggers[0]?.impact] ?? 0;
405
+ return impactB - impactA;
406
+ });
407
+
408
+ return {
409
+ schemaVersion: 1,
410
+ generatedBy: `nerviq@${version}`,
411
+ createdAt: new Date().toISOString(),
412
+ directory: options.dir,
413
+ projectSummary: report.projectSummary,
414
+ strengthsPreserved: report.strengthsPreserved,
415
+ topNextActions: report.topNextActions,
416
+ riskNotes: report.riskNotes,
417
+ mcpPreflightWarnings,
418
+ proposals,
419
+ };
420
+ }
421
+
422
+ function printProposalBundle(bundle, options = {}) {
423
+ if (options.json) {
424
+ console.log(JSON.stringify(bundle, null, 2));
425
+ return;
426
+ }
427
+
428
+ console.log('');
429
+ console.log(' nerviq plan');
430
+ console.log(' ═══════════════════════════════════════');
431
+ console.log(` ${bundle.projectSummary.name} | maturity=${bundle.projectSummary.maturity} | score=${bundle.projectSummary.score}/100`);
432
+ console.log('');
433
+
434
+ if (bundle.mcpPreflightWarnings && bundle.mcpPreflightWarnings.length > 0) {
435
+ console.log(' MCP Preflight Warnings');
436
+ for (const warning of bundle.mcpPreflightWarnings) {
437
+ console.log(` - ${warning.label}: missing ${warning.missingEnvVars.join(', ')}`);
438
+ }
439
+ console.log('');
440
+ }
441
+
442
+ if (bundle.proposals.length === 0) {
443
+ console.log(' No templated proposals are needed right now.');
444
+ console.log('');
445
+ return;
446
+ }
447
+
448
+ console.log(' Proposal Bundles');
449
+ for (const proposal of bundle.proposals) {
450
+ const applyState = proposal.readyToApply ? 'ready' : 'manual-review';
451
+ console.log(` - ${proposal.id} [${applyState}]`);
452
+ console.log(` ${proposal.title} | risk=${proposal.risk} | confidence=${proposal.confidence}`);
453
+ console.log(` triggers: ${proposal.triggers.map(item => item.name).join(', ')}`);
454
+ console.log(` files: ${proposal.files.map(file => `${file.path} (${file.action})`).join(', ')}`);
455
+ }
456
+ console.log('');
457
+ }
458
+
459
+ function writePlanFile(bundle, outFile) {
460
+ fs.mkdirSync(path.dirname(outFile), { recursive: true });
461
+ fs.writeFileSync(outFile, JSON.stringify(bundle, null, 2), 'utf8');
462
+ return writeActivityArtifact(bundle.directory, 'plan-export', {
463
+ exportedPlan: outFile,
464
+ proposalIds: bundle.proposals.map(proposal => proposal.id),
465
+ proposalCount: bundle.proposals.length,
466
+ });
467
+ }
468
+
469
+ function tryParseJson(content) {
470
+ try {
471
+ return JSON.parse(content);
472
+ } catch {
473
+ return null;
474
+ }
475
+ }
476
+
477
+ function applyRuntimeSettingsOverlays(bundle, options) {
478
+ if (options.platform === 'codex') {
479
+ return bundle;
480
+ }
481
+
482
+ if (!bundle || !Array.isArray(bundle.proposals)) {
483
+ return bundle;
484
+ }
485
+
486
+ const ctx = new ProjectContext(options.dir);
487
+ const existingHooks = ctx.hasDir('.claude/hooks')
488
+ ? ctx.dirFiles('.claude/hooks').filter(file => file.endsWith('.sh') || file.endsWith('.js'))
489
+ : [];
490
+
491
+ const proposals = bundle.proposals.map((proposal) => {
492
+ const settingsIndex = proposal.files.findIndex(file => file.path === '.claude/settings.json');
493
+ if (settingsIndex === -1) {
494
+ return proposal;
495
+ }
496
+
497
+ const plannedHookFiles = proposal.files
498
+ .filter(file => file.path.startsWith('.claude/hooks/') && file.path.endsWith('.sh'))
499
+ .map(file => path.basename(file.path));
500
+ const hookFiles = [...new Set([...existingHooks, ...plannedHookFiles])].sort();
501
+ const currentSettings = tryParseJson(proposal.files[settingsIndex].content) || ctx.jsonFile('.claude/settings.json') || null;
502
+ const mergedSettings = buildSettingsForProfile({
503
+ profileKey: options.profile || 'safe-write',
504
+ hookFiles,
505
+ existingSettings: currentSettings,
506
+ mcpPackKeys: options.mcpPacks || [],
507
+ });
508
+ const updatedContent = `${JSON.stringify(mergedSettings, null, 2)}\n`;
509
+ const currentFile = proposal.files[settingsIndex];
510
+
511
+ const files = [...proposal.files];
512
+ files[settingsIndex] = {
513
+ ...currentFile,
514
+ content: updatedContent,
515
+ preview: previewContent(updatedContent),
516
+ diffPreview: [
517
+ `--- ${ctx.fileContent(currentFile.path) !== null ? currentFile.path : 'missing'}`,
518
+ `+++ ${currentFile.path}`,
519
+ ...previewContent(updatedContent).split('\n').map(line => `+${line}`),
520
+ ].join('\n'),
521
+ currentState: currentFile.currentState || 'existing settings are missing runtime-selected protections or MCP packs',
522
+ proposedState: `merge ${options.profile || 'safe-write'} profile protections and requested MCP packs into settings`,
523
+ };
524
+
525
+ return {
526
+ ...proposal,
527
+ files,
528
+ };
529
+ });
530
+
531
+ return {
532
+ ...bundle,
533
+ proposals,
534
+ };
535
+ }
536
+
537
+ function resolvePlan(bundle, options) {
538
+ if (options.planFile) {
539
+ return applyRuntimeSettingsOverlays(JSON.parse(fs.readFileSync(options.planFile, 'utf8')), options);
540
+ }
541
+ return applyRuntimeSettingsOverlays(bundle, options);
542
+ }
543
+
544
+ async function applyProposalBundle(options) {
545
+ const liveBundle = options.planFile ? null : await buildProposalBundle(options);
546
+ const bundle = resolvePlan(liveBundle, options);
547
+ const mcpPreflightWarnings = getMcpPackPreflight(options.mcpPacks || [])
548
+ .filter(item => item.missingEnvVars.length > 0);
549
+ const selectedIds = options.only && options.only.length > 0
550
+ ? new Set(options.only)
551
+ : null;
552
+ const selected = bundle.proposals.filter(proposal => {
553
+ if (selectedIds && !selectedIds.has(proposal.id)) return false;
554
+ return proposal.readyToApply;
555
+ });
556
+
557
+ const createdFiles = [];
558
+ const patchedFiles = [];
559
+ const skippedFiles = [];
560
+ for (const proposal of selected) {
561
+ for (const file of proposal.files) {
562
+ if (!['create', 'patch'].includes(file.action)) {
563
+ skippedFiles.push(file.path);
564
+ continue;
565
+ }
566
+ const fullPath = path.join(options.dir, file.path);
567
+ if (file.action === 'create' && fs.existsSync(fullPath)) {
568
+ skippedFiles.push(file.path);
569
+ continue;
570
+ }
571
+ const previousContent = fs.existsSync(fullPath) ? fs.readFileSync(fullPath, 'utf8') : null;
572
+ if (!options.dryRun) {
573
+ fs.mkdirSync(path.dirname(fullPath), { recursive: true });
574
+ fs.writeFileSync(fullPath, file.content, 'utf8');
575
+ }
576
+ if (file.action === 'create') {
577
+ createdFiles.push(file.path);
578
+ } else {
579
+ patchedFiles.push({ path: file.path, previousContent });
580
+ }
581
+ }
582
+ }
583
+
584
+ let rollback = null;
585
+ let activity = null;
586
+ if (!options.dryRun && (createdFiles.length > 0 || patchedFiles.length > 0)) {
587
+ rollback = writeRollbackArtifact(options.dir, {
588
+ sourcePlan: options.planFile ? path.basename(options.planFile) : 'live-plan',
589
+ createdFiles,
590
+ patchedFiles,
591
+ rollbackInstructions: [
592
+ ...createdFiles.map(file => `Delete ${file}`),
593
+ ...patchedFiles.map(file => `Restore previous content for ${file.path} from this manifest`),
594
+ ],
595
+ });
596
+ activity = writeActivityArtifact(options.dir, 'apply', {
597
+ sourcePlan: options.planFile ? path.basename(options.planFile) : 'live-plan',
598
+ appliedProposalIds: selected.map(item => item.id),
599
+ createdFiles,
600
+ patchedFiles: patchedFiles.map(file => file.path),
601
+ skippedFiles,
602
+ rollbackArtifact: rollback.relativePath,
603
+ });
604
+ }
605
+
606
+ return {
607
+ proposalCount: bundle.proposals.length,
608
+ appliedProposalIds: selected.map(item => item.id),
609
+ createdFiles,
610
+ patchedFiles: patchedFiles.map(file => file.path),
611
+ skippedFiles,
612
+ dryRun: options.dryRun === true,
613
+ rollbackArtifact: rollback ? rollback.relativePath : null,
614
+ activityArtifact: activity ? activity.relativePath : null,
615
+ mcpPreflightWarnings,
616
+ };
617
+ }
618
+
619
+ function printApplyResult(result, options = {}) {
620
+ if (options.json) {
621
+ console.log(JSON.stringify(result, null, 2));
622
+ return;
623
+ }
624
+
625
+ console.log('');
626
+ console.log(' nerviq apply');
627
+ console.log(' ═══════════════════════════════════════');
628
+ if (result.dryRun) {
629
+ console.log(' Dry-run only. No files were written.');
630
+ }
631
+ console.log(` Applied proposal bundles: ${result.appliedProposalIds.join(', ') || 'none'}`);
632
+ console.log(` Created files: ${result.createdFiles.join(', ') || 'none'}`);
633
+ console.log(` Patched files: ${result.patchedFiles.join(', ') || 'none'}`);
634
+ if (result.mcpPreflightWarnings && result.mcpPreflightWarnings.length > 0) {
635
+ console.log(' MCP preflight warnings:');
636
+ for (const warning of result.mcpPreflightWarnings) {
637
+ console.log(` - ${warning.label}: missing ${warning.missingEnvVars.join(', ')}`);
638
+ }
639
+ }
640
+ if (result.rollbackArtifact) {
641
+ console.log(` Rollback: ${result.rollbackArtifact}`);
642
+ }
643
+ if (result.activityArtifact) {
644
+ console.log(` Activity log: ${result.activityArtifact}`);
645
+ }
646
+ console.log('');
647
+ }
648
+
649
+ module.exports = {
650
+ buildProposalBundle,
651
+ printProposalBundle,
652
+ writePlanFile,
653
+ applyProposalBundle,
654
+ printApplyResult,
655
+ };
@@ -0,0 +1,30 @@
1
+ const EMBEDDED_SECRET_PATTERNS = [
2
+ /\bsk-ant-[A-Za-z0-9_-]{20,}\b/g,
3
+ /\bsk-proj-[A-Za-z0-9_-]{20,}\b/g,
4
+ /\bsk-[A-Za-z0-9_-]{20,}\b/g,
5
+ /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g,
6
+ /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g,
7
+ /\bgh[pousr]_[A-Za-z0-9]{20,}\b/g,
8
+ ];
9
+
10
+ function containsEmbeddedSecret(text = '') {
11
+ return EMBEDDED_SECRET_PATTERNS.some((pattern) => {
12
+ pattern.lastIndex = 0;
13
+ return pattern.test(text);
14
+ });
15
+ }
16
+
17
+ function redactEmbeddedSecrets(text = '') {
18
+ let output = text;
19
+ for (const pattern of EMBEDDED_SECRET_PATTERNS) {
20
+ pattern.lastIndex = 0;
21
+ output = output.replace(pattern, '[REDACTED_SECRET]');
22
+ }
23
+ return output;
24
+ }
25
+
26
+ module.exports = {
27
+ EMBEDDED_SECRET_PATTERNS,
28
+ containsEmbeddedSecret,
29
+ redactEmbeddedSecrets,
30
+ };