@nexical/cli 0.11.8 → 0.11.10
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/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/src/commands/deploy.d.ts +2 -0
- package/dist/src/commands/deploy.js +3 -3
- package/dist/src/commands/deploy.js.map +1 -1
- package/dist/src/commands/init.js +3 -3
- package/dist/src/commands/module/add.js +53 -22
- package/dist/src/commands/module/add.js.map +1 -1
- package/dist/src/commands/module/list.d.ts +1 -0
- package/dist/src/commands/module/list.js +54 -45
- package/dist/src/commands/module/list.js.map +1 -1
- package/dist/src/commands/module/remove.js +37 -12
- package/dist/src/commands/module/remove.js.map +1 -1
- package/dist/src/commands/module/update.js +15 -3
- package/dist/src/commands/module/update.js.map +1 -1
- package/dist/src/commands/run.js +18 -1
- package/dist/src/commands/run.js.map +1 -1
- package/package.json +2 -2
- package/src/commands/deploy.ts +3 -3
- package/src/commands/module/add.ts +74 -31
- package/src/commands/module/list.ts +80 -57
- package/src/commands/module/remove.ts +50 -14
- package/src/commands/module/update.ts +19 -5
- package/src/commands/run.ts +21 -1
- package/test/e2e/lifecycle.e2e.test.ts +3 -2
- package/test/integration/commands/deploy.integration.test.ts +102 -0
- package/test/integration/commands/init.integration.test.ts +16 -1
- package/test/integration/commands/module.integration.test.ts +81 -55
- package/test/integration/commands/run.integration.test.ts +69 -74
- package/test/integration/commands/setup.integration.test.ts +53 -0
- package/test/unit/commands/deploy.test.ts +285 -0
- package/test/unit/commands/init.test.ts +15 -0
- package/test/unit/commands/module/add.test.ts +363 -254
- package/test/unit/commands/module/list.test.ts +100 -99
- package/test/unit/commands/module/remove.test.ts +143 -58
- package/test/unit/commands/module/update.test.ts +45 -62
- package/test/unit/commands/run.test.ts +16 -1
- package/test/unit/commands/setup.test.ts +25 -66
- package/test/unit/deploy/config-manager.test.ts +65 -0
- package/test/unit/deploy/providers/cloudflare.test.ts +210 -0
- package/test/unit/deploy/providers/github.test.ts +139 -0
- package/test/unit/deploy/providers/railway.test.ts +328 -0
- package/test/unit/deploy/registry.test.ts +227 -0
- package/test/unit/deploy/utils.test.ts +30 -0
- package/test/unit/utils/command-discovery.test.ts +145 -142
- 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
|
-
|
|
49
|
-
const dirs = discoverCommandDirectories(cwd);
|
|
50
|
-
expect(dirs).toHaveLength(0);
|
|
51
|
-
});
|
|
20
|
+
const root = path.resolve('/mock');
|
|
52
21
|
|
|
53
|
-
it('should
|
|
54
|
-
|
|
55
|
-
vi.mocked(fs.existsSync).mockImplementation((p:
|
|
56
|
-
|
|
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
|
-
|
|
60
|
-
|
|
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
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
if (p ===
|
|
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
|
-
|
|
74
|
-
|
|
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
|
-
|
|
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
|
-
|
|
81
|
-
|
|
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
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
if (p
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
94
|
-
|
|
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
|
-
|
|
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
|
|
104
|
-
|
|
105
|
-
vi.mocked(fs.existsSync).mockImplementation((p:
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const dirs = discoverCommandDirectories(
|
|
115
|
-
|
|
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
|
|
121
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
133
|
-
|
|
133
|
+
it('should skip module with no command directories', () => {
|
|
134
|
+
const modulesRoot = path.join(root, 'modules');
|
|
135
|
+
// modEmpty removed as unused
|
|
134
136
|
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
152
|
-
|
|
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
|
-
|
|
156
|
-
|
|
166
|
+
discoverCommandDirectories(root);
|
|
167
|
+
expect(logger.debug).toHaveBeenCalledWith(expect.stringContaining('no TS loader detected'));
|
|
157
168
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
169
|
+
process.argv = originalArgv;
|
|
170
|
+
Object.assign(process.env, originalEnv);
|
|
171
|
+
Object.defineProperty(process, 'execArgv', { value: originalExecArgv, configurable: true });
|
|
161
172
|
});
|
|
162
173
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
+
});
|