@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 +64 -3
- package/package.json +3 -2
- package/src/aider/techniques.js +3 -0
- package/src/audit.js +3 -2
- package/src/codex/techniques.js +3 -0
- package/src/convert.js +336 -0
- package/src/copilot/techniques.js +3 -0
- package/src/cursor/techniques.js +3 -0
- package/src/doctor.js +253 -0
- package/src/feedback.js +173 -0
- package/src/freshness.js +177 -0
- package/src/gemini/techniques.js +3 -0
- package/src/mcp-server.js +373 -0
- package/src/migrate.js +354 -0
- package/src/opencode/techniques.js +3 -0
- package/src/source-urls.js +219 -0
- package/src/techniques.js +3 -0
- package/src/windsurf/techniques.js +3 -0
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
|
+
"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",
|
package/src/aider/techniques.js
CHANGED
|
@@ -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,
|
package/src/codex/techniques.js
CHANGED
|
@@ -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
|
};
|
package/src/cursor/techniques.js
CHANGED
|
@@ -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
|
};
|