@nexical/cli 0.11.7 → 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 (46) hide show
  1. package/dist/{chunk-LZ3YQWAR.js → chunk-OUGA4CB4.js} +15 -11
  2. package/dist/chunk-OUGA4CB4.js.map +1 -0
  3. package/dist/index.js +1 -1
  4. package/dist/index.js.map +1 -1
  5. package/dist/src/commands/init.js +1 -1
  6. package/dist/src/commands/module/add.js +51 -20
  7. package/dist/src/commands/module/add.js.map +1 -1
  8. package/dist/src/commands/module/list.d.ts +1 -0
  9. package/dist/src/commands/module/list.js +55 -46
  10. package/dist/src/commands/module/list.js.map +1 -1
  11. package/dist/src/commands/module/remove.js +38 -13
  12. package/dist/src/commands/module/remove.js.map +1 -1
  13. package/dist/src/commands/module/update.js +16 -4
  14. package/dist/src/commands/module/update.js.map +1 -1
  15. package/dist/src/commands/run.js +19 -2
  16. package/dist/src/commands/run.js.map +1 -1
  17. package/dist/src/commands/setup.js +1 -1
  18. package/package.json +1 -1
  19. package/src/commands/module/add.ts +74 -31
  20. package/src/commands/module/list.ts +80 -57
  21. package/src/commands/module/remove.ts +50 -14
  22. package/src/commands/module/update.ts +19 -5
  23. package/src/commands/run.ts +21 -1
  24. package/test/e2e/lifecycle.e2e.test.ts +3 -2
  25. package/test/integration/commands/deploy.integration.test.ts +102 -0
  26. package/test/integration/commands/init.integration.test.ts +16 -1
  27. package/test/integration/commands/module.integration.test.ts +81 -55
  28. package/test/integration/commands/run.integration.test.ts +69 -74
  29. package/test/integration/commands/setup.integration.test.ts +53 -0
  30. package/test/unit/commands/deploy.test.ts +285 -0
  31. package/test/unit/commands/init.test.ts +15 -0
  32. package/test/unit/commands/module/add.test.ts +363 -254
  33. package/test/unit/commands/module/list.test.ts +100 -99
  34. package/test/unit/commands/module/remove.test.ts +143 -58
  35. package/test/unit/commands/module/update.test.ts +45 -62
  36. package/test/unit/commands/run.test.ts +16 -1
  37. package/test/unit/commands/setup.test.ts +25 -66
  38. package/test/unit/deploy/config-manager.test.ts +65 -0
  39. package/test/unit/deploy/providers/cloudflare.test.ts +210 -0
  40. package/test/unit/deploy/providers/github.test.ts +139 -0
  41. package/test/unit/deploy/providers/railway.test.ts +328 -0
  42. package/test/unit/deploy/registry.test.ts +227 -0
  43. package/test/unit/deploy/utils.test.ts +30 -0
  44. package/test/unit/utils/command-discovery.test.ts +145 -142
  45. package/test/unit/utils/git_utils.test.ts +49 -0
  46. package/dist/chunk-LZ3YQWAR.js.map +0 -1
@@ -1,132 +1,133 @@
1
- // import { BaseCommand } from '@nexical/cli-core';
2
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
3
2
  import ModuleListCommand from '../../../../src/commands/module/list.js';
4
3
  import fs from 'fs-extra';
5
4
 
6
- vi.mock('@nexical/cli-core', async (importOriginal) => {
7
- const mod = await importOriginal<typeof import('@nexical/cli-core')>();
8
- return {
9
- ...mod,
10
- logger: {
11
- ...mod.logger,
12
- success: vi.fn(),
13
- info: vi.fn(),
14
- debug: vi.fn(),
15
- error: vi.fn(),
16
- warn: vi.fn(),
17
- },
18
- runCommand: vi.fn(),
19
- };
20
- });
21
5
  vi.mock('fs-extra');
22
6
 
