@nexical/cli 0.1.6 → 0.10.0
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 +3 -3
- package/README.md +317 -104
- package/dist/chunk-JYASTIIW.js +42 -0
- package/dist/chunk-JYASTIIW.js.map +1 -0
- package/dist/chunk-LZ3YQWAR.js +2204 -0
- package/dist/chunk-LZ3YQWAR.js.map +1 -0
- package/dist/chunk-OKXOCNXP.js +105 -0
- package/dist/chunk-OKXOCNXP.js.map +1 -0
- package/dist/chunk-OYFWMYPG.js +52 -0
- package/dist/chunk-OYFWMYPG.js.map +1 -0
- package/dist/chunk-WKERTCM6.js +74 -0
- package/dist/chunk-WKERTCM6.js.map +1 -0
- package/dist/index.js +33 -6
- package/dist/index.js.map +1 -1
- package/dist/src/commands/init.d.ts +11 -0
- package/dist/src/commands/init.js +88 -0
- package/dist/src/commands/init.js.map +1 -0
- package/dist/src/commands/module/add.d.ts +14 -0
- package/dist/src/commands/module/add.js +136 -0
- package/dist/src/commands/module/add.js.map +1 -0
- package/dist/src/commands/module/list.d.ts +10 -0
- package/dist/src/commands/module/list.js +73 -0
- package/dist/src/commands/module/list.js.map +1 -0
- package/dist/src/commands/module/remove.d.ts +12 -0
- package/dist/src/commands/module/remove.js +71 -0
- package/dist/src/commands/module/remove.js.map +1 -0
- package/dist/src/commands/module/update.d.ts +11 -0
- package/dist/src/commands/module/update.js +52 -0
- package/dist/src/commands/module/update.js.map +1 -0
- package/dist/src/commands/run.d.ts +11 -0
- package/dist/src/commands/run.js +93 -0
- package/dist/src/commands/run.js.map +1 -0
- package/dist/src/utils/discovery.d.ts +13 -0
- package/dist/src/utils/discovery.js +9 -0
- package/dist/src/utils/git.d.ts +16 -0
- package/dist/src/utils/git.js +29 -0
- package/dist/src/utils/git.js.map +1 -0
- package/dist/src/utils/url-resolver.d.ts +15 -0
- package/dist/src/utils/url-resolver.js +9 -0
- package/dist/src/utils/url-resolver.js.map +1 -0
- package/index.ts +29 -5
- package/package.json +32 -30
- package/src/commands/init.ts +85 -0
- package/src/commands/module/add.ts +169 -0
- package/src/commands/module/list.ts +69 -0
- package/src/commands/module/remove.ts +74 -0
- package/src/commands/module/update.ts +50 -0
- package/src/commands/run.ts +98 -0
- package/src/utils/discovery.ts +134 -0
- package/src/utils/git.ts +65 -0
- package/src/utils/url-resolver.ts +57 -0
- package/test/e2e/lifecycle.e2e.test.ts +152 -0
- package/test/integration/commands/init.integration.test.ts +82 -0
- package/test/integration/commands/module.integration.test.ts +144 -0
- package/test/integration/commands/run.integration.test.ts +90 -0
- package/test/integration/utils/command-loading.integration.test.ts +80 -0
- package/test/unit/commands/init.test.ts +153 -0
- package/test/unit/commands/module/add.test.ts +262 -0
- package/test/unit/commands/module/list.test.ts +115 -0
- package/test/unit/commands/module/remove.test.ts +89 -0
- package/test/unit/commands/module/update.test.ts +91 -0
- package/test/unit/commands/run.test.ts +252 -0
- package/test/unit/utils/command-discovery.test.ts +176 -0
- package/test/unit/utils/git.test.ts +152 -0
- package/test/unit/utils/integration-helpers.test.ts +72 -0
- package/test/unit/utils/url-resolver.test.ts +39 -0
- package/test/utils/integration-helpers.ts +66 -0
- package/vitest.e2e.config.ts +0 -1
- package/dist/chunk-JDRAVUKK.js +0 -48
- package/dist/chunk-JDRAVUKK.js.map +0 -1
- package/dist/src/commands/admin/create-user.d.ts +0 -15
- package/dist/src/commands/admin/create-user.js +0 -49
- package/dist/src/commands/admin/create-user.js.map +0 -1
- package/dist/src/commands/branch/create.d.ts +0 -19
- package/dist/src/commands/branch/create.js +0 -59
- package/dist/src/commands/branch/create.js.map +0 -1
- package/dist/src/commands/branch/delete.d.ts +0 -15
- package/dist/src/commands/branch/delete.js +0 -50
- package/dist/src/commands/branch/delete.js.map +0 -1
- package/dist/src/commands/branch/get.d.ts +0 -15
- package/dist/src/commands/branch/get.js +0 -53
- package/dist/src/commands/branch/get.js.map +0 -1
- package/dist/src/commands/branch/list.d.ts +0 -15
- package/dist/src/commands/branch/list.js +0 -51
- package/dist/src/commands/branch/list.js.map +0 -1
- package/dist/src/commands/job/get.d.ts +0 -15
- package/dist/src/commands/job/get.js +0 -62
- package/dist/src/commands/job/get.js.map +0 -1
- package/dist/src/commands/job/list.d.ts +0 -15
- package/dist/src/commands/job/list.js +0 -57
- package/dist/src/commands/job/list.js.map +0 -1
- package/dist/src/commands/job/logs.d.ts +0 -15
- package/dist/src/commands/job/logs.js +0 -67
- package/dist/src/commands/job/logs.js.map +0 -1
- package/dist/src/commands/job/trigger.d.ts +0 -19
- package/dist/src/commands/job/trigger.js +0 -74
- package/dist/src/commands/job/trigger.js.map +0 -1
- package/dist/src/commands/login.d.ts +0 -8
- package/dist/src/commands/login.js +0 -31
- package/dist/src/commands/login.js.map +0 -1
- package/dist/src/commands/project/create.d.ts +0 -24
- package/dist/src/commands/project/create.js +0 -63
- package/dist/src/commands/project/create.js.map +0 -1
- package/dist/src/commands/project/delete.d.ts +0 -20
- package/dist/src/commands/project/delete.js +0 -58
- package/dist/src/commands/project/delete.js.map +0 -1
- package/dist/src/commands/project/get.d.ts +0 -15
- package/dist/src/commands/project/get.js +0 -49
- package/dist/src/commands/project/get.js.map +0 -1
- package/dist/src/commands/project/list.d.ts +0 -15
- package/dist/src/commands/project/list.js +0 -45
- package/dist/src/commands/project/list.js.map +0 -1
- package/dist/src/commands/project/update.d.ts +0 -19
- package/dist/src/commands/project/update.js +0 -66
- package/dist/src/commands/project/update.js.map +0 -1
- package/dist/src/commands/team/create.d.ts +0 -19
- package/dist/src/commands/team/create.js +0 -45
- package/dist/src/commands/team/create.js.map +0 -1
- package/dist/src/commands/team/delete.d.ts +0 -20
- package/dist/src/commands/team/delete.js +0 -52
- package/dist/src/commands/team/delete.js.map +0 -1
- package/dist/src/commands/team/get.d.ts +0 -15
- package/dist/src/commands/team/get.js +0 -42
- package/dist/src/commands/team/get.js.map +0 -1
- package/dist/src/commands/team/list.d.ts +0 -8
- package/dist/src/commands/team/list.js +0 -30
- package/dist/src/commands/team/list.js.map +0 -1
- package/dist/src/commands/team/member/invite.d.ts +0 -20
- package/dist/src/commands/team/member/invite.js +0 -54
- package/dist/src/commands/team/member/invite.js.map +0 -1
- package/dist/src/commands/team/member/remove.d.ts +0 -15
- package/dist/src/commands/team/member/remove.js +0 -43
- package/dist/src/commands/team/member/remove.js.map +0 -1
- package/dist/src/commands/team/update.d.ts +0 -19
- package/dist/src/commands/team/update.js +0 -55
- package/dist/src/commands/team/update.js.map +0 -1
- package/dist/src/commands/token/generate.d.ts +0 -19
- package/dist/src/commands/token/generate.js +0 -48
- package/dist/src/commands/token/generate.js.map +0 -1
- package/dist/src/commands/token/list.d.ts +0 -8
- package/dist/src/commands/token/list.js +0 -31
- package/dist/src/commands/token/list.js.map +0 -1
- package/dist/src/commands/token/revoke.d.ts +0 -15
- package/dist/src/commands/token/revoke.js +0 -38
- package/dist/src/commands/token/revoke.js.map +0 -1
- package/dist/src/commands/whoami.d.ts +0 -8
- package/dist/src/commands/whoami.js +0 -26
- package/dist/src/commands/whoami.js.map +0 -1
- package/dist/src/utils/nexical-client.d.ts +0 -10
- package/dist/src/utils/nexical-client.js +0 -12
- package/src/commands/admin/create-user.ts +0 -46
- package/src/commands/branch/create.ts +0 -57
- package/src/commands/branch/delete.ts +0 -47
- package/src/commands/branch/get.ts +0 -50
- package/src/commands/branch/list.ts +0 -50
- package/src/commands/job/get.ts +0 -59
- package/src/commands/job/list.ts +0 -56
- package/src/commands/job/logs.ts +0 -67
- package/src/commands/job/trigger.ts +0 -73
- package/src/commands/login.ts +0 -31
- package/src/commands/project/create.ts +0 -61
- package/src/commands/project/delete.ts +0 -56
- package/src/commands/project/get.ts +0 -46
- package/src/commands/project/list.ts +0 -44
- package/src/commands/project/update.ts +0 -63
- package/src/commands/team/create.ts +0 -43
- package/src/commands/team/delete.ts +0 -50
- package/src/commands/team/get.ts +0 -39
- package/src/commands/team/list.ts +0 -26
- package/src/commands/team/member/invite.ts +0 -56
- package/src/commands/team/member/remove.ts +0 -40
- package/src/commands/team/update.ts +0 -53
- package/src/commands/token/generate.ts +0 -45
- package/src/commands/token/list.ts +0 -27
- package/src/commands/token/revoke.ts +0 -35
- package/src/commands/whoami.ts +0 -21
- package/src/utils/nexical-client.ts +0 -47
- package/test/e2e/auth.e2e.test.ts +0 -46
- package/test/e2e/job-workflow.e2e.test.ts +0 -33
- package/test/e2e/project-lifecycle.e2e.test.ts +0 -48
- package/test/e2e/setup.ts +0 -237
- package/test/e2e/utils.ts +0 -33
- package/test/integration/commands/admin/create-user.test.ts +0 -51
- package/test/integration/commands/branch/create.test.ts +0 -51
- package/test/integration/commands/branch/delete.test.ts +0 -43
- package/test/integration/commands/branch/get.test.ts +0 -49
- package/test/integration/commands/branch/list.test.ts +0 -47
- package/test/integration/commands/job/get.test.ts +0 -54
- package/test/integration/commands/job/list.test.ts +0 -47
- package/test/integration/commands/job/logs.test.ts +0 -47
- package/test/integration/commands/job/trigger.test.ts +0 -57
- package/test/integration/commands/login.test.ts +0 -62
- package/test/integration/commands/project/create.test.ts +0 -53
- package/test/integration/commands/project/delete.test.ts +0 -43
- package/test/integration/commands/project/get.test.ts +0 -51
- package/test/integration/commands/project/list.test.ts +0 -47
- package/test/integration/commands/project/update.test.ts +0 -53
- package/test/integration/commands/team/create.test.ts +0 -53
- package/test/integration/commands/team/delete.test.ts +0 -43
- package/test/integration/commands/team/get.test.ts +0 -50
- package/test/integration/commands/team/list.test.ts +0 -47
- package/test/integration/commands/team/member/invite.test.ts +0 -46
- package/test/integration/commands/team/member/remove.test.ts +0 -43
- package/test/integration/commands/team/update.test.ts +0 -50
- package/test/integration/commands/token/generate.test.ts +0 -51
- package/test/integration/commands/token/list.test.ts +0 -47
- package/test/integration/commands/token/revoke.test.ts +0 -43
- package/test/integration/commands/whoami.test.ts +0 -49
- package/test/unit/commands/admin/create-user.test.ts +0 -51
- package/test/unit/commands/branch/create.test.ts +0 -57
- package/test/unit/commands/branch/delete.test.ts +0 -49
- package/test/unit/commands/branch/get.test.ts +0 -67
- package/test/unit/commands/branch/list.test.ts +0 -62
- package/test/unit/commands/job/get.test.ts +0 -76
- package/test/unit/commands/job/list.test.ts +0 -62
- package/test/unit/commands/job/logs.test.ts +0 -60
- package/test/unit/commands/job/trigger.test.ts +0 -75
- package/test/unit/commands/login.test.ts +0 -64
- package/test/unit/commands/project/create.test.ts +0 -64
- package/test/unit/commands/project/delete.test.ts +0 -72
- package/test/unit/commands/project/get.test.ts +0 -73
- package/test/unit/commands/project/list.test.ts +0 -62
- package/test/unit/commands/project/update.test.ts +0 -58
- package/test/unit/commands/team/create.test.ts +0 -68
- package/test/unit/commands/team/delete.test.ts +0 -71
- package/test/unit/commands/team/get.test.ts +0 -70
- package/test/unit/commands/team/list.test.ts +0 -56
- package/test/unit/commands/team/member/invite.test.ts +0 -52
- package/test/unit/commands/team/member/remove.test.ts +0 -49
- package/test/unit/commands/team/update.test.ts +0 -63
- package/test/unit/commands/token/generate.test.ts +0 -65
- package/test/unit/commands/token/list.test.ts +0 -58
- package/test/unit/commands/token/revoke.test.ts +0 -49
- package/test/unit/commands/whoami.test.ts +0 -49
- package/test/unit/utils/nexical-client.test.ts +0 -113
- /package/dist/src/utils/{nexical-client.js.map → discovery.js.map} +0 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { logger, runCommand } from '@nexical/cli-core';
|
|
2
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
3
|
+
import InitCommand from '../../../src/commands/init.js';
|
|
4
|
+
import * as git from '../../../src/utils/git.js';
|
|
5
|
+
import fs from 'fs-extra';
|
|
6
|
+
|
|
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
|
+
}
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
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()
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
vi.mock('fs-extra');
|
|
31
|
+
|
|
32
|
+
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
|
+
});
|
|
56
|
+
});
|
|
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
|
+
);
|
|
122
|
+
});
|
|
123
|
+
|
|
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 });
|
|
132
|
+
});
|
|
133
|
+
|
|
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);
|
|
138
|
+
|
|
139
|
+
await expect(command.run({ directory: 'existing-dir', repo: 'foo' }))
|
|
140
|
+
.rejects.toThrow('Process.exit(1)');
|
|
141
|
+
|
|
142
|
+
expect(command.error).toHaveBeenCalledWith(expect.stringContaining('not empty'));
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('should handle git errors gracefully', async () => {
|
|
146
|
+
vi.mocked(git.clone).mockRejectedValueOnce(new Error('Git fail'));
|
|
147
|
+
|
|
148
|
+
await expect(command.run({ directory: 'fail-project', repo: 'foo' }))
|
|
149
|
+
.rejects.toThrow('Process.exit(1)');
|
|
150
|
+
|
|
151
|
+
expect(command.error).toHaveBeenCalledWith(expect.stringContaining('Failed to initialize project'));
|
|
152
|
+
});
|
|
153
|
+
});
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { logger, runCommand } from '@nexical/cli-core';
|
|
2
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
3
|
+
import ModuleAddCommand from '../../../../src/commands/module/add.js';
|
|
4
|
+
import fs from 'fs-extra';
|
|
5
|
+
import * as git from '../../../../src/utils/git.js';
|
|
6
|
+
|
|
7
|
+
vi.mock('@nexical/cli-core', async (importOriginal) => {
|
|
8
|
+
const mod = await importOriginal<typeof import('@nexical/cli-core')>();
|
|
9
|
+
return {
|
|
10
|
+
...mod,
|
|
11
|
+
logger: {
|
|
12
|
+
...mod.logger,
|
|
13
|
+
success: vi.fn(),
|
|
14
|
+
info: vi.fn(),
|
|
15
|
+
debug: vi.fn(),
|
|
16
|
+
error: vi.fn(),
|
|
17
|
+
warn: vi.fn(),
|
|
18
|
+
},
|
|
19
|
+
runCommand: vi.fn(),
|
|
20
|
+
};
|
|
21
|
+
});
|
|
22
|
+
vi.mock('fs-extra');
|
|
23
|
+
vi.mock('../../../../src/utils/git.js', () => ({
|
|
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()
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
describe('ModuleAddCommand', () => {
|
|
38
|
+
let command: ModuleAddCommand;
|
|
39
|
+
|
|
40
|
+
beforeEach(async () => {
|
|
41
|
+
vi.clearAllMocks();
|
|
42
|
+
command = new ModuleAddCommand({}, { rootDir: '/mock/root' });
|
|
43
|
+
vi.spyOn(command, 'error').mockImplementation((() => { }) as any);
|
|
44
|
+
vi.spyOn(command, 'success').mockImplementation((() => { }) as any);
|
|
45
|
+
vi.spyOn(command, 'info').mockImplementation((() => { }) as any);
|
|
46
|
+
|
|
47
|
+
// Setup mocks
|
|
48
|
+
vi.mocked(fs.ensureDir).mockImplementation(async () => { });
|
|
49
|
+
vi.mocked(fs.remove).mockImplementation(async () => { });
|
|
50
|
+
vi.mocked(fs.pathExists).mockImplementation(async (p: string) => {
|
|
51
|
+
// We don't rely on this for init anymore since we force projectRoot
|
|
52
|
+
return false;
|
|
53
|
+
});
|
|
54
|
+
vi.mocked(fs.readFile).mockResolvedValue('name: test-module\n' as any);
|
|
55
|
+
|
|
56
|
+
// Mock git default behaviors
|
|
57
|
+
vi.mocked(git.clone).mockResolvedValue(undefined as any);
|
|
58
|
+
vi.mocked(git.getRemoteUrl).mockResolvedValue('' as any);
|
|
59
|
+
|
|
60
|
+
// Force project root
|
|
61
|
+
await command.init();
|
|
62
|
+
(command as any).projectRoot = '/mock/root';
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
afterEach(() => {
|
|
66
|
+
vi.resetAllMocks();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should have correct static properties', () => {
|
|
70
|
+
expect(ModuleAddCommand.usage).toContain('module add');
|
|
71
|
+
expect(ModuleAddCommand.description).toBeDefined();
|
|
72
|
+
expect(ModuleAddCommand.requiresProject).toBe(true);
|
|
73
|
+
expect(ModuleAddCommand.args).toBeDefined();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should error if project root is missing', async () => {
|
|
77
|
+
command = new ModuleAddCommand({}, { rootDir: undefined });
|
|
78
|
+
vi.spyOn(command, 'error').mockImplementation(() => { });
|
|
79
|
+
// Ensure init doesn't set it (mocked in beforeEach but this constructor overrides logic?)
|
|
80
|
+
// In beforeEach, we call command.init() then set projectRoot.
|
|
81
|
+
// Here we just created new command.
|
|
82
|
+
vi.spyOn(command, 'init').mockImplementation(async () => { });
|
|
83
|
+
|
|
84
|
+
await command.runInit({ url: 'arg' });
|
|
85
|
+
expect(command.error).toHaveBeenCalledWith(expect.stringContaining('requires to be run within an app project'), 1);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should handle gh@ syntax with .git suffix', async () => {
|
|
89
|
+
// We mock fs.pathExists to return false for targetDir to trigger install
|
|
90
|
+
// return true for stagingDir to simulate clone success
|
|
91
|
+
// return true for module.yaml check
|
|
92
|
+
vi.mocked(fs.pathExists).mockResolvedValue(false as any); // Default
|
|
93
|
+
|
|
94
|
+
await command.run({ url: 'gh@org/repo.git' }); // With .git suffix
|
|
95
|
+
|
|
96
|
+
// Should NOT append another .git
|
|
97
|
+
expect(git.clone).toHaveBeenCalledWith(
|
|
98
|
+
'https://github.com/org/repo.git',
|
|
99
|
+
expect.any(String),
|
|
100
|
+
expect.objectContaining({ depth: 1 })
|
|
101
|
+
);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should error if url is missing', async () => {
|
|
105
|
+
await command.run({ url: undefined });
|
|
106
|
+
expect(command.error).toHaveBeenCalledWith('Please specify a repository URL.');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should error if module.yaml is missing', async () => {
|
|
110
|
+
vi.mocked(fs.pathExists).mockResolvedValue(false as any); // No yaml found
|
|
111
|
+
await command.run({ url: 'https://github.com/org/repo.git' });
|
|
112
|
+
expect(command.error).toHaveBeenCalledWith(expect.stringContaining('No module.yaml found'));
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('should error if module.yaml is missing in subdirectory', async () => {
|
|
116
|
+
vi.mocked(fs.pathExists).mockResolvedValue(false as any); // No yaml
|
|
117
|
+
|
|
118
|
+
// We mocked fs.pathExists to return false for everything in this test setup unless selective
|
|
119
|
+
// But the run() logic:
|
|
120
|
+
// await clone(...)
|
|
121
|
+
// if (subPath) ...
|
|
122
|
+
// if (!exists) throw
|
|
123
|
+
|
|
124
|
+
await command.run({ url: 'https://github.com/org/repo.git//subdir' });
|
|
125
|
+
expect(command.error).toHaveBeenCalledWith(expect.stringContaining('No module.yaml found in https://github.com/org/repo.git//subdir'));
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('should error if name is missing in module.yaml', async () => {
|
|
129
|
+
vi.mocked(fs.pathExists).mockResolvedValueOnce(false as any).mockResolvedValueOnce(true as any);
|
|
130
|
+
vi.mocked(fs.readFile).mockResolvedValueOnce('dependencies: []' as any); // No name
|
|
131
|
+
await command.run({ url: 'https://github.com/org/repo.git' });
|
|
132
|
+
expect(command.error).toHaveBeenCalledWith(expect.stringContaining('missing \'name\' in module.yaml'));
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('should handle generic errors during install', async () => {
|
|
136
|
+
vi.mocked(git.clone).mockRejectedValue(new Error('Clone failed'));
|
|
137
|
+
await command.run({ url: 'https://github.com/org/repo.git' });
|
|
138
|
+
expect(command.error).toHaveBeenCalledWith(expect.stringContaining('Failed to add module: Clone failed'));
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should install a module using git submodule add', async () => {
|
|
142
|
+
vi.mocked(fs.pathExists).mockResolvedValueOnce(false as any); // Staging yaml
|
|
143
|
+
vi.mocked(fs.pathExists).mockResolvedValueOnce(true as any); // module.yaml in staging exists
|
|
144
|
+
vi.mocked(fs.readFile).mockResolvedValueOnce('name: my-module\n' as any);
|
|
145
|
+
|
|
146
|
+
await command.run({ url: 'https://github.com/org/repo.git' });
|
|
147
|
+
|
|
148
|
+
expect(git.clone).toHaveBeenCalledWith(
|
|
149
|
+
'https://github.com/org/repo.git',
|
|
150
|
+
expect.stringContaining('staging-'),
|
|
151
|
+
{ depth: 1 }
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
// Should use submodule add
|
|
155
|
+
expect(runCommand).toHaveBeenCalledWith(
|
|
156
|
+
expect.stringContaining('git submodule add https://github.com/org/repo.git modules/my-module'),
|
|
157
|
+
'/mock/root'
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
expect(runCommand).toHaveBeenCalledWith('npm install', '/mock/root');
|
|
161
|
+
expect(command.success).toHaveBeenCalledWith('All modules installed successfully.');
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('should recursively install dependencies', async () => {
|
|
165
|
+
// First module calls
|
|
166
|
+
vi.mocked(fs.pathExists)
|
|
167
|
+
.mockResolvedValueOnce(false as any).mockResolvedValueOnce(true as any) // module 1 yaml exists
|
|
168
|
+
.mockResolvedValueOnce(false as any) // target dir check
|
|
169
|
+
.mockResolvedValueOnce(false as any).mockResolvedValueOnce(true as any) // module 2 yaml exists
|
|
170
|
+
.mockResolvedValueOnce(false as any); // target dir check
|
|
171
|
+
|
|
172
|
+
vi.mocked(fs.readFile)
|
|
173
|
+
.mockResolvedValueOnce('name: parent\ndependencies:\n - gh@org/child' as any)
|
|
174
|
+
.mockResolvedValueOnce('name: child' as any);
|
|
175
|
+
|
|
176
|
+
await command.run({ url: 'gh@org/parent' });
|
|
177
|
+
|
|
178
|
+
// Should clone parent
|
|
179
|
+
expect(git.clone).toHaveBeenCalledWith(
|
|
180
|
+
expect.stringContaining('parent.git'),
|
|
181
|
+
expect.anything(),
|
|
182
|
+
expect.anything()
|
|
183
|
+
);
|
|
184
|
+
// Should clone child
|
|
185
|
+
expect(git.clone).toHaveBeenCalledWith(
|
|
186
|
+
expect.stringContaining('child.git'),
|
|
187
|
+
expect.anything(),
|
|
188
|
+
expect.anything()
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
expect(runCommand).toHaveBeenCalledTimes(3); // 2 submodules + npm install
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('should handle object-style dependencies', async () => {
|
|
195
|
+
vi.mocked(fs.pathExists)
|
|
196
|
+
.mockResolvedValueOnce(false as any).mockResolvedValueOnce(true as any) // module exists
|
|
197
|
+
.mockResolvedValueOnce(false as any); // target dir check
|
|
198
|
+
|
|
199
|
+
vi.mocked(fs.readFile)
|
|
200
|
+
.mockResolvedValueOnce('name: parent\ndependencies:\n gh@org/child: main' as any); // Object style
|
|
201
|
+
|
|
202
|
+
await command.run({ url: 'gh@org/parent' });
|
|
203
|
+
|
|
204
|
+
expect(git.clone).toHaveBeenCalledTimes(2); // parent + child
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('should detect conflicts (installed but different origin)', async () => {
|
|
208
|
+
vi.mocked(fs.pathExists).mockResolvedValueOnce(false as any).mockResolvedValueOnce(true as any); // staging yaml
|
|
209
|
+
vi.mocked(fs.readFile).mockResolvedValueOnce('name: conflict-mod' as any);
|
|
210
|
+
|
|
211
|
+
// Target dir check returns true (exists)
|
|
212
|
+
vi.mocked(fs.pathExists).mockResolvedValueOnce(true as any);
|
|
213
|
+
|
|
214
|
+
// Origin check returns different URL
|
|
215
|
+
vi.mocked(git.getRemoteUrl).mockResolvedValueOnce('https://other.com/repo.git' as any);
|
|
216
|
+
|
|
217
|
+
await command.run({ url: 'https://github.com/org/repo.git' });
|
|
218
|
+
|
|
219
|
+
expect(command.error).toHaveBeenCalledWith(
|
|
220
|
+
expect.stringContaining('Dependency Conflict! Module \'conflict-mod\' exists but remote')
|
|
221
|
+
);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('should skip installation if same module/origin already exists', async () => {
|
|
225
|
+
vi.mocked(fs.pathExists).mockResolvedValueOnce(false as any).mockResolvedValueOnce(true as any); // staging yaml
|
|
226
|
+
vi.mocked(fs.readFile).mockResolvedValueOnce('name: existing-mod' as any);
|
|
227
|
+
vi.mocked(fs.pathExists).mockResolvedValueOnce(true as any); // Exists
|
|
228
|
+
vi.mocked(git.getRemoteUrl).mockResolvedValueOnce('https://github.com/org/repo.git' as any); // Same URL
|
|
229
|
+
|
|
230
|
+
await command.run({ url: 'https://github.com/org/repo.git' });
|
|
231
|
+
|
|
232
|
+
expect(command.info).toHaveBeenCalledWith('Module existing-mod already installed.');
|
|
233
|
+
// Should NOT call submodule add
|
|
234
|
+
expect(runCommand).not.toHaveBeenCalledWith(expect.stringContaining('git submodule add'), expect.anything());
|
|
235
|
+
// But SHOULD call npm install at end
|
|
236
|
+
expect(runCommand).toHaveBeenCalledWith('npm install', '/mock/root');
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('should handle circular dependencies', async () => {
|
|
240
|
+
// Module A depends on B, B depends on A
|
|
241
|
+
// A
|
|
242
|
+
vi.mocked(fs.pathExists).mockResolvedValue(true as any); // Simplify pathExists to always true for yamls
|
|
243
|
+
vi.mocked(fs.readFile)
|
|
244
|
+
.mockResolvedValueOnce('name: mod-a\ndependencies:\n - gh@org/mod-b' as any)
|
|
245
|
+
.mockResolvedValueOnce('name: mod-b\ndependencies:\n - gh@org/mod-a' as any);
|
|
246
|
+
|
|
247
|
+
// Target dir checks (false = not installed)
|
|
248
|
+
// We need to carefully mock pathExists sequence or use implementation based on path
|
|
249
|
+
vi.mocked(fs.pathExists).mockImplementation(async (p: string) => {
|
|
250
|
+
if (p.includes('modules')) return false; // Not installed yet
|
|
251
|
+
return true; // Yaml exists
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
await command.run({ url: 'gh@org/mod-a' });
|
|
255
|
+
|
|
256
|
+
// Should install A and B, then see A again and skip
|
|
257
|
+
expect(git.clone).toHaveBeenCalledTimes(2);
|
|
258
|
+
// Should succeed
|
|
259
|
+
expect(command.success).toHaveBeenCalled();
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { BaseCommand, logger } from '@nexical/cli-core';
|
|
2
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
3
|
+
import ModuleListCommand from '../../../../src/commands/module/list.js';
|
|
4
|
+
import fs from 'fs-extra';
|
|
5
|
+
|
|
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
|
+
vi.mock('fs-extra');
|
|
22
|
+
|
|
23
|
+
describe('ModuleListCommand', () => {
|
|
24
|
+
let command: ModuleListCommand;
|
|
25
|
+
let consoleTableSpy: any;
|
|
26
|
+
|
|
27
|
+
beforeEach(async () => {
|
|
28
|
+
vi.clearAllMocks();
|
|
29
|
+
command = new ModuleListCommand({}, { rootDir: '/mock/root' });
|
|
30
|
+
consoleTableSpy = vi.spyOn(console, 'table').mockImplementation(() => { });
|
|
31
|
+
vi.spyOn(command, 'error').mockImplementation(() => { });
|
|
32
|
+
vi.spyOn(command, 'success').mockImplementation(() => { });
|
|
33
|
+
vi.spyOn(command, 'info').mockImplementation(() => { });
|
|
34
|
+
vi.mocked(fs.pathExists).mockImplementation(async (p: any) => {
|
|
35
|
+
if (p.includes('app.yml') || p.includes('nexical.yml')) return true;
|
|
36
|
+
return true;
|
|
37
|
+
});
|
|
38
|
+
vi.spyOn(process, 'exit').mockImplementation((() => { }) as any);
|
|
39
|
+
await command.init();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
afterEach(() => {
|
|
43
|
+
vi.resetAllMocks();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should have correct static properties', () => {
|
|
47
|
+
expect(ModuleListCommand.usage).toContain('module list');
|
|
48
|
+
expect(ModuleListCommand.description).toBeDefined();
|
|
49
|
+
expect(ModuleListCommand.requiresProject).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should error if project root is missing', async () => {
|
|
53
|
+
command = new ModuleListCommand({}, { 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(expect.stringContaining('requires to be run within an app project'), 1);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should handle missing modules directory', async () => {
|
|
62
|
+
vi.mocked(fs.pathExists).mockImplementation(async () => false);
|
|
63
|
+
await command.run();
|
|
64
|
+
expect(command.info).toHaveBeenCalledWith(expect.stringContaining('No modules installed'));
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should list modules with details', async () => {
|
|
68
|
+
vi.mocked(fs.readdir).mockResolvedValue(['mod1', 'file.txt', 'mod2', 'mod3', 'mod4'] as any);
|
|
69
|
+
// Mock directory check: mod1=dir, file.txt=file, mod2=dir, mod3=dir
|
|
70
|
+
vi.mocked(fs.stat).mockImplementation(async (p: any) => ({
|
|
71
|
+
isDirectory: () => !p.includes('file.txt')
|
|
72
|
+
} as any));
|
|
73
|
+
|
|
74
|
+
// Mock package.json existence: mod1=yes, mod2=no, mod3=yes
|
|
75
|
+
// Also ensure modules directory itself exists!
|
|
76
|
+
vi.mocked(fs.pathExists).mockImplementation(async (p: any) => {
|
|
77
|
+
if (p.includes('app.yml') || p.includes('nexical.yml')) return true;
|
|
78
|
+
if (p.endsWith('/modules')) return true;
|
|
79
|
+
return p.includes('package.json') && !p.includes('mod2');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Mock reading json: mod1=valid, mod3=invalid, mod4=empty
|
|
83
|
+
vi.mocked(fs.readJson).mockImplementation(async (p: any) => {
|
|
84
|
+
if (p.includes('mod3')) throw new Error('Invalid JSON');
|
|
85
|
+
if (p.includes('mod4')) return {}; // No version/desc
|
|
86
|
+
return { version: '1.0.0', description: 'Desc' };
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
await command.run();
|
|
90
|
+
|
|
91
|
+
// mod1: listed with version
|
|
92
|
+
// file.txt: ignored
|
|
93
|
+
// mod2: listed with unknown version (dir exists, no pkg.json)
|
|
94
|
+
// mod3: listed with unknown version (invalid pkg.json)
|
|
95
|
+
// mod4: listed with unknown/empty (fallback logic)
|
|
96
|
+
expect(consoleTableSpy).toHaveBeenCalledWith(expect.arrayContaining([
|
|
97
|
+
{ name: 'mod1', version: '1.0.0', description: 'Desc' },
|
|
98
|
+
{ name: 'mod2', version: 'unknown', description: '' },
|
|
99
|
+
{ name: 'mod3', version: 'unknown', description: '' },
|
|
100
|
+
{ name: 'mod4', version: 'unknown', description: '' }
|
|
101
|
+
]));
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should handle empty modules directory', async () => {
|
|
105
|
+
vi.mocked(fs.readdir).mockResolvedValue([] as any);
|
|
106
|
+
await command.run();
|
|
107
|
+
expect(command.info).toHaveBeenCalledWith('No modules installed.');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should handle failure during list', async () => {
|
|
111
|
+
vi.mocked(fs.readdir).mockRejectedValue(new Error('FS Error'));
|
|
112
|
+
await command.run();
|
|
113
|
+
expect(command.error).toHaveBeenCalledWith(expect.stringContaining('Failed to list modules'));
|
|
114
|
+
});
|
|
115
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { logger, runCommand } from '@nexical/cli-core';
|
|
2
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
3
|
+
import ModuleRemoveCommand from '../../../../src/commands/module/remove.js';
|
|
4
|
+
import fs from 'fs-extra';
|
|
5
|
+
|
|
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
|
+
vi.mock('fs-extra');
|
|
22
|
+
|
|
23
|
+
describe('ModuleRemoveCommand', () => {
|
|
24
|
+
let command: ModuleRemoveCommand;
|
|
25
|
+
|
|
26
|
+
beforeEach(async () => {
|
|
27
|
+
vi.clearAllMocks();
|
|
28
|
+
command = new ModuleRemoveCommand({}, { rootDir: '/mock/root' });
|
|
29
|
+
vi.spyOn(command, 'error').mockImplementation((() => { }) as any);
|
|
30
|
+
vi.spyOn(command, 'success').mockImplementation((() => { }) as any);
|
|
31
|
+
vi.spyOn(command, 'info').mockImplementation((() => { }) as any);
|
|
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
|
+
vi.spyOn(process, 'exit').mockImplementation((() => { }) as any);
|
|
37
|
+
await command.init();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
afterEach(() => {
|
|
41
|
+
vi.resetAllMocks();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should have correct static properties', () => {
|
|
45
|
+
expect(ModuleRemoveCommand.usage).toContain('module remove');
|
|
46
|
+
expect(ModuleRemoveCommand.description).toBeDefined();
|
|
47
|
+
expect(ModuleRemoveCommand.requiresProject).toBe(true);
|
|
48
|
+
expect(ModuleRemoveCommand.args).toBeDefined();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should error if project root is missing', async () => {
|
|
52
|
+
command = new ModuleRemoveCommand({}, { rootDir: undefined });
|
|
53
|
+
vi.spyOn(command, 'init').mockImplementation(async () => { });
|
|
54
|
+
vi.spyOn(command, 'error').mockImplementation((() => { }) as any);
|
|
55
|
+
|
|
56
|
+
await command.runInit({ name: 'mod' });
|
|
57
|
+
expect(command.error).toHaveBeenCalledWith(expect.stringContaining('requires to be run within an app project'), 1);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should remove submodule and sync', async () => {
|
|
61
|
+
await command.run({ name: 'mod' });
|
|
62
|
+
|
|
63
|
+
expect(runCommand).toHaveBeenCalledWith(expect.stringContaining('git submodule deinit'), '/mock/root');
|
|
64
|
+
expect(runCommand).toHaveBeenCalledWith(expect.stringContaining('git rm'), '/mock/root');
|
|
65
|
+
expect(fs.remove).toHaveBeenCalledWith(expect.stringContaining('.git/modules'));
|
|
66
|
+
expect(runCommand).toHaveBeenCalledWith('npm install', '/mock/root');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should error if module not found', async () => {
|
|
70
|
+
vi.mocked(fs.pathExists).mockImplementation(async () => false);
|
|
71
|
+
await command.run({ name: 'missing' });
|
|
72
|
+
expect(command.error).toHaveBeenCalledWith(expect.stringContaining('not found'));
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should handle failure during remove', async () => {
|
|
76
|
+
vi.mocked(runCommand).mockRejectedValue(new Error('Git remove failed'));
|
|
77
|
+
await command.run({ name: 'mod' });
|
|
78
|
+
expect(command.error).toHaveBeenCalledWith(expect.stringContaining('Failed to remove module'));
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should skip .git/modules cleanup if not found', async () => {
|
|
82
|
+
vi.mocked(fs.pathExists).mockImplementation(async (p: string) => {
|
|
83
|
+
if (p.includes('.git/modules')) return false;
|
|
84
|
+
return true;
|
|
85
|
+
});
|
|
86
|
+
await command.run({ name: 'mod' });
|
|
87
|
+
expect(fs.remove).not.toHaveBeenCalledWith(expect.stringContaining('.git/modules'));
|
|
88
|
+
});
|
|
89
|
+
});
|