@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.
- 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 +9 -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 +70 -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 +138 -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
|
@@ -6,164 +6,174 @@ import { resolveGitUrl } from '../../utils/url-resolver.js';
|
|
|
6
6
|
import YAML from 'yaml';
|
|
7
7
|
|
|
8
8
|
export default class ModuleAddCommand extends BaseCommand {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
static usage = 'module add <url>';
|
|
10
|
+
static description = 'Add a module and its dependencies as git submodules.';
|
|
11
|
+
static requiresProject = true;
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
]
|
|
17
|
-
};
|
|
13
|
+
static args: CommandDefinition = {
|
|
14
|
+
args: [{ name: 'url', required: true, description: 'Git repository URL or gh@org/repo' }],
|
|
15
|
+
};
|
|
18
16
|
|
|
19
|
-
|
|
17
|
+
private visited = new Set<string>();
|
|
20
18
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
async run(options: { url: string }) {
|
|
20
|
+
const projectRoot = this.projectRoot as string;
|
|
21
|
+
const { url } = options;
|
|
24
22
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
23
|
+
if (!url) {
|
|
24
|
+
this.error('Please specify a repository URL.');
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
await this.installModule(url);
|
|
29
30
|
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
this.info('Syncing workspace dependencies...');
|
|
32
|
+
await runCommand('npm install', projectRoot);
|
|
33
|
+
|
|
34
|
+
this.success('All modules installed successfully.');
|
|
35
|
+
} catch (e: unknown) {
|
|
36
|
+
if (e instanceof Error) {
|
|
37
|
+
this.error(`Failed to add module: ${e.message}`);
|
|
38
|
+
} else {
|
|
39
|
+
this.error(`Failed to add module: ${String(e)}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
32
43
|
|
|
33
|
-
|
|
34
|
-
|
|
44
|
+
private async installModule(url: string) {
|
|
45
|
+
const projectRoot = this.projectRoot as string;
|
|
35
46
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
47
|
+
// Resolve URL using utility
|
|
48
|
+
url = resolveGitUrl(url);
|
|
49
|
+
|
|
50
|
+
const [repoUrl, subPath] = url.split('.git//');
|
|
51
|
+
const cleanUrl = subPath ? repoUrl + '.git' : url;
|
|
52
|
+
|
|
53
|
+
if (this.visited.has(cleanUrl)) {
|
|
54
|
+
logger.debug(`Already visited ${cleanUrl}, skipping.`);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
this.visited.add(cleanUrl);
|
|
58
|
+
|
|
59
|
+
this.info(`Inspecting ${cleanUrl}...`);
|
|
60
|
+
|
|
61
|
+
// Stage 1: Inspect (Temp Clone)
|
|
62
|
+
const stagingDir = path.resolve(
|
|
63
|
+
projectRoot!,
|
|
64
|
+
'.nexical',
|
|
65
|
+
'cache',
|
|
66
|
+
`staging-${Date.now()}-${Math.random().toString(36).substring(7)}`,
|
|
67
|
+
);
|
|
68
|
+
let moduleName = '';
|
|
69
|
+
let dependencies: string[] = [];
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
await fs.ensureDir(stagingDir);
|
|
73
|
+
|
|
74
|
+
// Shallow clone to inspect
|
|
75
|
+
await clone(cleanUrl, stagingDir, { depth: 1 });
|
|
76
|
+
|
|
77
|
+
// Read module.yaml
|
|
78
|
+
const searchPath = subPath ? path.join(stagingDir, subPath) : stagingDir;
|
|
79
|
+
const moduleYamlPath = path.join(searchPath, 'module.yaml');
|
|
80
|
+
const moduleYmlPath = path.join(searchPath, 'module.yml');
|
|
81
|
+
|
|
82
|
+
let configPath = '';
|
|
83
|
+
if (await fs.pathExists(moduleYamlPath)) configPath = moduleYamlPath;
|
|
84
|
+
else if (await fs.pathExists(moduleYmlPath)) configPath = moduleYmlPath;
|
|
85
|
+
else {
|
|
86
|
+
throw new Error(`No module.yaml found in ${cleanUrl}${subPath ? '//' + subPath : ''}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const configContent = await fs.readFile(configPath, 'utf8');
|
|
90
|
+
const config = YAML.parse(configContent);
|
|
91
|
+
|
|
92
|
+
if (!config.name) {
|
|
93
|
+
throw new Error(`Module at ${url} is missing 'name' in module.yaml`);
|
|
94
|
+
}
|
|
95
|
+
moduleName = config.name;
|
|
96
|
+
dependencies = config.dependencies || [];
|
|
97
|
+
|
|
98
|
+
// Normalize dependencies to array if object (though spec says list of strings, defensiveness is good)
|
|
99
|
+
if (dependencies && !Array.isArray(dependencies)) {
|
|
100
|
+
dependencies = Object.keys(dependencies);
|
|
101
|
+
}
|
|
102
|
+
} finally {
|
|
103
|
+
// Cleanup staging always
|
|
104
|
+
await fs.remove(stagingDir);
|
|
40
105
|
}
|
|
41
106
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
dependencies = Object.keys(dependencies);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
} catch (e: any) { // Catching as 'any' for error message access
|
|
97
|
-
throw e;
|
|
98
|
-
} finally {
|
|
99
|
-
// Cleanup staging always
|
|
100
|
-
await fs.remove(stagingDir);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// Stage 2: Conflict Detection
|
|
104
|
-
const targetDir = path.join(projectRoot!, 'modules', moduleName);
|
|
105
|
-
const relativeTargetDir = path.relative(projectRoot!, targetDir);
|
|
106
|
-
|
|
107
|
-
if (await fs.pathExists(targetDir)) {
|
|
108
|
-
// Check origin
|
|
109
|
-
const existingRemote = await getRemoteUrl(targetDir);
|
|
110
|
-
// We compare cleanUrl (the repo root).
|
|
111
|
-
// normalize both
|
|
112
|
-
const normExisting = existingRemote.replace(/\.git$/, '');
|
|
113
|
-
const normNew = cleanUrl.replace(/\.git$/, '');
|
|
114
|
-
|
|
115
|
-
if (normExisting !== normNew && existingRemote !== '') {
|
|
116
|
-
throw new Error(`Dependency Conflict! Module '${moduleName}' exists but remote '${existingRemote}' does not match '${cleanUrl}'.`);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
this.info(`Module ${moduleName} already installed.`);
|
|
120
|
-
// Proceed to recurse, but skip add
|
|
121
|
-
} else {
|
|
122
|
-
// Stage 3: Submodule Add
|
|
123
|
-
this.info(`Installing ${moduleName} to ${relativeTargetDir}...`);
|
|
124
|
-
// We install the ROOT repo.
|
|
125
|
-
// IMPORTANT: If subPath exists, "Identity is Internal" means we name the folder `moduleName`.
|
|
126
|
-
// But the CONTENT will be the whole repo.
|
|
127
|
-
// If the user meant to only have the subdir, we can't do that with submodule add easily without manual git plumbing.
|
|
128
|
-
// Given instructions, I will proceed with submodule add of root repo to target dir.
|
|
129
|
-
await runCommand(`git submodule add ${cleanUrl} ${relativeTargetDir}`, projectRoot!);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// Update nexical.yaml
|
|
133
|
-
await this.addToConfig(moduleName);
|
|
134
|
-
|
|
135
|
-
// Stage 4: Recurse
|
|
136
|
-
if (dependencies.length > 0) {
|
|
137
|
-
this.info(`Resolving ${dependencies.length} dependencies for ${moduleName}...`);
|
|
138
|
-
for (const depUrl of dependencies) {
|
|
139
|
-
await this.installModule(depUrl);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
107
|
+
// Stage 2: Conflict Detection
|
|
108
|
+
const targetDir = path.join(projectRoot!, 'modules', moduleName);
|
|
109
|
+
const relativeTargetDir = path.relative(projectRoot!, targetDir);
|
|
110
|
+
|
|
111
|
+
if (await fs.pathExists(targetDir)) {
|
|
112
|
+
// Check origin
|
|
113
|
+
const existingRemote = await getRemoteUrl(targetDir);
|
|
114
|
+
// We compare cleanUrl (the repo root).
|
|
115
|
+
// normalize both
|
|
116
|
+
const normExisting = existingRemote.replace(/\.git$/, '');
|
|
117
|
+
const normNew = cleanUrl.replace(/\.git$/, '');
|
|
118
|
+
|
|
119
|
+
if (normExisting !== normNew && existingRemote !== '') {
|
|
120
|
+
throw new Error(
|
|
121
|
+
`Dependency Conflict! Module '${moduleName}' exists but remote '${existingRemote}' does not match '${cleanUrl}'.`,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
this.info(`Module ${moduleName} already installed.`);
|
|
126
|
+
// Proceed to recurse, but skip add
|
|
127
|
+
} else {
|
|
128
|
+
// Stage 3: Submodule Add
|
|
129
|
+
this.info(`Installing ${moduleName} to ${relativeTargetDir}...`);
|
|
130
|
+
// We install the ROOT repo.
|
|
131
|
+
// IMPORTANT: If subPath exists, "Identity is Internal" means we name the folder `moduleName`.
|
|
132
|
+
// But the CONTENT will be the whole repo.
|
|
133
|
+
// If the user meant to only have the subdir, we can't do that with submodule add easily without manual git plumbing.
|
|
134
|
+
// Given instructions, I will proceed with submodule add of root repo to target dir.
|
|
135
|
+
await runCommand(`git submodule add ${cleanUrl} ${relativeTargetDir}`, projectRoot!);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Update nexical.yaml
|
|
139
|
+
await this.addToConfig(moduleName);
|
|
140
|
+
|
|
141
|
+
// Stage 4: Recurse
|
|
142
|
+
if (dependencies.length > 0) {
|
|
143
|
+
this.info(`Resolving ${dependencies.length} dependencies for ${moduleName}...`);
|
|
144
|
+
for (const depUrl of dependencies) {
|
|
145
|
+
await this.installModule(depUrl);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private async addToConfig(moduleName: string) {
|
|
151
|
+
const projectRoot = this.projectRoot as string;
|
|
152
|
+
const configPath = path.join(projectRoot, 'nexical.yaml');
|
|
153
|
+
|
|
154
|
+
if (!(await fs.pathExists(configPath))) {
|
|
155
|
+
// Not strictly required to exist for all operations, but good to have if we are tracking modules.
|
|
156
|
+
logger.warn('nexical.yaml not found, skipping module list update.');
|
|
157
|
+
return;
|
|
142
158
|
}
|
|
143
159
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
config.modules.push(moduleName);
|
|
162
|
-
await fs.writeFile(configPath, YAML.stringify(config));
|
|
163
|
-
logger.debug(`Added ${moduleName} to nexical.yaml modules list.`);
|
|
164
|
-
}
|
|
165
|
-
} catch (e: any) {
|
|
166
|
-
logger.warn(`Failed to update nexical.yaml: ${e.message}`);
|
|
167
|
-
}
|
|
160
|
+
try {
|
|
161
|
+
const content = await fs.readFile(configPath, 'utf8');
|
|
162
|
+
const config = YAML.parse(content) || {};
|
|
163
|
+
|
|
164
|
+
if (!config.modules) config.modules = [];
|
|
165
|
+
|
|
166
|
+
if (!config.modules.includes(moduleName)) {
|
|
167
|
+
config.modules.push(moduleName);
|
|
168
|
+
await fs.writeFile(configPath, YAML.stringify(config));
|
|
169
|
+
logger.debug(`Added ${moduleName} to nexical.yaml modules list.`);
|
|
170
|
+
}
|
|
171
|
+
} catch (e: unknown) {
|
|
172
|
+
if (e instanceof Error) {
|
|
173
|
+
logger.warn(`Failed to update nexical.yaml: ${e.message}`);
|
|
174
|
+
} else {
|
|
175
|
+
logger.warn(`Failed to update nexical.yaml: ${String(e)}`);
|
|
176
|
+
}
|
|
168
177
|
}
|
|
178
|
+
}
|
|
169
179
|
}
|
|
@@ -4,66 +4,77 @@ import path from 'path';
|
|
|
4
4
|
import YAML from 'yaml';
|
|
5
5
|
|
|
6
6
|
export default class ModuleListCommand extends BaseCommand {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
static usage = 'module list';
|
|
8
|
+
static description = 'List installed modules.';
|
|
9
|
+
static requiresProject = true;
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
async run() {
|
|
12
|
+
const projectRoot = this.projectRoot as string;
|
|
13
|
+
const modulesDir = path.resolve(projectRoot, 'modules');
|
|
14
|
+
logger.debug(`Scanning for modules in: ${modulesDir}`);
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
try {
|
|
22
|
-
const modules = await fs.readdir(modulesDir);
|
|
23
|
-
const validModules: { name: string; version: string; description: string }[] = [];
|
|
24
|
-
|
|
25
|
-
for (const moduleName of modules) {
|
|
26
|
-
const modulePath = path.join(modulesDir, moduleName);
|
|
27
|
-
if ((await fs.stat(modulePath)).isDirectory()) {
|
|
28
|
-
let version = 'unknown';
|
|
29
|
-
let description = '';
|
|
16
|
+
if (!(await fs.pathExists(modulesDir))) {
|
|
17
|
+
this.info('No modules installed (modules directory missing).');
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
30
20
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
21
|
+
try {
|
|
22
|
+
const modules = await fs.readdir(modulesDir);
|
|
23
|
+
const validModules: { name: string; version: string; description: string }[] = [];
|
|
34
24
|
|
|
35
|
-
|
|
36
|
-
|
|
25
|
+
for (const moduleName of modules) {
|
|
26
|
+
const modulePath = path.join(modulesDir, moduleName);
|
|
27
|
+
if ((await fs.stat(modulePath)).isDirectory()) {
|
|
28
|
+
let version = 'unknown';
|
|
29
|
+
let description = '';
|
|
37
30
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
} catch (e) { /* ignore */ }
|
|
42
|
-
}
|
|
31
|
+
const pkgJsonPath = path.join(modulePath, 'package.json');
|
|
32
|
+
const moduleYamlPath = path.join(modulePath, 'module.yaml');
|
|
33
|
+
const moduleYmlPath = path.join(modulePath, 'module.yml');
|
|
43
34
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const configPath = await fs.pathExists(moduleYamlPath) ? moduleYamlPath : moduleYmlPath;
|
|
47
|
-
const content = await fs.readFile(configPath, 'utf8');
|
|
48
|
-
modConfig = YAML.parse(content) || {};
|
|
49
|
-
} catch (e) { /* ignore */ }
|
|
50
|
-
}
|
|
35
|
+
let pkg: Record<string, unknown> = {};
|
|
36
|
+
let modConfig: Record<string, unknown> = {};
|
|
51
37
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
38
|
+
if (await fs.pathExists(pkgJsonPath)) {
|
|
39
|
+
try {
|
|
40
|
+
pkg = await fs.readJson(pkgJsonPath);
|
|
41
|
+
} catch {
|
|
42
|
+
/* ignore */
|
|
58
43
|
}
|
|
44
|
+
}
|
|
59
45
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
46
|
+
if ((await fs.pathExists(moduleYamlPath)) || (await fs.pathExists(moduleYmlPath))) {
|
|
47
|
+
try {
|
|
48
|
+
const configPath = (await fs.pathExists(moduleYamlPath))
|
|
49
|
+
? moduleYamlPath
|
|
50
|
+
: moduleYmlPath;
|
|
51
|
+
const content = await fs.readFile(configPath, 'utf8');
|
|
52
|
+
modConfig = YAML.parse(content) || {};
|
|
53
|
+
} catch {
|
|
54
|
+
/* ignore */
|
|
64
55
|
}
|
|
65
|
-
|
|
66
|
-
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
version = (pkg.version as string) || (modConfig.version as string) || 'unknown';
|
|
59
|
+
description = (pkg.description as string) || (modConfig.description as string) || '';
|
|
60
|
+
// Optionally use display name from module.yaml if present, but strictly list is usually dir name.
|
|
61
|
+
// Let's stick to dir name for "name" column, but description from module.yaml is good.
|
|
62
|
+
validModules.push({ name: moduleName, version, description });
|
|
67
63
|
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (validModules.length === 0) {
|
|
67
|
+
this.info('No modules installed.');
|
|
68
|
+
} else {
|
|
69
|
+
// eslint-disable-next-line no-console
|
|
70
|
+
console.table(validModules);
|
|
71
|
+
}
|
|
72
|
+
} catch (error: unknown) {
|
|
73
|
+
if (error instanceof Error) {
|
|
74
|
+
this.error(`Failed to list modules: ${error.message}`);
|
|
75
|
+
} else {
|
|
76
|
+
this.error(`Failed to list modules: ${String(error)}`);
|
|
77
|
+
}
|
|
68
78
|
}
|
|
79
|
+
}
|
|
69
80
|
}
|
|
@@ -4,71 +4,76 @@ import path from 'path';
|
|
|
4
4
|
import YAML from 'yaml';
|
|
5
5
|
|
|
6
6
|
export default class ModuleRemoveCommand extends BaseCommand {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
static usage = 'module remove <name>';
|
|
8
|
+
static description = 'Remove an installed module.';
|
|
9
|
+
static requiresProject = true;
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
]
|
|
15
|
-
};
|
|
11
|
+
static args: CommandDefinition = {
|
|
12
|
+
args: [{ name: 'name', required: true, description: 'Name of the module to remove' }],
|
|
13
|
+
};
|
|
16
14
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
async run(options: { name: string }) {
|
|
16
|
+
const projectRoot = this.projectRoot as string;
|
|
17
|
+
const { name } = options;
|
|
20
18
|
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
const relativePath = `modules/${name}`;
|
|
20
|
+
const fullPath = path.resolve(projectRoot, relativePath);
|
|
23
21
|
|
|
24
|
-
|
|
22
|
+
logger.debug('Removing module at:', fullPath);
|
|
25
23
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
this.info(`Removing module ${name}...`);
|
|
24
|
+
if (!(await fs.pathExists(fullPath))) {
|
|
25
|
+
this.error(`Module ${name} not found at ${relativePath}.`);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
32
28
|
|
|
33
|
-
|
|
34
|
-
await runCommand(`git submodule deinit -f ${relativePath}`, projectRoot);
|
|
35
|
-
await runCommand(`git rm -f ${relativePath}`, projectRoot);
|
|
29
|
+
this.info(`Removing module ${name}...`);
|
|
36
30
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
await fs.remove(gitModulesDir);
|
|
41
|
-
}
|
|
31
|
+
try {
|
|
32
|
+
await runCommand(`git submodule deinit -f ${relativePath}`, projectRoot);
|
|
33
|
+
await runCommand(`git rm -f ${relativePath}`, projectRoot);
|
|
42
34
|
|
|
43
|
-
|
|
44
|
-
|
|
35
|
+
// Clean up .git/modules
|
|
36
|
+
const gitModulesDir = path.resolve(projectRoot, '.git', 'modules', 'modules', name);
|
|
37
|
+
if (await fs.pathExists(gitModulesDir)) {
|
|
38
|
+
await fs.remove(gitModulesDir);
|
|
39
|
+
}
|
|
45
40
|
|
|
41
|
+
this.info('Syncing workspace dependencies...');
|
|
42
|
+
await runCommand('npm install', projectRoot);
|
|
46
43
|
|
|
47
|
-
|
|
44
|
+
await this.removeFromConfig(name);
|
|
48
45
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
46
|
+
this.success(`Module ${name} removed successfully.`);
|
|
47
|
+
} catch (e: unknown) {
|
|
48
|
+
if (e instanceof Error) {
|
|
49
|
+
this.error(`Failed to remove module: ${e.message}`);
|
|
50
|
+
} else {
|
|
51
|
+
this.error(`Failed to remove module: ${String(e)}`);
|
|
52
|
+
}
|
|
53
53
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private async removeFromConfig(moduleName: string) {
|
|
57
|
+
const projectRoot = this.projectRoot as string;
|
|
58
|
+
const configPath = path.join(projectRoot, 'nexical.yaml');
|
|
59
|
+
|
|
60
|
+
if (!(await fs.pathExists(configPath))) return;
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const content = await fs.readFile(configPath, 'utf8');
|
|
64
|
+
const config = YAML.parse(content) || {};
|
|
65
|
+
|
|
66
|
+
if (config.modules && config.modules.includes(moduleName)) {
|
|
67
|
+
config.modules = config.modules.filter((m: string) => m !== moduleName);
|
|
68
|
+
await fs.writeFile(configPath, YAML.stringify(config));
|
|
69
|
+
logger.debug(`Removed ${moduleName} from nexical.yaml modules list.`);
|
|
70
|
+
}
|
|
71
|
+
} catch (e: unknown) {
|
|
72
|
+
if (e instanceof Error) {
|
|
73
|
+
logger.warn(`Failed to update nexical.yaml: ${e.message}`);
|
|
74
|
+
} else {
|
|
75
|
+
logger.warn(`Failed to update nexical.yaml: ${String(e)}`);
|
|
76
|
+
}
|
|
73
77
|
}
|
|
78
|
+
}
|
|
74
79
|
}
|