@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@objectstack/cli",
3
- "version": "2.0.7",
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": "2.0.7",
26
- "@objectstack/driver-memory": "^2.0.7",
27
- "@objectstack/objectql": "^2.0.7",
28
- "@objectstack/plugin-hono-server": "2.0.7",
29
- "@objectstack/rest": "2.0.7",
30
- "@objectstack/runtime": "^2.0.7",
31
- "@objectstack/spec": "2.0.7"
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": "2.0.7"
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
+ });