@nexical/cli 0.1.7 → 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.
Files changed (236) hide show
  1. package/.github/workflows/deploy.yml +3 -3
  2. package/README.md +317 -104
  3. package/dist/chunk-JYASTIIW.js +42 -0
  4. package/dist/chunk-JYASTIIW.js.map +1 -0
  5. package/dist/chunk-LZ3YQWAR.js +2204 -0
  6. package/dist/chunk-LZ3YQWAR.js.map +1 -0
  7. package/dist/chunk-OKXOCNXP.js +105 -0
  8. package/dist/chunk-OKXOCNXP.js.map +1 -0
  9. package/dist/chunk-OYFWMYPG.js +52 -0
  10. package/dist/chunk-OYFWMYPG.js.map +1 -0
  11. package/dist/chunk-WKERTCM6.js +74 -0
  12. package/dist/chunk-WKERTCM6.js.map +1 -0
  13. package/dist/index.js +32 -5
  14. package/dist/index.js.map +1 -1
  15. package/dist/src/commands/init.d.ts +11 -0
  16. package/dist/src/commands/init.js +88 -0
  17. package/dist/src/commands/init.js.map +1 -0
  18. package/dist/src/commands/module/add.d.ts +14 -0
  19. package/dist/src/commands/module/add.js +136 -0
  20. package/dist/src/commands/module/add.js.map +1 -0
  21. package/dist/src/commands/module/list.d.ts +10 -0
  22. package/dist/src/commands/module/list.js +73 -0
  23. package/dist/src/commands/module/list.js.map +1 -0
  24. package/dist/src/commands/module/remove.d.ts +12 -0
  25. package/dist/src/commands/module/remove.js +71 -0
  26. package/dist/src/commands/module/remove.js.map +1 -0
  27. package/dist/src/commands/module/update.d.ts +11 -0
  28. package/dist/src/commands/module/update.js +52 -0
  29. package/dist/src/commands/module/update.js.map +1 -0
  30. package/dist/src/commands/run.d.ts +11 -0
  31. package/dist/src/commands/run.js +93 -0
  32. package/dist/src/commands/run.js.map +1 -0
  33. package/dist/src/utils/discovery.d.ts +13 -0
  34. package/dist/src/utils/discovery.js +9 -0
  35. package/dist/src/utils/git.d.ts +16 -0
  36. package/dist/src/utils/git.js +29 -0
  37. package/dist/src/utils/git.js.map +1 -0
  38. package/dist/src/utils/url-resolver.d.ts +15 -0
  39. package/dist/src/utils/url-resolver.js +9 -0
  40. package/dist/src/utils/url-resolver.js.map +1 -0
  41. package/index.ts +29 -5
  42. package/package.json +33 -31
  43. package/src/commands/init.ts +85 -0
  44. package/src/commands/module/add.ts +169 -0
  45. package/src/commands/module/list.ts +69 -0
  46. package/src/commands/module/remove.ts +74 -0
  47. package/src/commands/module/update.ts +50 -0
  48. package/src/commands/run.ts +98 -0
  49. package/src/utils/discovery.ts +134 -0
  50. package/src/utils/git.ts +65 -0
  51. package/src/utils/url-resolver.ts +57 -0
  52. package/test/e2e/lifecycle.e2e.test.ts +152 -0
  53. package/test/integration/commands/init.integration.test.ts +82 -0
  54. package/test/integration/commands/module.integration.test.ts +144 -0
  55. package/test/integration/commands/run.integration.test.ts +90 -0
  56. package/test/integration/utils/command-loading.integration.test.ts +80 -0
  57. package/test/unit/commands/init.test.ts +153 -0
  58. package/test/unit/commands/module/add.test.ts +262 -0
  59. package/test/unit/commands/module/list.test.ts +115 -0
  60. package/test/unit/commands/module/remove.test.ts +89 -0
  61. package/test/unit/commands/module/update.test.ts +91 -0
  62. package/test/unit/commands/run.test.ts +252 -0
  63. package/test/unit/utils/command-discovery.test.ts +176 -0
  64. package/test/unit/utils/git.test.ts +152 -0
  65. package/test/unit/utils/integration-helpers.test.ts +72 -0
  66. package/test/unit/utils/url-resolver.test.ts +39 -0
  67. package/test/utils/integration-helpers.ts +66 -0
  68. package/vitest.e2e.config.ts +0 -1
  69. package/dist/chunk-JDRAVUKK.js +0 -48
  70. package/dist/chunk-JDRAVUKK.js.map +0 -1
  71. package/dist/src/commands/admin/create-user.d.ts +0 -15
  72. package/dist/src/commands/admin/create-user.js +0 -49
  73. package/dist/src/commands/admin/create-user.js.map +0 -1
  74. package/dist/src/commands/branch/create.d.ts +0 -19
  75. package/dist/src/commands/branch/create.js +0 -59
  76. package/dist/src/commands/branch/create.js.map +0 -1
  77. package/dist/src/commands/branch/delete.d.ts +0 -15
  78. package/dist/src/commands/branch/delete.js +0 -50
  79. package/dist/src/commands/branch/delete.js.map +0 -1
  80. package/dist/src/commands/branch/get.d.ts +0 -15
  81. package/dist/src/commands/branch/get.js +0 -53
  82. package/dist/src/commands/branch/get.js.map +0 -1
  83. package/dist/src/commands/branch/list.d.ts +0 -15
  84. package/dist/src/commands/branch/list.js +0 -51
  85. package/dist/src/commands/branch/list.js.map +0 -1
  86. package/dist/src/commands/job/get.d.ts +0 -15
  87. package/dist/src/commands/job/get.js +0 -62
  88. package/dist/src/commands/job/get.js.map +0 -1
  89. package/dist/src/commands/job/list.d.ts +0 -15
  90. package/dist/src/commands/job/list.js +0 -57
  91. package/dist/src/commands/job/list.js.map +0 -1
  92. package/dist/src/commands/job/logs.d.ts +0 -15
  93. package/dist/src/commands/job/logs.js +0 -67
  94. package/dist/src/commands/job/logs.js.map +0 -1
  95. package/dist/src/commands/job/trigger.d.ts +0 -19
  96. package/dist/src/commands/job/trigger.js +0 -74
  97. package/dist/src/commands/job/trigger.js.map +0 -1
  98. package/dist/src/commands/login.d.ts +0 -8
  99. package/dist/src/commands/login.js +0 -31
  100. package/dist/src/commands/login.js.map +0 -1
  101. package/dist/src/commands/project/create.d.ts +0 -24
  102. package/dist/src/commands/project/create.js +0 -63
  103. package/dist/src/commands/project/create.js.map +0 -1
  104. package/dist/src/commands/project/delete.d.ts +0 -20
  105. package/dist/src/commands/project/delete.js +0 -58
  106. package/dist/src/commands/project/delete.js.map +0 -1
  107. package/dist/src/commands/project/get.d.ts +0 -15
  108. package/dist/src/commands/project/get.js +0 -49
  109. package/dist/src/commands/project/get.js.map +0 -1
  110. package/dist/src/commands/project/list.d.ts +0 -15
  111. package/dist/src/commands/project/list.js +0 -45
  112. package/dist/src/commands/project/list.js.map +0 -1
  113. package/dist/src/commands/project/update.d.ts +0 -19
  114. package/dist/src/commands/project/update.js +0 -66
  115. package/dist/src/commands/project/update.js.map +0 -1
  116. package/dist/src/commands/team/create.d.ts +0 -19
  117. package/dist/src/commands/team/create.js +0 -45
  118. package/dist/src/commands/team/create.js.map +0 -1
  119. package/dist/src/commands/team/delete.d.ts +0 -20
  120. package/dist/src/commands/team/delete.js +0 -52
  121. package/dist/src/commands/team/delete.js.map +0 -1
  122. package/dist/src/commands/team/get.d.ts +0 -15
  123. package/dist/src/commands/team/get.js +0 -42
  124. package/dist/src/commands/team/get.js.map +0 -1
  125. package/dist/src/commands/team/list.d.ts +0 -8
  126. package/dist/src/commands/team/list.js +0 -30
  127. package/dist/src/commands/team/list.js.map +0 -1
  128. package/dist/src/commands/team/member/invite.d.ts +0 -20
  129. package/dist/src/commands/team/member/invite.js +0 -54
  130. package/dist/src/commands/team/member/invite.js.map +0 -1
  131. package/dist/src/commands/team/member/remove.d.ts +0 -15
  132. package/dist/src/commands/team/member/remove.js +0 -43
  133. package/dist/src/commands/team/member/remove.js.map +0 -1
  134. package/dist/src/commands/team/update.d.ts +0 -19
  135. package/dist/src/commands/team/update.js +0 -55
  136. package/dist/src/commands/team/update.js.map +0 -1
  137. package/dist/src/commands/token/generate.d.ts +0 -19
  138. package/dist/src/commands/token/generate.js +0 -48
  139. package/dist/src/commands/token/generate.js.map +0 -1
  140. package/dist/src/commands/token/list.d.ts +0 -8
  141. package/dist/src/commands/token/list.js +0 -31
  142. package/dist/src/commands/token/list.js.map +0 -1
  143. package/dist/src/commands/token/revoke.d.ts +0 -15
  144. package/dist/src/commands/token/revoke.js +0 -38
  145. package/dist/src/commands/token/revoke.js.map +0 -1
  146. package/dist/src/commands/whoami.d.ts +0 -8
  147. package/dist/src/commands/whoami.js +0 -26
  148. package/dist/src/commands/whoami.js.map +0 -1
  149. package/dist/src/utils/nexical-client.d.ts +0 -10
  150. package/dist/src/utils/nexical-client.js +0 -12
  151. package/src/commands/admin/create-user.ts +0 -46
  152. package/src/commands/branch/create.ts +0 -57
  153. package/src/commands/branch/delete.ts +0 -47
  154. package/src/commands/branch/get.ts +0 -50
  155. package/src/commands/branch/list.ts +0 -50
  156. package/src/commands/job/get.ts +0 -59
  157. package/src/commands/job/list.ts +0 -56
  158. package/src/commands/job/logs.ts +0 -67
  159. package/src/commands/job/trigger.ts +0 -73
  160. package/src/commands/login.ts +0 -31
  161. package/src/commands/project/create.ts +0 -61
  162. package/src/commands/project/delete.ts +0 -56
  163. package/src/commands/project/get.ts +0 -46
  164. package/src/commands/project/list.ts +0 -44
  165. package/src/commands/project/update.ts +0 -63
  166. package/src/commands/team/create.ts +0 -43
  167. package/src/commands/team/delete.ts +0 -50
  168. package/src/commands/team/get.ts +0 -39
  169. package/src/commands/team/list.ts +0 -26
  170. package/src/commands/team/member/invite.ts +0 -56
  171. package/src/commands/team/member/remove.ts +0 -40
  172. package/src/commands/team/update.ts +0 -53
  173. package/src/commands/token/generate.ts +0 -45
  174. package/src/commands/token/list.ts +0 -27
  175. package/src/commands/token/revoke.ts +0 -35
  176. package/src/commands/whoami.ts +0 -21
  177. package/src/utils/nexical-client.ts +0 -47
  178. package/test/e2e/auth.e2e.test.ts +0 -46
  179. package/test/e2e/job-workflow.e2e.test.ts +0 -33
  180. package/test/e2e/project-lifecycle.e2e.test.ts +0 -48
  181. package/test/e2e/setup.ts +0 -237
  182. package/test/e2e/utils.ts +0 -33
  183. package/test/integration/commands/admin/create-user.test.ts +0 -51
  184. package/test/integration/commands/branch/create.test.ts +0 -51
  185. package/test/integration/commands/branch/delete.test.ts +0 -43
  186. package/test/integration/commands/branch/get.test.ts +0 -49
  187. package/test/integration/commands/branch/list.test.ts +0 -47
  188. package/test/integration/commands/job/get.test.ts +0 -54
  189. package/test/integration/commands/job/list.test.ts +0 -47
  190. package/test/integration/commands/job/logs.test.ts +0 -47
  191. package/test/integration/commands/job/trigger.test.ts +0 -57
  192. package/test/integration/commands/login.test.ts +0 -62
  193. package/test/integration/commands/project/create.test.ts +0 -53
  194. package/test/integration/commands/project/delete.test.ts +0 -43
  195. package/test/integration/commands/project/get.test.ts +0 -51
  196. package/test/integration/commands/project/list.test.ts +0 -47
  197. package/test/integration/commands/project/update.test.ts +0 -53
  198. package/test/integration/commands/team/create.test.ts +0 -53
  199. package/test/integration/commands/team/delete.test.ts +0 -43
  200. package/test/integration/commands/team/get.test.ts +0 -50
  201. package/test/integration/commands/team/list.test.ts +0 -47
  202. package/test/integration/commands/team/member/invite.test.ts +0 -46
  203. package/test/integration/commands/team/member/remove.test.ts +0 -43
  204. package/test/integration/commands/team/update.test.ts +0 -50
  205. package/test/integration/commands/token/generate.test.ts +0 -51
  206. package/test/integration/commands/token/list.test.ts +0 -47
  207. package/test/integration/commands/token/revoke.test.ts +0 -43
  208. package/test/integration/commands/whoami.test.ts +0 -49
  209. package/test/unit/commands/admin/create-user.test.ts +0 -51
  210. package/test/unit/commands/branch/create.test.ts +0 -57
  211. package/test/unit/commands/branch/delete.test.ts +0 -49
  212. package/test/unit/commands/branch/get.test.ts +0 -67
  213. package/test/unit/commands/branch/list.test.ts +0 -62
  214. package/test/unit/commands/job/get.test.ts +0 -76
  215. package/test/unit/commands/job/list.test.ts +0 -62
  216. package/test/unit/commands/job/logs.test.ts +0 -60
  217. package/test/unit/commands/job/trigger.test.ts +0 -75
  218. package/test/unit/commands/login.test.ts +0 -64
  219. package/test/unit/commands/project/create.test.ts +0 -64
  220. package/test/unit/commands/project/delete.test.ts +0 -72
  221. package/test/unit/commands/project/get.test.ts +0 -73
  222. package/test/unit/commands/project/list.test.ts +0 -62
  223. package/test/unit/commands/project/update.test.ts +0 -58
  224. package/test/unit/commands/team/create.test.ts +0 -68
  225. package/test/unit/commands/team/delete.test.ts +0 -71
  226. package/test/unit/commands/team/get.test.ts +0 -70
  227. package/test/unit/commands/team/list.test.ts +0 -56
  228. package/test/unit/commands/team/member/invite.test.ts +0 -52
  229. package/test/unit/commands/team/member/remove.test.ts +0 -49
  230. package/test/unit/commands/team/update.test.ts +0 -63
  231. package/test/unit/commands/token/generate.test.ts +0 -65
  232. package/test/unit/commands/token/list.test.ts +0 -58
  233. package/test/unit/commands/token/revoke.test.ts +0 -49
  234. package/test/unit/commands/whoami.test.ts +0 -49
  235. package/test/unit/utils/nexical-client.test.ts +0 -113
  236. /package/dist/src/utils/{nexical-client.js.map → discovery.js.map} +0 -0
