@aiready/cli 0.12.20 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/.turbo/turbo-build.log +10 -10
  2. package/.turbo/turbo-lint.log +24 -5
  3. package/.turbo/turbo-test.log +66 -85
  4. package/coverage/base.css +224 -0
  5. package/coverage/block-navigation.js +87 -0
  6. package/coverage/clover.xml +865 -0
  7. package/coverage/coverage-final.json +15 -0
  8. package/coverage/favicon.png +0 -0
  9. package/coverage/index.html +146 -0
  10. package/coverage/prettify.css +1 -0
  11. package/coverage/prettify.js +2 -0
  12. package/coverage/sort-arrow-sprite.png +0 -0
  13. package/coverage/sorter.js +210 -0
  14. package/coverage/src/commands/agent-grounding.ts.html +271 -0
  15. package/coverage/src/commands/ai-signal-clarity.ts.html +253 -0
  16. package/coverage/src/commands/change-amplification.ts.html +94 -0
  17. package/coverage/src/commands/consistency.ts.html +781 -0
  18. package/coverage/src/commands/context.ts.html +871 -0
  19. package/coverage/src/commands/deps-health.ts.html +280 -0
  20. package/coverage/src/commands/doc-drift.ts.html +271 -0
  21. package/coverage/src/commands/index.html +281 -0
  22. package/coverage/src/commands/patterns.ts.html +745 -0
  23. package/coverage/src/commands/scan.ts.html +1393 -0
  24. package/coverage/src/commands/testability.ts.html +304 -0
  25. package/coverage/src/commands/upload.ts.html +466 -0
  26. package/coverage/src/commands/visualize.ts.html +1027 -0
  27. package/coverage/src/index.html +116 -0
  28. package/coverage/src/index.ts.html +1372 -0
  29. package/coverage/src/utils/helpers.ts.html +559 -0
  30. package/coverage/src/utils/index.html +116 -0
  31. package/dist/cli.js +259 -16
  32. package/dist/cli.mjs +259 -15
  33. package/package.json +13 -12
  34. package/src/.aiready/aiready-report-20260308-174006.json +29526 -0
  35. package/src/.aiready/aiready-report-20260310-153526.json +34441 -0
  36. package/src/.aiready/aiready-report-20260310-155555.json +34441 -0
  37. package/src/.aiready/aiready-report-20260310-162403.json +34441 -0
  38. package/src/.aiready/aiready-report-20260310-183821.json +34448 -0
  39. package/src/__tests__/unified.test.ts +95 -0
  40. package/src/cli.ts +86 -0
  41. package/src/commands/__tests__/agent-grounding.test.ts +24 -0
  42. package/src/commands/__tests__/ai-signal-clarity.test.ts +32 -0
  43. package/src/commands/__tests__/consistency.test.ts +97 -0
  44. package/src/commands/__tests__/deps-health.test.ts +26 -0
  45. package/src/commands/__tests__/doc-drift.test.ts +26 -0
  46. package/src/commands/__tests__/extra-commands.test.ts +177 -0
  47. package/src/commands/__tests__/scan.test.ts +151 -0
  48. package/src/commands/__tests__/testability.test.ts +36 -0
  49. package/src/commands/__tests__/upload.test.ts +51 -0
  50. package/src/commands/__tests__/visualize.test.ts +82 -0
  51. package/src/commands/clawmart.ts +162 -0
  52. package/src/commands/index.ts +8 -0
  53. package/src/commands/scan.ts +45 -13
  54. package/src/utils/__tests__/helpers.test.ts +35 -0
  55. package/vitest.config.ts +20 -0
