@aiready/cli 0.9.43 ā 0.9.46
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/.turbo/turbo-build.log +11 -11
- package/.turbo/turbo-test.log +5 -5
- package/README.md +45 -7
- package/dist/chunk-6FOVC2OE.mjs +392 -0
- package/dist/chunk-MEXEG3IJ.mjs +389 -0
- package/dist/cli.js +631 -415
- package/dist/cli.mjs +451 -377
- package/dist/index.js +149 -2
- package/dist/index.mjs +5 -3
- package/package.json +12 -12
- package/src/.aiready/aiready-report-20260301-141543.json +8261 -0
- package/src/.aiready/aiready-report-20260301-141556.json +8261 -0
- package/src/.aiready/aiready-report-20260301-141611.json +8261 -0
- package/src/.aiready/aiready-report-20260304-125348.json +8324 -0
- package/src/cli.ts +18 -0
- package/src/commands/index.ts +1 -0
- package/src/commands/scan.ts +176 -211
- package/src/commands/upload.ts +125 -0
- package/src/index.ts +190 -3
package/src/cli.ts
CHANGED
|
@@ -16,6 +16,8 @@ import {
|
|
|
16
16
|
visualizeHelpText,
|
|
17
17
|
visualiseHelpText,
|
|
18
18
|
changeAmplificationAction,
|
|
19
|
+
uploadAction,
|
|
20
|
+
uploadHelpText,
|
|
19
21
|
} from './commands';
|
|
20
22
|
|
|
21
23
|
const getDirname = () => {
|
|
@@ -111,6 +113,9 @@ program
|
|
|
111
113
|
'Fail on issues: critical, major, any',
|
|
112
114
|
'critical'
|
|
113
115
|
)
|
|
116
|
+
.option('--api-key <key>', 'Platform API key for automatic upload')
|
|
117
|
+
.option('--upload', 'Automatically upload results to the platform')
|
|
118
|
+
.option('--server <url>', 'Custom platform URL')
|
|
114
119
|
.addHelpText('after', scanHelpText)
|
|
115
120
|
.action(async (directory, options) => {
|
|
116
121
|
await scanAction(directory, options);
|
|
@@ -274,4 +279,17 @@ program
|
|
|
274
279
|
await changeAmplificationAction(directory, options);
|
|
275
280
|
});
|
|
276
281
|
|
|
282
|
+
// Upload command - Upload report JSON to platform
|
|
283
|
+
program
|
|
284
|
+
.command('upload')
|
|
285
|
+
.description('Upload an AIReady report JSON to the platform')
|
|
286
|
+
.argument('<file>', 'Report JSON file to upload')
|
|
287
|
+
.option('--api-key <key>', 'Platform API key')
|
|
288
|
+
.option('--repo-id <id>', 'Platform repository ID (optional)')
|
|
289
|
+
.option('--server <url>', 'Custom platform URL')
|
|
290
|
+
.addHelpText('after', uploadHelpText)
|
|
291
|
+
.action(async (file, options) => {
|
|
292
|
+
await uploadAction(file, options);
|
|
293
|
+
});
|
|
294
|
+
|
|
277
295
|
program.parse();
|
package/src/commands/index.ts
CHANGED
|
@@ -15,3 +15,4 @@ export { aiSignalClarityAction } from './ai-signal-clarity';
|
|
|
15
15
|
export { agentGroundingAction } from './agent-grounding';
|
|
16
16
|
export { testabilityAction } from './testability';
|
|
17
17
|
export { changeAmplificationAction } from './change-amplification';
|
|
18
|
+
export { uploadAction, uploadHelpText } from './upload';
|
package/src/commands/scan.ts
CHANGED
|
@@ -15,17 +15,22 @@ import {
|
|
|
15
15
|
calculateOverallScore,
|
|
16
16
|
formatScore,
|
|
17
17
|
formatToolScore,
|
|
18
|
+
calculateTokenBudget,
|
|
19
|
+
estimateCostFromBudget,
|
|
20
|
+
getModelPreset,
|
|
18
21
|
getRating,
|
|
19
22
|
getRatingDisplay,
|
|
20
23
|
parseWeightString,
|
|
24
|
+
getRepoMetadata,
|
|
21
25
|
type ToolScoringOutput,
|
|
22
26
|
} from '@aiready/core';
|
|
23
|
-
import { analyzeUnified } from '../index';
|
|
27
|
+
import { analyzeUnified, scoreUnified, type ScoringResult } from '../index';
|
|
24
28
|
import {
|
|
25
29
|
getReportTimestamp,
|
|
26
30
|
warnIfGraphCapExceeded,
|
|
27
31
|
truncateArray,
|
|
28
32
|
} from '../utils/helpers';
|
|
33
|
+
import { uploadAction } from './upload';
|
|
29
34
|
|
|
30
35
|
interface ScanOptions {
|
|
31
36
|
tools?: string;
|
|
@@ -41,6 +46,10 @@ interface ScanOptions {
|
|
|
41
46
|
threshold?: string;
|
|
42
47
|
ci?: boolean;
|
|
43
48
|
failOn?: string;
|
|
49
|
+
model?: string;
|
|
50
|
+
apiKey?: string;
|
|
51
|
+
upload?: boolean;
|
|
52
|
+
server?: string;
|
|
44
53
|
}
|
|
45
54
|
|
|
46
55
|
export async function scanAction(directory: string, options: ScanOptions) {
|
|
@@ -50,6 +59,9 @@ export async function scanAction(directory: string, options: ScanOptions) {
|
|
|
50
59
|
// Resolve directory to absolute path to ensure .aiready/ is created in the right location
|
|
51
60
|
const resolvedDir = resolvePath(process.cwd(), directory || '.');
|
|
52
61
|
|
|
62
|
+
// Extract repo metadata for linkage
|
|
63
|
+
const repoMetadata = getRepoMetadata(resolvedDir);
|
|
64
|
+
|
|
53
65
|
try {
|
|
54
66
|
// Define defaults
|
|
55
67
|
const defaults = {
|
|
@@ -476,237 +488,162 @@ export async function scanAction(directory: string, options: ScanOptions) {
|
|
|
476
488
|
void elapsedTime;
|
|
477
489
|
|
|
478
490
|
// Calculate score if requested: assemble per-tool scoring outputs
|
|
479
|
-
let scoringResult:
|
|
491
|
+
let scoringResult: ScoringResult | undefined;
|
|
480
492
|
if (options.score || finalOptions.scoring?.showBreakdown) {
|
|
481
|
-
|
|
493
|
+
scoringResult = await scoreUnified(results, finalOptions);
|
|
482
494
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
const { calculatePatternScore } =
|
|
486
|
-
await import('@aiready/pattern-detect');
|
|
487
|
-
try {
|
|
488
|
-
const patternScore = calculatePatternScore(
|
|
489
|
-
results.duplicates,
|
|
490
|
-
results.patterns?.length || 0
|
|
491
|
-
);
|
|
492
|
-
toolScores.set('pattern-detect', patternScore);
|
|
493
|
-
} catch (err) {
|
|
494
|
-
void err;
|
|
495
|
-
}
|
|
496
|
-
}
|
|
495
|
+
console.log(chalk.bold('\nš AI Readiness Overall Score'));
|
|
496
|
+
console.log(` ${formatScore(scoringResult)}`);
|
|
497
497
|
|
|
498
|
-
//
|
|
499
|
-
|
|
500
|
-
const { generateSummary: genContextSummary, calculateContextScore } =
|
|
501
|
-
await import('@aiready/context-analyzer');
|
|
502
|
-
try {
|
|
503
|
-
const ctxSummary = genContextSummary(results.context);
|
|
504
|
-
const contextScore = calculateContextScore(ctxSummary);
|
|
505
|
-
toolScores.set('context-analyzer', contextScore);
|
|
506
|
-
} catch (err) {
|
|
507
|
-
void err;
|
|
508
|
-
}
|
|
509
|
-
}
|
|
498
|
+
// Parse CLI weight overrides (if any)
|
|
499
|
+
// Note: weights are already handled inside scoreUnified via finalOptions and calculateOverallScore
|
|
510
500
|
|
|
511
|
-
//
|
|
512
|
-
if (
|
|
513
|
-
const { calculateConsistencyScore } =
|
|
514
|
-
await import('@aiready/consistency');
|
|
501
|
+
// Check if we need to compare to a previous report
|
|
502
|
+
if (options.compareTo) {
|
|
515
503
|
try {
|
|
516
|
-
const
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
const consistencyScore = calculateConsistencyScore(
|
|
520
|
-
issues,
|
|
521
|
-
totalFiles
|
|
504
|
+
const prevReportStr = readFileSync(
|
|
505
|
+
resolvePath(process.cwd(), options.compareTo),
|
|
506
|
+
'utf8'
|
|
522
507
|
);
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
}
|
|
527
|
-
}
|
|
508
|
+
const prevReport = JSON.parse(prevReportStr);
|
|
509
|
+
const prevScore =
|
|
510
|
+
prevReport.scoring?.score || prevReport.scoring?.overallScore;
|
|
528
511
|
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
512
|
+
if (typeof prevScore === 'number') {
|
|
513
|
+
const diff = scoringResult.overall - prevScore;
|
|
514
|
+
const diffStr = diff > 0 ? `+${diff}` : String(diff);
|
|
515
|
+
console.log();
|
|
516
|
+
if (diff > 0) {
|
|
517
|
+
console.log(
|
|
518
|
+
chalk.green(
|
|
519
|
+
` š Trend: ${diffStr} compared to ${options.compareTo} (${prevScore} ā ${scoringResult.overall})`
|
|
520
|
+
)
|
|
521
|
+
);
|
|
522
|
+
} else if (diff < 0) {
|
|
523
|
+
console.log(
|
|
524
|
+
chalk.red(
|
|
525
|
+
` š Trend: ${diffStr} compared to ${options.compareTo} (${prevScore} ā ${scoringResult.overall})`
|
|
526
|
+
)
|
|
527
|
+
);
|
|
528
|
+
// Trend gating: if we regressed and CI is on or threshold is present, we could lower the threshold effectively,
|
|
529
|
+
// but for now, we just highlight the regression.
|
|
530
|
+
} else {
|
|
531
|
+
console.log(
|
|
532
|
+
chalk.blue(
|
|
533
|
+
` ā Trend: No change compared to ${options.compareTo} (${prevScore} ā ${scoringResult.overall})`
|
|
534
|
+
)
|
|
535
|
+
);
|
|
536
|
+
}
|
|
542
537
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
538
|
+
// Add trend info to scoringResult for programmatic use
|
|
539
|
+
(scoringResult as any).trend = {
|
|
540
|
+
previousScore: prevScore,
|
|
541
|
+
difference: diff,
|
|
542
|
+
};
|
|
543
|
+
} else {
|
|
544
|
+
console.log(
|
|
545
|
+
chalk.yellow(
|
|
546
|
+
`\n ā ļø Previous report at ${options.compareTo} does not contain an overall score.`
|
|
547
|
+
)
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
} catch (e) {
|
|
551
|
+
void e;
|
|
552
|
+
console.log(
|
|
553
|
+
chalk.yellow(
|
|
554
|
+
`\n ā ļø Could not read or parse previous report at ${options.compareTo}.`
|
|
555
|
+
)
|
|
556
|
+
);
|
|
552
557
|
}
|
|
553
558
|
}
|
|
554
559
|
|
|
555
|
-
//
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
560
|
+
// Unified Token Budget Analysis
|
|
561
|
+
const totalWastedDuplication = (scoringResult.breakdown || []).reduce(
|
|
562
|
+
(sum, s) =>
|
|
563
|
+
sum + (s.tokenBudget?.wastedTokens.bySource.duplication || 0),
|
|
564
|
+
0
|
|
565
|
+
);
|
|
566
|
+
const totalWastedFragmentation = (scoringResult.breakdown || []).reduce(
|
|
567
|
+
(sum, s) =>
|
|
568
|
+
sum + (s.tokenBudget?.wastedTokens.bySource.fragmentation || 0),
|
|
569
|
+
0
|
|
570
|
+
);
|
|
571
|
+
const totalContext = Math.max(
|
|
572
|
+
...(scoringResult.breakdown || []).map(
|
|
573
|
+
(s) => s.tokenBudget?.totalContextTokens || 0
|
|
574
|
+
)
|
|
575
|
+
);
|
|
566
576
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
(action: string) => ({
|
|
576
|
-
action,
|
|
577
|
-
estimatedImpact: 5,
|
|
578
|
-
priority: 'medium',
|
|
579
|
-
})
|
|
580
|
-
),
|
|
577
|
+
if (totalContext > 0) {
|
|
578
|
+
const unifiedBudget = calculateTokenBudget({
|
|
579
|
+
totalContextTokens: totalContext,
|
|
580
|
+
wastedTokens: {
|
|
581
|
+
duplication: totalWastedDuplication,
|
|
582
|
+
fragmentation: totalWastedFragmentation,
|
|
583
|
+
chattiness: 0,
|
|
584
|
+
},
|
|
581
585
|
});
|
|
582
|
-
}
|
|
583
586
|
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
toolName: 'dependency-health',
|
|
588
|
-
score: results.deps.summary.score,
|
|
589
|
-
rawMetrics: results.deps.rawData,
|
|
590
|
-
factors: [],
|
|
591
|
-
recommendations: (results.deps.recommendations || []).map(
|
|
592
|
-
(action: string) => ({
|
|
593
|
-
action,
|
|
594
|
-
estimatedImpact: 5,
|
|
595
|
-
priority: 'medium',
|
|
596
|
-
})
|
|
597
|
-
),
|
|
598
|
-
});
|
|
599
|
-
}
|
|
587
|
+
const targetModel = options.model || 'claude-4.6';
|
|
588
|
+
const modelPreset = getModelPreset(targetModel);
|
|
589
|
+
const costEstimate = estimateCostFromBudget(unifiedBudget, modelPreset);
|
|
600
590
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
rawMetrics: results.changeAmplification.rawData,
|
|
607
|
-
factors: [],
|
|
608
|
-
recommendations: (
|
|
609
|
-
results.changeAmplification.recommendations || []
|
|
610
|
-
).map((action: string) => ({
|
|
611
|
-
action,
|
|
612
|
-
estimatedImpact: 5,
|
|
613
|
-
priority: 'medium',
|
|
614
|
-
})),
|
|
615
|
-
});
|
|
616
|
-
}
|
|
591
|
+
const barWidth = 20;
|
|
592
|
+
const filled = Math.round(unifiedBudget.efficiencyRatio * barWidth);
|
|
593
|
+
const bar =
|
|
594
|
+
chalk.green('ā'.repeat(filled)) +
|
|
595
|
+
chalk.dim('ā'.repeat(barWidth - filled));
|
|
617
596
|
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
597
|
+
console.log(chalk.bold('\nš AI Token Budget Analysis (v0.13)'));
|
|
598
|
+
console.log(
|
|
599
|
+
` Efficiency: [${bar}] ${(unifiedBudget.efficiencyRatio * 100).toFixed(0)}%`
|
|
600
|
+
);
|
|
601
|
+
console.log(
|
|
602
|
+
` Total Context: ${chalk.bold(unifiedBudget.totalContextTokens.toLocaleString())} tokens`
|
|
603
|
+
);
|
|
604
|
+
console.log(
|
|
605
|
+
` Wasted Tokens: ${chalk.red(unifiedBudget.wastedTokens.total.toLocaleString())} (${((unifiedBudget.wastedTokens.total / unifiedBudget.totalContextTokens) * 100).toFixed(1)}%)`
|
|
606
|
+
);
|
|
607
|
+
console.log(` Waste Breakdown:`);
|
|
608
|
+
console.log(
|
|
609
|
+
` ⢠Duplication: ${unifiedBudget.wastedTokens.bySource.duplication.toLocaleString()} tokens`
|
|
610
|
+
);
|
|
611
|
+
console.log(
|
|
612
|
+
` ⢠Fragmentation: ${unifiedBudget.wastedTokens.bySource.fragmentation.toLocaleString()} tokens`
|
|
613
|
+
);
|
|
614
|
+
console.log(
|
|
615
|
+
` Potential Savings: ${chalk.green(unifiedBudget.potentialRetrievableTokens.toLocaleString())} tokens retrievable`
|
|
616
|
+
);
|
|
617
|
+
console.log(
|
|
618
|
+
`\n Est. Monthly Cost (${modelPreset.name}): ${chalk.bold('$' + costEstimate.total)} [range: $${costEstimate.range[0]}-$${costEstimate.range[1]}]`
|
|
627
619
|
);
|
|
628
620
|
|
|
629
|
-
|
|
630
|
-
|
|
621
|
+
// Attach unified budget to report for JSON persistence
|
|
622
|
+
(scoringResult as any).tokenBudget = unifiedBudget;
|
|
623
|
+
(scoringResult as any).costEstimate = {
|
|
624
|
+
model: modelPreset.name,
|
|
625
|
+
...costEstimate,
|
|
626
|
+
};
|
|
627
|
+
}
|
|
631
628
|
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
if (typeof prevScore === 'number') {
|
|
644
|
-
const diff = scoringResult.overall - prevScore;
|
|
645
|
-
const diffStr = diff > 0 ? `+${diff}` : String(diff);
|
|
646
|
-
console.log();
|
|
647
|
-
if (diff > 0) {
|
|
648
|
-
console.log(
|
|
649
|
-
chalk.green(
|
|
650
|
-
` š Trend: ${diffStr} compared to ${options.compareTo} (${prevScore} ā ${scoringResult.overall})`
|
|
651
|
-
)
|
|
652
|
-
);
|
|
653
|
-
} else if (diff < 0) {
|
|
654
|
-
console.log(
|
|
655
|
-
chalk.red(
|
|
656
|
-
` š Trend: ${diffStr} compared to ${options.compareTo} (${prevScore} ā ${scoringResult.overall})`
|
|
657
|
-
)
|
|
658
|
-
);
|
|
659
|
-
// Trend gating: if we regressed and CI is on or threshold is present, we could lower the threshold effectively,
|
|
660
|
-
// but for now, we just highlight the regression.
|
|
661
|
-
} else {
|
|
662
|
-
console.log(
|
|
663
|
-
chalk.blue(
|
|
664
|
-
` ā Trend: No change compared to ${options.compareTo} (${prevScore} ā ${scoringResult.overall})`
|
|
665
|
-
)
|
|
666
|
-
);
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
// Add trend info to scoringResult for programmatic use
|
|
670
|
-
(scoringResult as any).trend = {
|
|
671
|
-
previousScore: prevScore,
|
|
672
|
-
difference: diff,
|
|
673
|
-
};
|
|
674
|
-
} else {
|
|
675
|
-
console.log(
|
|
676
|
-
chalk.yellow(
|
|
677
|
-
`\n ā ļø Previous report at ${options.compareTo} does not contain an overall score.`
|
|
678
|
-
)
|
|
679
|
-
);
|
|
680
|
-
}
|
|
681
|
-
} catch (e) {
|
|
682
|
-
void e;
|
|
683
|
-
console.log(
|
|
684
|
-
chalk.yellow(
|
|
685
|
-
`\n ā ļø Could not read or parse previous report at ${options.compareTo}.`
|
|
686
|
-
)
|
|
687
|
-
);
|
|
688
|
-
}
|
|
689
|
-
}
|
|
629
|
+
// Show concise breakdown; detailed breakdown only if config requests it
|
|
630
|
+
if (scoringResult.breakdown && scoringResult.breakdown.length > 0) {
|
|
631
|
+
console.log(chalk.bold('\nTool breakdown:'));
|
|
632
|
+
scoringResult.breakdown.forEach((tool) => {
|
|
633
|
+
const rating = getRating(tool.score);
|
|
634
|
+
const rd = getRatingDisplay(rating);
|
|
635
|
+
console.log(
|
|
636
|
+
` - ${tool.toolName}: ${tool.score}/100 (${rating}) ${rd.emoji}`
|
|
637
|
+
);
|
|
638
|
+
});
|
|
639
|
+
console.log();
|
|
690
640
|
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
console.log(chalk.bold('\nTool breakdown:'));
|
|
641
|
+
if (finalOptions.scoring?.showBreakdown) {
|
|
642
|
+
console.log(chalk.bold('Detailed tool breakdown:'));
|
|
694
643
|
scoringResult.breakdown.forEach((tool) => {
|
|
695
|
-
|
|
696
|
-
const rd = getRatingDisplay(rating);
|
|
697
|
-
console.log(
|
|
698
|
-
` - ${tool.toolName}: ${tool.score}/100 (${rating}) ${rd.emoji}`
|
|
699
|
-
);
|
|
644
|
+
console.log(formatToolScore(tool));
|
|
700
645
|
});
|
|
701
646
|
console.log();
|
|
702
|
-
|
|
703
|
-
if (finalOptions.scoring?.showBreakdown) {
|
|
704
|
-
console.log(chalk.bold('Detailed tool breakdown:'));
|
|
705
|
-
scoringResult.breakdown.forEach((tool) => {
|
|
706
|
-
console.log(formatToolScore(tool));
|
|
707
|
-
});
|
|
708
|
-
console.log();
|
|
709
|
-
}
|
|
710
647
|
}
|
|
711
648
|
}
|
|
712
649
|
}
|
|
@@ -723,13 +660,26 @@ export async function scanAction(directory: string, options: ScanOptions) {
|
|
|
723
660
|
defaultFilename,
|
|
724
661
|
resolvedDir
|
|
725
662
|
);
|
|
726
|
-
const outputData = {
|
|
663
|
+
const outputData = {
|
|
664
|
+
...results,
|
|
665
|
+
scoring: scoringResult,
|
|
666
|
+
repository: repoMetadata,
|
|
667
|
+
};
|
|
727
668
|
handleJSONOutput(
|
|
728
669
|
outputData,
|
|
729
670
|
outputPath,
|
|
730
671
|
`ā
Report saved to ${outputPath}`
|
|
731
672
|
);
|
|
732
673
|
|
|
674
|
+
// Automatic Upload
|
|
675
|
+
if (options.upload) {
|
|
676
|
+
console.log(chalk.blue('\nš¤ Automatic upload triggered...'));
|
|
677
|
+
await uploadAction(outputPath, {
|
|
678
|
+
apiKey: options.apiKey,
|
|
679
|
+
server: options.server,
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
|
|
733
683
|
// Warn if graph caps may be exceeded
|
|
734
684
|
await warnIfGraphCapExceeded(outputData, resolvedDir);
|
|
735
685
|
} else {
|
|
@@ -741,11 +691,24 @@ export async function scanAction(directory: string, options: ScanOptions) {
|
|
|
741
691
|
defaultFilename,
|
|
742
692
|
resolvedDir
|
|
743
693
|
);
|
|
744
|
-
const outputData = {
|
|
694
|
+
const outputData = {
|
|
695
|
+
...results,
|
|
696
|
+
scoring: scoringResult,
|
|
697
|
+
repository: repoMetadata,
|
|
698
|
+
};
|
|
745
699
|
|
|
746
700
|
try {
|
|
747
701
|
writeFileSync(outputPath, JSON.stringify(outputData, null, 2));
|
|
748
702
|
console.log(chalk.dim(`ā
Report auto-persisted to ${outputPath}`));
|
|
703
|
+
|
|
704
|
+
// Automatic Upload (from auto-persistent report)
|
|
705
|
+
if (options.upload) {
|
|
706
|
+
console.log(chalk.blue('\nš¤ Automatic upload triggered...'));
|
|
707
|
+
await uploadAction(outputPath, {
|
|
708
|
+
apiKey: options.apiKey,
|
|
709
|
+
server: options.server,
|
|
710
|
+
});
|
|
711
|
+
}
|
|
749
712
|
// Warn if graph caps may be exceeded
|
|
750
713
|
await warnIfGraphCapExceeded(outputData, resolvedDir);
|
|
751
714
|
} catch (err) {
|
|
@@ -897,6 +860,8 @@ EXAMPLES:
|
|
|
897
860
|
$ aiready scan --ci --threshold 70 # GitHub Actions gatekeeper
|
|
898
861
|
$ aiready scan --ci --fail-on major # Fail on major+ issues
|
|
899
862
|
$ aiready scan --output json --output-file report.json
|
|
863
|
+
$ aiready scan --upload --api-key ar_... # Automatic platform upload
|
|
864
|
+
$ aiready scan --upload --server custom-url.com # Upload to custom platform
|
|
900
865
|
|
|
901
866
|
PROFILES:
|
|
902
867
|
agentic: aiSignalClarity, grounding, testability
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import { resolve as resolvePath } from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { handleCLIError } from '@aiready/core';
|
|
5
|
+
|
|
6
|
+
interface UploadOptions {
|
|
7
|
+
apiKey?: string;
|
|
8
|
+
repoId?: string;
|
|
9
|
+
server?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function uploadAction(file: string, options: UploadOptions) {
|
|
13
|
+
const startTime = Date.now();
|
|
14
|
+
const filePath = resolvePath(process.cwd(), file);
|
|
15
|
+
const serverUrl =
|
|
16
|
+
options.server ||
|
|
17
|
+
process.env.AIREADY_SERVER ||
|
|
18
|
+
'https://dev.platform.getaiready.dev';
|
|
19
|
+
const apiKey = options.apiKey || process.env.AIREADY_API_KEY;
|
|
20
|
+
|
|
21
|
+
if (!apiKey) {
|
|
22
|
+
console.error(chalk.red('ā API Key is required for upload.'));
|
|
23
|
+
console.log(
|
|
24
|
+
chalk.dim(
|
|
25
|
+
' Set AIREADY_API_KEY environment variable or use --api-key flag.'
|
|
26
|
+
)
|
|
27
|
+
);
|
|
28
|
+
console.log(
|
|
29
|
+
chalk.dim(' Get an API key from https://getaiready.dev/dashboard')
|
|
30
|
+
);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (!fs.existsSync(filePath)) {
|
|
35
|
+
console.error(chalk.red(`ā File not found: ${filePath}`));
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
console.log(chalk.blue(`š Uploading report to ${serverUrl}...`));
|
|
41
|
+
|
|
42
|
+
// Read the report file
|
|
43
|
+
console.log(chalk.dim(` Reading report from ${filePath}...`));
|
|
44
|
+
const reportContent = fs.readFileSync(filePath, 'utf-8');
|
|
45
|
+
const reportData = JSON.parse(reportContent);
|
|
46
|
+
console.log(chalk.dim(` Successfully parsed report JSON.`));
|
|
47
|
+
|
|
48
|
+
// Prepare upload payload
|
|
49
|
+
// Note: repoId is optional if the metadata contains it, but for now we'll require it or infer from metadata
|
|
50
|
+
const repoId = options.repoId || reportData.repository?.repoId;
|
|
51
|
+
|
|
52
|
+
const res = await fetch(`${serverUrl}/api/analysis/upload`, {
|
|
53
|
+
method: 'POST',
|
|
54
|
+
headers: {
|
|
55
|
+
'Content-Type': 'application/json',
|
|
56
|
+
Authorization: `Bearer ${apiKey}`,
|
|
57
|
+
},
|
|
58
|
+
body: JSON.stringify({
|
|
59
|
+
data: reportData,
|
|
60
|
+
repoId, // Might be null, server will handle mapping
|
|
61
|
+
}),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const contentType = res.headers.get('content-type');
|
|
65
|
+
let result: any = {};
|
|
66
|
+
|
|
67
|
+
if (contentType?.includes('application/json')) {
|
|
68
|
+
result = await res.json();
|
|
69
|
+
} else {
|
|
70
|
+
const text = await res.text();
|
|
71
|
+
result = { error: text || res.statusText };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (!res.ok) {
|
|
75
|
+
console.error(
|
|
76
|
+
chalk.red(`ā Upload failed: ${result.error || res.statusText}`)
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
// Special case for redirects or HTML error pages
|
|
80
|
+
if (contentType?.includes('text/html')) {
|
|
81
|
+
console.log(
|
|
82
|
+
chalk.yellow(
|
|
83
|
+
' Note: Received an HTML response. This often indicates a redirect (e.g., to a login page) or a server error.'
|
|
84
|
+
)
|
|
85
|
+
);
|
|
86
|
+
if (result.error?.includes('Redirecting')) {
|
|
87
|
+
console.log(
|
|
88
|
+
chalk.dim(
|
|
89
|
+
' Detected redirect. Check if the API endpoint requires authentication or has changed.'
|
|
90
|
+
)
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (res.status === 401) {
|
|
96
|
+
console.log(
|
|
97
|
+
chalk.dim(' Hint: Your API key may be invalid or expired.')
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const duration = ((Date.now() - startTime) / 1000).toFixed(2);
|
|
104
|
+
console.log(chalk.green(`\nā
Upload successful! (${duration}s)`));
|
|
105
|
+
console.log(chalk.cyan(` View results: ${serverUrl}/dashboard`));
|
|
106
|
+
|
|
107
|
+
if (result.analysis) {
|
|
108
|
+
console.log(chalk.dim(` Analysis ID: ${result.analysis.id}`));
|
|
109
|
+
console.log(chalk.dim(` Score: ${result.analysis.aiScore}/100`));
|
|
110
|
+
}
|
|
111
|
+
} catch (error) {
|
|
112
|
+
handleCLIError(error, 'Upload');
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export const uploadHelpText = `
|
|
117
|
+
EXAMPLES:
|
|
118
|
+
$ aiready upload report.json --api-key ar_...
|
|
119
|
+
$ aiready upload .aiready/latest.json
|
|
120
|
+
$ AIREADY_API_KEY=ar_... aiready upload report.json
|
|
121
|
+
|
|
122
|
+
ENVIRONMENT VARIABLES:
|
|
123
|
+
AIREADY_API_KEY Your platform API key
|
|
124
|
+
AIREADY_SERVER Custom platform URL (default: https://dev.platform.getaiready.dev)
|
|
125
|
+
`;
|