@nerviq/cli 1.6.2 → 1.6.3
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/doctor.js +10 -7
- package/src/harmony/cli.js +50 -2
- package/src/migrate.js +1 -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.3",
|
|
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/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
|