@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 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.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 date = entry.createdAt?.split('T')[0] || 'unknown';
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(` ${date} ${score}/100 (${passed}/${total} passing)`);
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 'npx nerviq augment';
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 'npx nerviq setup';
785
+ return `npx nerviq${platFlag} setup`;
785
786
  }
786
787
 
787
788
  if (result.score < 80) {
788
- return 'npx nerviq suggest-only';
789
+ return `npx nerviq${platFlag} suggest-only`;
789
790
  }
790
791
 
791
- return 'npx nerviq augment';
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
- console.log(` Score: ${colorize(`${result.score}/100`, 'bold')}`);
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: 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
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.\x1b[0m`);
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
  }