codereview-aia 0.1.2 → 0.1.4
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/dist/analysis/static/wpPhpcsRunner.d.ts +11 -0
- package/dist/analysis/static/wpPhpcsRunner.js +219 -0
- package/dist/analysis/static/wpPhpcsRunner.js.map +1 -0
- package/dist/clients/implementations/openRouterClient.js +2 -2
- package/dist/clients/implementations/openRouterClient.js.map +1 -1
- package/dist/clients/openRouterClient.js +2 -2
- package/dist/clients/openRouterClient.js.map +1 -1
- package/dist/clients/utils/promptFormatter.d.ts +3 -2
- package/dist/clients/utils/promptFormatter.js +82 -24
- package/dist/clients/utils/promptFormatter.js.map +1 -1
- package/dist/core/ConfigurationService.d.ts +21 -0
- package/dist/core/ConfigurationService.js +39 -0
- package/dist/core/ConfigurationService.js.map +1 -1
- package/dist/core/handlers/FileProcessingHandler.js +5 -0
- package/dist/core/handlers/FileProcessingHandler.js.map +1 -1
- package/dist/core/reviewOrchestrator.js +61 -1
- package/dist/core/reviewOrchestrator.js.map +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -2
- package/dist/index.js.map +1 -1
- package/dist/runtime/cliEntry.js +57 -4
- package/dist/runtime/cliEntry.js.map +1 -1
- package/dist/runtime/fileCollector.d.ts +10 -1
- package/dist/runtime/fileCollector.js +217 -2
- package/dist/runtime/fileCollector.js.map +1 -1
- package/dist/runtime/reporting/markdownReportBuilder.d.ts +2 -0
- package/dist/runtime/reporting/markdownReportBuilder.js +57 -0
- package/dist/runtime/reporting/markdownReportBuilder.js.map +1 -1
- package/dist/runtime/reviewPipeline.d.ts +22 -3
- package/dist/runtime/reviewPipeline.js +46 -7
- package/dist/runtime/reviewPipeline.js.map +1 -1
- package/dist/runtime/runAiCodeReview.d.ts +19 -1
- package/dist/runtime/runAiCodeReview.js +243 -8
- package/dist/runtime/runAiCodeReview.js.map +1 -1
- package/dist/runtime/ui/RuntimeApp.js +15 -4
- package/dist/runtime/ui/RuntimeApp.js.map +1 -1
- package/dist/runtime/ui/screens/ProgressScreen.d.ts +6 -1
- package/dist/runtime/ui/screens/ProgressScreen.js +28 -2
- package/dist/runtime/ui/screens/ProgressScreen.js.map +1 -1
- package/dist/runtime/ui/screens/ResultsScreen.js +8 -1
- package/dist/runtime/ui/screens/ResultsScreen.js.map +1 -1
- package/dist/types/review.d.ts +60 -0
- package/dist/utils/detection/frameworkDetector.js +55 -0
- package/dist/utils/detection/frameworkDetector.js.map +1 -1
- package/dist/utils/promptTemplateManager.js +1 -0
- package/dist/utils/promptTemplateManager.js.map +1 -1
- package/package.json +13 -10
- package/.cr-aia.yml +0 -23
- package/.crignore +0 -0
- package/src/analysis/FindingsExtractor.ts +0 -431
- package/src/analysis/ai-detection/analyzers/BaseAnalyzer.ts +0 -267
- package/src/analysis/ai-detection/analyzers/DocumentationAnalyzer.ts +0 -622
- package/src/analysis/ai-detection/analyzers/GitHistoryAnalyzer.ts +0 -430
- package/src/analysis/ai-detection/core/AIDetectionEngine.ts +0 -467
- package/src/analysis/ai-detection/types/DetectionTypes.ts +0 -406
- package/src/analysis/ai-detection/utils/SubmissionConverter.ts +0 -390
- package/src/analysis/context/ReviewContext.ts +0 -378
- package/src/analysis/context/index.ts +0 -7
- package/src/analysis/index.ts +0 -8
- package/src/analysis/tokens/TokenAnalysisFormatter.ts +0 -154
- package/src/analysis/tokens/TokenAnalyzer.ts +0 -747
- package/src/analysis/tokens/index.ts +0 -8
- package/src/clients/base/abstractClient.ts +0 -190
- package/src/clients/base/httpClient.ts +0 -160
- package/src/clients/base/index.ts +0 -12
- package/src/clients/base/modelDetection.ts +0 -107
- package/src/clients/base/responseProcessor.ts +0 -586
- package/src/clients/factory/clientFactory.ts +0 -55
- package/src/clients/factory/index.ts +0 -8
- package/src/clients/implementations/index.ts +0 -8
- package/src/clients/implementations/openRouterClient.ts +0 -411
- package/src/clients/openRouterClient.ts +0 -863
- package/src/clients/openRouterClientWrapper.ts +0 -44
- package/src/clients/utils/directoryStructure.ts +0 -52
- package/src/clients/utils/index.ts +0 -11
- package/src/clients/utils/languageDetection.ts +0 -44
- package/src/clients/utils/promptFormatter.ts +0 -105
- package/src/clients/utils/promptLoader.ts +0 -53
- package/src/clients/utils/tokenCounter.ts +0 -297
- package/src/core/ApiClientSelector.ts +0 -37
- package/src/core/ConfigurationService.ts +0 -591
- package/src/core/ConsolidationService.ts +0 -423
- package/src/core/InteractiveDisplayManager.ts +0 -81
- package/src/core/OutputManager.ts +0 -275
- package/src/core/ReviewGenerator.ts +0 -140
- package/src/core/fileDiscovery.ts +0 -237
- package/src/core/handlers/EstimationHandler.ts +0 -104
- package/src/core/handlers/FileProcessingHandler.ts +0 -204
- package/src/core/handlers/OutputHandler.ts +0 -125
- package/src/core/handlers/ReviewExecutor.ts +0 -104
- package/src/core/reviewOrchestrator.ts +0 -333
- package/src/core/utils/ModelInfoUtils.ts +0 -56
- package/src/formatters/outputFormatter.ts +0 -62
- package/src/formatters/utils/IssueFormatters.ts +0 -83
- package/src/formatters/utils/JsonFormatter.ts +0 -77
- package/src/formatters/utils/MarkdownFormatters.ts +0 -609
- package/src/formatters/utils/MetadataFormatter.ts +0 -269
- package/src/formatters/utils/ModelInfoExtractor.ts +0 -115
- package/src/index.ts +0 -28
- package/src/plugins/PluginInterface.ts +0 -50
- package/src/plugins/PluginManager.ts +0 -126
- package/src/prompts/PromptManager.ts +0 -69
- package/src/prompts/cache/PromptCache.ts +0 -50
- package/src/prompts/promptText/common/variables/css-frameworks.json +0 -33
- package/src/prompts/promptText/common/variables/framework-versions.json +0 -45
- package/src/prompts/promptText/frameworks/react/comprehensive.hbs +0 -19
- package/src/prompts/promptText/languages/css/comprehensive.hbs +0 -18
- package/src/prompts/promptText/languages/generic/comprehensive.hbs +0 -20
- package/src/prompts/promptText/languages/html/comprehensive.hbs +0 -18
- package/src/prompts/promptText/languages/javascript/comprehensive.hbs +0 -18
- package/src/prompts/promptText/languages/python/comprehensive.hbs +0 -18
- package/src/prompts/promptText/languages/typescript/comprehensive.hbs +0 -18
- package/src/runtime/auth/service.ts +0 -58
- package/src/runtime/auth/session.ts +0 -103
- package/src/runtime/auth/types.ts +0 -11
- package/src/runtime/cliEntry.ts +0 -196
- package/src/runtime/debug/logManager.ts +0 -37
- package/src/runtime/errors.ts +0 -13
- package/src/runtime/fileCollector.ts +0 -222
- package/src/runtime/manifest.ts +0 -64
- package/src/runtime/openrouterProxy.ts +0 -45
- package/src/runtime/preprod/webCheck.ts +0 -104
- package/src/runtime/proxyConfig.ts +0 -94
- package/src/runtime/proxyEnvironment.ts +0 -71
- package/src/runtime/reportMerge.ts +0 -102
- package/src/runtime/reporting/markdownReportBuilder.ts +0 -138
- package/src/runtime/reporting/reportDataCollector.ts +0 -234
- package/src/runtime/reporting/summaryGenerator.ts +0 -86
- package/src/runtime/reviewPipeline.ts +0 -161
- package/src/runtime/runAiCodeReview.ts +0 -153
- package/src/runtime/runtimeConfig.ts +0 -5
- package/src/runtime/ui/Layout.tsx +0 -57
- package/src/runtime/ui/RuntimeApp.tsx +0 -233
- package/src/runtime/ui/inkModules.ts +0 -73
- package/src/runtime/ui/screens/AuthScreen.tsx +0 -128
- package/src/runtime/ui/screens/ModeSelection.tsx +0 -185
- package/src/runtime/ui/screens/ProgressScreen.tsx +0 -62
- package/src/runtime/ui/screens/ResultsScreen.tsx +0 -83
- package/src/strategies/ArchitecturalReviewStrategy.ts +0 -54
- package/src/strategies/CodingTestReviewStrategy.ts +0 -920
- package/src/strategies/ConsolidatedReviewStrategy.ts +0 -59
- package/src/strategies/ExtractPatternsReviewStrategy.ts +0 -64
- package/src/strategies/MultiPassReviewStrategy.ts +0 -785
- package/src/strategies/ReviewStrategy.ts +0 -64
- package/src/strategies/StrategyFactory.ts +0 -79
- package/src/strategies/index.ts +0 -14
- package/src/tokenizers/baseTokenizer.ts +0 -61
- package/src/tokenizers/gptTokenizer.ts +0 -27
- package/src/tokenizers/index.ts +0 -8
- package/src/types/apiResponses.ts +0 -40
- package/src/types/cli.ts +0 -24
- package/src/types/common.ts +0 -39
- package/src/types/configuration.ts +0 -201
- package/src/types/handlebars.d.ts +0 -5
- package/src/types/patch.d.ts +0 -25
- package/src/types/review.ts +0 -294
- package/src/types/reviewContext.d.ts +0 -65
- package/src/types/reviewSchema.ts +0 -181
- package/src/types/structuredReview.ts +0 -167
- package/src/types/tokenAnalysis.ts +0 -56
- package/src/utils/FileReader.ts +0 -93
- package/src/utils/FileWriter.ts +0 -76
- package/src/utils/PathGenerator.ts +0 -97
- package/src/utils/api/apiUtils.ts +0 -14
- package/src/utils/api/index.ts +0 -1
- package/src/utils/apiErrorHandler.ts +0 -287
- package/src/utils/ciDataCollector.ts +0 -252
- package/src/utils/codingTestConfigLoader.ts +0 -466
- package/src/utils/dependencies/aiDependencyAnalyzer.ts +0 -454
- package/src/utils/detection/frameworkDetector.ts +0 -879
- package/src/utils/detection/index.ts +0 -10
- package/src/utils/detection/projectTypeDetector.ts +0 -518
- package/src/utils/diagramGenerator.ts +0 -206
- package/src/utils/errorLogger.ts +0 -60
- package/src/utils/estimationUtils.ts +0 -407
- package/src/utils/fileFilters.ts +0 -373
- package/src/utils/fileSystem.ts +0 -57
- package/src/utils/index.ts +0 -36
- package/src/utils/logger.ts +0 -290
- package/src/utils/pathValidator.ts +0 -98
- package/src/utils/priorityFilter.ts +0 -59
- package/src/utils/projectDocs.ts +0 -189
- package/src/utils/promptPaths.ts +0 -29
- package/src/utils/promptTemplateManager.ts +0 -157
- package/src/utils/review/consolidateReview.ts +0 -553
- package/src/utils/review/fixDisplay.ts +0 -100
- package/src/utils/review/fixImplementation.ts +0 -61
- package/src/utils/review/index.ts +0 -36
- package/src/utils/review/interactiveProcessing.ts +0 -294
- package/src/utils/review/progressTracker.ts +0 -296
- package/src/utils/review/reviewExtraction.ts +0 -382
- package/src/utils/review/types.ts +0 -46
- package/src/utils/reviewActionHandler.ts +0 -18
- package/src/utils/reviewParser.ts +0 -253
- package/src/utils/sanitizer.ts +0 -238
- package/src/utils/smartFileSelector.ts +0 -255
- package/src/utils/templateLoader.ts +0 -514
- package/src/utils/treeGenerator.ts +0 -153
- package/tsconfig.build.json +0 -14
- package/tsconfig.json +0 -59
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
-
import { basename, isAbsolute, join, resolve } from 'node:path';
|
|
3
|
-
import { tmpdir } from 'node:os';
|
|
4
|
-
import { execa } from 'execa';
|
|
5
|
-
import { collectFiles, type FileCollectionMode } from './fileCollector';
|
|
6
|
-
import { loadManifest } from './manifest';
|
|
7
|
-
import { runAiCodeReview } from './runAiCodeReview';
|
|
8
|
-
import { ensureProxyEnvironmentInitialized } from './proxyEnvironment';
|
|
9
|
-
import { mergeReports } from './reportMerge';
|
|
10
|
-
import { collectReportData } from './reporting/reportDataCollector';
|
|
11
|
-
import { buildMarkdownReport, injectSummary } from './reporting/markdownReportBuilder';
|
|
12
|
-
import { generateReportSummary } from './reporting/summaryGenerator';
|
|
13
|
-
import logger from '../utils/logger';
|
|
14
|
-
|
|
15
|
-
export type ReviewStage = 'preparing' | 'collecting' | 'reviewing' | 'merging';
|
|
16
|
-
export type ReviewMode = FileCollectionMode;
|
|
17
|
-
|
|
18
|
-
export interface ReviewOptions {
|
|
19
|
-
model: string;
|
|
20
|
-
outDir: string;
|
|
21
|
-
debug?: boolean;
|
|
22
|
-
mode?: ReviewMode;
|
|
23
|
-
preprodTargetUrl?: string;
|
|
24
|
-
onStage?: (stage: ReviewStage, info?: { filesFound?: number }) => void;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export interface ReviewTotals {
|
|
28
|
-
critical: number;
|
|
29
|
-
high: number;
|
|
30
|
-
medium: number;
|
|
31
|
-
low: number;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export interface ReviewResult {
|
|
35
|
-
totals: ReviewTotals;
|
|
36
|
-
findings: any[];
|
|
37
|
-
duration: number;
|
|
38
|
-
repo: string;
|
|
39
|
-
filesReviewed: number;
|
|
40
|
-
reportPath?: string;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export async function executeReview(options: ReviewOptions): Promise<ReviewResult> {
|
|
44
|
-
const { model, outDir, debug = false, onStage, mode = 'uncommitted' } = options;
|
|
45
|
-
|
|
46
|
-
onStage?.('collecting');
|
|
47
|
-
logger.info('Collecting files for review', { mode, workspace: process.cwd() });
|
|
48
|
-
const files = await collectFiles({ mode });
|
|
49
|
-
const fileCount = files.length;
|
|
50
|
-
|
|
51
|
-
if (fileCount === 0) {
|
|
52
|
-
throw new Error('No files to review');
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
onStage?.('collecting', { filesFound: fileCount });
|
|
56
|
-
logger.info('File collection complete', { fileCount });
|
|
57
|
-
|
|
58
|
-
const workspaceRoot = process.cwd();
|
|
59
|
-
const repoRoot = await resolveRepoRoot();
|
|
60
|
-
const manifest = loadManifest(workspaceRoot);
|
|
61
|
-
const tempDir = mkdtempSync(join(tmpdir(), 'cr-aia-'));
|
|
62
|
-
const rawOutDir = mkdtempSync(join(tmpdir(), 'cr-aia-raw-'));
|
|
63
|
-
const manifestPath = join(tempDir, 'ai-code-review.json');
|
|
64
|
-
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
65
|
-
|
|
66
|
-
ensureProxyEnvironmentInitialized(repoRoot);
|
|
67
|
-
|
|
68
|
-
const finalOutDir = resolveOutputDirectory(workspaceRoot, outDir);
|
|
69
|
-
mkdirSync(finalOutDir, { recursive: true });
|
|
70
|
-
|
|
71
|
-
const start = Date.now();
|
|
72
|
-
|
|
73
|
-
try {
|
|
74
|
-
onStage?.('reviewing', { filesFound: fileCount });
|
|
75
|
-
const reports = await runAiCodeReview(files, {
|
|
76
|
-
provider: 'openrouter',
|
|
77
|
-
type: 'comprehensive',
|
|
78
|
-
outDir: rawOutDir,
|
|
79
|
-
format: 'json',
|
|
80
|
-
model,
|
|
81
|
-
configPath: manifestPath,
|
|
82
|
-
debug,
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
onStage?.('merging');
|
|
86
|
-
const merged = mergeReports(reports || []);
|
|
87
|
-
const totals =
|
|
88
|
-
(merged?.totals as ReviewTotals) || ({ critical: 0, high: 0, medium: 0, low: 0 } as ReviewTotals);
|
|
89
|
-
const findings = (merged?.findings as any[]) || [];
|
|
90
|
-
|
|
91
|
-
const repo = basename(workspaceRoot) || 'workspace';
|
|
92
|
-
const duration = Math.round((Date.now() - start) / 1000);
|
|
93
|
-
const reportTimestamp = new Date();
|
|
94
|
-
|
|
95
|
-
const collected = collectReportData(reports || []);
|
|
96
|
-
const severityTotals = totals || collected.totals;
|
|
97
|
-
|
|
98
|
-
const markdownDraft = buildMarkdownReport({
|
|
99
|
-
repoName: repo,
|
|
100
|
-
generatedAt: reportTimestamp,
|
|
101
|
-
durationSeconds: duration,
|
|
102
|
-
filesReviewed: fileCount,
|
|
103
|
-
totals: severityTotals,
|
|
104
|
-
tokenStats: collected.tokenStats,
|
|
105
|
-
estimatedCostUSD: collected.estimatedCostUSD,
|
|
106
|
-
issueGroups: collected.groupedIssues,
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
const summary = await generateReportSummary(markdownDraft, {
|
|
110
|
-
model,
|
|
111
|
-
totals: severityTotals,
|
|
112
|
-
durationSeconds: duration,
|
|
113
|
-
estimatedCostUSD: collected.estimatedCostUSD,
|
|
114
|
-
issueCount: collected.issueCount,
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
const finalMarkdown = injectSummary(markdownDraft, summary);
|
|
118
|
-
const reportFileName = formatReportFileName(repo, reportTimestamp);
|
|
119
|
-
const reportPath = join(finalOutDir, reportFileName);
|
|
120
|
-
writeFileSync(reportPath, finalMarkdown, 'utf-8');
|
|
121
|
-
|
|
122
|
-
return {
|
|
123
|
-
totals: severityTotals,
|
|
124
|
-
findings,
|
|
125
|
-
duration,
|
|
126
|
-
repo,
|
|
127
|
-
filesReviewed: fileCount,
|
|
128
|
-
reportPath,
|
|
129
|
-
};
|
|
130
|
-
} finally {
|
|
131
|
-
rmSync(tempDir, { recursive: true, force: true });
|
|
132
|
-
rmSync(rawOutDir, { recursive: true, force: true });
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
async function resolveRepoRoot(): Promise<string> {
|
|
137
|
-
try {
|
|
138
|
-
const { stdout } = await execa('git', ['rev-parse', '--show-toplevel']);
|
|
139
|
-
return stdout.trim();
|
|
140
|
-
} catch {
|
|
141
|
-
return process.cwd();
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function resolveOutputDirectory(root: string, outputDir: string): string {
|
|
146
|
-
if (isAbsolute(outputDir)) {
|
|
147
|
-
return outputDir;
|
|
148
|
-
}
|
|
149
|
-
return resolve(root, outputDir);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function formatReportFileName(repo: string, date: Date): string {
|
|
153
|
-
const sanitizedRepo = repo.replace(/[^a-zA-Z0-9_-]+/g, '-').replace(/-+/g, '-');
|
|
154
|
-
const pad = (value: number) => value.toString().padStart(2, '0');
|
|
155
|
-
const day = pad(date.getDate());
|
|
156
|
-
const month = pad(date.getMonth() + 1);
|
|
157
|
-
const year = date.getFullYear();
|
|
158
|
-
const hours = pad(date.getHours());
|
|
159
|
-
const minutes = pad(date.getMinutes());
|
|
160
|
-
return `cr-${sanitizedRepo}-${day}-${month}-${year}-${hours}-${minutes}.md`;
|
|
161
|
-
}
|
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
2
|
-
import { join, relative, resolve } from 'node:path';
|
|
3
|
-
import { execa } from 'execa';
|
|
4
|
-
import type { OutputFormat } from '../types/common';
|
|
5
|
-
import type { ReviewOptions, ReviewType } from '../types/review';
|
|
6
|
-
import { orchestrateReview } from '../core/reviewOrchestrator';
|
|
7
|
-
import { RUNTIME_CONFIG } from './runtimeConfig';
|
|
8
|
-
|
|
9
|
-
export interface AiReviewOptions {
|
|
10
|
-
provider: 'openrouter';
|
|
11
|
-
type: string;
|
|
12
|
-
outDir: string;
|
|
13
|
-
format: 'json' | 'md';
|
|
14
|
-
model?: string;
|
|
15
|
-
configPath?: string;
|
|
16
|
-
debug?: boolean;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export async function runAiCodeReview(files: string[], opts: AiReviewOptions): Promise<any[]> {
|
|
20
|
-
if (files.length === 0) return [];
|
|
21
|
-
|
|
22
|
-
const workspaceRoot = process.cwd();
|
|
23
|
-
let repoRoot: string;
|
|
24
|
-
try {
|
|
25
|
-
const { stdout } = await execa('git', ['rev-parse', '--show-toplevel']);
|
|
26
|
-
repoRoot = stdout.trim();
|
|
27
|
-
} catch {
|
|
28
|
-
repoRoot = process.cwd();
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const absOutDir = resolve(workspaceRoot, opts.outDir);
|
|
32
|
-
|
|
33
|
-
if (!existsSync(absOutDir)) {
|
|
34
|
-
mkdirSync(absOutDir, { recursive: true });
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const outputFileMeta = new Map<string, { mtimeMs: number; size: number }>();
|
|
38
|
-
if (existsSync(absOutDir)) {
|
|
39
|
-
for (const file of readdirSync(absOutDir)) {
|
|
40
|
-
if (!file.endsWith('.json')) continue;
|
|
41
|
-
try {
|
|
42
|
-
const stats = statSync(join(absOutDir, file));
|
|
43
|
-
outputFileMeta.set(file, { mtimeMs: stats.mtimeMs, size: stats.size });
|
|
44
|
-
} catch {
|
|
45
|
-
// ignore stale files
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function getAllFilesInDir(dirPath: string, repo: string): string[] {
|
|
51
|
-
const entries = readdirSync(dirPath, { withFileTypes: true });
|
|
52
|
-
const nested: string[] = [];
|
|
53
|
-
for (const entry of entries) {
|
|
54
|
-
const fullPath = join(dirPath, entry.name);
|
|
55
|
-
const relPath = relative(repo, fullPath);
|
|
56
|
-
if (relPath.includes('.git/') || relPath.includes('node_modules/')) {
|
|
57
|
-
continue;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
if (entry.isDirectory()) {
|
|
61
|
-
const subFiles = getAllFilesInDir(fullPath, repo);
|
|
62
|
-
nested.push(...subFiles);
|
|
63
|
-
} else if (entry.isFile()) {
|
|
64
|
-
nested.push(fullPath);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
return nested;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const expandedFiles: string[] = [];
|
|
71
|
-
for (const file of files) {
|
|
72
|
-
const absPath = resolve(file);
|
|
73
|
-
try {
|
|
74
|
-
const stats = statSync(absPath);
|
|
75
|
-
if (stats.isDirectory()) {
|
|
76
|
-
const dirFiles = getAllFilesInDir(absPath, repoRoot);
|
|
77
|
-
expandedFiles.push(...dirFiles);
|
|
78
|
-
} else if (stats.isFile()) {
|
|
79
|
-
expandedFiles.push(absPath);
|
|
80
|
-
}
|
|
81
|
-
} catch {
|
|
82
|
-
continue;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const uniqueExpandedFiles = Array.from(new Set(expandedFiles));
|
|
87
|
-
const relativeFilesSet = new Set<string>();
|
|
88
|
-
for (const absPath of uniqueExpandedFiles) {
|
|
89
|
-
try {
|
|
90
|
-
const stats = statSync(absPath);
|
|
91
|
-
if (!stats.isFile()) continue;
|
|
92
|
-
const repoRelative = relative(repoRoot, absPath);
|
|
93
|
-
if (!repoRelative || repoRelative.startsWith('..')) continue;
|
|
94
|
-
|
|
95
|
-
const workspaceRelative = relative(workspaceRoot, absPath).replace(/\\/g, '/');
|
|
96
|
-
if (!workspaceRelative || workspaceRelative.startsWith('..')) continue;
|
|
97
|
-
|
|
98
|
-
relativeFilesSet.add(workspaceRelative);
|
|
99
|
-
} catch {
|
|
100
|
-
continue;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const outputs: any[] = [];
|
|
105
|
-
|
|
106
|
-
const collectOutputs = () => {
|
|
107
|
-
if (!existsSync(absOutDir)) {
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
|
-
const jsonFiles = readdirSync(absOutDir).filter((f) => f.endsWith('.json'));
|
|
111
|
-
for (const file of jsonFiles) {
|
|
112
|
-
const filePath = join(absOutDir, file);
|
|
113
|
-
try {
|
|
114
|
-
const stats = statSync(filePath);
|
|
115
|
-
const previous = outputFileMeta.get(file);
|
|
116
|
-
if (previous && stats.mtimeMs <= previous.mtimeMs && stats.size === previous.size) {
|
|
117
|
-
continue;
|
|
118
|
-
}
|
|
119
|
-
const content = JSON.parse(readFileSync(filePath, 'utf-8'));
|
|
120
|
-
outputs.push(content);
|
|
121
|
-
outputFileMeta.set(file, { mtimeMs: stats.mtimeMs, size: stats.size });
|
|
122
|
-
} catch {
|
|
123
|
-
continue;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
};
|
|
127
|
-
|
|
128
|
-
if (relativeFilesSet.size === 0) {
|
|
129
|
-
throw new Error('No files to review after filtering');
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const targets = Array.from(relativeFilesSet);
|
|
133
|
-
const mappedFormat: OutputFormat = opts.format === 'md' ? 'markdown' : 'json';
|
|
134
|
-
|
|
135
|
-
const baseOptions: (ReviewOptions & { config?: string }) = {
|
|
136
|
-
type: opts.type as ReviewType,
|
|
137
|
-
output: mappedFormat,
|
|
138
|
-
outputDir: absOutDir,
|
|
139
|
-
model: opts.model || RUNTIME_CONFIG.DEFAULT_MODEL,
|
|
140
|
-
debug: opts.debug ?? false,
|
|
141
|
-
};
|
|
142
|
-
|
|
143
|
-
if (opts.configPath) {
|
|
144
|
-
baseOptions.config = opts.configPath;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
for (const target of targets) {
|
|
148
|
-
await orchestrateReview(target, baseOptions);
|
|
149
|
-
collectOutputs();
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
return outputs;
|
|
153
|
-
}
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import React, { createContext, useContext, useEffect, useMemo, useState } from 'react';
|
|
2
|
-
import { getInk } from './inkModules';
|
|
3
|
-
|
|
4
|
-
interface LayoutValue {
|
|
5
|
-
columns: number;
|
|
6
|
-
rows: number;
|
|
7
|
-
frameWidth: number;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
const DEFAULT_COLUMNS = 80;
|
|
11
|
-
const DEFAULT_ROWS = 24;
|
|
12
|
-
|
|
13
|
-
const LayoutContext = createContext<LayoutValue>({
|
|
14
|
-
columns: DEFAULT_COLUMNS,
|
|
15
|
-
rows: DEFAULT_ROWS,
|
|
16
|
-
frameWidth: DEFAULT_COLUMNS - 4,
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
export function LayoutProvider({ children }: { children: React.ReactNode }) {
|
|
20
|
-
const { useStdout } = getInk();
|
|
21
|
-
const { stdout } = useStdout();
|
|
22
|
-
const [dimensions, setDimensions] = useState<LayoutValue>(() => ({
|
|
23
|
-
columns: stdout?.columns ?? DEFAULT_COLUMNS,
|
|
24
|
-
rows: stdout?.rows ?? DEFAULT_ROWS,
|
|
25
|
-
frameWidth: Math.max(60, Math.min((stdout?.columns ?? DEFAULT_COLUMNS) - 4, 110)),
|
|
26
|
-
}));
|
|
27
|
-
|
|
28
|
-
useEffect(() => {
|
|
29
|
-
if (!stdout) {
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const handleResize = () => {
|
|
34
|
-
const cols = stdout.columns ?? DEFAULT_COLUMNS;
|
|
35
|
-
const rows = stdout.rows ?? DEFAULT_ROWS;
|
|
36
|
-
setDimensions({
|
|
37
|
-
columns: cols,
|
|
38
|
-
rows,
|
|
39
|
-
frameWidth: Math.max(60, Math.min(cols - 4, 110)),
|
|
40
|
-
});
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
stdout.on('resize', handleResize);
|
|
44
|
-
return () => {
|
|
45
|
-
stdout.off('resize', handleResize);
|
|
46
|
-
};
|
|
47
|
-
}, [stdout]);
|
|
48
|
-
|
|
49
|
-
const value = useMemo<LayoutValue>(() => dimensions, [dimensions]);
|
|
50
|
-
|
|
51
|
-
return <LayoutContext.Provider value={value}>{children}</LayoutContext.Provider>;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function useLayout(): LayoutValue {
|
|
55
|
-
return useContext(LayoutContext);
|
|
56
|
-
}
|
|
57
|
-
|
|
@@ -1,233 +0,0 @@
|
|
|
1
|
-
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
2
|
-
import { LayoutProvider, useLayout } from './Layout';
|
|
3
|
-
import { AuthScreen } from './screens/AuthScreen';
|
|
4
|
-
import { ModeSelection } from './screens/ModeSelection';
|
|
5
|
-
import { ProgressScreen } from './screens/ProgressScreen';
|
|
6
|
-
import { ResultsScreen } from './screens/ResultsScreen';
|
|
7
|
-
import { getInk, getInkSpinner } from './inkModules';
|
|
8
|
-
import type { ReviewMode, ReviewResult, ReviewStage } from '../reviewPipeline';
|
|
9
|
-
import { executeReview } from '../reviewPipeline';
|
|
10
|
-
import { loadSession, getSessionToken } from '../auth/session';
|
|
11
|
-
import type { SessionUser } from '../auth/types';
|
|
12
|
-
import { RUNTIME_CONFIG } from '../runtimeConfig';
|
|
13
|
-
import { MissingCrIgnoreError } from '../errors';
|
|
14
|
-
import { triggerManualWebCheck } from '../preprod/webCheck';
|
|
15
|
-
import { startDebugLogSession, type DebugLogSession } from '../debug/logManager';
|
|
16
|
-
import logger from '../../utils/logger';
|
|
17
|
-
|
|
18
|
-
type ScreenState = 'auth' | 'mode' | 'progress' | 'results';
|
|
19
|
-
|
|
20
|
-
interface RuntimeAppProps {
|
|
21
|
-
debug?: boolean;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
interface ProgressState {
|
|
25
|
-
stage: ReviewStage;
|
|
26
|
-
filesFound?: number;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const defaultProgress: ProgressState = { stage: 'collecting' };
|
|
30
|
-
const GENERIC_ERROR_MESSAGE =
|
|
31
|
-
'Something went wrong. Press D to enable debug mode and rerun to capture logs.';
|
|
32
|
-
const PREPROD_ERROR_MESSAGE =
|
|
33
|
-
'Could not start the website check. Double-check the URL or try again later.';
|
|
34
|
-
|
|
35
|
-
function AppBody({ debug = false }: RuntimeAppProps) {
|
|
36
|
-
const { Box, Text, useApp, useInput } = getInk();
|
|
37
|
-
const { exit } = useApp();
|
|
38
|
-
const layout = useLayout();
|
|
39
|
-
const [screen, setScreen] = useState<ScreenState>('auth');
|
|
40
|
-
const [user, setUser] = useState<SessionUser | null>(null);
|
|
41
|
-
const [progress, setProgress] = useState<ProgressState>(defaultProgress);
|
|
42
|
-
const [results, setResults] = useState<ReviewResult | null>(null);
|
|
43
|
-
const [debugEnabled, setDebugEnabled] = useState(false);
|
|
44
|
-
const [lastDebugLogPath, setLastDebugLogPath] = useState<string | null>(null);
|
|
45
|
-
type ErrorInfo = { type: 'generic' | 'crignore'; message: string; hint?: string };
|
|
46
|
-
const [error, setError] = useState<ErrorInfo | null>(null);
|
|
47
|
-
const combinedDebugFlag = useMemo(() => debug || debugEnabled, [debug, debugEnabled]);
|
|
48
|
-
|
|
49
|
-
useEffect(() => {
|
|
50
|
-
const sessionUser = loadSession();
|
|
51
|
-
const token = getSessionToken();
|
|
52
|
-
if (token) {
|
|
53
|
-
process.env.CR_AIA_PROXY_SESSION_TOKEN = token;
|
|
54
|
-
}
|
|
55
|
-
if (sessionUser) {
|
|
56
|
-
setUser(sessionUser);
|
|
57
|
-
setScreen('mode');
|
|
58
|
-
}
|
|
59
|
-
}, []);
|
|
60
|
-
|
|
61
|
-
useInput(
|
|
62
|
-
useCallback(
|
|
63
|
-
(input, key) => {
|
|
64
|
-
if (key.ctrl && input === 'c') {
|
|
65
|
-
exit();
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
if (screen === 'results' && key.return) {
|
|
69
|
-
setScreen('mode');
|
|
70
|
-
}
|
|
71
|
-
},
|
|
72
|
-
[exit, screen],
|
|
73
|
-
),
|
|
74
|
-
);
|
|
75
|
-
|
|
76
|
-
const handleAuth = useCallback((nextUser: SessionUser) => {
|
|
77
|
-
setUser(nextUser);
|
|
78
|
-
setScreen('mode');
|
|
79
|
-
}, []);
|
|
80
|
-
|
|
81
|
-
const handleToggleDebug = useCallback(() => {
|
|
82
|
-
setDebugEnabled((prev) => {
|
|
83
|
-
if (prev) {
|
|
84
|
-
setLastDebugLogPath(null);
|
|
85
|
-
}
|
|
86
|
-
return !prev;
|
|
87
|
-
});
|
|
88
|
-
}, []);
|
|
89
|
-
|
|
90
|
-
const handleRunReview = useCallback(async (mode: ReviewMode, extras?: { targetUrl?: string }) => {
|
|
91
|
-
const trimmedUrl = extras?.targetUrl?.trim();
|
|
92
|
-
|
|
93
|
-
if (mode === 'preprod' && !trimmedUrl) {
|
|
94
|
-
setError({ type: 'generic', message: 'Pre-production mode requires a website URL.' });
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
setScreen('progress');
|
|
99
|
-
setResults(null);
|
|
100
|
-
setError(null);
|
|
101
|
-
setProgress(mode === 'preprod' ? { stage: 'preparing' } : defaultProgress);
|
|
102
|
-
setLastDebugLogPath(null);
|
|
103
|
-
|
|
104
|
-
let debugSession: DebugLogSession | null = null;
|
|
105
|
-
const finalizeDebugLogging = () => {
|
|
106
|
-
if (debugSession) {
|
|
107
|
-
debugSession.stop();
|
|
108
|
-
setLastDebugLogPath(debugSession.filePath);
|
|
109
|
-
} else {
|
|
110
|
-
setLastDebugLogPath(null);
|
|
111
|
-
}
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
if (debugEnabled) {
|
|
115
|
-
try {
|
|
116
|
-
debugSession = startDebugLogSession(process.cwd());
|
|
117
|
-
} catch (logInitError) {
|
|
118
|
-
const message =
|
|
119
|
-
logInitError instanceof Error
|
|
120
|
-
? logInitError.message
|
|
121
|
-
: 'Unable to initialize debug logging.';
|
|
122
|
-
setError({ type: 'generic', message: `Debug logging disabled: ${message}` });
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
if (mode === 'preprod' && trimmedUrl) {
|
|
127
|
-
try {
|
|
128
|
-
logger.info('Triggering manual website check', { url: trimmedUrl });
|
|
129
|
-
await triggerManualWebCheck(trimmedUrl);
|
|
130
|
-
} catch (webError) {
|
|
131
|
-
logger.error('Pre-production web check failed', webError);
|
|
132
|
-
setError({ type: 'generic', message: PREPROD_ERROR_MESSAGE });
|
|
133
|
-
setScreen('mode');
|
|
134
|
-
finalizeDebugLogging();
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
try {
|
|
140
|
-
logger.info('Starting code review run', { mode, workspace: process.cwd(), preprodTarget: trimmedUrl });
|
|
141
|
-
setProgress({ stage: 'collecting' });
|
|
142
|
-
const reviewResult = await executeReview({
|
|
143
|
-
model: process.env.AI_CODE_REVIEW_MODEL || RUNTIME_CONFIG.DEFAULT_MODEL,
|
|
144
|
-
outDir: './reports',
|
|
145
|
-
debug: combinedDebugFlag,
|
|
146
|
-
mode,
|
|
147
|
-
preprodTargetUrl: trimmedUrl,
|
|
148
|
-
onStage: (stage, info) => setProgress({ stage, filesFound: info?.filesFound }),
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
logger.info('Code review run complete', {
|
|
152
|
-
mode,
|
|
153
|
-
durationSeconds: reviewResult.duration,
|
|
154
|
-
filesReviewed: reviewResult.filesReviewed,
|
|
155
|
-
});
|
|
156
|
-
setResults(reviewResult);
|
|
157
|
-
setScreen('results');
|
|
158
|
-
} catch (reviewError) {
|
|
159
|
-
logger.error('Code review run failed', reviewError);
|
|
160
|
-
if (reviewError instanceof MissingCrIgnoreError) {
|
|
161
|
-
setError({
|
|
162
|
-
type: 'crignore',
|
|
163
|
-
message: 'cr-aia needs a .crignore file in this repo.',
|
|
164
|
-
hint: `Create ${reviewError.crIgnorePath} (can be empty, gitignore syntax) and rerun.`,
|
|
165
|
-
});
|
|
166
|
-
} else {
|
|
167
|
-
setError({ type: 'generic', message: GENERIC_ERROR_MESSAGE });
|
|
168
|
-
}
|
|
169
|
-
setScreen('mode');
|
|
170
|
-
} finally {
|
|
171
|
-
finalizeDebugLogging();
|
|
172
|
-
}
|
|
173
|
-
}, [combinedDebugFlag, debugEnabled]);
|
|
174
|
-
|
|
175
|
-
return (
|
|
176
|
-
<Box width={layout.frameWidth} flexDirection="column" gap={1}>
|
|
177
|
-
{screen === 'auth' && <AuthScreen onAuth={handleAuth} />}
|
|
178
|
-
{screen === 'mode' && user && (
|
|
179
|
-
<ModeSelection
|
|
180
|
-
onSelect={(mode, payload) => handleRunReview(mode, payload)}
|
|
181
|
-
debugEnabled={debugEnabled}
|
|
182
|
-
onToggleDebug={handleToggleDebug}
|
|
183
|
-
/>
|
|
184
|
-
)}
|
|
185
|
-
{screen === 'progress' && <ProgressScreen stage={progress.stage} filesFound={progress.filesFound} />}
|
|
186
|
-
{screen === 'results' && results && (
|
|
187
|
-
<ResultsScreen result={results} debugLogPath={lastDebugLogPath} />
|
|
188
|
-
)}
|
|
189
|
-
{error && (
|
|
190
|
-
<Box
|
|
191
|
-
borderStyle="round"
|
|
192
|
-
borderColor={error.type === 'crignore' ? 'yellow' : 'red'}
|
|
193
|
-
paddingX={1}
|
|
194
|
-
paddingY={1}
|
|
195
|
-
>
|
|
196
|
-
<Text color={error.type === 'crignore' ? 'yellow' : 'red'}>
|
|
197
|
-
{error.message}
|
|
198
|
-
{error.hint ? `\n${error.hint}` : ''}
|
|
199
|
-
</Text>
|
|
200
|
-
</Box>
|
|
201
|
-
)}
|
|
202
|
-
{lastDebugLogPath && screen !== 'results' && (
|
|
203
|
-
<Box>
|
|
204
|
-
<Text dimColor>Debug logs saved to: {lastDebugLogPath}</Text>
|
|
205
|
-
</Box>
|
|
206
|
-
)}
|
|
207
|
-
<Box justifyContent="space-between">
|
|
208
|
-
<Text dimColor>{user ? `Logged in as ${user.username}` : 'Authenticate to start reviewing'}</Text>
|
|
209
|
-
<Text dimColor>Ctrl+C to exit</Text>
|
|
210
|
-
</Box>
|
|
211
|
-
</Box>
|
|
212
|
-
);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
export function RuntimeApp(props: RuntimeAppProps) {
|
|
216
|
-
return (
|
|
217
|
-
<LayoutProvider>
|
|
218
|
-
<AppBody {...props} />
|
|
219
|
-
</LayoutProvider>
|
|
220
|
-
);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
export function LoadingScreen() {
|
|
224
|
-
const { Box, Text } = getInk();
|
|
225
|
-
const Spinner = getInkSpinner();
|
|
226
|
-
return (
|
|
227
|
-
<Box flexDirection="column" paddingX={1} paddingY={1}>
|
|
228
|
-
<Text>
|
|
229
|
-
<Spinner type="dots" /> Preparing runtime environment...
|
|
230
|
-
</Text>
|
|
231
|
-
</Box>
|
|
232
|
-
);
|
|
233
|
-
}
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
type InkModule = typeof import('ink');
|
|
2
|
-
type SpinnerModule = typeof import('ink-spinner');
|
|
3
|
-
type TextInputModule = typeof import('ink-text-input');
|
|
4
|
-
type SelectInputModule = typeof import('ink-select-input');
|
|
5
|
-
|
|
6
|
-
const dynamicImporter = new Function('specifier', 'return import(specifier);') as (
|
|
7
|
-
specifier: string,
|
|
8
|
-
) => Promise<unknown>;
|
|
9
|
-
|
|
10
|
-
let inkModule: InkModule | null = null;
|
|
11
|
-
let spinnerModule: SpinnerModule['default'] | null = null;
|
|
12
|
-
let textInputModule: TextInputModule['default'] | null = null;
|
|
13
|
-
let selectInputModule: SelectInputModule['default'] | null = null;
|
|
14
|
-
|
|
15
|
-
async function loadInk(): Promise<void> {
|
|
16
|
-
if (!inkModule) {
|
|
17
|
-
inkModule = (await dynamicImporter('ink')) as InkModule;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
async function loadSpinner(): Promise<void> {
|
|
22
|
-
if (!spinnerModule) {
|
|
23
|
-
const mod = (await dynamicImporter('ink-spinner')) as SpinnerModule;
|
|
24
|
-
spinnerModule = mod.default;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
async function loadTextInput(): Promise<void> {
|
|
29
|
-
if (!textInputModule) {
|
|
30
|
-
const mod = (await dynamicImporter('ink-text-input')) as TextInputModule;
|
|
31
|
-
textInputModule = mod.default;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
async function loadSelectInput(): Promise<void> {
|
|
36
|
-
if (!selectInputModule) {
|
|
37
|
-
const mod = (await dynamicImporter('ink-select-input')) as SelectInputModule;
|
|
38
|
-
selectInputModule = mod.default;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export async function preloadInkModules(): Promise<void> {
|
|
43
|
-
await Promise.all([loadInk(), loadSpinner(), loadTextInput(), loadSelectInput()]);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export function getInk(): InkModule {
|
|
47
|
-
if (!inkModule) {
|
|
48
|
-
throw new Error('Ink module not loaded. Call preloadInkModules() before rendering UI.');
|
|
49
|
-
}
|
|
50
|
-
return inkModule;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export function getInkSpinner(): SpinnerModule['default'] {
|
|
54
|
-
if (!spinnerModule) {
|
|
55
|
-
throw new Error('Ink spinner module not loaded. Call preloadInkModules() before rendering UI.');
|
|
56
|
-
}
|
|
57
|
-
return spinnerModule;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export function getInkTextInput(): TextInputModule['default'] {
|
|
61
|
-
if (!textInputModule) {
|
|
62
|
-
throw new Error('Ink TextInput module not loaded. Call preloadInkModules() before rendering UI.');
|
|
63
|
-
}
|
|
64
|
-
return textInputModule;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export function getInkSelectInput(): SelectInputModule['default'] {
|
|
68
|
-
if (!selectInputModule) {
|
|
69
|
-
throw new Error('Ink SelectInput module not loaded. Call preloadInkModules() before rendering UI.');
|
|
70
|
-
}
|
|
71
|
-
return selectInputModule;
|
|
72
|
-
}
|
|
73
|
-
|