@nexical/cli 0.11.17 → 0.11.19
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/dist/chunk-54HY52LH.js +38 -0
- package/dist/chunk-54HY52LH.js.map +1 -0
- package/dist/index.js +6 -23
- package/dist/index.js.map +1 -1
- package/dist/src/commands/prompt.js +10 -240
- package/dist/src/commands/prompt.js.map +1 -1
- package/dist/src/utils/filter.d.ts +9 -0
- package/dist/src/utils/filter.js +9 -0
- package/dist/src/utils/filter.js.map +1 -0
- package/index.ts +2 -31
- package/package.json +2 -4
- package/src/commands/prompt.ts +10 -273
- package/src/utils/filter.ts +47 -0
- package/test/integration/commands/prompt.integration.test.ts +110 -0
- package/test/unit/commands/prompt.test.ts +257 -0
- package/test/unit/utils/filter.test.ts +40 -0
- package/vitest.config.ts +1 -1
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import PromptCommand from '../../../src/commands/prompt.js';
|
|
3
|
+
import fs from 'fs-extra';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import YAML from 'yaml';
|
|
6
|
+
import { PromptRunner } from '@nexical/ai';
|
|
7
|
+
import { logger } from '@nexical/cli-core';
|
|
8
|
+
|
|
9
|
+
vi.mock('@nexical/cli-core', async (importOriginal) => {
|
|
10
|
+
const mod = await importOriginal<typeof import('@nexical/cli-core')>();
|
|
11
|
+
return {
|
|
12
|
+
...mod,
|
|
13
|
+
logger: {
|
|
14
|
+
code: vi.fn(),
|
|
15
|
+
debug: vi.fn(),
|
|
16
|
+
error: vi.fn(),
|
|
17
|
+
success: vi.fn(),
|
|
18
|
+
info: vi.fn(),
|
|
19
|
+
warn: vi.fn(),
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
vi.mock('fs-extra');
|
|
25
|
+
vi.mock('yaml');
|
|
26
|
+
vi.mock('@nexical/ai');
|
|
27
|
+
|
|
28
|
+
describe('PromptCommand', () => {
|
|
29
|
+
let command: PromptCommand;
|
|
30
|
+
const projectRoot = '/test/project';
|
|
31
|
+
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
vi.clearAllMocks();
|
|
34
|
+
command = new PromptCommand({});
|
|
35
|
+
(command as unknown as { projectRoot: string }).projectRoot = projectRoot;
|
|
36
|
+
|
|
37
|
+
vi.spyOn(command, 'error').mockImplementation(() => {});
|
|
38
|
+
|
|
39
|
+
// Default fs mocks
|
|
40
|
+
vi.spyOn(fs, 'pathExists').mockResolvedValue(false);
|
|
41
|
+
vi.spyOn(fs, 'readFile').mockResolvedValue('');
|
|
42
|
+
|
|
43
|
+
// Default PromptRunner mock
|
|
44
|
+
vi.mocked(PromptRunner.run).mockResolvedValue(0);
|
|
45
|
+
|
|
46
|
+
// Mock process.exit
|
|
47
|
+
vi.spyOn(process, 'exit').mockImplementation((code) => {
|
|
48
|
+
throw new Error(`Process.exit(${code})`);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
afterEach(() => {
|
|
53
|
+
vi.resetAllMocks();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should have correct metadata', () => {
|
|
57
|
+
expect(PromptCommand.usage).toBeDefined();
|
|
58
|
+
expect(PromptCommand.description).toBeDefined();
|
|
59
|
+
expect(PromptCommand.requiresProject).toBe(true);
|
|
60
|
+
expect(PromptCommand.args).toBeDefined();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should run prompt with default options', async () => {
|
|
64
|
+
await command.run({ promptName: 'test-prompt' });
|
|
65
|
+
|
|
66
|
+
expect(PromptRunner.run).toHaveBeenCalledWith(
|
|
67
|
+
expect.objectContaining({
|
|
68
|
+
promptName: 'test-prompt',
|
|
69
|
+
models: ['gemini-3-flash-preview', 'gemini-3-pro-preview'],
|
|
70
|
+
interactive: false,
|
|
71
|
+
}),
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should handle interactive flag', async () => {
|
|
76
|
+
await command.run({ promptName: 'test-prompt', interactive: true });
|
|
77
|
+
expect(PromptRunner.run).toHaveBeenCalledWith(
|
|
78
|
+
expect.objectContaining({
|
|
79
|
+
interactive: true,
|
|
80
|
+
}),
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
vi.clearAllMocks();
|
|
84
|
+
await command.run({ promptName: 'test-prompt', i: true });
|
|
85
|
+
expect(PromptRunner.run).toHaveBeenCalledWith(
|
|
86
|
+
expect.objectContaining({
|
|
87
|
+
interactive: true,
|
|
88
|
+
}),
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
vi.clearAllMocks();
|
|
92
|
+
await command.run({ promptName: 'test-prompt', args: ['--interactive'] });
|
|
93
|
+
expect(PromptRunner.run).toHaveBeenCalledWith(
|
|
94
|
+
expect.objectContaining({
|
|
95
|
+
interactive: true,
|
|
96
|
+
}),
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should handle custom models', async () => {
|
|
101
|
+
await command.run({ promptName: 'test-prompt', models: 'model1, model2 ' });
|
|
102
|
+
expect(PromptRunner.run).toHaveBeenCalledWith(
|
|
103
|
+
expect.objectContaining({
|
|
104
|
+
models: ['model1', 'model2'],
|
|
105
|
+
}),
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should include generator agents prompts if they exist', async () => {
|
|
110
|
+
vi.spyOn(fs, 'pathExists').mockImplementation(async (p: string | Buffer | URL) => {
|
|
111
|
+
return (p as string).includes('packages/generator/prompts/agents');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
await command.run({ promptName: 'test-prompt' });
|
|
115
|
+
|
|
116
|
+
const call = vi.mocked(PromptRunner.run).mock.calls[0][0];
|
|
117
|
+
expect(call?.promptDirs).toHaveLength(2);
|
|
118
|
+
expect(call?.promptDirs).toContain(path.join(projectRoot, 'prompts'));
|
|
119
|
+
expect(call?.promptDirs).toContain(path.join(projectRoot, 'packages/generator/prompts/agents'));
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('should resolve frontend module context', async () => {
|
|
123
|
+
vi.spyOn(fs, 'pathExists').mockImplementation(async (p: string | Buffer | URL) => {
|
|
124
|
+
return (p as string).includes('apps/frontend/modules/test-module');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
await command.run({ promptName: 'test-prompt', module: 'test-module' });
|
|
128
|
+
|
|
129
|
+
expect(PromptRunner.run).toHaveBeenCalledWith(
|
|
130
|
+
expect.objectContaining({
|
|
131
|
+
args: expect.objectContaining({
|
|
132
|
+
module_name: 'test-module',
|
|
133
|
+
module_type: 'frontend',
|
|
134
|
+
module_root: path.join(projectRoot, 'apps/frontend/modules/test-module'),
|
|
135
|
+
}),
|
|
136
|
+
}),
|
|
137
|
+
);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('should resolve backend module context', async () => {
|
|
141
|
+
vi.spyOn(fs, 'pathExists').mockImplementation(async (p: string | Buffer | URL) => {
|
|
142
|
+
return (p as string).includes('apps/backend/modules/test-module');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
await command.run({ promptName: 'test-prompt', m: 'test-module' });
|
|
146
|
+
|
|
147
|
+
expect(PromptRunner.run).toHaveBeenCalledWith(
|
|
148
|
+
expect.objectContaining({
|
|
149
|
+
args: expect.objectContaining({
|
|
150
|
+
module_name: 'test-module',
|
|
151
|
+
module_type: 'backend',
|
|
152
|
+
module_root: path.join(projectRoot, 'apps/backend/modules/test-module'),
|
|
153
|
+
}),
|
|
154
|
+
}),
|
|
155
|
+
);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('should fail if module not found', async () => {
|
|
159
|
+
vi.mocked(fs.pathExists).mockResolvedValue(false);
|
|
160
|
+
|
|
161
|
+
await command.run({ promptName: 'test-prompt', module: 'missing-module' });
|
|
162
|
+
|
|
163
|
+
expect(command.error).toHaveBeenCalledWith(
|
|
164
|
+
expect.stringContaining("Module 'missing-module' not found"),
|
|
165
|
+
);
|
|
166
|
+
expect(PromptRunner.run).not.toHaveBeenCalled();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('should load AI config from nexical.yaml', async () => {
|
|
170
|
+
vi.spyOn(fs, 'pathExists').mockImplementation(async (p: string | Buffer | URL) =>
|
|
171
|
+
(p as string).includes('nexical.yaml'),
|
|
172
|
+
);
|
|
173
|
+
vi.spyOn(fs, 'readFile').mockResolvedValue('ai:\n provider: vertex');
|
|
174
|
+
vi.mocked(YAML.parse).mockReturnValue({ ai: { provider: 'vertex' } });
|
|
175
|
+
|
|
176
|
+
await command.run({ promptName: 'test-prompt' });
|
|
177
|
+
|
|
178
|
+
expect(PromptRunner.run).toHaveBeenCalledWith(
|
|
179
|
+
expect.objectContaining({
|
|
180
|
+
aiConfig: { provider: 'vertex' },
|
|
181
|
+
}),
|
|
182
|
+
);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('should handle missing AI config in nexical.yaml', async () => {
|
|
186
|
+
vi.spyOn(fs, 'pathExists').mockImplementation(async (p: string | Buffer | URL) =>
|
|
187
|
+
(p as string).includes('nexical.yaml'),
|
|
188
|
+
);
|
|
189
|
+
vi.spyOn(fs, 'readFile').mockResolvedValue('name: my-project');
|
|
190
|
+
vi.mocked(YAML.parse).mockReturnValue({ name: 'my-project' });
|
|
191
|
+
|
|
192
|
+
await command.run({ promptName: 'test-prompt' });
|
|
193
|
+
|
|
194
|
+
expect(PromptRunner.run).toHaveBeenCalledWith(
|
|
195
|
+
expect.objectContaining({
|
|
196
|
+
aiConfig: {},
|
|
197
|
+
}),
|
|
198
|
+
);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('should handle falsy YAML parse result', async () => {
|
|
202
|
+
vi.spyOn(fs, 'pathExists').mockImplementation(async (p: string | Buffer | URL) =>
|
|
203
|
+
(p as string).includes('nexical.yaml'),
|
|
204
|
+
);
|
|
205
|
+
vi.spyOn(fs, 'readFile').mockResolvedValue('');
|
|
206
|
+
vi.mocked(YAML.parse).mockReturnValue(null);
|
|
207
|
+
|
|
208
|
+
await command.run({ promptName: 'test-prompt' });
|
|
209
|
+
|
|
210
|
+
expect(PromptRunner.run).toHaveBeenCalledWith(
|
|
211
|
+
expect.objectContaining({
|
|
212
|
+
aiConfig: {},
|
|
213
|
+
}),
|
|
214
|
+
);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('should handle nexical.yaml parse errors', async () => {
|
|
218
|
+
vi.spyOn(fs, 'pathExists').mockImplementation(async (p: string | Buffer | URL) =>
|
|
219
|
+
(p as string).includes('nexical.yaml'),
|
|
220
|
+
);
|
|
221
|
+
vi.spyOn(fs, 'readFile').mockResolvedValue('invalid: yaml: :');
|
|
222
|
+
vi.mocked(YAML.parse).mockImplementation(() => {
|
|
223
|
+
throw new Error('parse error');
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
await command.run({ promptName: 'test-prompt' });
|
|
227
|
+
|
|
228
|
+
expect(logger.warn).toHaveBeenCalledWith(
|
|
229
|
+
expect.stringContaining('Failed to parse nexical.yaml'),
|
|
230
|
+
);
|
|
231
|
+
expect(PromptRunner.run).toHaveBeenCalledWith(
|
|
232
|
+
expect.objectContaining({
|
|
233
|
+
aiConfig: {},
|
|
234
|
+
}),
|
|
235
|
+
);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('should exit with error code if PromptRunner fails', async () => {
|
|
239
|
+
vi.mocked(PromptRunner.run).mockResolvedValue(1);
|
|
240
|
+
|
|
241
|
+
await expect(command.run({ promptName: 'test-prompt' })).rejects.toThrow('Process.exit(1)');
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('should set default root_path if not provided', async () => {
|
|
245
|
+
await command.run({ promptName: 'test-prompt' });
|
|
246
|
+
|
|
247
|
+
const call = vi.mocked(PromptRunner.run).mock.calls[0][0];
|
|
248
|
+
expect(call?.args?.root_path).toBe(process.cwd() + '/');
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('should use provided root_path from args', async () => {
|
|
252
|
+
await command.run({ promptName: 'test-prompt', args: ['--root_path', '/custom/path/'] });
|
|
253
|
+
|
|
254
|
+
const call = vi.mocked(PromptRunner.run).mock.calls[0][0];
|
|
255
|
+
expect(call?.args?.root_path).toBe('/custom/path/');
|
|
256
|
+
});
|
|
257
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { filterCommandDirectories } from '../../../src/utils/filter.js';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
describe('filterCommandDirectories', () => {
|
|
6
|
+
const coreDir = path.resolve('/test/packages/cli/src/commands');
|
|
7
|
+
|
|
8
|
+
it('should filter out the core commands directory itself', () => {
|
|
9
|
+
const dirs = [coreDir, path.resolve('/test/other')];
|
|
10
|
+
const filtered = filterCommandDirectories(dirs, coreDir);
|
|
11
|
+
expect(filtered).toEqual([path.resolve('/test/other')]);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('should filter out default core suffixes', () => {
|
|
15
|
+
const dirs = [
|
|
16
|
+
path.join('/some/path', '@nexical', 'cli', 'dist', 'src', 'commands'),
|
|
17
|
+
path.join('/another/path', 'packages', 'cli', 'dist', 'src', 'commands'),
|
|
18
|
+
path.join('/yet/another', 'packages', 'cli', 'src', 'commands'),
|
|
19
|
+
path.resolve('/test/valid'),
|
|
20
|
+
];
|
|
21
|
+
const filtered = filterCommandDirectories(dirs, coreDir);
|
|
22
|
+
expect(filtered).toEqual([path.resolve('/test/valid')]);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should handle dist/src mismatch and filter src version', () => {
|
|
26
|
+
const base = '/another/project';
|
|
27
|
+
const distCore = path.join(base, 'dist', 'src', 'commands');
|
|
28
|
+
const srcCore = path.join(base, 'src', 'commands');
|
|
29
|
+
|
|
30
|
+
const dirs = [srcCore, path.resolve('/test/other')];
|
|
31
|
+
const filtered = filterCommandDirectories(dirs, distCore);
|
|
32
|
+
expect(filtered).toEqual([path.resolve('/test/other')]);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should not filter out non-matching directories', () => {
|
|
36
|
+
const dirs = [path.resolve('/test/modules/mod1/src/commands')];
|
|
37
|
+
const filtered = filterCommandDirectories(dirs, coreDir);
|
|
38
|
+
expect(filtered).toEqual(dirs);
|
|
39
|
+
});
|
|
40
|
+
});
|
package/vitest.config.ts
CHANGED