create-quiver 0.16.0 → 0.17.1
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 +8 -0
- package/README.md +7 -1
- package/README_FOR_AI.md +4 -1
- package/docs/CLI_UX_GUIDE.md +11 -0
- package/docs/INDEX.md +5 -1
- package/docs/reference/commands.md +34 -0
- package/package.json +1 -1
- package/specs/quiver-v43-cli-i18n-audit-release-readiness/command-language-mode-matrix.json +31 -1
- package/specs/quiver-v46-deep-project-analysis/EVIDENCE_REPORT.md +94 -0
- package/specs/quiver-v46-deep-project-analysis/EXECUTION_PLAN.md +26 -0
- package/specs/quiver-v46-deep-project-analysis/SPEC.md +157 -0
- package/specs/quiver-v46-deep-project-analysis/STATUS.md +26 -0
- package/specs/quiver-v46-deep-project-analysis/pr.md +131 -0
- package/specs/quiver-v46-deep-project-analysis/slices/slice-00-analysis-contract-foundation/CLOSURE_BRIEF.md +14 -0
- package/specs/quiver-v46-deep-project-analysis/slices/slice-00-analysis-contract-foundation/EXECUTION_BRIEF.md +41 -0
- package/specs/quiver-v46-deep-project-analysis/slices/slice-00-analysis-contract-foundation/slice.json +61 -0
- package/specs/quiver-v46-deep-project-analysis/slices/slice-01-stack-agnostic-discovery-sampling/CLOSURE_BRIEF.md +22 -0
- package/specs/quiver-v46-deep-project-analysis/slices/slice-01-stack-agnostic-discovery-sampling/EXECUTION_BRIEF.md +33 -0
- package/specs/quiver-v46-deep-project-analysis/slices/slice-01-stack-agnostic-discovery-sampling/slice.json +80 -0
- package/specs/quiver-v46-deep-project-analysis/slices/slice-02-provider-analysis-json-contract/CLOSURE_BRIEF.md +21 -0
- package/specs/quiver-v46-deep-project-analysis/slices/slice-02-provider-analysis-json-contract/EXECUTION_BRIEF.md +34 -0
- package/specs/quiver-v46-deep-project-analysis/slices/slice-02-provider-analysis-json-contract/slice.json +79 -0
- package/specs/quiver-v46-deep-project-analysis/slices/slice-03-doc-proposal-review-safe-writes/CLOSURE_BRIEF.md +19 -0
- package/specs/quiver-v46-deep-project-analysis/slices/slice-03-doc-proposal-review-safe-writes/EXECUTION_BRIEF.md +33 -0
- package/specs/quiver-v46-deep-project-analysis/slices/slice-03-doc-proposal-review-safe-writes/slice.json +79 -0
- package/specs/quiver-v46-deep-project-analysis/slices/slice-04-validation-docs-release-readiness/CLOSURE_BRIEF.md +20 -0
- package/specs/quiver-v46-deep-project-analysis/slices/slice-04-validation-docs-release-readiness/EXECUTION_BRIEF.md +32 -0
- package/specs/quiver-v46-deep-project-analysis/slices/slice-04-validation-docs-release-readiness/slice.json +85 -0
- package/src/create-quiver/commands/ai.js +587 -0
- package/src/create-quiver/index.js +113 -3
- package/src/create-quiver/lib/ai/analyze-project-discovery.js +387 -0
- package/src/create-quiver/lib/ai/analyze-project-docs.js +364 -0
- package/src/create-quiver/lib/ai/analyze-project-parser.js +277 -0
- package/src/create-quiver/lib/ai/analyze-project-prompts.js +214 -0
- package/src/create-quiver/lib/ai/analyze-project-review.js +99 -0
- package/src/create-quiver/lib/ai/analyze-project-sampling.js +402 -0
- package/src/create-quiver/lib/ai/analyze-project-schema.js +92 -0
- package/src/create-quiver/lib/ai/analyze-project-validation.js +309 -0
- package/src/create-quiver/lib/ai/artifacts.js +4 -1
- package/src/create-quiver/lib/cli/command-registry.js +1 -0
- package/src/create-quiver/lib/i18n/messages/en.js +1 -0
- package/src/create-quiver/lib/i18n/messages/es.js +1 -0
- package/.quiver/runs/run-2026-06-09t19-14-39z/approvals.json +0 -5
- package/.quiver/runs/run-2026-06-09t19-14-39z/decisions.md +0 -2
- package/.quiver/runs/run-2026-06-09t19-14-39z/requirement.md +0 -0
- package/.quiver/runs/run-2026-06-09t19-14-39z/snapshots/20260609T191439Z/docs/INDEX.md +0 -97
- package/.quiver/runs/run-2026-06-09t19-14-39z/snapshots/20260609T191439Z/manifest.json +0 -61
- package/.quiver/runs/run-2026-06-09t19-14-39z/state.json +0 -28
- package/ACTIVE_SLICES.md +0 -43
- package/auditoria-ux-ui-performance-documentacion.md +0 -705
- package/copys-landing-page.md +0 -244
- package/docs/ai/ACTIVE_SLICE.md +0 -61
- package/pr.md +0 -154
|
@@ -12,6 +12,7 @@ const {
|
|
|
12
12
|
const {
|
|
13
13
|
runActiveSlice: runAiActiveSlice,
|
|
14
14
|
runAgent: runAiAgent,
|
|
15
|
+
runAnalyzeProject: runAiAnalyzeProject,
|
|
15
16
|
runApprovalStatus: runAiApprovalStatus,
|
|
16
17
|
runApprove: runAiApprove,
|
|
17
18
|
runDoctor: runAiDoctor,
|
|
@@ -229,6 +230,7 @@ const COMMAND_HELP_GROUPS = [
|
|
|
229
230
|
['ai active-slice status|reconcile', 'Inspect or dry-run reconcile local active-slice state from every supported source.'],
|
|
230
231
|
['ai status', 'Show current AI lifecycle phase, approved versions, blockers, and next command.'],
|
|
231
232
|
['ai resume', 'Resume guidance from the last valid lifecycle phase without chat memory.'],
|
|
233
|
+
['ai analyze-project', 'Read and explain a bounded project sample without provider execution or writes.'],
|
|
232
234
|
['ai onboard', 'Run or print the planner onboarding prompt with a token-aware context pack.'],
|
|
233
235
|
['ai prepare-context', 'Preview or write docs-only AI context updates with assumptions and risks.'],
|
|
234
236
|
['ai agent set|list|show|doctor|repair', 'Manage, diagnose, and dry-run repair planner, executor, reviewer, and doctor provider profiles without secrets.'],
|
|
@@ -327,6 +329,7 @@ function printUsage(language = DEFAULT_LANGUAGE) {
|
|
|
327
329
|
npx create-quiver ai active-slice reconcile --dry-run
|
|
328
330
|
npx create-quiver ai status [options]
|
|
329
331
|
npx create-quiver ai resume [options]
|
|
332
|
+
npx create-quiver ai analyze-project [--deep] [--dry-run] [--json]
|
|
330
333
|
npx create-quiver ai inspect [options]
|
|
331
334
|
npx create-quiver ai export [--format json|markdown]
|
|
332
335
|
npx create-quiver ai specs list [--json]
|
|
@@ -386,7 +389,14 @@ ${helpText(help, 'headings', 'options', 'Options:')}
|
|
|
386
389
|
--full ${optionDescription(help, 'Plan or run the full compatibility init profile')}
|
|
387
390
|
--legacy-scripts ${optionDescription(help, 'Include legacy Bash wrappers in init profile')}
|
|
388
391
|
--include-templates ${optionDescription(help, 'Export packaged templates in init profile')}
|
|
389
|
-
--dry-run ${optionDescription(help, 'Preview init, analyze, migrate, prepare, spec create/start/close, demo, ai agent set, or AI work without executing writes/providers')}
|
|
392
|
+
--dry-run ${optionDescription(help, 'Preview init, analyze, migrate, prepare, spec create/start/close, demo, ai agent set, ai analyze-project, or AI work without executing writes/providers')}
|
|
393
|
+
--deep ${optionDescription(help, 'For ai analyze-project, include source and DB files in the read-only sample')}
|
|
394
|
+
--max-files <n> ${optionDescription(help, 'For ai analyze-project, maximum files in the semantic sample')}
|
|
395
|
+
--max-bytes <n> ${optionDescription(help, 'For ai analyze-project, maximum selected bytes in the semantic sample')}
|
|
396
|
+
--include-source ${optionDescription(help, 'For ai analyze-project, include source files without requiring --deep')}
|
|
397
|
+
--include-tests ${optionDescription(help, 'For ai analyze-project, include test files in the sample')}
|
|
398
|
+
--include-db ${optionDescription(help, 'For ai analyze-project, include schema, migration, and DB files in the sample')}
|
|
399
|
+
--scope <path|name> ${optionDescription(help, 'For ai analyze-project, restrict discovery to a repo path or workspace name')}
|
|
390
400
|
--print-prompt ${optionDescription(help, 'Print the exact AI prompt and exit without executing provider CLIs')}
|
|
391
401
|
--with-planner ${optionDescription(help, 'Enable planner-assisted behavior on commands that explicitly support it')}
|
|
392
402
|
--interactive ${optionDescription(help, 'Enable prompts on commands that explicitly support interactive choices')}
|
|
@@ -435,6 +445,8 @@ ${helpText(help, 'headings', 'examples', 'Examples:')}
|
|
|
435
445
|
cd ./my-project && npx create-quiver plan --json
|
|
436
446
|
cd ./my-project && npx create-quiver ai onboard --dry-run
|
|
437
447
|
cd ./my-project && npx create-quiver ai onboard --print-prompt
|
|
448
|
+
cd ./my-project && npx create-quiver ai analyze-project --deep --dry-run
|
|
449
|
+
cd ./my-project && npx create-quiver ai analyze-project --deep --json
|
|
438
450
|
cd ./my-project && npx create-quiver ai prepare-context --dry-run
|
|
439
451
|
cd ./my-project && npx create-quiver ai run create --input requirements.md
|
|
440
452
|
cd ./my-project && npx create-quiver ai active-slice reconcile --dry-run
|
|
@@ -563,6 +575,14 @@ function parseArgs(argv, options = {}) {
|
|
|
563
575
|
aiInput: '',
|
|
564
576
|
aiSlice: '',
|
|
565
577
|
aiTimeout: null,
|
|
578
|
+
aiAnalyzeDeep: false,
|
|
579
|
+
aiAnalyzeIncludeDb: false,
|
|
580
|
+
aiAnalyzeIncludeSource: false,
|
|
581
|
+
aiAnalyzeIncludeTests: false,
|
|
582
|
+
aiAnalyzeMaxBytes: null,
|
|
583
|
+
aiAnalyzeMaxFiles: null,
|
|
584
|
+
aiAnalyzeScope: '',
|
|
585
|
+
aiAnalyzeOptionUsed: false,
|
|
566
586
|
aiCommit: false,
|
|
567
587
|
aiAllowDirty: false,
|
|
568
588
|
aiExecute: false,
|
|
@@ -719,6 +739,68 @@ function parseArgs(argv, options = {}) {
|
|
|
719
739
|
continue;
|
|
720
740
|
}
|
|
721
741
|
|
|
742
|
+
if (arg === '--deep') {
|
|
743
|
+
result.aiAnalyzeDeep = true;
|
|
744
|
+
result.aiAnalyzeOptionUsed = true;
|
|
745
|
+
continue;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
if (arg === '--include-source') {
|
|
749
|
+
result.aiAnalyzeIncludeSource = true;
|
|
750
|
+
result.aiAnalyzeOptionUsed = true;
|
|
751
|
+
continue;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
if (arg === '--include-tests') {
|
|
755
|
+
result.aiAnalyzeIncludeTests = true;
|
|
756
|
+
result.aiAnalyzeOptionUsed = true;
|
|
757
|
+
continue;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
if (arg === '--include-db') {
|
|
761
|
+
result.aiAnalyzeIncludeDb = true;
|
|
762
|
+
result.aiAnalyzeOptionUsed = true;
|
|
763
|
+
continue;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
if (arg === '--max-files') {
|
|
767
|
+
const value = args[++index];
|
|
768
|
+
if (typeof value === 'undefined') {
|
|
769
|
+
throw new Error(formatError('missing value for --max-files'));
|
|
770
|
+
}
|
|
771
|
+
const parsed = Number.parseInt(value, 10);
|
|
772
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
773
|
+
throw new Error(formatError('invalid value for --max-files'));
|
|
774
|
+
}
|
|
775
|
+
result.aiAnalyzeMaxFiles = parsed;
|
|
776
|
+
result.aiAnalyzeOptionUsed = true;
|
|
777
|
+
continue;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
if (arg === '--max-bytes') {
|
|
781
|
+
const value = args[++index];
|
|
782
|
+
if (typeof value === 'undefined') {
|
|
783
|
+
throw new Error(formatError('missing value for --max-bytes'));
|
|
784
|
+
}
|
|
785
|
+
const parsed = Number.parseInt(value, 10);
|
|
786
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
787
|
+
throw new Error(formatError('invalid value for --max-bytes'));
|
|
788
|
+
}
|
|
789
|
+
result.aiAnalyzeMaxBytes = parsed;
|
|
790
|
+
result.aiAnalyzeOptionUsed = true;
|
|
791
|
+
continue;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
if (arg === '--scope') {
|
|
795
|
+
const value = args[++index];
|
|
796
|
+
if (!value || String(value).startsWith('--')) {
|
|
797
|
+
throw new Error(formatError('missing value for --scope'));
|
|
798
|
+
}
|
|
799
|
+
result.aiAnalyzeScope = value;
|
|
800
|
+
result.aiAnalyzeOptionUsed = true;
|
|
801
|
+
continue;
|
|
802
|
+
}
|
|
803
|
+
|
|
722
804
|
if (arg === '--print-prompt') {
|
|
723
805
|
result.aiPrintPrompt = true;
|
|
724
806
|
continue;
|
|
@@ -1178,6 +1260,10 @@ function parseArgs(argv, options = {}) {
|
|
|
1178
1260
|
positional.push(arg);
|
|
1179
1261
|
}
|
|
1180
1262
|
|
|
1263
|
+
if (result.aiAnalyzeOptionUsed && result.mode !== 'ai') {
|
|
1264
|
+
throw new Error(formatError('analysis flags are only supported by ai analyze-project'));
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1181
1267
|
if (result.mode === 'init') {
|
|
1182
1268
|
if (!result.projectName && positional.length > 0) {
|
|
1183
1269
|
result.projectName = positional.shift();
|
|
@@ -1232,6 +1318,9 @@ function parseArgs(argv, options = {}) {
|
|
|
1232
1318
|
if (!result.aiCommand && positional.length > 0) {
|
|
1233
1319
|
result.aiCommand = positional.shift();
|
|
1234
1320
|
}
|
|
1321
|
+
if (result.aiAnalyzeOptionUsed && result.aiCommand !== 'analyze-project') {
|
|
1322
|
+
throw new Error(formatError('analysis flags are only supported by ai analyze-project'));
|
|
1323
|
+
}
|
|
1235
1324
|
if (result.aiCommand === 'agent') {
|
|
1236
1325
|
if (!result.aiAgentCommand && positional.length > 0) {
|
|
1237
1326
|
result.aiAgentCommand = positional.shift();
|
|
@@ -3420,7 +3509,28 @@ async function run(argv) {
|
|
|
3420
3509
|
|
|
3421
3510
|
if (args.mode === 'ai') {
|
|
3422
3511
|
if (!args.aiCommand) {
|
|
3423
|
-
throw new Error(formatError('missing ai subcommand. Use: npx create-quiver ai onboard | prepare-context | run | active-slice | status | resume | inspect | export | specs | slices | models | trace | plan | revise | repair-plan | review-plan | approve | approvals | agent | prompt-slice | execute-slice | execute-plan | doctor | pr'));
|
|
3512
|
+
throw new Error(formatError('missing ai subcommand. Use: npx create-quiver ai analyze-project | onboard | prepare-context | run | active-slice | status | resume | inspect | export | specs | slices | models | trace | plan | revise | repair-plan | review-plan | approve | approvals | agent | prompt-slice | execute-slice | execute-plan | doctor | pr'));
|
|
3513
|
+
}
|
|
3514
|
+
|
|
3515
|
+
if (args.aiCommand === 'analyze-project') {
|
|
3516
|
+
await runAiAnalyzeProject(process.cwd(), {
|
|
3517
|
+
deep: args.aiAnalyzeDeep,
|
|
3518
|
+
dryRun: args.dryRun,
|
|
3519
|
+
force: args.force,
|
|
3520
|
+
includeDb: args.aiAnalyzeIncludeDb,
|
|
3521
|
+
includeSource: args.aiAnalyzeIncludeSource,
|
|
3522
|
+
includeTests: args.aiAnalyzeIncludeTests,
|
|
3523
|
+
interactive: args.interactive,
|
|
3524
|
+
json: args.json,
|
|
3525
|
+
language: args.language,
|
|
3526
|
+
maxBytes: args.aiAnalyzeMaxBytes || undefined,
|
|
3527
|
+
maxFiles: args.aiAnalyzeMaxFiles || undefined,
|
|
3528
|
+
printPrompt: args.aiPrintPrompt,
|
|
3529
|
+
review: args.review,
|
|
3530
|
+
scope: args.aiAnalyzeScope || undefined,
|
|
3531
|
+
strict: args.strict,
|
|
3532
|
+
});
|
|
3533
|
+
return;
|
|
3424
3534
|
}
|
|
3425
3535
|
|
|
3426
3536
|
if (args.aiCommand === 'run') {
|
|
@@ -3738,7 +3848,7 @@ async function run(argv) {
|
|
|
3738
3848
|
return;
|
|
3739
3849
|
}
|
|
3740
3850
|
|
|
3741
|
-
throw new Error(formatError(`unsupported ai subcommand: ${args.aiCommand}. Supported tasks: onboard, prepare-context, run, active-slice, status, resume, inspect, export, specs, slices, models, trace, plan, revise, repair-plan, review-plan, approve, approvals, agent, prompt-slice, execute-slice, execute-plan, doctor, pr`));
|
|
3851
|
+
throw new Error(formatError(`unsupported ai subcommand: ${args.aiCommand}. Supported tasks: analyze-project, onboard, prepare-context, run, active-slice, status, resume, inspect, export, specs, slices, models, trace, plan, revise, repair-plan, review-plan, approve, approvals, agent, prompt-slice, execute-slice, execute-plan, doctor, pr`));
|
|
3742
3852
|
}
|
|
3743
3853
|
|
|
3744
3854
|
if (args.mode === 'graph') {
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
|
|
4
|
+
const { getContextPathExclusionReason } = require('./safety');
|
|
5
|
+
const { classifyProjectFile, summarizeByReason } = require('./analyze-project-sampling');
|
|
6
|
+
|
|
7
|
+
const BINARY_EXTENSIONS = new Set([
|
|
8
|
+
'.7z',
|
|
9
|
+
'.avif',
|
|
10
|
+
'.bmp',
|
|
11
|
+
'.class',
|
|
12
|
+
'.dll',
|
|
13
|
+
'.dmg',
|
|
14
|
+
'.doc',
|
|
15
|
+
'.docx',
|
|
16
|
+
'.eot',
|
|
17
|
+
'.exe',
|
|
18
|
+
'.gif',
|
|
19
|
+
'.gz',
|
|
20
|
+
'.ico',
|
|
21
|
+
'.jar',
|
|
22
|
+
'.jpeg',
|
|
23
|
+
'.jpg',
|
|
24
|
+
'.lockb',
|
|
25
|
+
'.mov',
|
|
26
|
+
'.mp3',
|
|
27
|
+
'.mp4',
|
|
28
|
+
'.otf',
|
|
29
|
+
'.pdf',
|
|
30
|
+
'.png',
|
|
31
|
+
'.so',
|
|
32
|
+
'.tar',
|
|
33
|
+
'.ttf',
|
|
34
|
+
'.wasm',
|
|
35
|
+
'.webm',
|
|
36
|
+
'.webp',
|
|
37
|
+
'.woff',
|
|
38
|
+
'.woff2',
|
|
39
|
+
'.zip',
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
function toPosix(filePath) {
|
|
43
|
+
return String(filePath || '').replace(/\\/g, '/');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function relativePath(from, to) {
|
|
47
|
+
const relative = toPosix(path.relative(from, to));
|
|
48
|
+
return relative || '.';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function isInside(parent, child) {
|
|
52
|
+
const relative = path.relative(parent, child);
|
|
53
|
+
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function safeReadJson(filePath) {
|
|
57
|
+
try {
|
|
58
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
59
|
+
} catch (_) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function detectPackageManager(repoRoot) {
|
|
65
|
+
const checks = [
|
|
66
|
+
['pnpm', 'pnpm-lock.yaml'],
|
|
67
|
+
['yarn', 'yarn.lock'],
|
|
68
|
+
['npm', 'package-lock.json'],
|
|
69
|
+
['bun', 'bun.lockb'],
|
|
70
|
+
];
|
|
71
|
+
for (const [manager, fileName] of checks) {
|
|
72
|
+
if (fs.existsSync(path.join(repoRoot, fileName))) {
|
|
73
|
+
return manager;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const pkg = safeReadJson(path.join(repoRoot, 'package.json'));
|
|
77
|
+
if (pkg && typeof pkg.packageManager === 'string') {
|
|
78
|
+
return pkg.packageManager.split('@')[0] || 'unknown';
|
|
79
|
+
}
|
|
80
|
+
return 'unknown';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function readPackageJson(repoRoot, relativeDir = '.') {
|
|
84
|
+
const packagePath = path.join(repoRoot, relativeDir, 'package.json');
|
|
85
|
+
const pkg = fs.existsSync(packagePath) ? safeReadJson(packagePath) : null;
|
|
86
|
+
return pkg && typeof pkg === 'object' ? pkg : null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function expandWorkspacePattern(repoRoot, pattern) {
|
|
90
|
+
const normalized = toPosix(pattern).trim();
|
|
91
|
+
if (!normalized || normalized.startsWith('!')) {
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const withoutRecursive = normalized.replace(/\/\*\*.*$/, '');
|
|
96
|
+
if (!withoutRecursive.includes('*')) {
|
|
97
|
+
const absolute = path.resolve(repoRoot, withoutRecursive);
|
|
98
|
+
return fs.existsSync(absolute) && fs.statSync(absolute).isDirectory() ? [relativePath(repoRoot, absolute)] : [];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const parts = withoutRecursive.split('/');
|
|
102
|
+
const starIndex = parts.indexOf('*');
|
|
103
|
+
if (starIndex === -1) {
|
|
104
|
+
return [];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const baseRelative = parts.slice(0, starIndex).join('/') || '.';
|
|
108
|
+
const suffix = parts.slice(starIndex + 1);
|
|
109
|
+
const baseAbsolute = path.resolve(repoRoot, baseRelative);
|
|
110
|
+
if (!fs.existsSync(baseAbsolute) || !fs.statSync(baseAbsolute).isDirectory()) {
|
|
111
|
+
return [];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const results = [];
|
|
115
|
+
for (const entry of fs.readdirSync(baseAbsolute, { withFileTypes: true })) {
|
|
116
|
+
if (!entry.isDirectory()) {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
const workspaceAbsolute = path.join(baseAbsolute, entry.name, ...suffix);
|
|
120
|
+
if (fs.existsSync(workspaceAbsolute) && fs.statSync(workspaceAbsolute).isDirectory()) {
|
|
121
|
+
results.push(relativePath(repoRoot, workspaceAbsolute));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return results;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function readPnpmWorkspacePatterns(repoRoot) {
|
|
128
|
+
const filePath = path.join(repoRoot, 'pnpm-workspace.yaml');
|
|
129
|
+
if (!fs.existsSync(filePath)) {
|
|
130
|
+
return [];
|
|
131
|
+
}
|
|
132
|
+
const lines = fs.readFileSync(filePath, 'utf8').split(/\r?\n/);
|
|
133
|
+
return lines
|
|
134
|
+
.map((line) => line.trim().match(/^-\s+['"]?([^'"]+)['"]?$/))
|
|
135
|
+
.filter(Boolean)
|
|
136
|
+
.map((match) => match[1]);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function discoverWorkspaceRoots(repoRoot) {
|
|
140
|
+
const rootPackage = readPackageJson(repoRoot);
|
|
141
|
+
const patterns = [];
|
|
142
|
+
const sources = new Map();
|
|
143
|
+
|
|
144
|
+
if (rootPackage && Array.isArray(rootPackage.workspaces)) {
|
|
145
|
+
for (const pattern of rootPackage.workspaces) {
|
|
146
|
+
patterns.push(pattern);
|
|
147
|
+
sources.set(pattern, 'package.json#workspaces');
|
|
148
|
+
}
|
|
149
|
+
} else if (rootPackage && rootPackage.workspaces && Array.isArray(rootPackage.workspaces.packages)) {
|
|
150
|
+
for (const pattern of rootPackage.workspaces.packages) {
|
|
151
|
+
patterns.push(pattern);
|
|
152
|
+
sources.set(pattern, 'package.json#workspaces.packages');
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
for (const pattern of readPnpmWorkspacePatterns(repoRoot)) {
|
|
157
|
+
patterns.push(pattern);
|
|
158
|
+
sources.set(pattern, 'pnpm-workspace.yaml');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
for (const conventional of ['apps/*', 'packages/*']) {
|
|
162
|
+
if (fs.existsSync(path.join(repoRoot, conventional.split('/')[0]))) {
|
|
163
|
+
patterns.push(conventional);
|
|
164
|
+
sources.set(conventional, 'convention');
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const rootsByPath = new Map();
|
|
169
|
+
rootsByPath.set('.', {
|
|
170
|
+
name: rootPackage?.name || path.basename(repoRoot),
|
|
171
|
+
path: '.',
|
|
172
|
+
source: 'repo-root',
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
for (const pattern of patterns) {
|
|
176
|
+
for (const relative of expandWorkspacePattern(repoRoot, pattern)) {
|
|
177
|
+
const pkg = readPackageJson(repoRoot, relative);
|
|
178
|
+
rootsByPath.set(relative, {
|
|
179
|
+
name: pkg?.name || path.basename(relative),
|
|
180
|
+
path: relative,
|
|
181
|
+
source: sources.get(pattern) || 'workspace-pattern',
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return [...rootsByPath.values()].sort((a, b) => a.path.localeCompare(b.path));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function resolveAnalysisRoot(repoRoot, scope, workspaceRoots) {
|
|
190
|
+
if (!scope) {
|
|
191
|
+
return {
|
|
192
|
+
absolutePath: repoRoot,
|
|
193
|
+
relativePath: '.',
|
|
194
|
+
source: 'repo-root',
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const matchingWorkspace = workspaceRoots.find((workspace) => workspace.name === scope || workspace.path === scope);
|
|
199
|
+
if (matchingWorkspace) {
|
|
200
|
+
return {
|
|
201
|
+
absolutePath: path.resolve(repoRoot, matchingWorkspace.path),
|
|
202
|
+
relativePath: matchingWorkspace.path,
|
|
203
|
+
source: 'workspace-scope',
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const absolutePath = path.resolve(repoRoot, scope);
|
|
208
|
+
if (!isInside(repoRoot, absolutePath)) {
|
|
209
|
+
throw new Error(`scope is outside the repository: ${scope}`);
|
|
210
|
+
}
|
|
211
|
+
if (!fs.existsSync(absolutePath) || !fs.statSync(absolutePath).isDirectory()) {
|
|
212
|
+
throw new Error(`scope directory does not exist: ${scope}`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
absolutePath,
|
|
217
|
+
relativePath: relativePath(repoRoot, absolutePath),
|
|
218
|
+
source: 'path-scope',
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function isBinaryFile(filePath) {
|
|
223
|
+
const extension = path.extname(filePath).toLowerCase();
|
|
224
|
+
if (BINARY_EXTENSIONS.has(extension)) {
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
let descriptor;
|
|
229
|
+
try {
|
|
230
|
+
descriptor = fs.openSync(filePath, 'r');
|
|
231
|
+
const buffer = Buffer.alloc(1024);
|
|
232
|
+
const bytesRead = fs.readSync(descriptor, buffer, 0, buffer.length, 0);
|
|
233
|
+
for (let index = 0; index < bytesRead; index += 1) {
|
|
234
|
+
if (buffer[index] === 0) {
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
} catch (_) {
|
|
239
|
+
return true;
|
|
240
|
+
} finally {
|
|
241
|
+
if (typeof descriptor === 'number') {
|
|
242
|
+
fs.closeSync(descriptor);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function walkFiles(repoRoot, analysisRoot) {
|
|
250
|
+
const files = [];
|
|
251
|
+
const skipped = [];
|
|
252
|
+
const safetyExclusions = [];
|
|
253
|
+
|
|
254
|
+
function visit(currentPath) {
|
|
255
|
+
const entries = fs.readdirSync(currentPath, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
256
|
+
for (const entry of entries) {
|
|
257
|
+
const absolutePath = path.join(currentPath, entry.name);
|
|
258
|
+
const relative = relativePath(repoRoot, absolutePath);
|
|
259
|
+
const normalizedRelative = relative === '.' ? entry.name : relative;
|
|
260
|
+
|
|
261
|
+
if (entry.isSymbolicLink()) {
|
|
262
|
+
skipped.push({ path: toPosix(normalizedRelative), reason: 'symlink' });
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const exclusionReason = getContextPathExclusionReason(normalizedRelative);
|
|
267
|
+
if (exclusionReason) {
|
|
268
|
+
safetyExclusions.push({ path: toPosix(normalizedRelative), reason: exclusionReason });
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (entry.isDirectory()) {
|
|
273
|
+
visit(absolutePath);
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (!entry.isFile()) {
|
|
278
|
+
skipped.push({ path: toPosix(normalizedRelative), reason: 'not-regular-file' });
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const stat = fs.statSync(absolutePath);
|
|
283
|
+
if (isBinaryFile(absolutePath)) {
|
|
284
|
+
skipped.push({ path: toPosix(normalizedRelative), reason: 'binary-file' });
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
files.push({
|
|
289
|
+
path: toPosix(normalizedRelative),
|
|
290
|
+
bytes: stat.size,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
visit(analysisRoot);
|
|
296
|
+
return { files, safetyExclusions, skipped };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function detectStack(repoRoot, files) {
|
|
300
|
+
const stack = new Set();
|
|
301
|
+
const configs = [];
|
|
302
|
+
const entrypoints = [];
|
|
303
|
+
const sourceRoots = new Set();
|
|
304
|
+
const packageJson = readPackageJson(repoRoot);
|
|
305
|
+
const dependencies = {
|
|
306
|
+
...(packageJson?.dependencies || {}),
|
|
307
|
+
...(packageJson?.devDependencies || {}),
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
for (const dependency of Object.keys(dependencies)) {
|
|
311
|
+
if (dependency === 'next') stack.add('nextjs');
|
|
312
|
+
if (dependency === 'react') stack.add('react');
|
|
313
|
+
if (dependency === 'vue') stack.add('vue');
|
|
314
|
+
if (dependency === 'svelte') stack.add('svelte');
|
|
315
|
+
if (dependency === '@nestjs/core') stack.add('nestjs');
|
|
316
|
+
if (dependency === 'express') stack.add('express');
|
|
317
|
+
if (dependency === 'fastify') stack.add('fastify');
|
|
318
|
+
if (dependency === 'prisma' || dependency === '@prisma/client') stack.add('prisma');
|
|
319
|
+
if (dependency === 'drizzle-orm') stack.add('drizzle');
|
|
320
|
+
if (dependency.includes('supabase')) stack.add('supabase');
|
|
321
|
+
if (dependency === 'typescript') stack.add('typescript');
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
for (const file of files) {
|
|
325
|
+
const classification = classifyProjectFile(file.path);
|
|
326
|
+
const firstSegment = file.path.split('/')[0];
|
|
327
|
+
if (classification.signals.includes('config')) {
|
|
328
|
+
configs.push(file.path);
|
|
329
|
+
}
|
|
330
|
+
if (classification.signals.includes('entrypoint')) {
|
|
331
|
+
entrypoints.push(file.path);
|
|
332
|
+
}
|
|
333
|
+
if (classification.signals.some((signal) => ['source', 'frontend', 'backend'].includes(signal))) {
|
|
334
|
+
sourceRoots.add(firstSegment);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const base = path.posix.basename(file.path);
|
|
338
|
+
if (base === 'pyproject.toml') stack.add('python');
|
|
339
|
+
if (base === 'pom.xml') stack.add('java');
|
|
340
|
+
if (base === 'Gemfile') stack.add('ruby');
|
|
341
|
+
if (base === 'go.mod') stack.add('go');
|
|
342
|
+
if (base === 'composer.json') stack.add('php');
|
|
343
|
+
if (base === 'pubspec.yaml') stack.add('dart');
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return {
|
|
347
|
+
stack: [...stack].sort(),
|
|
348
|
+
configs: configs.sort(),
|
|
349
|
+
entrypoints: entrypoints.sort(),
|
|
350
|
+
source_roots: [...sourceRoots].sort(),
|
|
351
|
+
scripts: packageJson?.scripts || {},
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function discoverProjectFiles(repoRoot, options = {}) {
|
|
356
|
+
const resolvedRepoRoot = path.resolve(repoRoot);
|
|
357
|
+
const workspaceRoots = discoverWorkspaceRoots(resolvedRepoRoot);
|
|
358
|
+
const analysisRoot = resolveAnalysisRoot(resolvedRepoRoot, options.scope, workspaceRoots);
|
|
359
|
+
const walked = walkFiles(resolvedRepoRoot, analysisRoot.absolutePath);
|
|
360
|
+
const packageJson = readPackageJson(resolvedRepoRoot);
|
|
361
|
+
|
|
362
|
+
return {
|
|
363
|
+
project: {
|
|
364
|
+
name: packageJson?.name || path.basename(resolvedRepoRoot),
|
|
365
|
+
root: resolvedRepoRoot,
|
|
366
|
+
package_manager: detectPackageManager(resolvedRepoRoot),
|
|
367
|
+
},
|
|
368
|
+
roots: {
|
|
369
|
+
repo_root: resolvedRepoRoot,
|
|
370
|
+
analysis_root: analysisRoot.relativePath,
|
|
371
|
+
analysis_root_source: analysisRoot.source,
|
|
372
|
+
workspaces: workspaceRoots,
|
|
373
|
+
},
|
|
374
|
+
files: walked.files,
|
|
375
|
+
skippedFiles: walked.skipped,
|
|
376
|
+
skippedSummary: summarizeByReason(walked.skipped),
|
|
377
|
+
safetyExclusions: walked.safetyExclusions,
|
|
378
|
+
safetySummary: summarizeByReason(walked.safetyExclusions),
|
|
379
|
+
detected: detectStack(resolvedRepoRoot, walked.files),
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
module.exports = {
|
|
384
|
+
discoverProjectFiles,
|
|
385
|
+
discoverWorkspaceRoots,
|
|
386
|
+
resolveAnalysisRoot,
|
|
387
|
+
};
|