@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.
- package/.turbo/turbo-build.log +7 -7
- package/CHANGELOG.md +27 -0
- package/README.md +88 -3
- package/dist/bin.js +361 -19
- package/dist/index.d.ts +14 -1
- package/dist/index.js +422 -88
- package/package.json +9 -9
- package/src/bin.ts +18 -1
- package/src/commands/plugin.ts +372 -0
- package/src/index.ts +2 -0
- package/src/utils/plugin-commands.ts +163 -0
- package/test/commands.test.ts +6 -0
- package/test/plugin-commands.test.ts +162 -0
- package/test/plugin.test.ts +173 -0
|
@@ -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
|
+
});
|