@aiready/cli 0.12.20 → 0.12.23

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 (51) hide show
  1. package/.turbo/turbo-build.log +8 -8
  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 +249 -15
  32. package/dist/cli.mjs +247 -13
  33. package/package.json +13 -12
  34. package/src/.aiready/aiready-report-20260308-174006.json +29526 -0
  35. package/src/__tests__/unified.test.ts +95 -0
  36. package/src/cli.ts +86 -0
  37. package/src/commands/__tests__/agent-grounding.test.ts +24 -0
  38. package/src/commands/__tests__/ai-signal-clarity.test.ts +32 -0
  39. package/src/commands/__tests__/consistency.test.ts +97 -0
  40. package/src/commands/__tests__/deps-health.test.ts +26 -0
  41. package/src/commands/__tests__/doc-drift.test.ts +26 -0
  42. package/src/commands/__tests__/extra-commands.test.ts +177 -0
  43. package/src/commands/__tests__/scan.test.ts +147 -0
  44. package/src/commands/__tests__/testability.test.ts +36 -0
  45. package/src/commands/__tests__/upload.test.ts +51 -0
  46. package/src/commands/__tests__/visualize.test.ts +82 -0
  47. package/src/commands/clawmart.ts +162 -0
  48. package/src/commands/index.ts +8 -0
  49. package/src/commands/scan.ts +33 -11
  50. package/src/utils/__tests__/helpers.test.ts +35 -0
  51. package/vitest.config.ts +20 -0
@@ -0,0 +1,147 @@
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
+ exitSpy.mockRestore();
132
+ });
133
+
134
+ it('handles upload flag', async () => {
135
+ await scanAction('.', { upload: true, apiKey: 'test-key' });
136
+ expect(upload.uploadAction).toHaveBeenCalled();
137
+ });
138
+
139
+ it('supports JSON output format', async () => {
140
+ vi.mocked(core.loadMergedConfig).mockResolvedValue({
141
+ tools: ['pattern-detect'],
142
+ output: { format: 'json', file: 'out.json' },
143
+ });
144
+ await scanAction('.', {});
145
+ expect(core.handleJSONOutput).toHaveBeenCalled();
146
+ });
147
+ });
@@ -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';
@@ -262,28 +262,50 @@ export async function scanAction(directory: string, options: ScanOptions) {
262
262
  wastedTokens: {
263
263
  duplication: totalWastedDuplication,
264
264
  fragmentation: totalWastedFragmentation,
265
- chattiness: 0,
265
+ chattiness: totalContext * 0.1, // Default chattiness
266
266
  },
267
267
  });
268
- const modelPreset = getModelPreset(options.model || 'claude-4.6');
269
- const costEstimate = estimateCostFromBudget(unifiedBudget, modelPreset);
270
268
 
271
- console.log(chalk.bold('\nšŸ“Š AI Token Budget Analysis'));
269
+ const allIssues: any[] = [];
270
+ for (const toolId of results.summary.toolsRun) {
271
+ if (results[toolId]?.results) {
272
+ results[toolId].results.forEach((fileRes: any) => {
273
+ if (fileRes.issues) {
274
+ allIssues.push(...fileRes.issues);
275
+ }
276
+ });
277
+ }
278
+ }
279
+
280
+ const modelId = options.model || 'claude-3-5-sonnet';
281
+ const roi = (await import('@aiready/core')).calculateBusinessROI({
282
+ tokenWaste: unifiedBudget.wastedTokens.total,
283
+ issues: allIssues,
284
+ modelId: modelId,
285
+ });
286
+
287
+ console.log(chalk.bold('\nšŸ’° Business Impact Analysis (Monthly)'));
272
288
  console.log(
273
- ` Efficiency: ${(unifiedBudget.efficiencyRatio * 100).toFixed(0)}%`
289
+ ` Potential Savings: ${chalk.green(chalk.bold('$' + roi.monthlySavings.toLocaleString()))}`
274
290
  );
275
291
  console.log(
276
- ` Wasted Tokens: ${chalk.red(unifiedBudget.wastedTokens.total.toLocaleString())}`
292
+ ` Productivity Gain: ${chalk.cyan(chalk.bold(roi.productivityGainHours + 'h'))} (est. dev time)`
277
293
  );
278
294
  console.log(
279
- ` Est. Monthly Cost (${modelPreset.name}): ${chalk.bold('$' + costEstimate.total)}`
295
+ ` Context Efficiency: ${chalk.yellow((unifiedBudget.efficiencyRatio * 100).toFixed(0) + '%')}`
296
+ );
297
+ console.log(
298
+ ` Annual Value: ${chalk.bold('$' + roi.annualValue.toLocaleString())} (ROI Prediction)`
280
299
  );
281
300
 
282
- (scoringResult as any).tokenBudget = unifiedBudget;
283
- (scoringResult as any).costEstimate = {
284
- model: modelPreset.name,
285
- ...costEstimate,
301
+ (results.summary as any).businessImpact = {
302
+ estimatedMonthlyWaste: roi.monthlySavings,
303
+ potentialSavings: roi.monthlySavings,
304
+ productivityHours: roi.productivityGainHours,
286
305
  };
306
+
307
+ (scoringResult as any).tokenBudget = unifiedBudget;
308
+ (scoringResult as any).businessROI = roi;
287
309
  }
288
310
 
289
311
  if (scoringResult.breakdown) {
@@ -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
+ });