@nexical/cli 0.11.8 → 0.11.9

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 (42) hide show
  1. package/dist/index.js +1 -1
  2. package/dist/index.js.map +1 -1
  3. package/dist/src/commands/init.js +3 -3
  4. package/dist/src/commands/module/add.js +53 -22
  5. package/dist/src/commands/module/add.js.map +1 -1
  6. package/dist/src/commands/module/list.d.ts +1 -0
  7. package/dist/src/commands/module/list.js +54 -45
  8. package/dist/src/commands/module/list.js.map +1 -1
  9. package/dist/src/commands/module/remove.js +37 -12
  10. package/dist/src/commands/module/remove.js.map +1 -1
  11. package/dist/src/commands/module/update.js +15 -3
  12. package/dist/src/commands/module/update.js.map +1 -1
  13. package/dist/src/commands/run.js +18 -1
  14. package/dist/src/commands/run.js.map +1 -1
  15. package/package.json +1 -1
  16. package/src/commands/module/add.ts +74 -31
  17. package/src/commands/module/list.ts +80 -57
  18. package/src/commands/module/remove.ts +50 -14
  19. package/src/commands/module/update.ts +19 -5
  20. package/src/commands/run.ts +21 -1
  21. package/test/e2e/lifecycle.e2e.test.ts +3 -2
  22. package/test/integration/commands/deploy.integration.test.ts +102 -0
  23. package/test/integration/commands/init.integration.test.ts +16 -1
  24. package/test/integration/commands/module.integration.test.ts +81 -55
  25. package/test/integration/commands/run.integration.test.ts +69 -74
  26. package/test/integration/commands/setup.integration.test.ts +53 -0
  27. package/test/unit/commands/deploy.test.ts +285 -0
  28. package/test/unit/commands/init.test.ts +15 -0
  29. package/test/unit/commands/module/add.test.ts +363 -254
  30. package/test/unit/commands/module/list.test.ts +100 -99
  31. package/test/unit/commands/module/remove.test.ts +143 -58
  32. package/test/unit/commands/module/update.test.ts +45 -62
  33. package/test/unit/commands/run.test.ts +16 -1
  34. package/test/unit/commands/setup.test.ts +25 -66
  35. package/test/unit/deploy/config-manager.test.ts +65 -0
  36. package/test/unit/deploy/providers/cloudflare.test.ts +210 -0
  37. package/test/unit/deploy/providers/github.test.ts +139 -0
  38. package/test/unit/deploy/providers/railway.test.ts +328 -0
  39. package/test/unit/deploy/registry.test.ts +227 -0
  40. package/test/unit/deploy/utils.test.ts +30 -0
  41. package/test/unit/utils/command-discovery.test.ts +145 -142
  42. package/test/unit/utils/git_utils.test.ts +49 -0
@@ -1,189 +1,192 @@
1
1
  import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- import { discoverCommandDirectories } from '../../../src/utils/discovery';
2
+ import { discoverCommandDirectories } from '../../../src/utils/discovery.js';
3
3
  import fs from 'node:fs';
4
4
  import path from 'node:path';
5
+ import { logger } from '@nexical/cli-core';
5
6
 
6
7
  vi.mock('node:fs');
7
-
8
- // Mock path module to allow controlled resolution for duplicate testing
9
- const originalPath = await import('node:path');
10
- const originalResolve = originalPath.resolve;
11
-
12
- vi.mock('node:path', async (importOriginal) => {
13
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
14
- const mod: any = await importOriginal();
15
- return {
16
- ...mod,
17
- default: {
18
- ...mod.default,
19
- resolve: vi.fn((...args: string[]) => mod.default.resolve(...args)),
20
- },
21
- resolve: vi.fn((...args: string[]) => mod.resolve(...args)),
22
- };
23
- });
24
-
25
8
  vi.mock('@nexical/cli-core', () => ({
26
9
  logger: {
27
10
  debug: vi.fn(),
28
11
  warn: vi.fn(),
29
- error: vi.fn(),
30
12
  },
31
13
  }));
32
14
 
