@nexical/cli 0.11.0 → 0.11.2
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 +36 -30
- 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 +8 -4
- 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 +24 -13
- 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.js +9 -4
- package/dist/src/commands/setup.js.map +1 -1
- 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 +56 -32
- package/src/commands/init.ts +79 -76
- 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 +70 -60
- 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 -131
- package/test/integration/commands/init.integration.test.ts +64 -64
- 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 +138 -128
- 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,152 +1,170 @@
|
|
|
1
1
|
import { runCommand } from '@nexical/cli-core';
|
|
2
|
-
import { describe, it, expect, vi, beforeEach
|
|
2
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
3
3
|
import * as git from '../../../src/utils/git.js';
|
|
4
4
|
|
|
5
5
|
const mocks = vi.hoisted(() => ({
|
|
6
|
-
|
|
6
|
+
exec: vi.fn(),
|
|
7
7
|
}));
|
|
8
8
|
|
|
9
9
|
vi.mock('@nexical/cli-core', async (importOriginal) => {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
10
|
+
const mod = await importOriginal<typeof import('@nexical/cli-core')>();
|
|
11
|
+
return {
|
|
12
|
+
...mod,
|
|
13
|
+
runCommand: vi.fn(),
|
|
14
|
+
logger: { code: vi.fn(), debug: vi.fn() },
|
|
15
|
+
};
|
|
16
16
|
});
|
|
17
17
|
|
|
18
18
|
vi.mock('node:child_process', () => ({
|
|
19
|
-
|
|
19
|
+
exec: mocks.exec,
|
|
20
20
|
}));
|
|
21
21
|
|
|
22
22
|
vi.mock('node:util', async () => {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
|
|
23
|
+
const actual = await vi.importActual<typeof import('node:util')>('node:util');
|
|
24
|
+
return {
|
|
25
|
+
...actual,
|
|
26
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
|
27
|
+
promisify: (fn: Function) => {
|
|
28
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
29
|
+
return (...args: any[]) =>
|
|
30
|
+
new Promise((resolve, reject) => {
|
|
31
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
32
|
+
fn(...args, (err: Error | null, ...values: any[]) => {
|
|
33
|
+
if (err) return reject(err);
|
|
34
|
+
// Handle exec-like signature (stdout, stderr) -> { stdout, stderr }
|
|
35
|
+
// Simple heuristic: if values.length > 1, assume explicit mapping needed?
|
|
36
|
+
// Or just hardcode for our known usage (exec).
|
|
37
|
+
if (values.length >= 2) {
|
|
38
|
+
resolve({ stdout: values[0], stderr: values[1] });
|
|
39
|
+
} else {
|
|
40
|
+
resolve(values[0]);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
},
|
|
45
|
+
};
|
|
42
46
|
});
|
|
43
47
|
|
|
44
48
|
describe('git utils', () => {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
});
|
|
56
|
-
|
|
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
|
-
});
|
|
49
|
+
beforeEach(() => {
|
|
50
|
+
vi.clearAllMocks();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should clone repository', async () => {
|
|
54
|
+
await git.clone('http://repo.git', 'dest', { recursive: true });
|
|
55
|
+
expect(runCommand).toHaveBeenCalledWith('git clone --recursive http://repo.git .', 'dest');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should clone repository with depth', async () => {
|
|
59
|
+
await git.clone('http://repo.git', 'dest', { depth: 1 });
|
|
60
|
+
expect(runCommand).toHaveBeenCalledWith('git clone --depth 1 http://repo.git .', 'dest');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should update submodules', async () => {
|
|
64
|
+
await git.updateSubmodules('dest');
|
|
65
|
+
expect(runCommand).toHaveBeenCalledWith(
|
|
66
|
+
'git submodule foreach --recursive "git checkout main && git pull origin main"',
|
|
67
|
+
'dest',
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should checkout orphan branch', async () => {
|
|
72
|
+
await git.checkoutOrphan('branch', 'dest');
|
|
73
|
+
expect(runCommand).toHaveBeenCalledWith('git checkout --orphan branch', 'dest');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should get remote url', async () => {
|
|
77
|
+
// Mock exec to call the callback with success
|
|
78
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
79
|
+
mocks.exec.mockImplementation(((cmd: string, options: any, callback: any) => {
|
|
80
|
+
if (typeof options === 'function') {
|
|
81
|
+
callback = options;
|
|
82
|
+
options = {};
|
|
83
|
+
}
|
|
84
|
+
// callback(error, stdout, stderr)
|
|
85
|
+
callback(null, 'https://github.com/origin.git\n', '');
|
|
86
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
87
|
+
return {} as any; // exec returns a ChildProcess
|
|
88
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
89
|
+
}) as any);
|
|
90
|
+
|
|
91
|
+
const url = await git.getRemoteUrl('cwd');
|
|
92
|
+
expect(url).toBe('https://github.com/origin.git');
|
|
93
|
+
expect(mocks.exec).toHaveBeenCalledWith(
|
|
94
|
+
'git remote get-url origin',
|
|
95
|
+
{ cwd: 'cwd' },
|
|
96
|
+
expect.any(Function),
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should return empty string on getRemoteUrl failure', async () => {
|
|
101
|
+
// Mock exec to call the callback with error
|
|
102
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
103
|
+
mocks.exec.mockImplementation(((cmd: string, options: any, callback: any) => {
|
|
104
|
+
if (typeof options === 'function') callback = options;
|
|
105
|
+
callback(new Error('fail'), '', '');
|
|
106
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
107
|
+
return {} as any;
|
|
108
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
109
|
+
}) as any);
|
|
110
|
+
|
|
111
|
+
const url = await git.getRemoteUrl('cwd');
|
|
112
|
+
expect(url).toBe('');
|
|
113
|
+
});
|
|
106
114
|
});
|
|
107
115
|
|
|
108
116
|
it('should add all files', async () => {
|
|
109
|
-
|
|
110
|
-
|
|
117
|
+
await git.addAll('cwd');
|
|
118
|
+
expect(runCommand).toHaveBeenCalledWith('git add -A', 'cwd');
|
|
111
119
|
});
|
|
112
120
|
|
|
113
121
|
it('should commit', async () => {
|
|
114
|
-
|
|
115
|
-
|
|
122
|
+
await git.commit('msg', 'cwd');
|
|
123
|
+
expect(runCommand).toHaveBeenCalledWith('git commit -m "msg"', 'cwd');
|
|
116
124
|
});
|
|
117
125
|
|
|
118
126
|
it('should delete branch', async () => {
|
|
119
|
-
|
|
120
|
-
|
|
127
|
+
await git.deleteBranch('branch', 'cwd');
|
|
128
|
+
expect(runCommand).toHaveBeenCalledWith('git branch -D branch', 'cwd');
|
|
121
129
|
});
|
|
122
130
|
|
|
123
131
|
it('should rename branch', async () => {
|
|
124
|
-
|
|
125
|
-
|
|
132
|
+
await git.renameBranch('branch', 'cwd');
|
|
133
|
+
expect(runCommand).toHaveBeenCalledWith('git branch -m branch', 'cwd');
|
|
126
134
|
});
|
|
127
135
|
|
|
128
136
|
it('should remove remote', async () => {
|
|
129
|
-
|
|
130
|
-
|
|
137
|
+
await git.removeRemote('origin', 'cwd');
|
|
138
|
+
expect(runCommand).toHaveBeenCalledWith('git remote remove origin', 'cwd');
|
|
131
139
|
});
|
|
132
140
|
|
|
133
141
|
it('should check if branch exists', async () => {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
142
|
+
// Mock success
|
|
143
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
144
|
+
mocks.exec.mockImplementation(((cmd: string, options: any, callback: any) => {
|
|
145
|
+
if (typeof options === 'function') callback = options;
|
|
146
|
+
callback(null, '', '');
|
|
147
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
148
|
+
return {} as any;
|
|
149
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
150
|
+
}) as any);
|
|
151
|
+
|
|
152
|
+
expect(await git.branchExists('branch', 'cwd')).toBe(true);
|
|
153
|
+
expect(mocks.exec).toHaveBeenCalledWith(
|
|
154
|
+
'git show-ref --verify --quiet refs/heads/branch',
|
|
155
|
+
{ cwd: 'cwd' },
|
|
156
|
+
expect.any(Function),
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
// Mock failure
|
|
160
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
161
|
+
mocks.exec.mockImplementation(((cmd: string, options: any, callback: any) => {
|
|
162
|
+
if (typeof options === 'function') callback = options;
|
|
163
|
+
callback(new Error('fail'), '', '');
|
|
164
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
165
|
+
return {} as any;
|
|
166
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
167
|
+
}) as any);
|
|
168
|
+
|
|
169
|
+
expect(await git.branchExists('branch', 'cwd')).toBe(false);
|
|
152
170
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
2
|
import * as helpers from '../../utils/integration-helpers.js';
|
|
3
3
|
import fs from 'fs-extra';
|
|
4
4
|
import { execa } from 'execa';
|
|
@@ -8,65 +8,75 @@ vi.mock('fs-extra');
|
|
|
8
8
|
vi.mock('execa');
|
|
9
9
|
|
|
10
10
|
describe('Integration Helpers Unit', () => {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
vi.clearAllMocks();
|
|
13
|
+
});
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
it('runCLI should execute node with correct arguments', async () => {
|
|
16
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
17
|
+
vi.mocked(execa).mockResolvedValue({ exitCode: 0 } as any);
|
|
18
|
+
const args = ['init', 'my-project'];
|
|
19
|
+
const cwd = '/test/cwd';
|
|
20
|
+
const env = { FOO: 'bar' };
|
|
20
21
|
|
|
21
|
-
|
|
22
|
+
await helpers.runCLI(args, cwd, { env });
|
|
22
23
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
24
|
+
expect(execa).toHaveBeenCalledWith(
|
|
25
|
+
'node',
|
|
26
|
+
expect.arrayContaining([expect.stringContaining('dist/index.js'), ...args]),
|
|
27
|
+
expect.objectContaining({
|
|
28
|
+
cwd,
|
|
29
|
+
env: expect.objectContaining({ FOO: 'bar' }),
|
|
30
|
+
reject: false,
|
|
31
|
+
}),
|
|
32
|
+
);
|
|
33
|
+
});
|
|
33
34
|
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
it('createTempDir should create directory and return path', async () => {
|
|
36
|
+
vi.mocked(fs.ensureDir).mockResolvedValue(undefined);
|
|
36
37
|
|
|
37
|
-
|
|
38
|
+
const dir = await helpers.createTempDir('test-prefix-');
|
|
38
39
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
expect(dir).toContain('test-prefix-');
|
|
41
|
+
expect(fs.ensureDir).toHaveBeenCalledWith(dir);
|
|
42
|
+
});
|
|
42
43
|
|
|
43
|
-
|
|
44
|
-
|
|
44
|
+
it('cleanupTestRoot should remove test root directory', async () => {
|
|
45
|
+
vi.mocked(fs.remove).mockResolvedValue(undefined);
|
|
45
46
|
|
|
46
|
-
|
|
47
|
+
await helpers.cleanupTestRoot();
|
|
47
48
|
|
|
48
|
-
|
|
49
|
-
|
|
49
|
+
expect(fs.remove).toHaveBeenCalledWith(expect.stringContaining('.test-tmp'));
|
|
50
|
+
});
|
|
50
51
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
it('createMockRepo should initialize git repo and commit files', async () => {
|
|
53
|
+
vi.mocked(fs.ensureDir).mockResolvedValue(undefined);
|
|
54
|
+
vi.mocked(fs.outputFile).mockResolvedValue(undefined);
|
|
55
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
56
|
+
vi.mocked(execa).mockResolvedValue({} as any);
|
|
55
57
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
const dir = '/test/repo';
|
|
59
|
+
const files = {
|
|
60
|
+
'package.json': '{}',
|
|
61
|
+
'README.md': '# Test',
|
|
62
|
+
};
|
|
61
63
|
|
|
62
|
-
|
|
64
|
+
await helpers.createMockRepo(dir, files);
|
|
63
65
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
});
|
|
66
|
+
expect(fs.ensureDir).toHaveBeenCalledWith(dir);
|
|
67
|
+
expect(execa).toHaveBeenCalledWith('git', ['init'], expect.objectContaining({ cwd: dir }));
|
|
68
|
+
expect(execa).toHaveBeenCalledWith(
|
|
69
|
+
'git',
|
|
70
|
+
['config', 'user.email', 'test@test.com'],
|
|
71
|
+
expect.objectContaining({ cwd: dir }),
|
|
72
|
+
);
|
|
73
|
+
expect(fs.outputFile).toHaveBeenCalledWith(path.join(dir, 'package.json'), '{}');
|
|
74
|
+
expect(fs.outputFile).toHaveBeenCalledWith(path.join(dir, 'README.md'), '# Test');
|
|
75
|
+
expect(execa).toHaveBeenCalledWith('git', ['add', '.'], expect.objectContaining({ cwd: dir }));
|
|
76
|
+
expect(execa).toHaveBeenCalledWith(
|
|
77
|
+
'git',
|
|
78
|
+
['commit', '-m', 'Initial commit'],
|
|
79
|
+
expect.objectContaining({ cwd: dir }),
|
|
80
|
+
);
|
|
81
|
+
});
|
|
72
82
|
});
|
|
@@ -2,38 +2,50 @@ import { describe, it, expect } from 'vitest';
|
|
|
2
2
|
import { resolveGitUrl } from '../../../src/utils/url-resolver';
|
|
3
3
|
|
|
4
4
|
describe('resolveGitUrl', () => {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
5
|
+
it('should expand gh@ shorthand correctly', () => {
|
|
6
|
+
expect(resolveGitUrl('gh@nexical-cms/starter')).toBe(
|
|
7
|
+
'https://github.com/nexical-cms/starter.git',
|
|
8
|
+
);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('should expand gh@ shorthand with subpath correctly', () => {
|
|
12
|
+
expect(resolveGitUrl('gh@nexical-cms/starter//path/to/module')).toBe(
|
|
13
|
+
'https://github.com/nexical-cms/starter.git//path/to/module',
|
|
14
|
+
);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should add .git extension to standard URLs if missing', () => {
|
|
18
|
+
expect(resolveGitUrl('https://github.com/nexical-cms/starter')).toBe(
|
|
19
|
+
'https://github.com/nexical-cms/starter.git',
|
|
20
|
+
);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should preserve .git extension if already present', () => {
|
|
24
|
+
expect(resolveGitUrl('https://github.com/nexical-cms/starter.git')).toBe(
|
|
25
|
+
'https://github.com/nexical-cms/starter.git',
|
|
26
|
+
);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should handle standard URLs with subpath correctly', () => {
|
|
30
|
+
expect(resolveGitUrl('https://github.com/nexical-cms/starter//path/to/dir')).toBe(
|
|
31
|
+
'https://github.com/nexical-cms/starter.git//path/to/dir',
|
|
32
|
+
);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should handle standard URLs with .git and subpath correctly', () => {
|
|
36
|
+
expect(resolveGitUrl('https://github.com/nexical-cms/starter.git//path/to/dir')).toBe(
|
|
37
|
+
'https://github.com/nexical-cms/starter.git//path/to/dir',
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should throw error for empty url', () => {
|
|
42
|
+
expect(() => resolveGitUrl('')).toThrow('URL cannot be empty');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should not append .git to local paths', () => {
|
|
46
|
+
expect(resolveGitUrl('/tmp/local/repo')).toBe('/tmp/local/repo');
|
|
47
|
+
expect(resolveGitUrl('./local/repo')).toBe('./local/repo');
|
|
48
|
+
expect(resolveGitUrl('../local/repo')).toBe('../local/repo');
|
|
49
|
+
expect(resolveGitUrl('file:///tmp/repo')).toBe('file:///tmp/repo');
|
|
50
|
+
});
|
|
39
51
|
});
|
|
@@ -11,16 +11,20 @@ export const CLI_BIN = path.resolve(__dirname, '../../dist/index.js');
|
|
|
11
11
|
/**
|
|
12
12
|
* Runs the CLI command against the compiled binary (E2E style)
|
|
13
13
|
*/
|
|
14
|
-
export async function runCLI(
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
14
|
+
export async function runCLI(
|
|
15
|
+
args: string[],
|
|
16
|
+
cwd: string,
|
|
17
|
+
options: { env?: NodeJS.ProcessEnv; [key: string]: unknown } = {},
|
|
18
|
+
) {
|
|
19
|
+
return execa('node', [CLI_BIN, ...args], {
|
|
20
|
+
cwd,
|
|
21
|
+
...options,
|
|
22
|
+
env: {
|
|
23
|
+
...process.env,
|
|
24
|
+
...options.env,
|
|
25
|
+
},
|
|
26
|
+
reject: false, // Allow checking exit code in tests
|
|
27
|
+
});
|
|
24
28
|
}
|
|
25
29
|
|
|
26
30
|
/**
|
|
@@ -28,39 +32,42 @@ export async function runCLI(args: string[], cwd: string, options: any = {}) {
|
|
|
28
32
|
* @returns The absolute path to the temporary directory.
|
|
29
33
|
*/
|
|
30
34
|
export async function createTempDir(prefix = 'test-'): Promise<string> {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
35
|
+
const dir = path.join(TEST_ROOT, `${prefix}${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
36
|
+
await fs.ensureDir(dir);
|
|
37
|
+
return dir;
|
|
34
38
|
}
|
|
35
39
|
|
|
36
40
|
/**
|
|
37
41
|
* Cleans up the temporary test root.
|
|
38
42
|
*/
|
|
39
43
|
export async function cleanupTestRoot(): Promise<void> {
|
|
40
|
-
|
|
44
|
+
await fs.remove(TEST_ROOT);
|
|
41
45
|
}
|
|
42
46
|
|
|
43
47
|
/**
|
|
44
48
|
* Creates a mock git repository at the specified path.
|
|
45
49
|
* This is useful for testing commands that clone from a remote.
|
|
46
50
|
*/
|
|
47
|
-
export async function createMockRepo(
|
|
48
|
-
|
|
51
|
+
export async function createMockRepo(
|
|
52
|
+
dir: string,
|
|
53
|
+
initialFiles: Record<string, string> = {},
|
|
54
|
+
): Promise<string> {
|
|
55
|
+
await fs.ensureDir(dir);
|
|
49
56
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
57
|
+
// Initialize bare repo? No, usually we want a regular repo then commit,
|
|
58
|
+
// but if we want to clone FROM it locally, it acts as a remote.
|
|
59
|
+
// Let's make it a regular repo.
|
|
60
|
+
await execa('git', ['init'], { cwd: dir });
|
|
61
|
+
await execa('git', ['config', 'user.email', 'test@test.com'], { cwd: dir });
|
|
62
|
+
await execa('git', ['config', 'user.name', 'Test User'], { cwd: dir });
|
|
56
63
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
64
|
+
// Write initial files
|
|
65
|
+
for (const [filename, content] of Object.entries(initialFiles)) {
|
|
66
|
+
await fs.outputFile(path.join(dir, filename), content);
|
|
67
|
+
}
|
|
61
68
|
|
|
62
|
-
|
|
63
|
-
|
|
69
|
+
await execa('git', ['add', '.'], { cwd: dir });
|
|
70
|
+
await execa('git', ['commit', '-m', 'Initial commit'], { cwd: dir });
|
|
64
71
|
|
|
65
|
-
|
|
72
|
+
return dir;
|
|
66
73
|
}
|