@objectstack/cli 2.0.5 → 2.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.
@@ -0,0 +1,162 @@
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);
161
+ });
162
+ });
@@ -0,0 +1,173 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { pluginCommand } from '../src/commands/plugin';
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+ import os from 'os';
6
+
7
+ describe('Plugin Command', () => {
8
+ it('should have plugin command with subcommands', () => {
9
+ expect(pluginCommand.name()).toBe('plugin');
10
+ expect(pluginCommand.description()).toContain('plugin');
11
+ });
12
+
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');
18
+ });
19
+
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');
24
+ });
25
+
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');
30
+ });
31
+
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');
37
+ });
38
+ });
39
+
40
+ describe('Plugin Config Manipulation', () => {
41
+ let tmpDir: string;
42
+ let configPath: string;
43
+
44
+ beforeEach(() => {
45
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'os-plugin-test-'));
46
+ configPath = path.join(tmpDir, 'objectstack.config.ts');
47
+ });
48
+
49
+ afterEach(() => {
50
+ fs.rmSync(tmpDir, { recursive: true, force: true });
51
+ });
52
+
53
+ describe('addPluginToConfig (via file manipulation)', () => {
54
+ it('should add plugin import and entry to config with existing plugins array', () => {
55
+ fs.writeFileSync(configPath, `import { defineStack } from '@objectstack/spec';
56
+
57
+ export default defineStack({
58
+ manifest: {
59
+ name: 'test-app',
60
+ version: '1.0.0',
61
+ },
62
+ plugins: [
63
+ ],
64
+ });
65
+ `);
66
+
67
+ // Simulate what the add command does
68
+ let content = fs.readFileSync(configPath, 'utf-8');
69
+ const packageName = '@objectstack/plugin-auth';
70
+ const varName = 'authPlugin';
71
+ const importLine = `import ${varName} from '${packageName}';\n`;
72
+
73
+ // Add import after last import
74
+ const importRegex = /^import .+$/gm;
75
+ let lastImportEnd = 0;
76
+ let match: RegExpExecArray | null;
77
+ while ((match = importRegex.exec(content)) !== null) {
78
+ lastImportEnd = match.index + match[0].length;
79
+ }
80
+ content = content.slice(0, lastImportEnd) + '\n' + importLine + content.slice(lastImportEnd);
81
+
82
+ // Add to plugins array
83
+ content = content.replace(
84
+ /(plugins\s*:\s*\[)/,
85
+ `$1\n ${varName},`
86
+ );
87
+
88
+ fs.writeFileSync(configPath, content);
89
+
90
+ const result = fs.readFileSync(configPath, 'utf-8');
91
+ expect(result).toContain("import authPlugin from '@objectstack/plugin-auth'");
92
+ expect(result).toContain('authPlugin,');
93
+ });
94
+
95
+ it('should add plugin to config without existing plugins array', () => {
96
+ fs.writeFileSync(configPath, `import { defineStack } from '@objectstack/spec';
97
+
98
+ export default defineStack({
99
+ manifest: {
100
+ name: 'test-app',
101
+ version: '1.0.0',
102
+ },
103
+ });
104
+ `);
105
+
106
+ let content = fs.readFileSync(configPath, 'utf-8');
107
+ const packageName = '@objectstack/plugin-security';
108
+ const varName = 'securityPlugin';
109
+ const importLine = `import ${varName} from '${packageName}';\n`;
110
+
111
+ // Add import
112
+ const importRegex = /^import .+$/gm;
113
+ let lastImportEnd = 0;
114
+ let match: RegExpExecArray | null;
115
+ while ((match = importRegex.exec(content)) !== null) {
116
+ lastImportEnd = match.index + match[0].length;
117
+ }
118
+ content = content.slice(0, lastImportEnd) + '\n' + importLine + content.slice(lastImportEnd);
119
+
120
+ // Add plugins array
121
+ if (!/plugins\s*:\s*\[/.test(content)) {
122
+ content = content.replace(
123
+ /(defineStack\(\{[\s\S]*?)(}\s*\))/,
124
+ `$1 plugins: [\n ${varName},\n ],\n$2`
125
+ );
126
+ }
127
+
128
+ fs.writeFileSync(configPath, content);
129
+
130
+ const result = fs.readFileSync(configPath, 'utf-8');
131
+ expect(result).toContain("import securityPlugin from '@objectstack/plugin-security'");
132
+ expect(result).toContain('plugins: [');
133
+ expect(result).toContain('securityPlugin,');
134
+ });
135
+ });
136
+
137
+ describe('removePluginFromConfig (via file manipulation)', () => {
138
+ it('should remove plugin import and entry from config', () => {
139
+ fs.writeFileSync(configPath, `import { defineStack } from '@objectstack/spec';
140
+ import authPlugin from '@objectstack/plugin-auth';
141
+
142
+ export default defineStack({
143
+ manifest: {
144
+ name: 'test-app',
145
+ version: '1.0.0',
146
+ },
147
+ plugins: [
148
+ authPlugin,
149
+ ],
150
+ });
151
+ `);
152
+
153
+ let content = fs.readFileSync(configPath, 'utf-8');
154
+ const packageName = '@objectstack/plugin-auth';
155
+
156
+ // Remove import
157
+ const importRegex = new RegExp(`^import .+['"]${packageName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}['"].*$\\n?`, 'gm');
158
+ content = content.replace(importRegex, '');
159
+
160
+ // Remove from plugins array
161
+ content = content.replace(/\s*authPlugin,?\n?/g, '\n');
162
+
163
+ fs.writeFileSync(configPath, content);
164
+
165
+ const result = fs.readFileSync(configPath, 'utf-8');
166
+ expect(result).not.toContain('@objectstack/plugin-auth');
167
+ expect(result).not.toContain('authPlugin');
168
+ // The rest of the config should remain intact
169
+ expect(result).toContain("import { defineStack } from '@objectstack/spec'");
170
+ expect(result).toContain('manifest');
171
+ });
172
+ });
173
+ });