create-quiver 0.4.0 → 0.5.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.
Files changed (31) hide show
  1. package/README.md +56 -22
  2. package/README_FOR_AI.md +28 -10
  3. package/docs/AI_CONTEXT.md.template +58 -0
  4. package/docs/AI_ONBOARDING_PROMPT.md.template +56 -0
  5. package/docs/CONTEXTO.md.template +1 -1
  6. package/docs/DOCUMENTATION_GUIDE.md.template +9 -7
  7. package/docs/INDEX.md.template +4 -0
  8. package/docs/WORKFLOW.md.template +6 -1
  9. package/package.json +1 -1
  10. package/scripts/init-docs.sh +53 -8
  11. package/scripts/package-quiver.sh +2 -0
  12. package/specs/quiver-v07-ai-context-pack/EVIDENCE_REPORT.md +24 -0
  13. package/specs/quiver-v07-ai-context-pack/SPEC.md +40 -0
  14. package/specs/quiver-v07-ai-context-pack/STATUS.md +24 -0
  15. package/specs/quiver-v07-ai-context-pack/slices/slice-01-ai-context-pack/slice.json +79 -0
  16. package/specs/quiver-v08-agent-onboarding-analysis/EVIDENCE_REPORT.md +49 -0
  17. package/specs/quiver-v08-agent-onboarding-analysis/SPEC.md +53 -0
  18. package/specs/quiver-v08-agent-onboarding-analysis/STATUS.md +26 -0
  19. package/specs/quiver-v08-agent-onboarding-analysis/slices/slice-01-project-scan-command/slice.json +73 -0
  20. package/specs/quiver-v08-agent-onboarding-analysis/slices/slice-02-ai-onboarding-prompt/slice.json +82 -0
  21. package/specs/quiver-v08-agent-onboarding-analysis/slices/slice-03-doctor-readme-adoption-flow/slice.json +76 -0
  22. package/specs/quiver-v09-onboarding-readme-flow/EVIDENCE_REPORT.md +33 -0
  23. package/specs/quiver-v09-onboarding-readme-flow/SPEC.md +44 -0
  24. package/specs/quiver-v09-onboarding-readme-flow/STATUS.md +25 -0
  25. package/specs/quiver-v09-onboarding-readme-flow/slices/slice-01-developer-readme-onboarding-flow/slice.json +69 -0
  26. package/specs/quiver-v09-onboarding-readme-flow/slices/slice-02-ai-handoff-doctor-guidance/slice.json +71 -0
  27. package/specs/quiver-v10-local-project-installation-guidance/EVIDENCE_REPORT.md +25 -0
  28. package/specs/quiver-v10-local-project-installation-guidance/SPEC.md +42 -0
  29. package/specs/quiver-v10-local-project-installation-guidance/STATUS.md +24 -0
  30. package/specs/quiver-v10-local-project-installation-guidance/slices/slice-01-local-project-installation-guidance/slice.json +75 -0
  31. package/src/create-quiver/index.js +608 -4