33
15
  describe('discoverCommandDirectories', () => {
34
- // ... setup ...
35
- const cwd = '/app';
36
-
37
16
  beforeEach(() => {
38
17
  vi.resetAllMocks();
39
- // Restore default path behavior
40
- vi.mocked(path.resolve).mockImplementation(originalResolve);
41
- // Default fs mocks
42
- vi.mocked(fs.existsSync).mockReturnValue(false);
43
- vi.mocked(fs.readdirSync).mockReturnValue([]);
44
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
45
- vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => true } as any);
46
18
  });
47
19
 
48
- it('should return empty list if no directories exist', () => {
49
- const dirs = discoverCommandDirectories(cwd);
50
- expect(dirs).toHaveLength(0);
51
- });
20
+ const root = path.resolve('/mock');
52
21
 
53
- it('should find core commands in project directory', () => {
54
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
55
- vi.mocked(fs.existsSync).mockImplementation((p: any) => {
56
- return p === path.resolve('/app/src/commands');
57
- });
22
+ it('should discover core commands', () => {
23
+ const corePath = path.join(root, 'src/commands');
24
+ vi.mocked(fs.existsSync).mockImplementation(((p: fs.PathLike) => p === corePath) as any);
25
+ const dirs = discoverCommandDirectories(root);
26
+ expect(dirs).toContain(corePath);
27
+ });
58
28
 
59
- const dirs = discoverCommandDirectories(cwd);
60
- expect(dirs).toContain(path.resolve('/app/src/commands'));
29
+ it('should skip directories that do not exist', () => {
30
+ vi.mocked(fs.existsSync).mockReturnValue(false);
31
+ const dirs = discoverCommandDirectories(root);
32
+ expect(dirs).toEqual([]);
33
+ expect(logger.debug).toHaveBeenCalledWith(
34
+ expect.stringContaining('Command directory not found'),
35
+ );
61
36
  });
62
37
 
63
- it('should scan modules for commands', () => {
64
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
65
- vi.mocked(fs.existsSync).mockImplementation((p: any) => {
66
- if (p === path.resolve('/app/modules')) return true;
67
- if (p === path.resolve('/app/modules/mod1')) return true;
68
- if (p === path.resolve('/app/modules/mod1/src/commands')) return true;
69
- if (p === path.resolve('/app/modules/mod2')) return true;
38
+ it('should skip src commands if dist exists', () => {
39
+ const corePath = path.join(root, 'src/commands');
40
+ const distPath = path.join(root, 'dist/commands');
41
+ vi.mocked(fs.existsSync).mockImplementation(((p: fs.PathLike) => {
42
+ if (p === corePath || p === distPath) return true;
70
43
  return false;
71
- });
72
-
73
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
74
- vi.mocked(fs.readdirSync).mockReturnValue(['mod1', 'mod2', '.hidden'] as any);
75
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
76
- vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => true } as any);
44
+ }) as any);
45
+ const dirs = discoverCommandDirectories(root);
46
+ expect(dirs).not.toContain(corePath);
47
+ });
77
48
 
78
- const dirs = discoverCommandDirectories(cwd);
49
+ it('should discover module commands', () => {
50
+ const modulesRoot = path.join(root, 'modules');
51
+ const mod1Path = path.join(modulesRoot, 'mod1');
52
+ const mod1SrcCommands = path.join(mod1Path, 'src/commands');
79
53
 
80
- expect(dirs).toContain(path.resolve('/app/modules/mod1/src/commands'));
81
- expect(dirs).not.toContain(path.resolve('/app/modules/mod2/src/commands'));
54
+ vi.mocked(fs.existsSync).mockImplementation(((p: fs.PathLike) => {
55
+ if (p === modulesRoot) return true;
56
+ if (p === mod1SrcCommands) return true;
57
+ return false;
58
+ }) as any);
59
+ vi.mocked(fs.readdirSync).mockImplementation(((p: fs.PathLike) => {
60
+ if (p === modulesRoot) return ['mod1'] as unknown as string[];
61
+ return [] as string[];
62
+ }) as any);
63
+ vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => true } as unknown as fs.Stats);
64
+
65
+ const dirs = discoverCommandDirectories(root);
66
+ expect(dirs).toContain(mod1SrcCommands);
82
67
  });
