@nexical/cli 0.10.0 → 0.11.1
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/.github/workflows/deploy.yml +1 -1
- package/.husky/pre-commit +1 -0
- package/.prettierignore +8 -0
- package/.prettierrc +7 -0
- package/GEMINI.md +199 -0
- package/README.md +85 -56
- package/dist/chunk-AC4B3HPJ.js +93 -0
- package/dist/chunk-AC4B3HPJ.js.map +1 -0
- package/dist/{chunk-JYASTIIW.js → chunk-PJIOCW2A.js} +1 -1
- package/dist/chunk-PJIOCW2A.js.map +1 -0
- package/dist/{chunk-WKERTCM6.js → chunk-Q7YLW5HJ.js} +5 -2
- package/dist/chunk-Q7YLW5HJ.js.map +1 -0
- package/dist/index.js +41 -12
- package/dist/index.js.map +1 -1
- package/dist/src/commands/init.d.ts +4 -1
- package/dist/src/commands/init.js +15 -10
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/module/add.d.ts +3 -1
- package/dist/src/commands/module/add.js +27 -16
- package/dist/src/commands/module/add.js.map +1 -1
- package/dist/src/commands/module/list.js +9 -5
- package/dist/src/commands/module/list.js.map +1 -1
- package/dist/src/commands/module/remove.d.ts +3 -1
- package/dist/src/commands/module/remove.js +13 -7
- package/dist/src/commands/module/remove.js.map +1 -1
- package/dist/src/commands/module/update.d.ts +3 -1
- package/dist/src/commands/module/update.js +7 -5
- package/dist/src/commands/module/update.js.map +1 -1
- package/dist/src/commands/run.d.ts +4 -1
- package/dist/src/commands/run.js +10 -2
- package/dist/src/commands/run.js.map +1 -1
- package/dist/src/commands/setup.d.ts +8 -0
- package/dist/src/commands/setup.js +75 -0
- package/dist/src/commands/setup.js.map +1 -0
- package/dist/src/utils/discovery.js +1 -1
- package/dist/src/utils/git.js +1 -1
- package/dist/src/utils/url-resolver.js +1 -1
- package/eslint.config.mjs +67 -0
- package/index.ts +34 -20
- package/package.json +57 -33
- package/src/commands/init.ts +79 -75
- package/src/commands/module/add.ts +158 -148
- package/src/commands/module/list.ts +61 -50
- package/src/commands/module/remove.ts +59 -54
- package/src/commands/module/update.ts +44 -42
- package/src/commands/run.ts +89 -81
- package/src/commands/setup.ts +92 -0
- package/src/utils/discovery.ts +98 -113
- package/src/utils/git.ts +35 -28
- package/src/utils/url-resolver.ts +50 -45
- package/test/e2e/lifecycle.e2e.test.ts +139 -130
- package/test/integration/commands/init.integration.test.ts +64 -61
- package/test/integration/commands/module.integration.test.ts +122 -122
- package/test/integration/commands/run.integration.test.ts +70 -63
- package/test/integration/utils/command-loading.integration.test.ts +40 -53
- package/test/unit/commands/init.test.ts +163 -128
- package/test/unit/commands/module/add.test.ts +312 -245
- package/test/unit/commands/module/list.test.ts +108 -91
- package/test/unit/commands/module/remove.test.ts +74 -67
- package/test/unit/commands/module/update.test.ts +74 -70
- package/test/unit/commands/run.test.ts +253 -201
- package/test/unit/commands/setup.test.ts +187 -0
- package/test/unit/utils/command-discovery.test.ts +138 -125
- package/test/unit/utils/git.test.ts +135 -117
- package/test/unit/utils/integration-helpers.test.ts +59 -49
- package/test/unit/utils/url-resolver.test.ts +46 -34
- package/test/utils/integration-helpers.ts +36 -29
- package/tsconfig.json +15 -25
- package/tsup.config.ts +14 -14
- package/vitest.config.ts +10 -10
- package/vitest.e2e.config.ts +6 -6
- package/vitest.integration.config.ts +17 -17
- package/dist/chunk-JYASTIIW.js.map +0 -1
- package/dist/chunk-OKXOCNXP.js +0 -105
- package/dist/chunk-OKXOCNXP.js.map +0 -1
- package/dist/chunk-WKERTCM6.js.map +0 -1
|
@@ -1,153 +1,188 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { runCommand } from '@nexical/cli-core';
|
|
2
2
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
3
3
|
import InitCommand from '../../../src/commands/init.js';
|
|
4
4
|
import * as git from '../../../src/utils/git.js';
|
|
5
5
|
import fs from 'fs-extra';
|
|
6
6
|
|
|
7
7
|
vi.mock('@nexical/cli-core', async (importOriginal) => {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
const mod = await importOriginal<typeof import('@nexical/cli-core')>();
|
|
9
|
+
return {
|
|
10
|
+
...mod,
|
|
11
|
+
runCommand: vi.fn(),
|
|
12
|
+
logger: {
|
|
13
|
+
code: vi.fn(),
|
|
14
|
+
debug: vi.fn(),
|
|
15
|
+
error: vi.fn(),
|
|
16
|
+
success: vi.fn(),
|
|
17
|
+
info: vi.fn(),
|
|
18
|
+
warn: vi.fn(),
|
|
19
|
+
},
|
|
20
|
+
};
|
|
14
21
|
});
|
|
15
22
|
|
|
16
23
|
vi.mock('../../../src/utils/git.js', () => ({
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
clone: vi.fn(),
|
|
25
|
+
updateSubmodules: vi.fn(),
|
|
26
|
+
checkoutOrphan: vi.fn(),
|
|
27
|
+
addAll: vi.fn(),
|
|
28
|
+
commit: vi.fn(),
|
|
29
|
+
deleteBranch: vi.fn(),
|
|
30
|
+
renameBranch: vi.fn(),
|
|
31
|
+
removeRemote: vi.fn(),
|
|
32
|
+
branchExists: vi.fn(),
|
|
33
|
+
renameRemote: vi.fn(),
|
|
34
|
+
getRemoteUrl: vi.fn(),
|
|
28
35
|
}));
|
|
29
36
|
|
|
30
37
|
vi.mock('fs-extra');
|
|
31
38
|
|
|
32
39
|
describe('InitCommand', () => {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
});
|
|
40
|
+
let command: InitCommand;
|
|
41
|
+
// Spy on process.exit but rely on catching the error if it throws (default vitest behavior)
|
|
42
|
+
// or mock it to throw a custom error we can check.
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
vi.clearAllMocks();
|
|
45
|
+
command = new InitCommand({});
|
|
46
|
+
vi.spyOn(command, 'error').mockImplementation(() => {});
|
|
47
|
+
vi.spyOn(command, 'info').mockImplementation(() => {});
|
|
48
|
+
vi.spyOn(command, 'success').mockImplementation(() => {});
|
|
49
|
+
|
|
50
|
+
// Default fs mocks
|
|
51
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
52
|
+
vi.mocked(fs.pathExists as any).mockResolvedValue(false); // Target not exist
|
|
53
|
+
vi.mocked(fs.mkdir).mockResolvedValue(undefined);
|
|
54
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
55
|
+
vi.mocked(fs.readdir).mockResolvedValue([] as any);
|
|
56
|
+
vi.mocked(fs.copy).mockResolvedValue(undefined);
|
|
57
|
+
vi.mocked(fs.ensureDir).mockResolvedValue(undefined);
|
|
58
|
+
|
|
59
|
+
// Mock process.exit to throw a known error so we can stop execution and verify it
|
|
60
|
+
vi.spyOn(process, 'exit').mockImplementation((code) => {
|
|
61
|
+
throw new Error(`Process.exit(${code})`);
|
|
56
62
|
});
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
'https://github.com/nexical/nexical-starter.git',
|
|
119
|
-
expect.stringContaining(targetDir),
|
|
120
|
-
{ recursive: true }
|
|
121
|
-
);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
afterEach(() => {
|
|
66
|
+
vi.resetAllMocks();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should have correct metadata', () => {
|
|
70
|
+
expect(InitCommand.description).toBeDefined();
|
|
71
|
+
expect(InitCommand.args).toBeDefined();
|
|
72
|
+
expect(InitCommand.requiresProject).toBe(false);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should initialize project with default repo', async () => {
|
|
76
|
+
const targetDir = 'new-project';
|
|
77
|
+
await command.run({ directory: targetDir, repo: 'https://default.com/repo' });
|
|
78
|
+
|
|
79
|
+
expect(fs.mkdir).toHaveBeenCalledWith(expect.stringContaining(targetDir), { recursive: true });
|
|
80
|
+
|
|
81
|
+
// Clone
|
|
82
|
+
expect(git.clone).toHaveBeenCalledWith(
|
|
83
|
+
'https://default.com/repo.git',
|
|
84
|
+
expect.stringContaining(targetDir),
|
|
85
|
+
{ recursive: true },
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
// Submodules
|
|
89
|
+
expect(git.updateSubmodules).toHaveBeenCalledWith(expect.stringContaining(targetDir));
|
|
90
|
+
|
|
91
|
+
// Npm install
|
|
92
|
+
expect(runCommand).toHaveBeenCalledWith('npm install', expect.stringContaining(targetDir));
|
|
93
|
+
|
|
94
|
+
// Remote rename
|
|
95
|
+
expect(git.renameRemote).toHaveBeenCalledWith(
|
|
96
|
+
'origin',
|
|
97
|
+
'upstream',
|
|
98
|
+
expect.stringContaining(targetDir),
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
// Version and Config creation
|
|
102
|
+
expect(fs.writeFile).toHaveBeenCalledWith(
|
|
103
|
+
expect.stringContaining('nexical.yaml'),
|
|
104
|
+
expect.stringContaining('name: new-project'),
|
|
105
|
+
);
|
|
106
|
+
expect(fs.writeFile).toHaveBeenCalledWith(expect.stringContaining('VERSION'), '0.1.0');
|
|
107
|
+
|
|
108
|
+
expect(git.addAll).toHaveBeenCalledWith(expect.stringContaining(targetDir));
|
|
109
|
+
expect(git.commit).toHaveBeenCalledWith(
|
|
110
|
+
'Initial site commit',
|
|
111
|
+
expect.stringContaining(targetDir),
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
expect(command.success).toHaveBeenCalledWith(expect.stringContaining('successfully'));
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('should skip version and config creation if they already exist', async () => {
|
|
118
|
+
const targetDir = 'existing-files';
|
|
119
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
120
|
+
vi.mocked(fs.pathExists as any).mockImplementation(async (p: string) => {
|
|
121
|
+
if (p.includes('nexical.yaml')) return true;
|
|
122
|
+
if (p.includes('VERSION')) return true;
|
|
123
|
+
return false;
|
|
122
124
|
});
|
|
123
125
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
126
|
+
await command.run({ directory: targetDir, repo: 'foo' });
|
|
127
|
+
|
|
128
|
+
expect(fs.writeFile).not.toHaveBeenCalledWith(
|
|
129
|
+
expect.stringContaining('nexical.yaml'),
|
|
130
|
+
expect.anything(),
|
|
131
|
+
);
|
|
132
|
+
expect(fs.writeFile).not.toHaveBeenCalledWith(
|
|
133
|
+
expect.stringContaining('VERSION'),
|
|
134
|
+
expect.anything(),
|
|
135
|
+
);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('should handle gh@ syntax', async () => {
|
|
139
|
+
const targetDir = 'gh-project';
|
|
140
|
+
await command.run({ directory: targetDir, repo: 'gh@nexical/nexical-starter' });
|
|
141
|
+
|
|
142
|
+
expect(git.clone).toHaveBeenCalledWith(
|
|
143
|
+
'https://github.com/nexical/nexical-starter.git',
|
|
144
|
+
expect.stringContaining(targetDir),
|
|
145
|
+
{ recursive: true },
|
|
146
|
+
);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('should proceed if directory exists but is empty', async () => {
|
|
150
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
151
|
+
vi.mocked(fs.pathExists as any).mockResolvedValue(true);
|
|
152
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
153
|
+
vi.mocked(fs.readdir).mockResolvedValue([] as any);
|
|
154
|
+
|
|
155
|
+
await command.run({ directory: 'empty-dir', repo: 'foo' });
|
|
156
|
+
|
|
157
|
+
expect(fs.mkdir).not.toHaveBeenCalled(); // Should assume dir exists
|
|
158
|
+
expect(git.clone).toHaveBeenCalledWith('foo.git', expect.stringContaining('empty-dir'), {
|
|
159
|
+
recursive: true,
|
|
132
160
|
});
|
|
161
|
+
});
|
|
133
162
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
163
|
+
it('should fail if directory exists and is not empty', async () => {
|
|
164
|
+
// First exists check for targetDir
|
|
165
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
166
|
+
vi.mocked(fs.pathExists as any).mockResolvedValue(true);
|
|
167
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
168
|
+
vi.mocked(fs.readdir).mockResolvedValue(['file.txt'] as any);
|
|
138
169
|
|
|
139
|
-
|
|
140
|
-
|
|
170
|
+
await expect(command.run({ directory: 'existing-dir', repo: 'foo' })).rejects.toThrow(
|
|
171
|
+
'Process.exit(1)',
|
|
172
|
+
);
|
|
141
173
|
|
|
142
|
-
|
|
143
|
-
|
|
174
|
+
expect(command.error).toHaveBeenCalledWith(expect.stringContaining('not empty'));
|
|
175
|
+
});
|
|
144
176
|
|
|
145
|
-
|
|
146
|
-
|
|
177
|
+
it('should handle git errors gracefully', async () => {
|
|
178
|
+
vi.mocked(git.clone).mockRejectedValueOnce(new Error('Git fail'));
|
|
147
179
|
|
|
148
|
-
|
|
149
|
-
|
|
180
|
+
await expect(command.run({ directory: 'fail-project', repo: 'foo' })).rejects.toThrow(
|
|
181
|
+
'Process.exit(1)',
|
|
182
|
+
);
|
|
150
183
|
|
|
151
|
-
|
|
152
|
-
|
|
184
|
+
expect(command.error).toHaveBeenCalledWith(
|
|
185
|
+
expect.stringContaining('Failed to initialize project'),
|
|
186
|
+
);
|
|
187
|
+
});
|
|
153
188
|
});
|