@objectstack/cli 3.0.5 → 3.0.7

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 (141) hide show
  1. package/.turbo/turbo-build.log +2 -26
  2. package/CHANGELOG.md +27 -0
  3. package/README.md +98 -54
  4. package/bin/run-dev.js +5 -0
  5. package/bin/run.js +5 -0
  6. package/dist/bin.d.ts +11 -0
  7. package/dist/bin.d.ts.map +1 -0
  8. package/dist/bin.js +12 -3759
  9. package/dist/bin.js.map +1 -0
  10. package/dist/commands/codemod/v2-to-v3.d.ts +10 -0
  11. package/dist/commands/codemod/v2-to-v3.d.ts.map +1 -0
  12. package/dist/commands/codemod/v2-to-v3.js +145 -0
  13. package/dist/commands/codemod/v2-to-v3.js.map +1 -0
  14. package/dist/commands/compile.d.ts +13 -0
  15. package/dist/commands/compile.d.ts.map +1 -0
  16. package/dist/commands/compile.js +91 -0
  17. package/dist/commands/compile.js.map +1 -0
  18. package/dist/commands/create.d.ts +91 -0
  19. package/dist/commands/create.d.ts.map +1 -0
  20. package/dist/commands/create.js +259 -0
  21. package/dist/commands/create.js.map +1 -0
  22. package/dist/commands/dev.d.ts +14 -0
  23. package/dist/commands/dev.d.ts.map +1 -0
  24. package/dist/commands/dev.js +67 -0
  25. package/dist/commands/dev.js.map +1 -0
  26. package/dist/commands/diff.d.ts +16 -0
  27. package/dist/commands/diff.d.ts.map +1 -0
  28. package/dist/commands/diff.js +239 -0
  29. package/dist/commands/diff.js.map +1 -0
  30. package/dist/commands/doctor.d.ts +10 -0
  31. package/dist/commands/doctor.d.ts.map +1 -0
  32. package/dist/commands/doctor.js +532 -0
  33. package/dist/commands/doctor.js.map +1 -0
  34. package/dist/commands/explain.d.ts +12 -0
  35. package/dist/commands/explain.d.ts.map +1 -0
  36. package/dist/commands/explain.js +368 -0
  37. package/dist/commands/explain.js.map +1 -0
  38. package/dist/commands/generate.d.ts +17 -0
  39. package/dist/commands/generate.d.ts.map +1 -0
  40. package/dist/commands/generate.js +833 -0
  41. package/dist/commands/generate.js.map +1 -0
  42. package/dist/commands/info.d.ts +12 -0
  43. package/dist/commands/info.d.ts.map +1 -0
  44. package/dist/commands/info.js +100 -0
  45. package/dist/commands/info.js.map +1 -0
  46. package/dist/commands/init.d.ts +22 -0
  47. package/dist/commands/init.d.ts.map +1 -0
  48. package/dist/commands/init.js +295 -0
  49. package/dist/commands/init.js.map +1 -0
  50. package/dist/commands/lint.d.ts +13 -0
  51. package/dist/commands/lint.d.ts.map +1 -0
  52. package/dist/commands/lint.js +255 -0
  53. package/dist/commands/lint.js.map +1 -0
  54. package/dist/commands/plugin/add.d.ts +22 -0
  55. package/dist/commands/plugin/add.d.ts.map +1 -0
  56. package/dist/commands/plugin/add.js +93 -0
  57. package/dist/commands/plugin/add.js.map +1 -0
  58. package/dist/commands/plugin/info.d.ts +10 -0
  59. package/dist/commands/plugin/info.d.ts.map +1 -0
  60. package/dist/commands/plugin/info.js +65 -0
  61. package/dist/commands/plugin/info.js.map +1 -0
  62. package/dist/commands/plugin/list.d.ts +13 -0
  63. package/dist/commands/plugin/list.d.ts.map +1 -0
  64. package/dist/commands/plugin/list.js +78 -0
  65. package/dist/commands/plugin/list.js.map +1 -0
  66. package/dist/commands/plugin/remove.d.ts +20 -0
  67. package/dist/commands/plugin/remove.d.ts.map +1 -0
  68. package/dist/commands/plugin/remove.js +79 -0
  69. package/dist/commands/plugin/remove.js.map +1 -0
  70. package/dist/commands/serve.d.ts +15 -0
  71. package/dist/commands/serve.d.ts.map +1 -0
  72. package/dist/commands/serve.js +286 -0
  73. package/dist/commands/serve.js.map +1 -0
  74. package/dist/commands/studio.d.ts +19 -0
  75. package/dist/commands/studio.d.ts.map +1 -0
  76. package/dist/commands/studio.js +43 -0
  77. package/dist/commands/studio.js.map +1 -0
  78. package/dist/commands/test.d.ts +13 -0
  79. package/dist/commands/test.d.ts.map +1 -0
  80. package/dist/commands/test.js +120 -0
  81. package/dist/commands/test.js.map +1 -0
  82. package/dist/commands/validate.d.ts +13 -0
  83. package/dist/commands/validate.d.ts.map +1 -0
  84. package/dist/commands/validate.js +115 -0
  85. package/dist/commands/validate.js.map +1 -0
  86. package/dist/index.d.ts +15 -114
  87. package/dist/index.d.ts.map +1 -0
  88. package/dist/index.js +20 -2799
  89. package/dist/index.js.map +1 -0
  90. package/dist/utils/config.d.ts +21 -0
  91. package/dist/utils/config.d.ts.map +1 -0
  92. package/dist/utils/config.js +66 -0
  93. package/dist/utils/config.js.map +1 -0
  94. package/dist/utils/format.d.ts +52 -0
  95. package/dist/utils/format.d.ts.map +1 -0
  96. package/dist/utils/format.js +202 -0
  97. package/dist/utils/format.js.map +1 -0
  98. package/dist/utils/plugin-helpers.d.ts +14 -0
  99. package/dist/utils/plugin-helpers.d.ts.map +1 -0
  100. package/dist/utils/plugin-helpers.js +40 -0
  101. package/dist/utils/plugin-helpers.js.map +1 -0
  102. package/dist/utils/studio.d.ts +58 -0
  103. package/dist/utils/studio.d.ts.map +1 -0
  104. package/dist/utils/studio.js +288 -0
  105. package/dist/utils/studio.js.map +1 -0
  106. package/package.json +33 -15
  107. package/src/bin.ts +11 -104
  108. package/src/commands/{codemod.ts → codemod/v2-to-v3.ts} +21 -28
  109. package/src/commands/compile.ts +35 -25
  110. package/src/commands/create.ts +29 -19
  111. package/src/commands/dev.ts +21 -10
  112. package/src/commands/diff.ts +28 -19
  113. package/src/commands/doctor.ts +20 -11
  114. package/src/commands/explain.ts +20 -10
  115. package/src/commands/generate.ts +81 -90
  116. package/src/commands/info.ts +22 -11
  117. package/src/commands/init.ts +32 -20
  118. package/src/commands/lint.ts +27 -15
  119. package/src/commands/plugin/add.ts +112 -0
  120. package/src/commands/plugin/info.ts +79 -0
  121. package/src/commands/plugin/list.ts +93 -0
  122. package/src/commands/plugin/remove.ts +97 -0
  123. package/src/commands/serve.ts +30 -20
  124. package/src/commands/studio.ts +21 -11
  125. package/src/commands/test.ts +21 -10
  126. package/src/commands/validate.ts +36 -25
  127. package/src/index.ts +20 -12
  128. package/src/utils/format.ts +10 -6
  129. package/src/utils/plugin-helpers.ts +37 -0
  130. package/src/utils/studio.ts +0 -1
  131. package/test/commands.test.ts +76 -37
  132. package/test/plugin-commands.test.ts +42 -160
  133. package/test/plugin.test.ts +19 -23
  134. package/tsconfig.build.json +18 -0
  135. package/bin/objectstack.js +0 -2
  136. package/dist/chunk-CSHQEILI.js +0 -246
  137. package/dist/chunk-Q74JNWKD.js +0 -248
  138. package/dist/config-A7BN6UIT.js +0 -11
  139. package/dist/config-UN34WBHT.js +0 -10
  140. package/src/commands/plugin.ts +0 -372
  141. package/src/utils/plugin-commands.ts +0 -163