83
68
 
84
- it('should scan src/modules for commands', () => {
85
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
86
- vi.mocked(fs.existsSync).mockImplementation((p: any) => {
87
- if (p === path.resolve('/app/src/modules')) return true;
88
- if (p === path.resolve('/app/src/modules/mod-src')) return true;
89
- if (p === path.resolve('/app/src/modules/mod-src/src/commands')) return true;
90
- return false;
69
+ it('should skip already visited directories', () => {
70
+ const corePath = path.join(root, 'src/commands');
71
+
72
+ vi.mocked(fs.existsSync).mockImplementation(((p: fs.PathLike) => {
73
+ if ((p as string).includes('dist')) return false;
74
+ return true;
75
+ }) as any);
76
+
77
+ const modulesRoot = path.join(root, 'modules');
78
+ vi.mocked(fs.readdirSync).mockImplementation(((p: fs.PathLike) => {
79
+ if (p === modulesRoot) return ['core-link'] as unknown as string[];
80
+ return [] as string[];
81
+ }) as any);
82
+ vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => true } as unknown as fs.Stats);
83
+
84
+ const mockModuleSrcPath = path.join(root, 'modules/core-link/src/commands');
85
+ const originalResolve = path.resolve;
86
+ path.resolve = vi.fn().mockImplementation((p: string) => {
87
+ if (p === mockModuleSrcPath) return corePath;
88
+ if (p === root || p.startsWith(root)) return p;
89
+ return originalResolve(p);
91
90
  });
92
91
 
93
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
94
- vi.mocked(fs.readdirSync).mockReturnValue(['mod-src'] as any);
95
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
96
- vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => true } as any);
92
+ const dirs = discoverCommandDirectories(root);
93
+ expect(dirs.filter((d: string) => d === corePath).length).toBe(1);
97
94
 
98
- const dirs = discoverCommandDirectories(cwd);
99
-
100
- expect(dirs).toContain(path.resolve('/app/src/modules/mod-src/src/commands'));
95
+ path.resolve = originalResolve;
101
96
  });
102
97
 
