@nexical/cli 0.10.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 (76) 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 +199 -0
  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 +15 -10
  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 +27 -16
  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.d.ts +8 -0
  33. package/dist/src/commands/setup.js +75 -0
  34. package/dist/src/commands/setup.js.map +1 -0
  35. package/dist/src/utils/discovery.js +1 -1
  36. package/dist/src/utils/git.js +1 -1
  37. package/dist/src/utils/url-resolver.js +1 -1
  38. package/eslint.config.mjs +67 -0
  39. package/index.ts +34 -20
  40. package/package.json +57 -33
  41. package/src/commands/init.ts +79 -75
  42. package/src/commands/module/add.ts +158 -148
  43. package/src/commands/module/list.ts +61 -50
  44. package/src/commands/module/remove.ts +59 -54
  45. package/src/commands/module/update.ts +44 -42
  46. package/src/commands/run.ts +89 -81
  47. package/src/commands/setup.ts +92 -0
  48. package/src/utils/discovery.ts +98 -113
  49. package/src/utils/git.ts +35 -28
  50. package/src/utils/url-resolver.ts +50 -45
  51. package/test/e2e/lifecycle.e2e.test.ts +139 -130
  52. package/test/integration/commands/init.integration.test.ts +64 -61
  53. package/test/integration/commands/module.integration.test.ts +122 -122
  54. package/test/integration/commands/run.integration.test.ts +70 -63
  55. package/test/integration/utils/command-loading.integration.test.ts +40 -53
  56. package/test/unit/commands/init.test.ts +163 -128
  57. package/test/unit/commands/module/add.test.ts +312 -245
  58. package/test/unit/commands/module/list.test.ts +108 -91
  59. package/test/unit/commands/module/remove.test.ts +74 -67
  60. package/test/unit/commands/module/update.test.ts +74 -70
  61. package/test/unit/commands/run.test.ts +253 -201
  62. package/test/unit/commands/setup.test.ts +187 -0
  63. package/test/unit/utils/command-discovery.test.ts +138 -125
  64. package/test/unit/utils/git.test.ts +135 -117
  65. package/test/unit/utils/integration-helpers.test.ts +59 -49
  66. package/test/unit/utils/url-resolver.test.ts +46 -34
  67. package/test/utils/integration-helpers.ts +36 -29
  68. package/tsconfig.json +15 -25
  69. package/tsup.config.ts +14 -14
  70. package/vitest.config.ts +10 -10
  71. package/vitest.e2e.config.ts +6 -6
  72. package/vitest.integration.config.ts +17 -17
  73. package/dist/chunk-JYASTIIW.js.map +0 -1
  74. package/dist/chunk-OKXOCNXP.js +0 -105
  75. package/dist/chunk-OKXOCNXP.js.map +0 -1
  76. package/dist/chunk-WKERTCM6.js.map +0 -1
@@ -5,28 +5,33 @@ import fs from 'fs-extra';
5
5
  import { execa } from 'execa';
6
6
 
