@nerviq/cli 1.27.0 → 1.29.0
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/CHANGELOG.md +1493 -0
- package/README.md +2 -2
- package/SECURITY.md +82 -0
- package/contracts/audit-webhook-event.schema.json +138 -0
- package/contracts/pack-contract.schema.json +15 -0
- package/contracts/technique-contract.schema.json +18 -0
- package/docs/ARCHITECTURE.md +74 -0
- package/docs/api-reference.md +356 -0
- package/docs/autofix.md +64 -0
- package/docs/bitbucket-pipe.yml +57 -0
- package/docs/case-studies.md +149 -0
- package/docs/category-definition-kit.md +56 -0
- package/docs/ci-integration.md +127 -0
- package/docs/claude-code-style.md +24 -0
- package/docs/claude-maintainer-ops.md +19 -0
- package/docs/external-validation.md +78 -0
- package/docs/first-tier-integration-gate.md +59 -0
- package/docs/getting-started.md +119 -0
- package/docs/gitlab-ci-template.yml +54 -0
- package/docs/index.html +597 -0
- package/docs/integration-contracts.md +287 -0
- package/docs/license-faq.md +53 -0
- package/docs/maintenance.md +155 -0
- package/docs/methodology.md +236 -0
- package/docs/new-platform-guide.md +202 -0
- package/docs/open-vsx-publishing.md +46 -0
- package/docs/platform-change-ingestion.md +46 -0
- package/docs/plugins.md +101 -0
- package/docs/pre-commit.md +58 -0
- package/docs/security-model.md +63 -0
- package/docs/shallow-risk.md +246 -0
- package/docs/versioning-policy.md +63 -0
- package/docs/why-nerviq.md +82 -0
- package/package.json +7 -2
- package/sdk/README.md +190 -0
- package/src/codex/setup.js +3 -2
- package/src/gemini/setup.js +3 -2
- package/src/init.js +4 -3
- package/src/opencode/context.js +42 -3
- package/src/opencode/techniques.js +198 -142
- package/src/output-icons.js +44 -0
- package/src/setup/runtime.js +6 -5
- package/src/setup.js +4 -3
- package/src/shallow-risk/patterns/agent-config-missing-file.js +254 -9
- package/src/shallow-risk/shared.js +135 -7
package/src/gemini/setup.js
CHANGED
|
@@ -6,6 +6,7 @@ const { STACKS } = require('../techniques');
|
|
|
6
6
|
const { writeActivityArtifact, writeRollbackArtifact } = require('../activity');
|
|
7
7
|
const { GeminiProjectContext } = require('./context');
|
|
8
8
|
const { recommendGeminiMcpPacks, packToJson } = require('./mcp-packs');
|
|
9
|
+
const { icon } = require('../output-icons');
|
|
9
10
|
|
|
10
11
|
function detectScripts(ctx) {
|
|
11
12
|
const pkg = ctx.jsonFile('package.json');
|
|
@@ -622,14 +623,14 @@ async function setupGemini(options) {
|
|
|
622
623
|
const fullPath = path.join(options.dir, file.path);
|
|
623
624
|
if (fs.existsSync(fullPath)) {
|
|
624
625
|
preservedFiles.push(file.path);
|
|
625
|
-
log(` \x1b[2m
|
|
626
|
+
log(` \x1b[2m${icon('skip')} Skipped ${file.path} (already exists - your version is kept)\x1b[0m`);
|
|
626
627
|
continue;
|
|
627
628
|
}
|
|
628
629
|
|
|
629
630
|
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
630
631
|
fs.writeFileSync(fullPath, file.content, 'utf8');
|
|
631
632
|
writtenFiles.push(file.path);
|
|
632
|
-
log(` \x1b[32m
|
|
633
|
+
log(` \x1b[32m${icon('ok')}\x1b[0m Created ${file.path}`);
|
|
633
634
|
}
|
|
634
635
|
|
|
635
636
|
const skippedSet = new Set(preservedFiles);
|
package/src/init.js
CHANGED
|
@@ -6,6 +6,7 @@ const { audit } = require('./audit');
|
|
|
6
6
|
const { setup } = require('./setup');
|
|
7
7
|
const { ProjectContext } = require('./context');
|
|
8
8
|
const { STACKS } = require('./techniques');
|
|
9
|
+
const { icon } = require('./output-icons');
|
|
9
10
|
|
|
10
11
|
const PLATFORM_LABELS = {
|
|
11
12
|
claude: 'Claude Code',
|
|
@@ -136,10 +137,10 @@ async function runInit(dir, flags) {
|
|
|
136
137
|
});
|
|
137
138
|
|
|
138
139
|
for (const f of setupResult.writtenFiles) {
|
|
139
|
-
console.log(` ${green}
|
|
140
|
+
console.log(` ${green}${icon('ok')}${reset} Created ${f}`);
|
|
140
141
|
}
|
|
141
142
|
for (const f of setupResult.preservedFiles) {
|
|
142
|
-
console.log(` ${dim}
|
|
143
|
+
console.log(` ${dim}${icon('skip')} Kept ${f} (already exists)${reset}`);
|
|
143
144
|
}
|
|
144
145
|
|
|
145
146
|
// --- Run additional platform setups ---
|
|
@@ -153,7 +154,7 @@ async function runInit(dir, flags) {
|
|
|
153
154
|
mcpPacks: [],
|
|
154
155
|
});
|
|
155
156
|
for (const f of extraResult.writtenFiles) {
|
|
156
|
-
console.log(` ${green}
|
|
157
|
+
console.log(` ${green}${icon('ok')}${reset} Created ${f}`);
|
|
157
158
|
}
|
|
158
159
|
} catch {
|
|
159
160
|
// Platform setup not available, skip
|
package/src/opencode/context.js
CHANGED
|
@@ -203,12 +203,51 @@ class OpenCodeProjectContext extends ProjectContext {
|
|
|
203
203
|
}
|
|
204
204
|
|
|
205
205
|
skillDirs() {
|
|
206
|
-
const
|
|
207
|
-
|
|
206
|
+
const names = new Set();
|
|
207
|
+
const roots = [
|
|
208
|
+
path.join('.opencode', 'skills'),
|
|
209
|
+
path.join('.opencode', 'skill'),
|
|
210
|
+
path.join('.claude', 'skills'),
|
|
211
|
+
path.join('.agents', 'skills'),
|
|
212
|
+
];
|
|
213
|
+
|
|
214
|
+
for (const root of roots) {
|
|
215
|
+
const fullRoot = path.join(this.dir, root);
|
|
216
|
+
for (const entry of listDirs(fullRoot)) {
|
|
217
|
+
if (this.fileContent(path.join(root, entry.name, 'SKILL.md'))) {
|
|
218
|
+
names.add(entry.name);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Legacy NERVIQ compatibility: older generated fixtures placed skills
|
|
224
|
+
// under .opencode/commands/<name>/SKILL.md before OpenCode's native
|
|
225
|
+
// .opencode/skills/ path was verified.
|
|
226
|
+
const legacyCommandsRoot = path.join(this.dir, '.opencode', 'commands');
|
|
227
|
+
for (const entry of listDirs(legacyCommandsRoot)) {
|
|
228
|
+
if (this.fileContent(path.join('.opencode', 'commands', entry.name, 'SKILL.md'))) {
|
|
229
|
+
names.add(entry.name);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return [...names];
|
|
208
234
|
}
|
|
209
235
|
|
|
210
236
|
skillMetadata(name) {
|
|
211
|
-
|
|
237
|
+
const candidates = [
|
|
238
|
+
path.join('.opencode', 'skills', name, 'SKILL.md'),
|
|
239
|
+
path.join('.opencode', 'skill', name, 'SKILL.md'),
|
|
240
|
+
path.join('.claude', 'skills', name, 'SKILL.md'),
|
|
241
|
+
path.join('.agents', 'skills', name, 'SKILL.md'),
|
|
242
|
+
path.join('.opencode', 'commands', name, 'SKILL.md'),
|
|
243
|
+
];
|
|
244
|
+
|
|
245
|
+
for (const candidate of candidates) {
|
|
246
|
+
const content = this.fileContent(candidate);
|
|
247
|
+
if (content) return content;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return null;
|
|
212
251
|
}
|
|
213
252
|
|
|
214
253
|
themeFiles() {
|
|
@@ -163,15 +163,69 @@ function hasCommandMention(content, category) {
|
|
|
163
163
|
return false;
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
-
function docsBundle(ctx) {
|
|
167
|
-
return `${agentsContent(ctx)}\n${ctx.fileContent('README.md') || ''}`;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
166
|
+
function docsBundle(ctx) {
|
|
167
|
+
return `${agentsContent(ctx)}\n${ctx.fileContent('README.md') || ''}`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function hasExplicitModelSetting(value) {
|
|
171
|
+
if (!value || typeof value !== 'object') return false;
|
|
172
|
+
if (Array.isArray(value)) return value.some((item) => hasExplicitModelSetting(item));
|
|
173
|
+
if (typeof value.model === 'string' && value.model.trim()) return true;
|
|
174
|
+
return Object.values(value).some((item) => hasExplicitModelSetting(item));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function configuredPluginRefs(ctx) {
|
|
178
|
+
const config = ctx.configJson();
|
|
179
|
+
if (!config.ok || !config.data) return [];
|
|
180
|
+
const refs = config.data.plugin || config.data.plugins || [];
|
|
181
|
+
return Array.isArray(refs) ? refs.filter(Boolean) : [];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function extractSkillDescription(content) {
|
|
185
|
+
if (!content) return null;
|
|
186
|
+
const frontmatter = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
187
|
+
if (!frontmatter) return null;
|
|
188
|
+
|
|
189
|
+
const lines = frontmatter[1].split(/\r?\n/);
|
|
190
|
+
for (let index = 0; index < lines.length; index++) {
|
|
191
|
+
const line = lines[index];
|
|
192
|
+
const match = line.match(/^description:\s*(.*)$/);
|
|
193
|
+
if (!match) continue;
|
|
194
|
+
|
|
195
|
+
const rawValue = match[1].trim();
|
|
196
|
+
if (rawValue === '|' || rawValue === '>') {
|
|
197
|
+
const block = [];
|
|
198
|
+
for (let next = index + 1; next < lines.length; next++) {
|
|
199
|
+
const candidate = lines[next];
|
|
200
|
+
if (candidate.trim() && !/^\s/.test(candidate)) break;
|
|
201
|
+
block.push(candidate.replace(/^\s{2}/, ''));
|
|
202
|
+
}
|
|
203
|
+
return block.join('\n').trim();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return rawValue.replace(/^['"]|['"]$/g, '').trim();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function preferredSkillRoot(ctx) {
|
|
213
|
+
const candidates = [
|
|
214
|
+
'.opencode/skills',
|
|
215
|
+
'.opencode/skill',
|
|
216
|
+
'.claude/skills',
|
|
217
|
+
'.agents/skills',
|
|
218
|
+
'.opencode/commands',
|
|
219
|
+
];
|
|
220
|
+
const found = candidates.find((candidate) => ctx.hasDir(candidate));
|
|
221
|
+
return `${(found || '.opencode/skills').replace(/\\/g, '/')}/`;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function repoLooksRegulated(ctx) {
|
|
225
|
+
const filenames = ctx.files.join('\n');
|
|
226
|
+
const packageJson = ctx.fileContent('package.json') || '';
|
|
227
|
+
const readme = ctx.fileContent('README.md') || '';
|
|
228
|
+
const combined = `${filenames}\n${packageJson}\n${readme}`;
|
|
175
229
|
|
|
176
230
|
const strongSignals = /\bhipaa\b|\bphi\b|\bpci\b|\bsoc2\b|\biso[- ]?27001\b|\bcompliance\b|\bhealth(?:care)?\b|\bmedical\b|\bbank(?:ing)?\b|\bpayments?\b|\bfintech\b/i;
|
|
177
231
|
if (strongSignals.test(combined)) return true;
|
|
@@ -364,35 +418,35 @@ const OPENCODE_TECHNIQUES = {
|
|
|
364
418
|
line: () => 1,
|
|
365
419
|
},
|
|
366
420
|
|
|
367
|
-
opencodeModelExplicit: {
|
|
368
|
-
id: 'OC-B04',
|
|
369
|
-
name: 'model is set explicitly (not relying on silent default)',
|
|
370
|
-
check: (ctx) => {
|
|
371
|
-
const config = ctx.configJson();
|
|
372
|
-
if (!config.ok || !config.data) return null;
|
|
373
|
-
return
|
|
374
|
-
},
|
|
375
|
-
impact: 'medium',
|
|
376
|
-
rating: 3,
|
|
377
|
-
category: 'config',
|
|
378
|
-
fix: 'Set "model" explicitly in opencode.json to avoid relying on silent provider defaults.',
|
|
421
|
+
opencodeModelExplicit: {
|
|
422
|
+
id: 'OC-B04',
|
|
423
|
+
name: 'model is set explicitly (not relying on silent default)',
|
|
424
|
+
check: (ctx) => {
|
|
425
|
+
const config = ctx.configJson();
|
|
426
|
+
if (!config.ok || !config.data) return null;
|
|
427
|
+
return hasExplicitModelSetting(config.data) ? true : null;
|
|
428
|
+
},
|
|
429
|
+
impact: 'medium',
|
|
430
|
+
rating: 3,
|
|
431
|
+
category: 'config',
|
|
432
|
+
fix: 'Set "model" explicitly in opencode.json to avoid relying on silent provider defaults.',
|
|
379
433
|
template: 'opencode-config',
|
|
380
434
|
file: (ctx) => configFileName(ctx),
|
|
381
435
|
line: () => null,
|
|
382
436
|
},
|
|
383
437
|
|
|
384
|
-
opencodeSmallModelSet: {
|
|
385
|
-
id: 'OC-B05',
|
|
386
|
-
name: 'small_model is set for task delegation',
|
|
387
|
-
check: (ctx) => {
|
|
388
|
-
const config = ctx.configJson();
|
|
389
|
-
if (!config.ok || !config.data) return null;
|
|
390
|
-
return
|
|
391
|
-
},
|
|
392
|
-
impact: 'medium',
|
|
393
|
-
rating: 3,
|
|
394
|
-
category: 'config',
|
|
395
|
-
fix: 'Set "small_model" in opencode.json for efficient task delegation and cost control.',
|
|
438
|
+
opencodeSmallModelSet: {
|
|
439
|
+
id: 'OC-B05',
|
|
440
|
+
name: 'small_model is set for task delegation',
|
|
441
|
+
check: (ctx) => {
|
|
442
|
+
const config = ctx.configJson();
|
|
443
|
+
if (!config.ok || !config.data) return null;
|
|
444
|
+
return config.data.small_model ? true : null;
|
|
445
|
+
},
|
|
446
|
+
impact: 'medium',
|
|
447
|
+
rating: 3,
|
|
448
|
+
category: 'config',
|
|
449
|
+
fix: 'Set "small_model" in opencode.json for efficient task delegation and cost control.',
|
|
396
450
|
template: 'opencode-config',
|
|
397
451
|
file: (ctx) => configFileName(ctx),
|
|
398
452
|
line: () => null,
|
|
@@ -1104,22 +1158,22 @@ const OPENCODE_TECHNIQUES = {
|
|
|
1104
1158
|
// I. Skills (5 checks)
|
|
1105
1159
|
// ==============================
|
|
1106
1160
|
|
|
1107
|
-
opencodeSkillDirsExist: {
|
|
1108
|
-
id: 'OC-I01',
|
|
1109
|
-
name: 'Skill directories exist
|
|
1110
|
-
check: (ctx) => {
|
|
1111
|
-
const skillDirs = ctx.skillDirs();
|
|
1112
|
-
if (skillDirs.length === 0) return null;
|
|
1113
|
-
return skillDirs.length > 0;
|
|
1114
|
-
},
|
|
1115
|
-
impact: 'medium',
|
|
1116
|
-
rating: 3,
|
|
1117
|
-
category: 'skills',
|
|
1118
|
-
fix: 'Create skill directories under
|
|
1119
|
-
template: 'opencode-skills',
|
|
1120
|
-
file: () =>
|
|
1121
|
-
line: () => null,
|
|
1122
|
-
},
|
|
1161
|
+
opencodeSkillDirsExist: {
|
|
1162
|
+
id: 'OC-I01',
|
|
1163
|
+
name: 'Skill directories exist in supported OpenCode paths',
|
|
1164
|
+
check: (ctx) => {
|
|
1165
|
+
const skillDirs = ctx.skillDirs();
|
|
1166
|
+
if (skillDirs.length === 0) return null;
|
|
1167
|
+
return skillDirs.length > 0;
|
|
1168
|
+
},
|
|
1169
|
+
impact: 'medium',
|
|
1170
|
+
rating: 3,
|
|
1171
|
+
category: 'skills',
|
|
1172
|
+
fix: 'Create skill directories under `.opencode/skills/` with `SKILL.md` files. `.claude/skills/` and `.agents/skills/` remain compatible, and older `.opencode/commands/<name>/SKILL.md` layouts are still tolerated for backwards compatibility.',
|
|
1173
|
+
template: 'opencode-skills',
|
|
1174
|
+
file: (ctx) => preferredSkillRoot(ctx),
|
|
1175
|
+
line: () => null,
|
|
1176
|
+
},
|
|
1123
1177
|
|
|
1124
1178
|
opencodeSkillFrontmatter: {
|
|
1125
1179
|
id: 'OC-I02',
|
|
@@ -1133,15 +1187,15 @@ const OPENCODE_TECHNIQUES = {
|
|
|
1133
1187
|
if (!/^#\s+/m.test(content)) return false;
|
|
1134
1188
|
}
|
|
1135
1189
|
return true;
|
|
1136
|
-
},
|
|
1137
|
-
impact: 'high',
|
|
1138
|
-
rating: 4,
|
|
1139
|
-
category: 'skills',
|
|
1140
|
-
fix: 'Each SKILL.md needs a title (# heading) and description for skill invocation.',
|
|
1141
|
-
template: 'opencode-skills',
|
|
1142
|
-
file: () =>
|
|
1143
|
-
line: () => null,
|
|
1144
|
-
},
|
|
1190
|
+
},
|
|
1191
|
+
impact: 'high',
|
|
1192
|
+
rating: 4,
|
|
1193
|
+
category: 'skills',
|
|
1194
|
+
fix: 'Each SKILL.md needs a title (# heading) and description for skill invocation.',
|
|
1195
|
+
template: 'opencode-skills',
|
|
1196
|
+
file: (ctx) => preferredSkillRoot(ctx),
|
|
1197
|
+
line: () => null,
|
|
1198
|
+
},
|
|
1145
1199
|
|
|
1146
1200
|
opencodeSkillKebabCase: {
|
|
1147
1201
|
id: 'OC-I03',
|
|
@@ -1151,54 +1205,56 @@ const OPENCODE_TECHNIQUES = {
|
|
|
1151
1205
|
if (skillDirs.length === 0) return null;
|
|
1152
1206
|
return skillDirs.every(name => /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name));
|
|
1153
1207
|
},
|
|
1154
|
-
impact: 'medium',
|
|
1155
|
-
rating: 2,
|
|
1156
|
-
category: 'skills',
|
|
1157
|
-
fix: 'Prefer kebab-case for skill names, but treat it as a style recommendation rather than a hard runtime requirement. Current runtime still discovered underscore-based names.',
|
|
1158
|
-
template: 'opencode-skills',
|
|
1159
|
-
file: () =>
|
|
1160
|
-
line: () => null,
|
|
1161
|
-
},
|
|
1162
|
-
|
|
1163
|
-
opencodeSkillDescriptionBounded: {
|
|
1164
|
-
id: 'OC-I04',
|
|
1165
|
-
name: 'Skill descriptions are bounded for implicit invocation context cost',
|
|
1166
|
-
check: (ctx) => {
|
|
1167
|
-
const skillDirs = ctx.skillDirs();
|
|
1168
|
-
if (skillDirs.length === 0) return null;
|
|
1169
|
-
for (const name of skillDirs) {
|
|
1170
|
-
const content = ctx.skillMetadata(name);
|
|
1171
|
-
if (!content) continue;
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
const
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1208
|
+
impact: 'medium',
|
|
1209
|
+
rating: 2,
|
|
1210
|
+
category: 'skills',
|
|
1211
|
+
fix: 'Prefer kebab-case for skill names, but treat it as a style recommendation rather than a hard runtime requirement. Current runtime still discovered underscore-based names.',
|
|
1212
|
+
template: 'opencode-skills',
|
|
1213
|
+
file: (ctx) => preferredSkillRoot(ctx),
|
|
1214
|
+
line: () => null,
|
|
1215
|
+
},
|
|
1216
|
+
|
|
1217
|
+
opencodeSkillDescriptionBounded: {
|
|
1218
|
+
id: 'OC-I04',
|
|
1219
|
+
name: 'Skill descriptions are bounded for implicit invocation context cost',
|
|
1220
|
+
check: (ctx) => {
|
|
1221
|
+
const skillDirs = ctx.skillDirs();
|
|
1222
|
+
if (skillDirs.length === 0) return null;
|
|
1223
|
+
for (const name of skillDirs) {
|
|
1224
|
+
const content = ctx.skillMetadata(name);
|
|
1225
|
+
if (!content) continue;
|
|
1226
|
+
const description = extractSkillDescription(content);
|
|
1227
|
+
if (description && description.length > 3000) return false;
|
|
1228
|
+
}
|
|
1229
|
+
return true;
|
|
1230
|
+
},
|
|
1231
|
+
impact: 'medium',
|
|
1232
|
+
rating: 3,
|
|
1233
|
+
category: 'skills',
|
|
1234
|
+
fix: 'Keep SKILL.md descriptions under 3000 characters to manage implicit invocation context cost.',
|
|
1235
|
+
template: 'opencode-skills',
|
|
1236
|
+
file: (ctx) => preferredSkillRoot(ctx),
|
|
1237
|
+
line: () => null,
|
|
1238
|
+
},
|
|
1239
|
+
|
|
1240
|
+
opencodeSkillCompatPaths: {
|
|
1241
|
+
id: 'OC-I05',
|
|
1242
|
+
name: 'OpenCode skill discovery accepts native and compatible skill trees',
|
|
1243
|
+
check: (ctx) => {
|
|
1244
|
+
const hasNativeSkillTree = ctx.hasDir('.opencode/skills') || ctx.hasDir('.opencode/skill');
|
|
1245
|
+
const hasCompatibleSkillTree = ctx.hasDir('.claude/skills') || ctx.hasDir('.agents/skills');
|
|
1246
|
+
const hasLegacyCommandsSkills = ctx.hasDir('.opencode/commands') && ctx.skillDirs().length > 0;
|
|
1247
|
+
if (!hasNativeSkillTree && !hasCompatibleSkillTree && !hasLegacyCommandsSkills) return null;
|
|
1248
|
+
return hasNativeSkillTree || hasCompatibleSkillTree || hasLegacyCommandsSkills;
|
|
1249
|
+
},
|
|
1250
|
+
impact: 'medium',
|
|
1251
|
+
rating: 3,
|
|
1252
|
+
category: 'skills',
|
|
1253
|
+
fix: 'Use `.opencode/skills/` for native OpenCode skills. `.claude/skills/` and `.agents/skills/` remain compatible, and older `.opencode/commands/<name>/SKILL.md` layouts are kept as legacy compatibility only.',
|
|
1254
|
+
template: 'opencode-skills',
|
|
1255
|
+
file: (ctx) => preferredSkillRoot(ctx),
|
|
1256
|
+
line: () => null,
|
|
1257
|
+
},
|
|
1202
1258
|
|
|
1203
1259
|
// ==============================
|
|
1204
1260
|
// J. Agents & Subagents (4 checks)
|
|
@@ -1498,42 +1554,42 @@ const OPENCODE_TECHNIQUES = {
|
|
|
1498
1554
|
line: () => null,
|
|
1499
1555
|
},
|
|
1500
1556
|
|
|
1501
|
-
opencodeConfigKeysFresh: {
|
|
1502
|
-
id: 'OC-N02',
|
|
1503
|
-
name: 'Config references current OpenCode features (no removed or renamed keys)',
|
|
1504
|
-
check: (ctx) => {
|
|
1505
|
-
const docs = docsBundle(ctx);
|
|
1506
|
-
const config = ctx.configContent();
|
|
1507
|
-
if (!docs.trim() && !config) return null;
|
|
1508
|
-
const combined = `${docs}\n${config || ''}`;
|
|
1509
|
-
return
|
|
1510
|
-
},
|
|
1511
|
-
impact: 'medium',
|
|
1512
|
-
rating: 3,
|
|
1513
|
-
category: 'release-freshness',
|
|
1514
|
-
fix: 'Update stale OpenCode references. Use `opencode.json`/`opencode.jsonc
|
|
1515
|
-
template: 'opencode-config',
|
|
1516
|
-
file: (ctx) => configFileName(ctx),
|
|
1517
|
-
line: () => null,
|
|
1518
|
-
},
|
|
1519
|
-
|
|
1520
|
-
opencodePropagationCompleteness: {
|
|
1521
|
-
id: 'OC-N03',
|
|
1522
|
-
name: 'No dangling surface references (plugins, skills, MCP mentioned but not defined)',
|
|
1523
|
-
check: (ctx) => {
|
|
1524
|
-
const agents = agentsContent(ctx);
|
|
1525
|
-
if (!agents) return null;
|
|
1526
|
-
const issues = [];
|
|
1527
|
-
if (/\bplugins?\b/i.test(agents) && ctx.pluginFiles().length === 0) {
|
|
1528
|
-
issues.push('plugins referenced but .opencode/plugins/ empty');
|
|
1529
|
-
}
|
|
1530
|
-
if (/\bskills?\b/i.test(agents) &&
|
|
1531
|
-
issues.push('skills referenced but .opencode/
|
|
1532
|
-
}
|
|
1533
|
-
const config = ctx.configJson();
|
|
1534
|
-
if (config.ok && config.data && /\bmcp\b/i.test(agents)) {
|
|
1535
|
-
const mcp = config.data.mcp || {};
|
|
1536
|
-
if (Object.keys(mcp).length === 0) {
|
|
1557
|
+
opencodeConfigKeysFresh: {
|
|
1558
|
+
id: 'OC-N02',
|
|
1559
|
+
name: 'Config references current OpenCode features (no removed or renamed keys)',
|
|
1560
|
+
check: (ctx) => {
|
|
1561
|
+
const docs = docsBundle(ctx);
|
|
1562
|
+
const config = ctx.configContent();
|
|
1563
|
+
if (!docs.trim() && !config) return null;
|
|
1564
|
+
const combined = `${docs}\n${config || ''}`;
|
|
1565
|
+
return !/(?:^|[\s`"'(])~\/\.opencode\.json\b|(?:^|[\s`"'(])\.opencode\/config\.json\b|mode\s*->\s*agent|CLAUDE\.md fallback/i.test(combined);
|
|
1566
|
+
},
|
|
1567
|
+
impact: 'medium',
|
|
1568
|
+
rating: 3,
|
|
1569
|
+
category: 'release-freshness',
|
|
1570
|
+
fix: 'Update stale OpenCode references. Use `opencode.json`/`opencode.jsonc` plus the current `.opencode/{agents,commands,plugins,skills}/` directory layout. Treat legacy `~/.opencode.json`, `.opencode/config.json`, and `CLAUDE.md` fallback claims as stale unless you have fresh runtime proof.',
|
|
1571
|
+
template: 'opencode-config',
|
|
1572
|
+
file: (ctx) => configFileName(ctx),
|
|
1573
|
+
line: () => null,
|
|
1574
|
+
},
|
|
1575
|
+
|
|
1576
|
+
opencodePropagationCompleteness: {
|
|
1577
|
+
id: 'OC-N03',
|
|
1578
|
+
name: 'No dangling surface references (plugins, skills, MCP mentioned but not defined)',
|
|
1579
|
+
check: (ctx) => {
|
|
1580
|
+
const agents = agentsContent(ctx);
|
|
1581
|
+
if (!agents) return null;
|
|
1582
|
+
const issues = [];
|
|
1583
|
+
if (/\bplugins?\b/i.test(agents) && ctx.pluginFiles().length === 0 && configuredPluginRefs(ctx).length === 0) {
|
|
1584
|
+
issues.push('plugins referenced but .opencode/plugins/ empty');
|
|
1585
|
+
}
|
|
1586
|
+
if (/\bskills?\b/i.test(agents) && ctx.skillDirs().length === 0) {
|
|
1587
|
+
issues.push('skills referenced but no supported skill tree found (.opencode/skills, .claude/skills, .agents/skills, or legacy .opencode/commands/<name>/SKILL.md)');
|
|
1588
|
+
}
|
|
1589
|
+
const config = ctx.configJson();
|
|
1590
|
+
if (config.ok && config.data && /\bmcp\b/i.test(agents)) {
|
|
1591
|
+
const mcp = config.data.mcp || {};
|
|
1592
|
+
if (Object.keys(mcp).length === 0) {
|
|
1537
1593
|
issues.push('MCP referenced in AGENTS.md but no MCP servers in config');
|
|
1538
1594
|
}
|
|
1539
1595
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const ASCII_ENV = 'NERVIQ_ASCII_OUTPUT';
|
|
2
|
+
|
|
3
|
+
const ASCII_TRUE = new Set(['1', 'true', 'yes', 'on']);
|
|
4
|
+
const ASCII_FALSE = new Set(['0', 'false', 'no', 'off']);
|
|
5
|
+
|
|
6
|
+
const ICONS = {
|
|
7
|
+
ok: { emoji: '✅', ascii: '[OK]' },
|
|
8
|
+
fail: { emoji: '❌', ascii: '[FAIL]' },
|
|
9
|
+
warn: { emoji: '⚠', ascii: '[WARN]' },
|
|
10
|
+
skip: { emoji: '⏭️', ascii: '[SKIP]' },
|
|
11
|
+
delete: { emoji: '🗑️', ascii: '[DEL]' },
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function parseAsciiOverride(env = process.env) {
|
|
15
|
+
const raw = env && env[ASCII_ENV];
|
|
16
|
+
if (typeof raw !== 'string') return null;
|
|
17
|
+
const normalized = raw.trim().toLowerCase();
|
|
18
|
+
if (ASCII_TRUE.has(normalized)) return true;
|
|
19
|
+
if (ASCII_FALSE.has(normalized)) return false;
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function shouldUseAsciiOutput(options = {}) {
|
|
24
|
+
const env = options.env || process.env;
|
|
25
|
+
const platform = options.platform || process.platform;
|
|
26
|
+
const stream = options.stream || process.stdout;
|
|
27
|
+
const override = parseAsciiOverride(env);
|
|
28
|
+
if (override !== null) return override;
|
|
29
|
+
if (!stream || stream.isTTY === false) return true;
|
|
30
|
+
return platform === 'win32';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function icon(name, options = {}) {
|
|
34
|
+
const token = ICONS[name];
|
|
35
|
+
if (!token) {
|
|
36
|
+
throw new Error(`Unknown icon token: ${name}`);
|
|
37
|
+
}
|
|
38
|
+
return shouldUseAsciiOutput(options) ? token.ascii : token.emoji;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = {
|
|
42
|
+
icon,
|
|
43
|
+
shouldUseAsciiOutput,
|
|
44
|
+
};
|
package/src/setup/runtime.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const { buildSettingsForProfile } = require('../governance');
|
|
4
|
+
const { icon } = require('../output-icons');
|
|
4
5
|
|
|
5
6
|
function snapshotSettingsBeforeSetup(dir) {
|
|
6
7
|
const settingsPath = path.join(dir, '.claude/settings.json');
|
|
@@ -53,11 +54,11 @@ function applyTemplateResults({ dir, failedWithTemplates, stacks, ctx, templates
|
|
|
53
54
|
if (!fs.existsSync(fullPath)) {
|
|
54
55
|
fs.writeFileSync(fullPath, result, 'utf8');
|
|
55
56
|
writtenFiles.push(filePath);
|
|
56
|
-
log(` \x1b[32m
|
|
57
|
+
log(` \x1b[32m${icon('ok')}\x1b[0m Created ${filePath}`);
|
|
57
58
|
created++;
|
|
58
59
|
} else {
|
|
59
60
|
preservedFiles.push(filePath);
|
|
60
|
-
log(` \x1b[2m
|
|
61
|
+
log(` \x1b[2m${icon('skip')} Skipped ${filePath} (already exists - your version is kept)\x1b[0m`);
|
|
61
62
|
skipped++;
|
|
62
63
|
}
|
|
63
64
|
} else if (typeof result === 'object') {
|
|
@@ -84,7 +85,7 @@ function applyTemplateResults({ dir, failedWithTemplates, stacks, ctx, templates
|
|
|
84
85
|
if (!fs.existsSync(filePath)) {
|
|
85
86
|
fs.writeFileSync(filePath, content, 'utf8');
|
|
86
87
|
writtenFiles.push(path.relative(dir, filePath));
|
|
87
|
-
log(` \x1b[32m
|
|
88
|
+
log(` \x1b[32m${icon('ok')}\x1b[0m Created ${path.relative(dir, filePath)}`);
|
|
88
89
|
created++;
|
|
89
90
|
} else {
|
|
90
91
|
preservedFiles.push(path.relative(dir, filePath));
|
|
@@ -151,10 +152,10 @@ function mergeGeneratedHookSettings({ dir, profile, mcpPacks, writtenFiles, pres
|
|
|
151
152
|
fs.writeFileSync(settingsPath, JSON.stringify(existingSettings, null, 2), 'utf8');
|
|
152
153
|
if (!writtenFiles.includes('.claude/settings.json') && !preservedFiles.includes('.claude/settings.json')) {
|
|
153
154
|
writtenFiles.push('.claude/settings.json');
|
|
154
|
-
log(` \x1b[32m
|
|
155
|
+
log(` \x1b[32m${icon('ok')}\x1b[0m Updated .claude/settings.json (hooks registered)`);
|
|
155
156
|
created++;
|
|
156
157
|
} else {
|
|
157
|
-
log(` \x1b[32m
|
|
158
|
+
log(` \x1b[32m${icon('ok')}\x1b[0m Merged hooks into existing .claude/settings.json`);
|
|
158
159
|
}
|
|
159
160
|
|
|
160
161
|
return {
|
package/src/setup.js
CHANGED
|
@@ -9,8 +9,9 @@ const { TECHNIQUES, STACKS } = require('./techniques');
|
|
|
9
9
|
const { ProjectContext } = require('./context');
|
|
10
10
|
const { getMcpPackPreflight } = require('./mcp-packs');
|
|
11
11
|
const { writeRollbackArtifact } = require('./activity');
|
|
12
|
-
const { setupCodex } = require('./codex/setup');
|
|
13
|
-
const {
|
|
12
|
+
const { setupCodex } = require('./codex/setup');
|
|
13
|
+
const { icon } = require('./output-icons');
|
|
14
|
+
const { detectDependencies, detectMainDirs, detectProjectMetadata, detectScripts, generateMermaid, getFrameworkInstructions } = require('./setup/analysis');
|
|
14
15
|
const { applyTemplateResults, collectFailedSetupTemplates, mergeGeneratedHookSettings, snapshotSettingsBeforeSetup } = require('./setup/runtime');
|
|
15
16
|
|
|
16
17
|
// ============================================================
|
|
@@ -617,7 +618,7 @@ async function setup(options) {
|
|
|
617
618
|
preservedFiles = settingsMerge.preservedFiles;
|
|
618
619
|
log('');
|
|
619
620
|
if (created === 0 && skipped > 0) {
|
|
620
|
-
log(
|
|
621
|
+
log(` \x1b[32m${icon('ok')}\x1b[0m Your project is already well configured!`);
|
|
621
622
|
log(` \x1b[2m ${skipped} files already exist and were preserved.\x1b[0m`);
|
|
622
623
|
log(' \x1b[2m We never overwrite your existing config — your setup is kept.\x1b[0m');
|
|
623
624
|
} else if (created > 0) {
|