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.
- package/README.md +56 -22
- package/README_FOR_AI.md +28 -10
- package/docs/AI_CONTEXT.md.template +58 -0
- package/docs/AI_ONBOARDING_PROMPT.md.template +56 -0
- package/docs/CONTEXTO.md.template +1 -1
- package/docs/DOCUMENTATION_GUIDE.md.template +9 -7
- package/docs/INDEX.md.template +4 -0
- package/docs/WORKFLOW.md.template +6 -1
- package/package.json +1 -1
- package/scripts/init-docs.sh +53 -8
- package/scripts/package-quiver.sh +2 -0
- package/specs/quiver-v07-ai-context-pack/EVIDENCE_REPORT.md +24 -0
- package/specs/quiver-v07-ai-context-pack/SPEC.md +40 -0
- package/specs/quiver-v07-ai-context-pack/STATUS.md +24 -0
- package/specs/quiver-v07-ai-context-pack/slices/slice-01-ai-context-pack/slice.json +79 -0
- package/specs/quiver-v08-agent-onboarding-analysis/EVIDENCE_REPORT.md +49 -0
- package/specs/quiver-v08-agent-onboarding-analysis/SPEC.md +53 -0
- package/specs/quiver-v08-agent-onboarding-analysis/STATUS.md +26 -0
- package/specs/quiver-v08-agent-onboarding-analysis/slices/slice-01-project-scan-command/slice.json +73 -0
- package/specs/quiver-v08-agent-onboarding-analysis/slices/slice-02-ai-onboarding-prompt/slice.json +82 -0
- package/specs/quiver-v08-agent-onboarding-analysis/slices/slice-03-doctor-readme-adoption-flow/slice.json +76 -0
- package/specs/quiver-v09-onboarding-readme-flow/EVIDENCE_REPORT.md +33 -0
- package/specs/quiver-v09-onboarding-readme-flow/SPEC.md +44 -0
- package/specs/quiver-v09-onboarding-readme-flow/STATUS.md +25 -0
- package/specs/quiver-v09-onboarding-readme-flow/slices/slice-01-developer-readme-onboarding-flow/slice.json +69 -0
- package/specs/quiver-v09-onboarding-readme-flow/slices/slice-02-ai-handoff-doctor-guidance/slice.json +71 -0
- package/specs/quiver-v10-local-project-installation-guidance/EVIDENCE_REPORT.md +25 -0
- package/specs/quiver-v10-local-project-installation-guidance/SPEC.md +42 -0
- package/specs/quiver-v10-local-project-installation-guidance/STATUS.md +24 -0
- package/specs/quiver-v10-local-project-installation-guidance/slices/slice-01-local-project-installation-guidance/slice.json +75 -0
- 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
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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;
|