@@ -0,0 +1,151 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { scanAction } from '../scan';
3
+ import * as core from '@aiready/core';
4
+ import * as index from '../../index';
5
+ import * as upload from '../upload';
6
+ import { writeFileSync, readFileSync, existsSync } from 'fs';
7
+ import { Severity } from '@aiready/core';
8
+
9
+ vi.mock('../../index', () => ({
10
+ analyzeUnified: vi.fn(),
11
+ scoreUnified: vi.fn(),
12
+ }));
13
+
14
+ vi.mock('../upload', () => ({
15
+ uploadAction: vi.fn(),
16
+ }));
17
+
18
+ vi.mock('@aiready/core', async () => {
19
+ const actual = await vi.importActual('@aiready/core');
20
+ return {
21
+ ...actual,
22
+ loadMergedConfig: vi.fn(),
23
+ loadConfig: vi.fn(),
24
+ getRepoMetadata: vi.fn().mockReturnValue({ name: 'test-repo' }),
25
+ handleJSONOutput: vi.fn(),
26
+ handleCLIError: vi.fn(),
27
+ getElapsedTime: vi.fn().mockReturnValue('1.0'),
28
+ resolveOutputPath: vi.fn().mockReturnValue('report.json'),
29
+ formatScore: vi.fn().mockReturnValue('80/100'),
30
+ calculateTokenBudget: vi.fn().mockReturnValue({
31
+ efficiencyRatio: 0.8,
32
+ wastedTokens: {
33
+ total: 100,
34
+ bySource: { duplication: 50, fragmentation: 50 },
35
+ },
36
+ totalContextTokens: 1000,
37
+ }),
38
+ calculateBusinessROI: vi.fn().mockReturnValue({
39
+ monthlySavings: 500,
40
+ productivityGainHours: 20,
41
+ annualValue: 6000,
42
+ }),
43
+ };
44
+ });
45
+
46
+ vi.mock('fs', () => ({
47
+ writeFileSync: vi.fn(),
48
+ readFileSync: vi.fn(),
49
+ existsSync: vi.fn().mockReturnValue(true),
50
+ }));
51
+
52
+ describe('Scan CLI Action', () => {
53
+ let consoleSpy: any;
54
+
55
+ beforeEach(() => {
56
+ vi.clearAllMocks();
57
+ consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
58
+ vi.mocked(core.loadMergedConfig).mockResolvedValue({
59
+ tools: ['pattern-detect'],
60
+ output: { format: 'console' },
61
+ });
62
+ vi.mocked(index.analyzeUnified).mockResolvedValue({
63
+ summary: {
64
+ totalIssues: 5,
65
+ toolsRun: ['pattern-detect'],
66
+ totalFiles: 10,
67
+ executionTime: 1000,
68
+ },
69
+ 'pattern-detect': {
70
+ results: [
71
+ {
72
+ fileName: 'f1.ts',
73
+ issues: [
74
+ { severity: Severity.Critical },
75
+ { severity: Severity.Major },
76
+ ],
77
+ },
78
+ ],
79
+ },
80
+ } as any);
81
+ vi.mocked(index.scoreUnified).mockResolvedValue({
82
+ overall: 80,
83
+ breakdown: [
84
+ {
85
+ toolName: 'pattern-detect',
86
+ score: 80,
87
+ tokenBudget: {
88
+ totalContextTokens: 1000,
89
+ wastedTokens: { bySource: { duplication: 50, fragmentation: 50 } },
90
+ },
91
+ },
92
+ ],
93
+ } as any);
94
+ });
95
+
96
+ it('runs standard scan with scoring', async () => {
97
+ await scanAction('.', { score: true });
98
+ expect(index.analyzeUnified).toHaveBeenCalled();
99
+ expect(index.scoreUnified).toHaveBeenCalled();
100
+ expect(consoleSpy).toHaveBeenCalledWith(
101
+ expect.stringContaining('AI Readiness Overall Score')
102
+ );
103
+ });
104
+
105
+ it('handles profiles correctly', async () => {
106
+ await scanAction('.', { profile: 'agentic' });
107
+ expect(core.loadMergedConfig).toHaveBeenCalled();
108
+ });
109
+
110
+ it('compares with previous report', async () => {
111
+ vi.mocked(readFileSync).mockReturnValue(
112
+ JSON.stringify({ scoring: { overall: 70 } })
113
+ );
114
+ await scanAction('.', { compareTo: 'prev.json', score: true });
115
+ expect(consoleSpy).toHaveBeenCalledWith(
116
+ expect.stringContaining('Trend: +10')
117
+ );
118
+ });
119
+
120
+ it('handles CI failure on critical issues', async () => {
121
+ const exitSpy = vi
122
+ .spyOn(process, 'exit')
123
+ .mockImplementation((() => {}) as any);
124
+
125
+ await scanAction('.', { ci: true, failOn: 'critical', score: true });
126
+
127
+ expect(exitSpy).toHaveBeenCalledWith(1);
128
+ expect(consoleSpy).toHaveBeenCalledWith(
129
+ expect.stringContaining('PR BLOCKED')
130
+ );
131
+ // Verify annotations are emitted
132
+ expect(consoleSpy).toHaveBeenCalledWith(
133
+ expect.stringContaining('Emitting GitHub Action annotations')
134
+ );
135
+ exitSpy.mockRestore();
136
+ });
137
+
138
+ it('handles upload flag', async () => {
139
+ await scanAction('.', { upload: true, apiKey: 'test-key' });
140
+ expect(upload.uploadAction).toHaveBeenCalled();
141
+ });
142
+
143
+ it('supports JSON output format', async () => {
144
+ vi.mocked(core.loadMergedConfig).mockResolvedValue({
145
+ tools: ['pattern-detect'],
146
+ output: { format: 'json', file: 'out.json' },
147
+ });
148
+ await scanAction('.', {});
149
+ expect(core.handleJSONOutput).toHaveBeenCalled();
150
+ });
151
+ });
@@ -0,0 +1,36 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { testabilityAction } from '../testability';
3
+
4
+ vi.mock('@aiready/testability', () => ({
5
+ analyzeTestability: vi.fn().mockResolvedValue({
6
+ summary: {
7
+ score: 80,
8
+ rating: 'good',
9
+ aiChangeSafetyRating: 'safe',
10
+ coverageRatio: 0.5,
11
+ },
12
+ rawData: { testFiles: 5, sourceFiles: 10 },
13
+ }),
14
+ calculateTestabilityScore: vi.fn().mockReturnValue({ score: 80 }),
15
+ }));
16
+
17
+ vi.mock('@aiready/core', () => ({
18
+ loadConfig: vi.fn().mockResolvedValue({}),
19
+ mergeConfigWithDefaults: vi
20
+ .fn()
21
+ .mockImplementation((c, d) => ({ ...d, ...c })),
22
+ }));
23
+
24
+ describe('Testability CLI Action', () => {
25
+ it('should run analysis and return scoring in json mode', async () => {
26
+ const result = await testabilityAction('.', { output: 'json' });
27
+ expect(result?.score).toBe(80);
28
+ });
29
+
30
+ it('should run analysis and print to console in default mode', async () => {
31
+ const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
32
+ await testabilityAction('.', { output: 'console' });
33
+ expect(consoleSpy).toHaveBeenCalled();
34
+ consoleSpy.mockRestore();
35
+ });
36
+ });
@@ -0,0 +1,51 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { uploadAction } from '../upload';
3
+ import fs from 'fs';
4
+
5
+ vi.mock('fs', () => ({
6
+ default: {
7
+ existsSync: vi.fn().mockReturnValue(true),
8
+ readFileSync: vi.fn().mockReturnValue('{"test": true}'),
9
+ },
10
+ }));
11
+
12
+ vi.mock('@aiready/core', () => ({
13
+ handleCLIError: vi.fn(),
14
+ }));
15
+
16
+ describe('Upload CLI Action', () => {
17
+ beforeEach(() => {
18
+ vi.stubGlobal(
19
+ 'fetch',
20
+ vi.fn().mockResolvedValue({
21
+ ok: true,
22
+ headers: { get: () => 'application/json' },
23
+ json: () =>
24
+ Promise.resolve({
25
+ success: true,
26
+ analysis: { id: '123', aiScore: 80 },
27
+ }),
28
+ })
29
+ );
30
+ vi.stubGlobal('process', {
31
+ ...process,
32
+ exit: vi.fn(),
33
+ });
34
+ });
35
+
36
+ it('should upload report successfully', async () => {
37
+ const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
38
+ await uploadAction('report.json', { apiKey: 'test-key' });
39
+ expect(consoleSpy).toHaveBeenCalledWith(
40
+ expect.stringContaining('Upload successful')
41
+ );
42
+ consoleSpy.mockRestore();
43
+ });
44
+
45
+ it('should fail if API key is missing', async () => {
46
+ const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
47
+ await uploadAction('report.json', {});
48
+ expect(process.exit).toHaveBeenCalledWith(1);
49
+ consoleSpy.mockRestore();
50
+ });
51
+ });
@@ -0,0 +1,82 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { visualizeAction } from '../visualize';
3
+ import * as fs from 'fs';
4
+ import * as core from '@aiready/core';
5
+ import * as helpers from '../../utils/helpers';
6
+ import { spawn } from 'child_process';
7
+
8
+ vi.mock('fs', async () => {
9
+ const actual = await vi.importActual('fs');
10
+ return {
11
+ ...actual,
12
+ readFileSync: vi.fn(),
13
+ existsSync: vi.fn(),
14
+ writeFileSync: vi.fn(),
15
+ copyFileSync: vi.fn(),
16
+ };
17
+ });
18
+
19
+ vi.mock('child_process', () => ({
20
+ spawn: vi.fn().mockReturnValue({ on: vi.fn(), kill: vi.fn() }),
21
+ }));
22
+
23
+ vi.mock('@aiready/visualizer/graph', () => ({
24
+ GraphBuilder: {
25
+ buildFromReport: vi.fn().mockReturnValue({ nodes: [], edges: [] }),
26
+ },
27
+ }));
28
+
29
+ vi.mock('@aiready/core', () => ({
30
+ handleCLIError: vi.fn(),
31
+ generateHTML: vi.fn().mockReturnValue('<html></html>'),
32
+ }));
33
+
34
+ vi.mock('../../utils/helpers', () => ({
35
+ findLatestScanReport: vi.fn(),
36
+ }));
37
+
38
+ describe('Visualize CLI Action', () => {
39
+ beforeEach(() => {
40
+ vi.clearAllMocks();
41
+ vi.spyOn(fs, 'existsSync').mockReturnValue(true);
42
+ vi.spyOn(fs, 'readFileSync').mockReturnValue(
43
+ JSON.stringify({ scoring: { overall: 80 } })
44
+ );
45
+ });
46
+
47
+ it('should generate HTML from specified report', async () => {
48
+ await visualizeAction('.', { report: 'report.json' });
49
+ expect(fs.writeFileSync).toHaveBeenCalledWith(
50
+ expect.stringContaining('visualization.html'),
51
+ '<html></html>',
52
+ 'utf8'
53
+ );
54
+ });
55
+
56
+ it('should find latest report if none specified', async () => {
57
+ vi.spyOn(helpers, 'findLatestScanReport').mockReturnValue('latest.json');
58
+ await visualizeAction('.', {});
59
+ expect(fs.readFileSync).toHaveBeenCalledWith(
60
+ expect.stringContaining('latest.json'),
61
+ 'utf8'
62
+ );
63
+ });
64
+
65
+ it('should handle missing reports', async () => {
66
+ vi.spyOn(fs, 'existsSync').mockReturnValue(false);
67
+ vi.spyOn(helpers, 'findLatestScanReport').mockReturnValue(null);
68
+ const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
69
+
70
+ await visualizeAction('.', { report: 'missing.json' });
71
+
72
+ expect(consoleSpy).toHaveBeenCalledWith(
73
+ expect.stringContaining('No AI readiness report found')
74
+ );
75
+ consoleSpy.mockRestore();
76
+ });
77
+
78
+ it('should attempt to open visualization if requested', async () => {
79
+ await visualizeAction('.', { report: 'report.json', open: true });
80
+ expect(spawn).toHaveBeenCalled();
81
+ });
82
+ });
@@ -0,0 +1,162 @@
1
+ import chalk from 'chalk';
2
+ import fs from 'fs';
3
+ import { resolve as resolvePath } from 'path';
4
+ import {
5
+ ClawMartClient,
6
+ ClawMartListing,
7
+ DownloadPackageResponse,
8
+ } from '@aiready/clawmart';
9
+
10
+ function getClient(options: any) {
11
+ const apiKey = options.apiKey || process.env.CLAWMART_API_KEY;
12
+ if (!apiKey) {
13
+ console.error(chalk.red('āŒ ClawMart API Key is required.'));
14
+ console.log(
15
+ chalk.dim(
16
+ ' Set CLAWMART_API_KEY environment variable or use --api-key flag.'
17
+ )
18
+ );
19
+ process.exit(1);
20
+ }
21
+ return new ClawMartClient(apiKey, options.server);
22
+ }
23
+
24
+ export async function clawmartMeAction(options: any) {
25
+ const client = getClient(options);
26
+ try {
27
+ const me = await client.getMe();
28
+ console.log(chalk.blue('\nšŸ‘¤ ClawMart Profile:'));
29
+ console.log(` Name: ${chalk.bold(me.name)}`);
30
+ console.log(` Email: ${me.email}`);
31
+ console.log(` Role: ${me.isCreator ? 'Creator' : 'User'}`);
32
+ console.log(
33
+ ` Sub: ${me.subscriptionActive ? chalk.green('Active') : chalk.red('Inactive')}`
34
+ );
35
+ } catch (error: any) {
36
+ console.error(chalk.red(`āŒ Failed to fetch profile: ${error.message}`));
37
+ }
38
+ }
39
+
40
+ export async function clawmartListingsAction(options: any) {
41
+ const client = getClient(options);
42
+ try {
43
+ let listings;
44
+ if (options.query) {
45
+ listings = await client.searchListings(
46
+ options.query,
47
+ options.type,
48
+ options.limit
49
+ );
50
+ } else {
51
+ listings = await client.getListings();
52
+ }
53
+
54
+ if (listings.length === 0) {
55
+ console.log(chalk.yellow('\nšŸ“­ No listings found.'));
56
+ return;
57
+ }
58
+
59
+ console.log(chalk.blue(`\nšŸ  ClawMart Listings (${listings.length}):`));
60
+ listings.forEach((l: ClawMartListing) => {
61
+ const status = l.published
62
+ ? chalk.green('Published')
63
+ : chalk.yellow('Draft');
64
+ console.log(` - ${chalk.bold(l.name)} (${chalk.dim(l.id)})`);
65
+ console.log(` ${chalk.italic(l.tagline)}`);
66
+ console.log(
67
+ ` Price: $${l.price} | Type: ${l.productType} | Status: ${status}`
68
+ );
69
+ console.log('');
70
+ });
71
+ } catch (error: any) {
72
+ console.error(chalk.red(`āŒ Failed to fetch listings: ${error.message}`));
73
+ }
74
+ }
75
+
76
+ export async function clawmartCreateAction(options: any) {
77
+ const client = getClient(options);
78
+ try {
79
+ const data = {
80
+ name: options.name,
81
+ tagline: options.tagline,
82
+ about: options.about || '',
83
+ category: options.category || 'Utility',
84
+ capabilities: options.capabilities ? options.capabilities.split(',') : [],
85
+ price: parseFloat(options.price) || 0,
86
+ productType: options.type as 'skill' | 'persona',
87
+ };
88
+
89
+ const listing = await client.createListing(data);
90
+ console.log(chalk.green(`\nāœ… Listing created successfully!`));
91
+ console.log(` ID: ${listing.id}`);
92
+ console.log(` Name: ${listing.name}`);
93
+ } catch (error: any) {
94
+ console.error(chalk.red(`āŒ Failed to create listing: ${error.message}`));
95
+ }
96
+ }
97
+
98
+ export async function clawmartUploadAction(
99
+ id: string,
100
+ files: string[],
101
+ options: any
102
+ ) {
103
+ const client = getClient(options);
104
+ try {
105
+ const fileData = files.map((f) => {
106
+ const path = resolvePath(process.cwd(), f);
107
+ if (!fs.existsSync(path)) {
108
+ throw new Error(`File not found: ${f}`);
109
+ }
110
+ return {
111
+ path: f,
112
+ content: fs.readFileSync(path, 'utf-8'),
113
+ };
114
+ });
115
+
116
+ await client.uploadVersion(id, fileData);
117
+ console.log(
118
+ chalk.green(`\nāœ… New version uploaded successfully to listing ${id}!`)
119
+ );
120
+ } catch (error: any) {
121
+ console.error(chalk.red(`āŒ Failed to upload version: ${error.message}`));
122
+ }
123
+ }
124
+
125
+ export async function clawmartDownloadAction(idOrSlug: string, options: any) {
126
+ const client = getClient(options);
127
+ try {
128
+ const pkg = await client.downloadPackage(idOrSlug);
129
+ const outDir = options.outDir || `./clawmart-${pkg.slug}`;
130
+
131
+ if (!fs.existsSync(outDir)) {
132
+ fs.mkdirSync(outDir, { recursive: true });
133
+ }
134
+
135
+ pkg.files.forEach((f: DownloadPackageResponse['files'][number]) => {
136
+ const filePath = resolvePath(outDir, f.path);
137
+ const dir = resolvePath(filePath, '..');
138
+ if (!fs.existsSync(dir)) {
139
+ fs.mkdirSync(dir, { recursive: true });
140
+ }
141
+ fs.writeFileSync(filePath, f.content);
142
+ });
143
+
144
+ console.log(
145
+ chalk.green(`\nāœ… Package ${idOrSlug} downloaded to ${outDir}`)
146
+ );
147
+ } catch (error: any) {
148
+ console.error(chalk.red(`āŒ Failed to download package: ${error.message}`));
149
+ }
150
+ }
151
+
152
+ export const clawmartHelpText = `
153
+ EXAMPLES:
154
+ $ aiready clawmart me
155
+ $ aiready clawmart listings --query "marketing"
156
+ $ aiready clawmart create --name "SEO Booster" --tagline "Boost your SEO" --type skill --price 10
157
+ $ aiready clawmart upload <listing-id> SKILL.md rules/
158
+ $ aiready clawmart download <listing-id-or-slug> --outDir ./my-skill
159
+
160
+ ENVIRONMENT VARIABLES:
161
+ CLAWMART_API_KEY Your ClawMart creator API key
162
+ `;
@@ -16,3 +16,11 @@ export { agentGroundingAction } from './agent-grounding';
16
16
  export { testabilityAction } from './testability';
