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