7
7
  describe('CLI Lifecycle E2E', () => {
8
- let testRoot: string;
9
- let starterDir: string;
10
- let moduleDir: string;
11
- let mockAstroDir: string;
12
-
13
- beforeEach(async () => {
14
- testRoot = await createTempDir('e2e-life-');
15
-
16
- // 1. Setup Mock "Astro" package
17
- // We create a local package that impersonates 'astro'.
18
- // This avoids npm install downloading the internet.
19
- mockAstroDir = path.join(testRoot, 'mock-astro');
20
- await fs.ensureDir(mockAstroDir);
21
- await fs.outputFile(path.join(mockAstroDir, 'package.json'), JSON.stringify({
22
- name: 'astro',
23
- version: '1.0.0',
24
- bin: {
25
- astro: './bin.js'
26
- }
27
- }));
28
- // The mock binary acts as "npx astro"
29
- await fs.outputFile(path.join(mockAstroDir, 'bin.js'), `#!/usr/bin/env node
8
+ let testRoot: string;
9
+ let starterDir: string;
10
+ let moduleDir: string;
11
+ let mockAstroDir: string;
12
+
13
+ beforeEach(async () => {
14
+ testRoot = await createTempDir('e2e-life-');
15
+
16
+ // 1. Setup Mock "Astro" package
17
+ // We create a local package that impersonates 'astro'.
18
+ // This avoids npm install downloading the internet.
19
+ mockAstroDir = path.join(testRoot, 'mock-astro');
20
+ await fs.ensureDir(mockAstroDir);
21
+ await fs.outputFile(
22
+ path.join(mockAstroDir, 'package.json'),
23
+ JSON.stringify({
24
+ name: 'astro',
25
+ version: '1.0.0',
26
+ bin: {
27
+ astro: './bin.js',
28
+ },
29
+ }),
30
+ );
31
+ // The mock binary acts as "npx astro"
32
+ await fs.outputFile(
33
+ path.join(mockAstroDir, 'bin.js'),
34
+ `#!/usr/bin/env node
30
35
  const fs = require('fs');
31
36
  const path = require('path');
32
37
  const args = process.argv.slice(2);
@@ -37,116 +42,120 @@ if (args[0] === 'build') {
37
42
  if (!fs.existsSync(dist)) fs.mkdirSync(dist, { recursive: true });
38
43
  fs.writeFileSync(path.join(dist, 'index.html'), '<html></html>');
39
44
  }
40
- `);
41
- await fs.chmod(path.join(mockAstroDir, 'bin.js'), '755');
42
-
43
- // 2. Setup Mock Starter Repo
44
- starterDir = path.join(testRoot, 'starter-repo');
45
- // We need a package.json that points 'astro' to our mock
46
- await createMockRepo(starterDir, {
47
- 'package.json': JSON.stringify({
48
- name: 'e2e-project',
49
- version: '0.0.0',
50
- dependencies: {
51
- // Use file: protocol to point to local mock
52
- 'astro': `file:${mockAstroDir}`
53
- },
54
- scripts: {
55
- 'build': 'astro build',
56
- 'dev': 'astro dev',
57
- 'preview': 'astro preview'
58
- }
59
- }),
60
- 'README.md': '# E2E Starter',
61
- 'nexical.yml': 'name: e2e-test\nversion: 0.0.1', // ESSENTIAL for CLI to recognize project
62
- 'src/pages/index.astro': '--- ---',
63
- 'src/core/index.ts': '// core',
64
- 'src/core/package.json': JSON.stringify({
65
- scripts: {
66
- 'build': 'astro build',
67
- 'dev': 'astro dev',
68
- 'preview': 'astro preview'
69
- }
70
- })
71
- });
72
-
73
- // 3. Setup Mock Module Repo
74
- moduleDir = path.join(testRoot, 'module-repo');
75
- await createMockRepo(moduleDir, {
76
- 'package.json': JSON.stringify({ name: 'my-module', version: '0.1.0' }),
77
- 'module.yaml': 'name: my-test-module\nversion: 0.1.0', // Name matches E2E expectation
78
- 'index.ts': 'console.log("module")'
79
- });
45
+ `,
46
+ );
47
+ await fs.chmod(path.join(mockAstroDir, 'bin.js'), '755');
48
+
49
+ // 2. Setup Mock Starter Repo
50
+ starterDir = path.join(testRoot, 'starter-repo');
51
+ // We need a package.json that points 'astro' to our mock
52
+ await createMockRepo(starterDir, {
53
+ 'package.json': JSON.stringify({
54
+ name: 'e2e-project',
55
+ version: '0.0.0',
56
+ dependencies: {
57
+ // Use file: protocol to point to local mock
58
+ astro: `file:${mockAstroDir}`,
59
+ },
60
+ scripts: {
61
+ build: 'astro build',
62
+ dev: 'astro dev',
63
+ preview: 'astro preview',
64
+ setup: 'echo setup',
65
+ },
66
+ }),
67
+ 'README.md': '# E2E Starter',
68
+ 'nexical.yml': 'name: e2e-test\nversion: 0.0.1', // ESSENTIAL for CLI to recognize project
69
+ 'src/pages/index.astro': '--- ---',
70
+ 'src/core/index.ts': '// core',
71
+ 'src/core/package.json': JSON.stringify({
72
+ scripts: {
73
+ build: 'astro build',
74
+ dev: 'astro dev',
75
+ preview: 'astro preview',
76
+ },
77
+ }),
80
78
  });
81
79
 
82
- afterAll(async () => {
83
- if (testRoot) await fs.remove(testRoot);
80
+ // 3. Setup Mock Module Repo
81
+ moduleDir = path.join(testRoot, 'module-repo');
82
+ await createMockRepo(moduleDir, {
83
+ 'package.json': JSON.stringify({ name: 'my-module', version: '0.1.0' }),
84
+ 'module.yaml': 'name: my-test-module\nversion: 0.1.0', // Name matches E2E expectation
85
+ 'index.ts': 'console.log("module")',
86
+ });
87
+ });
88
+
89
+ afterAll(async () => {
90
+ if (testRoot) await fs.remove(testRoot);
91
+ });
92
+
93
+ it('should complete a full project lifecycle', async () => {
94
+ const projectDir = path.join(testRoot, 'my-project');
95
+
96
+ const env = {
97
+ // Essential for git inside init/module commands
98
+ GIT_AUTHOR_NAME: 'Test User',
99
+ GIT_AUTHOR_EMAIL: 'test@example.com',
100
+ GIT_COMMITTER_NAME: 'Test User',
101
+ GIT_COMMITTER_EMAIL: 'test@example.com',
102
+ GIT_ALLOW_PROTOCOL: 'file',
103
+ // DEBUG: 'true' - Removed to reduce noise
104
+ };
105
+
106
+ // --- STEP 1: INIT ---
107
+ // Run: nexical init my-project --repo <starter>
108
+ const _initResult = await runCLI(['init', 'my-project', '--repo', starterDir], testRoot, {
109
+ env,
84
110
  });
85
111
 
86
- it('should complete a full project lifecycle', async () => {
87
- const projectDir = path.join(testRoot, 'my-project');
88
-
89
- const env = {
90
- // Essential for git inside init/module commands
91
- GIT_AUTHOR_NAME: 'Test User',
92
- GIT_AUTHOR_EMAIL: 'test@example.com',
93
- GIT_COMMITTER_NAME: 'Test User',
94
- GIT_COMMITTER_EMAIL: 'test@example.com',
95
- GIT_ALLOW_PROTOCOL: 'file'
96
- // DEBUG: 'true' - Removed to reduce noise
97
- };
98
-
99
- // --- STEP 1: INIT ---
100
- // Run: nexical init my-project --repo <starter>
101
- const initResult = await runCLI([
102
- 'init',
103
- 'my-project',
104
- '--repo', starterDir
105
- ], testRoot, { env });
106
-
107
- // 4. Check git initialization (preserved history)
108
- const { stdout: log } = await execa('git', ['log', '--oneline'], { cwd: projectDir });
109
- const lines = log.split('\n').filter(Boolean);
110
- expect(lines.length).toBeGreaterThanOrEqual(2);
111
- expect(lines[0]).toContain('Initial site commit');
112
-
113
- // --- STEP 2: MODULE ADD ---
114
- // Run: nexical module add <module>
115
- const modResult = await runCLI([
116
- 'module',
117
- 'add',
118
- moduleDir,
119
- 'my-test-module' // Explicit name
120
- ], projectDir, { env });
121
-
122
- if (modResult.exitCode !== 0) {
123
- console.error('Module Add Failed:', modResult.stderr || modResult.stdout);
124
- }
125
- expect(modResult.exitCode).toBe(0);
126
- expect(fs.existsSync(path.join(projectDir, 'modules/my-test-module'))).toBe(true);
127
-
128
- // --- STEP 3: BUILD ---
129
- // Run: nexical run build
130
- // Should trigger our mock astro binary
131
- const buildResult = await runCLI(['run', 'build'], projectDir, { env });
132
-
133
- if (buildResult.exitCode !== 0) {
134
- console.log(buildResult.stderr || buildResult.stdout);
135
- }
136
-
137
- expect(buildResult.exitCode).toBe(0);
138
- expect(buildResult.stdout).toContain('MOCK_ASTRO_EXECUTED build');
139
-
140
- // --- STEP 4: PREVIEW ---
141
- // Run: nexical run preview
142
- const previewResult = await runCLI(['run', 'preview'], projectDir, { env });
143
-
144
- expect(previewResult.exitCode).toBe(0);
145
- expect(previewResult.stdout).toContain('MOCK_ASTRO_EXECUTED preview');
146
-
147
- // --- STEP 5: CLEAN ---
148
- // Clean is now handled by manual file operations or external scripts,
149
- // it's no longer a top-level command.
150
-
151
- }, 120000); // Long timeout for full chain
112
+ // 4. Check git initialization (preserved history)
113
+ const { stdout: log } = await execa('git', ['log', '--oneline'], { cwd: projectDir });
114
+ const lines = log.split('\n').filter(Boolean);
115
+ expect(lines.length).toBeGreaterThanOrEqual(2);
116
+ expect(lines[0]).toContain('Initial site commit');
117
+
118
+ // --- STEP 2: MODULE ADD ---
119
+ // Run: nexical module add <module>
120
+ const modResult = await runCLI(
121
+ [
122
+ 'module',
123
+ 'add',
124
+ moduleDir,
125
+ 'my-test-module', // Explicit name
126
+ ],
127
+ projectDir,
128
+ { env },
129
+ );
130
+
131
+ if (modResult.exitCode !== 0) {
132
+ console.error('Module Add Failed:', modResult.stderr || modResult.stdout);
133
+ }
134
+ expect(modResult.exitCode).toBe(0);
135
+ expect(fs.existsSync(path.join(projectDir, 'modules/my-test-module'))).toBe(true);
136
+
137
+ // --- STEP 3: BUILD ---
138
+ // Run: nexical run build
139
+ // Should trigger our mock astro binary
140
+ const buildResult = await runCLI(['run', 'build'], projectDir, { env });
141
+
142
+ if (buildResult.exitCode !== 0) {
143
+ // eslint-disable-next-line no-console
144
+ console.log(buildResult.stderr || buildResult.stdout);
145
+ }
146
+
147
+ expect(buildResult.exitCode).toBe(0);
148
+ expect(buildResult.stdout).toContain('MOCK_ASTRO_EXECUTED build');
149
+
150
+ // --- STEP 4: PREVIEW ---
151
+ // Run: nexical run preview
152
+ const previewResult = await runCLI(['run', 'preview'], projectDir, { env });
153
+
154
+ expect(previewResult.exitCode).toBe(0);
155
+ expect(previewResult.stdout).toContain('MOCK_ASTRO_EXECUTED preview');
156
+
157
+ // --- STEP 5: CLEAN ---
158
+ // Clean is now handled by manual file operations or external scripts,
159
+ // it's no longer a top-level command.
160
+ }, 120000); // Long timeout for full chain
152
161
  });
@@ -1,82 +1,85 @@
1
1
  import { CLI } from '@nexical/cli-core';
2
- import { describe, it, expect, beforeEach, afterEach, afterAll } from 'vitest';
2
+ import { describe, it, expect, beforeEach, afterAll } from 'vitest';
3
3
  import InitCommand from '../../../src/commands/init.js';
4
- import { createTempDir, createMockRepo, cleanupTestRoot } from '../../utils/integration-helpers.js';
4
+ import { createTempDir, createMockRepo } from '../../utils/integration-helpers.js';
5
5
  import path from 'node:path';
6
6
  import fs from 'fs-extra';
7
7
  import { execa } from 'execa';
8
8
 
9
9
  describe('InitCommand Integration', () => {
10
- let tempDir: string;
11
- let starterRepoDir: string;
10
+ let tempDir: string;
11
+ let starterRepoDir: string;
12
12
 
13
- beforeEach(async () => {
14
- tempDir = await createTempDir('init-integration-');
15
- const starterDir = await createTempDir('starter-repo-');
13
+ beforeEach(async () => {
14
+ tempDir = await createTempDir('init-integration-');
15
+ const starterDir = await createTempDir('starter-repo-');
16
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';
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
+ scripts: {
23
+ setup: 'echo setup',
24
+ },
25
+ dependencies: {
26
+ 'is-odd': '3.0.1',
27
+ },
28
+ }),
29
+ 'README.md': '# Starter Template',
36
30
  });
37
31
 
38
- afterAll(async () => {
39
- // await cleanupTestRoot(); // conflicting with parallel tests
40
- if (tempDir) await fs.remove(tempDir);
41
- });
32
+ // Set Git Identity for the test process so InitCommand's commit works in CI
33
+ process.env.GIT_AUTHOR_NAME = 'Test User';
34
+ process.env.GIT_AUTHOR_EMAIL = 'test@example.com';
35
+ process.env.GIT_COMMITTER_NAME = 'Test User';
36
+ process.env.GIT_COMMITTER_EMAIL = 'test@example.com';
37
+ // Allow file protocol for local cloning in CI
38
+ process.env.GIT_ALLOW_PROTOCOL = 'file';
39
+ });
42
40
 
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' });
41
+ afterAll(async () => {
42
+ // await cleanupTestRoot(); // conflicting with parallel tests
43
+ if (tempDir) await fs.remove(tempDir);
44
+ });
47
45
 
48
- const command = new InitCommand(cli);
46
+ it('should initialize a new project from a local git repo', async () => {
47
+ const targetProjectName = 'my-new-project';
48
+ const targetPath = path.join(tempDir, targetProjectName);
49
+ const cli = new CLI({ commandName: 'nexical' });
49
50
 
50
- // Capture stdout/stderr? InitCommand uses consola.
51
- // For integration, we care about the FS side effects.
51
+ const command = new InitCommand(cli);
52
52
 
53
- await command.run({
54
- directory: targetPath,
55
- repo: starterRepoDir // Passing local path as repo URL
56
- });
53
+ // Capture stdout/stderr? InitCommand uses consola.
54
+ // For integration, we care about the FS side effects.
55
+
56
+ await command.run({
57
+ directory: targetPath,
58
+ repo: starterRepoDir, // Passing local path as repo URL
59
+ });
57
60
 
58
- // 1. Check directory exists
59
- expect(fs.existsSync(targetPath)).toBe(true);
61
+ // 1. Check directory exists
62
+ expect(fs.existsSync(targetPath)).toBe(true);
60
63
 
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
+ // 2. Check files cloned
65
+ expect(fs.existsSync(path.join(targetPath, 'package.json'))).toBe(true);
66
+ expect(fs.existsSync(path.join(targetPath, 'README.md'))).toBe(true);
64
67
 
65
- // 3. Check git initialization
66
- expect(fs.existsSync(path.join(targetPath, '.git'))).toBe(true);
68
+ // 3. Check git initialization
69
+ expect(fs.existsSync(path.join(targetPath, '.git'))).toBe(true);
67
70
 
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');
71
+ // 4. Check git initialization (history should be preserved + one new commit)
72
+ const { stdout: log } = await execa('git', ['log', '--oneline'], { cwd: targetPath });
73
+ const lines = log.split('\n').filter(Boolean);
74
+ expect(lines.length).toBe(2); // Should have "Initial site commit" and "Initial commit"
75
+ expect(lines[0]).toContain('Initial site commit');
76
+ expect(lines[1]).toContain('Initial commit');
74
77
 
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
78
+ // 5. Check dependencies (optional, but command tries to install them)
79
+ // Since we are mocking the repo, it doesn't have a real lockfile or valid deps,
80
+ // so `npm install` might have failed or done nothing.
81
+ // However, `InitCommand` runs `npm install`. If that fails, the command throws/exits.
82
+ // We provided a minimal package.json so it should succeed.
83
+ expect(fs.existsSync(path.join(targetPath, 'node_modules'))).toBe(true);
84
+ }, 60000); // Increase timeout for real git/npm ops
82
85
  });