17
17
  export { changeAmplificationAction } from './change-amplification';
18
18
  export { uploadAction, uploadHelpText } from './upload';
19
+ export {
20
+ clawmartMeAction,
21
+ clawmartListingsAction,
22
+ clawmartCreateAction,
23
+ clawmartUploadAction,
24
+ clawmartDownloadAction,
25
+ clawmartHelpText,
26
+ } from './clawmart';
@@ -23,6 +23,7 @@ import {
23
23
  IssueType,
24
24
  ToolName,
25
25
  ToolRegistry,
26
+ emitIssuesAsAnnotations,
26
27
  } from '@aiready/core';
27
28
  import { analyzeUnified, scoreUnified, type ScoringResult } from '../index';
28
29
  import {
@@ -262,28 +263,50 @@ export async function scanAction(directory: string, options: ScanOptions) {
262
263
  wastedTokens: {
263
264
  duplication: totalWastedDuplication,
264
265
  fragmentation: totalWastedFragmentation,
265
- chattiness: 0,
266
+ chattiness: totalContext * 0.1, // Default chattiness
266
267
  },
267
268
  });
268
- const modelPreset = getModelPreset(options.model || 'claude-4.6');
269
- const costEstimate = estimateCostFromBudget(unifiedBudget, modelPreset);
270
269
 
