@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 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 fixable = failedResults.filter(r => TECHNIQUES[r.key] && TECHNIQUES[r.key].template);
1165
- const nonFixable = failedResults.filter(r => !TECHNIQUES[r.key] || !TECHNIQUES[r.key].template);
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.2",
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: gate.stale.length === 0 ? 'pass' : 'warn',
126
- fresh: gate.fresh.length,
127
- total: gate.results.length,
128
- stale: gate.stale.length,
129
- detail: gate.stale.length === 0
130
- ? `All ${gate.results.length} P0 sources fresh`
131
- : `${gate.stale.length}/${gate.results.length} P0 sources unverified/stale`,
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 });
@@ -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 = ['claude', 'codex', 'gemini', 'copilot', 'cursor', 'windsurf', 'aider', 'opencode'];
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