103
- it('should handle errors when scanning modules', () => {
104
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
105
- vi.mocked(fs.existsSync).mockImplementation((p: any) => {
106
- return p === path.resolve('/app/src/commands');
107
- });
108
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
109
- vi.mocked(fs.readdirSync).mockImplementation((p: any) => {
110
- if (p.includes('modules')) throw new Error('Permission denied');
111
- return [];
112
- });
113
-
114
- const dirs = discoverCommandDirectories(cwd);
115
- // Should not crash
116
- expect(dirs).toHaveLength(1);
117
- expect(dirs).toContain(path.resolve('/app/src/commands'));
98
+ it('should skip hidden entries and files in module search', () => {
99
+ const modulesRoot = path.join(root, 'modules');
100
+ vi.mocked(fs.existsSync).mockImplementation((p: fs.PathLike) => p === modulesRoot);
101
+ vi.mocked(fs.readdirSync).mockReturnValue(['.git', 'file.txt'] as any);
102
+ vi.mocked(fs.statSync).mockImplementation(
103
+ (p: fs.PathLike) =>
104
+ ({
105
+ isDirectory: () => !(p as string).endsWith('file.txt'),
106
+ }) as fs.Stats,
107
+ );
108
+
109
+ const dirs = discoverCommandDirectories(root);
110
+ expect(dirs).toEqual([]);
118
111
  });
119
112
 
120
- it('should deduplicate dist and src core commands', () => {
121
- // const srcPath = path.resolve('/app/src/commands');
113
+ it('should discover dist commands in modules', () => {
114
+ const modulesRoot = path.join(root, 'modules');
115
+ const modDistPath = path.join(modulesRoot, 'mod-dist');
116
+ const distCommands = path.join(modDistPath, 'dist/commands');
122
117
 
123
- // First we add distPath (manually simulate index.ts adding it to visited if we could,
124
- // but here we test the internal visited set of discoverCommandDirectories for multiple calls if we used it that way,
125
- // or rather we test how it handles its OWN loops.
126
- // Actually discoverCommandDirectories doesn't see distPath unless we add it to its loops.
127
-
128
- // Let's test if it skips src/commands if it SHOULD.
129
- // Wait, the new logic in discovery.ts skips src/commands if dist/src/commands is in visited.
130
- // So we need to simulate adding dist/src/commands first.
118
+ vi.mocked(fs.existsSync).mockImplementation(((p: fs.PathLike) => {
119
+ if (p === modulesRoot) return true;
120
+ if (p === distCommands) return true;
121
+ return false;
122
+ }) as any);
123
+ vi.mocked(fs.readdirSync).mockImplementation(((p: fs.PathLike) => {
124
+ if (p === modulesRoot) return ['mod-dist'] as unknown as string[];
125
+ return [] as string[];
126
+ }) as any);
127
+ vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => true } as unknown as fs.Stats);
128
+
129
+ const dirs = discoverCommandDirectories(root);
130
+ expect(dirs).toContain(distCommands);
131
+ });
131
132
 
132
- // Actually my new logic in discovery.ts DOES NOT scan for dist/src/commands automatically.
133
- // It relies on index.ts adding it, OR if it's found in a module.
133
+ it('should skip module with no command directories', () => {
134
+ const modulesRoot = path.join(root, 'modules');
135
+ // modEmpty removed as unused
134
136
 
135
- // Let's test the deduplication logic in addDir specifically if we can.
136
- // I'll add a test case that calls it twice conceptually.
137
+ vi.mocked(fs.existsSync).mockImplementation(((p: fs.PathLike) => {
138
+ if (p === modulesRoot) return true;
139
+ // No dist, no src
140
+ return false;
141
+ }) as any);
142
+ vi.mocked(fs.readdirSync).mockImplementation(((p: fs.PathLike) => {
143
+ if (p === modulesRoot) return ['mod-empty'] as unknown as string[];
144
+ return [] as string[];
145
+ }) as any);
146
+ vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => true } as unknown as fs.Stats);
147
+
148
+ discoverCommandDirectories(root);
149
+ expect(discoverCommandDirectories(root)).not.toContain(expect.stringContaining('mod-empty'));
150
+ });
137
151
 
