@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.
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 +9 -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 +70 -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 +138 -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
@@ -31,4 +31,4 @@ jobs:
31
31
  - name: Publish to NPM
32
32
  run: npm publish --access public
33
33
  env:
34
- NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
34
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
@@ -0,0 +1 @@
1
+ npx lint-staged
@@ -0,0 +1,8 @@
1
+ dist/
2
+ coverage/
3
+ node_modules/
4
+ .astro/
5
+ .next/
6
+ pnpm-lock.yaml
7
+ package-lock.json
8
+ yarn.lock
package/.prettierrc ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "semi": true,
3
+ "singleQuote": true,
4
+ "tabWidth": 2,
5
+ "trailingComma": "all",
6
+ "printWidth": 100
7
+ }
package/GEMINI.md CHANGED
@@ -5,6 +5,7 @@ This guide details how to build command libraries and CLIs using the `@nexical/c
5
5
  ## Overview
6
6
 
7
7
  The project is configured to use `@nexical/cli-core` as its CLI framework. usage entails:
8
+
8
9
  1. **Entry Point**: `index.ts` initializes the `CLI` instance.
9
10
  2. **Command Discovery**: The CLI automatically scans `src/commands` for command files.
10
11
  3. **Command Implementation**: Commands are TypeScript classes extending `BaseCommand`.
@@ -13,11 +14,11 @@ The project is configured to use `@nexical/cli-core` as its CLI framework. usage
13
14
 
14
15
  Commands are defined in `src/commands`. The file structure directly maps to the command hierarchy.
15
16
 
16
- | File Path | Command | Description |
17
- | :--- | :--- | :--- |
18
- | `src/commands/init.ts` | `app init` | Root level command |
19
- | `src/commands/user/create.ts` | `app user create` | Subcommand |
20
- | `src/commands/user/index.ts` | `app user` | Parent command handler (optional) |
17
+ | File Path | Command | Description |
18
+ | :---------------------------- | :---------------- | :-------------------------------- |
19
+ | `src/commands/init.ts` | `app init` | Root level command |
20
+ | `src/commands/user/create.ts` | `app user create` | Subcommand |
21
+ | `src/commands/user/index.ts` | `app user` | Parent command handler (optional) |
21
22
 
22
23
  > **Note**: The CLI name (`app`) is configured in `index.ts`.
23
24
 
@@ -44,6 +45,7 @@ export default class HelloCommand extends BaseCommand {
44
45
  ```
45
46
 
46
47
  **Run it:**
48
+
47
49
  ```bash
48
50
  npm run cli hello
49
51
  ```
@@ -64,33 +66,33 @@ export default class GreetCommand extends BaseCommand {
64
66
  {
65
67
  name: 'name',
66
68
  description: 'Name of the user',
67
- required: true
68
- }
69
+ required: true,
70
+ },
69
71
  ],
70
72
  // Options (Flags)
71
73
  options: [
72
74
  {
73
75
  name: '--loud',
74
76
  description: 'Print in uppercase',
75
- default: false
77
+ default: false,
76
78
  },
77
79
  {
78
80
  name: '-c, --count <n>',
79
81
  description: 'Number of times to greet',
80
- default: 1
81
- }
82
- ]
82
+ default: 1,
83
+ },
84
+ ],
83
85
  };
84
86
 
