@nexical/cli 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/GEMINI.md +193 -0
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/src/commands/init.js +7 -6
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/module/add.js +3 -3
- package/dist/src/commands/setup.d.ts +8 -0
- package/dist/src/commands/setup.js +62 -0
- package/dist/src/commands/setup.js.map +1 -0
- package/package.json +2 -2
- package/src/commands/init.ts +5 -4
- package/src/commands/setup.ts +74 -0
- package/test/e2e/lifecycle.e2e.test.ts +2 -1
- package/test/integration/commands/init.integration.test.ts +3 -0
- package/test/unit/commands/setup.test.ts +169 -0
package/GEMINI.md
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# Nexical CLI Core Development Guide
|
|
2
|
+
|
|
3
|
+
This guide details how to build command libraries and CLIs using the `@nexical/cli-core` framework within this project.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
The project is configured to use `@nexical/cli-core` as its CLI framework. usage entails:
|
|
8
|
+
1. **Entry Point**: `index.ts` initializes the `CLI` instance.
|
|
9
|
+
2. **Command Discovery**: The CLI automatically scans `src/commands` for command files.
|
|
10
|
+
3. **Command Implementation**: Commands are TypeScript classes extending `BaseCommand`.
|
|
11
|
+
|
|
12
|
+
## Directory Structure
|
|
13
|
+
|
|
14
|
+
Commands are defined in `src/commands`. The file structure directly maps to the command hierarchy.
|
|
15
|
+
|
|
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) |
|
|
21
|
+
|
|
22
|
+
> **Note**: The CLI name (`app`) is configured in `index.ts`.
|
|
23
|
+
|
|
24
|
+
## Creating a New Command
|
|
25
|
+
|
|
26
|
+
To create a new command, add a `.ts` file in `src/commands`.
|
|
27
|
+
|
|
28
|
+
### 1. Basic Command Template
|
|
29
|
+
|
|
30
|
+
Create `src/commands/hello.ts`:
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
import { BaseCommand } from '@nexical/cli-core';
|
|
34
|
+
|
|
35
|
+
export default class HelloCommand extends BaseCommand {
|
|
36
|
+
// Description displayed in help menus
|
|
37
|
+
static description = 'Prints a hello message';
|
|
38
|
+
|
|
39
|
+
// The main execution method
|
|
40
|
+
async run(options: any) {
|
|
41
|
+
this.success('Hello World!');
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
**Run it:**
|
|
47
|
+
```bash
|
|
48
|
+
npm run cli hello
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### 2. Arguments and Options
|
|
52
|
+
|
|
53
|
+
Use the static `args` property to define inputs.
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
import { BaseCommand } from '@nexical/cli-core';
|
|
57
|
+
|
|
58
|
+
export default class GreetCommand extends BaseCommand {
|
|
59
|
+
static description = 'Greets a user';
|
|
60
|
+
|
|
61
|
+
static args = {
|
|
62
|
+
// Positional Arguments
|
|
63
|
+
args: [
|
|
64
|
+
{
|
|
65
|
+
name: 'name',
|
|
66
|
+
description: 'Name of the user',
|
|
67
|
+
required: true
|
|
68
|
+
}
|
|
69
|
+
],
|
|
70
|
+
// Options (Flags)
|
|
71
|
+
options: [
|
|
72
|
+
{
|
|
73
|
+
name: '--loud',
|
|
74
|
+
description: 'Print in uppercase',
|
|
75
|
+
default: false
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: '-c, --count <n>',
|
|
79
|
+
description: 'Number of times to greet',
|
|
80
|
+
default: 1
|
|
81
|
+
}
|
|
82
|
+
]
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
async run(options: any) {
|
|
86
|
+
// 'name' is mapped from args
|
|
87
|
+
// 'loud' and 'count' are mapped from options
|
|
88
|
+
const { name, loud, count } = options;
|
|
89
|
+
|
|
90
|
+
let message = `Hello, ${name}`;
|
|
91
|
+
if (loud) message = message.toUpperCase();
|
|
92
|
+
|
|
93
|
+
for(let i=0; i < count; i++) {
|
|
94
|
+
this.info(message);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
**Run it:**
|
|
101
|
+
```bash
|
|
102
|
+
npm run cli greet Adrian --loud --count 3
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### 3. User Input & Prompts
|
|
106
|
+
|
|
107
|
+
`BaseCommand` provides built-in methods for interactivity.
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
export default class InteractiveCommand extends BaseCommand {
|
|
111
|
+
async run() {
|
|
112
|
+
// Simple confirmation or input
|
|
113
|
+
const name = await this.prompt('What is your name?');
|
|
114
|
+
|
|
115
|
+
this.success(`Nice to meet you, ${name}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### 4. Output Helpers
|
|
121
|
+
|
|
122
|
+
Use the built-in helper methods for consistent logging:
|
|
123
|
+
|
|
124
|
+
- `this.success(msg)`: Green checkmark (✔ Success)
|
|
125
|
+
- `this.error(msg)`: Red cross (✖ Error) - **Exits process**
|
|
126
|
+
- `this.warn(msg)`: Yellow warning (⚠ Warning)
|
|
127
|
+
- `this.info(msg)`: Standard log
|
|
128
|
+
- `this.notice(msg)`: Blue notice (📢 Note)
|
|
129
|
+
- `this.input(msg)`: Cyan input prompt (? Question)
|
|
130
|
+
|
|
131
|
+
## Subcommands
|
|
132
|
+
|
|
133
|
+
To create grouped commands (e.g., `user create`, `user list`), use directories.
|
|
134
|
+
|
|
135
|
+
1. **Create Directory**: `src/commands/user/`
|
|
136
|
+
2. **Add Commands**:
|
|
137
|
+
* `src/commands/user/create.ts` -> `app user create`
|
|
138
|
+
* `src/commands/user/list.ts` -> `app user list`
|
|
139
|
+
|
|
140
|
+
### Index Commands (Container Commands)
|
|
141
|
+
|
|
142
|
+
If you need the parent command `app user` to do something (or just provide a description for the group), create `src/commands/user/index.ts`.
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
// src/commands/user/index.ts
|
|
146
|
+
import { BaseCommand } from '@nexical/cli-core';
|
|
147
|
+
|
|
148
|
+
export default class UserCommand extends BaseCommand {
|
|
149
|
+
static description = 'Manage users';
|
|
150
|
+
|
|
151
|
+
async run() {
|
|
152
|
+
// This runs when user types 'app user' without a subcommand
|
|
153
|
+
// Often used to show help
|
|
154
|
+
this.cli.getRawCLI().outputHelp();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Internal API Reference
|
|
160
|
+
|
|
161
|
+
### `BaseCommand`
|
|
162
|
+
|
|
163
|
+
All commands inherit from `BaseCommand`.
|
|
164
|
+
|
|
165
|
+
**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`).
|
|
170
|
+
|
|
171
|
+
**Methods:**
|
|
172
|
+
* `init()`: Called before `run()`. Useful for setup.
|
|
173
|
+
* `run(options)`: Abstract method. Must be implemented.
|
|
174
|
+
* `prompt(message)`: Async, returns string.
|
|
175
|
+
|
|
176
|
+
**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
|
+
## Best Practices
|
|
184
|
+
|
|
185
|
+
1. **Type Safety**: While `options` is `any` in `run()`, validate inputs early.
|
|
186
|
+
2. **Error Handling**: Use `this.error()` for fatal errors to ensure proper exit codes.
|
|
187
|
+
3. **Clean Output**: Use the helper methods (`success`, `info`, etc.) instead of `console.log` for a consistent UI.
|
|
188
|
+
4. **Async**: The `run` method is async. Always `await` asynchronous operations.
|
|
189
|
+
|
|
190
|
+
## Troubleshooting
|
|
191
|
+
|
|
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.
|
package/dist/index.js
CHANGED
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../index.ts","../package.json"],"sourcesContent":["#!/usr/bin/env node\nimport { CLI, findProjectRoot } from '@nexical/cli-core';\nimport { fileURLToPath } from 'node:url';\nimport { discoverCommandDirectories } from './src/utils/discovery.js';\nimport pkg from './package.json';\nimport path from 'node:path';\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\nconst commandName = 'nexical';\nconst projectRoot = await findProjectRoot(commandName, process.cwd()) || process.cwd();\nconst coreCommandsDir = path.resolve(__dirname, './src/commands');\nconst additionalCommands = discoverCommandDirectories(projectRoot);\n\n// Filter out the source version of core commands if we are running from dist\nconst filteredAdditional = additionalCommands.filter(dir => {\n if (dir === coreCommandsDir) return false;\n\n // Handle the case where we are running from dist/ and it finds src/commands in projectRoot\n if (coreCommandsDir.includes(path.join(path.sep, 'dist', 'src', 'commands'))) {\n const srcVersion = coreCommandsDir.replace(\n path.join(path.sep, 'dist', 'src', 'commands'),\n path.join(path.sep, 'src', 'commands')\n );\n if (dir === srcVersion) return false;\n }\n return true;\n});\n\nconst app = new CLI({\n version: pkg.version,\n commandName: commandName,\n searchDirectories: [...new Set([\n coreCommandsDir,\n ...filteredAdditional\n ])]\n});\napp.start();\n","{\n \"name\": \"@nexical/cli\",\n \"version\": \"0.
|
|
1
|
+
{"version":3,"sources":["../index.ts","../package.json"],"sourcesContent":["#!/usr/bin/env node\nimport { CLI, findProjectRoot } from '@nexical/cli-core';\nimport { fileURLToPath } from 'node:url';\nimport { discoverCommandDirectories } from './src/utils/discovery.js';\nimport pkg from './package.json';\nimport path from 'node:path';\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\nconst commandName = 'nexical';\nconst projectRoot = await findProjectRoot(commandName, process.cwd()) || process.cwd();\nconst coreCommandsDir = path.resolve(__dirname, './src/commands');\nconst additionalCommands = discoverCommandDirectories(projectRoot);\n\n// Filter out the source version of core commands if we are running from dist\nconst filteredAdditional = additionalCommands.filter(dir => {\n if (dir === coreCommandsDir) return false;\n\n // Handle the case where we are running from dist/ and it finds src/commands in projectRoot\n if (coreCommandsDir.includes(path.join(path.sep, 'dist', 'src', 'commands'))) {\n const srcVersion = coreCommandsDir.replace(\n path.join(path.sep, 'dist', 'src', 'commands'),\n path.join(path.sep, 'src', 'commands')\n );\n if (dir === srcVersion) return false;\n }\n return true;\n});\n\nconst app = new CLI({\n version: pkg.version,\n commandName: commandName,\n searchDirectories: [...new Set([\n coreCommandsDir,\n ...filteredAdditional\n ])]\n});\napp.start();\n","{\n \"name\": \"@nexical/cli\",\n \"version\": \"0.11.0\",\n \"type\": \"module\",\n \"bin\": {\n \"nexical\": \"./dist/index.js\"\n },\n \"scripts\": {\n \"build\": \"tsup\",\n \"dev\": \"tsup --watch\",\n \"start\": \"node dist/index.js\",\n \"test\": \"npm run test:unit && npm run test:integration && npm run test:e2e\",\n \"test:unit\": \"vitest run --config vitest.config.ts --coverage\",\n \"test:integration\": \"vitest run --config vitest.integration.config.ts\",\n \"test:e2e\": \"npm run build && vitest run --config vitest.e2e.config.ts\",\n \"test:watch\": \"vitest\"\n },\n \"dependencies\": {\n \"@nexical/cli-core\": \"^0.1.12\",\n \"yaml\": \"^2.3.4\",\n \"fast-glob\": \"^3.3.3\"\n },\n \"devDependencies\": {\n \"@types/fs-extra\": \"^11.0.4\",\n \"@types/node\": \"^20.10.0\",\n \"@vitest/coverage-v8\": \"^4.0.15\",\n \"execa\": \"^9.6.1\",\n \"fs-extra\": \"^11.3.2\",\n \"tsup\": \"^8.0.1\",\n \"tsx\": \"^4.21.0\",\n \"typescript\": \"^5.3.3\",\n \"vitest\": \"^4.0.15\"\n }\n}\n"],"mappings":";;;;;;;;;;AAAA;AACA,SAAS,KAAK,uBAAuB;AACrC,SAAS,qBAAqB;;;ACF9B;AAAA,EACI,MAAQ;AAAA,EACR,SAAW;AAAA,EACX,MAAQ;AAAA,EACR,KAAO;AAAA,IACH,SAAW;AAAA,EACf;AAAA,EACA,SAAW;AAAA,IACP,OAAS;AAAA,IACT,KAAO;AAAA,IACP,OAAS;AAAA,IACT,MAAQ;AAAA,IACR,aAAa;AAAA,IACb,oBAAoB;AAAA,IACpB,YAAY;AAAA,IACZ,cAAc;AAAA,EAClB;AAAA,EACA,cAAgB;AAAA,IACZ,qBAAqB;AAAA,IACrB,MAAQ;AAAA,IACR,aAAa;AAAA,EACjB;AAAA,EACA,iBAAmB;AAAA,IACf,mBAAmB;AAAA,IACnB,eAAe;AAAA,IACf,uBAAuB;AAAA,IACvB,OAAS;AAAA,IACT,YAAY;AAAA,IACZ,MAAQ;AAAA,IACR,KAAO;AAAA,IACP,YAAc;AAAA,IACd,QAAU;AAAA,EACd;AACJ;;;AD5BA,OAAO,UAAU;AAEjB,IAAM,YAAY,KAAK,QAAQ,cAAc,YAAY,GAAG,CAAC;AAE7D,IAAM,cAAc;AACpB,IAAM,cAAc,MAAM,gBAAgB,aAAa,QAAQ,IAAI,CAAC,KAAK,QAAQ,IAAI;AACrF,IAAM,kBAAkB,KAAK,QAAQ,WAAW,gBAAgB;AAChE,IAAM,qBAAqB,2BAA2B,WAAW;AAGjE,IAAM,qBAAqB,mBAAmB,OAAO,SAAO;AACxD,MAAI,QAAQ,gBAAiB,QAAO;AAGpC,MAAI,gBAAgB,SAAS,KAAK,KAAK,KAAK,KAAK,QAAQ,OAAO,UAAU,CAAC,GAAG;AAC1E,UAAM,aAAa,gBAAgB;AAAA,MAC/B,KAAK,KAAK,KAAK,KAAK,QAAQ,OAAO,UAAU;AAAA,MAC7C,KAAK,KAAK,KAAK,KAAK,OAAO,UAAU;AAAA,IACzC;AACA,QAAI,QAAQ,WAAY,QAAO;AAAA,EACnC;AACA,SAAO;AACX,CAAC;AAED,IAAM,MAAM,IAAI,IAAI;AAAA,EAChB,SAAS,gBAAI;AAAA,EACb;AAAA,EACA,mBAAmB,CAAC,GAAG,oBAAI,IAAI;AAAA,IAC3B;AAAA,IACA,GAAG;AAAA,EACP,CAAC,CAAC;AACN,CAAC;AACD,IAAI,MAAM;","names":[]}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { createRequire } from "module"; const require = createRequire(import.meta.url);
|
|
2
|
+
import {
|
|
3
|
+
require_lib
|
|
4
|
+
} from "../../chunk-LZ3YQWAR.js";
|
|
2
5
|
import {
|
|
3
6
|
addAll,
|
|
4
7
|
clone,
|
|
@@ -9,9 +12,6 @@ import {
|
|
|
9
12
|
import {
|
|
10
13
|
resolveGitUrl
|
|
11
14
|
} from "../../chunk-JYASTIIW.js";
|
|
12
|
-
import {
|
|
13
|
-
require_lib
|
|
14
|
-
} from "../../chunk-LZ3YQWAR.js";
|
|
15
15
|
import {
|
|
16
16
|
__toESM,
|
|
17
17
|
init_esm_shims
|
|
@@ -34,7 +34,7 @@ var InitCommand = class extends BaseCommand {
|
|
|
34
34
|
{
|
|
35
35
|
name: "--repo <url>",
|
|
36
36
|
description: "Starter repository URL (supports gh@owner/repo syntax)",
|
|
37
|
-
default: "gh@nexical/app-
|
|
37
|
+
default: "gh@nexical/app-starter"
|
|
38
38
|
}
|
|
39
39
|
]
|
|
40
40
|
};
|
|
@@ -54,7 +54,7 @@ var InitCommand = class extends BaseCommand {
|
|
|
54
54
|
await import_fs_extra.default.mkdir(targetPath, { recursive: true });
|
|
55
55
|
}
|
|
56
56
|
try {
|
|
57
|
-
this.info("Cloning
|
|
57
|
+
this.info("Cloning starter repository...");
|
|
58
58
|
await clone(repoUrl, targetPath, { recursive: true });
|
|
59
59
|
this.info("Updating submodules...");
|
|
60
60
|
await updateSubmodules(targetPath);
|
|
@@ -62,7 +62,8 @@ var InitCommand = class extends BaseCommand {
|
|
|
62
62
|
await runCommand("npm install", targetPath);
|
|
63
63
|
this.info("Setting up upstream remote...");
|
|
64
64
|
await renameRemote("origin", "upstream", targetPath);
|
|
65
|
-
|
|
65
|
+
this.info("Running project setup...");
|
|
66
|
+
await runCommand("npm run setup", targetPath);
|
|
66
67
|
const configPath = path.join(targetPath, "nexical.yaml");
|
|
67
68
|
if (!await import_fs_extra.default.pathExists(configPath)) {
|
|
68
69
|
this.info("Creating default nexical.yaml...");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/commands/init.ts"],"sourcesContent":["import { type CommandDefinition, BaseCommand, logger, runCommand } from '@nexical/cli-core';\nimport * as git from '../utils/git.js';\nimport { resolveGitUrl } from '../utils/url-resolver.js';\nimport fs from 'fs-extra';\nimport path from 'path';\n\nexport default class InitCommand extends BaseCommand {\n static usage = 'init';\n static description = 'Initialize a new Nexical project.';\n static requiresProject = false;\n\n static args: CommandDefinition = {\n args: [\n { name: 'directory', required: true, description: 'Directory to initialize the project in' }\n ],\n options: [\n {\n name: '--repo <url>',\n description: 'Starter repository URL (supports gh@owner/repo syntax)',\n default: 'gh@nexical/app-
|
|
1
|
+
{"version":3,"sources":["../../../src/commands/init.ts"],"sourcesContent":["import { type CommandDefinition, BaseCommand, logger, runCommand } from '@nexical/cli-core';\nimport * as git from '../utils/git.js';\nimport { resolveGitUrl } from '../utils/url-resolver.js';\nimport fs from 'fs-extra';\nimport path from 'path';\n\nexport default class InitCommand extends BaseCommand {\n static usage = 'init';\n static description = 'Initialize a new Nexical project.';\n static requiresProject = false;\n\n static args: CommandDefinition = {\n args: [\n { name: 'directory', required: true, description: 'Directory to initialize the project in' }\n ],\n options: [\n {\n name: '--repo <url>',\n description: 'Starter repository URL (supports gh@owner/repo syntax)',\n default: 'gh@nexical/app-starter'\n }\n ]\n };\n\n async run(options: any) {\n const directory = options.directory;\n const targetPath = path.resolve(process.cwd(), directory);\n let repoUrl = resolveGitUrl(options.repo);\n\n logger.debug('Init options:', { directory, targetPath, repoUrl });\n\n this.info(`Initializing project in: ${targetPath}`);\n this.info(`Using starter repository: ${repoUrl}`);\n\n if (await fs.pathExists(targetPath)) {\n if ((await fs.readdir(targetPath)).length > 0) {\n this.error(`Directory ${directory} is not empty.`);\n process.exit(1);\n }\n } else {\n await fs.mkdir(targetPath, { recursive: true });\n }\n\n try {\n this.info('Cloning starter repository...');\n await git.clone(repoUrl, targetPath, { recursive: true });\n\n this.info('Updating submodules...');\n await git.updateSubmodules(targetPath);\n\n this.info('Installing dependencies...');\n await runCommand('npm install', targetPath);\n\n this.info('Setting up upstream remote...');\n await git.renameRemote('origin', 'upstream', targetPath);\n\n // Run setup script\n this.info('Running project setup...');\n await runCommand('npm run setup', targetPath);\n\n // Check for nexical.yaml, if not present create a default one\n const configPath = path.join(targetPath, 'nexical.yaml');\n if (!await fs.pathExists(configPath)) {\n this.info('Creating default nexical.yaml...');\n await fs.writeFile(configPath, 'name: ' + path.basename(targetPath) + '\\nmodules: []\\n');\n }\n\n // Create VERSION file\n const versionPath = path.join(targetPath, 'VERSION');\n // Check if version file exists, if not create it\n if (!await fs.pathExists(versionPath)) {\n this.info('Creating VERSION file with 0.1.0...');\n await fs.writeFile(versionPath, '0.1.0');\n }\n\n await git.addAll(targetPath);\n await git.commit('Initial site commit', targetPath);\n\n this.success(`Project initialized successfully in ${directory}!`);\n\n } catch (error: any) {\n this.error(`Failed to initialize project: ${error.message}`);\n process.exit(1);\n }\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA,SAAiC,aAAa,QAAQ,kBAAkB;AAGxE,sBAAe;AACf,OAAO,UAAU;AAEjB,IAAqB,cAArB,cAAyC,YAAY;AAAA,EACjD,OAAO,QAAQ;AAAA,EACf,OAAO,cAAc;AAAA,EACrB,OAAO,kBAAkB;AAAA,EAEzB,OAAO,OAA0B;AAAA,IAC7B,MAAM;AAAA,MACF,EAAE,MAAM,aAAa,UAAU,MAAM,aAAa,yCAAyC;AAAA,IAC/F;AAAA,IACA,SAAS;AAAA,MACL;AAAA,QACI,MAAM;AAAA,QACN,aAAa;AAAA,QACb,SAAS;AAAA,MACb;AAAA,IACJ;AAAA,EACJ;AAAA,EAEA,MAAM,IAAI,SAAc;AACpB,UAAM,YAAY,QAAQ;AAC1B,UAAM,aAAa,KAAK,QAAQ,QAAQ,IAAI,GAAG,SAAS;AACxD,QAAI,UAAU,cAAc,QAAQ,IAAI;AAExC,WAAO,MAAM,iBAAiB,EAAE,WAAW,YAAY,QAAQ,CAAC;AAEhE,SAAK,KAAK,4BAA4B,UAAU,EAAE;AAClD,SAAK,KAAK,6BAA6B,OAAO,EAAE;AAEhD,QAAI,MAAM,gBAAAA,QAAG,WAAW,UAAU,GAAG;AACjC,WAAK,MAAM,gBAAAA,QAAG,QAAQ,UAAU,GAAG,SAAS,GAAG;AAC3C,aAAK,MAAM,aAAa,SAAS,gBAAgB;AACjD,gBAAQ,KAAK,CAAC;AAAA,MAClB;AAAA,IACJ,OAAO;AACH,YAAM,gBAAAA,QAAG,MAAM,YAAY,EAAE,WAAW,KAAK,CAAC;AAAA,IAClD;AAEA,QAAI;AACA,WAAK,KAAK,+BAA+B;AACzC,YAAU,MAAM,SAAS,YAAY,EAAE,WAAW,KAAK,CAAC;AAExD,WAAK,KAAK,wBAAwB;AAClC,YAAU,iBAAiB,UAAU;AAErC,WAAK,KAAK,4BAA4B;AACtC,YAAM,WAAW,eAAe,UAAU;AAE1C,WAAK,KAAK,+BAA+B;AACzC,YAAU,aAAa,UAAU,YAAY,UAAU;AAGvD,WAAK,KAAK,0BAA0B;AACpC,YAAM,WAAW,iBAAiB,UAAU;AAG5C,YAAM,aAAa,KAAK,KAAK,YAAY,cAAc;AACvD,UAAI,CAAC,MAAM,gBAAAA,QAAG,WAAW,UAAU,GAAG;AAClC,aAAK,KAAK,kCAAkC;AAC5C,cAAM,gBAAAA,QAAG,UAAU,YAAY,WAAW,KAAK,SAAS,UAAU,IAAI,iBAAiB;AAAA,MAC3F;AAGA,YAAM,cAAc,KAAK,KAAK,YAAY,SAAS;AAEnD,UAAI,CAAC,MAAM,gBAAAA,QAAG,WAAW,WAAW,GAAG;AACnC,aAAK,KAAK,qCAAqC;AAC/C,cAAM,gBAAAA,QAAG,UAAU,aAAa,OAAO;AAAA,MAC3C;AAEA,YAAU,OAAO,UAAU;AAC3B,YAAU,OAAO,uBAAuB,UAAU;AAElD,WAAK,QAAQ,uCAAuC,SAAS,GAAG;AAAA,IAEpE,SAAS,OAAY;AACjB,WAAK,MAAM,iCAAiC,MAAM,OAAO,EAAE;AAC3D,cAAQ,KAAK,CAAC;AAAA,IAClB;AAAA,EACJ;AACJ;","names":["fs"]}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { createRequire } from "module"; const require = createRequire(import.meta.url);
|
|
2
|
+
import {
|
|
3
|
+
require_lib
|
|
4
|
+
} from "../../../chunk-LZ3YQWAR.js";
|
|
2
5
|
import {
|
|
3
6
|
clone,
|
|
4
7
|
getRemoteUrl
|
|
@@ -6,9 +9,6 @@ import {
|
|
|
6
9
|
import {
|
|
7
10
|
resolveGitUrl
|
|
8
11
|
} from "../../../chunk-JYASTIIW.js";
|
|
9
|
-
import {
|
|
10
|
-
require_lib
|
|
11
|
-
} from "../../../chunk-LZ3YQWAR.js";
|
|
12
12
|
import {
|
|
13
13
|
__toESM,
|
|
14
14
|
init_esm_shims
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { createRequire } from "module"; const require = createRequire(import.meta.url);
|
|
2
|
+
import {
|
|
3
|
+
require_lib
|
|
4
|
+
} from "../../chunk-LZ3YQWAR.js";
|
|
5
|
+
import {
|
|
6
|
+
__toESM,
|
|
7
|
+
init_esm_shims
|
|
8
|
+
} from "../../chunk-OYFWMYPG.js";
|
|
9
|
+
|
|
10
|
+
// src/commands/setup.ts
|
|
11
|
+
init_esm_shims();
|
|
12
|
+
var import_fs_extra = __toESM(require_lib(), 1);
|
|
13
|
+
import { BaseCommand, logger } from "@nexical/cli-core";
|
|
14
|
+
import path from "path";
|
|
15
|
+
var SetupCommand = class extends BaseCommand {
|
|
16
|
+
static description = "Setup the application environment by symlinking core assets.";
|
|
17
|
+
async run() {
|
|
18
|
+
const rootDir = process.cwd();
|
|
19
|
+
if (!import_fs_extra.default.existsSync(path.join(rootDir, "core"))) {
|
|
20
|
+
this.error('Could not find "core" directory. Are you in the project root?');
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
const apps = ["frontend", "backend"];
|
|
24
|
+
const sharedAssets = ["prisma", "src", "public", "locales", "scripts", "astro.config.mjs", "tsconfig.json"];
|
|
25
|
+
for (const app of apps) {
|
|
26
|
+
const appDir = path.join(rootDir, "apps", app);
|
|
27
|
+
if (!import_fs_extra.default.existsSync(appDir)) {
|
|
28
|
+
this.warn(`App directory ${app} not found. Skipping.`);
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
this.info(`Setting up ${app}...`);
|
|
32
|
+
for (const asset of sharedAssets) {
|
|
33
|
+
const source = path.join(rootDir, "core", asset);
|
|
34
|
+
const dest = path.join(appDir, asset);
|
|
35
|
+
if (!import_fs_extra.default.existsSync(source)) {
|
|
36
|
+
this.warn(`Source asset ${asset} not found in core.`);
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
const destDir = path.dirname(dest);
|
|
41
|
+
await import_fs_extra.default.ensureDir(destDir);
|
|
42
|
+
try {
|
|
43
|
+
const stats = import_fs_extra.default.lstatSync(dest);
|
|
44
|
+
import_fs_extra.default.removeSync(dest);
|
|
45
|
+
} catch (e) {
|
|
46
|
+
if (e.code !== "ENOENT") throw e;
|
|
47
|
+
}
|
|
48
|
+
const relSource = path.relative(destDir, source);
|
|
49
|
+
await import_fs_extra.default.symlink(relSource, dest);
|
|
50
|
+
logger.debug(`Symlinked ${asset} to ${app}`);
|
|
51
|
+
} catch (e) {
|
|
52
|
+
this.error(`Failed to symlink ${asset} to ${app}: ${e.message}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
this.success("Application setup complete.");
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
export {
|
|
60
|
+
SetupCommand as default
|
|
61
|
+
};
|
|
62
|
+
//# sourceMappingURL=setup.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/commands/setup.ts"],"sourcesContent":["import { BaseCommand, logger } from '@nexical/cli-core';\nimport fs from 'fs-extra';\nimport path from 'path';\n\nexport default class SetupCommand extends BaseCommand {\n static description = 'Setup the application environment by symlinking core assets.';\n\n async run() {\n // We assume we are in the project root\n // But the CLI might be run from anywhere?\n // findProjectRoot in index.ts handles finding the root.\n // BaseCommand has this.projectRoot?\n\n // BaseCommand doesn't expose projectRoot directly in current implementation seen in memory, checking source if possible?\n // InitCommand used process.cwd().\n\n // Let's assume process.cwd() is project root if run via `npm run setup` from root.\n const rootDir = process.cwd();\n\n // Verify we are in the right place\n if (!fs.existsSync(path.join(rootDir, 'core'))) {\n this.error('Could not find \"core\" directory. Are you in the project root?');\n process.exit(1);\n }\n\n const apps = ['frontend', 'backend'];\n const sharedAssets = ['prisma', 'src', 'public', 'locales', 'scripts', 'astro.config.mjs', 'tsconfig.json']; // tsconfig might be needed if extended\n\n for (const app of apps) {\n const appDir = path.join(rootDir, 'apps', app);\n if (!fs.existsSync(appDir)) {\n this.warn(`App directory ${app} not found. Skipping.`);\n continue;\n }\n\n this.info(`Setting up ${app}...`);\n\n for (const asset of sharedAssets) {\n const source = path.join(rootDir, 'core', asset);\n const dest = path.join(appDir, asset);\n\n if (!fs.existsSync(source)) {\n this.warn(`Source asset ${asset} not found in core.`);\n continue;\n }\n\n try {\n // Remove existing destination if it exists (to ensure clean symlink)\n // Be careful not to delete real files if they aren't symlinks?\n // For now, we assume setup controls these.\n\n const destDir = path.dirname(dest);\n await fs.ensureDir(destDir);\n\n try {\n const stats = fs.lstatSync(dest);\n fs.removeSync(dest);\n } catch (e: any) {\n if (e.code !== 'ENOENT') throw e;\n }\n\n const relSource = path.relative(destDir, source);\n await fs.symlink(relSource, dest);\n\n logger.debug(`Symlinked ${asset} to ${app}`);\n } catch (e: any) {\n this.error(`Failed to symlink ${asset} to ${app}: ${e.message}`);\n }\n }\n }\n\n this.success('Application setup complete.');\n }\n}\n"],"mappings":";;;;;;;;;;AAAA;AACA,sBAAe;AADf,SAAS,aAAa,cAAc;AAEpC,OAAO,UAAU;AAEjB,IAAqB,eAArB,cAA0C,YAAY;AAAA,EAClD,OAAO,cAAc;AAAA,EAErB,MAAM,MAAM;AAUR,UAAM,UAAU,QAAQ,IAAI;AAG5B,QAAI,CAAC,gBAAAA,QAAG,WAAW,KAAK,KAAK,SAAS,MAAM,CAAC,GAAG;AAC5C,WAAK,MAAM,+DAA+D;AAC1E,cAAQ,KAAK,CAAC;AAAA,IAClB;AAEA,UAAM,OAAO,CAAC,YAAY,SAAS;AACnC,UAAM,eAAe,CAAC,UAAU,OAAO,UAAU,WAAW,WAAW,oBAAoB,eAAe;AAE1G,eAAW,OAAO,MAAM;AACpB,YAAM,SAAS,KAAK,KAAK,SAAS,QAAQ,GAAG;AAC7C,UAAI,CAAC,gBAAAA,QAAG,WAAW,MAAM,GAAG;AACxB,aAAK,KAAK,iBAAiB,GAAG,uBAAuB;AACrD;AAAA,MACJ;AAEA,WAAK,KAAK,cAAc,GAAG,KAAK;AAEhC,iBAAW,SAAS,cAAc;AAC9B,cAAM,SAAS,KAAK,KAAK,SAAS,QAAQ,KAAK;AAC/C,cAAM,OAAO,KAAK,KAAK,QAAQ,KAAK;AAEpC,YAAI,CAAC,gBAAAA,QAAG,WAAW,MAAM,GAAG;AACxB,eAAK,KAAK,gBAAgB,KAAK,qBAAqB;AACpD;AAAA,QACJ;AAEA,YAAI;AAKA,gBAAM,UAAU,KAAK,QAAQ,IAAI;AACjC,gBAAM,gBAAAA,QAAG,UAAU,OAAO;AAE1B,cAAI;AACA,kBAAM,QAAQ,gBAAAA,QAAG,UAAU,IAAI;AAC/B,4BAAAA,QAAG,WAAW,IAAI;AAAA,UACtB,SAAS,GAAQ;AACb,gBAAI,EAAE,SAAS,SAAU,OAAM;AAAA,UACnC;AAEA,gBAAM,YAAY,KAAK,SAAS,SAAS,MAAM;AAC/C,gBAAM,gBAAAA,QAAG,QAAQ,WAAW,IAAI;AAEhC,iBAAO,MAAM,aAAa,KAAK,OAAO,GAAG,EAAE;AAAA,QAC/C,SAAS,GAAQ;AACb,eAAK,MAAM,qBAAqB,KAAK,OAAO,GAAG,KAAK,EAAE,OAAO,EAAE;AAAA,QACnE;AAAA,MACJ;AAAA,IACJ;AAEA,SAAK,QAAQ,6BAA6B;AAAA,EAC9C;AACJ;","names":["fs"]}
|
package/package.json
CHANGED
package/src/commands/init.ts
CHANGED
|
@@ -17,7 +17,7 @@ export default class InitCommand extends BaseCommand {
|
|
|
17
17
|
{
|
|
18
18
|
name: '--repo <url>',
|
|
19
19
|
description: 'Starter repository URL (supports gh@owner/repo syntax)',
|
|
20
|
-
default: 'gh@nexical/app-
|
|
20
|
+
default: 'gh@nexical/app-starter'
|
|
21
21
|
}
|
|
22
22
|
]
|
|
23
23
|
};
|
|
@@ -42,7 +42,7 @@ export default class InitCommand extends BaseCommand {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
try {
|
|
45
|
-
this.info('Cloning
|
|
45
|
+
this.info('Cloning starter repository...');
|
|
46
46
|
await git.clone(repoUrl, targetPath, { recursive: true });
|
|
47
47
|
|
|
48
48
|
this.info('Updating submodules...');
|
|
@@ -54,8 +54,9 @@ export default class InitCommand extends BaseCommand {
|
|
|
54
54
|
this.info('Setting up upstream remote...');
|
|
55
55
|
await git.renameRemote('origin', 'upstream', targetPath);
|
|
56
56
|
|
|
57
|
-
//
|
|
58
|
-
|
|
57
|
+
// Run setup script
|
|
58
|
+
this.info('Running project setup...');
|
|
59
|
+
await runCommand('npm run setup', targetPath);
|
|
59
60
|
|
|
60
61
|
// Check for nexical.yaml, if not present create a default one
|
|
61
62
|
const configPath = path.join(targetPath, 'nexical.yaml');
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { BaseCommand, logger } from '@nexical/cli-core';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
export default class SetupCommand extends BaseCommand {
|
|
6
|
+
static description = 'Setup the application environment by symlinking core assets.';
|
|
7
|
+
|
|
8
|
+
async run() {
|
|
9
|
+
// We assume we are in the project root
|
|
10
|
+
// But the CLI might be run from anywhere?
|
|
11
|
+
// findProjectRoot in index.ts handles finding the root.
|
|
12
|
+
// BaseCommand has this.projectRoot?
|
|
13
|
+
|
|
14
|
+
// BaseCommand doesn't expose projectRoot directly in current implementation seen in memory, checking source if possible?
|
|
15
|
+
// InitCommand used process.cwd().
|
|
16
|
+
|
|
17
|
+
// Let's assume process.cwd() is project root if run via `npm run setup` from root.
|
|
18
|
+
const rootDir = process.cwd();
|
|
19
|
+
|
|
20
|
+
// Verify we are in the right place
|
|
21
|
+
if (!fs.existsSync(path.join(rootDir, 'core'))) {
|
|
22
|
+
this.error('Could not find "core" directory. Are you in the project root?');
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const apps = ['frontend', 'backend'];
|
|
27
|
+
const sharedAssets = ['prisma', 'src', 'public', 'locales', 'scripts', 'astro.config.mjs', 'tsconfig.json']; // tsconfig might be needed if extended
|
|
28
|
+
|
|
29
|
+
for (const app of apps) {
|
|
30
|
+
const appDir = path.join(rootDir, 'apps', app);
|
|
31
|
+
if (!fs.existsSync(appDir)) {
|
|
32
|
+
this.warn(`App directory ${app} not found. Skipping.`);
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
this.info(`Setting up ${app}...`);
|
|
37
|
+
|
|
38
|
+
for (const asset of sharedAssets) {
|
|
39
|
+
const source = path.join(rootDir, 'core', asset);
|
|
40
|
+
const dest = path.join(appDir, asset);
|
|
41
|
+
|
|
42
|
+
if (!fs.existsSync(source)) {
|
|
43
|
+
this.warn(`Source asset ${asset} not found in core.`);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
// Remove existing destination if it exists (to ensure clean symlink)
|
|
49
|
+
// Be careful not to delete real files if they aren't symlinks?
|
|
50
|
+
// For now, we assume setup controls these.
|
|
51
|
+
|
|
52
|
+
const destDir = path.dirname(dest);
|
|
53
|
+
await fs.ensureDir(destDir);
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const stats = fs.lstatSync(dest);
|
|
57
|
+
fs.removeSync(dest);
|
|
58
|
+
} catch (e: any) {
|
|
59
|
+
if (e.code !== 'ENOENT') throw e;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const relSource = path.relative(destDir, source);
|
|
63
|
+
await fs.symlink(relSource, dest);
|
|
64
|
+
|
|
65
|
+
logger.debug(`Symlinked ${asset} to ${app}`);
|
|
66
|
+
} catch (e: any) {
|
|
67
|
+
this.error(`Failed to symlink ${asset} to ${app}: ${e.message}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
this.success('Application setup complete.');
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import SetupCommand from '../../../src/commands/setup.js';
|
|
3
|
+
import fs from 'fs-extra';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { CLI } from '@nexical/cli-core';
|
|
6
|
+
|
|
7
|
+
// Mock fs-extra
|
|
8
|
+
vi.mock('fs-extra');
|
|
9
|
+
|
|
10
|
+
describe('SetupCommand', () => {
|
|
11
|
+
let command: SetupCommand;
|
|
12
|
+
let mockCli: CLI;
|
|
13
|
+
let exitSpy: any;
|
|
14
|
+
|
|
15
|
+
// Mock BaseCommand methods
|
|
16
|
+
// We need to extend SetupCommand or mock the prototype to capture error/warn/success
|
|
17
|
+
// Or we can just spy on them if we can access the instance methods.
|
|
18
|
+
|
|
19
|
+
// Better approach: Spy on the prototype methods of BaseCommand or the instance itself.
|
|
20
|
+
// However, BaseCommand methods like `error` might process.exit.
|
|
21
|
+
|
|
22
|
+
// Let's create a subclass for testing or mock the CLI and use the standard instantiation.
|
|
23
|
+
// The current SetupCommand implementation calls `process.exit(1)` in `error` logic in `run`.
|
|
24
|
+
// Wait, looking at `setup.ts`:
|
|
25
|
+
// if (!fs.existsSync(path.join(rootDir, 'core'))) {
|
|
26
|
+
// this.error('Could not find "core" directory. Are you in the project root?');
|
|
27
|
+
// process.exit(1);
|
|
28
|
+
// }
|
|
29
|
+
|
|
30
|
+
// So we need to stub process.exit to prevent test runner from exiting.
|
|
31
|
+
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
vi.clearAllMocks();
|
|
34
|
+
mockCli = new CLI({ commandName: 'test-cli' });
|
|
35
|
+
command = new SetupCommand(mockCli);
|
|
36
|
+
|
|
37
|
+
// Spy on logging methods
|
|
38
|
+
vi.spyOn(command, 'error').mockImplementation(() => { });
|
|
39
|
+
vi.spyOn(command, 'warn').mockImplementation(() => { });
|
|
40
|
+
vi.spyOn(command, 'info').mockImplementation(() => { });
|
|
41
|
+
vi.spyOn(command, 'success').mockImplementation(() => { });
|
|
42
|
+
|
|
43
|
+
// Mock process.cwd to return a known path
|
|
44
|
+
vi.spyOn(process, 'cwd').mockReturnValue('/mock/project/root');
|
|
45
|
+
|
|
46
|
+
// Mock process.exit
|
|
47
|
+
exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => { }) as any);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
afterEach(() => {
|
|
51
|
+
vi.restoreAllMocks();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should error if "core" directory is missing', async () => {
|
|
55
|
+
// specific check: fs.existsSync returns false for core
|
|
56
|
+
vi.mocked(fs.existsSync).mockReturnValue(false);
|
|
57
|
+
|
|
58
|
+
await command.run();
|
|
59
|
+
|
|
60
|
+
expect(command.error).toHaveBeenCalledWith('Could not find "core" directory. Are you in the project root?');
|
|
61
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should warn and skip if app directory is missing', async () => {
|
|
65
|
+
// Setup fs mocks
|
|
66
|
+
vi.mocked(fs.existsSync).mockImplementation((p) => {
|
|
67
|
+
const pStr = p.toString();
|
|
68
|
+
if (pStr.endsWith('core')) return true;
|
|
69
|
+
if (pStr.endsWith('apps/frontend')) return true;
|
|
70
|
+
if (pStr.endsWith('apps/backend')) return false; // Missing backend
|
|
71
|
+
return false;
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
await command.run();
|
|
75
|
+
|
|
76
|
+
expect(command.warn).toHaveBeenCalledWith('App directory backend not found. Skipping.');
|
|
77
|
+
expect(command.info).toHaveBeenCalledWith('Setting up frontend...');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should symlink shared assets', async () => {
|
|
81
|
+
// Setup fs mocks
|
|
82
|
+
vi.mocked(fs.existsSync).mockImplementation((p) => {
|
|
83
|
+
const pStr = p.toString();
|
|
84
|
+
// Core exists
|
|
85
|
+
if (pStr.endsWith('core')) return true;
|
|
86
|
+
// Apps exist
|
|
87
|
+
if (pStr.endsWith('apps/frontend') || pStr.endsWith('apps/backend')) return true;
|
|
88
|
+
|
|
89
|
+
// Shared assets in core exist
|
|
90
|
+
if (pStr.includes('core/') && !pStr.endsWith('core')) return true;
|
|
91
|
+
|
|
92
|
+
return false;
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
vi.mocked(fs.lstatSync).mockReturnValue({ isSymbolicLink: () => true } as any);
|
|
96
|
+
|
|
97
|
+
await command.run();
|
|
98
|
+
|
|
99
|
+
// Check if verify apps are processed
|
|
100
|
+
expect(command.info).toHaveBeenCalledWith('Setting up frontend...');
|
|
101
|
+
expect(command.info).toHaveBeenCalledWith('Setting up backend...');
|
|
102
|
+
|
|
103
|
+
// Check symlink calls
|
|
104
|
+
// We have 2 apps * 7 shared assets = 14 symlinks
|
|
105
|
+
// sharedAssets = ['prisma', 'src', 'public', 'locales', 'scripts', 'astro.config.mjs', 'tsconfig.json']
|
|
106
|
+
|
|
107
|
+
const assets = ['prisma', 'src', 'public', 'locales', 'scripts', 'astro.config.mjs', 'tsconfig.json'];
|
|
108
|
+
|
|
109
|
+
for (const app of ['frontend', 'backend']) {
|
|
110
|
+
for (const asset of assets) {
|
|
111
|
+
const dest = path.join('/mock/project/root', 'apps', app, asset);
|
|
112
|
+
const source = path.join('/mock/project/root', 'core', asset);
|
|
113
|
+
|
|
114
|
+
// Ensure removeSync called
|
|
115
|
+
expect(fs.removeSync).toHaveBeenCalledWith(dest);
|
|
116
|
+
|
|
117
|
+
// Ensure symlink called
|
|
118
|
+
// valid relative path calculation might vary, but verify arguments
|
|
119
|
+
expect(fs.symlink).toHaveBeenCalled();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
expect(command.success).toHaveBeenCalledWith('Application setup complete.');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should warn if source asset is missing in core', async () => {
|
|
127
|
+
// Setup fs mocks
|
|
128
|
+
vi.mocked(fs.existsSync).mockImplementation((p) => {
|
|
129
|
+
const pStr = p.toString();
|
|
130
|
+
if (pStr.endsWith('core')) return true;
|
|
131
|
+
if (pStr.includes('apps/')) return true;
|
|
132
|
+
|
|
133
|
+
// Mock that 'prisma' is missing in core
|
|
134
|
+
if (pStr.endsWith('core/prisma')) return false;
|
|
135
|
+
|
|
136
|
+
// Others exist
|
|
137
|
+
if (pStr.includes('core/') && !pStr.endsWith('core')) return true;
|
|
138
|
+
|
|
139
|
+
return false;
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
await command.run();
|
|
143
|
+
|
|
144
|
+
expect(command.warn).toHaveBeenCalledWith('Source asset prisma not found in core.');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('should throw error if removal fails with non-ENOENT', async () => {
|
|
148
|
+
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
149
|
+
vi.mocked(fs.lstatSync).mockReturnValue({ isSymbolicLink: () => true } as any);
|
|
150
|
+
|
|
151
|
+
const error = new Error('Permission denied');
|
|
152
|
+
(error as any).code = 'EACCES';
|
|
153
|
+
vi.mocked(fs.removeSync).mockImplementation(() => { throw error; });
|
|
154
|
+
|
|
155
|
+
await command.run();
|
|
156
|
+
|
|
157
|
+
expect(command.error).toHaveBeenCalledWith(expect.stringContaining('Failed to symlink'));
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('should log error if symlink fails', async () => {
|
|
161
|
+
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
162
|
+
vi.mocked(fs.lstatSync).mockReturnValue({ isSymbolicLink: () => true } as any);
|
|
163
|
+
vi.mocked(fs.symlink).mockRejectedValue(new Error('Symlink failed'));
|
|
164
|
+
|
|
165
|
+
await command.run();
|
|
166
|
+
|
|
167
|
+
expect(command.error).toHaveBeenCalledWith(expect.stringContaining('Failed to symlink'));
|
|
168
|
+
});
|
|
169
|
+
});
|