138
- // Wait, discovery.ts:
139
- /*
140
- const isSrc = resolved.endsWith(path.join('src', 'commands'));
141
- if (isSrc) {
142
- const distEquivalent = resolved.replace(path.sep + 'src' + path.sep, path.sep + 'dist' + path.sep + 'src' + path.sep);
143
- if (visited.has(distEquivalent)) return;
144
- }
145
- */
152
+ describe('edge cases', () => {
153
+ it('should handle non-TS environment', () => {
154
+ const originalArgv = process.argv;
155
+ const originalEnv = { ...process.env };
146
156
 
147
- // Implementation check:
148
- vi.mocked(fs.existsSync).mockReturnValue(true);
149
- vi.mocked(fs.readdirSync).mockReturnValue([]);
157
+ Object.defineProperty(process, 'argv', { value: ['node', 'cli.js'], configurable: true });
158
+ process.env.VITEST = 'false';
159
+ process.env.NODE_ENV = 'production';
160
+ const originalExecArgv = process.execArgv;
161
+ Object.defineProperty(process, 'execArgv', { value: [], configurable: true });
150
162
 
151
- // Since we can't easily control 'visited' from outside, we trust the logic.
152
- // But we can verify it doesn't return BOTH if they resolve to same thing (already handled by visited.has(resolved)).
153
- });
163
+ const corePath = path.join(root, 'src/commands');
164
+ vi.mocked(fs.existsSync).mockImplementation(((p: fs.PathLike) => p === corePath) as any);
154
165
 
155
- it('should ignore duplicate paths', () => {
156
- const corePath = path.resolve('/app/src/commands');
166
+ discoverCommandDirectories(root);
167
+ expect(logger.debug).toHaveBeenCalledWith(expect.stringContaining('no TS loader detected'));
157
168
 
158
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
159
- vi.mocked(fs.existsSync).mockImplementation((p: any) => {
160
- return p === corePath;
169
+ process.argv = originalArgv;
170
+ Object.assign(process.env, originalEnv);
171
+ Object.defineProperty(process, 'execArgv', { value: originalExecArgv, configurable: true });
161
172
  });
162
173
 
163
- const dirs = discoverCommandDirectories(cwd);
164
-
165
- expect(dirs).toContain(corePath);
166
- expect(dirs).toHaveLength(1);
167
- });
168
-
169
- it('should ignore files in modules directory', () => {
170
- vi.mocked(fs.existsSync).mockReturnValue(true);
171
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
172
- vi.mocked(fs.readdirSync).mockReturnValue(['mod1', 'file.txt'] as any);
173
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
174
- vi.mocked(fs.statSync).mockImplementation((p: any) => {
175
- if (typeof p === 'string' && p.endsWith('file.txt')) {
176
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
177
- return { isDirectory: () => false } as any;
178
- }
179
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
180
- return { isDirectory: () => true } as any;
174
+ it('should handle readdir failure with Error', () => {
175
+ vi.mocked(fs.existsSync).mockReturnValue(true);
176
+ vi.mocked(fs.readdirSync).mockImplementation(() => {
177
+ throw new Error('fail');
178
+ });
179
+ discoverCommandDirectories(root);
180
+ expect(logger.debug).toHaveBeenCalledWith(expect.stringContaining('Error scanning root'));
181
181
  });
182
182
 
183
- const dirs = discoverCommandDirectories(cwd);
184
- // Should process mod1, ignore file.txt
185
- // The logic prefers dist/src/commands if it exists, and our mock returns true for all existsSync
186
- expect(dirs).toContain(path.resolve('/app/modules/mod1/dist/src/commands'));
187
- expect(dirs).not.toContain(path.resolve('/app/modules/file.txt/src/commands'));
183
+ it('should handle readdir failure with String', () => {
184
+ vi.mocked(fs.existsSync).mockReturnValue(true);
185
+ vi.mocked(fs.readdirSync).mockImplementation(() => {
186
+ throw 'String Fail';
187
+ });
188
+ discoverCommandDirectories(root);
189
+ expect(logger.debug).toHaveBeenCalledWith(expect.stringContaining('String Fail'));
190
+ });
188
191
  });
189
192
  });
@@ -0,0 +1,49 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import * as git from '../../../src/utils/git.js';
3
+ import { runCommand } from '@nexical/cli-core';
4
+
5
+ vi.mock('@nexical/cli-core', () => ({
6
+ runCommand: vi.fn(),
7
+ logger: {
8
+ debug: vi.fn(),
9
+ },
10
+ }));
11
+
12
+ vi.mock('node:child_process', () => ({
13
+ exec: vi.fn(),
14
+ }));
15
+
16
+ describe('git utils', () => {
17
+ it('should call git clone', async () => {
18
+ await git.clone('url', 'dest');
19
+ expect(runCommand).toHaveBeenCalledWith(expect.stringContaining('git clone'), 'dest');
20
+ });
21
+
22
+ it('should call renameRemote', async () => {
23
+ await git.renameRemote('old', 'new', 'cwd');
24
+ expect(runCommand).toHaveBeenCalledWith('git remote rename old new', 'cwd');
25
+ });
26
+
27
+ it('should check if branch exists', async () => {
28
+ const { exec } = await import('node:child_process');
29
+ vi.mocked(exec).mockImplementation(((
30
+ _cmd: string,
31
+ _opts: any,
32
+ callback: (err: Error | null, res: { stdout: string }) => void,
33
+ ) => {
34
+ callback(null, { stdout: '' });
35
+ return {} as any;
36
+ }) as any);
37
+ expect(await git.branchExists('main', 'cwd')).toBe(true);
38
+
39
+ vi.mocked(exec).mockImplementation(((
40
+ _cmd: string,
41
+ _opts: any,
42
+ callback: (err: Error | null) => void,
43
+ ) => {
44
+ callback(new Error());
45
+ return {} as any;
46
+ }) as any);
47
+ expect(await git.branchExists('main', 'cwd')).toBe(false);
48
+ });
49
+ });