@nexical/cli 0.11.0 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/deploy.yml +1 -1
- package/.husky/pre-commit +1 -0
- package/.prettierignore +8 -0
- package/.prettierrc +7 -0
- package/GEMINI.md +36 -30
- package/README.md +85 -56
- package/dist/chunk-AC4B3HPJ.js +93 -0
- package/dist/chunk-AC4B3HPJ.js.map +1 -0
- package/dist/{chunk-JYASTIIW.js → chunk-PJIOCW2A.js} +1 -1
- package/dist/chunk-PJIOCW2A.js.map +1 -0
- package/dist/{chunk-WKERTCM6.js → chunk-Q7YLW5HJ.js} +5 -2
- package/dist/chunk-Q7YLW5HJ.js.map +1 -0
- package/dist/index.js +41 -12
- package/dist/index.js.map +1 -1
- package/dist/src/commands/init.d.ts +4 -1
- package/dist/src/commands/init.js +8 -4
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/module/add.d.ts +3 -1
- package/dist/src/commands/module/add.js +24 -13
- package/dist/src/commands/module/add.js.map +1 -1
- package/dist/src/commands/module/list.js +9 -5
- package/dist/src/commands/module/list.js.map +1 -1
- package/dist/src/commands/module/remove.d.ts +3 -1
- package/dist/src/commands/module/remove.js +13 -7
- package/dist/src/commands/module/remove.js.map +1 -1
- package/dist/src/commands/module/update.d.ts +3 -1
- package/dist/src/commands/module/update.js +7 -5
- package/dist/src/commands/module/update.js.map +1 -1
- package/dist/src/commands/run.d.ts +4 -1
- package/dist/src/commands/run.js +10 -2
- package/dist/src/commands/run.js.map +1 -1
- package/dist/src/commands/setup.js +17 -4
- package/dist/src/commands/setup.js.map +1 -1
- package/dist/src/utils/discovery.js +1 -1
- package/dist/src/utils/git.js +1 -1
- package/dist/src/utils/url-resolver.js +1 -1
- package/eslint.config.mjs +67 -0
- package/index.ts +34 -20
- package/package.json +56 -32
- package/src/commands/init.ts +79 -76
- package/src/commands/module/add.ts +158 -148
- package/src/commands/module/list.ts +61 -50
- package/src/commands/module/remove.ts +59 -54
- package/src/commands/module/update.ts +44 -42
- package/src/commands/run.ts +89 -81
- package/src/commands/setup.ts +78 -60
- package/src/utils/discovery.ts +98 -113
- package/src/utils/git.ts +35 -28
- package/src/utils/url-resolver.ts +50 -45
- package/test/e2e/lifecycle.e2e.test.ts +139 -131
- package/test/integration/commands/init.integration.test.ts +64 -64
- package/test/integration/commands/module.integration.test.ts +122 -122
- package/test/integration/commands/run.integration.test.ts +70 -63
- package/test/integration/utils/command-loading.integration.test.ts +40 -53
- package/test/unit/commands/init.test.ts +163 -128
- package/test/unit/commands/module/add.test.ts +312 -245
- package/test/unit/commands/module/list.test.ts +108 -91
- package/test/unit/commands/module/remove.test.ts +74 -67
- package/test/unit/commands/module/update.test.ts +74 -70
- package/test/unit/commands/run.test.ts +253 -201
- package/test/unit/commands/setup.test.ts +146 -128
- package/test/unit/utils/command-discovery.test.ts +138 -125
- package/test/unit/utils/git.test.ts +135 -117
- package/test/unit/utils/integration-helpers.test.ts +59 -49
- package/test/unit/utils/url-resolver.test.ts +46 -34
- package/test/utils/integration-helpers.ts +36 -29
- package/tsconfig.json +15 -25
- package/tsup.config.ts +14 -14
- package/vitest.config.ts +10 -10
- package/vitest.e2e.config.ts +6 -6
- package/vitest.integration.config.ts +17 -17
- package/dist/chunk-JYASTIIW.js.map +0 -1
- package/dist/chunk-OKXOCNXP.js +0 -105
- package/dist/chunk-OKXOCNXP.js.map +0 -1
- package/dist/chunk-WKERTCM6.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
npx lint-staged
|
package/.prettierignore
ADDED
package/.prettierrc
ADDED
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
|
|
17
|
-
|
|
|
18
|
-
| `src/commands/init.ts`
|
|
19
|
-
| `src/commands/user/create.ts` | `app user create` | Subcommand
|
|
20
|
-
| `src/commands/user/index.ts`
|
|
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
|
-
|
|
138
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
193
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
107
|
-
|
|
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
|
-
|
|
202
|
-
|
|
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
|
-
|
|
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
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
-
|
|
312
|
-
|
|
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
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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":"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","names":[]}
|
|
@@ -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":[]}
|