@nerviq/cli 1.6.2 → 1.6.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 +58 -3
- package/package.json +1 -1
- package/src/activity.js +5 -2
- package/src/audit.js +26 -5
- package/src/doctor.js +10 -7
- package/src/harmony/cli.js +50 -2
- package/src/migrate.js +1 -1
- package/src/setup.js +4 -1
package/bin/cli.js
CHANGED
|
@@ -762,6 +762,13 @@ async function main() {
|
|
|
762
762
|
console.log('');
|
|
763
763
|
}
|
|
764
764
|
} else if (normalizedCommand === 'apply') {
|
|
765
|
+
if (flags.includes('--rollback')) {
|
|
766
|
+
console.error('\n Error: --rollback is not yet supported as a flag.');
|
|
767
|
+
console.error(' Why: Rollback artifacts are saved in .nerviq/rollbacks/ but automatic rollback is not implemented yet.');
|
|
768
|
+
console.error(' Fix: Manually delete the files listed in .nerviq/rollbacks/<latest>.json, or use `nerviq apply --dry-run` to preview before applying.');
|
|
769
|
+
console.error(' Docs: https://github.com/nerviq/nerviq#rollback\n');
|
|
770
|
+
process.exit(1);
|
|
771
|
+
}
|
|
765
772
|
const result = await applyProposalBundle(options);
|
|
766
773
|
printApplyResult(result, options);
|
|
767
774
|
} else if (normalizedCommand === 'governance') {
|
|
@@ -1138,6 +1145,40 @@ async function main() {
|
|
|
1138
1145
|
|
|
1139
1146
|
// Step 2: Determine which checks to fix
|
|
1140
1147
|
const { TECHNIQUES } = require('../src/techniques');
|
|
1148
|
+
const fs = require('fs');
|
|
1149
|
+
const pathMod = require('path');
|
|
1150
|
+
|
|
1151
|
+
// Inline fixers for checks without templates but with trivial auto-fixes
|
|
1152
|
+
const INLINE_FIXERS = {
|
|
1153
|
+
gitIgnoreEnv: (dir) => {
|
|
1154
|
+
const gitignorePath = pathMod.join(dir, '.gitignore');
|
|
1155
|
+
const existing = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, 'utf8') : '';
|
|
1156
|
+
if (!existing.includes('.env')) {
|
|
1157
|
+
const lines = existing.endsWith('\n') || existing === '' ? '' : '\n';
|
|
1158
|
+
fs.appendFileSync(gitignorePath, `${lines}.env\n.env.*\n`, 'utf8');
|
|
1159
|
+
return true;
|
|
1160
|
+
}
|
|
1161
|
+
return false;
|
|
1162
|
+
},
|
|
1163
|
+
secretsProtection: (dir) => {
|
|
1164
|
+
const settingsPath = pathMod.join(dir, '.claude', 'settings.json');
|
|
1165
|
+
const settingsDir = pathMod.join(dir, '.claude');
|
|
1166
|
+
if (!fs.existsSync(settingsDir)) fs.mkdirSync(settingsDir, { recursive: true });
|
|
1167
|
+
let settings = {};
|
|
1168
|
+
if (fs.existsSync(settingsPath)) {
|
|
1169
|
+
try { settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); } catch { settings = {}; }
|
|
1170
|
+
}
|
|
1171
|
+
if (!settings.permissions) settings.permissions = {};
|
|
1172
|
+
if (!settings.permissions.deny) settings.permissions.deny = [];
|
|
1173
|
+
const denyEntries = ['.env', '.env.*', '**/.env', '**/*.pem', '**/secrets/**'];
|
|
1174
|
+
for (const entry of denyEntries) {
|
|
1175
|
+
if (!settings.permissions.deny.includes(entry)) settings.permissions.deny.push(entry);
|
|
1176
|
+
}
|
|
1177
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf8');
|
|
1178
|
+
return true;
|
|
1179
|
+
},
|
|
1180
|
+
};
|
|
1181
|
+
|
|
1141
1182
|
let targetKeys = [];
|
|
1142
1183
|
|
|
1143
1184
|
if (fixKey) {
|
|
@@ -1161,8 +1202,9 @@ async function main() {
|
|
|
1161
1202
|
}
|
|
1162
1203
|
} else {
|
|
1163
1204
|
// No key specified — show fixable checks and exit
|
|
1164
|
-
const
|
|
1165
|
-
const
|
|
1205
|
+
const INLINE_FIX_KEYS = new Set(Object.keys(INLINE_FIXERS));
|
|
1206
|
+
const fixable = failedResults.filter(r => (TECHNIQUES[r.key] && TECHNIQUES[r.key].template) || INLINE_FIX_KEYS.has(r.key));
|
|
1207
|
+
const nonFixable = failedResults.filter(r => !(TECHNIQUES[r.key] && TECHNIQUES[r.key].template) && !INLINE_FIX_KEYS.has(r.key));
|
|
1166
1208
|
console.log('');
|
|
1167
1209
|
console.log(` nerviq fix — ${failedResults.length} failed checks\n`);
|
|
1168
1210
|
if (fixable.length > 0) {
|
|
@@ -1193,7 +1235,7 @@ async function main() {
|
|
|
1193
1235
|
process.exit(0);
|
|
1194
1236
|
}
|
|
1195
1237
|
|
|
1196
|
-
// Step 3: For each target, either use template or show manual instructions
|
|
1238
|
+
// Step 3: For each target, either use template, inline fix, or show manual instructions
|
|
1197
1239
|
const preScore = auditResult.score;
|
|
1198
1240
|
let fixed = 0;
|
|
1199
1241
|
let manual = 0;
|
|
@@ -1212,6 +1254,19 @@ async function main() {
|
|
|
1212
1254
|
console.log(` ✅ Fixed: ${failedCheck.name}`);
|
|
1213
1255
|
fixed++;
|
|
1214
1256
|
}
|
|
1257
|
+
} else if (INLINE_FIXERS[key]) {
|
|
1258
|
+
if (options.dryRun) {
|
|
1259
|
+
console.log(` [dry-run] Would fix: ${failedCheck.name} (${key})`);
|
|
1260
|
+
fixed++;
|
|
1261
|
+
} else {
|
|
1262
|
+
const didFix = INLINE_FIXERS[key](options.dir);
|
|
1263
|
+
if (didFix) {
|
|
1264
|
+
console.log(` ✅ Fixed: ${failedCheck.name}`);
|
|
1265
|
+
fixed++;
|
|
1266
|
+
} else {
|
|
1267
|
+
console.log(` ⏭️ Already fixed: ${failedCheck.name}`);
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1215
1270
|
} else {
|
|
1216
1271
|
console.log(` 📋 ${failedCheck.name} (manual fix needed)`);
|
|
1217
1272
|
console.log(` ${failedCheck.fix}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nerviq/cli",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.4",
|
|
4
4
|
"description": "The intelligent nervous system for AI coding agents — 2,431 checks across 8 platforms, 10 languages, and 62 domain packs. Audit, align, and amplify.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
package/src/activity.js
CHANGED
|
@@ -283,11 +283,14 @@ function formatHistory(dir) {
|
|
|
283
283
|
|
|
284
284
|
const lines = ['Score history (most recent first):', ''];
|
|
285
285
|
for (const entry of history) {
|
|
286
|
-
const
|
|
286
|
+
const dateStr = entry.createdAt || 'unknown';
|
|
287
|
+
const date = dateStr.split('T')[0] || 'unknown';
|
|
288
|
+
const time = dateStr.includes('T') ? dateStr.split('T')[1]?.substring(0, 5) || '' : '';
|
|
289
|
+
const dateDisplay = time ? `${date} ${time}` : date;
|
|
287
290
|
const score = entry.summary?.score ?? '?';
|
|
288
291
|
const passed = entry.summary?.passed ?? '?';
|
|
289
292
|
const total = entry.summary?.checkCount ?? '?';
|
|
290
|
-
lines.push(` ${
|
|
293
|
+
lines.push(` ${dateDisplay} ${score}/100 (${passed}/${total} passing)`);
|
|
291
294
|
}
|
|
292
295
|
|
|
293
296
|
const comparison = compareLatest(dir);
|
package/src/audit.js
CHANGED
|
@@ -769,9 +769,10 @@ function inferSuggestedNextCommand(result) {
|
|
|
769
769
|
}
|
|
770
770
|
|
|
771
771
|
const actionKeys = new Set((result.topNextActions || []).map(item => item.key));
|
|
772
|
+
const platFlag = result.platform && result.platform !== 'claude' ? ` --platform ${result.platform}` : '';
|
|
772
773
|
|
|
773
774
|
if (result.failed === 0) {
|
|
774
|
-
return
|
|
775
|
+
return `npx nerviq${platFlag} augment`;
|
|
775
776
|
}
|
|
776
777
|
|
|
777
778
|
if (
|
|
@@ -781,14 +782,14 @@ function inferSuggestedNextCommand(result) {
|
|
|
781
782
|
actionKeys.has('settingsPermissions') ||
|
|
782
783
|
actionKeys.has('permissionDeny')
|
|
783
784
|
) {
|
|
784
|
-
return
|
|
785
|
+
return `npx nerviq${platFlag} setup`;
|
|
785
786
|
}
|
|
786
787
|
|
|
787
788
|
if (result.score < 80) {
|
|
788
|
-
return
|
|
789
|
+
return `npx nerviq${platFlag} suggest-only`;
|
|
789
790
|
}
|
|
790
791
|
|
|
791
|
-
return
|
|
792
|
+
return `npx nerviq${platFlag} augment`;
|
|
792
793
|
}
|
|
793
794
|
|
|
794
795
|
function getPlatformScopeNote(spec, ctx) {
|
|
@@ -876,7 +877,11 @@ function printLiteAudit(result, dir) {
|
|
|
876
877
|
console.log(colorize(' ═══════════════════════════════════════', 'dim'));
|
|
877
878
|
console.log(colorize(` Scanning: ${dir}`, 'dim'));
|
|
878
879
|
console.log('');
|
|
879
|
-
|
|
880
|
+
if (result.detectedConfigFiles && result.detectedConfigFiles.length > 0) {
|
|
881
|
+
console.log(colorize(` Found: ${result.detectedConfigFiles.join(', ')}`, 'dim'));
|
|
882
|
+
}
|
|
883
|
+
console.log('');
|
|
884
|
+
console.log(` Score: ${colorize(`${result.score}/100`, 'bold')} (${result.passed}/${result.passed + result.failed} checks passing)`);
|
|
880
885
|
if (result.platformScopeNote) {
|
|
881
886
|
console.log(colorize(` Scope: ${result.platformScopeNote.message}`, 'dim'));
|
|
882
887
|
}
|
|
@@ -1128,6 +1133,22 @@ async function audit(options) {
|
|
|
1128
1133
|
platformCaveats,
|
|
1129
1134
|
recommendedDomainPacks,
|
|
1130
1135
|
};
|
|
1136
|
+
// Detect which AI config files are present
|
|
1137
|
+
const configFiles = [];
|
|
1138
|
+
const configChecks = [
|
|
1139
|
+
['CLAUDE.md', 'CLAUDE.md'], ['.claude/settings.json', '.claude/settings.json'],
|
|
1140
|
+
['AGENTS.md', 'AGENTS.md'], ['.cursorrules', '.cursorrules'],
|
|
1141
|
+
['.cursor/rules', '.cursor/rules/'], ['GEMINI.md', 'GEMINI.md'],
|
|
1142
|
+
['.windsurfrules', '.windsurfrules'], ['.aider.conf.yml', '.aider.conf.yml'],
|
|
1143
|
+
['opencode.json', 'opencode.json'], ['.mcp.json', '.mcp.json'],
|
|
1144
|
+
];
|
|
1145
|
+
for (const [file, label] of configChecks) {
|
|
1146
|
+
try {
|
|
1147
|
+
if (require('fs').existsSync(require('path').join(options.dir, file))) configFiles.push(label);
|
|
1148
|
+
} catch {}
|
|
1149
|
+
}
|
|
1150
|
+
result.detectedConfigFiles = configFiles;
|
|
1151
|
+
|
|
1131
1152
|
result.suggestedNextCommand = inferSuggestedNextCommand(result);
|
|
1132
1153
|
result.liteSummary = {
|
|
1133
1154
|
topNextActions: topNextActions.slice(0, 3),
|
package/src/doctor.js
CHANGED
|
@@ -120,15 +120,18 @@ function checkFreshnessGates() {
|
|
|
120
120
|
try {
|
|
121
121
|
const freshness = require(modulePath);
|
|
122
122
|
const gate = freshness.checkReleaseGate({});
|
|
123
|
+
const staleCount = (gate.stale || []).length;
|
|
124
|
+
const freshCount = (gate.fresh || []).length;
|
|
125
|
+
const totalCount = (gate.results || []).length;
|
|
123
126
|
results.push({
|
|
124
127
|
platform,
|
|
125
|
-
status:
|
|
126
|
-
fresh:
|
|
127
|
-
total:
|
|
128
|
-
stale:
|
|
129
|
-
detail:
|
|
130
|
-
? `All ${
|
|
131
|
-
: `${
|
|
128
|
+
status: staleCount === 0 ? 'pass' : 'warn',
|
|
129
|
+
fresh: freshCount,
|
|
130
|
+
total: totalCount,
|
|
131
|
+
stale: staleCount,
|
|
132
|
+
detail: staleCount === 0
|
|
133
|
+
? `All ${totalCount} P0 sources fresh`
|
|
134
|
+
: `${staleCount}/${totalCount} P0 sources unverified/stale`,
|
|
132
135
|
});
|
|
133
136
|
} catch (e) {
|
|
134
137
|
results.push({ platform, status: 'error', detail: e.message });
|
package/src/harmony/cli.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
const path = require('path');
|
|
11
11
|
const { generateStrategicAdvice, PLATFORM_STRENGTHS } = require('./advisor');
|
|
12
12
|
const { startHarmonyWatch, buildHarmonyWatchPlan } = require('./watch');
|
|
13
|
-
const { saveHarmonyState, loadHarmonyState, getHarmonyHistory, recordPlatformScore } = require('./memory');
|
|
13
|
+
const { saveHarmonyState, loadHarmonyState, getHarmonyHistory, recordPlatformScore, recordDrift } = require('./memory');
|
|
14
14
|
const { getHarmonyGovernanceSummary, formatHarmonyGovernanceReport } = require('./governance');
|
|
15
15
|
|
|
16
16
|
const COLORS = {
|
|
@@ -30,9 +30,34 @@ function resolveDir(options) {
|
|
|
30
30
|
* Collect audit results from all detectable platforms.
|
|
31
31
|
* Runs audit() for each platform in sequence (audit is async).
|
|
32
32
|
*/
|
|
33
|
+
/**
|
|
34
|
+
* Detect which platforms have config files present in the directory.
|
|
35
|
+
* Only these platforms will be audited in harmony commands.
|
|
36
|
+
*/
|
|
37
|
+
function detectPresentPlatforms(dir) {
|
|
38
|
+
const fs = require('fs');
|
|
39
|
+
const pathMod = require('path');
|
|
40
|
+
const exists = (f) => fs.existsSync(pathMod.join(dir, f));
|
|
41
|
+
|
|
42
|
+
const detected = [];
|
|
43
|
+
if (exists('CLAUDE.md') || exists('.claude/settings.json') || exists('.claude/CLAUDE.md')) detected.push('claude');
|
|
44
|
+
if (exists('AGENTS.md') || exists('.codex/config.toml')) detected.push('codex');
|
|
45
|
+
if (exists('GEMINI.md') || exists('.gemini/settings.json')) detected.push('gemini');
|
|
46
|
+
if (exists('.github/copilot-instructions.md')) detected.push('copilot');
|
|
47
|
+
if (exists('.cursorrules') || exists('.cursor/rules')) detected.push('cursor');
|
|
48
|
+
if (exists('.windsurfrules') || exists('.windsurf/rules')) detected.push('windsurf');
|
|
49
|
+
if (exists('.aider.conf.yml') || exists('.aiderignore')) detected.push('aider');
|
|
50
|
+
if (exists('opencode.json') || exists('.opencode')) detected.push('opencode');
|
|
51
|
+
|
|
52
|
+
// AGENTS.md is shared by codex, copilot, and opencode — only add if not already detected via platform-specific file
|
|
53
|
+
if (exists('AGENTS.md') && !detected.includes('codex')) detected.push('codex');
|
|
54
|
+
|
|
55
|
+
return detected.length > 0 ? detected : ['claude']; // default to claude if nothing found
|
|
56
|
+
}
|
|
57
|
+
|
|
33
58
|
async function collectPlatformAudits(dir) {
|
|
34
59
|
const { audit } = require('../audit');
|
|
35
|
-
const platforms =
|
|
60
|
+
const platforms = detectPresentPlatforms(dir);
|
|
36
61
|
const results = [];
|
|
37
62
|
|
|
38
63
|
for (const platform of platforms) {
|
|
@@ -84,6 +109,29 @@ async function runHarmonyAudit(options) {
|
|
|
84
109
|
} catch (_e) { /* memory write optional */ }
|
|
85
110
|
}
|
|
86
111
|
|
|
112
|
+
// Detect drift: compare current scores to last recorded scores
|
|
113
|
+
try {
|
|
114
|
+
const state = loadHarmonyState(dir);
|
|
115
|
+
const prevScores = state.platformScores || [];
|
|
116
|
+
for (const audit of platformAudits) {
|
|
117
|
+
const prevEntries = (prevScores || []).filter(e => e.platform === audit.platform);
|
|
118
|
+
if (prevEntries.length >= 2) {
|
|
119
|
+
// Compare to the second-to-last entry (last is what we just recorded)
|
|
120
|
+
const prev = prevEntries[prevEntries.length - 2];
|
|
121
|
+
const delta = audit.score - (prev.score || 0);
|
|
122
|
+
if (Math.abs(delta) >= 5) {
|
|
123
|
+
recordDrift(dir, {
|
|
124
|
+
platform: audit.platform,
|
|
125
|
+
driftScore: delta,
|
|
126
|
+
driftedFields: [`score: ${prev.score} → ${audit.score}`],
|
|
127
|
+
});
|
|
128
|
+
const sign = delta > 0 ? '+' : '';
|
|
129
|
+
console.log(c(` ${audit.platform}: ${sign}${delta} since last audit`, delta > 0 ? 'green' : 'red'));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
} catch (_e) { /* drift recording is optional */ }
|
|
134
|
+
|
|
87
135
|
// Average score
|
|
88
136
|
const avgScore = Math.round(platformAudits.reduce((sum, a) => sum + (a.score || 0), 0) / platformAudits.length);
|
|
89
137
|
const avgColor = avgScore >= 70 ? 'green' : avgScore >= 40 ? 'yellow' : 'red';
|
package/src/migrate.js
CHANGED
|
@@ -248,7 +248,7 @@ async function runMigrate({ dir = process.cwd(), platform, from: fromVersion, to
|
|
|
248
248
|
|
|
249
249
|
const platformMigrations = MIGRATIONS[platform];
|
|
250
250
|
if (!platformMigrations) {
|
|
251
|
-
throw new Error(`No migrations available for platform '${platform}'. Supported: ${Object.keys(MIGRATIONS).join(', ')}`);
|
|
251
|
+
throw new Error(`No migrations available for platform '${platform}'. Supported: ${Object.keys(MIGRATIONS).join(', ')}.\nUsage: nerviq migrate --platform <platform> [--from <version> --to <version>]`);
|
|
252
252
|
}
|
|
253
253
|
|
|
254
254
|
// Determine migration key
|
package/src/setup.js
CHANGED
|
@@ -1242,7 +1242,10 @@ async function setup(options) {
|
|
|
1242
1242
|
log(` \x1b[2m ${skipped} files already exist and were preserved.\x1b[0m`);
|
|
1243
1243
|
log(' \x1b[2m We never overwrite your existing config — your setup is kept.\x1b[0m');
|
|
1244
1244
|
} else if (created > 0) {
|
|
1245
|
-
log(` \x1b[1m${created} files created
|
|
1245
|
+
log(` \x1b[1m${created} files created:\x1b[0m`);
|
|
1246
|
+
for (const f of writtenFiles) {
|
|
1247
|
+
log(` \x1b[32m + ${f}\x1b[0m`);
|
|
1248
|
+
}
|
|
1246
1249
|
if (skipped > 0) {
|
|
1247
1250
|
log(` \x1b[2m${skipped} existing files preserved (not overwritten).\x1b[0m`);
|
|
1248
1251
|
}
|