@nerviq/cli 0.9.3 → 0.9.4

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/bin/cli.js CHANGED
@@ -7,6 +7,7 @@ const { buildProposalBundle, printProposalBundle, writePlanFile, applyProposalBu
7
7
  const { getGovernanceSummary, printGovernanceSummary, ensureWritableProfile, renderGovernanceMarkdown } = require('../src/governance');
8
8
  const { runBenchmark, printBenchmark, writeBenchmarkReport } = require('../src/benchmark');
9
9
  const { writeSnapshotArtifact, recordRecommendationOutcome, formatRecommendationOutcomeSummary, getRecommendationOutcomeSummary } = require('../src/activity');
10
+ const { collectFeedback } = require('../src/feedback');
10
11
  const { version } = require('../package.json');
11
12
 
12
13
  const args = process.argv.slice(2);
@@ -20,7 +21,7 @@ const COMMAND_ALIASES = {
20
21
  gov: 'governance',
21
22
  outcome: 'feedback',
22
23
  };
23
- const KNOWN_COMMANDS = ['audit', 'setup', 'augment', 'suggest-only', 'plan', 'apply', 'governance', 'benchmark', 'deep-review', 'interactive', 'watch', 'badge', 'insights', 'history', 'compare', 'trend', 'scan', 'feedback', 'help', 'version'];
24
+ const KNOWN_COMMANDS = ['audit', 'setup', 'augment', 'suggest-only', 'plan', 'apply', 'governance', 'benchmark', 'deep-review', 'interactive', 'watch', 'badge', 'insights', 'history', 'compare', 'trend', 'scan', 'feedback', 'doctor', 'convert', 'migrate', 'help', 'version'];
24
25
 
25
26
  function levenshtein(a, b) {
26
27
  const matrix = Array.from({ length: a.length + 1 }, () => Array(b.length + 1).fill(0));
@@ -73,11 +74,15 @@ function parseArgs(rawArgs) {
73
74
  let format = null;
74
75
  let commandSet = false;
75
76
  let extraArgs = [];
77
+ let convertFrom = null;
78
+ let convertTo = null;
79
+ let migrateFrom = null;
80
+ let migrateTo = null;
76
81
 
77
82
  for (let i = 0; i < rawArgs.length; i++) {
78
83
  const arg = rawArgs[i];
79
84
 
80
- if (arg === '--threshold' || arg === '--out' || arg === '--plan' || arg === '--only' || arg === '--profile' || arg === '--mcp-pack' || arg === '--require' || arg === '--key' || arg === '--status' || arg === '--effect' || arg === '--notes' || arg === '--source' || arg === '--score-delta' || arg === '--platform' || arg === '--format') {
85
+ if (arg === '--threshold' || arg === '--out' || arg === '--plan' || arg === '--only' || arg === '--profile' || arg === '--mcp-pack' || arg === '--require' || arg === '--key' || arg === '--status' || arg === '--effect' || arg === '--notes' || arg === '--source' || arg === '--score-delta' || arg === '--platform' || arg === '--format' || arg === '--from' || arg === '--to') {
81
86
  const value = rawArgs[i + 1];
82
87
  if (!value || value.startsWith('--')) {
83
88
  throw new Error(`${arg} requires a value`);
@@ -97,6 +102,8 @@ function parseArgs(rawArgs) {
97
102
  if (arg === '--score-delta') feedbackScoreDelta = value.trim();
98
103
  if (arg === '--platform') platform = value.trim().toLowerCase();
99
104
  if (arg === '--format') format = value.trim().toLowerCase();
105
+ if (arg === '--from') { convertFrom = value.trim(); migrateFrom = value.trim(); }
106
+ if (arg === '--to') { convertTo = value.trim(); migrateTo = value.trim(); }
100
107
  i++;
101
108
  continue;
102
109
  }
@@ -191,7 +198,7 @@ function parseArgs(rawArgs) {
191
198
 
192
199
  const normalizedCommand = COMMAND_ALIASES[command] || command;
193
200
 
194
- return { flags, command, normalizedCommand, threshold, out, planFile, only, profile, mcpPacks, requireChecks, feedbackKey, feedbackStatus, feedbackEffect, feedbackNotes, feedbackSource, feedbackScoreDelta, platform, format, extraArgs };
201
+ return { flags, command, normalizedCommand, threshold, out, planFile, only, profile, mcpPacks, requireChecks, feedbackKey, feedbackStatus, feedbackEffect, feedbackNotes, feedbackSource, feedbackScoreDelta, platform, format, extraArgs, convertFrom, convertTo, migrateFrom, migrateTo };
195
202
  }
196
203
 
197
204
  const HELP = `
@@ -230,6 +237,11 @@ const HELP = `
230
237
  npx nerviq badge Generate shields.io badge markdown
231
238
  npx nerviq feedback Record recommendation outcomes or show local outcome summary
232
239
 
240
+ Utilities:
241
+ npx nerviq doctor Self-diagnostics: Node version, deps, freshness gates, platform detection
242
+ npx nerviq convert --from claude --to codex Convert config between platforms
243
+ npx nerviq migrate --platform cursor --from v2 --to v3 Migrate platform config to newer version
244
+
233
245
  Options:
234
246
  --threshold N Exit with code 1 if score is below N (useful for CI)
235
247
  --require A,B Exit with code 1 if named checks fail (e.g. --require secretsProtection,permissionDeny)
@@ -246,6 +258,7 @@ const HELP = `
246
258
  --score-delta N Optional observed score delta tied to the outcome
247
259
  --platform NAME Choose platform surface (claude default, codex advisory/build preview)
248
260
  --format NAME Output format for audit results (json, sarif)
261
+ --feedback After audit output, prompt "Was this helpful? (y/n)" for each displayed top action and save answers locally
249
262
  --snapshot Save a normalized snapshot artifact under .claude/nerviq/snapshots/
250
263
  --lite Show a short top-3 quick scan with one clear next command
251
264
  --dry-run Preview apply without writing files
@@ -316,6 +329,7 @@ async function main() {
316
329
  auto: flags.includes('--auto'),
317
330
  lite: flags.includes('--lite'),
318
331
  snapshot: flags.includes('--snapshot'),
332
+ feedback: flags.includes('--feedback'),
319
333
  dryRun: flags.includes('--dry-run'),
320
334
  threshold: parsed.threshold !== null ? Number(parsed.threshold) : null,
321
335
  out: parsed.out,
@@ -699,6 +713,34 @@ async function main() {
699
713
  } else if (normalizedCommand === 'watch') {
700
714
  const { watch } = require('../src/watch');
701
715
  await watch(options);
716
+ } else if (normalizedCommand === 'doctor') {
717
+ const { runDoctor } = require('../src/doctor');
718
+ const output = await runDoctor({ dir: options.dir, json: options.json, verbose: options.verbose });
719
+ console.log(output);
720
+ process.exit(0);
721
+ } else if (normalizedCommand === 'convert') {
722
+ const { runConvert } = require('../src/convert');
723
+ const output = await runConvert({
724
+ dir: options.dir,
725
+ from: parsed.convertFrom,
726
+ to: parsed.convertTo,
727
+ dryRun: options.dryRun,
728
+ json: options.json,
729
+ });
730
+ console.log(output);
731
+ process.exit(0);
732
+ } else if (normalizedCommand === 'migrate') {
733
+ const { runMigrate } = require('../src/migrate');
734
+ const output = await runMigrate({
735
+ dir: options.dir,
736
+ platform: options.platform || parsed.platform || 'claude',
737
+ from: parsed.migrateFrom,
738
+ to: parsed.migrateTo,
739
+ dryRun: options.dryRun,
740
+ json: options.json,
741
+ });
742
+ console.log(output);
743
+ process.exit(0);
702
744
  } else if (normalizedCommand === 'setup') {
703
745
  await setup(options);
704
746
  if (options.snapshot) {
@@ -712,6 +754,25 @@ async function main() {
712
754
  }
713
755
  } else {
714
756
  const result = await audit(options);
757
+ if (options.feedback && !options.json && options.format === null) {
758
+ const feedbackTargets = options.lite
759
+ ? (result.liteSummary?.topNextActions || [])
760
+ : (result.topNextActions || []);
761
+ const feedbackResult = await collectFeedback(options.dir, {
762
+ findings: feedbackTargets,
763
+ platform: result.platform,
764
+ sourceCommand: normalizedCommand,
765
+ score: result.score,
766
+ });
767
+ if (feedbackResult.mode === 'skipped-noninteractive') {
768
+ console.log(' Feedback prompt skipped: interactive terminal required.');
769
+ console.log('');
770
+ } else if (feedbackResult.saved > 0) {
771
+ console.log(` Feedback saved: ${feedbackResult.relativeDir}`);
772
+ console.log(` Helpful: ${feedbackResult.helpful} | Not helpful: ${feedbackResult.unhelpful}`);
773
+ console.log('');
774
+ }
775
+ }
715
776
  const snapshot = options.snapshot ? writeSnapshotArtifact(options.dir, 'audit', result, {
716
777
  sourceCommand: normalizedCommand,
717
778
  }) : null;
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@nerviq/cli",
3
- "version": "0.9.3",
3
+ "version": "0.9.4",
4
4
  "description": "The intelligent nervous system for AI coding agents — audit, align, and amplify every platform on every project.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
7
- "nerviq": "bin/cli.js"
7
+ "nerviq": "bin/cli.js",
8
+ "nerviq-mcp": "src/mcp-server.js"
8
9
  },
9
10
  "files": [
10
11
  "bin",
@@ -20,6 +20,7 @@
20
20
  */
21
21
 
22
22
  const { containsEmbeddedSecret } = require('../secret-patterns');
23
+ const { attachSourceUrls } = require('../source-urls');
23
24
 
24
25
  const FILLER_PATTERNS = [
25
26
  /\bbe helpful\b/i,
@@ -1389,6 +1390,8 @@ const AIDER_TECHNIQUES = {
1389
1390
  },
1390
1391
  };
1391
1392
 
1393
+ attachSourceUrls('aider', AIDER_TECHNIQUES);
1394
+
1392
1395
  module.exports = {
1393
1396
  AIDER_TECHNIQUES,
1394
1397
  };
package/src/audit.js CHANGED
@@ -461,7 +461,7 @@ function buildTopNextActions(failed, limit = 5, outcomeSummaryByKey = {}, option
461
461
  return scoreB - scoreA;
462
462
  })
463
463
  .slice(0, limit)
464
- .map(({ key, id, name, impact, fix, category }) => {
464
+ .map(({ key, id, name, impact, fix, category, sourceUrl }) => {
465
465
  const feedback = outcomeSummaryByKey[key] || null;
466
466
  const rankingAdjustment = getRecommendationAdjustment(outcomeSummaryByKey, key);
467
467
  const signals = [
@@ -491,6 +491,7 @@ function buildTopNextActions(failed, limit = 5, outcomeSummaryByKey = {}, option
491
491
  name,
492
492
  impact,
493
493
  category,
494
+ sourceUrl,
494
495
  module: CATEGORY_MODULES[category] || category,
495
496
  fix,
496
497
  priorityScore,
@@ -810,7 +811,7 @@ async function audit(options) {
810
811
  stacks,
811
812
  results,
812
813
  categoryScores,
813
- quickWins: quickWins.map(({ key, name, impact, fix, category }) => ({ key, name, impact, category, fix })),
814
+ quickWins: quickWins.map(({ key, name, impact, fix, category, sourceUrl }) => ({ key, name, impact, category, fix, sourceUrl })),
814
815
  topNextActions,
815
816
  recommendationOutcomes: {
816
817
  totalEntries: outcomeSummary.totalEntries,
@@ -1,6 +1,7 @@
1
1
  const os = require('os');
2
2
  const path = require('path');
3
3
  const { EMBEDDED_SECRET_PATTERNS, containsEmbeddedSecret } = require('../secret-patterns');
4
+ const { attachSourceUrls } = require('../source-urls');
4
5
 
5
6
  const DEFAULT_PROJECT_DOC_MAX_BYTES = 32768;
6
7
  const SUPPORTED_HOOK_EVENTS = new Set(['SessionStart', 'PreToolUse', 'PostToolUse', 'UserPromptSubmit', 'Stop']);
@@ -3249,6 +3250,8 @@ const CODEX_TECHNIQUES = {
3249
3250
  },
3250
3251
  };
3251
3252
 
3253
+ attachSourceUrls('codex', CODEX_TECHNIQUES);
3254
+
3252
3255
  module.exports = {
3253
3256
  CODEX_TECHNIQUES,
3254
3257
  };
package/src/convert.js ADDED
@@ -0,0 +1,336 @@
1
+ /**
2
+ * Nerviq Convert
3
+ *
4
+ * Converts configuration files between AI coding platforms.
5
+ * Reads the source platform's config and emits equivalent config
6
+ * for the target platform, preserving intent where possible.
7
+ *
8
+ * Supported conversions:
9
+ * claude → codex, cursor, copilot, gemini, windsurf, aider
10
+ * codex → claude, cursor, copilot, gemini, windsurf, aider
11
+ * cursor → claude, codex, copilot, gemini, windsurf, aider
12
+ * (any) → (any) using canonical model as intermediary
13
+ */
14
+
15
+ 'use strict';
16
+
17
+ const fs = require('fs');
18
+ const path = require('path');
19
+
20
+ const COLORS = {
21
+ reset: '\x1b[0m',
22
+ bold: '\x1b[1m',
23
+ dim: '\x1b[2m',
24
+ red: '\x1b[31m',
25
+ green: '\x1b[32m',
26
+ yellow: '\x1b[33m',
27
+ blue: '\x1b[36m',
28
+ };
29
+
30
+ function c(text, color) {
31
+ return `${COLORS[color] || ''}${text}${COLORS.reset}`;
32
+ }
33
+
34
+ // ─── Platform config readers ─────────────────────────────────────────────────
35
+
36
+ /**
37
+ * Read the canonical "intent" from a source platform.
38
+ * Returns a normalized object with: name, description, rules[], mcpServers{}, hooks[]
39
+ */
40
+ function readSourceConfig(dir, from) {
41
+ const canonical = {
42
+ platform: from,
43
+ name: path.basename(dir),
44
+ description: null,
45
+ rules: [], // Array of { name, content, alwaysOn, glob, description }
46
+ mcpServers: {}, // { serverName: { command, args, env, url, type } }
47
+ hooks: [], // Array of { event, command, matcher }
48
+ techStack: [], // Detected languages/frameworks
49
+ lintCmd: null,
50
+ testCmd: null,
51
+ buildCmd: null,
52
+ };
53
+
54
+ if (from === 'claude') {
55
+ const claudeMd = fs.existsSync(path.join(dir, 'CLAUDE.md'))
56
+ ? fs.readFileSync(path.join(dir, 'CLAUDE.md'), 'utf8')
57
+ : null;
58
+ if (claudeMd) {
59
+ canonical.description = claudeMd.slice(0, 500);
60
+ canonical.rules.push({ name: 'CLAUDE.md', content: claudeMd, alwaysOn: true });
61
+ }
62
+ // Read .claude/settings.json for MCP
63
+ const settingsPath = path.join(dir, '.claude', 'settings.json');
64
+ if (fs.existsSync(settingsPath)) {
65
+ try {
66
+ const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
67
+ if (settings.mcpServers) canonical.mcpServers = settings.mcpServers;
68
+ } catch {}
69
+ }
70
+ }
71
+
72
+ if (from === 'codex') {
73
+ const agentsMd = fs.existsSync(path.join(dir, 'AGENTS.md'))
74
+ ? fs.readFileSync(path.join(dir, 'AGENTS.md'), 'utf8')
75
+ : null;
76
+ if (agentsMd) {
77
+ canonical.description = agentsMd.slice(0, 500);
78
+ canonical.rules.push({ name: 'AGENTS.md', content: agentsMd, alwaysOn: true });
79
+ }
80
+ }
81
+
82
+ if (from === 'cursor') {
83
+ const rulesDir = path.join(dir, '.cursor', 'rules');
84
+ if (fs.existsSync(rulesDir)) {
85
+ const files = fs.readdirSync(rulesDir).filter(f => f.endsWith('.mdc'));
86
+ for (const file of files) {
87
+ const content = fs.readFileSync(path.join(rulesDir, file), 'utf8');
88
+ // Parse frontmatter
89
+ const fmMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
90
+ let alwaysOn = false;
91
+ let glob = null;
92
+ let desc = null;
93
+ if (fmMatch) {
94
+ alwaysOn = /alwaysApply\s*:\s*true/i.test(fmMatch[1]);
95
+ const globMatch = fmMatch[1].match(/globs?\s*:\s*(.+)/i);
96
+ if (globMatch) glob = globMatch[1].trim();
97
+ const descMatch = fmMatch[1].match(/description\s*:\s*"?([^"\n]+)"?/i);
98
+ if (descMatch) desc = descMatch[1].trim();
99
+ }
100
+ canonical.rules.push({
101
+ name: file.replace('.mdc', ''),
102
+ content,
103
+ alwaysOn,
104
+ glob,
105
+ description: desc,
106
+ });
107
+ }
108
+ }
109
+ // Cursor MCP
110
+ const mcpPath = path.join(dir, '.cursor', 'mcp.json');
111
+ if (fs.existsSync(mcpPath)) {
112
+ try {
113
+ const mcp = JSON.parse(fs.readFileSync(mcpPath, 'utf8'));
114
+ if (mcp.mcpServers) canonical.mcpServers = mcp.mcpServers;
115
+ } catch {}
116
+ }
117
+ }
118
+
119
+ if (from === 'gemini') {
120
+ const geminiMd = fs.existsSync(path.join(dir, 'GEMINI.md'))
121
+ ? fs.readFileSync(path.join(dir, 'GEMINI.md'), 'utf8')
122
+ : null;
123
+ if (geminiMd) {
124
+ canonical.description = geminiMd.slice(0, 500);
125
+ canonical.rules.push({ name: 'GEMINI.md', content: geminiMd, alwaysOn: true });
126
+ }
127
+ }
128
+
129
+ if (from === 'windsurf') {
130
+ const windsurfRulesDir = path.join(dir, '.windsurf', 'rules');
131
+ if (fs.existsSync(windsurfRulesDir)) {
132
+ const files = fs.readdirSync(windsurfRulesDir).filter(f => f.endsWith('.md'));
133
+ for (const file of files) {
134
+ const content = fs.readFileSync(path.join(windsurfRulesDir, file), 'utf8');
135
+ canonical.rules.push({ name: file.replace('.md', ''), content, alwaysOn: true });
136
+ }
137
+ }
138
+ }
139
+
140
+ if (from === 'aider') {
141
+ const aiderConf = fs.existsSync(path.join(dir, '.aider.conf.yml'))
142
+ ? fs.readFileSync(path.join(dir, '.aider.conf.yml'), 'utf8')
143
+ : null;
144
+ if (aiderConf) {
145
+ canonical.rules.push({ name: '.aider.conf.yml', content: aiderConf, alwaysOn: false });
146
+ const lintMatch = aiderConf.match(/lint-cmd\s*:\s*(.+)/);
147
+ if (lintMatch) canonical.lintCmd = lintMatch[1].trim().replace(/^['"]|['"]$/g, '');
148
+ const testMatch = aiderConf.match(/test-cmd\s*:\s*(.+)/);
149
+ if (testMatch) canonical.testCmd = testMatch[1].trim().replace(/^['"]|['"]$/g, '');
150
+ }
151
+ }
152
+
153
+ if (from === 'copilot') {
154
+ const copilotPath = path.join(dir, '.github', 'copilot-instructions.md');
155
+ if (fs.existsSync(copilotPath)) {
156
+ const content = fs.readFileSync(copilotPath, 'utf8');
157
+ canonical.rules.push({ name: 'copilot-instructions', content, alwaysOn: true });
158
+ }
159
+ }
160
+
161
+ return canonical;
162
+ }
163
+
164
+ // ─── Platform config writers ─────────────────────────────────────────────────
165
+
166
+ function buildTargetOutput(canonical, to, { dryRun = false } = {}) {
167
+ const outputs = []; // Array of { path, content }
168
+ const combinedContent = canonical.rules.map(r => r.content).join('\n\n');
169
+
170
+ if (to === 'claude') {
171
+ // Extract or create CLAUDE.md from combined rules
172
+ const content = `# ${canonical.name}\n\n${combinedContent}\n`;
173
+ outputs.push({ file: 'CLAUDE.md', content });
174
+
175
+ if (Object.keys(canonical.mcpServers).length > 0) {
176
+ const settings = { mcpServers: canonical.mcpServers };
177
+ outputs.push({ file: '.claude/settings.json', content: JSON.stringify(settings, null, 2) + '\n' });
178
+ }
179
+ }
180
+
181
+ if (to === 'codex') {
182
+ const content = `# ${canonical.name}\n\n${combinedContent}\n`;
183
+ outputs.push({ file: 'AGENTS.md', content });
184
+ }
185
+
186
+ if (to === 'cursor') {
187
+ // Write each rule as an .mdc file
188
+ if (canonical.rules.length === 0) {
189
+ const content = `---\nalwaysApply: true\n---\n\n# ${canonical.name}\n\n${combinedContent}\n`;
190
+ outputs.push({ file: '.cursor/rules/core.mdc', content });
191
+ } else {
192
+ for (const rule of canonical.rules) {
193
+ const fm = rule.alwaysOn
194
+ ? `---\nalwaysApply: true\n---\n`
195
+ : rule.glob
196
+ ? `---\nglobs: ${rule.glob}\nalwaysApply: false\n---\n`
197
+ : `---\nalwaysApply: false\n---\n`;
198
+ outputs.push({ file: `.cursor/rules/${rule.name}.mdc`, content: `${fm}\n${rule.content}\n` });
199
+ }
200
+ }
201
+ if (Object.keys(canonical.mcpServers).length > 0) {
202
+ const mcp = { mcpServers: canonical.mcpServers };
203
+ outputs.push({ file: '.cursor/mcp.json', content: JSON.stringify(mcp, null, 2) + '\n' });
204
+ }
205
+ }
206
+
207
+ if (to === 'gemini') {
208
+ const content = `# ${canonical.name}\n\n${combinedContent}\n`;
209
+ outputs.push({ file: 'GEMINI.md', content });
210
+ if (Object.keys(canonical.mcpServers).length > 0) {
211
+ const settings = { mcpServers: canonical.mcpServers };
212
+ outputs.push({ file: '.gemini/settings.json', content: JSON.stringify(settings, null, 2) + '\n' });
213
+ }
214
+ }
215
+
216
+ if (to === 'windsurf') {
217
+ if (canonical.rules.length === 0) {
218
+ outputs.push({ file: '.windsurf/rules/core.md', content: `---\ntrigger: always_on\n---\n\n${combinedContent}\n` });
219
+ } else {
220
+ for (const rule of canonical.rules) {
221
+ const fm = `---\ntrigger: always_on\n---\n`;
222
+ const safeContent = rule.content.replace(/^---[\s\S]*?---\n/m, '').trim();
223
+ outputs.push({ file: `.windsurf/rules/${rule.name}.md`, content: `${fm}\n${safeContent}\n` });
224
+ }
225
+ }
226
+ }
227
+
228
+ if (to === 'aider') {
229
+ const confLines = ['# Generated by nerviq convert'];
230
+ if (canonical.lintCmd) confLines.push(`lint-cmd: '${canonical.lintCmd}'`);
231
+ if (canonical.testCmd) confLines.push(`test-cmd: '${canonical.testCmd}'`);
232
+ confLines.push('auto-commits: true');
233
+ confLines.push('auto-lint: true');
234
+ outputs.push({ file: '.aider.conf.yml', content: confLines.join('\n') + '\n' });
235
+ if (combinedContent.trim()) {
236
+ outputs.push({ file: 'CONVENTIONS.md', content: `# ${canonical.name} Conventions\n\n${combinedContent}\n` });
237
+ }
238
+ }
239
+
240
+ if (to === 'copilot') {
241
+ const content = `# ${canonical.name}\n\n${combinedContent}\n`;
242
+ outputs.push({ file: '.github/copilot-instructions.md', content });
243
+ }
244
+
245
+ return outputs;
246
+ }
247
+
248
+ // ─── Main convert function ────────────────────────────────────────────────────
249
+
250
+ async function runConvert({ dir = process.cwd(), from, to, dryRun = false, json = false } = {}) {
251
+ if (!from || !to) {
252
+ throw new Error('Both --from and --to are required. Example: nerviq convert --from claude --to codex');
253
+ }
254
+
255
+ const SUPPORTED = ['claude', 'codex', 'cursor', 'copilot', 'gemini', 'windsurf', 'aider', 'opencode'];
256
+ if (!SUPPORTED.includes(from)) throw new Error(`Unsupported source platform '${from}'. Use: ${SUPPORTED.join(', ')}`);
257
+ if (!SUPPORTED.includes(to)) throw new Error(`Unsupported target platform '${to}'. Use: ${SUPPORTED.join(', ')}`);
258
+ if (from === to) throw new Error(`Source and target platform are the same: '${from}'`);
259
+
260
+ const canonical = readSourceConfig(dir, from);
261
+ const outputs = buildTargetOutput(canonical, to, { dryRun });
262
+
263
+ const written = [];
264
+ const skipped = [];
265
+
266
+ if (!dryRun) {
267
+ for (const out of outputs) {
268
+ const outPath = path.join(dir, out.file);
269
+ const outDir = path.dirname(outPath);
270
+ if (!fs.existsSync(outPath)) {
271
+ fs.mkdirSync(outDir, { recursive: true });
272
+ fs.writeFileSync(outPath, out.content, 'utf8');
273
+ written.push(out.file);
274
+ } else {
275
+ skipped.push(out.file);
276
+ }
277
+ }
278
+ }
279
+
280
+ const result = {
281
+ from,
282
+ to,
283
+ dir,
284
+ dryRun,
285
+ sourceRulesFound: canonical.rules.length,
286
+ mcpServersFound: Object.keys(canonical.mcpServers).length,
287
+ outputFiles: outputs.map(o => o.file),
288
+ written: dryRun ? [] : written,
289
+ skipped: dryRun ? [] : skipped,
290
+ wouldWrite: dryRun ? outputs.map(o => o.file) : [],
291
+ };
292
+
293
+ if (json) return JSON.stringify(result, null, 2);
294
+
295
+ const lines = [''];
296
+ lines.push(c(` nerviq convert ${from} → ${to}`, 'bold'));
297
+ lines.push(c(' ═══════════════════════════════════════', 'dim'));
298
+ lines.push('');
299
+ lines.push(` Source platform: ${c(from, 'blue')} (${canonical.rules.length} rule(s) found)`);
300
+ lines.push(` Target platform: ${c(to, 'blue')}`);
301
+ lines.push(` Directory: ${dir}`);
302
+ lines.push(` MCP servers: ${Object.keys(canonical.mcpServers).length}`);
303
+ lines.push('');
304
+
305
+ if (dryRun) {
306
+ lines.push(c(' Dry run — no files written', 'yellow'));
307
+ lines.push('');
308
+ lines.push(' Would generate:');
309
+ for (const f of outputs) {
310
+ lines.push(` ${c('→', 'dim')} ${f.file}`);
311
+ }
312
+ } else if (written.length > 0 || skipped.length > 0) {
313
+ if (written.length > 0) {
314
+ lines.push(' Written:');
315
+ for (const f of written) lines.push(` ${c('✓', 'green')} ${f}`);
316
+ }
317
+ if (skipped.length > 0) {
318
+ lines.push(' Skipped (already exists):');
319
+ for (const f of skipped) lines.push(` ${c('-', 'dim')} ${f}`);
320
+ }
321
+ }
322
+
323
+ lines.push('');
324
+ if (!dryRun && written.length > 0) {
325
+ lines.push(c(` ✓ Conversion complete. Run \`nerviq audit --platform ${to}\` to verify.`, 'green'));
326
+ } else if (dryRun) {
327
+ lines.push(c(` Run without --dry-run to write files.`, 'dim'));
328
+ } else {
329
+ lines.push(c(` No new files written (all already exist).`, 'dim'));
330
+ }
331
+ lines.push('');
332
+
333
+ return lines.join('\n');
334
+ }
335
+
336
+ module.exports = { runConvert };
@@ -15,6 +15,7 @@ const os = require('os');
15
15
  const path = require('path');
16
16
  const { CopilotProjectContext } = require('./context');
17
17
  const { EMBEDDED_SECRET_PATTERNS, containsEmbeddedSecret } = require('../secret-patterns');
18
+ const { attachSourceUrls } = require('../source-urls');
18
19
  const { extractFrontmatter, validateInstructionFrontmatter, validatePromptFrontmatter } = require('./config-parser');
19
20
 
20
21
  // ─── Shared helpers ─────────────────────────────────────────────────────────
@@ -1928,6 +1929,8 @@ const COPILOT_TECHNIQUES = {
1928
1929
  },
1929
1930
  };
1930
1931
 
1932
+ attachSourceUrls('copilot', COPILOT_TECHNIQUES);
1933
+
1931
1934
  module.exports = {
1932
1935
  COPILOT_TECHNIQUES,
1933
1936
  };
@@ -15,6 +15,7 @@ const os = require('os');
15
15
  const path = require('path');
16
16
  const { CursorProjectContext } = require('./context');
17
17
  const { EMBEDDED_SECRET_PATTERNS, containsEmbeddedSecret } = require('../secret-patterns');
18
+ const { attachSourceUrls } = require('../source-urls');
18
19
  const { validateMdcFrontmatter, validateMcpEnvVars } = require('./config-parser');
19
20
 
20
21
  // ─── Shared helpers ─────────────────────────────────────────────────────────
@@ -1861,6 +1862,8 @@ const CURSOR_TECHNIQUES = {
1861
1862
  },
1862
1863
  };
1863
1864
 
1865
+ attachSourceUrls('cursor', CURSOR_TECHNIQUES);
1866
+
1864
1867
  module.exports = {
1865
1868
  CURSOR_TECHNIQUES,
1866
1869
  };