@@ -1,9 +1,9 @@
1
1
  // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
2
 
3
- import { Command } from 'commander';
3
+ import { Args, Command, Flags } from '@oclif/core';
4
4
  import chalk from 'chalk';
5
5
  import { ZodError } from 'zod';
6
- import { ObjectStackDefinitionSchema } from '@objectstack/spec';
6
+ import { ObjectStackDefinitionSchema, normalizeStackInput } from '@objectstack/spec';
7
7
  import { loadConfig } from '../utils/config.js';
8
8
  import {
9
9
  printHeader,
@@ -17,52 +17,62 @@ import {
17
17
  printMetadataStats,
18
18
  } from '../utils/format.js';
19
19
 
20
- export const validateCommand = new Command('validate')
21
- .description('Validate ObjectStack configuration against the protocol schema')
22
- .argument('[config]', 'Configuration file path')
23
- .option('--strict', 'Treat warnings as errors')
24
- .option('--json', 'Output results as JSON')
25
- .action(async (configPath, options) => {
20
+ export default class Validate extends Command {
21
+ static override description = 'Validate ObjectStack configuration against the protocol schema';
22
+
23
+ static override args = {
24
+ config: Args.string({ description: 'Configuration file path', required: false }),
25
+ };
26
+
27
+ static override flags = {
28
+ strict: Flags.boolean({ description: 'Treat warnings as errors' }),
29
+ json: Flags.boolean({ description: 'Output results as JSON' }),
30
+ };
31
+
32
+ async run(): Promise<void> {
33
+ const { args, flags } = await this.parse(Validate);
34
+
26
35
  const timer = createTimer();
27
36
 
28
- if (!options.json) {
37
+ if (!flags.json) {
29
38
  printHeader('Validate');
30
39
  }
31
40
 
32
41
  try {
33
42
  // 1. Load configuration
34
- if (!options.json) printStep('Loading configuration...');
35
- const { config, absolutePath, duration } = await loadConfig(configPath);
43
+ if (!flags.json) printStep('Loading configuration...');
44
+ const { config, absolutePath, duration } = await loadConfig(args.config);
36
45
 
37
- if (!options.json) {
46
+ if (!flags.json) {
38
47
  printKV('Config', absolutePath);
39
48
  printKV('Load time', `${duration}ms`);
40
49
  }
41
50
 
42
- // 2. Validate against schema
43
- if (!options.json) printStep('Validating against ObjectStack Protocol...');
44
- const result = ObjectStackDefinitionSchema.safeParse(config);
51
+ // 2. Normalize map-formatted stack definition and validate against schema
52
+ if (!flags.json) printStep('Validating against ObjectStack Protocol...');
53
+ const normalized = normalizeStackInput(config as Record<string, unknown>);
54
+ const result = ObjectStackDefinitionSchema.safeParse(normalized);
45
55
 
46
56
  if (!result.success) {
47
- if (options.json) {
57
+ if (flags.json) {
48
58
  console.log(JSON.stringify({
49
59
  valid: false,
50
60
  errors: (result.error as unknown as ZodError).issues,
51
61
  duration: timer.elapsed(),
52
62
  }, null, 2));
53
- process.exit(1);
63
+ this.exit(1);
54
64
  }
55
65
 
56
66
  console.log('');
57
67
  printError('Validation failed');
58
68
  formatZodErrors(result.error as unknown as ZodError);
59
- process.exit(1);
69
+ this.exit(1);
60
70
  }
61
71
 
62
72
  // 3. Collect and display stats
63
73
  const stats = collectMetadataStats(config);
64
74
 
65
- if (options.json) {
75
+ if (flags.json) {
66
76
  console.log(JSON.stringify({
67
77
  valid: true,
68
78
  manifest: config.manifest,
@@ -108,25 +118,26 @@ export const validateCommand = new Command('validate')
108
118
  for (const w of warnings) {
109
119
  console.log(chalk.yellow(` ⚠ ${w}`));
110
120
  }
111
- if (options.strict) {
121
+ if (flags.strict) {
112
122
  console.log('');
113
123
  printError('Strict mode: warnings treated as errors');
114
- process.exit(1);
124
+ this.exit(1);
115
125
  }
116
126
  }
117
127
 
118
128
  console.log('');
119
129
  } catch (error: any) {
120
- if (options.json) {
130
+ if (flags.json) {
121
131
  console.log(JSON.stringify({
122
132
  valid: false,
123
133
  error: error.message,
124
134
  duration: timer.elapsed(),
125
135
  }, null, 2));
126
- process.exit(1);
136
+ this.exit(1);
127
137
  }
128
138
  console.log('');
129
139
  printError(error.message || String(error));
130
- process.exit(1);
140
+ this.exit(1);
131
141
  }
132
- });
142
+ }
143
+ }
package/src/index.ts CHANGED
@@ -1,14 +1,22 @@
1
1
  // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
2
 
3
- export * from './commands/compile.js';
4
- export * from './commands/validate.js';
5
- export * from './commands/info.js';
6
- export * from './commands/init.js';
7
- export * from './commands/generate.js';
8
- export * from './commands/create.js';
9
- export * from './commands/plugin.js';
10
- export * from './commands/dev.js';
11
- export * from './commands/serve.js';
12
- export * from './commands/test.js';
13
- export * from './commands/doctor.js';
14
- export * from './utils/plugin-commands.js';
3
+ // ─── oclif Command Classes ──────────────────────────────────────────
4
+ // Each command is auto-discovered by oclif from `src/commands/`.
5
+ // These re-exports provide programmatic access for testing and integration.
6
+
7
+ export { default as CompileCommand } from './commands/compile.js';
8
+ export { default as ValidateCommand } from './commands/validate.js';
9
+ export { default as InfoCommand } from './commands/info.js';
10
+ export { default as InitCommand } from './commands/init.js';
11
+ export { default as GenerateCommand } from './commands/generate.js';
12
+ export { default as CreateCommand } from './commands/create.js';
13
+ export { default as DevCommand } from './commands/dev.js';
14
+ export { default as ServeCommand } from './commands/serve.js';
15
+ export { default as TestCommand } from './commands/test.js';
16
+ export { default as DoctorCommand } from './commands/doctor.js';
17
+
18
+ // ─── Plugin topic subcommands ───────────────────────────────────────
19
+ export { default as PluginListCommand } from './commands/plugin/list.js';
20
+ export { default as PluginInfoCommand } from './commands/plugin/info.js';
21
+ export { default as PluginAddCommand } from './commands/plugin/add.js';
22
+ export { default as PluginRemoveCommand } from './commands/plugin/remove.js';
@@ -134,15 +134,19 @@ export interface MetadataStats {
134
134
  }
135
135
 
136
136
  export function collectMetadataStats(config: any): MetadataStats {
137
- const count = (arr: any) => (Array.isArray(arr) ? arr.length : 0);
137
+ const count = (val: any) => {
138
+ if (Array.isArray(val)) return val.length;
139
+ if (val && typeof val === 'object') return Object.keys(val).length;
140
+ return 0;
141
+ };
138
142
 
139
143
  // Count total fields across all objects
140
144
  let fields = 0;
141
- if (Array.isArray(config.objects)) {
142
- for (const obj of config.objects) {
143
- if (obj.fields && typeof obj.fields === 'object') {
144
- fields += Object.keys(obj.fields).length;
145
- }
145
+ const objects = Array.isArray(config.objects) ? config.objects :
146
+ (config.objects && typeof config.objects === 'object' ? Object.values(config.objects) : []);
147
+ for (const obj of objects as any[]) {
148
+ if (obj.fields && typeof obj.fields === 'object') {
149
+ fields += Object.keys(obj.fields).length;
146
150
  }
147
151
  }
148
152
 
@@ -0,0 +1,37 @@
1
+ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
+
3
+ /**
4
+ * Resolve plugin display name from a plugin entry.
5
+ * Plugins can be string package names, objects with `.name`, or class instances.
6
+ */
7
+ export function resolvePluginName(plugin: unknown): string {
8
+ if (typeof plugin === 'string') return plugin;
9
+ if (plugin && typeof plugin === 'object') {
10
+ const p = plugin as Record<string, unknown>;
11
+ if (typeof p.name === 'string') return p.name;
12
+ if (p.constructor && p.constructor.name !== 'Object') return p.constructor.name;
13
+ }
14
+ return 'unknown';
15
+ }
16
+
17
+ /**
18
+ * Resolve plugin version from a plugin entry.
19
+ */
20
+ export function resolvePluginVersion(plugin: unknown): string {
21
+ if (plugin && typeof plugin === 'object') {
22
+ const p = plugin as Record<string, unknown>;
23
+ if (typeof p.version === 'string') return p.version;
24
+ }
25
+ return '-';
26
+ }
27
+
28
+ /**
29
+ * Resolve plugin type from a plugin entry.
30
+ */
31
+ export function resolvePluginType(plugin: unknown): string {
32
+ if (plugin && typeof plugin === 'object') {
33
+ const p = plugin as Record<string, unknown>;
34
+ if (typeof p.type === 'string') return p.type;
35
+ }
36
+ return 'standard';
37
+ }
@@ -236,7 +236,6 @@ export function createStudioProxyPlugin(vitePort: number) {
236
236
  method: c.req.method,
237
237
  headers,
238
238
  body: isBodyAllowed ? c.req.raw.body : undefined,
239
- // @ts-expect-error — duplex required for streaming request body
240
239
  duplex: isBodyAllowed ? 'half' : undefined,
241
240
  });
242
241
 
@@ -1,70 +1,109 @@
1
1
  import { describe, it, expect } from 'vitest';
2
- import { compileCommand } from '../src/commands/compile';
3
- import { serveCommand } from '../src/commands/serve';
4
- import { devCommand } from '../src/commands/dev';
5
- import { doctorCommand } from '../src/commands/doctor';
6
- import { createCommand } from '../src/commands/create';
7
- import { testCommand } from '../src/commands/test';
8
- import { validateCommand } from '../src/commands/validate';
9
- import { initCommand } from '../src/commands/init';
10
- import { infoCommand } from '../src/commands/info';
11
- import { generateCommand } from '../src/commands/generate';
12
- import { pluginCommand } from '../src/commands/plugin';
13
-
14
- describe('CLI Commands', () => {
2
+ import Compile from '../src/commands/compile';
3
+ import Serve from '../src/commands/serve';
4
+ import Dev from '../src/commands/dev';
5
+ import Doctor from '../src/commands/doctor';
6
+ import Create from '../src/commands/create';
7
+ import Test from '../src/commands/test';
8
+ import Validate from '../src/commands/validate';
9
+ import Init from '../src/commands/init';
10
+ import Info from '../src/commands/info';
11
+ import Generate from '../src/commands/generate';
12
+ import Lint from '../src/commands/lint';
13
+ import Diff from '../src/commands/diff';
14
+ import Explain from '../src/commands/explain';
15
+ import Studio from '../src/commands/studio';
16
+ import PluginList from '../src/commands/plugin/list';
17
+ import PluginInfo from '../src/commands/plugin/info';
18
+ import PluginAdd from '../src/commands/plugin/add';
19
+ import PluginRemove from '../src/commands/plugin/remove';
20
+ import V2ToV3 from '../src/commands/codemod/v2-to-v3';
21
+
22
+ describe('CLI Commands (oclif)', () => {
15
23
  it('should have compile command', () => {
16
- expect(compileCommand.name()).toBe('compile');
17
- expect(compileCommand.description()).toContain('Compile');
24
+ expect(Compile.description).toContain('Compile');
18
25
  });
19
26
 
20
27
  it('should have serve command', () => {
21
- expect(serveCommand.name()).toBe('serve');
22
- expect(serveCommand.description()).toContain('server');
28
+ expect(Serve.description).toContain('server');
23
29
  });
24
30
 
25
31
  it('should have dev command', () => {
26
- expect(devCommand.name()).toBe('dev');
27
- expect(devCommand.description()).toContain('development mode');
32
+ expect(Dev.description).toContain('development mode');
28
33
  });
29
34
 
30
35
  it('should have doctor command', () => {
31
- expect(doctorCommand.name()).toBe('doctor');
32
- expect(doctorCommand.description()).toContain('health');
36
+ expect(Doctor.description).toContain('health');
33
37
  });
34
38
 
35
39
  it('should have create command', () => {
36
- expect(createCommand.name()).toBe('create');
37
- expect(createCommand.description()).toContain('Create');
40
+ expect(Create.description).toContain('Create');
38
41
  });
39
42
 
40
43
  it('should have test command', () => {
41
- expect(testCommand.name()).toBe('test');
42
- expect(testCommand.description()).toContain('Quality Protocol');
44
+ expect(Test.description).toContain('Quality Protocol');
43
45
  });
44
46
 
45
47
  it('should have validate command', () => {
46
- expect(validateCommand.name()).toBe('validate');
47
- expect(validateCommand.description()).toContain('Validate');
48
+ expect(Validate.description).toContain('Validate');
48
49
  });
49
50
 
50
51
  it('should have init command', () => {
51
- expect(initCommand.name()).toBe('init');
52
- expect(initCommand.description()).toContain('Initialize');
52
+ expect(Init.description).toContain('Initialize');
53
53
  });
54
54
 
55
55
  it('should have info command', () => {
56
- expect(infoCommand.name()).toBe('info');
57
- expect(infoCommand.description()).toContain('summary');
56
+ expect(Info.description).toContain('summary');
58
57
  });
59
58
 
60
59
  it('should have generate command with alias', () => {
61
- expect(generateCommand.name()).toBe('generate');
62
- expect(generateCommand.alias()).toBe('g');
63
- expect(generateCommand.description()).toContain('Generate');
60
+ expect(Generate.aliases).toContain('g');
61
+ expect(Generate.description).toContain('Generate');
62
+ });
63
+
64
+ it('should have lint command', () => {
65
+ expect(Lint.description).toContain('style');
66
+ });
67
+
68
+ it('should have diff command', () => {
69
+ expect(Diff.description).toContain('Compare');
70
+ });
71
+
72
+ it('should have explain command', () => {
73
+ expect(Explain.description).toContain('explanation');
74
+ });
75
+
76
+ it('should have studio command', () => {
77
+ expect(Studio.description).toContain('Studio');
78
+ });
79
+
80
+ it('should have codemod v2-to-v3 command', () => {
81
+ expect(V2ToV3.description).toContain('v2');
64
82
  });
65
83
 
66
- it('should have plugin command with subcommands', () => {
67
- expect(pluginCommand.name()).toBe('plugin');
68
- expect(pluginCommand.description()).toContain('plugin');
84
+ describe('Plugin subcommands', () => {
85
+ it('should have plugin list command', () => {
86
+ expect(PluginList.description).toContain('List');
87
+ });
88
+
89
+ it('should have plugin info command', () => {
90
+ expect(PluginInfo.description).toContain('information');
91
+ });
92
+
93
+ it('should have plugin add command', () => {
94
+ expect(PluginAdd.description).toContain('Add');
95
+ });
96
+
97
+ it('should have plugin remove command', () => {
98
+ expect(PluginRemove.description).toContain('Remove');
99
+ });
100
+
101
+ it('should have plugin remove alias', () => {
102
+ expect(PluginRemove.aliases).toContain('plugin rm');
103
+ });
104
+
105
+ it('should have plugin list alias', () => {
106
+ expect(PluginList.aliases).toContain('plugin ls');
107
+ });
69
108
  });
70
109
  });
@@ -1,162 +1,44 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
- import { Command } from 'commander';
3
- import { loadPluginCommands } from '../src/utils/plugin-commands';
4
-
5
- // Mock the config loader
6
- vi.mock('../src/utils/config.js', () => ({
7
- loadConfig: vi.fn(),
8
- }));
9
-
10
- import { loadConfig } from '../src/utils/config';
11
-
12
- const mockedLoadConfig = vi.mocked(loadConfig);
13
-
14
- describe('loadPluginCommands', () => {
15
- let program: Command;
16
-
17
- beforeEach(() => {
18
- program = new Command();
19
- program.name('objectstack');
20
- vi.clearAllMocks();
21
- });
22
-
23
- it('should do nothing when no config file exists', async () => {
24
- mockedLoadConfig.mockRejectedValue(new Error('No config found'));
25
-
26
- await loadPluginCommands(program);
27
- expect(program.commands).toHaveLength(0);
28
- });
29
-
30
- it('should do nothing when no plugins have command contributions', async () => {
31
- mockedLoadConfig.mockResolvedValue({
32
- config: {
33
- plugins: [
34
- { name: 'simple-plugin' },
35
- ],
36
- },
37
- absolutePath: '/test/objectstack.config.ts',
38
- duration: 10,
39
- });
40
-
41
- await loadPluginCommands(program);
42
- expect(program.commands).toHaveLength(0);
43
- });
44
-
45
- it('should do nothing when plugins array is empty', async () => {
46
- mockedLoadConfig.mockResolvedValue({
47
- config: {
48
- plugins: [],
49
- },
50
- absolutePath: '/test/objectstack.config.ts',
51
- duration: 10,
52
- });
53
-
54
- await loadPluginCommands(program);
55
- expect(program.commands).toHaveLength(0);
56
- });
57
-
58
- it('should do nothing when config has no plugins key', async () => {
59
- mockedLoadConfig.mockResolvedValue({
60
- config: {},
61
- absolutePath: '/test/objectstack.config.ts',
62
- duration: 10,
63
- });
64
-
65
- await loadPluginCommands(program);
66
- expect(program.commands).toHaveLength(0);
67
- });
68
-
69
- it('should detect command contributions from plugin manifest', async () => {
70
- // This test verifies that the contribution detection logic works,
71
- // even though the dynamic import will fail (no real module to load)
72
- mockedLoadConfig.mockResolvedValue({
73
- config: {
74
- plugins: [
75
- {
76
- name: '@acme/plugin-marketplace',
77
- manifest: {
78
- contributes: {
79
- commands: [
80
- {
81
- name: 'marketplace',
82
- description: 'Manage marketplace apps',
83
- module: './cli',
84
- },
85
- ],
86
- },
87
- },
88
- },
89
- ],
90
- },
91
- absolutePath: '/test/objectstack.config.ts',
92
- duration: 10,
93
- });
94
-
95
- // loadPluginCommands will try to import the module and fail silently
96
- // (since no real module exists), but it should not throw
97
- await loadPluginCommands(program);
98
- });
99
-
100
- it('should detect contributions from top-level contributes field', async () => {
101
- mockedLoadConfig.mockResolvedValue({
102
- config: {
103
- plugins: [
104
- {
105
- name: '@acme/plugin-deploy',
106
- contributes: {
107
- commands: [
108
- {
109
- name: 'deploy',
110
- description: 'Deploy to cloud',
111
- },
112
- ],
113
- },
114
- },
115
- ],
116
- },
117
- absolutePath: '/test/objectstack.config.ts',
118
- duration: 10,
119
- });
120
-
121
- // Should not throw even if import fails
122
- await loadPluginCommands(program);
123
- });
124
-
125
- it('should skip non-object plugins', async () => {
126
- mockedLoadConfig.mockResolvedValue({
127
- config: {
128
- plugins: [
129
- 'some-string-plugin',
130
- null,
131
- undefined,
132
- 42,
133
- ],
134
- },
135
- absolutePath: '/test/objectstack.config.ts',
136
- duration: 10,
137
- });
138
-
139
- await loadPluginCommands(program);
140
- expect(program.commands).toHaveLength(0);
141
- });
142
-
143
- it('should handle plugins with non-array commands gracefully', async () => {
144
- mockedLoadConfig.mockResolvedValue({
145
- config: {
146
- plugins: [
147
- {
148
- name: '@acme/bad-plugin',
149
- contributes: {
150
- commands: 'not-an-array',
151
- },
152
- },
153
- ],
154
- },
155
- absolutePath: '/test/objectstack.config.ts',
156
- duration: 10,
157
- });
158
-
159
- await loadPluginCommands(program);
160
- expect(program.commands).toHaveLength(0);
1
+ import { describe, it, expect } from 'vitest';
2
+
3
+ /**
4
+ * The custom loadPluginCommands mechanism has been removed.
5
+ * Plugin command extension is now handled by oclif's built-in plugin system.
6
+ *
7
+ * Plugins extend the CLI by:
8
+ * 1. Including `oclif` config in their package.json
9
+ * 2. Exporting oclif Command classes from `src/commands/`
10
+ * 3. Being installed via `os plugins install <package>`
11
+ *
12
+ * The objectstack.config.ts no longer determines CLI command availability.
13
+ */
14
+
15
+ describe('oclif Plugin System', () => {
16
+ it('should have oclif plugins configured in package.json', async () => {
17
+ const { createRequire } = await import('module');
18
+ const require = createRequire(import.meta.url);
19
+ const pkg = require('../package.json');
20
+
21
+ expect(pkg.oclif).toBeDefined();
22
+ expect(pkg.oclif.plugins).toContain('@oclif/plugin-help');
23
+ expect(pkg.oclif.plugins).toContain('@oclif/plugin-plugins');
24
+ });
25
+
26
+ it('should have oclif command discovery configured', async () => {
27
+ const { createRequire } = await import('module');
28
+ const require = createRequire(import.meta.url);
29
+ const pkg = require('../package.json');
30
+
31
+ expect(pkg.oclif.commands).toBeDefined();
32
+ expect(pkg.oclif.commands.strategy).toBe('pattern');
33
+ expect(pkg.oclif.commands.target).toBe('./dist/commands');
34
+ });
35
+
36
+ it('should have bin entries pointing to oclif runner', async () => {
37
+ const { createRequire } = await import('module');
38
+ const require = createRequire(import.meta.url);
39
+ const pkg = require('../package.json');
40
+
41
+ expect(pkg.bin.os).toBe('./bin/run.js');
42
+ expect(pkg.bin.objectstack).toBe('./bin/run.js');
161
43
  });
162
44
  });
@@ -1,39 +1,35 @@
1
1
  import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
- import { pluginCommand } from '../src/commands/plugin';
2
+ import PluginList from '../src/commands/plugin/list';
3
+ import PluginInfo from '../src/commands/plugin/info';
4
+ import PluginAdd from '../src/commands/plugin/add';
5
+ import PluginRemove from '../src/commands/plugin/remove';
3
6
  import fs from 'fs';
4
7
  import path from 'path';
5
8
  import os from 'os';
6
9
 
7
- describe('Plugin Command', () => {
8
- it('should have plugin command with subcommands', () => {
9
- expect(pluginCommand.name()).toBe('plugin');
10
- expect(pluginCommand.description()).toContain('plugin');
10
+ describe('Plugin Commands (oclif)', () => {
11
+ it('should have plugin list command', () => {
12
+ expect(PluginList.description).toContain('List');
11
13
  });
12
14
 
13
- it('should have list subcommand with alias', () => {
14
- const list = pluginCommand.commands.find(c => c.name() === 'list');
15
- expect(list).toBeDefined();
16
- expect(list!.alias()).toBe('ls');
17
- expect(list!.description()).toContain('List');
15
+ it('should have plugin list alias', () => {
16
+ expect(PluginList.aliases).toContain('plugin ls');
18
17
  });
19
18
 
20
- it('should have info subcommand', () => {
21
- const info = pluginCommand.commands.find(c => c.name() === 'info');
22
- expect(info).toBeDefined();
23
- expect(info!.description()).toContain('information');
19
+ it('should have plugin info command', () => {
20
+ expect(PluginInfo.description).toContain('information');
24
21
  });
25
22
 
26
- it('should have add subcommand', () => {
27
- const add = pluginCommand.commands.find(c => c.name() === 'add');
28
- expect(add).toBeDefined();
29
- expect(add!.description()).toContain('Add');
23
+ it('should have plugin add command', () => {
24
+ expect(PluginAdd.description).toContain('Add');
30
25
  });
31
26
 
32
- it('should have remove subcommand with alias', () => {
33
- const remove = pluginCommand.commands.find(c => c.name() === 'remove');
34
- expect(remove).toBeDefined();
35
- expect(remove!.alias()).toBe('rm');
36
- expect(remove!.description()).toContain('Remove');
27
+ it('should have plugin remove command', () => {
28
+ expect(PluginRemove.description).toContain('Remove');
29
+ });
30
+
31
+ it('should have plugin remove alias', () => {
32
+ expect(PluginRemove.aliases).toContain('plugin rm');
37
33
  });
38
34
  });
39
35