@nerviq/cli 1.12.0 → 1.13.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/src/setup.js CHANGED
@@ -7,618 +7,11 @@ const fs = require('fs');
7
7
  const path = require('path');
8
8
  const { TECHNIQUES, STACKS } = require('./techniques');
9
9
  const { ProjectContext } = require('./context');
10
- const { audit } = require('./audit');
11
- const { buildSettingsForProfile } = require('./governance');
12
10
  const { getMcpPackPreflight } = require('./mcp-packs');
13
11
  const { writeRollbackArtifact } = require('./activity');
14
- const { setupCodex } = require('./codex/setup');
15
-
16
- // ============================================================
17
- // Helper: detect project scripts from package.json
18
- // ============================================================
19
- function detectScripts(ctx) {
20
- const pkg = ctx.jsonFile('package.json');
21
- if (!pkg || !pkg.scripts) return {};
22
- const relevant = ['test', 'build', 'lint', 'dev', 'start', 'format', 'typecheck', 'check'];
23
- const found = {};
24
- for (const key of relevant) {
25
- if (pkg.scripts[key]) {
26
- found[key] = pkg.scripts[key];
27
- }
28
- }
29
- return found;
30
- }
31
-
32
- // ============================================================
33
- // Helper: detect key dependencies and generate guidelines
34
- // ============================================================
35
- function detectDependencies(ctx) {
36
- const pkg = ctx.jsonFile('package.json') || {};
37
- const allDeps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
38
- const guidelines = [];
39
-
40
- // Data fetching
41
- if (allDeps['@tanstack/react-query']) {
42
- guidelines.push('- Use React Query (TanStack Query) for all server data fetching — never raw useEffect + fetch');
43
- guidelines.push('- Define query keys as constants. Invalidate related queries after mutations');
44
- }
45
- if (allDeps['swr']) {
46
- guidelines.push('- Use SWR for data fetching with automatic revalidation');
47
- }
48
-
49
- // Validation
50
- if (allDeps['zod']) {
51
- guidelines.push('- Use Zod for all input validation and type inference (z.infer<typeof schema>)');
52
- guidelines.push('- Define schemas in a shared location. Use .parse() at API boundaries');
53
- }
54
-
55
- // ORM / Database
56
- if (allDeps['prisma'] || allDeps['@prisma/client']) {
57
- guidelines.push('- Use Prisma for all database operations. Run `npx prisma generate` after schema changes');
58
- guidelines.push('- Never write raw SQL unless Prisma cannot express the query');
59
- }
60
- if (allDeps['drizzle-orm']) {
61
- guidelines.push('- Use Drizzle ORM for database operations. Schema-first approach');
62
- }
63
- if (allDeps['mongoose']) {
64
- guidelines.push('- Use Mongoose for MongoDB operations. Define schemas with validation');
65
- }
66
-
67
- // Auth
68
- if (allDeps['next-auth'] || allDeps['@auth/core']) {
69
- guidelines.push('- Use NextAuth.js for authentication. Access session via auth() in Server Components');
70
- }
71
- if (allDeps['clerk'] || allDeps['@clerk/nextjs']) {
72
- guidelines.push('- Use Clerk for authentication. Protect routes with middleware');
73
- }
74
-
75
- // State management
76
- if (allDeps['zustand']) {
77
- guidelines.push('- Use Zustand for client state. Keep stores small and focused');
78
- }
79
- if (allDeps['@reduxjs/toolkit']) {
80
- guidelines.push('- Use Redux Toolkit for state management. Use createSlice and RTK Query');
81
- }
82
-
83
- // Styling
84
- if (allDeps['tailwindcss']) {
85
- guidelines.push('- Use Tailwind CSS for all styling. Avoid inline styles and CSS modules');
86
- }
87
- if (allDeps['styled-components'] || allDeps['@emotion/react']) {
88
- guidelines.push('- Use CSS-in-JS for component styling. Colocate styles with components');
89
- }
90
-
91
- // Testing
92
- if (allDeps['vitest']) {
93
- guidelines.push('- Use Vitest for testing. Colocate test files with source (*.test.ts)');
94
- }
95
- if (allDeps['jest']) {
96
- guidelines.push('- Use Jest for testing. Follow existing test patterns in the codebase');
97
- }
98
- if (allDeps['playwright'] || allDeps['@playwright/test']) {
99
- guidelines.push('- Use Playwright for E2E tests. Keep tests in tests/ or e2e/');
100
- }
101
-
102
- // Testing tools
103
- if (allDeps['msw']) {
104
- guidelines.push('- Use MSW (Mock Service Worker) for API mocking in tests. Define handlers in __mocks__/');
105
- }
106
- if (allDeps['@testing-library/react']) {
107
- guidelines.push('- Use Testing Library for component tests. Prefer userEvent over fireEvent, query by role/label');
108
- }
109
- if (allDeps['@vitest/coverage-v8'] || allDeps['@vitest/coverage-istanbul']) {
110
- guidelines.push('- Coverage configured. Maintain coverage thresholds. Check reports before merging');
111
- }
112
-
113
- // tRPC
114
- if (allDeps['@trpc/server'] || allDeps['@trpc/client']) {
115
- guidelines.push('- Use tRPC for type-safe API calls. Define routers in server, use client hooks in components');
116
- }
117
-
118
- // Stripe
119
- if (allDeps['stripe']) {
120
- guidelines.push('- Use Stripe SDK for payments. Always verify webhooks with stripe.webhooks.constructEvent()');
121
- }
122
-
123
- // Resend
124
- if (allDeps['resend']) {
125
- guidelines.push('- Use Resend for transactional email. Define templates as React components');
126
- }
127
-
128
- // Express security
129
- if (allDeps['helmet']) {
130
- guidelines.push('- Helmet is configured — ensure all middleware is applied before routes');
131
- }
132
- if (allDeps['jsonwebtoken']) {
133
- guidelines.push('- Use JWT for authentication. Always verify tokens with the correct secret/algorithm');
134
- }
135
- if (allDeps['bcrypt']) {
136
- guidelines.push('- Use bcrypt for password hashing. Never store plaintext passwords');
137
- }
138
- if (allDeps['cors']) {
139
- guidelines.push('- CORS is configured — restrict origins to known domains in production');
140
- }
141
-
142
- // Monorepo
143
- if (allDeps['turbo'] || allDeps['turborepo']) {
144
- guidelines.push('- Turborepo monorepo — use `turbo run` for all tasks. Respect package boundaries');
145
- }
146
- if (allDeps['nx']) {
147
- guidelines.push('- Nx monorepo — use `nx affected` for incremental builds and tests');
148
- }
149
-
150
- // Python
151
- const reqTxt = ctx.fileContent('requirements.txt') || '';
152
- if (reqTxt.includes('sqlalchemy')) {
153
- guidelines.push('- Use SQLAlchemy for database operations. Define models in models/');
154
- }
155
- if (reqTxt.includes('pydantic')) {
156
- guidelines.push('- Use Pydantic for data validation and serialization');
157
- }
158
- if (reqTxt.includes('pytest')) {
159
- guidelines.push('- Use pytest for testing. Run with `python -m pytest`');
160
- }
161
- if (reqTxt.includes('alembic')) {
162
- guidelines.push('- Use Alembic for database migrations. Run `alembic upgrade head` after model changes');
163
- }
164
- if (reqTxt.includes('celery')) {
165
- guidelines.push('- Use Celery for background tasks. Define tasks in tasks/ or services/');
166
- }
167
- if (reqTxt.includes('redis')) {
168
- guidelines.push('- Redis is available for caching and task queues');
169
- }
170
- if (reqTxt.includes('langchain')) {
171
- guidelines.push('- Use LangChain for chain/agent orchestration. Define chains in chains/ directory');
172
- }
173
- if (reqTxt.includes('openai')) {
174
- guidelines.push('- OpenAI SDK available. Use structured outputs where possible');
175
- }
176
- if (reqTxt.includes('anthropic')) {
177
- guidelines.push('- Anthropic SDK available. Prefer Claude for complex reasoning tasks');
178
- }
179
- if (reqTxt.includes('chromadb')) {
180
- guidelines.push('- Use ChromaDB for local vector storage. Persist collections to disk');
181
- }
182
- if (reqTxt.includes('pinecone')) {
183
- guidelines.push('- Use Pinecone for production vector search. Define index schemas upfront');
184
- }
185
- if (reqTxt.includes('mlflow')) {
186
- guidelines.push('- Use MLflow for experiment tracking. Log all model parameters and metrics');
187
- }
188
- if (reqTxt.includes('wandb')) {
189
- guidelines.push('- Use Weights & Biases for experiment tracking and visualization');
190
- }
191
- if (reqTxt.includes('transformers')) {
192
- guidelines.push('- HuggingFace Transformers available. Use AutoModel/AutoTokenizer for loading');
193
- }
194
-
195
- // JS AI/ML/Cloud deps
196
- if (allDeps['@anthropic-ai/sdk']) {
197
- guidelines.push('- Anthropic SDK configured. Use Messages API with structured tool_use for agents');
198
- }
199
- if (allDeps['openai']) {
200
- guidelines.push('- OpenAI SDK available. Use structured outputs and function calling');
201
- }
202
- if (allDeps['@modelcontextprotocol/sdk']) {
203
- guidelines.push('- MCP SDK available. Build MCP servers with stdio transport');
204
- }
205
- if (allDeps['langchain'] || allDeps['@langchain/core']) {
206
- guidelines.push('- LangChain available. Use LCEL for chain composition');
207
- }
208
- if (allDeps['@aws-sdk/client-s3'] || allDeps['@aws-sdk/client-dynamodb']) {
209
- guidelines.push('- AWS SDK v3 configured. Use modular imports, not aws-sdk v2');
210
- }
211
- if (allDeps['@aws-cdk/aws-lambda'] || allDeps['aws-cdk-lib']) {
212
- guidelines.push('- AWS CDK available. Define stacks in lib/, constructs as separate classes');
213
- }
214
-
215
- // Security middleware
216
- if (allDeps['express-rate-limit']) {
217
- guidelines.push('- Rate limiting configured. Apply to auth endpoints. Set appropriate windowMs and max values');
218
- }
219
- if (allDeps['hpp']) {
220
- guidelines.push('- HPP (HTTP Parameter Pollution) protection enabled');
221
- }
222
- if (allDeps['csurf']) {
223
- guidelines.push('- CSRF protection enabled. Ensure tokens are included in all state-changing requests');
224
- }
225
-
226
- // AWS Lambda
227
- if (allDeps['@aws-sdk/client-lambda'] || allDeps['@aws-cdk/aws-lambda'] || allDeps['aws-cdk-lib']) {
228
- guidelines.push('- Lambda handlers: keep cold start fast, use layers for deps, set appropriate memory/timeout');
229
- }
230
-
231
- // Deprecated dependency warnings
232
- if (allDeps['moment']) {
233
- guidelines.push('- ⚠️ moment.js is deprecated and heavy (330KB). Migrate to date-fns or dayjs');
234
- }
235
- if (allDeps['request']) {
236
- guidelines.push('- ⚠️ request is deprecated. Use fetch (native) or axios instead');
237
- }
238
- if (allDeps['lodash'] && !allDeps['lodash-es']) {
239
- guidelines.push('- Consider replacing lodash with native JS methods or lodash-es for tree-shaking');
240
- }
241
- if (allDeps['node-sass']) {
242
- guidelines.push('- ⚠️ node-sass is deprecated. Migrate to sass (dart-sass)');
243
- }
244
- if (allDeps['tslint']) {
245
- guidelines.push('- ⚠️ TSLint is deprecated. Migrate to ESLint with @typescript-eslint');
246
- }
247
-
248
- return guidelines;
249
- }
250
-
251
- // ============================================================
252
- // Helper: detect main directories
253
- // ============================================================
254
- function detectMainDirs(ctx) {
255
- const candidates = ['src', 'lib', 'app', 'pages', 'components', 'api', 'routes', 'utils', 'helpers', 'services', 'models', 'controllers', 'views', 'public', 'assets', 'config', 'tests', 'test', '__tests__', 'spec', 'scripts', 'prisma', 'db', 'middleware', 'hooks', 'agents', 'chains', 'workers', 'jobs', 'dags', 'macros', 'migrations'];
256
- // Also check inside src/ for nested structure (common in Next.js, React)
257
- const srcNested = ['src/components', 'src/app', 'src/pages', 'src/api', 'src/lib', 'src/hooks', 'src/utils', 'src/services', 'src/models', 'src/middleware', 'src/app/api', 'app/api', 'src/agents', 'src/chains', 'src/workers', 'src/jobs', 'models/staging', 'models/marts'];
258
- const found = [];
259
- const seenNames = new Set();
260
-
261
- for (const dir of [...candidates, ...srcNested]) {
262
- if (ctx.hasDir(dir)) {
263
- const files = ctx.dirFiles(dir);
264
- const displayName = dir.includes('/') ? dir : dir;
265
- if (!seenNames.has(displayName)) {
266
- found.push({ name: displayName, fileCount: files.length, files: files.slice(0, 10) });
267
- seenNames.add(displayName);
268
- }
269
- }
270
- }
271
- return found;
272
- }
273
-
274
- function escapeRegex(value) {
275
- return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
276
- }
277
-
278
- function extractTomlSection(content, sectionName) {
279
- const pattern = new RegExp(`\\[${escapeRegex(sectionName)}\\]([\\s\\S]*?)(?:\\n\\s*\\[|$)`);
280
- const match = content.match(pattern);
281
- return match ? match[1] : null;
282
- }
283
-
284
- function extractTomlValue(sectionContent, key) {
285
- if (!sectionContent) return null;
286
- const pattern = new RegExp(`^\\s*${escapeRegex(key)}\\s*=\\s*["']([^"']+)["']`, 'm');
287
- const match = sectionContent.match(pattern);
288
- return match ? match[1].trim() : null;
289
- }
290
-
291
- function detectProjectMetadata(ctx) {
292
- const pkg = ctx.jsonFile('package.json');
293
- if (pkg && (pkg.name || pkg.description)) {
294
- return {
295
- name: pkg.name || path.basename(ctx.dir),
296
- description: pkg.description || '',
297
- };
298
- }
299
-
300
- const pyproject = ctx.fileContent('pyproject.toml') || '';
301
- if (pyproject) {
302
- const projectSection = extractTomlSection(pyproject, 'project');
303
- const poetrySection = extractTomlSection(pyproject, 'tool.poetry');
304
- const name = extractTomlValue(projectSection, 'name') ||
305
- extractTomlValue(poetrySection, 'name');
306
- const description = extractTomlValue(projectSection, 'description') ||
307
- extractTomlValue(poetrySection, 'description');
308
-
309
- if (name || description) {
310
- return {
311
- name: name || path.basename(ctx.dir),
312
- description: description || '',
313
- };
314
- }
315
- }
316
-
317
- return {
318
- name: path.basename(ctx.dir),
319
- description: '',
320
- };
321
- }
322
-
323
- // ============================================================
324
- // Helper: generate Mermaid diagram from directory structure
325
- // ============================================================
326
- function generateMermaid(dirs, stacks) {
327
- const stackKeys = stacks.map(s => s.key);
328
- const dirNames = dirs.map(d => d.name);
329
-
330
- // Build nodes based on what exists
331
- const nodes = [];
332
- const edges = [];
333
- let nodeId = 0;
334
- const ids = {};
335
-
336
- function addNode(label, shape) {
337
- const id = String.fromCharCode(65 + nodeId++); // A, B, C...
338
- ids[label] = id;
339
- if (shape === 'db') return ` ${id}[(${label})]`;
340
- if (shape === 'round') return ` ${id}(${label})`;
341
- return ` ${id}[${label}]`;
342
- }
343
-
344
- // Detect Next.js App Router specifically
345
- const hasAppRouter = dirNames.includes('app') || dirNames.includes('src/app');
346
- const hasPages = dirNames.includes('pages') || dirNames.includes('src/pages');
347
- const hasAppApi = dirNames.includes('app/api') || dirNames.includes('src/app/api');
348
- const hasSrcComponents = dirNames.includes('src/components') || dirNames.includes('components');
349
- const hasSrcHooks = dirNames.includes('src/hooks') || dirNames.includes('hooks');
350
- const hasSrcLib = dirNames.includes('src/lib') || dirNames.includes('lib');
351
- const hasSrcNode = dirNames.includes('src');
352
- const hasAgents = dirNames.includes('src/agents') || dirNames.includes('agents');
353
- const hasChains = dirNames.includes('src/chains') || dirNames.includes('chains');
354
- const hasWorkers = dirNames.includes('src/workers') || dirNames.includes('workers') || dirNames.includes('jobs');
355
- const hasPipelines = dirNames.includes('dags') || dirNames.includes('macros');
356
-
357
- // Smart entry point based on framework
358
- const isNextJs = stackKeys.includes('nextjs');
359
- const isDjango = stackKeys.includes('django');
360
- const isFastApi = stackKeys.includes('fastapi');
361
-
362
- if (isNextJs) {
363
- nodes.push(addNode('Next.js', 'round'));
364
- } else if (isDjango) {
365
- nodes.push(addNode('Django', 'round'));
366
- } else if (isFastApi) {
367
- nodes.push(addNode('FastAPI', 'round'));
368
- } else {
369
- nodes.push(addNode('Entry Point', 'round'));
370
- }
371
-
372
- const root = ids['Next.js'] || ids['Django'] || ids['FastAPI'] || ids['Entry Point'] || 'A';
373
- const pickNodeId = (...labels) => labels.map(label => ids[label]).find(Boolean) || root;
374
-
375
- // Detect layers
376
- if (hasAppRouter || hasPages) {
377
- const label = hasAppRouter ? 'App Router' : 'Pages';
378
- nodes.push(addNode(label, 'default'));
379
- edges.push(` ${root} --> ${ids[label]}`);
380
- }
381
-
382
- if (hasAppApi) {
383
- nodes.push(addNode('API Routes', 'default'));
384
- const parent = ids['App Router'] || ids['Pages'] || root;
385
- edges.push(` ${parent} --> ${ids['API Routes']}`);
386
- }
387
-
388
- if (hasSrcComponents) {
389
- nodes.push(addNode('Components', 'default'));
390
- const parent = ids['App Router'] || ids['Pages'] || root;
391
- edges.push(` ${parent} --> ${ids['Components']}`);
392
- }
393
-
394
- if (hasSrcHooks) {
395
- nodes.push(addNode('Hooks', 'default'));
396
- const parent = ids['Components'] || root;
397
- edges.push(` ${parent} --> ${ids['Hooks']}`);
398
- }
399
-
400
- if (hasSrcLib) {
401
- nodes.push(addNode('lib/', 'default'));
402
- const parent = pickNodeId('API Routes', 'Hooks', 'Components');
403
- edges.push(` ${parent} --> ${ids['lib/']}`);
404
- } else if (hasSrcNode && !hasAppRouter && !hasPages) {
405
- nodes.push(addNode('src/', 'default'));
406
- edges.push(` ${root} --> ${ids['src/']}`);
407
- }
408
-
409
- if (dirNames.includes('api') || dirNames.includes('routes') || dirNames.includes('controllers')) {
410
- const label = dirNames.includes('api') ? 'API Layer' : 'Routes';
411
- nodes.push(addNode(label, 'default'));
412
- const parent = pickNodeId('src/', 'App Router', 'Pages');
413
- edges.push(` ${parent} --> ${ids[label]}`);
414
- }
415
-
416
- if (dirNames.includes('services')) {
417
- nodes.push(addNode('Services', 'default'));
418
- const parent = pickNodeId('API Layer', 'Routes', 'src/', 'App Router', 'Pages');
419
- edges.push(` ${parent} --> ${ids['Services']}`);
420
- }
421
-
422
- if (dirNames.includes('models') || dirNames.includes('prisma') || dirNames.includes('db')) {
423
- nodes.push(addNode('Data Layer', 'default'));
424
- const parent = pickNodeId('Services', 'API Layer', 'Routes', 'src/', 'App Router', 'Pages');
425
- edges.push(` ${parent} --> ${ids['Data Layer']}`);
426
- nodes.push(addNode('Database', 'db'));
427
- edges.push(` ${ids['Data Layer']} --> ${ids['Database']}`);
428
- }
429
-
430
- if (dirNames.includes('utils') || dirNames.includes('helpers')) {
431
- nodes.push(addNode('Utils', 'default'));
432
- const parent = pickNodeId('src/', 'Services', 'lib/', 'Components');
433
- edges.push(` ${parent} --> ${ids['Utils']}`);
434
- }
435
-
436
- if (dirNames.includes('middleware')) {
437
- nodes.push(addNode('Middleware', 'default'));
438
- const parent = pickNodeId('API Layer', 'Routes', 'App Router', 'Pages');
439
- edges.push(` ${parent} --> ${ids['Middleware']}`);
440
- }
441
-
442
- if (hasChains) {
443
- nodes.push(addNode('Chains', 'default'));
444
- const parent = pickNodeId('Services', 'src/', 'lib/', 'API Layer');
445
- edges.push(` ${parent} --> ${ids['Chains']}`);
446
- }
447
-
448
- if (hasAgents) {
449
- nodes.push(addNode('Agents', 'default'));
450
- const parent = pickNodeId('Chains', 'Services', 'src/', 'lib/');
451
- edges.push(` ${parent} --> ${ids['Agents']}`);
452
- }
453
-
454
- if (hasWorkers) {
455
- nodes.push(addNode('Workers', 'default'));
456
- const parent = pickNodeId('Services', 'API Layer', 'src/');
457
- edges.push(` ${parent} --> ${ids['Workers']}`);
458
- }
459
-
460
- if (hasPipelines) {
461
- nodes.push(addNode('Pipelines', 'default'));
462
- const parent = pickNodeId('Services', 'Data Layer', 'src/');
463
- edges.push(` ${parent} --> ${ids['Pipelines']}`);
464
- }
465
-
466
- if (dirNames.includes('tests') || dirNames.includes('test') || dirNames.includes('__tests__') || dirNames.includes('spec')) {
467
- nodes.push(addNode('Tests', 'round'));
468
- const parent = pickNodeId('src/', 'App Router', 'Pages', 'Services', 'Components');
469
- edges.push(` ${ids['Tests']} -.-> ${parent}`);
470
- }
471
-
472
- // Fallback: if we only have Entry Point, make a generic diagram
473
- if (nodes.length <= 1) {
474
- return `\`\`\`mermaid
475
- graph TD
476
- A[Entry Point] --> B[Core Logic]
477
- B --> C[Data Layer]
478
- B --> D[API / Routes]
479
- C --> E[(Database)]
480
- D --> F[External Services]
481
- \`\`\`
482
- <!-- Update this diagram to match your actual architecture -->`;
483
- }
484
-
485
- return '```mermaid\ngraph TD\n' + nodes.join('\n') + '\n' + edges.join('\n') + '\n```';
486
- }
487
-
488
- // ============================================================
489
- // Helper: framework-specific instructions
490
- // ============================================================
491
- function getFrameworkInstructions(stacks) {
492
- const stackKeys = stacks.map(s => s.key);
493
- const sections = [];
494
-
495
- if (stackKeys.includes('nextjs')) {
496
- sections.push(`### Next.js
497
- - Use App Router conventions (app/ directory) when applicable
498
- - Prefer Server Components by default; add 'use client' only when needed
499
- - Use next/image for images, next/link for navigation
500
- - API routes go in app/api/ (App Router) or pages/api/ (Pages Router)
501
- - Use loading.tsx, error.tsx, and not-found.tsx for route-level UX
502
- - If app/ exists, use Server Actions for mutations, validate with Zod, and call revalidatePath after writes
503
- - Route handlers in app/api/ should export named functions: GET, POST, PUT, DELETE
504
- - Middleware in middleware.ts should handle auth checks, redirects, and headers`);
505
- } else if (stackKeys.includes('react')) {
506
- sections.push(`### React
507
- - Use functional components with hooks exclusively
508
- - Prefer named exports over default exports
509
- - Keep components under 150 lines; extract sub-components
510
- - Use custom hooks to share stateful logic
511
- - Colocate styles, tests, and types with components`);
512
- }
513
-
514
- if (stackKeys.includes('vue')) {
515
- sections.push(`### Vue
516
- - Use Composition API with \`<script setup>\` syntax
517
- - Prefer defineProps/defineEmits macros
518
- - Keep components under 200 lines
519
- - Use composables for shared logic`);
520
- }
521
-
522
- if (stackKeys.includes('angular')) {
523
- sections.push(`### Angular
524
- - Use standalone components when possible
525
- - Follow Angular style guide naming conventions
526
- - Use reactive forms over template-driven forms
527
- - Keep services focused on a single responsibility`);
528
- }
529
-
530
- if (stackKeys.includes('typescript')) {
531
- sections.push(`### TypeScript
532
- - Use \`interface\` for object shapes, \`type\` for unions/intersections
533
- - Enable strict mode in tsconfig.json
534
- - Avoid \`any\` — use \`unknown\` and narrow with type guards
535
- - Prefer \`as const\` assertions over enum when practical
536
- - Export types alongside their implementations`);
537
- }
538
-
539
- if (stackKeys.includes('django')) {
540
- sections.push(`### Django
541
- - Follow fat models, thin views pattern
542
- - Use class-based views for complex logic, function views for simple
543
- - Always use Django ORM; avoid raw SQL unless necessary
544
- - Keep business logic in models or services, not views`);
545
- } else if (stackKeys.includes('fastapi')) {
546
- sections.push(`### FastAPI
547
- - Use Pydantic models for request/response validation
548
- - Use dependency injection for shared logic
549
- - Keep route handlers thin; delegate to service functions
550
- - Use async def for I/O-bound endpoints`);
551
- }
552
-
553
- if (stackKeys.includes('python') || stackKeys.includes('django') || stackKeys.includes('fastapi')) {
554
- sections.push(`### Python
555
- - Use type hints on all function signatures and return types
556
- - Follow PEP 8; use f-strings for formatting
557
- - Prefer pathlib over os.path
558
- - Use dataclasses or pydantic for structured data
559
- - Raise specific exceptions; never bare \`except:\``);
560
- }
561
-
562
- if (stackKeys.includes('rust')) {
563
- sections.push(`### Rust
564
- - Use Result<T, E> for error handling, avoid unwrap() in production code
565
- - Prefer &str over String for function parameters
566
- - Use clippy: \`cargo clippy -- -D warnings\`
567
- - Structure: src/lib.rs for library, src/main.rs for binary`);
568
- }
569
-
570
- if (stackKeys.includes('go')) {
571
- sections.push(`### Go
572
- - Follow standard Go project layout (cmd/, internal/, pkg/)
573
- - Use interfaces for dependency injection and testability
574
- - Handle all errors explicitly — never ignore err returns
575
- - Use context.Context for cancellation and timeouts
576
- - Prefer table-driven tests
577
- - Run \`go vet\` and \`golangci-lint\` before committing
578
- - If using gRPC: define .proto files in proto/ or pkg/proto, generate with protoc
579
- - If Makefile exists: use make targets for build/test/lint
580
- - Organize: cmd/ for entry points, internal/ for private packages, pkg/ for public`);
581
- }
582
-
583
- if (stackKeys.includes('cpp')) {
584
- sections.push(`### C++
585
- - Follow project coding standards (check .clang-format if present)
586
- - Use smart pointers (unique_ptr, shared_ptr) over raw pointers
587
- - Run clang-tidy for static analysis
588
- - Prefer const references for function parameters
589
- - Use CMake targets, not raw compiler flags`);
590
- }
591
-
592
- if (stackKeys.includes('bazel')) {
593
- sections.push(`### Bazel
594
- - Define BUILD files per package. Keep targets focused
595
- - Use visibility carefully — prefer package-private
596
- - Run buildifier for formatting`);
597
- }
598
-
599
- if (stackKeys.includes('terraform')) {
600
- sections.push(`### Terraform
601
- - Use modules for reusable infrastructure components
602
- - Always run \`terraform plan\` before \`terraform apply\`
603
- - Store state remotely (S3 + DynamoDB, or Terraform Cloud)
604
- - Use variables.tf for all configurable values
605
- - Tag all resources consistently
606
- - If using Helm: define charts in charts/ or helm/, use values.yaml for config
607
- - Lock providers: always commit .terraform.lock.hcl
608
- - Use terraform fmt before committing`);
609
- }
610
-
611
- const hasJS = stackKeys.some(k => ['react', 'vue', 'angular', 'nextjs', 'node', 'svelte'].includes(k));
612
- if (hasJS && !stackKeys.includes('typescript')) {
613
- sections.push(`### JavaScript
614
- - Use \`const\` by default, \`let\` when reassignment needed; never \`var\`
615
- - Use \`async/await\` over raw Promises
616
- - Use named exports over default exports
617
- - Import order: stdlib > external > internal > relative`);
618
- }
619
-
620
- return sections.join('\n\n');
621
- }
12
+ const { setupCodex } = require('./codex/setup');
13
+ const { detectDependencies, detectMainDirs, detectProjectMetadata, detectScripts, generateMermaid, getFrameworkInstructions } = require('./setup/analysis');
14
+ const { applyTemplateResults, collectFailedSetupTemplates, mergeGeneratedHookSettings, snapshotSettingsBeforeSetup } = require('./setup/runtime');
622
15
 