@@ -10,6 +10,7 @@ function formatError(message) {
10
10
  function printUsage() {
11
11
  console.log(`Usage:
12
12
  npx create-quiver [options]
13
+ npx create-quiver analyze [options]
13
14
  npx create-quiver doctor [options]
14
15
 
15
16
  Options:
@@ -21,6 +22,7 @@ Options:
21
22
  Examples:
22
23
  npx create-quiver --name "My Project"
23
24
  npx create-quiver --name "My Project" --dir ./my-project
25
+ npx create-quiver analyze --dir ./my-project
24
26
  npx create-quiver doctor --dir ./my-project
25
27
  node bin/create-quiver.js doctor --dir ./my-project
26
28
  `);
@@ -36,7 +38,13 @@ function parseArgs(argv) {
36
38
  };
37
39
 
38
40
  const args = [...argv];
39
- if (args[0] === 'doctor') {
41
+ if (args[0] === 'doctor' || args[0] === 'analyze') {
42
+ result.mode = args[0];
43
+ args.shift();
44
+ } else if (args[0] === '--analyze') {
45
+ result.mode = 'analyze';
46
+ args.shift();
47
+ } else if (args[0] === '--doctor') {
40
48
  result.mode = 'doctor';
41
49
  args.shift();
42
50
  }
@@ -215,6 +223,588 @@ function loadPackageJson(projectRoot) {
215
223
  return JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
216
224
  }
217
225
 
226
+ function readJsonIfExists(filePath) {
227
+ if (!fs.existsSync(filePath)) {
228
+ return null;
229
+ }
230
+
231
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
232
+ }
233
+
234
+ function readTextIfExists(filePath) {
235
+ if (!fs.existsSync(filePath)) {
236
+ return null;
237
+ }
238
+
239
+ return fs.readFileSync(filePath, 'utf8');
240
+ }
241
+
242
+ function toRelativePath(root, absolutePath) {
243
+ return path.relative(root, absolutePath).split(path.sep).join('/');
244
+ }
245
+
246
+ function escapeMarkdownCell(value) {
247
+ return String(value).replace(/\|/g, '\\|');
248
+ }
249
+
250
+ function collectPackageManagers(projectRoot) {
251
+ const packageManagerField = readJsonIfExists(path.join(projectRoot, 'package.json'))?.packageManager;
252
+
253
+ if (typeof packageManagerField === 'string' && packageManagerField.length > 0) {
254
+ return packageManagerField.split('@')[0];
255
+ }
256
+
257
+ const priority = [
258
+ ['pnpm', 'pnpm-lock.yaml'],
259
+ ['yarn', 'yarn.lock'],
260
+ ['bun', 'bun.lockb'],
261
+ ['bun', 'bun.lock'],
262
+ ['npm', 'package-lock.json'],
263
+ ];
264
+
265
+ for (const [manager, filename] of priority) {
266
+ if (fs.existsSync(path.join(projectRoot, filename))) {
267
+ return manager;
268
+ }
269
+ }
270
+
271
+ return 'unknown';
272
+ }
273
+
274
+ function collectProjectFiles(projectRoot, maxDepth = 2) {
275
+ const files = [];
276
+ const skippedPaths = [];
277
+ const ignoredDirs = new Set([
278
+ '.git',
279
+ 'node_modules',
280
+ 'dist',
281
+ 'build',
282
+ '.next',
283
+ 'coverage',
284
+ 'vendor',
285
+ '.turbo',
286
+ '.cache',
287
+ 'out',
288
+ 'tmp',
289
+ 'docs-template',
290
+ ]);
291
+ const allowedHiddenDirs = new Set(['.github', '.vscode', '.devcontainer']);
292
+
293
+ function walk(currentDir, depth, relativeDir = '') {
294
+ const entries = fs.readdirSync(currentDir, { withFileTypes: true });
295
+
296
+ for (const entry of entries) {
297
+ const entryRelativePath = relativeDir ? path.posix.join(relativeDir, entry.name) : entry.name;
298
+ const absolutePath = path.join(currentDir, entry.name);
299
+
300
+ if (entry.isDirectory()) {
301
+ if (ignoredDirs.has(entry.name)) {
302
+ skippedPaths.push(entryRelativePath);
303
+ continue;
304
+ }
305
+
306
+ if (entry.name.startsWith('.') && !allowedHiddenDirs.has(entry.name)) {
307
+ skippedPaths.push(entryRelativePath);
308
+ continue;
309
+ }
310
+
311
+ if (depth < maxDepth) {
312
+ walk(absolutePath, depth + 1, entryRelativePath);
313
+ }
314
+
315
+ continue;
316
+ }
317
+
318
+ files.push(entryRelativePath);
319
+ }
320
+ }
321
+
322
+ walk(projectRoot, 0);
323
+
324
+ return { files, skippedPaths };
325
+ }
326
+
327
+ function collectRootEntries(projectRoot) {
328
+ return fs.readdirSync(projectRoot, { withFileTypes: true }).map((entry) => ({
329
+ name: entry.name,
330
+ type: entry.isDirectory() ? 'directory' : 'file',
331
+ }));
332
+ }
333
+
334
+ function detectSourceDirectories(rootEntries) {
335
+ const commonNames = new Set([
336
+ 'src',
337
+ 'app',
338
+ 'pages',
339
+ 'components',
340
+ 'lib',
341
+ 'server',
342
+ 'client',
343
+ 'api',
344
+ 'packages',
345
+ 'services',
346
+ 'modules',
347
+ 'tests',
348
+ 'test',
349
+ 'spec',
350
+ 'stories',
351
+ ]);
352
+
353
+ return rootEntries
354
+ .filter((entry) => entry.type === 'directory' && commonNames.has(entry.name))
355
+ .map((entry) => entry.name);
356
+ }
357
+
358
+ function collectLanguageSignals(files) {
359
+ const extensions = new Map();
360
+
361
+ for (const file of files) {
362
+ const ext = path.extname(file).toLowerCase();
363
+
364
+ if (!ext) {
365
+ continue;
366
+ }
367
+
368
+ extensions.set(ext, (extensions.get(ext) || 0) + 1);
369
+ }
370
+
371
+ const languages = [];
372
+ const extToLanguage = new Map([
373
+ ['.ts', 'typescript'],
374
+ ['.tsx', 'typescript'],
375
+ ['.mts', 'typescript'],
376
+ ['.cts', 'typescript'],
377
+ ['.js', 'javascript'],
378
+ ['.jsx', 'javascript'],
379
+ ['.mjs', 'javascript'],
380
+ ['.cjs', 'javascript'],
381
+ ['.py', 'python'],
382
+ ['.go', 'go'],
383
+ ['.php', 'php'],
384
+ ['.rb', 'ruby'],
385
+ ['.rs', 'rust'],
386
+ ['.java', 'java'],
387
+ ['.kt', 'kotlin'],
388
+ ['.swift', 'swift'],
389
+ ['.cs', 'csharp'],
390
+ ['.sh', 'shell'],
391
+ ['.toml', 'toml'],
392
+ ['.yaml', 'yaml'],
393
+ ['.yml', 'yaml'],
394
+ ]);
395
+
396
+ for (const [ext, language] of extToLanguage.entries()) {
397
+ if (extensions.has(ext)) {
398
+ languages.push(language);
399
+ }
400
+ }
401
+
402
+ return languages;
403
+ }
404
+
405
+ function collectWorkspaces(packageJson) {
406
+ if (!packageJson) {
407
+ return [];
408
+ }
409
+
410
+ const workspaces = packageJson.workspaces;
411
+
412
+ if (Array.isArray(workspaces)) {
413
+ return workspaces.filter((workspace) => typeof workspace === 'string');
414
+ }
415
+
416
+ if (workspaces && Array.isArray(workspaces.packages)) {
417
+ return workspaces.packages.filter((workspace) => typeof workspace === 'string');
418
+ }
419
+
420
+ return [];
421
+ }
422
+
423
+ function collectDependencies(packageJson) {
424
+ const dependencySets = [
425
+ packageJson?.dependencies,
426
+ packageJson?.devDependencies,
427
+ packageJson?.peerDependencies,
428
+ packageJson?.optionalDependencies,
429
+ ];
430
+ const dependencies = new Set();
431
+
432
+ for (const set of dependencySets) {
433
+ if (!set) {
434
+ continue;
435
+ }
436
+
437
+ for (const name of Object.keys(set)) {
438
+ dependencies.add(name);
439
+ }
440
+ }
441
+
442
+ return dependencies;
443
+ }
444
+
445
+ function detectFrameworks(projectRoot, files, rootEntries, packageJson) {
446
+ const dependencies = collectDependencies(packageJson);
447
+ const rootFileSet = new Set(rootEntries.filter((entry) => entry.type === 'file').map((entry) => entry.name));
448
+ const rootDirSet = new Set(rootEntries.filter((entry) => entry.type === 'directory').map((entry) => entry.name));
449
+ const frameworks = [];
450
+ const evidence = [];
451
+
452
+ const candidates = [
453
+ {
454
+ name: 'nextjs',
455
+ matches: () => dependencies.has('next') || rootFileSet.has('next.config.js') || rootFileSet.has('next.config.mjs') || rootFileSet.has('next.config.ts') || rootDirSet.has('pages') || rootDirSet.has('app'),
456
+ signals: ['next', 'next.config.*', 'app/pages'],
457
+ },
458
+ {
459
+ name: 'nuxt',
460
+ matches: () => dependencies.has('nuxt') || rootFileSet.has('nuxt.config.js') || rootFileSet.has('nuxt.config.ts') || rootFileSet.has('app.vue'),
461
+ signals: ['nuxt', 'nuxt.config.*'],
462
+ },
463
+ {
464
+ name: 'angular',
465
+ matches: () => dependencies.has('@angular/core') || rootFileSet.has('angular.json'),
466
+ signals: ['@angular/core', 'angular.json'],
467
+ },
468
+ {
469
+ name: 'sveltekit',
470
+ matches: () => dependencies.has('@sveltejs/kit') || rootFileSet.has('svelte.config.js') || rootFileSet.has('svelte.config.ts'),
471
+ signals: ['@sveltejs/kit', 'svelte.config.*'],
472
+ },
473
+ {
474
+ name: 'vue',
475
+ matches: () => dependencies.has('vue') || rootFileSet.has('vue.config.js') || rootFileSet.has('vite.config.js') || rootFileSet.has('vite.config.ts'),
476
+ signals: ['vue', 'vue.config.*', 'vite.config.*'],
477
+ },
478
+ {
479
+ name: 'react',
480
+ matches: () => dependencies.has('react'),
481
+ signals: ['react'],
482
+ },
483
+ {
484
+ name: 'vite',
485
+ matches: () => dependencies.has('vite') || rootFileSet.has('vite.config.js') || rootFileSet.has('vite.config.ts') || rootFileSet.has('vite.config.mjs'),
486
+ signals: ['vite', 'vite.config.*'],
487
+ },
488
+ {
489
+ name: 'express',
490
+ matches: () => dependencies.has('express'),
491
+ signals: ['express'],
492
+ },
493
+ {
494
+ name: 'python',
495
+ matches: () => rootFileSet.has('pyproject.toml') || rootFileSet.has('requirements.txt') || rootFileSet.has('Pipfile'),
496
+ signals: ['pyproject.toml', 'requirements.txt', 'Pipfile'],
497
+ },
498
+ {
499
+ name: 'go',
500
+ matches: () => rootFileSet.has('go.mod'),
501
+ signals: ['go.mod'],
502
+ },
503
+ {
504
+ name: 'php',
505
+ matches: () => rootFileSet.has('composer.json'),
506
+ signals: ['composer.json'],
507
+ },
508
+ {
509
+ name: 'ruby',
510
+ matches: () => rootFileSet.has('Gemfile'),
511
+ signals: ['Gemfile'],
512
+ },
513
+ {
514
+ name: 'rust',
515
+ matches: () => rootFileSet.has('Cargo.toml'),
516
+ signals: ['Cargo.toml'],
517
+ },
518
+ ];
519
+
520
+ for (const candidate of candidates) {
521
+ if (candidate.matches()) {
522
+ frameworks.push(candidate.name);
523
+ evidence.push({ framework: candidate.name, signals: candidate.signals });
524
+ }
525
+ }
526
+
527
+ const languages = collectLanguageSignals(files);
528
+
529
+ if (frameworks.length === 0 && languages.includes('typescript') && dependencies.has('react')) {
530
+ frameworks.push('react');
531
+ evidence.push({ framework: 'react', signals: ['react', 'typescript files'] });
532
+ }
533
+
534
+ const primary = frameworks[0] || 'unknown';
535
+
536
+ return {
537
+ primary,
538
+ frameworks,
539
+ languages,
540
+ evidence,
541
+ };
542
+ }
543
+
544
+ function detectConfigFiles(rootEntries) {
545
+ const rootFiles = rootEntries.filter((entry) => entry.type === 'file').map((entry) => entry.name);
546
+ const configNames = new Set([
547
+ 'package.json',
548
+ 'pnpm-lock.yaml',
549
+ 'yarn.lock',
550
+ 'package-lock.json',
551
+ 'bun.lockb',
552
+ 'bun.lock',
553
+ 'pyproject.toml',
554
+ 'requirements.txt',
555
+ 'Pipfile',
556
+ 'go.mod',
557
+ 'composer.json',
558
+ 'Cargo.toml',
559
+ 'Gemfile',
560
+ 'angular.json',
561
+ 'tsconfig.json',
562
+ 'tsconfig.app.json',
563
+ 'tsconfig.node.json',
564
+ 'vite.config.js',
565
+ 'vite.config.ts',
566
+ 'vite.config.mjs',
567
+ 'next.config.js',
568
+ 'next.config.mjs',
569
+ 'next.config.ts',
570
+ 'nuxt.config.js',
571
+ 'nuxt.config.ts',
572
+ 'svelte.config.js',
573
+ 'svelte.config.ts',
574
+ 'vue.config.js',
575
+ 'eslint.config.js',
576
+ '.eslintrc',
577
+ '.eslintrc.js',
578
+ '.eslintrc.json',
579
+ '.prettierrc',
580
+ '.prettierrc.js',
581
+ '.prettierrc.json',
582
+ ]);
583
+
584
+ return rootFiles.filter((name) => configNames.has(name));
585
+ }
586
+
587
+ function detectWorkflowFiles(files) {
588
+ return files.filter((file) => file.startsWith('.github/workflows/') && /\.(ya?ml)$/i.test(file));
589
+ }
590
+
591
+ function detectDocsFiles(files) {
592
+ return files.filter((file) => file === 'README.md' || file.startsWith('docs/'));
593
+ }
594
+
595
+ function detectRisks(projectRoot, scan) {
596
+ const risks = [];
597
+
598
+ if (!scan.project.has_package_json) {
599
+ risks.push('package.json is missing, so command detection is limited');
600
+ }
601
+
602
+ if (!scan.docs.has_readme) {
603
+ risks.push('README.md is missing, so onboarding guidance is limited');
604
+ }
605
+
606
+ if (scan.ci.github_actions_workflows.length === 0) {
607
+ risks.push('no GitHub Actions workflows were found');
608
+ }
609
+
610
+ if (scan.stack.primary === 'unknown') {
611
+ risks.push('no primary framework could be inferred from the repository signals');
612
+ }
613
+
614
+ if (scan.structure.source_directories.length === 0) {
615
+ risks.push('no common source directory names were found at the repository root');
616
+ }
617
+
618
+ if (scan.skipped_paths.length === 0) {
619
+ risks.push('no large or secret-like paths were skipped, or the repository is very small');
620
+ }
621
+
622
+ return risks;
623
+ }
624
+
625
+ function buildProjectScan(projectRoot) {
626
+ const packageJson = readJsonIfExists(path.join(projectRoot, 'package.json'));
627
+ const rootEntries = collectRootEntries(projectRoot);
628
+ const { files, skippedPaths } = collectProjectFiles(projectRoot);
629
+ const topLevelDirectories = rootEntries.filter((entry) => entry.type === 'directory' && !entry.name.startsWith('.')).map((entry) => entry.name);
630
+ const sourceDirectories = detectSourceDirectories(rootEntries);
631
+ const configFiles = detectConfigFiles(rootEntries);
632
+ const workflowFiles = detectWorkflowFiles(files);
633
+ const docsFiles = detectDocsFiles(files);
634
+ const stack = detectFrameworks(projectRoot, files, rootEntries, packageJson);
635
+ const packageManager = collectPackageManagers(projectRoot);
636
+ const workspaces = collectWorkspaces(packageJson);
637
+ const scripts = packageJson?.scripts && typeof packageJson.scripts === 'object' ? packageJson.scripts : {};
638
+ const projectName = packageJson?.name || path.basename(projectRoot) || 'unknown';
639
+ const hasReadme = fs.existsSync(path.join(projectRoot, 'README.md'));
640
+ const generatedDocs = docsFiles.filter((file) => file.startsWith('docs/'));
641
+
642
+ const scan = {
643
+ project: {
644
+ name: projectName,
645
+ root_name: path.basename(projectRoot),
646
+ has_package_json: Boolean(packageJson),
647
+ package_manager: packageManager,
648
+ workspaces,
649
+ scripts,
650
+ top_level_files: rootEntries.filter((entry) => entry.type === 'file').map((entry) => entry.name),
651
+ top_level_directories: topLevelDirectories,
652
+ },
653
+ stack: {
654
+ primary: stack.primary,
655
+ frameworks: stack.frameworks,
656
+ languages: stack.languages,
657
+ evidence: stack.evidence,
658
+ },
659
+ commands: {
660
+ install: packageManager === 'pnpm' ? 'pnpm install' : packageManager === 'yarn' ? 'yarn install' : packageManager === 'bun' ? 'bun install' : 'npm install',
661
+ scripts,
662
+ common: {
663
+ dev: scripts.dev || scripts.start || '',
664
+ build: scripts.build || '',
665
+ test: scripts.test || '',
666
+ lint: scripts.lint || '',
667
+ },
668
+ },
669
+ structure: {
670
+ top_level_directories: topLevelDirectories,
671
+ source_directories: sourceDirectories,
672
+ config_files: configFiles,
673
+ workspace_patterns: workspaces,
674
+ },
675
+ ci: {
676
+ github_actions_workflows: workflowFiles,
677
+ has_ci: workflowFiles.length > 0,
678
+ },
679
+ docs: {
680
+ has_readme: hasReadme,
681
+ files: docsFiles,
682
+ generated_files: generatedDocs,
683
+ },
684
+ risks: [],
685
+ skipped_paths: skippedPaths,
686
+ };
687
+
688
+ scan.risks = detectRisks(projectRoot, scan);
689
+
690
+ return scan;
691
+ }
692
+
693
+ function renderProjectMap(scan) {
694
+ const lines = [];
695
+
696
+ lines.push('# Project Map');
697
+ lines.push('');
698
+ lines.push('## Project');
699
+ lines.push(`- Name: ${scan.project.name}`);
700
+ lines.push(`- Root folder: ${scan.project.root_name}`);
701
+ lines.push(`- Package manager: ${scan.project.package_manager}`);
702
+ lines.push(`- package.json present: ${scan.project.has_package_json ? 'yes' : 'no'}`);
703
+ if (scan.project.workspaces.length > 0) {
704
+ lines.push(`- Workspaces: ${scan.project.workspaces.join(', ')}`);
705
+ }
706
+
707
+ lines.push('');
708
+ lines.push('## Stack');
709
+ lines.push(`- Primary: ${scan.stack.primary}`);
710
+ lines.push(`- Frameworks: ${scan.stack.frameworks.length > 0 ? scan.stack.frameworks.join(', ') : 'none detected'}`);
711
+ lines.push(`- Languages: ${scan.stack.languages.length > 0 ? scan.stack.languages.join(', ') : 'none detected'}`);
712
+
713
+ if (scan.stack.evidence.length > 0) {
714
+ lines.push('');
715
+ lines.push('### Evidence');
716
+ for (const item of scan.stack.evidence) {
717
+ lines.push(`- ${item.framework}: ${item.signals.join(', ')}`);
718
+ }
719
+ }
720
+
721
+ lines.push('');
722
+ lines.push('## Commands');
723
+ lines.push('| Command | Value |');
724
+ lines.push('|---------|-------|');
725
+ lines.push(`| Install | ${escapeMarkdownCell(scan.commands.install || 'npm install')} |`);
726
+ lines.push(`| dev | ${escapeMarkdownCell(scan.commands.common.dev || 'not defined')} |`);
727
+ lines.push(`| build | ${escapeMarkdownCell(scan.commands.common.build || 'not defined')} |`);
728
+ lines.push(`| test | ${escapeMarkdownCell(scan.commands.common.test || 'not defined')} |`);
729
+ lines.push(`| lint | ${escapeMarkdownCell(scan.commands.common.lint || 'not defined')} |`);
730
+
731
+ if (Object.keys(scan.commands.scripts).length > 0) {
732
+ lines.push('');
733
+ lines.push('### package.json scripts');
734
+ for (const [name, command] of Object.entries(scan.commands.scripts)) {
735
+ lines.push(`- ${name}: \`${command}\``);
736
+ }
737
+ }
738
+
739
+ lines.push('');
740
+ lines.push('## Structure');
741
+ lines.push(`- Top-level directories: ${scan.structure.top_level_directories.length > 0 ? scan.structure.top_level_directories.join(', ') : 'none detected'}`);
742
+ lines.push(`- Source directories: ${scan.structure.source_directories.length > 0 ? scan.structure.source_directories.join(', ') : 'none detected'}`);
743
+ lines.push(`- Config files: ${scan.structure.config_files.length > 0 ? scan.structure.config_files.join(', ') : 'none detected'}`);
744
+
745
+ lines.push('');
746
+ lines.push('## CI');
747
+ lines.push(`- GitHub Actions workflows: ${scan.ci.github_actions_workflows.length > 0 ? scan.ci.github_actions_workflows.join(', ') : 'none detected'}`);
748
+
749
+ lines.push('');
750
+ lines.push('## Docs');
751
+ lines.push(`- README present: ${scan.docs.has_readme ? 'yes' : 'no'}`);
752
+ lines.push(`- Docs files: ${scan.docs.files.length > 0 ? scan.docs.files.join(', ') : 'none detected'}`);
753
+
754
+ lines.push('');
755
+ lines.push('## Risks');
756
+ if (scan.risks.length > 0) {
757
+ for (const risk of scan.risks) {
758
+ lines.push(`- ${risk}`);
759
+ }
760
+ } else {
761
+ lines.push('- No major onboarding risks detected.');
762
+ }
763
+
764
+ lines.push('');
765
+ lines.push('## Skipped Paths');
766
+ if (scan.skipped_paths.length > 0) {
767
+ for (const skippedPath of scan.skipped_paths) {
768
+ lines.push(`- ${skippedPath}`);
769
+ }
770
+ } else {
771
+ lines.push('- None');
772
+ }
773
+
774
+ lines.push('');
775
+ return lines.join('\n');
776
+ }
777
+
778
+ function writeProjectScanArtifacts(projectRoot, scan) {
779
+ const docsDir = path.join(projectRoot, 'docs');
780
+ ensureDir(docsDir);
781
+
782
+ const jsonPath = path.join(docsDir, 'PROJECT_SCAN.json');
783
+ const mdPath = path.join(docsDir, 'PROJECT_MAP.md');
784
+
785
+ fs.writeFileSync(jsonPath, `${JSON.stringify(scan, null, 2)}\n`);
786
+ fs.writeFileSync(mdPath, `${renderProjectMap(scan)}\n`);
787
+
788
+ return { jsonPath, mdPath };
789
+ }
790
+
791
+ function runAnalyze(targetDir) {
792
+ const projectRoot = path.resolve(process.cwd(), targetDir);
793
+
794
+ if (!fs.existsSync(projectRoot)) {
795
+ throw new Error(formatError(`target directory does not exist: ${projectRoot}`));
796
+ }
797
+
798
+ const scan = buildProjectScan(projectRoot);
799
+ const artifacts = writeProjectScanArtifacts(projectRoot, scan);
800
+
801
+ console.log(`Project analysis completed for ${projectRoot}`);
802
+ console.log(`Wrote ${path.relative(projectRoot, artifacts.jsonPath)}`);
803
+ console.log(`Wrote ${path.relative(projectRoot, artifacts.mdPath)}`);
804
+ console.log(`Detected primary stack: ${scan.stack.primary}`);
805
+ console.log(`Detected package manager: ${scan.project.package_manager}`);
806
+ }
807
+
218
808
  function runDoctor(targetDir) {
219
809
  const projectRoot = path.resolve(process.cwd(), targetDir);
220
810
 
@@ -231,6 +821,8 @@ function runDoctor(targetDir) {
231
821
  const requiredFiles = [
232
822
  'README.md',
233
823
  'docs/INDEX.md',
824
+ 'docs/AI_CONTEXT.md',
825
+ 'docs/AI_ONBOARDING_PROMPT.md',
234
826
  'docs/CONTEXTO.md',
235
827
  'docs/WORKFLOW.md',
236
828
  'docs/SUPPORT_MATRIX.md',
@@ -262,6 +854,8 @@ function runDoctor(targetDir) {
262
854
  const pkg = loadPackageJson(projectRoot);
263
855
  const requiredScripts = ['check:slice', 'check:pr', 'start:slice', 'cleanup:slice'];
264
856
  const missingScripts = requiredScripts.filter((name) => typeof pkg.scripts?.[name] !== 'string');
857
+ const hasScanArtifacts = fs.existsSync(path.join(projectRoot, 'docs', 'PROJECT_SCAN.json'))
858
+ && fs.existsSync(path.join(projectRoot, 'docs', 'PROJECT_MAP.md'));
265
859
 
266
860
  const problems = [
267
861
  ...missingFiles.map((file) => `missing file: ${file}`),
@@ -276,9 +870,14 @@ function runDoctor(targetDir) {
276
870
  console.log(`Quiver doctor passed for ${projectRoot}`);
277
871
  console.log(`Generated project slug: ${projectSlug}`);
278
872
  console.log('Next steps:');
279
- console.log(`- Run ${path.join(projectRoot, 'tools', 'scripts', 'start-slice.sh')} ${path.join(projectRoot, 'specs', projectSlug, 'slices', 'slice-template', 'slice.json')}`);
280
- console.log(`- Validate a slice with ${path.join(projectRoot, 'tools', 'scripts', 'check-slice-readiness.sh')}`);
281
- console.log(`- Validate the PR gate with ${path.join(projectRoot, 'tools', 'scripts', 'check-pr-readiness.sh')}`);
873
+ if (!hasScanArtifacts) {
874
+ console.log('- Analyze the project first: npx create-quiver analyze --dir .');
875
+ } else {
876
+ console.log('- Ask your AI agent: Read docs/AI_ONBOARDING_PROMPT.md and execute it.');
877
+ }
878
+ console.log(`- Start a slice: bash tools/scripts/start-slice.sh specs/${projectSlug}/slices/slice-template/slice.json`);
879
+ console.log('- Validate a slice: bash tools/scripts/check-slice-readiness.sh');
880
+ console.log('- Validate the PR gate: bash tools/scripts/check-pr-readiness.sh');
282
881
  }
283
882
 
284
883
  function printInitNextSteps(targetDir, projectName) {
@@ -300,6 +899,11 @@ async function run(argv) {
300
899
  return;
301
900
  }
302
901
 
902
+ if (args.mode === 'analyze') {
903
+ runAnalyze(args.targetDir);
904
+ return;
905
+ }
906
+
303
907
  if (args.mode === 'doctor') {
304
908
  runDoctor(args.targetDir);
305
909
  return;