@nexical/cli 0.11.0 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/.github/workflows/deploy.yml +1 -1
  2. package/.husky/pre-commit +1 -0
  3. package/.prettierignore +8 -0
  4. package/.prettierrc +7 -0
  5. package/GEMINI.md +36 -30
  6. package/README.md +85 -56
  7. package/dist/chunk-AC4B3HPJ.js +93 -0
  8. package/dist/chunk-AC4B3HPJ.js.map +1 -0
  9. package/dist/{chunk-JYASTIIW.js → chunk-PJIOCW2A.js} +1 -1
  10. package/dist/chunk-PJIOCW2A.js.map +1 -0
  11. package/dist/{chunk-WKERTCM6.js → chunk-Q7YLW5HJ.js} +5 -2
  12. package/dist/chunk-Q7YLW5HJ.js.map +1 -0
  13. package/dist/index.js +41 -12
  14. package/dist/index.js.map +1 -1
  15. package/dist/src/commands/init.d.ts +4 -1
  16. package/dist/src/commands/init.js +8 -4
  17. package/dist/src/commands/init.js.map +1 -1
  18. package/dist/src/commands/module/add.d.ts +3 -1
  19. package/dist/src/commands/module/add.js +24 -13
  20. package/dist/src/commands/module/add.js.map +1 -1
  21. package/dist/src/commands/module/list.js +9 -5
  22. package/dist/src/commands/module/list.js.map +1 -1
  23. package/dist/src/commands/module/remove.d.ts +3 -1
  24. package/dist/src/commands/module/remove.js +13 -7
  25. package/dist/src/commands/module/remove.js.map +1 -1
  26. package/dist/src/commands/module/update.d.ts +3 -1
  27. package/dist/src/commands/module/update.js +7 -5
  28. package/dist/src/commands/module/update.js.map +1 -1
  29. package/dist/src/commands/run.d.ts +4 -1
  30. package/dist/src/commands/run.js +10 -2
  31. package/dist/src/commands/run.js.map +1 -1
  32. package/dist/src/commands/setup.js +17 -4
  33. package/dist/src/commands/setup.js.map +1 -1
  34. package/dist/src/utils/discovery.js +1 -1
  35. package/dist/src/utils/git.js +1 -1
  36. package/dist/src/utils/url-resolver.js +1 -1
  37. package/eslint.config.mjs +67 -0
  38. package/index.ts +34 -20
  39. package/package.json +56 -32
  40. package/src/commands/init.ts +79 -76
  41. package/src/commands/module/add.ts +158 -148
  42. package/src/commands/module/list.ts +61 -50
  43. package/src/commands/module/remove.ts +59 -54
  44. package/src/commands/module/update.ts +44 -42
  45. package/src/commands/run.ts +89 -81
  46. package/src/commands/setup.ts +78 -60
  47. package/src/utils/discovery.ts +98 -113
  48. package/src/utils/git.ts +35 -28
  49. package/src/utils/url-resolver.ts +50 -45
  50. package/test/e2e/lifecycle.e2e.test.ts +139 -131
  51. package/test/integration/commands/init.integration.test.ts +64 -64
  52. package/test/integration/commands/module.integration.test.ts +122 -122
  53. package/test/integration/commands/run.integration.test.ts +70 -63
  54. package/test/integration/utils/command-loading.integration.test.ts +40 -53
  55. package/test/unit/commands/init.test.ts +163 -128
  56. package/test/unit/commands/module/add.test.ts +312 -245
  57. package/test/unit/commands/module/list.test.ts +108 -91
  58. package/test/unit/commands/module/remove.test.ts +74 -67
  59. package/test/unit/commands/module/update.test.ts +74 -70
  60. package/test/unit/commands/run.test.ts +253 -201
  61. package/test/unit/commands/setup.test.ts +146 -128
  62. package/test/unit/utils/command-discovery.test.ts +138 -125
  63. package/test/unit/utils/git.test.ts +135 -117
  64. package/test/unit/utils/integration-helpers.test.ts +59 -49
  65. package/test/unit/utils/url-resolver.test.ts +46 -34
  66. package/test/utils/integration-helpers.ts +36 -29
  67. package/tsconfig.json +15 -25
  68. package/tsup.config.ts +14 -14
  69. package/vitest.config.ts +10 -10
  70. package/vitest.e2e.config.ts +6 -6
  71. package/vitest.integration.config.ts +17 -17
  72. package/dist/chunk-JYASTIIW.js.map +0 -1
  73. package/dist/chunk-OKXOCNXP.js +0 -105
  74. package/dist/chunk-OKXOCNXP.js.map +0 -1
  75. package/dist/chunk-WKERTCM6.js.map +0 -1