23
7
  describe('ModuleListCommand', () => {
24
8
  let command: ModuleListCommand;
25
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
26
- let consoleTableSpy: any;
27
-
28
- beforeEach(async () => {
29
- vi.clearAllMocks();
30
- command = new ModuleListCommand({}, { rootDir: '/mock/root' });
31
- consoleTableSpy = vi.spyOn(console, 'table').mockImplementation(() => {});
32
- vi.spyOn(command, 'error').mockImplementation(() => {});
33
- vi.spyOn(command, 'success').mockImplementation(() => {});
9
+ const projectRoot = '/mock/project';
10
+
11
+ beforeEach(() => {
12
+ vi.resetAllMocks();
13
+ command = new ModuleListCommand({} as any, { rootDir: projectRoot });
14
+ (command as any).projectRoot = projectRoot;
15
+ vi.spyOn(console, 'table').mockImplementation(() => {});
34
16
  vi.spyOn(command, 'info').mockImplementation(() => {});
35
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
36
- vi.mocked(fs.pathExists).mockImplementation(async (p: any) => {
37
- if (p.includes('app.yml') || p.includes('nexical.yml')) return true;
38
- return true;
17
+
18
+ // Default mocks for info gathering
19
+ (fs.readJson as unknown as { mockResolvedValue: any }).mockResolvedValue({});
20
+ (fs.readFile as unknown as { mockResolvedValue: any }).mockResolvedValue('');
21
+ (fs.pathExists as unknown as { mockResolvedValue: any }).mockResolvedValue(false);
22
+ (fs.stat as unknown as { mockResolvedValue: any }).mockResolvedValue({
23
+ isDirectory: () => true,
39
24
  });
40
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
41
- vi.spyOn(process, 'exit').mockImplementation((() => {}) as any);
42
- await command.init();
43
25
  });
44
26
 
45
- afterEach(() => {
46
- vi.resetAllMocks();
47
- });
27
+ it('should list modules from both backend and frontend', async () => {
28
+ (fs.pathExists as unknown as { mockImplementation: any }).mockImplementation(
29
+ (p: string) => true,
30
+ );
31
+ (fs.readdir as unknown as { mockImplementation: any }).mockImplementation((p: string) => {
32
+ if (p.includes('backend')) return ['mod-b'];
33
+ if (p.includes('frontend')) return ['mod-f'];
34
+ return [];
35
+ });
36
+
37
+ await command.run();
48
38
 
49
- it('should have correct static properties', () => {
50
- expect(ModuleListCommand.usage).toContain('module list');
51
- expect(ModuleListCommand.description).toBeDefined();
52
- expect(ModuleListCommand.requiresProject).toBe(true);
39
+ // eslint-disable-next-line no-console
40
+ expect(console.table).toHaveBeenCalledWith([
41
+ { name: 'mod-b', type: 'backend', version: 'unknown', description: '' },
42
+ { name: 'mod-f', type: 'frontend', version: 'unknown', description: '' },
43
+ ]);
53
44
  });
54
45
 
55
- it('should error if project root is missing', async () => {
56
- command = new ModuleListCommand({}, { rootDir: undefined });
57
- vi.spyOn(command, 'init').mockImplementation(async () => {});
58
- vi.spyOn(command, 'error').mockImplementation(() => {});
46
+ it('should handle empty module directories', async () => {
47
+ (fs.pathExists as unknown as { mockReturnValue: any }).mockReturnValue(true);
48
+ (fs.readdir as unknown as { mockResolvedValue: any }).mockResolvedValue([]);
59
49
 
60
- await command.runInit({});
61
- expect(command.error).toHaveBeenCalledWith(
62
- expect.stringContaining('requires to be run within an app project'),
63
- 1,
64
- );
50
+ await command.run();
51
+ expect(command.info).toHaveBeenCalledWith('No modules installed.');
65
52
  });
66
53
 
67
- it('should handle missing modules directory', async () => {
68
- vi.mocked(fs.pathExists).mockImplementation(async () => false);
54
+ it('should sort modules by name', async () => {
55
+ (fs.pathExists as unknown as { mockReturnValue: any }).mockReturnValue(true);
56
+ (fs.readdir as unknown as { mockImplementation: any }).mockImplementation((p: string) => {
57
+ if (p.includes('backend')) return ['z-mod', 'a-mod'];
58
+ return [];
59
+ });
60
+
69
61
  await command.run();
70
- expect(command.info).toHaveBeenCalledWith(expect.stringContaining('No modules installed'));
62
+
63
+ // eslint-disable-next-line no-console
64
+ expect(console.table).toHaveBeenCalledWith([
65
+ { name: 'a-mod', type: 'backend', version: 'unknown', description: '' },
66
+ { name: 'z-mod', type: 'backend', version: 'unknown', description: '' },
67
+ ]);
71
68
  });
72
69
 
73
- it('should list modules with details', async () => {
74
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
75
- vi.mocked(fs.readdir).mockResolvedValue(['mod1', 'file.txt', 'mod2', 'mod3', 'mod4'] as any);
76
- // Mock directory check: mod1=dir, file.txt=file, mod2=dir, mod3=dir
77
- vi.mocked(fs.stat).mockImplementation(
78
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
79
- async (p: any) =>
80
- ({
81
- isDirectory: () => !p.includes('file.txt'),
82
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
83
- }) as any,
84
- );
70
+ it('should handle missing directories', async () => {
71
+ (fs.pathExists as any).mockReturnValue(false);
72
+ await command.run();
73
+ expect(command.info).toHaveBeenCalledWith('No modules installed.');
74
+ });
85
75
 
86
- // Mock package.json existence: mod1=yes, mod2=no, mod3=yes
87
- // Also ensure modules directory itself exists!
88
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
89
- vi.mocked(fs.pathExists).mockImplementation(async (p: any) => {
90
- if (p.includes('app.yml') || p.includes('nexical.yml')) return true;
91
- if (p.endsWith('/modules')) return true;
92
- return p.includes('package.json') && !p.includes('mod2');
93
- });
76
+ it('should handle one directory missing and one empty', async () => {
77
+ (fs.pathExists as any).mockImplementation((p: string) => p.includes('backend'));
78
+ (fs.readdir as unknown as { mockResolvedValue: any }).mockResolvedValue([]);
79
+ await command.run();
80
+ expect(command.info).toHaveBeenCalledWith('No modules installed.');
81
+ });
94
82
 
95
- // Mock reading json: mod1=valid, mod3=invalid, mod4=empty
96
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
97
- vi.mocked(fs.readJson).mockImplementation(async (p: any) => {
98
- if (p.includes('mod3')) throw new Error('Invalid JSON');
99
- if (p.includes('mod4')) return {}; // No version/desc
100
- return { version: '1.0.0', description: 'Desc' };
83
+ it('should handle backend empty and frontend not empty', async () => {
84
+ (fs.pathExists as unknown as { mockReturnValue: any }).mockReturnValue(true);
85
+ (fs.readdir as unknown as { mockImplementation: any }).mockImplementation((p: string) => {
86
+ if (p.includes('backend')) return [];
87
+ if (p.includes('frontend')) return ['front-mod'];
88
+ return [];
101
89
  });
102
-
103
90
  await command.run();
104
-
105
- // mod1: listed with version
106
- // file.txt: ignored
107
- // mod2: listed with unknown version (dir exists, no pkg.json)
108
- // mod3: listed with unknown version (invalid pkg.json)
109
- // mod4: listed with unknown/empty (fallback logic)
110
- expect(consoleTableSpy).toHaveBeenCalledWith(
111
- expect.arrayContaining([
112
- { name: 'mod1', version: '1.0.0', description: 'Desc' },
113
- { name: 'mod2', version: 'unknown', description: '' },
114
- { name: 'mod3', version: 'unknown', description: '' },
115
- { name: 'mod4', version: 'unknown', description: '' },
116
- ]),
117
- );
91
+ // eslint-disable-next-line no-console
92
+ expect(console.table).toHaveBeenCalledWith([
93
+ { name: 'front-mod', type: 'frontend', version: 'unknown', description: '' },
94
+ ]);
118
95
  });
119
96
 
120
- it('should handle empty modules directory', async () => {
121
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
122
- vi.mocked(fs.readdir).mockResolvedValue([] as any);
97
+ it('should handle non-directory entries', async () => {
98
+ (fs.pathExists as unknown as { mockReturnValue: any }).mockReturnValue(true);
99
+ (fs.readdir as any).mockResolvedValue(['file.txt']);
100
+ (fs.stat as any).mockResolvedValue({ isDirectory: () => false });
101
+
123
102
  await command.run();
124
103
  expect(command.info).toHaveBeenCalledWith('No modules installed.');
125
104
  });
126
105
 
127
- it('should handle failure during list', async () => {
128
- vi.mocked(fs.readdir).mockRejectedValue(new Error('FS Error'));
106
+ it('should handle empty config and package.json', async () => {
107
+ (fs.pathExists as unknown as { mockReturnValue: any }).mockReturnValue(true);
108
+
109
+ // Config files exist check returns true, but readJson/readFile fail or return null
110
+ (fs.pathExists as unknown as { mockImplementation: any }).mockImplementation(
111
+ (p: string) => true,
112
+ );
113
+
114
+ // Only return module for backend path to avoid duplicates in test output
115
+ (fs.readdir as unknown as { mockImplementation: any }).mockImplementation((p: string) => {
116
+ if (p.includes('apps/backend/modules')) return ['mod-a'];
117
+ return [];
118
+ });
119
+
120
+ (fs.stat as unknown as { mockResolvedValue: any }).mockResolvedValue({
121
+ isDirectory: () => true,
122
+ });
123
+ (fs.readJson as any).mockResolvedValue(null);
124
+ (fs.readFile as unknown as { mockResolvedValue: any }).mockResolvedValue(''); // Empty string -> YAML.parse returns null/undefined
125
+
129
126
  await command.run();
130
- expect(command.error).toHaveBeenCalledWith(expect.stringContaining('Failed to list modules'));
127
+
128
+ // eslint-disable-next-line no-console
129
+ expect(console.table).toHaveBeenCalledWith([
130
+ { name: 'mod-a', type: 'backend', version: 'unknown', description: '' },
131
+ ]);
131
132
  });
132
133
  });
@@ -1,96 +1,181 @@
1
- import { runCommand } from '@nexical/cli-core';
2
1
  import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
3
2
  import ModuleRemoveCommand from '../../../../src/commands/module/remove.js';
4
3
  import fs from 'fs-extra';
4
+ import * as cliCore from '@nexical/cli-core';
5
5
 
6
+ // Mocks
7
+ vi.mock('fs-extra');
6
8
  vi.mock('@nexical/cli-core', async (importOriginal) => {
7
9
  const mod = await importOriginal<typeof import('@nexical/cli-core')>();
8
10
  return {
9
11
  ...mod,
10
- logger: {
11
- ...mod.logger,
12
- success: vi.fn(),
13
- info: vi.fn(),
14
- debug: vi.fn(),
15
- error: vi.fn(),
16
- warn: vi.fn(),
17
- },
18
12
  runCommand: vi.fn(),
19
13
  };
20
14
  });
21
- vi.mock('fs-extra');
22
15
 
23
16
  describe('ModuleRemoveCommand', () => {
24
17
  let command: ModuleRemoveCommand;
18
+ const projectRoot = '/mock/project/root';
25
19
 
26
- beforeEach(async () => {
27
- vi.clearAllMocks();
28
- command = new ModuleRemoveCommand({}, { rootDir: '/mock/root' });
29
- vi.spyOn(command, 'error').mockImplementation(() => {});
30
- vi.spyOn(command, 'success').mockImplementation(() => {});
20
+ beforeEach(() => {
21
+ vi.resetAllMocks();
22
+
23
+ // Mock logger
24
+ vi.spyOn(cliCore.logger, 'debug').mockImplementation(() => {});
25
+ vi.spyOn(cliCore.logger, 'warn').mockImplementation(() => {});
26
+ vi.spyOn(cliCore.logger, 'info').mockImplementation(() => {});
27
+
28
+ command = new ModuleRemoveCommand({} as unknown as any, { rootDir: projectRoot });
29
+ (command as unknown as { projectRoot: string }).projectRoot = projectRoot;
30
+
31
+ // Explicitly spy on command methods
31
32
  vi.spyOn(command, 'info').mockImplementation(() => {});
32
- vi.mocked(fs.pathExists).mockImplementation(async (p: string) => {
33
- if (p.includes('app.yml') || p.includes('nexical.yml')) return true;
34
- return true;
35
- });
36
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
37
- vi.spyOn(process, 'exit').mockImplementation((() => {}) as any);
38
- await command.init();
33
+ vi.spyOn(command, 'success').mockImplementation(() => {});
34
+ vi.spyOn(command, 'error').mockImplementation(() => {});
35
+ vi.spyOn(command, 'warn').mockImplementation(() => {});
36
+
37
+ (cliCore.runCommand as unknown as { mockResolvedValue: any }).mockResolvedValue(undefined);
39
38
  });
40
39
 
41
40
  afterEach(() => {
42
- vi.resetAllMocks();
41
+ vi.restoreAllMocks();
43
42
  });
44
43
 
45
- it('should have correct static properties', () => {
46
- expect(ModuleRemoveCommand.usage).toContain('module remove');
47
- expect(ModuleRemoveCommand.description).toBeDefined();
48
- expect(ModuleRemoveCommand.requiresProject).toBe(true);
49
- expect(ModuleRemoveCommand.args).toBeDefined();
44
+ it('should identify and remove a backend module', async () => {
45
+ // Setup: Simulate module exists in backend
46
+ (fs.pathExists as unknown as { mockImplementation: any }).mockImplementation((p: string) => {
47
+ const pStr = p.toString();
48
+ if (pStr.includes('apps/backend/modules/test-mod')) return true;
49
+ if (pStr.includes('nexical.yaml')) return true;
50
+ return false;
51
+ });
52
+ (fs.readFile as unknown as { mockResolvedValue: any }).mockResolvedValue(
53
+ 'modules:\n backend:\n - test-mod',
54
+ );
55
+ (fs.writeFile as unknown as { mockResolvedValue: any }).mockResolvedValue(undefined);
56
+
57
+ await command.run({ name: 'test-mod' });
58
+
59
+ // Verify git commands
60
+ expect(cliCore.runCommand).toHaveBeenCalledWith(
61
+ expect.stringContaining('git submodule deinit -f apps/backend/modules/test-mod'),
62
+ projectRoot,
63
+ );
64
+ expect(cliCore.runCommand).toHaveBeenCalledWith(
65
+ expect.stringContaining('git rm -f apps/backend/modules/test-mod'),
66
+ projectRoot,
67
+ );
68
+
69
+ // Verify config update
70
+ expect(fs.writeFile).toHaveBeenCalledWith(
71
+ expect.stringContaining('nexical.yaml'),
72
+ expect.not.stringContaining('test-mod'),
73
+ );
50
74
  });
51
75
 
52
- it('should error if project root is missing', async () => {
53
- command = new ModuleRemoveCommand({}, { rootDir: undefined });
54
- vi.spyOn(command, 'init').mockImplementation(async () => {});
55
- vi.spyOn(command, 'error').mockImplementation(() => {});
76
+ it('should error if module not found', async () => {
77
+ (fs.pathExists as unknown as { mockResolvedValue: any }).mockResolvedValue(false);
56
78
 
57
- await command.runInit({ name: 'mod' });
58
- expect(command.error).toHaveBeenCalledWith(
59
- expect.stringContaining('requires to be run within an app project'),
60
- 1,
79
+ await command.run({ name: 'missing-mod' });
80
+
81
+ expect(command.error).toHaveBeenCalledWith(expect.stringContaining('not found'));
82
+ expect(cliCore.runCommand).not.toHaveBeenCalled();
83
+ });
84
+
85
+ it('should remove from legacy modules array', async () => {
86
+ (fs.pathExists as unknown as { mockImplementation: any }).mockImplementation((p: string) => {
87
+ const pStr = p.toString();
88
+ if (pStr.includes('apps/backend/modules/legacy-mod')) return true;
89
+ if (pStr.includes('nexical.yaml')) return true;
90
+ return false;
91
+ });
92
+ (fs.readFile as unknown as { mockResolvedValue: any }).mockResolvedValue(
93
+ 'modules:\n - other-mod\n - legacy-mod',
94
+ );
95
+
96
+ await command.run({ name: 'legacy-mod' });
97
+
98
+ expect(fs.writeFile).toHaveBeenCalledWith(
99
+ expect.stringContaining('nexical.yaml'),
100
+ expect.not.stringContaining('legacy-mod'),
61
101
  );
62
102
  });
63
103
 
64
- it('should remove submodule and sync', async () => {
65
- await command.run({ name: 'mod' });
104
+ it('should handle error during nexical.yaml update', async () => {
105
+ (fs.pathExists as unknown as { mockImplementation: any }).mockImplementation((p: string) => {
106
+ const pStr = p.toString();
107
+ if (pStr.includes('apps/backend/modules/test-mod')) return true;
108
+ if (pStr.includes('nexical.yaml')) return true;
109
+ return false;
110
+ });
111
+ (fs.readFile as unknown as { mockResolvedValue: any }).mockResolvedValue(
112
+ 'modules:\n backend:\n - test-mod',
113
+ );
114
+ (fs.writeFile as unknown as { mockRejectedValue: any }).mockRejectedValue(
115
+ new Error('Write fail'),
116
+ );
66
117
 
67
- expect(runCommand).toHaveBeenCalledWith(
68
- expect.stringContaining('git submodule deinit'),
69
- '/mock/root',
118
+ await command.run({ name: 'test-mod' });
119
+ expect(cliCore.logger.warn).toHaveBeenCalledWith(
120
+ expect.stringContaining('Failed to update nexical.yaml: Write fail'),
70
121
  );
71
- expect(runCommand).toHaveBeenCalledWith(expect.stringContaining('git rm'), '/mock/root');
72
- expect(fs.remove).toHaveBeenCalledWith(expect.stringContaining('.git/modules'));
73
- expect(runCommand).toHaveBeenCalledWith('npm install', '/mock/root');
74
122
  });
75
123
 
76
- it('should error if module not found', async () => {
77
- vi.mocked(fs.pathExists).mockImplementation(async () => false);
78
- await command.run({ name: 'missing' });
79
- expect(command.error).toHaveBeenCalledWith(expect.stringContaining('not found'));
124
+ it('should handle non-Error exception during nexical.yaml update', async () => {
125
+ (fs.pathExists as unknown as { mockImplementation: any }).mockImplementation((p: string) => {
126
+ const pStr = p.toString();
127
+ if (pStr.includes('apps/backend/modules/test-mod')) return true;
128
+ if (pStr.includes('nexical.yaml')) return true;
129
+ return false;
130
+ });
131
+ (fs.readFile as unknown as { mockResolvedValue: any }).mockResolvedValue(
132
+ 'modules:\n backend:\n - test-mod',
133
+ );
134
+ (fs.writeFile as unknown as { mockRejectedValue: any }).mockRejectedValue('String fail');
135
+
136
+ await command.run({ name: 'test-mod' });
137
+ expect(cliCore.logger.warn).toHaveBeenCalledWith(
138
+ expect.stringContaining('Failed to update nexical.yaml: String fail'),
139
+ );
80
140
  });
81
141
 
82
- it('should handle failure during remove', async () => {
83
- vi.mocked(runCommand).mockRejectedValue(new Error('Git remove failed'));
84
- await command.run({ name: 'mod' });
85
- expect(command.error).toHaveBeenCalledWith(expect.stringContaining('Failed to remove module'));
142
+ it('should handle error during run method', async () => {
143
+ (fs.pathExists as unknown as { mockImplementation: any }).mockImplementation((p: string) => {
144
+ if (p.includes('apps/backend/modules/fail-mod')) return true;
145
+ return false;
146
+ });
147
+ (cliCore.runCommand as unknown as { mockRejectedValue: any }).mockRejectedValue(
148
+ new Error('Git fail'),
149
+ );
150
+
151
+ await command.run({ name: 'fail-mod' });
152
+ expect(command.error).toHaveBeenCalledWith(
153
+ expect.stringContaining('Failed to remove module: Git fail'),
154
+ );
86
155
  });
87
156
 
88
- it('should skip .git/modules cleanup if not found', async () => {
89
- vi.mocked(fs.pathExists).mockImplementation(async (p: string) => {
90
- if (p.includes('.git/modules')) return false;
91
- return true;
157
+ it('should handle non-Error exception during run method', async () => {
158
+ (fs.pathExists as unknown as { mockImplementation: any }).mockImplementation((p: string) => {
159
+ if (p.includes('apps/backend/modules/fail-mod')) return true;
160
+ return false;
92
161
  });
93
- await command.run({ name: 'mod' });
94
- expect(fs.remove).not.toHaveBeenCalledWith(expect.stringContaining('.git/modules'));
162
+ (cliCore.runCommand as unknown as { mockRejectedValue: any }).mockRejectedValue('String error');
163
+
164
+ await command.run({ name: 'fail-mod' });
165
+ expect(command.error).toHaveBeenCalledWith(
166
+ expect.stringContaining('Failed to remove module: String error'),
167
+ );
168
+ });
169
+ it('should do nothing if nexical.yaml is missing', async () => {
170
+ (fs.pathExists as unknown as { mockImplementation: any }).mockImplementation((p: string) => {
171
+ if (p.includes('apps/backend/modules/test-mod')) return true;
172
+ if (p.includes('nexical.yaml')) return false;
173
+ return false;
174
+ });
175
+
176
+ await command.run({ name: 'test-mod' });
177
+ // Should return early and not try to read config
178
+ expect(fs.readFile).not.toHaveBeenCalled();
179
+ expect(command.success).toHaveBeenCalledWith(expect.stringContaining('removed successfully'));
95
180
  });
96
181
  });
@@ -1,95 +1,78 @@
1
- import { runCommand } from '@nexical/cli-core';
2
1
  import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
3
2
  import ModuleUpdateCommand from '../../../../src/commands/module/update.js';
4
3
  import fs from 'fs-extra';
4
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
5
+ import { runCommand, logger } from '@nexical/cli-core';
5
6
 
6
- vi.mock('@nexical/cli-core', async (importOriginal) => {
7
- const mod = await importOriginal<typeof import('@nexical/cli-core')>();
7
+ // Mocks
8
+ vi.mock('fs-extra');
9
+ vi.mock('@nexical/cli-core', async () => {
8
10
  return {
9
- ...mod,
10
- logger: {
11
- ...mod.logger,
12
- success: vi.fn(),
13
- info: vi.fn(),
14
- debug: vi.fn(),
15
- error: vi.fn(),
16
- warn: vi.fn(),
11
+ BaseCommand: class {
12
+ projectRoot = '/mock/project/root';
13
+ info = vi.fn();
14
+ success = vi.fn();
15
+ error = vi.fn();
17
16
  },
17
+ logger: { debug: vi.fn(), warn: vi.fn() },
18
18
  runCommand: vi.fn(),
19
19
  };
20
20
  });
21
- vi.mock('fs-extra');
22
21
 
23
22
  describe('ModuleUpdateCommand', () => {
24
23
  let command: ModuleUpdateCommand;
25
24
 
26
- beforeEach(async () => {
27
- vi.clearAllMocks();
28
- command = new ModuleUpdateCommand({}, { rootDir: '/mock/root' });
29
- vi.spyOn(command, 'error').mockImplementation(() => {});
30
- vi.spyOn(command, 'success').mockImplementation(() => {});
31
- vi.spyOn(command, 'info').mockImplementation(() => {});
32
- vi.mocked(fs.pathExists).mockImplementation(async (p: string) => {
33
- if (p.includes('app.yml') || p.includes('nexical.yml')) return true;
34
- return true;
35
- });
36
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
37
- vi.spyOn(process, 'exit').mockImplementation((() => {}) as any);
38
- await command.init();
39
- });
40
-
41
- afterEach(() => {
25
+ beforeEach(() => {
42
26
  vi.resetAllMocks();
27
+ command = new ModuleUpdateCommand({} as unknown as any, {} as unknown as any);
28
+ (runCommand as unknown as { mockResolvedValue: any }).mockResolvedValue(undefined);
43
29
  });
44
30
 
45
- it('should have correct static properties', () => {
46
- expect(ModuleUpdateCommand.usage).toContain('module update');
47
- expect(ModuleUpdateCommand.description).toBeDefined();
48
- expect(ModuleUpdateCommand.requiresProject).toBe(true);
49
- expect(ModuleUpdateCommand.args).toBeDefined();
50
- });
51
-
52
- it('should error if project root is missing', async () => {
53
- command = new ModuleUpdateCommand({}, { rootDir: undefined });
54
- vi.spyOn(command, 'init').mockImplementation(async () => {});
55
- vi.spyOn(command, 'error').mockImplementation(() => {});
56
-
57
- await command.runInit({});
58
- expect(command.error).toHaveBeenCalledWith(
59
- expect.stringContaining('requires to be run within an app project'),
60
- 1,
61
- );
31
+ afterEach(() => {
32
+ vi.clearAllMocks();
62
33
  });
63
34
 
64
35
  it('should update all modules if no name provided', async () => {
65
36
  await command.run({});
37
+
66
38
  expect(runCommand).toHaveBeenCalledWith(
67
- expect.stringContaining('git submodule update --remote'),
68
- '/mock/root',
39
+ 'git submodule update --remote --merge',
40
+ expect.any(String),
69
41
  );
70
- expect(runCommand).toHaveBeenCalledWith('npm install', '/mock/root');
71
42
  });
72
43
 
73
- it('should update specific module', async () => {
74
- await command.run({ name: 'mod' });
44
+ it('should update specific module if name provided', async () => {
45
+ (fs.pathExists as unknown as { mockImplementation: any }).mockImplementation((p: string) => {
46
+ return p.includes('apps/frontend/modules/ui-mod');
47
+ });
48
+
49
+ await command.run({ name: 'ui-mod' });
50
+
75
51
  expect(runCommand).toHaveBeenCalledWith(
76
- expect.stringContaining('git submodule update --remote --merge modules/mod'),
77
- '/mock/root',
52
+ expect.stringContaining('git submodule update --remote --merge apps/frontend/modules/ui-mod'),
53
+ expect.any(String),
78
54
  );
79
55
  });
80
56
 
81
- it('should handle failure during update', async () => {
82
- vi.mocked(runCommand).mockRejectedValue(new Error('Update failed'));
57
+ it('should error if specific module not found', async () => {
58
+ (fs.pathExists as unknown as { mockResolvedValue: any }).mockResolvedValue(false);
59
+ await command.run({ name: 'missing-mod' });
60
+ expect(command.error).toHaveBeenCalledWith('Module missing-mod not found.');
61
+ });
62
+
63
+ it('should handle error during update', async () => {
64
+ (runCommand as unknown as { mockRejectedValue: any }).mockRejectedValue(new Error('Git fail'));
83
65
  await command.run({});
84
- expect(command.error).toHaveBeenCalledWith(expect.stringContaining('Failed to update'));
66
+ expect(command.error).toHaveBeenCalledWith(
67
+ expect.stringContaining('Failed to update modules: Git fail'),
68
+ );
85
69
  });
86
70
 
87
- it('should error if module to update not found', async () => {
88
- vi.mocked(fs.pathExists).mockImplementation(async (p) => {
89
- // console.log('UpdateTest: pathExists check:', p);
90
- return false;
91
- });
92
- await command.run({ name: 'missing-mod' });
93
- expect(command.error).toHaveBeenCalledWith('Module missing-mod not found.');
71
+ it('should handle non-Error exception during update', async () => {
72
+ (runCommand as unknown as { mockRejectedValue: any }).mockRejectedValue('String error');
73
+ await command.run({});
74
+ expect(command.error).toHaveBeenCalledWith(
75
+ expect.stringContaining('Failed to update modules: String error'),
76
+ );
94
77
  });
95
78
  });
@@ -260,7 +260,7 @@ describe('RunCommand', () => {
260
260
  it('should fall back to default behavior if script not found in module', async () => {
261
261
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
262
262
  vi.mocked(fs.pathExists).mockImplementation(async (p: any) => {
263
- return p.includes('src/modules/mymod') || p.includes('package.json');
263
+ return p.includes('apps/backend/modules/mymod') || p.includes('package.json');
264
264
  });
265
265
  vi.mocked(fs.readJson).mockResolvedValue({
266
266
  name: 'mymod',
@@ -300,5 +300,20 @@ describe('RunCommand', () => {
300
300
  expect(command.error).toHaveBeenCalledWith(
301
301
  expect.stringContaining('does not exist in Nexical core'),
302
302
  );
303
+ expect(command.error).toHaveBeenCalledWith(
304
+ expect.stringContaining('does not exist in Nexical core'),
305
+ );
306
+ });
307
+
308
+ it('should handle non-Error exception in package.json read', async () => {
309
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
310
+ vi.mocked(fs.pathExists).mockImplementation(async (p: any) => p.includes('package.json'));
311
+ vi.mocked(fs.readJson).mockRejectedValue('String error');
312
+
313
+ await command.run({ script: 'test', args: [] });
314
+
315
+ expect(command.error).toHaveBeenCalledWith(
316
+ expect.stringContaining('Failed to read package.json at /mock/root: String error'),
317
+ );
303
318
  });
304
319
  });