271
- console.log(chalk.bold('\nšŸ“Š AI Token Budget Analysis'));
270
+ const allIssues: any[] = [];
271
+ for (const toolId of results.summary.toolsRun) {
272
+ if (results[toolId]?.results) {
273
+ results[toolId].results.forEach((fileRes: any) => {
274
+ if (fileRes.issues) {
275
+ allIssues.push(...fileRes.issues);
276
+ }
277
+ });
278
+ }
279
+ }
280
+
281
+ const modelId = options.model || 'claude-3-5-sonnet';
282
+ const roi = (await import('@aiready/core')).calculateBusinessROI({
283
+ tokenWaste: unifiedBudget.wastedTokens.total,
284
+ issues: allIssues,
285
+ modelId: modelId,
286
+ });
287
+
288
+ console.log(chalk.bold('\nšŸ’° Business Impact Analysis (Monthly)'));
272
289
  console.log(
273
- ` Efficiency: ${(unifiedBudget.efficiencyRatio * 100).toFixed(0)}%`
290
+ ` Potential Savings: ${chalk.green(chalk.bold('$' + roi.monthlySavings.toLocaleString()))}`
274
291
  );
275
292
  console.log(
276
- ` Wasted Tokens: ${chalk.red(unifiedBudget.wastedTokens.total.toLocaleString())}`
293
+ ` Productivity Gain: ${chalk.cyan(chalk.bold(roi.productivityGainHours + 'h'))} (est. dev time)`
277
294
  );