@@ -1,152 +1,170 @@
1
1
  import { runCommand } from '@nexical/cli-core';
2
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
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
- exec: vi.fn(),
6
+ exec: vi.fn(),
7
7
  }));
8
8
 
9
9
  vi.mock('@nexical/cli-core', async (importOriginal) => {
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
- }
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
- exec: mocks.exec,
19
+ exec: mocks.exec,
20
20
  }));
21
21
 
22
22
  vi.mock('node:util', async () => {
23
- const actual = await vi.importActual<any>('node:util');
24
- return {
25
- ...actual,
26
- promisify: (fn: Function) => {
27
- return (...args: any[]) => new Promise((resolve, reject) => {
28
- fn(...args, (err: Error | null, ...values: any[]) => {
29
- if (err) return reject(err);
30
- // Handle exec-like signature (stdout, stderr) -> { stdout, stderr }
31
- // Simple heuristic: if values.length > 1, assume explicit mapping needed?
32
- // Or just hardcode for our known usage (exec).
33
- if (values.length >= 2) {
34
- resolve({ stdout: values[0], stderr: values[1] });
35
- } else {
36
- resolve(values[0]);
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
- beforeEach(() => {
46
- vi.clearAllMocks();
47
- });
48
-
49
- it('should clone repository', async () => {
50
- await git.clone('http://repo.git', 'dest', { recursive: true });
51
- expect(runCommand).toHaveBeenCalledWith(
52
- 'git clone --recursive http://repo.git .',
53
- 'dest'
54
- );
55
- });
56
-
57
- it('should clone repository with depth', async () => {
58
- await git.clone('http://repo.git', 'dest', { depth: 1 });
59
- expect(runCommand).toHaveBeenCalledWith(
60
- 'git clone --depth 1 http://repo.git .',
61
- 'dest'
62
- );
63
- });
64
-
65
- it('should update submodules', async () => {
66
- await git.updateSubmodules('dest');
67
- expect(runCommand).toHaveBeenCalledWith(
68
- 'git submodule foreach --recursive "git checkout main && git pull origin main"',
69
- 'dest'
70
- );
71
- });
72
-
73
- it('should checkout orphan branch', async () => {
74
- await git.checkoutOrphan('branch', 'dest');
75
- expect(runCommand).toHaveBeenCalledWith('git checkout --orphan branch', 'dest');
76
- });
77
-
78
- it('should get remote url', async () => {
79
- // Mock exec to call the callback with success
80
- mocks.exec.mockImplementation((((cmd: string, options: any, callback: any) => {
81
- if (typeof options === 'function') {
82
- callback = options;
83
- options = {};
84
- }
85
- // callback(error, stdout, stderr)
86
- callback(null, 'https://github.com/origin.git\n', '');
87
- return {} as any; // exec returns a ChildProcess
88
- }) as any));
89
-
90
- const url = await git.getRemoteUrl('cwd');
91
- expect(url).toBe('https://github.com/origin.git');
92
- expect(mocks.exec).toHaveBeenCalledWith('git remote get-url origin', { cwd: 'cwd' }, expect.any(Function));
93
- });
94
-
95
- it('should return empty string on getRemoteUrl failure', async () => {
96
- // Mock exec to call the callback with error
97
- mocks.exec.mockImplementation((((cmd: string, options: any, callback: any) => {
98
- if (typeof options === 'function') callback = options;
99
- callback(new Error('fail'), '', '');
100
- return {} as any;
101
- }) as any));
102
-
103
- const url = await git.getRemoteUrl('cwd');
104
- expect(url).toBe('');
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
- await git.addAll('cwd');
110
- expect(runCommand).toHaveBeenCalledWith('git add -A', 'cwd');
117
+ await git.addAll('cwd');
118
+ expect(runCommand).toHaveBeenCalledWith('git add -A', 'cwd');
111
119
  });
112
120
 
113
121
  it('should commit', async () => {
114
- await git.commit('msg', 'cwd');
115
- expect(runCommand).toHaveBeenCalledWith('git commit -m "msg"', 'cwd');
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
- await git.deleteBranch('branch', 'cwd');
120
- expect(runCommand).toHaveBeenCalledWith('git branch -D branch', 'cwd');
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
- await git.renameBranch('branch', 'cwd');
125
- expect(runCommand).toHaveBeenCalledWith('git branch -m branch', 'cwd');
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
- await git.removeRemote('origin', 'cwd');
130
- expect(runCommand).toHaveBeenCalledWith('git remote remove origin', 'cwd');
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
- // Mock success
135
- mocks.exec.mockImplementation((((cmd: string, options: any, callback: any) => {
136
- if (typeof options === 'function') callback = options;
137
- callback(null, '', '');
138
- return {} as any;
139
- }) as any));
140
-
141
- expect(await git.branchExists('branch', 'cwd')).toBe(true);
142
- expect(mocks.exec).toHaveBeenCalledWith('git show-ref --verify --quiet refs/heads/branch', { cwd: 'cwd' }, expect.any(Function));
143
-
144
- // Mock failure
145
- mocks.exec.mockImplementation((((cmd: string, options: any, callback: any) => {
146
- if (typeof options === 'function') callback = options;
147
- callback(new Error('fail'), '', '');
148
- return {} as any;
149
- }) as any));
150
-
151
- expect(await git.branchExists('branch', 'cwd')).toBe(false);
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, afterEach } from 'vitest';
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
- beforeEach(() => {
12
- vi.clearAllMocks();
13
- });
11
+ beforeEach(() => {
12
+ vi.clearAllMocks();
13
+ });
14
14
 
15
- it('runCLI should execute node with correct arguments', async () => {
16
- vi.mocked(execa).mockResolvedValue({ exitCode: 0 } as any);
17
- const args = ['init', 'my-project'];
18
- const cwd = '/test/cwd';
19
- const env = { FOO: 'bar' };
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
- await helpers.runCLI(args, cwd, { env });
22
+ await helpers.runCLI(args, cwd, { env });
22
23
 
23
- expect(execa).toHaveBeenCalledWith(
24
- 'node',
25
- expect.arrayContaining([expect.stringContaining('dist/index.js'), ...args]),
26
- expect.objectContaining({
27
- cwd,
28
- env: expect.objectContaining({ FOO: 'bar' }),
29
- reject: false
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
- it('createTempDir should create directory and return path', async () => {
35
- vi.mocked(fs.ensureDir).mockResolvedValue(undefined);
35
+ it('createTempDir should create directory and return path', async () => {
36
+ vi.mocked(fs.ensureDir).mockResolvedValue(undefined);
36
37
 
37
- const dir = await helpers.createTempDir('test-prefix-');
38
+ const dir = await helpers.createTempDir('test-prefix-');
38
39
 
39
- expect(dir).toContain('test-prefix-');
40
- expect(fs.ensureDir).toHaveBeenCalledWith(dir);
41
- });
40
+ expect(dir).toContain('test-prefix-');
41
+ expect(fs.ensureDir).toHaveBeenCalledWith(dir);
42
+ });
42
43
 
43
- it('cleanupTestRoot should remove test root directory', async () => {
44
- vi.mocked(fs.remove).mockResolvedValue(undefined);
44
+ it('cleanupTestRoot should remove test root directory', async () => {
45
+ vi.mocked(fs.remove).mockResolvedValue(undefined);
45
46
 
46
- await helpers.cleanupTestRoot();
47
+ await helpers.cleanupTestRoot();
47
48
 
48
- expect(fs.remove).toHaveBeenCalledWith(expect.stringContaining('.test-tmp'));
49
- });
49
+ expect(fs.remove).toHaveBeenCalledWith(expect.stringContaining('.test-tmp'));
50
+ });
50
51
 
51
- it('createMockRepo should initialize git repo and commit files', async () => {
52
- vi.mocked(fs.ensureDir).mockResolvedValue(undefined);
53
- vi.mocked(fs.outputFile).mockResolvedValue(undefined);
54
- vi.mocked(execa).mockResolvedValue({} as any);
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
- const dir = '/test/repo';
57
- const files = {
58
- 'package.json': '{}',
59
- 'README.md': '# Test'
60
- };
58
+ const dir = '/test/repo';
59
+ const files = {
60
+ 'package.json': '{}',
61
+ 'README.md': '# Test',
62
+ };
61
63
 
62
- await helpers.createMockRepo(dir, files);
64
+ await helpers.createMockRepo(dir, files);
63
65
 
64
- expect(fs.ensureDir).toHaveBeenCalledWith(dir);
65
- expect(execa).toHaveBeenCalledWith('git', ['init'], expect.objectContaining({ cwd: dir }));
66
- expect(execa).toHaveBeenCalledWith('git', ['config', 'user.email', 'test@test.com'], expect.objectContaining({ cwd: dir }));
67
- expect(fs.outputFile).toHaveBeenCalledWith(path.join(dir, 'package.json'), '{}');
68
- expect(fs.outputFile).toHaveBeenCalledWith(path.join(dir, 'README.md'), '# Test');
69
- expect(execa).toHaveBeenCalledWith('git', ['add', '.'], expect.objectContaining({ cwd: dir }));
70
- expect(execa).toHaveBeenCalledWith('git', ['commit', '-m', 'Initial commit'], expect.objectContaining({ cwd: dir }));
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
- it('should expand gh@ shorthand correctly', () => {
6
- expect(resolveGitUrl('gh@nexical-cms/starter')).toBe('https://github.com/nexical-cms/starter.git');
7
- });
8
-
9
- it('should expand gh@ shorthand with subpath correctly', () => {
10
- expect(resolveGitUrl('gh@nexical-cms/starter//path/to/module')).toBe('https://github.com/nexical-cms/starter.git//path/to/module');
11
- });
12
-
13
- it('should add .git extension to standard URLs if missing', () => {
14
- expect(resolveGitUrl('https://github.com/nexical-cms/starter')).toBe('https://github.com/nexical-cms/starter.git');
15
- });
16
-
17
- it('should preserve .git extension if already present', () => {
18
- expect(resolveGitUrl('https://github.com/nexical-cms/starter.git')).toBe('https://github.com/nexical-cms/starter.git');
19
- });
20
-
21
- it('should handle standard URLs with subpath correctly', () => {
22
- expect(resolveGitUrl('https://github.com/nexical-cms/starter//path/to/dir')).toBe('https://github.com/nexical-cms/starter.git//path/to/dir');
23
- });
24
-
25
- it('should handle standard URLs with .git and subpath correctly', () => {
26
- expect(resolveGitUrl('https://github.com/nexical-cms/starter.git//path/to/dir')).toBe('https://github.com/nexical-cms/starter.git//path/to/dir');
27
- });
28
-
29
- it('should throw error for empty url', () => {
30
- expect(() => resolveGitUrl('')).toThrow('URL cannot be empty');
31
- });
32
-
33
- it('should not append .git to local paths', () => {
34
- expect(resolveGitUrl('/tmp/local/repo')).toBe('/tmp/local/repo');
35
- expect(resolveGitUrl('./local/repo')).toBe('./local/repo');
36
- expect(resolveGitUrl('../local/repo')).toBe('../local/repo');
37
- expect(resolveGitUrl('file:///tmp/repo')).toBe('file:///tmp/repo');
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(args: string[], cwd: string, options: any = {}) {
15
- return execa('node', [CLI_BIN, ...args], {
16
- cwd,
17
- ...options,
18
- env: {
19
- ...process.env,
20
- ...options.env
21
- },
22
- reject: false // Allow checking exit code in tests
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
- const dir = path.join(TEST_ROOT, `${prefix}${Date.now()}-${Math.random().toString(36).slice(2)}`);
32
- await fs.ensureDir(dir);
33
- return dir;
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
- await fs.remove(TEST_ROOT);
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(dir: string, initialFiles: Record<string, string> = {}): Promise<string> {
48
- await fs.ensureDir(dir);
51
+ export async function createMockRepo(
52
+ dir: string,
53
+ initialFiles: Record<string, string> = {},
54
+ ): Promise<string> {
55
+ await fs.ensureDir(dir);
49
56
 
50
- // Initialize bare repo? No, usually we want a regular repo then commit,
51
- // but if we want to clone FROM it locally, it acts as a remote.
52
- // Let's make it a regular repo.
53
- await execa('git', ['init'], { cwd: dir });
54
- await execa('git', ['config', 'user.email', 'test@test.com'], { cwd: dir });
55
- await execa('git', ['config', 'user.name', 'Test User'], { cwd: dir });
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
- // Write initial files
58
- for (const [filename, content] of Object.entries(initialFiles)) {
59
- await fs.outputFile(path.join(dir, filename), content);
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
- await execa('git', ['add', '.'], { cwd: dir });
63
- await execa('git', ['commit', '-m', 'Initial commit'], { cwd: dir });
69
+ await execa('git', ['add', '.'], { cwd: dir });
70
+ await execa('git', ['commit', '-m', 'Initial commit'], { cwd: dir });
64
71
 
65
- return dir;
72
+ return dir;
66
73
  }