@objectstack/cli 2.0.7 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +10 -6
- package/CHANGELOG.md +30 -0
- package/dist/bin.js +1988 -487
- package/dist/chunk-CSHQEILI.js +246 -0
- package/dist/chunk-Q74JNWKD.js +248 -0
- package/dist/config-A7BN6UIT.js +11 -0
- package/dist/config-UN34WBHT.js +10 -0
- package/dist/index.js +1058 -449
- package/package.json +9 -9
- package/src/bin.ts +12 -0
- package/src/commands/codemod.ts +178 -0
- package/src/commands/diff.ts +285 -0
- package/src/commands/doctor.ts +385 -3
- package/src/commands/explain.ts +402 -0
- package/src/commands/generate.ts +638 -4
- package/src/commands/lint.ts +303 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@objectstack/cli",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.1",
|
|
4
4
|
"description": "Command Line Interface for ObjectStack Protocol",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -22,16 +22,16 @@
|
|
|
22
22
|
"commander": "^14.0.3",
|
|
23
23
|
"tsx": "^4.7.1",
|
|
24
24
|
"zod": "^4.3.6",
|
|
25
|
-
"@objectstack/core": "
|
|
26
|
-
"@objectstack/driver-memory": "^
|
|
27
|
-
"@objectstack/objectql": "^
|
|
28
|
-
"@objectstack/plugin-hono-server": "
|
|
29
|
-
"@objectstack/rest": "
|
|
30
|
-
"@objectstack/runtime": "^
|
|
31
|
-
"@objectstack/spec": "
|
|
25
|
+
"@objectstack/core": "3.0.1",
|
|
26
|
+
"@objectstack/driver-memory": "^3.0.1",
|
|
27
|
+
"@objectstack/objectql": "^3.0.1",
|
|
28
|
+
"@objectstack/plugin-hono-server": "3.0.1",
|
|
29
|
+
"@objectstack/rest": "3.0.1",
|
|
30
|
+
"@objectstack/runtime": "^3.0.1",
|
|
31
|
+
"@objectstack/spec": "3.0.1"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
|
-
"@objectstack/core": "
|
|
34
|
+
"@objectstack/core": "3.0.1"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@types/node": "^25.2.2",
|
package/src/bin.ts
CHANGED
|
@@ -17,6 +17,10 @@ import { initCommand } from './commands/init.js';
|
|
|
17
17
|
import { infoCommand } from './commands/info.js';
|
|
18
18
|
import { generateCommand } from './commands/generate.js';
|
|
19
19
|
import { pluginCommand } from './commands/plugin.js';
|
|
20
|
+
import { diffCommand } from './commands/diff.js';
|
|
21
|
+
import { lintCommand } from './commands/lint.js';
|
|
22
|
+
import { explainCommand } from './commands/explain.js';
|
|
23
|
+
import { codemodCommand } from './commands/codemod.js';
|
|
20
24
|
import { loadPluginCommands } from './utils/plugin-commands.js';
|
|
21
25
|
|
|
22
26
|
const require = createRequire(import.meta.url);
|
|
@@ -79,6 +83,14 @@ program.addCommand(pluginCommand);
|
|
|
79
83
|
// ── Quality ──
|
|
80
84
|
program.addCommand(testCommand);
|
|
81
85
|
program.addCommand(doctorCommand);
|
|
86
|
+
program.addCommand(lintCommand);
|
|
87
|
+
program.addCommand(diffCommand);
|
|
88
|
+
|
|
89
|
+
// ── Reference ──
|
|
90
|
+
program.addCommand(explainCommand);
|
|
91
|
+
|
|
92
|
+
// ── Code Transforms ──
|
|
93
|
+
program.addCommand(codemodCommand);
|
|
82
94
|
|
|
83
95
|
// ── Plugin-Contributed Commands ──
|
|
84
96
|
// Load commands from installed plugins that declare `contributes.commands` in their manifest.
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import { printHeader, printSuccess, printError, printInfo, printStep, createTimer } from '../utils/format.js';
|
|
8
|
+
|
|
9
|
+
// ─── Transform Definitions ──────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
interface Transform {
|
|
12
|
+
pattern: RegExp;
|
|
13
|
+
replacement: string;
|
|
14
|
+
description: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const V2_TO_V3_TRANSFORMS: Transform[] = [
|
|
18
|
+
{
|
|
19
|
+
pattern: /\bEnhancedObjectKernel\b/g,
|
|
20
|
+
replacement: 'ObjectKernel',
|
|
21
|
+
description: 'EnhancedObjectKernel → ObjectKernel',
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
pattern: /\bmax_length\b/g,
|
|
25
|
+
replacement: 'maxLength',
|
|
26
|
+
description: 'max_length → maxLength',
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
pattern: /\breference_filters\b/g,
|
|
30
|
+
replacement: 'referenceFilters',
|
|
31
|
+
description: 'reference_filters → referenceFilters',
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
pattern: /\bdefault_value\b/g,
|
|
35
|
+
replacement: 'defaultValue',
|
|
36
|
+
description: 'default_value → defaultValue',
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
pattern: /\bmin_length\b/g,
|
|
40
|
+
replacement: 'minLength',
|
|
41
|
+
description: 'min_length → minLength',
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
pattern: /\bunique_name\b/g,
|
|
45
|
+
replacement: 'uniqueName',
|
|
46
|
+
description: 'unique_name → uniqueName',
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
pattern: /from\s+['"]@objectstack\/core\/enhanced['"]/g,
|
|
50
|
+
replacement: "from '@objectstack/core'",
|
|
51
|
+
description: 'Update import from @objectstack/core/enhanced',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
pattern: /from\s+['"]@objectstack\/spec\/dist\/[^'"]+['"]/g,
|
|
55
|
+
replacement: "from '@objectstack/spec'",
|
|
56
|
+
description: 'Update deep import from @objectstack/spec/dist/',
|
|
57
|
+
},
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
// ─── Helpers ────────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
function walkDir(dir: string, ext: string): string[] {
|
|
63
|
+
const results: string[] = [];
|
|
64
|
+
if (!fs.existsSync(dir)) return results;
|
|
65
|
+
|
|
66
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
67
|
+
for (const entry of entries) {
|
|
68
|
+
if (entry.name === 'node_modules') continue;
|
|
69
|
+
const fullPath = path.join(dir, entry.name);
|
|
70
|
+
if (entry.isDirectory()) {
|
|
71
|
+
results.push(...walkDir(fullPath, ext));
|
|
72
|
+
} else if (entry.name.endsWith(ext)) {
|
|
73
|
+
results.push(fullPath);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return results;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ─── v2-to-v3 Sub-Command ──────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
const v2ToV3Command = new Command('v2-to-v3')
|
|
82
|
+
.description('Migrate ObjectStack v2 code patterns to v3')
|
|
83
|
+
.option('--dir <directory>', 'Directory to scan', 'src/')
|
|
84
|
+
.option('--dry-run', 'Show changes without writing files')
|
|
85
|
+
.action(async (options) => {
|
|
86
|
+
printHeader('Codemod: v2 → v3');
|
|
87
|
+
|
|
88
|
+
const timer = createTimer();
|
|
89
|
+
const dir = path.resolve(process.cwd(), options.dir);
|
|
90
|
+
|
|
91
|
+
if (!fs.existsSync(dir)) {
|
|
92
|
+
printError(`Directory not found: ${dir}`);
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
console.log(` ${chalk.dim('Directory:')} ${chalk.white(options.dir)}`);
|
|
97
|
+
console.log(` ${chalk.dim('Dry run:')} ${chalk.white(options.dryRun ? 'yes' : 'no')}`);
|
|
98
|
+
console.log('');
|
|
99
|
+
|
|
100
|
+
printStep('Scanning TypeScript files...');
|
|
101
|
+
const files = walkDir(dir, '.ts');
|
|
102
|
+
|
|
103
|
+
if (files.length === 0) {
|
|
104
|
+
printInfo('No .ts files found in directory');
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
printInfo(`Found ${files.length} TypeScript file(s)`);
|
|
109
|
+
console.log('');
|
|
110
|
+
|
|
111
|
+
let totalTransforms = 0;
|
|
112
|
+
let filesModified = 0;
|
|
113
|
+
const transformCounts: Record<string, number> = {};
|
|
114
|
+
|
|
115
|
+
for (const file of files) {
|
|
116
|
+
const original = fs.readFileSync(file, 'utf-8');
|
|
117
|
+
let content = original;
|
|
118
|
+
let fileTransforms = 0;
|
|
119
|
+
|
|
120
|
+
for (const transform of V2_TO_V3_TRANSFORMS) {
|
|
121
|
+
const matches = content.match(transform.pattern);
|
|
122
|
+
if (matches) {
|
|
123
|
+
const count = matches.length;
|
|
124
|
+
content = content.replace(transform.pattern, transform.replacement);
|
|
125
|
+
fileTransforms += count;
|
|
126
|
+
transformCounts[transform.description] = (transformCounts[transform.description] || 0) + count;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (fileTransforms > 0) {
|
|
131
|
+
const relPath = path.relative(process.cwd(), file);
|
|
132
|
+
filesModified++;
|
|
133
|
+
totalTransforms += fileTransforms;
|
|
134
|
+
|
|
135
|
+
if (options.dryRun) {
|
|
136
|
+
printInfo(`${relPath} — ${fileTransforms} change(s)`);
|
|
137
|
+
} else {
|
|
138
|
+
fs.writeFileSync(file, content);
|
|
139
|
+
printSuccess(`${relPath} — ${fileTransforms} change(s)`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
console.log('');
|
|
145
|
+
|
|
146
|
+
if (totalTransforms === 0) {
|
|
147
|
+
printSuccess('No v2 patterns found — code is already v3 compatible');
|
|
148
|
+
} else {
|
|
149
|
+
console.log(chalk.bold(' Summary:'));
|
|
150
|
+
for (const [desc, count] of Object.entries(transformCounts)) {
|
|
151
|
+
console.log(` ${chalk.dim(desc)}: ${chalk.white(count)}`);
|
|
152
|
+
}
|
|
153
|
+
console.log('');
|
|
154
|
+
|
|
155
|
+
if (options.dryRun) {
|
|
156
|
+
printInfo(`Would modify ${filesModified} file(s) with ${totalTransforms} total change(s)`);
|
|
157
|
+
console.log(chalk.dim(' Run without --dry-run to apply changes'));
|
|
158
|
+
} else {
|
|
159
|
+
printSuccess(`Modified ${filesModified} file(s) with ${totalTransforms} total change(s) (${timer.display()})`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
console.log('');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// ─── Main Codemod Command ───────────────────────────────────────────
|
|
167
|
+
|
|
168
|
+
export const codemodCommand = new Command('codemod')
|
|
169
|
+
.description('Run automated code transformations')
|
|
170
|
+
.addCommand(v2ToV3Command)
|
|
171
|
+
.action(() => {
|
|
172
|
+
printHeader('Codemod');
|
|
173
|
+
console.log(chalk.bold(' Available codemods:'));
|
|
174
|
+
console.log(` ${chalk.cyan('v2-to-v3'.padEnd(16))} Migrate ObjectStack v2 code patterns to v3`);
|
|
175
|
+
console.log('');
|
|
176
|
+
console.log(chalk.dim(' Usage: objectstack codemod v2-to-v3 [--dir src/] [--dry-run]'));
|
|
177
|
+
console.log('');
|
|
178
|
+
});
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { loadConfig } from '../utils/config.js';
|
|
6
|
+
import {
|
|
7
|
+
printHeader,
|
|
8
|
+
printSuccess,
|
|
9
|
+
printWarning,
|
|
10
|
+
printError,
|
|
11
|
+
printInfo,
|
|
12
|
+
printStep,
|
|
13
|
+
createTimer,
|
|
14
|
+
} from '../utils/format.js';
|
|
15
|
+
|
|
16
|
+
// ─── Types ──────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
interface DiffEntry {
|
|
19
|
+
type: 'added' | 'removed' | 'modified';
|
|
20
|
+
category: string;
|
|
21
|
+
name: string;
|
|
22
|
+
detail?: string;
|
|
23
|
+
breaking: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ─── Helpers ────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
function getNames(items: any[] | undefined): Map<string, any> {
|
|
29
|
+
const map = new Map<string, any>();
|
|
30
|
+
if (!Array.isArray(items)) return map;
|
|
31
|
+
for (const item of items) {
|
|
32
|
+
if (item?.name) map.set(item.name, item);
|
|
33
|
+
}
|
|
34
|
+
return map;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function getFieldNames(obj: any): string[] {
|
|
38
|
+
if (!obj?.fields || typeof obj.fields !== 'object') return [];
|
|
39
|
+
return Object.keys(obj.fields);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function getFieldType(obj: any, fieldName: string): string | undefined {
|
|
43
|
+
return obj?.fields?.[fieldName]?.type;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function isFieldRequired(obj: any, fieldName: string): boolean {
|
|
47
|
+
return obj?.fields?.[fieldName]?.required === true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function diffNamedArrays(
|
|
51
|
+
beforeItems: any[] | undefined,
|
|
52
|
+
afterItems: any[] | undefined,
|
|
53
|
+
category: string,
|
|
54
|
+
detectFieldChanges: boolean,
|
|
55
|
+
): DiffEntry[] {
|
|
56
|
+
const entries: DiffEntry[] = [];
|
|
57
|
+
const beforeMap = getNames(beforeItems);
|
|
58
|
+
const afterMap = getNames(afterItems);
|
|
59
|
+
|
|
60
|
+
// Removed items
|
|
61
|
+
for (const [name] of beforeMap) {
|
|
62
|
+
if (!afterMap.has(name)) {
|
|
63
|
+
entries.push({
|
|
64
|
+
type: 'removed',
|
|
65
|
+
category,
|
|
66
|
+
name,
|
|
67
|
+
breaking: true,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Added items
|
|
73
|
+
for (const [name] of afterMap) {
|
|
74
|
+
if (!beforeMap.has(name)) {
|
|
75
|
+
entries.push({
|
|
76
|
+
type: 'added',
|
|
77
|
+
category,
|
|
78
|
+
name,
|
|
79
|
+
breaking: false,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Modified items — field-level diff for objects
|
|
85
|
+
if (detectFieldChanges) {
|
|
86
|
+
for (const [name, beforeObj] of beforeMap) {
|
|
87
|
+
const afterObj = afterMap.get(name);
|
|
88
|
+
if (!afterObj) continue;
|
|
89
|
+
|
|
90
|
+
const beforeFields = getFieldNames(beforeObj);
|
|
91
|
+
const afterFields = getFieldNames(afterObj);
|
|
92
|
+
const beforeSet = new Set(beforeFields);
|
|
93
|
+
const afterSet = new Set(afterFields);
|
|
94
|
+
|
|
95
|
+
// Removed fields
|
|
96
|
+
for (const f of beforeFields) {
|
|
97
|
+
if (!afterSet.has(f)) {
|
|
98
|
+
entries.push({
|
|
99
|
+
type: 'removed',
|
|
100
|
+
category: `${category}.${name}.fields`,
|
|
101
|
+
name: f,
|
|
102
|
+
breaking: true,
|
|
103
|
+
detail: 'field removed',
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Added fields
|
|
109
|
+
for (const f of afterFields) {
|
|
110
|
+
if (!beforeSet.has(f)) {
|
|
111
|
+
const breaking = isFieldRequired(afterObj, f);
|
|
112
|
+
entries.push({
|
|
113
|
+
type: 'added',
|
|
114
|
+
category: `${category}.${name}.fields`,
|
|
115
|
+
name: f,
|
|
116
|
+
breaking,
|
|
117
|
+
detail: breaking ? 'required field added' : 'optional field added',
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Type changes
|
|
123
|
+
for (const f of beforeFields) {
|
|
124
|
+
if (!afterSet.has(f)) continue;
|
|
125
|
+
const oldType = getFieldType(beforeObj, f);
|
|
126
|
+
const newType = getFieldType(afterObj, f);
|
|
127
|
+
if (oldType && newType && oldType !== newType) {
|
|
128
|
+
entries.push({
|
|
129
|
+
type: 'modified',
|
|
130
|
+
category: `${category}.${name}.fields`,
|
|
131
|
+
name: f,
|
|
132
|
+
breaking: true,
|
|
133
|
+
detail: `type changed: ${oldType} → ${newType}`,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Label / ownership changes on the object itself
|
|
139
|
+
if (beforeObj.label !== afterObj.label) {
|
|
140
|
+
entries.push({
|
|
141
|
+
type: 'modified',
|
|
142
|
+
category,
|
|
143
|
+
name,
|
|
144
|
+
breaking: false,
|
|
145
|
+
detail: `label changed: "${beforeObj.label ?? '(none)'}" → "${afterObj.label ?? '(none)'}"`,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return entries;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ─── Command ────────────────────────────────────────────────────────
|
|
155
|
+
|
|
156
|
+
export const diffCommand = new Command('diff')
|
|
157
|
+
.description('Compare two ObjectStack configurations and detect breaking changes')
|
|
158
|
+
.argument('[before]', 'Path to the "before" config file')
|
|
159
|
+
.argument('[after]', 'Path to the "after" config file')
|
|
160
|
+
.option('--before <path>', 'Path to the "before" config (alternative)')
|
|
161
|
+
.option('--after <path>', 'Path to the "after" config (alternative)')
|
|
162
|
+
.option('--json', 'Output as JSON')
|
|
163
|
+
.option('--breaking-only', 'Show only breaking changes')
|
|
164
|
+
.action(async (beforeArg, afterArg, options) => {
|
|
165
|
+
const timer = createTimer();
|
|
166
|
+
|
|
167
|
+
const beforePath: string | undefined = beforeArg || options.before;
|
|
168
|
+
const afterPath: string | undefined = afterArg || options.after;
|
|
169
|
+
|
|
170
|
+
if (!beforePath || !afterPath) {
|
|
171
|
+
printError('Two config file paths are required.');
|
|
172
|
+
console.log('');
|
|
173
|
+
console.log(chalk.dim(' Usage: objectstack diff <before> <after>'));
|
|
174
|
+
console.log(chalk.dim(' or: objectstack diff --before path1 --after path2'));
|
|
175
|
+
process.exit(1);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (!options.json) {
|
|
179
|
+
printHeader('Diff');
|
|
180
|
+
printStep('Loading configurations...');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
const { config: beforeConfig } = await loadConfig(beforePath);
|
|
185
|
+
const { config: afterConfig } = await loadConfig(afterPath);
|
|
186
|
+
|
|
187
|
+
if (!options.json) {
|
|
188
|
+
printInfo(`Before: ${chalk.white(beforePath)}`);
|
|
189
|
+
printInfo(`After: ${chalk.white(afterPath)}`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ── Diff all categories ──
|
|
193
|
+
const allDiffs: DiffEntry[] = [];
|
|
194
|
+
|
|
195
|
+
// Objects (with field-level diff)
|
|
196
|
+
allDiffs.push(...diffNamedArrays(beforeConfig.objects, afterConfig.objects, 'objects', true));
|
|
197
|
+
|
|
198
|
+
// Views, Flows, Agents, Apps (name-level diff)
|
|
199
|
+
const simpleCats: Array<{ key: string; label: string }> = [
|
|
200
|
+
{ key: 'views', label: 'views' },
|
|
201
|
+
{ key: 'flows', label: 'flows' },
|
|
202
|
+
{ key: 'agents', label: 'agents' },
|
|
203
|
+
{ key: 'apps', label: 'apps' },
|
|
204
|
+
{ key: 'dashboards', label: 'dashboards' },
|
|
205
|
+
{ key: 'actions', label: 'actions' },
|
|
206
|
+
{ key: 'workflows', label: 'workflows' },
|
|
207
|
+
{ key: 'apis', label: 'apis' },
|
|
208
|
+
{ key: 'roles', label: 'roles' },
|
|
209
|
+
];
|
|
210
|
+
|
|
211
|
+
for (const cat of simpleCats) {
|
|
212
|
+
allDiffs.push(
|
|
213
|
+
...diffNamedArrays(beforeConfig[cat.key], afterConfig[cat.key], cat.label, false),
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ── Filter ──
|
|
218
|
+
const diffs = options.breakingOnly
|
|
219
|
+
? allDiffs.filter((d) => d.breaking)
|
|
220
|
+
: allDiffs;
|
|
221
|
+
|
|
222
|
+
const breakingCount = allDiffs.filter((d) => d.breaking).length;
|
|
223
|
+
|
|
224
|
+
// ── Output ──
|
|
225
|
+
if (options.json) {
|
|
226
|
+
console.log(JSON.stringify({
|
|
227
|
+
before: beforePath,
|
|
228
|
+
after: afterPath,
|
|
229
|
+
total: diffs.length,
|
|
230
|
+
breaking: breakingCount,
|
|
231
|
+
changes: diffs,
|
|
232
|
+
duration: timer.elapsed(),
|
|
233
|
+
}, null, 2));
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
console.log('');
|
|
238
|
+
|
|
239
|
+
if (diffs.length === 0) {
|
|
240
|
+
printSuccess(options.breakingOnly
|
|
241
|
+
? 'No breaking changes detected.'
|
|
242
|
+
: 'No changes detected.');
|
|
243
|
+
console.log('');
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Group by category
|
|
248
|
+
const grouped = new Map<string, DiffEntry[]>();
|
|
249
|
+
for (const d of diffs) {
|
|
250
|
+
const key = d.category;
|
|
251
|
+
if (!grouped.has(key)) grouped.set(key, []);
|
|
252
|
+
grouped.get(key)!.push(d);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
for (const [category, items] of grouped) {
|
|
256
|
+
console.log(` ${chalk.bold(category)}`);
|
|
257
|
+
for (const item of items) {
|
|
258
|
+
const icon = item.type === 'added' ? '+' : item.type === 'removed' ? '-' : '~';
|
|
259
|
+
const color = item.type === 'added' ? chalk.green : item.type === 'removed' ? chalk.red : chalk.yellow;
|
|
260
|
+
const breakingTag = item.breaking ? chalk.bgRed.white(' BREAKING ') + ' ' : '';
|
|
261
|
+
const detail = item.detail ? chalk.dim(` (${item.detail})`) : '';
|
|
262
|
+
console.log(` ${color(icon)} ${breakingTag}${color(item.name)}${detail}`);
|
|
263
|
+
}
|
|
264
|
+
console.log('');
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Summary
|
|
268
|
+
if (breakingCount > 0) {
|
|
269
|
+
printError(`${breakingCount} breaking change(s) detected`);
|
|
270
|
+
} else {
|
|
271
|
+
printSuccess('No breaking changes');
|
|
272
|
+
}
|
|
273
|
+
console.log(chalk.dim(` ${diffs.length} total change(s) in ${timer.display()}`));
|
|
274
|
+
console.log('');
|
|
275
|
+
|
|
276
|
+
} catch (error: any) {
|
|
277
|
+
if (options.json) {
|
|
278
|
+
console.log(JSON.stringify({ error: error.message }));
|
|
279
|
+
process.exit(1);
|
|
280
|
+
}
|
|
281
|
+
console.log('');
|
|
282
|
+
printError(error.message || String(error));
|
|
283
|
+
process.exit(1);
|
|
284
|
+
}
|
|
285
|
+
});
|