278
295
  console.log(
279
- ` Est. Monthly Cost (${modelPreset.name}): ${chalk.bold('$' + costEstimate.total)}`
296
+ ` Context Efficiency: ${chalk.yellow((unifiedBudget.efficiencyRatio * 100).toFixed(0) + '%')}`
297
+ );
298
+ console.log(
299
+ ` Annual Value: ${chalk.bold('$' + roi.annualValue.toLocaleString())} (ROI Prediction)`
280
300
  );
281
301
 
282
- (scoringResult as any).tokenBudget = unifiedBudget;
283
- (scoringResult as any).costEstimate = {
284
- model: modelPreset.name,
285
- ...costEstimate,
302
+ (results.summary as any).businessImpact = {
303
+ estimatedMonthlyWaste: roi.monthlySavings,
304
+ potentialSavings: roi.monthlySavings,
305
+ productivityHours: roi.productivityGainHours,
286
306
  };
307
+
308
+ (scoringResult as any).tokenBudget = unifiedBudget;
309
+ (scoringResult as any).businessROI = roi;
287
310
  }
288
311
 
289
312
  if (scoringResult.breakdown) {
@@ -380,12 +403,21 @@ export async function scanAction(directory: string, options: ScanOptions) {
380
403
  let shouldFail = false;
381
404
  let failReason = '';
382
405
 
406
+ // Emit annotations for all issues found
407
+ const report = mapToUnifiedReport(results, scoringResult);
408
+ if (report.results && report.results.length > 0) {
409
+ console.log(
410
+ chalk.cyan(
411
+ `\nšŸ“ Emitting GitHub Action annotations for ${report.results.length} issues...`
412
+ )
413
+ );
414
+ emitIssuesAsAnnotations(report.results);
415
+ }
416
+
383
417
  if (threshold && scoringResult.overall < threshold) {
384
418
  shouldFail = true;
385
419
  failReason = `Score ${scoringResult.overall} < threshold ${threshold}`;
386
420
  }
387
-
388
- const report = mapToUnifiedReport(results, scoringResult);
389
421
  if (failOnLevel !== 'none') {
390
422
  if (failOnLevel === 'critical' && report.summary.criticalIssues > 0) {
391
423
  shouldFail = true;
@@ -0,0 +1,35 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import {
3
+ getReportTimestamp,
4
+ truncateArray,
5
+ generateMarkdownReport,
6
+ } from '../helpers';
7
+
8
+ describe('CLI Helpers', () => {
9
+ it('should generate a valid timestamp', () => {
10
+ const ts = getReportTimestamp();
11
+ expect(ts).toMatch(/^\d{8}-\d{6}$/);
12
+ });
13
+
14
+ it('should truncate arrays correctly', () => {
15
+ const arr = [1, 2, 3, 4, 5];
16
+ expect(truncateArray(arr, 3)).toContain('+2 more');
17
+ expect(truncateArray(arr, 10)).toBe('1, 2, 3, 4, 5');
18
+ });
19
+
20
+ it('should generate markdown report', () => {
21
+ const report = {
22
+ summary: {
23
+ filesAnalyzed: 10,
24
+ totalIssues: 5,
25
+ namingIssues: 2,
26
+ patternIssues: 3,
27
+ },
28
+ recommendations: ['Fix naming'],
29
+ };
30
+ const md = generateMarkdownReport(report, '1.5');
31
+ expect(md).toContain('# Consistency Analysis Report');
32
+ expect(md).toContain('**Files Analyzed:** 10');
33
+ expect(md).toContain('Fix naming');
34
+ });
35
+ });
@@ -0,0 +1,20 @@
1
+ import { defineConfig } from 'vitest/config';
2
+ import { resolve } from 'path';
3
+
4
+ export default defineConfig({
5
+ test: {
6
+ globals: true,
7
+ environment: 'node',
8
+ alias: {
9
+ '@aiready/core': resolve(__dirname, '../core/src/index.ts'),
10
+ '@aiready/pattern-detect': resolve(
11
+ __dirname,
12
+ '../pattern-detect/src/index.ts'
13
+ ),
14
+ '@aiready/context-analyzer': resolve(
15
+ __dirname,
16
+ '../context-analyzer/src/index.ts'
17
+ ),
18
+ },
19
+ },
20
+ });