623
16
  // ============================================================
624
17
  // TEMPLATES
@@ -1180,21 +573,11 @@ async function setup(options) {
1180
573
  const ctx = new ProjectContext(options.dir);
1181
574
  const stacks = ctx.detectStacks(STACKS);
1182
575
  const silent = options.silent === true;
1183
- const writtenFiles = [];
1184
- const preservedFiles = [];
1185
576
  const mcpPreflightWarnings = getMcpPackPreflight(options.mcpPacks || [])
1186
577
  .filter(item => item.missingEnvVars.length > 0);
1187
578
 
1188
- // Snapshot settings.json before any changes for rollback support
1189
- const settingsPathForSnapshot = path.join(options.dir, '.claude/settings.json');
1190
- let settingsSnapshotBefore = null;
1191
- if (fs.existsSync(settingsPathForSnapshot)) {
1192
- try {
1193
- settingsSnapshotBefore = fs.readFileSync(settingsPathForSnapshot, 'utf8');
1194
- } catch (_) {
1195
- // Ignore read errors
1196
- }
1197
- }
579
+ const settingsSnapshotBefore = snapshotSettingsBeforeSetup(options.dir);
580
+
1198
581
 
1199
582
  function log(message = '') {
1200
583
  if (!silent) {
@@ -1211,132 +594,27 @@ async function setup(options) {
1211
594
  }
1212
595
  log('');
1213
596
 
1214
- let created = 0;
1215
- let skipped = 0;
1216
-
1217
- let failedWithTemplates = [];
1218
- for (const [key, technique] of Object.entries(TECHNIQUES)) {
1219
- if (technique.passed || technique.check(ctx)) continue;
1220
- if (!technique.template) continue;
1221
- failedWithTemplates.push({ key, technique });
1222
- }
1223
-
1224
- // Filter by 'only' list if provided (interactive wizard selections)
1225
- if (options.only && options.only.length > 0) {
1226
- failedWithTemplates = failedWithTemplates.filter(r => options.only.includes(r.key));
1227
- }
1228
-
1229
- for (const { key, technique } of failedWithTemplates) {
1230
-
1231
- const template = TEMPLATES[technique.template];
1232
- if (!template) continue;
1233
-
1234
- // Pass ctx as second argument — only claude-md uses it
1235
- const result = template(stacks, ctx);
1236
-
1237
- if (typeof result === 'string') {
1238
- // Single file template (like CLAUDE.md)
1239
- // Map technique keys to actual file paths
1240
- const filePathMap = {
1241
- 'claudeMd': 'CLAUDE.md',
1242
- 'mermaidArchitecture': 'CLAUDE.md', // mermaid is part of CLAUDE.md, skip separate file
1243
- };
1244
- if (key === 'mermaidArchitecture') continue; // Mermaid is generated inside CLAUDE.md template
1245
- const filePath = filePathMap[key] || key;
1246
- const fullPath = path.join(options.dir, filePath);
1247
-
1248
- if (!fs.existsSync(fullPath)) {
1249
- fs.writeFileSync(fullPath, result, 'utf8');
1250
- writtenFiles.push(filePath);
1251
- log(` \x1b[32m✅\x1b[0m Created ${filePath}`);
1252
- created++;
1253
- } else {
1254
- preservedFiles.push(filePath);
1255
- log(` \x1b[2m⏭️ Skipped ${filePath} (already exists — your version is kept)\x1b[0m`);
1256
- skipped++;
1257
- }
1258
- } else if (typeof result === 'object') {
1259
- // Multiple files template (hooks, commands, etc)
1260
- const dirMap = {
1261
- 'hooks': '.claude/hooks',
1262
- 'commands': '.claude/commands',
1263
- 'skills': '.claude/skills',
1264
- 'rules': '.claude/rules',
1265
- 'agents': '.claude/agents',
1266
- };
1267
- const targetDir = dirMap[technique.template] || `.claude/${technique.template}`;
1268
- const fullDir = path.join(options.dir, targetDir);
1269
-
1270
- if (!fs.existsSync(fullDir)) {
1271
- fs.mkdirSync(fullDir, { recursive: true });
1272
- }
1273
-
1274
- for (const [fileName, content] of Object.entries(result)) {
1275
- const filePath = path.join(fullDir, fileName);
1276
- const fileDir = path.dirname(filePath);
1277
- if (!fs.existsSync(fileDir)) {
1278
- fs.mkdirSync(fileDir, { recursive: true });
1279
- }
1280
- if (!fs.existsSync(filePath)) {
1281
- fs.writeFileSync(filePath, content, 'utf8');
1282
- writtenFiles.push(path.relative(options.dir, filePath));
1283
- log(` \x1b[32m✅\x1b[0m Created ${path.relative(options.dir, filePath)}`);
1284
- created++;
1285
- } else {
1286
- preservedFiles.push(path.relative(options.dir, filePath));
1287
- skipped++;
1288
- }
1289
- }
1290
- }
1291
- }
1292
-
1293
- // Auto-register hooks in settings — always merge hooks into settings.json
1294
- const hooksDir = path.join(options.dir, '.claude/hooks');
1295
- const settingsPath = path.join(options.dir, '.claude/settings.json');
1296
- if (fs.existsSync(hooksDir)) {
1297
- const hookFiles = fs.readdirSync(hooksDir).filter(f => f.endsWith('.sh') || f.endsWith('.js'));
1298
- if (hookFiles.length > 0) {
1299
- const newSettings = buildSettingsForProfile({
1300
- profileKey: options.profile || 'safe-write',
1301
- hookFiles,
1302
- mcpPackKeys: options.mcpPacks || [],
1303
- });
1304
- // Merge new settings into existing settings.json, preserving all fields
1305
- let existingSettings = {};
1306
- if (fs.existsSync(settingsPath)) {
1307
- try {
1308
- existingSettings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
1309
- } catch (_) {
1310
- // If settings.json is malformed, start fresh
1311
- existingSettings = {};
1312
- }
1313
- }
1314
- // Merge all fields from newSettings into existing, preserving existing values
1315
- if (newSettings.hooks) existingSettings.hooks = newSettings.hooks;
1316
- if (newSettings.permissions) {
1317
- existingSettings.permissions = existingSettings.permissions || {};
1318
- // MERGE deny rules: keep existing + add new (deduplicate)
1319
- const existingDeny = existingSettings.permissions.deny || [];
1320
- const newDeny = newSettings.permissions.deny || [];
1321
- existingSettings.permissions.deny = [...new Set([...existingDeny, ...newDeny])];
1322
- // Only set defaultMode if not already set
1323
- if (!existingSettings.permissions.defaultMode && newSettings.permissions.defaultMode) {
1324
- existingSettings.permissions.defaultMode = newSettings.permissions.defaultMode;
1325
- }
1326
- }
1327
- if (newSettings.mcpServers) existingSettings.mcpServers = { ...existingSettings.mcpServers, ...newSettings.mcpServers };
1328
- if (newSettings.nerviqSetup) existingSettings.nerviqSetup = { ...existingSettings.nerviqSetup, ...newSettings.nerviqSetup };
1329
- fs.writeFileSync(settingsPath, JSON.stringify(existingSettings, null, 2), 'utf8');
1330
- if (!writtenFiles.includes('.claude/settings.json') && !preservedFiles.includes('.claude/settings.json')) {
1331
- writtenFiles.push('.claude/settings.json');
1332
- log(` \x1b[32m✅\x1b[0m Updated .claude/settings.json (hooks registered)`);
1333
- created++;
1334
- } else {
1335
- log(` \x1b[32m✅\x1b[0m Merged hooks into existing .claude/settings.json`);
1336
- }
1337
- }
1338
- }
1339
-
597
+ const failedWithTemplates = collectFailedSetupTemplates(ctx, TECHNIQUES, options.only);
598
+ let { created, skipped, writtenFiles, preservedFiles } = applyTemplateResults({
599
+ dir: options.dir,
600
+ failedWithTemplates,
601
+ stacks,
602
+ ctx,
603
+ templates: TEMPLATES,
604
+ log,
605
+ });
606
+
607
+ const settingsMerge = mergeGeneratedHookSettings({
608
+ dir: options.dir,
609
+ profile: options.profile,
610
+ mcpPacks: options.mcpPacks || [],
611
+ writtenFiles,
612
+ preservedFiles,
613
+ log,
614
+ });
615
+ created += settingsMerge.created;
616
+ writtenFiles = settingsMerge.writtenFiles;
617
+ preservedFiles = settingsMerge.preservedFiles;
1340
618
  log('');
1341
619
  if (created === 0 && skipped > 0) {
1342
620
  log(' \x1b[32m✅\x1b[0m Your project is already well configured!');
@@ -1400,3 +678,5 @@ async function setup(options) {
1400
678
  }
1401
679
 
1402
680
  module.exports = { setup, TEMPLATES };
681
+
682
+