85
87
  async run(options: any) {
86
88
  // 'name' is mapped from args
87
89
  // 'loud' and 'count' are mapped from options
88
90
  const { name, loud, count } = options;
89
-
91
+
90
92
  let message = `Hello, ${name}`;
91
93
  if (loud) message = message.toUpperCase();
92
94
 
93
- for(let i=0; i < count; i++) {
95
+ for (let i = 0; i < count; i++) {
94
96
  this.info(message);
95
97
  }
96
98
  }
@@ -98,6 +100,7 @@ export default class GreetCommand extends BaseCommand {
98
100
  ```
99
101
 
100
102
  **Run it:**
103
+
101
104
  ```bash
102
105
  npm run cli greet Adrian --loud --count 3
103
106
  ```
@@ -111,7 +114,7 @@ export default class InteractiveCommand extends BaseCommand {
111
114
  async run() {
112
115
  // Simple confirmation or input
113
116
  const name = await this.prompt('What is your name?');
114
-
117
+
115
118
  this.success(`Nice to meet you, ${name}`);
116
119
  }
117
120
  }
@@ -134,8 +137,8 @@ To create grouped commands (e.g., `user create`, `user list`), use directories.
134
137
 
135
138
  1. **Create Directory**: `src/commands/user/`
136
139
  2. **Add Commands**:
137
- * `src/commands/user/create.ts` -> `app user create`
138
- * `src/commands/user/list.ts` -> `app user list`
140
+ - `src/commands/user/create.ts` -> `app user create`
141
+ - `src/commands/user/list.ts` -> `app user list`
139
142
 
140
143
  ### Index Commands (Container Commands)
141
144
 
@@ -163,22 +166,25 @@ export default class UserCommand extends BaseCommand {
163
166
  All commands inherit from `BaseCommand`.
164
167
 
165
168
  **Properties:**
166
- * `cli`: Access to the main `CLI` instance.
167
- * `projectRoot`: Path to the project root (if detected).
168
- * `config`: Loaded configuration from `{cliName}.yml`.
169
- * `globalOptions`: Global flags passed to the CLI (e.g., `--debug`).
169
+
170
+ - `cli`: Access to the main `CLI` instance.
171
+ - `projectRoot`: Path to the project root (if detected).
172
+ - `config`: Loaded configuration from `{cliName}.yml`.
173
+ - `globalOptions`: Global flags passed to the CLI (e.g., `--debug`).
170
174
 
171
175
  **Methods:**
172
- * `init()`: Called before `run()`. Useful for setup.
173
- * `run(options)`: Abstract method. Must be implemented.
174
- * `prompt(message)`: Async, returns string.
176
+
177
+ - `init()`: Called before `run()`. Useful for setup.
178
+ - `run(options)`: Abstract method. Must be implemented.
179
+ - `prompt(message)`: Async, returns string.
175
180
 
176
181
  **Static Properties:**
177
- * `description`: String. Shown in help.
178
- * `args`: Object. Defines arguments and options.
179
- * `args`: Array of `{ name, description, required, default }`.
180
- * `options`: Array of `{ name, description, default }`.
181
- * `requiresProject`: Boolean. If true, command fails if not run inside a project with a config file.
182
+
183
+ - `description`: String. Shown in help.
184
+ - `args`: Object. Defines arguments and options.
185
+ - `args`: Array of `{ name, description, required, default }`.
186
+ - `options`: Array of `{ name, description, default }`.
187
+ - `requiresProject`: Boolean. If true, command fails if not run inside a project with a config file.
182
188
 
183
189
  ## Best Practices
184
190
 
@@ -189,5 +195,5 @@ All commands inherit from `BaseCommand`.
189
195
 
190
196
  ## Troubleshooting
191
197
 
192
- * **Command not found**: Ensure the file exports a class leveraging `export default` and extends `BaseCommand`.
193
- * **Changes not reflected**: If using `tsup`, ensure you are building or running in dev mode (`npm run dev`). For `npm run cli` using `ts-node` (via `cli.ts` or similar), changes should be instant.
198
+ - **Command not found**: Ensure the file exports a class leveraging `export default` and extends `BaseCommand`.
199
+ - **Changes not reflected**: If using `tsup`, ensure you are building or running in dev mode (`npm run dev`). For `npm run cli` using `ts-node` (via `cli.ts` or similar), changes should be instant.
package/README.md CHANGED
@@ -11,9 +11,9 @@ This project serves as the primary entry point for managing Nexical projects, ha
11
11
  - [Getting Started](#getting-started)
12
12
  - [Project Structure](#project-structure)
13
13
  - [Development Workflow](#development-workflow)
14
- - [Prerequisites](#prerequisites)
15
- - [Setup](#setup)
16
- - [Running Tests](#running-tests)
14
+ - [Prerequisites](#prerequisites)
15
+ - [Setup](#setup)
16
+ - [Running Tests](#running-tests)
17
17
  - [Adding New Commands](#adding-new-commands)
18
18
  - [Contributing](#contributing)
19
19
  - [License](#license)
@@ -23,6 +23,7 @@ This project serves as the primary entry point for managing Nexical projects, ha
23
23
  ## Purpose
24
24
 
25
25
  The Nexical CLI allows developers to:
26
+
26
27
  1. **Initialize** new Nexical projects with best practices built-in.
27
28
  2. **Manage** project configuration and dependencies.
28
29
  3. **Extend** the framework functionality through modular commands.
@@ -34,27 +35,30 @@ It acts as a unification layer, bringing together various tools and configuratio
34
35
  This CLI is built with **TypeScript** and follows a **Class-Based Command Pattern** to ensure type safety and maintainability.
35
36
 
36
37
  ### Key Technologies
37
- * **[CAC (Command And Conquer)](https://github.com/cacjs/cac)**: A lightweight, robust framework for building CLIs. It handles argument parsing, help generation, and command registration.
38
- * **[Consola](https://github.com/unjs/consola)**: Elegant console logging with fallback and structured output capabilities.
39
- * **[Lilconfig](https://github.com/antonk52/lilconfig)**: Configuration loading (searching for `nexical.yml`, `nexical.yaml`) akin to `cosmiconfig` but lighter.
40
- * **Vitest**: A blazing fast unit test framework powered by Vite.
38
+
39
+ - **[CAC (Command And Conquer)](https://github.com/cacjs/cac)**: A lightweight, robust framework for building CLIs. It handles argument parsing, help generation, and command registration.
40
+ - **[Consola](https://github.com/unjs/consola)**: Elegant console logging with fallback and structured output capabilities.
41
+ - **[Lilconfig](https://github.com/antonk52/lilconfig)**: Configuration loading (searching for `nexical.yml`, `nexical.yaml`) akin to `cosmiconfig` but lighter.
42
+ - **Vitest**: A blazing fast unit test framework powered by Vite.
41
43
 
42
44
  ### Core Components
45
+
43
46
  1. **`CLI` Class** (`src/core/CLI.ts`): The orchestrator. It initializes the CAC instance, discovers commands using the `CommandLoader`, registers them, and handles the execution lifecycle.
44
47
  2. **`CommandLoader`** (`src/core/CommandLoader.ts`): Responsible for dynamically discovering and importing command files from the filesystem. It supports:
45
- * Recursive directory scanning.
46
- * Nested commands (e.g., `module/add.ts` -> `module add`).
47
- * Index files as parent commands (e.g., `module/index.ts` -> `module`).
48
+ - Recursive directory scanning.
49
+ - Nested commands (e.g., `module/add.ts` -> `module add`).
50
+ - Index files as parent commands (e.g., `module/index.ts` -> `module`).
48
51
  3. **`BaseCommand`** (`src/core/BaseCommand.ts`): The abstract base class that all commands MUST extend. It provides:
49
- * Standardized `init()` and `run()` lifecycle methods.
50
- * Built-in access to global options (like `--root-dir`).
51
- * Helper methods for logging (`this.log`, `this.warn`, `this.error`).
52
- * Project root detection (`this.projectRoot`).
52
+ - Standardized `init()` and `run()` lifecycle methods.
53
+ - Built-in access to global options (like `--root-dir`).
54
+ - Helper methods for logging (`this.log`, `this.warn`, `this.error`).
55
+ - Project root detection (`this.projectRoot`).
53
56
 
54
57
  ### Design Goals
55
- * **Zero-Config Defaults**: It should work out of the box but allow rich configuration via `nexical.yaml`.
56
- * **Extensibility**: Adding a command should be as simple as adding a file.
57
- * **Testability**: Every component is designed to be unit-testable, with dependency injection where appropriate (e.g., `CommandLoader` importer).
58
+
59
+ - **Zero-Config Defaults**: It should work out of the box but allow rich configuration via `nexical.yaml`.
60
+ - **Extensibility**: Adding a command should be as simple as adding a file.
61
+ - **Testability**: Every component is designed to be unit-testable, with dependency injection where appropriate (e.g., `CommandLoader` importer).
58
62
 
59
63
  ---
60
64
 
@@ -94,19 +98,23 @@ npx nexical help module add
94
98
  Initializes a new Nexical project by cloning a starter repository, setting up dependencies, and preparing a fresh git history.
95
99
 
96
100
  **Usage:**
101
+
97
102
  ```bash
98
103
  npx nexical init <directory> [options]
99
104
  ```
100
105
 
101
106
  **Arguments:**
107
+
102
108
  - `directory` (Required): The directory to initialize the project in. If the directory does not exist, it will be created. If it does exist, it must be empty.
103
109
 
104
110
  **Options:**
111
+
105
112
  - `--repo <url>` (Default: `https://github.com/nexical/app-core`): The URL of the starter repository to clone.
106
- - Supports standard Git URLs (e.g., `https://github.com/user/repo.git`).
107
- - Supports GitHub short syntax `gh@owner/repo` (e.g., `gh@nexical/app-core`).
113
+ - Supports standard Git URLs (e.g., `https://github.com/user/repo.git`).
114
+ - Supports GitHub short syntax `gh@owner/repo` (e.g., `gh@nexical/app-core`).
108
115
 
109
116
  **What it does:**
117
+
110
118
  1. **Clones** the specified starter repository (recursively, including submodules) into the target directory.
111
119
  2. **Updates** all submodules to their latest `main` branch.
112
120
  3. **Installs** dependencies using `npm install`.
@@ -118,6 +126,7 @@ npx nexical init <directory> [options]
118
126
  - Removes the `origin` remote to prevent accidental pushes to the starter repo.
119
127
 
120
128
  **Output:**
129
+
121
130
  - A ready-to-use Nexical project in the specified directory, with fresh git history and installed dependencies.
122
131
 
123
132
  ---
@@ -127,13 +136,12 @@ npx nexical init <directory> [options]
127
136
  Starts the development server in ephemeral mode. It constructs a temporary build environment in `site` and runs the Astro dev server with Hot Module Replacement (HMR).
128
137
 
129
138
  **Usage:**
139
+
130
140
  ```bash
131
141
  npx nexical dev
132
142
  ```
133
143
 
134
- **What it does:**
135
- 2. **Starts** the Astro development server (accessible at `http://localhost:4321` by default).
136
- 3. **Watches** for changes in your project and updates the ephemeral build automatically.
144
+ **What it does:** 2. **Starts** the Astro development server (accessible at `http://localhost:4321` by default). 3. **Watches** for changes in your project and updates the ephemeral build automatically.
137
145
 
138
146
  ---
139
147
 
@@ -142,16 +150,19 @@ npx nexical dev
142
150
  Compiles the project for production. It assembles the final site structure in `site` and generates static assets.
143
151
 
144
152
  **Usage:**
153
+
145
154
  ```bash
146
155
  npx nexical build
147
156
  ```
148
157
 
149
158
  **What it does:**
159
+
150
160
  1. **Cleans** the `site` directory to ensure a fresh build.
151
161
  2. **Copies** all necessary source files (`src/`, `modules`, `src/content`, `public`) into `site`.
152
162
  3. **Runs** `astro build` to generate the production output in `site/dist`.
153
163
 
154
164
  **Output:**
165
+
155
166
  - A production-ready static site in `site/dist`.
156
167
 
157
168
  ---
@@ -161,14 +172,17 @@ npx nexical build
161
172
  Previews the locally built production site. This is useful for verifying the output of `nexical build` before deploying.
162
173
 
163
174
  **Usage:**
175
+
164
176
  ```bash
165
177
  npx nexical preview
166
178
  ```
167
179
 
168
180
  **Prerequisites:**
181
+
169
182
  - You must run `nexical build` first.
170
183
 
171
184
  **What it does:**
185
+
172
186
  - Starts a local web server serving the static files from `site/dist`.
173
187
 
174
188
  ---
@@ -178,11 +192,13 @@ npx nexical preview
178
192
  Removes generated build artifacts and temporary directories to ensure a clean state.
179
193
 
180
194
  **Usage:**
195
+
181
196
  ```bash
182
197
  npx nexical clean
183
198
  ```
184
199
 
185
200
  **What it does:**
201
+
186
202
  - Deletes `site`, `dist`, and `node_modules/.vite`.
187
203
 
188
204
  ---
@@ -192,17 +208,20 @@ npx nexical clean
192
208
  Executes a script within the Nexical environment context. This handles path resolution and environment variable setup for you.
193
209
 
194
210
  **Usage:**
211
+
195
212
  ```bash
196
213
  npx nexical run <script> [args...]
197
214
  ```
198
215
 
199
216
  **Arguments:**
217
+
200
218
  - `script` (Required): The name of the script to run.
201
- - Can be a standard `package.json` script (e.g., `test`).
202
- - Can be a module-specific script using `module:script` syntax (e.g., `blog:sync`).
219
+ - Can be a standard `package.json` script (e.g., `test`).
220
+ - Can be a module-specific script using `module:script` syntax (e.g., `blog:sync`).
203
221
  - `args` (Optional): Additional arguments to pass to the script.
204
222
 
205
223
  **Examples:**
224
+
206
225
  ```bash
207
226
  # Run a core project script
208
227
  npx nexical run test
@@ -222,16 +241,19 @@ Manages the modular architecture of your Nexical project. Allows you to add, rem
222
241
  Adds a new module as a Git submodule.
223
242
 
224
243
  **Usage:**
244
+
225
245
  ```bash
226
246
  npx nexical module add <url> [name]
227
247
  ```
228
248
 
229
249
  **Arguments:**
250
+
230
251
  - `url` (Required): The Git repository URL of the module.
231
- - Supports `gh@owner/repo` shorthand.
252
+ - Supports `gh@owner/repo` shorthand.
232
253
  - `name` (Optional): The folder name for the module. Defaults to the repository name.
233
254
 
234
255
  **What it does:**
256
+
235
257
  1. Adds the repository as a git submodule in `src/modules/<name>`.
236
258
  2. Installs any new dependencies via `npm install`.
237
259
 
@@ -240,11 +262,13 @@ npx nexical module add <url> [name]
240
262
  Lists all installed modules in the project.
241
263
 
242
264
  **Usage:**
265
+
243
266
  ```bash
244
267
  npx nexical module list
245
268
  ```
246
269
 
247
270
  **Output:**
271
+
248
272
  - A table showing the name, version, and description of each installed module found in `src/modules`.
249
273
 
250
274
  ##### `module update`
@@ -252,14 +276,17 @@ npx nexical module list
252
276
  Updates one or all modules to their latest remote commit.
253
277
 
254
278
  **Usage:**
279
+
255
280
  ```bash
256
281
  npx nexical module update [name]
257
282
  ```
258
283
 
259
284
  **Arguments:**
285
+
260
286
  - `name` (Optional): The specific module to update. If omitted, all modules are updated.
261
287
 
262
288
  **What it does:**
289
+
263
290
  1. Runs `git submodule update --remote --merge` for the target(s).
264
291
  2. Re-installs dependencies to ensure `package-lock.json` is consistent.
265
292
 
@@ -268,14 +295,17 @@ npx nexical module update [name]
268
295
  Removes an installed module and cleans up references.
269
296
 
270
297
  **Usage:**
298
+
271
299
  ```bash
272
300
  npx nexical module remove <name>
273
301
  ```
274
302
 
275
303
  **Arguments:**
304
+
276
305
  - `name` (Required): The name of the module to remove.
277
306
 
278
307
  **What it does:**
308
+
279
309
  1. De-initializes the git submodule.
280
310
  2. Removes the module directory from `src/modules`.
281
311
  3. Cleans up internal git metadata (`.git/modules`).
@@ -298,22 +328,24 @@ graph TD
298
328
  utils-->logger.ts
299
329
  ```
300
330
 
301
- * **`src/commands/`**: Contains the implementations of individual CLI commands. File names correspond to command names.
302
- * **`src/core/`**: The framework logic (Command loading, Base class, CLI orchestration).
303
- * **`src/utils/`**: Shared utilities (Logging, Configuration parsing).
304
- * **`test/unit/`**: Co-located unit tests. Mirrors the `src` structure.
331
+ - **`src/commands/`**: Contains the implementations of individual CLI commands. File names correspond to command names.
332
+ - **`src/core/`**: The framework logic (Command loading, Base class, CLI orchestration).
333
+ - **`src/utils/`**: Shared utilities (Logging, Configuration parsing).
334
+ - **`test/unit/`**: Co-located unit tests. Mirrors the `src` structure.
305
335
 
306
336
  ---
307
337
 
308
338
  ## Development Workflow
309
339
 
310
340
  ### Prerequisites
311
- * Node.js (v18+ recommended)
312
- * NPM
341
+
342
+ - Node.js (v18+ recommended)
343
+ - NPM
313
344
 
314
345
  ### Setup
315
346
 
316
347
  1. **Install Dependencies**:
348
+
317
349
  ```bash
318
350
  npm install
319
351
  ```
@@ -347,43 +379,40 @@ To create a new command, add a TypeScript file to `src/commands/`.
347
379
  import { BaseCommand } from '../core/BaseCommand.js';
348
380
 
349
381
  export default class HelloCommand extends BaseCommand {
350
- // 1. Define command metadata
351
- static description = 'Say hello to the world';
352
-
353
- static args = {
354
- args: [
355
- { name: 'name', required: false, description: 'User name' }
356
- ],
357
- options: [
358
- { name: '--shout', description: 'Say it loud', default: false }
359
- ]
360
- };
361
-
362
- // 2. Implement the run method
363
- async run(options: any) {
364
- const name = options.name || 'World';
365
-
366
- if (options.shout) {
367
- this.success(`HELLO ${name.toUpperCase()}!`);
368
- } else {
369
- this.log(`Hello ${name}`);
370
- }
382
+ // 1. Define command metadata
383
+ static description = 'Say hello to the world';
384
+
385
+ static args = {
386
+ args: [{ name: 'name', required: false, description: 'User name' }],
387
+ options: [{ name: '--shout', description: 'Say it loud', default: false }],
388
+ };
389
+
390
+ // 2. Implement the run method
391
+ async run(options: any) {
392
+ const name = options.name || 'World';
393
+
394
+ if (options.shout) {
395
+ this.success(`HELLO ${name.toUpperCase()}!`);
396
+ } else {
397
+ this.log(`Hello ${name}`);
371
398
  }
399
+ }
372
400
  }
373
401
  ```
374
402
 
375
403
  **Key Requirement**: The file MUST default export a class extending `BaseCommand`.
376
404
 
377
- * **File Naming**:
378
- * `hello.ts` -> Command: `hello`
379
- * `users/create.ts` -> Command: `users create`
380
- * `users/index.ts` -> Command: `users` (Parent command)
405
+ - **File Naming**:
406
+ - `hello.ts` -> Command: `hello`
407
+ - `users/create.ts` -> Command: `users create`
408
+ - `users/index.ts` -> Command: `users` (Parent command)
381
409
 
382
410
  ---
383
411
 
384
412
  ## Contributing
385
413
 
386
414
  Contributions are welcome! Please follow these steps:
415
+
387
416
  1. Fork the repository.
388
417
  2. Create a feature branch.
389
418
  3. Add your changes and **ensure tests pass with 100% coverage**.
@@ -0,0 +1,93 @@
1
+ import { createRequire } from "module"; const require = createRequire(import.meta.url);
2
+ import {
3
+ init_esm_shims
4
+ } from "./chunk-OYFWMYPG.js";
5
+
6
+ // src/utils/discovery.ts
7
+ init_esm_shims();
8
+ import { logger } from "@nexical/cli-core";
9
+ import path from "path";
10
+ import fs from "fs";
11
+ function discoverCommandDirectories(projectRoot) {
12
+ const directories = [];
13
+ const visited = /* @__PURE__ */ new Set();
14
+ const isTsEnvironment = process.argv[1]?.endsWith(".ts") || process.argv[1]?.endsWith(".mts") || process.execArgv.some(
15
+ (arg) => arg.includes("tsx") || arg.includes("ts-node") || arg.includes("vitest")
16
+ ) || process.env.VITEST === "true" || process.env.NODE_ENV === "test";
17
+ const addDir = (dir) => {
18
+ const resolved = path.resolve(dir);
19
+ if (!fs.existsSync(resolved)) {
20
+ logger.debug(`Command directory not found (skipping): ${resolved}`);
21
+ return;
22
+ }
23
+ if (visited.has(resolved)) return;
24
+ const srcPattern = path.join(path.sep, "src", "commands");
25
+ const distPattern = path.join(path.sep, "dist");
26
+ const isSrcDir = resolved.endsWith(srcPattern) && !resolved.includes(distPattern);
27
+ if (isSrcDir) {
28
+ const distPath1 = resolved.replace(
29
+ srcPattern,
30
+ path.join(path.sep, "dist", "src", "commands")
31
+ );
32
+ const distPath2 = resolved.replace(srcPattern, path.join(path.sep, "dist", "commands"));
33
+ if (fs.existsSync(distPath1) || fs.existsSync(distPath2)) {
34
+ logger.debug(`Skipping src commands at ${resolved} because dist exists`);
35
+ return;
36
+ }
37
+ if (!isTsEnvironment) {
38
+ logger.debug(`Skipping src commands at ${resolved}: no TS loader detected`);
39
+ return;
40
+ }
41
+ }
42
+ logger.debug(`Found command directory: ${resolved}`);
43
+ directories.push(resolved);
44
+ visited.add(resolved);
45
+ };
46
+ const possibleCorePaths = [path.join(projectRoot, "src/commands")];
47
+ possibleCorePaths.forEach(addDir);
48
+ const searchRoots = [
49
+ path.join(projectRoot, "modules"),
50
+ path.join(projectRoot, "src", "modules"),
51
+ path.join(projectRoot, "packages")
52
+ ];
53
+ searchRoots.forEach((root) => {
54
+ if (!fs.existsSync(root)) return;
55
+ try {
56
+ const entries = fs.readdirSync(root);
57
+ for (const entry of entries) {
58
+ if (entry.startsWith(".")) continue;
59
+ const entryPath = path.join(root, entry);
60
+ if (!fs.statSync(entryPath).isDirectory()) continue;
61
+ const possiblePaths = [
62
+ path.join(entryPath, "dist/src/commands"),
63
+ path.join(entryPath, "dist/commands"),
64
+ path.join(entryPath, "src/commands")
65
+ ];
66
+ let foundDist = false;
67
+ for (const p of possiblePaths) {
68
+ if (fs.existsSync(p) && fs.statSync(p).isDirectory()) {
69
+ if (p.includes(path.sep + "dist" + path.sep)) {
70
+ addDir(p);
71
+ foundDist = true;
72
+ break;
73
+ }
74
+ }
75
+ }
76
+ if (!foundDist) {
77
+ const srcPath = path.join(entryPath, "src/commands");
78
+ if (fs.existsSync(srcPath) && fs.statSync(srcPath).isDirectory()) {
79
+ addDir(srcPath);
80
+ }
81
+ }
82
+ }
83
+ } catch (e) {
84
+ logger.debug(`Error scanning root ${root}: ${e instanceof Error ? e.message : String(e)}`);
85
+ }
86
+ });
87
+ return directories;
88
+ }
89
+
90
+ export {
91
+ discoverCommandDirectories
92
+ };
93
+ //# sourceMappingURL=chunk-AC4B3HPJ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/utils/discovery.ts"],"sourcesContent":["import { logger } from '@nexical/cli-core';\nimport path from 'node:path';\nimport fs from 'node:fs';\n\n/**\n * Discovers command directories to load into the CLI.\n *\n * Scans for:\n * 1. Core commands (projectRoot/src/commands)\n * 2. Module commands (projectRoot/src/modules/ * /src/commands)\n *\n * @param projectRoot - The root directory of the project\n * @returns Array of absolute paths to command directories\n */\nexport function discoverCommandDirectories(projectRoot: string): string[] {\n const directories: string[] = [];\n const visited = new Set<string>();\n\n const isTsEnvironment =\n process.argv[1]?.endsWith('.ts') ||\n process.argv[1]?.endsWith('.mts') ||\n process.execArgv.some(\n (arg) => arg.includes('tsx') || arg.includes('ts-node') || arg.includes('vitest'),\n ) ||\n process.env.VITEST === 'true' ||\n process.env.NODE_ENV === 'test';\n\n const addDir = (dir: string) => {\n const resolved = path.resolve(dir);\n if (!fs.existsSync(resolved)) {\n logger.debug(`Command directory not found (skipping): ${resolved}`);\n return;\n }\n\n if (visited.has(resolved)) return;\n\n // Detect if this is a source command directory\n const srcPattern = path.join(path.sep, 'src', 'commands');\n const distPattern = path.join(path.sep, 'dist');\n const isSrcDir = resolved.endsWith(srcPattern) && !resolved.includes(distPattern);\n\n // Strict check: if we are adding a 'src' directory...\n if (isSrcDir) {\n // 1. Check if an equivalent 'dist' exists in the same package\n const distPath1 = resolved.replace(\n srcPattern,\n path.join(path.sep, 'dist', 'src', 'commands'),\n );\n const distPath2 = resolved.replace(srcPattern, path.join(path.sep, 'dist', 'commands'));\n\n if (fs.existsSync(distPath1) || fs.existsSync(distPath2)) {\n logger.debug(`Skipping src commands at ${resolved} because dist exists`);\n return;\n }\n\n // 2. If no TS loader, skip src/commands entirely IF it's likely to contain .ts\n if (!isTsEnvironment) {\n logger.debug(`Skipping src commands at ${resolved}: no TS loader detected`);\n return;\n }\n }\n\n logger.debug(`Found command directory: ${resolved}`);\n directories.push(resolved);\n visited.add(resolved);\n };\n\n // 1. Core commands\n const possibleCorePaths = [path.join(projectRoot, 'src/commands')];\n possibleCorePaths.forEach(addDir);\n\n // 2. Module & Package commands\n const searchRoots = [\n path.join(projectRoot, 'modules'),\n path.join(projectRoot, 'src', 'modules'),\n path.join(projectRoot, 'packages'),\n ];\n\n searchRoots.forEach((root) => {\n if (!fs.existsSync(root)) return;\n try {\n const entries = fs.readdirSync(root);\n for (const entry of entries) {\n if (entry.startsWith('.')) continue;\n const entryPath = path.join(root, entry);\n if (!fs.statSync(entryPath).isDirectory()) continue;\n\n // Preference: dist/src/commands > dist/commands > src/commands\n const possiblePaths = [\n path.join(entryPath, 'dist/src/commands'),\n path.join(entryPath, 'dist/commands'),\n path.join(entryPath, 'src/commands'),\n ];\n\n let foundDist = false;\n for (const p of possiblePaths) {\n if (fs.existsSync(p) && fs.statSync(p).isDirectory()) {\n if (p.includes(path.sep + 'dist' + path.sep)) {\n addDir(p);\n foundDist = true;\n break; // Found a dist version, skip others for this entry\n }\n }\n }\n\n if (!foundDist) {\n const srcPath = path.join(entryPath, 'src/commands');\n if (fs.existsSync(srcPath) && fs.statSync(srcPath).isDirectory()) {\n addDir(srcPath);\n }\n }\n }\n } catch (e: unknown) {\n logger.debug(`Error scanning root ${root}: ${e instanceof Error ? e.message : String(e)}`);\n }\n });\n\n return directories;\n}\n"],"mappings":";;;;;;AAAA;AAAA,SAAS,cAAc;AACvB,OAAO,UAAU;AACjB,OAAO,QAAQ;AAYR,SAAS,2BAA2B,aAA+B;AACxE,QAAM,cAAwB,CAAC;AAC/B,QAAM,UAAU,oBAAI,IAAY;AAEhC,QAAM,kBACJ,QAAQ,KAAK,CAAC,GAAG,SAAS,KAAK,KAC/B,QAAQ,KAAK,CAAC,GAAG,SAAS,MAAM,KAChC,QAAQ,SAAS;AAAA,IACf,CAAC,QAAQ,IAAI,SAAS,KAAK,KAAK,IAAI,SAAS,SAAS,KAAK,IAAI,SAAS,QAAQ;AAAA,EAClF,KACA,QAAQ,IAAI,WAAW,UACvB,QAAQ,IAAI,aAAa;AAE3B,QAAM,SAAS,CAAC,QAAgB;AAC9B,UAAM,WAAW,KAAK,QAAQ,GAAG;AACjC,QAAI,CAAC,GAAG,WAAW,QAAQ,GAAG;AAC5B,aAAO,MAAM,2CAA2C,QAAQ,EAAE;AAClE;AAAA,IACF;AAEA,QAAI,QAAQ,IAAI,QAAQ,EAAG;AAG3B,UAAM,aAAa,KAAK,KAAK,KAAK,KAAK,OAAO,UAAU;AACxD,UAAM,cAAc,KAAK,KAAK,KAAK,KAAK,MAAM;AAC9C,UAAM,WAAW,SAAS,SAAS,UAAU,KAAK,CAAC,SAAS,SAAS,WAAW;AAGhF,QAAI,UAAU;AAEZ,YAAM,YAAY,SAAS;AAAA,QACzB;AAAA,QACA,KAAK,KAAK,KAAK,KAAK,QAAQ,OAAO,UAAU;AAAA,MAC/C;AACA,YAAM,YAAY,SAAS,QAAQ,YAAY,KAAK,KAAK,KAAK,KAAK,QAAQ,UAAU,CAAC;AAEtF,UAAI,GAAG,WAAW,SAAS,KAAK,GAAG,WAAW,SAAS,GAAG;AACxD,eAAO,MAAM,4BAA4B,QAAQ,sBAAsB;AACvE;AAAA,MACF;AAGA,UAAI,CAAC,iBAAiB;AACpB,eAAO,MAAM,4BAA4B,QAAQ,yBAAyB;AAC1E;AAAA,MACF;AAAA,IACF;AAEA,WAAO,MAAM,4BAA4B,QAAQ,EAAE;AACnD,gBAAY,KAAK,QAAQ;AACzB,YAAQ,IAAI,QAAQ;AAAA,EACtB;AAGA,QAAM,oBAAoB,CAAC,KAAK,KAAK,aAAa,cAAc,CAAC;AACjE,oBAAkB,QAAQ,MAAM;AAGhC,QAAM,cAAc;AAAA,IAClB,KAAK,KAAK,aAAa,SAAS;AAAA,IAChC,KAAK,KAAK,aAAa,OAAO,SAAS;AAAA,IACvC,KAAK,KAAK,aAAa,UAAU;AAAA,EACnC;AAEA,cAAY,QAAQ,CAAC,SAAS;AAC5B,QAAI,CAAC,GAAG,WAAW,IAAI,EAAG;AAC1B,QAAI;AACF,YAAM,UAAU,GAAG,YAAY,IAAI;AACnC,iBAAW,SAAS,SAAS;AAC3B,YAAI,MAAM,WAAW,GAAG,EAAG;AAC3B,cAAM,YAAY,KAAK,KAAK,MAAM,KAAK;AACvC,YAAI,CAAC,GAAG,SAAS,SAAS,EAAE,YAAY,EAAG;AAG3C,cAAM,gBAAgB;AAAA,UACpB,KAAK,KAAK,WAAW,mBAAmB;AAAA,UACxC,KAAK,KAAK,WAAW,eAAe;AAAA,UACpC,KAAK,KAAK,WAAW,cAAc;AAAA,QACrC;AAEA,YAAI,YAAY;AAChB,mBAAW,KAAK,eAAe;AAC7B,cAAI,GAAG,WAAW,CAAC,KAAK,GAAG,SAAS,CAAC,EAAE,YAAY,GAAG;AACpD,gBAAI,EAAE,SAAS,KAAK,MAAM,SAAS,KAAK,GAAG,GAAG;AAC5C,qBAAO,CAAC;AACR,0BAAY;AACZ;AAAA,YACF;AAAA,UACF;AAAA,QACF;AAEA,YAAI,CAAC,WAAW;AACd,gBAAM,UAAU,KAAK,KAAK,WAAW,cAAc;AACnD,cAAI,GAAG,WAAW,OAAO,KAAK,GAAG,SAAS,OAAO,EAAE,YAAY,GAAG;AAChE,mBAAO,OAAO;AAAA,UAChB;AAAA,QACF;AAAA,MACF;AAAA,IACF,SAAS,GAAY;AACnB,aAAO,MAAM,uBAAuB,IAAI,KAAK,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC,CAAC,EAAE;AAAA,IAC3F;AAAA,EACF,CAAC;AAED,SAAO;AACT;","names":[]}
@@ -39,4 +39,4 @@ function resolveGitUrl(url) {
39
39
  export {
40
40
  resolveGitUrl
41
41
  };
42
- //# sourceMappingURL=chunk-JYASTIIW.js.map
42
+ //# sourceMappingURL=chunk-PJIOCW2A.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/utils/url-resolver.ts"],"sourcesContent":["/**\n * Resolves a git URL from various shorthand formats.\n *\n * Supported formats:\n * - gh@org/repo -> https://github.com/org/repo.git\n * - gh@org/repo//path -> https://github.com/org/repo.git//path\n * - https://github.com/org/repo -> https://github.com/org/repo.git\n * - https://github.com/org/repo.git -> https://github.com/org/repo.git\n *\n * @param url The URL string to resolve\n * @returns The fully qualified git URL with .git extension\n */\nexport function resolveGitUrl(url: string): string {\n if (!url) {\n throw new Error('URL cannot be empty');\n }\n\n let resolved = url;\n\n // Handle gh@ syntax\n if (resolved.startsWith('gh@')) {\n resolved = resolved.replace(/^gh@/, 'https://github.com/');\n }\n\n // Handle subpaths (split by //)\n // We must be careful not to split the protocol (e.g. https://)\n const protocolMatch = resolved.match(/^[a-z0-9]+:\\/\\//i);\n let splitIndex = -1;\n\n if (protocolMatch) {\n splitIndex = resolved.indexOf('//', protocolMatch[0].length);\n } else {\n splitIndex = resolved.indexOf('//');\n }\n\n let repoUrl = resolved;\n let subPath = '';\n\n if (splitIndex !== -1) {\n repoUrl = resolved.substring(0, splitIndex);\n subPath = resolved.substring(splitIndex + 2);\n }\n\n // Ensure .git extension, but ONLY for remote URLs (not local paths)\n const isLocal =\n repoUrl.startsWith('/') ||\n repoUrl.startsWith('./') ||\n repoUrl.startsWith('../') ||\n repoUrl.startsWith('file:') ||\n repoUrl.startsWith('~');\n\n if (!isLocal && !repoUrl.endsWith('.git')) {\n repoUrl += '.git';\n }\n\n // Reconstruction\n if (subPath) {\n return `${repoUrl}//${subPath}`;\n }\n\n return repoUrl;\n}\n"],"mappings":";;;;;;AAAA;AAYO,SAAS,cAAc,KAAqB;AACjD,MAAI,CAAC,KAAK;AACR,UAAM,IAAI,MAAM,qBAAqB;AAAA,EACvC;AAEA,MAAI,WAAW;AAGf,MAAI,SAAS,WAAW,KAAK,GAAG;AAC9B,eAAW,SAAS,QAAQ,QAAQ,qBAAqB;AAAA,EAC3D;AAIA,QAAM,gBAAgB,SAAS,MAAM,kBAAkB;AACvD,MAAI,aAAa;AAEjB,MAAI,eAAe;AACjB,iBAAa,SAAS,QAAQ,MAAM,cAAc,CAAC,EAAE,MAAM;AAAA,EAC7D,OAAO;AACL,iBAAa,SAAS,QAAQ,IAAI;AAAA,EACpC;AAEA,MAAI,UAAU;AACd,MAAI,UAAU;AAEd,MAAI,eAAe,IAAI;AACrB,cAAU,SAAS,UAAU,GAAG,UAAU;AAC1C,cAAU,SAAS,UAAU,aAAa,CAAC;AAAA,EAC7C;AAGA,QAAM,UACJ,QAAQ,WAAW,GAAG,KACtB,QAAQ,WAAW,IAAI,KACvB,QAAQ,WAAW,KAAK,KACxB,QAAQ,WAAW,OAAO,KAC1B,QAAQ,WAAW,GAAG;AAExB,MAAI,CAAC,WAAW,CAAC,QAAQ,SAAS,MAAM,GAAG;AACzC,eAAW;AAAA,EACb;AAGA,MAAI,SAAS;AACX,WAAO,GAAG,OAAO,KAAK,OAAO;AAAA,EAC/B;AAEA,SAAO;AACT;","names":[]}