@@ -0,0 +1,82 @@
1
+ import { CLI } from '@nexical/cli-core';
2
+ import { describe, it, expect, beforeEach, afterEach, afterAll } from 'vitest';
3
+ import InitCommand from '../../../src/commands/init.js';
4
+ import { createTempDir, createMockRepo, cleanupTestRoot } from '../../utils/integration-helpers.js';
5
+ import path from 'node:path';
6
+ import fs from 'fs-extra';
7
+ import { execa } from 'execa';
8
+
9
+ describe('InitCommand Integration', () => {
10
+ let tempDir: string;
11
+ let starterRepoDir: string;
12
+
13
+ beforeEach(async () => {
14
+ tempDir = await createTempDir('init-integration-');
15
+ const starterDir = await createTempDir('starter-repo-');
16
+
17
+ // precise setup of a starter repo
18
+ starterRepoDir = await createMockRepo(starterDir, {
19
+ 'package.json': JSON.stringify({
20
+ name: 'nexical-starter',
21
+ version: '0.0.0',
22
+ dependencies: {
23
+ 'is-odd': '3.0.1'
24
+ }
25
+ }),
26
+ 'README.md': '# Starter Template'
27
+ });
28
+
29
+ // Set Git Identity for the test process so InitCommand's commit works in CI
30
+ process.env.GIT_AUTHOR_NAME = 'Test User';
31
+ process.env.GIT_AUTHOR_EMAIL = 'test@example.com';
32
+ process.env.GIT_COMMITTER_NAME = 'Test User';
33
+ process.env.GIT_COMMITTER_EMAIL = 'test@example.com';
34
+ // Allow file protocol for local cloning in CI
35
+ process.env.GIT_ALLOW_PROTOCOL = 'file';
36
+ });
37
+
38
+ afterAll(async () => {
39
+ // await cleanupTestRoot(); // conflicting with parallel tests
40
+ if (tempDir) await fs.remove(tempDir);
41
+ });
42
+
43
+ it('should initialize a new project from a local git repo', async () => {
44
+ const targetProjectName = 'my-new-project';
45
+ const targetPath = path.join(tempDir, targetProjectName);
46
+ const cli = new CLI({ commandName: 'nexical' });
47
+
48
+ const command = new InitCommand(cli);
49
+
50
+ // Capture stdout/stderr? InitCommand uses consola.
51
+ // For integration, we care about the FS side effects.
52
+
53
+ await command.run({
54
+ directory: targetPath,
55
+ repo: starterRepoDir // Passing local path as repo URL
56
+ });
57
+
58
+ // 1. Check directory exists
59
+ expect(fs.existsSync(targetPath)).toBe(true);
60
+
61
+ // 2. Check files cloned
62
+ expect(fs.existsSync(path.join(targetPath, 'package.json'))).toBe(true);
63
+ expect(fs.existsSync(path.join(targetPath, 'README.md'))).toBe(true);
64
+
65
+ // 3. Check git initialization
66
+ expect(fs.existsSync(path.join(targetPath, '.git'))).toBe(true);
67
+
68
+ // 4. Check git initialization (history should be preserved + one new commit)
69
+ const { stdout: log } = await execa('git', ['log', '--oneline'], { cwd: targetPath });
70
+ const lines = log.split('\n').filter(Boolean);
71
+ expect(lines.length).toBe(2); // Should have "Initial site commit" and "Initial commit"
72
+ expect(lines[0]).toContain('Initial site commit');
73
+ expect(lines[1]).toContain('Initial commit');
74
+
75
+ // 5. Check dependencies (optional, but command tries to install them)
76
+ // Since we are mocking the repo, it doesn't have a real lockfile or valid deps,
77
+ // so `npm install` might have failed or done nothing.
78
+ // However, `InitCommand` runs `npm install`. If that fails, the command throws/exits.
79
+ // We provided a minimal package.json so it should succeed.
80
+ expect(fs.existsSync(path.join(targetPath, 'node_modules'))).toBe(true);
81
+ }, 60000); // Increase timeout for real git/npm ops
82
+ });
@@ -0,0 +1,144 @@
1
+ import { CLI } from '@nexical/cli-core';
2
+ import { describe, it, expect, beforeEach, afterEach, afterAll, vi } from 'vitest';
3
+ import ModuleAddCommand from '../../../src/commands/module/add.js';
4
+ import ModuleRemoveCommand from '../../../src/commands/module/remove.js';
5
+ import ModuleListCommand from '../../../src/commands/module/list.js';
6
+ import ModuleUpdateCommand from '../../../src/commands/module/update.js';
7
+
8
+ import { createTempDir, createMockRepo, cleanupTestRoot } from '../../utils/integration-helpers.js';
9
+ import path from 'node:path';
10
+ import fs from 'fs-extra';
11
+
12
+ // Mock picocolors to return strings as-is for easy matching
13
+ vi.mock('picocolors', () => ({
14
+ default: {
15
+ bold: (s: string) => s,
16
+ cyan: (s: string) => s,
17
+ yellow: (s: string) => s,
18
+ dim: (s: string) => s,
19
+ red: (s: string) => s,
20
+ green: (s: string) => s,
21
+ blue: (s: string) => s,
22
+ magenta: (s: string) => s,
23
+ }
24
+ }));
25
+
26
+ describe('Module Commands Integration', () => {
27
+ let projectDir: string;
28
+ let moduleRepo: string;
29
+ let consoleTableSpy: any;
30
+ let consoleLogSpy: any;
31
+
32
+ beforeEach(async () => {
33
+ // 1. Create a "Project" that is a git repo
34
+ const temp = await createTempDir('module-project-');
35
+ projectDir = await createMockRepo(temp, {
36
+ 'package.json': '{"name": "test-project", "version": "1.0.0"}',
37
+ 'nexical.yaml': 'site: test\nmodules: []'
38
+ });
39
+
40
+ // Allow file protocol for submodules in this repo
41
+ // await execa('git', ['config', 'protocol.file.allow', 'always'], { cwd: projectDir }); // Config approach failed
42
+ process.env.GIT_ALLOW_PROTOCOL = 'file';
43
+
44
+ // 2. Create a "Module" that is a SEPARATE git repo
45
+ const modTemp = await createTempDir('module-source-');
46
+ moduleRepo = await createMockRepo(modTemp, {
47
+ 'package.json': '{"name": "my-module", "version": "1.0.0", "description": "Awesome module"}',
48
+ 'module.yaml': 'name: my-module\nversion: 1.0.0',
49
+ 'index.ts': 'export const hello = "world";'
50
+ });
51
+
52
+ consoleTableSpy = vi.spyOn(console, 'table').mockImplementation(() => { });
53
+ consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
54
+
55
+ // Switch CWD to project for commands to find root
56
+ // Note: process.chdir behavior might persist, so we rely on mocking or careful cleanup
57
+ // But integration tests run sequentially in same thread usually with vitest unless configured otherwise.
58
+ // We will pass specific CWD to commands if possible, OR chdir and restore.
59
+ });
60
+
61
+ afterEach(() => {
62
+ vi.restoreAllMocks();
63
+ });
64
+
65
+ afterAll(async () => {
66
+ await cleanupTestRoot();
67
+ });
68
+
69
+ it('should add, list, update and remove a module', async () => {
70
+ const originalCwd = process.cwd();
71
+ const cli = new CLI({ commandName: 'nexical' });
72
+ try {
73
+ process.chdir(projectDir);
74
+
75
+ // 1. ADD MODULE
76
+ const addCmd = new ModuleAddCommand(cli);
77
+
78
+ await addCmd.init();
79
+ await addCmd.run({ url: moduleRepo, name: 'my-module' });
80
+
81
+ const modulePath = path.join(projectDir, 'modules/my-module');
82
+ expect(fs.existsSync(modulePath)).toBe(true);
83
+ expect(fs.existsSync(path.join(modulePath, 'package.json'))).toBe(true);
84
+
85
+ // Verify nexical.yaml updated
86
+ const config = await fs.readFile(path.join(projectDir, 'nexical.yaml'), 'utf8');
87
+ expect(config).toContain('modules:');
88
+ expect(config).toContain('- my-module');
89
+
90
+ // Check it is a submodule
91
+ // .git file in module dir pointing to gitdir
92
+ expect(fs.existsSync(path.join(modulePath, '.git'))).toBe(true);
93
+ const gitModules = await fs.readFile(path.join(projectDir, '.gitmodules'), 'utf-8');
94
+ expect(gitModules).toContain('path = modules/my-module');
95
+
96
+ // 2. LIST MODULES with valid module
97
+ const listCmd = new ModuleListCommand(cli);
98
+ await listCmd.init();
99
+ await listCmd.run();
100
+
101
+ // Check console.table called with module info
102
+ expect(consoleTableSpy).toHaveBeenCalledWith(expect.arrayContaining([
103
+ expect.objectContaining({
104
+ name: 'my-module',
105
+ version: '1.0.0',
106
+ description: 'Awesome module'
107
+ })
108
+ ]));
109
+
110
+ // 3. UPDATE MODULE
111
+ const updateCmd = new ModuleUpdateCommand(cli);
112
+ await updateCmd.init();
113
+ await updateCmd.run({ name: 'my-module' });
114
+ // Hard to check "update" without changing the remote first.
115
+ // But we verify it ran without throwing.
116
+
117
+ // 4. REMOVE MODULE
118
+ const removeCmd = new ModuleRemoveCommand(cli);
119
+ await removeCmd.init();
120
+ await removeCmd.run({ name: 'my-module' });
121
+
122
+ expect(fs.existsSync(modulePath)).toBe(false);
123
+
124
+ // Verify git cleanup
125
+ // .git/modules/modules/my-module should be gone
126
+ const gitInternalModuleDir = path.join(projectDir, '.git/modules/modules/my-module');
127
+ expect(fs.existsSync(gitInternalModuleDir)).toBe(false);
128
+
129
+ // .gitmodules entry gone? `git rm` usually handles this.
130
+ // Check if .gitmodules file exists (if empty it might remain or be deleted depending on git version, usually implicitly updated)
131
+ if (fs.existsSync(path.join(projectDir, '.gitmodules'))) {
132
+ const updatedGitModules = await fs.readFile(path.join(projectDir, '.gitmodules'), 'utf-8');
133
+ expect(updatedGitModules).not.toContain('modules/my-module');
134
+ }
135
+
136
+ // Verify nexical.yaml updated
137
+ const configRemoved = await fs.readFile(path.join(projectDir, 'nexical.yaml'), 'utf8');
138
+ expect(configRemoved).not.toContain('- my-module');
139
+
140
+ } finally {
141
+ process.chdir(originalCwd);
142
+ }
143
+ });
144
+ });
@@ -0,0 +1,90 @@
1
+ import { CLI } from '@nexical/cli-core';
2
+ import { describe, it, expect, beforeEach, afterEach, afterAll, vi } from 'vitest';
3
+ import RunCommand from '../../../src/commands/run.js';
4
+ import { createTempDir } from '../../utils/integration-helpers.js';
5
+ import path from 'node:path';
6
+ import fs from 'fs-extra';
7
+ import { spawn } from 'child_process';
8
+ import EventEmitter from 'events';
9
+
10
+ vi.mock('child_process', () => ({
11
+ spawn: vi.fn(),
12
+ exec: vi.fn(),
13
+ }));
14
+
15
+ describe('RunCommand Integration', () => {
16
+ let projectDir: string;
17
+ let spawnMock: any;
18
+
19
+ beforeEach(async () => {
20
+ projectDir = await createTempDir('run-project-');
21
+ vi.mocked(spawn).mockClear();
22
+
23
+ // Setup minimal env (New Architecture: no site/ directory)
24
+ await fs.ensureDir(projectDir);
25
+ await fs.outputFile(path.join(projectDir, 'package.json'), JSON.stringify({
26
+ name: 'nexical-core',
27
+ scripts: {
28
+ 'test-script': 'echo test'
29
+ }
30
+ }));
31
+
32
+ await fs.ensureDir(path.join(projectDir, 'modules', 'my-auth'));
33
+ await fs.outputFile(path.join(projectDir, 'modules', 'my-auth', 'package.json'), JSON.stringify({
34
+ scripts: {
35
+ 'seed': 'node seed.js'
36
+ }
37
+ }));
38
+
39
+ spawnMock = vi.mocked(spawn).mockImplementation(() => {
40
+ const child: any = new EventEmitter();
41
+ child.stdout = new EventEmitter();
42
+ child.stderr = new EventEmitter();
43
+ setTimeout(() => child.emit('close', 0), 10);
44
+ child.kill = vi.fn();
45
+ return child;
46
+ });
47
+ });
48
+
49
+ afterEach(() => {
50
+ vi.clearAllMocks();
51
+ vi.restoreAllMocks();
52
+ });
53
+
54
+ afterAll(async () => {
55
+ if (projectDir) await fs.remove(projectDir);
56
+ });
57
+
58
+ it('should run standard npm scripts', async () => {
59
+ const cli = new CLI({ commandName: 'nexical' });
60
+ const command = new RunCommand(cli);
61
+ Object.assign(command, { projectRoot: projectDir });
62
+
63
+ await command.run({ script: 'test-script', args: ['--flag'] });
64
+
65
+ expect(spawnMock).toHaveBeenCalledWith(
66
+ 'npm',
67
+ ['run', 'test-script', '--', '--flag'],
68
+ expect.objectContaining({
69
+ cwd: projectDir
70
+ })
71
+ );
72
+ });
73
+
74
+ it('should run module specific scripts', async () => {
75
+ const cli = new CLI({ commandName: 'nexical' });
76
+ const command = new RunCommand(cli);
77
+ Object.assign(command, { projectRoot: projectDir });
78
+
79
+ await command.run({ script: 'my-auth:seed', args: ['--force'] });
80
+
81
+ // Module scripts run via npm run scriptName inside module dir
82
+ expect(spawnMock).toHaveBeenCalledWith(
83
+ 'npm',
84
+ ['run', 'seed', '--', '--force'],
85
+ expect.objectContaining({
86
+ cwd: path.resolve(projectDir, 'modules', 'my-auth')
87
+ })
88
+ );
89
+ });
90
+ });
@@ -0,0 +1,80 @@
1
+
2
+ import { describe, it, expect, beforeAll, afterAll } from 'vitest';
3
+ import path from 'node:path';
4
+ import fs from 'fs-extra';
5
+ import { execa } from 'execa';
6
+
7
+ const CLI_ENTRY = path.resolve(__dirname, '../../../index.ts');
8
+ const TEST_TMP_DIR = path.resolve(__dirname, '../../.test-tmp/command-loading');
9
+
10
+ import { createRequire } from 'module';
11
+
12
+ const require = createRequire(import.meta.url);
13
+ const tsxPackagePath = require.resolve('tsx/package.json');
14
+ const tsxDir = path.dirname(tsxPackagePath);
15
+ const TSX_BIN = path.resolve(tsxDir, 'dist/cli.mjs');
16
+
17
+ console.log('Using TSX binary at:', TSX_BIN);
18
+
19
+ describe('Command Loading Integration', () => {
20
+ beforeAll(async () => {
21
+ await fs.ensureDir(TEST_TMP_DIR);
22
+ });
23
+
24
+ afterAll(async () => {
25
+ await fs.remove(TEST_TMP_DIR);
26
+ });
27
+
28
+ it('should load commands from modules', async () => {
29
+ // Create nexical.yaml to mock project root
30
+ await fs.writeFile(path.join(TEST_TMP_DIR, 'nexical.yaml'), 'name: test-project');
31
+
32
+ // Setup module structure
33
+ const moduleDir = path.join(TEST_TMP_DIR, 'src/modules/test-mod/src/commands');
34
+ await fs.ensureDir(moduleDir);
35
+
36
+ const commandFile = path.join(moduleDir, 'hello.ts');
37
+ const commandContent = `
38
+ import { BaseCommand } from '@nexical/cli-core';
39
+ export default class HelloCommand extends BaseCommand {
40
+ static description = 'Test hello command';
41
+ static args = {
42
+ args: [{ name: 'name', required: false }]
43
+ };
44
+
45
+ async run(options: any) {
46
+ console.log('Hello from test module!');
47
+ }
48
+ }
49
+ `;
50
+
51
+ const jsCommandContent = `
52
+ export default class HelloCommand {
53
+ constructor(cli) { this.cli = cli; }
54
+ async init() {}
55
+ async runInit(options) { return this.run(options); }
56
+ async run() { console.log('Hello from test module!'); }
57
+ }
58
+ HelloCommand.description = 'Test hello command';
59
+ HelloCommand.args = {};
60
+ `;
61
+
62
+ await fs.writeFile(path.join(moduleDir, 'hello.js'), jsCommandContent);
63
+
64
+ const { stdout, stderr } = await execa('node', [TSX_BIN, CLI_ENTRY, 'hello', '--debug'], {
65
+ cwd: TEST_TMP_DIR,
66
+ reject: false,
67
+ env: {
68
+ ...process.env,
69
+ FORCE_COLOR: '0'
70
+ }
71
+ });
72
+
73
+ if (!stdout.includes('Hello from test module!')) {
74
+ console.log('STDOUT:', stdout);
75
+ console.log('STDERR:', stderr);
76
+ }
77
+
78
+ expect(stdout).toContain('Hello from test module!');
79
+ }, 20000);
80
+ });
@@ -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
+ });