@nexical/cli 0.10.0 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/.github/workflows/deploy.yml +1 -1
  2. package/.husky/pre-commit +1 -0
  3. package/.prettierignore +8 -0
  4. package/.prettierrc +7 -0
  5. package/GEMINI.md +199 -0
  6. package/README.md +85 -56
  7. package/dist/chunk-AC4B3HPJ.js +93 -0
  8. package/dist/chunk-AC4B3HPJ.js.map +1 -0
  9. package/dist/{chunk-JYASTIIW.js → chunk-PJIOCW2A.js} +1 -1
  10. package/dist/chunk-PJIOCW2A.js.map +1 -0
  11. package/dist/{chunk-WKERTCM6.js → chunk-Q7YLW5HJ.js} +5 -2
  12. package/dist/chunk-Q7YLW5HJ.js.map +1 -0
  13. package/dist/index.js +41 -12
  14. package/dist/index.js.map +1 -1
  15. package/dist/src/commands/init.d.ts +4 -1
  16. package/dist/src/commands/init.js +15 -10
  17. package/dist/src/commands/init.js.map +1 -1
  18. package/dist/src/commands/module/add.d.ts +3 -1
  19. package/dist/src/commands/module/add.js +27 -16
  20. package/dist/src/commands/module/add.js.map +1 -1
  21. package/dist/src/commands/module/list.js +9 -5
  22. package/dist/src/commands/module/list.js.map +1 -1
  23. package/dist/src/commands/module/remove.d.ts +3 -1
  24. package/dist/src/commands/module/remove.js +13 -7
  25. package/dist/src/commands/module/remove.js.map +1 -1
  26. package/dist/src/commands/module/update.d.ts +3 -1
  27. package/dist/src/commands/module/update.js +7 -5
  28. package/dist/src/commands/module/update.js.map +1 -1
  29. package/dist/src/commands/run.d.ts +4 -1
  30. package/dist/src/commands/run.js +10 -2
  31. package/dist/src/commands/run.js.map +1 -1
  32. package/dist/src/commands/setup.d.ts +8 -0
  33. package/dist/src/commands/setup.js +75 -0
  34. package/dist/src/commands/setup.js.map +1 -0
  35. package/dist/src/utils/discovery.js +1 -1
  36. package/dist/src/utils/git.js +1 -1
  37. package/dist/src/utils/url-resolver.js +1 -1
  38. package/eslint.config.mjs +67 -0
  39. package/index.ts +34 -20
  40. package/package.json +57 -33
  41. package/src/commands/init.ts +79 -75
  42. package/src/commands/module/add.ts +158 -148
  43. package/src/commands/module/list.ts +61 -50
  44. package/src/commands/module/remove.ts +59 -54
  45. package/src/commands/module/update.ts +44 -42
  46. package/src/commands/run.ts +89 -81
  47. package/src/commands/setup.ts +92 -0
  48. package/src/utils/discovery.ts +98 -113
  49. package/src/utils/git.ts +35 -28
  50. package/src/utils/url-resolver.ts +50 -45
  51. package/test/e2e/lifecycle.e2e.test.ts +139 -130
  52. package/test/integration/commands/init.integration.test.ts +64 -61
  53. package/test/integration/commands/module.integration.test.ts +122 -122
  54. package/test/integration/commands/run.integration.test.ts +70 -63
  55. package/test/integration/utils/command-loading.integration.test.ts +40 -53
  56. package/test/unit/commands/init.test.ts +163 -128
  57. package/test/unit/commands/module/add.test.ts +312 -245
  58. package/test/unit/commands/module/list.test.ts +108 -91
  59. package/test/unit/commands/module/remove.test.ts +74 -67
  60. package/test/unit/commands/module/update.test.ts +74 -70
  61. package/test/unit/commands/run.test.ts +253 -201
  62. package/test/unit/commands/setup.test.ts +187 -0
  63. package/test/unit/utils/command-discovery.test.ts +138 -125
  64. package/test/unit/utils/git.test.ts +135 -117
  65. package/test/unit/utils/integration-helpers.test.ts +59 -49
  66. package/test/unit/utils/url-resolver.test.ts +46 -34
  67. package/test/utils/integration-helpers.ts +36 -29
  68. package/tsconfig.json +15 -25
  69. package/tsup.config.ts +14 -14
  70. package/vitest.config.ts +10 -10
  71. package/vitest.e2e.config.ts +6 -6
  72. package/vitest.integration.config.ts +17 -17
  73. package/dist/chunk-JYASTIIW.js.map +0 -1
  74. package/dist/chunk-OKXOCNXP.js +0 -105
  75. package/dist/chunk-OKXOCNXP.js.map +0 -1
  76. package/dist/chunk-WKERTCM6.js.map +0 -1
@@ -5,81 +5,85 @@ import fs from 'fs-extra';
5
5
  import path from 'path';
6
6
 
7
7
  export default class InitCommand extends BaseCommand {
8
- static usage = 'init';
9
- static description = 'Initialize a new Nexical project.';
10
- static requiresProject = false;
11
-
12
- static args: CommandDefinition = {
13
- args: [
14
- { name: 'directory', required: true, description: 'Directory to initialize the project in' }
15
- ],
16
- options: [
17
- {
18
- name: '--repo <url>',
19
- description: 'Starter repository URL (supports gh@owner/repo syntax)',
20
- default: 'gh@nexical/app-core'
21
- }
22
- ]
23
- };
24
-
25
- async run(options: any) {
26
- const directory = options.directory;
27
- const targetPath = path.resolve(process.cwd(), directory);
28
- let repoUrl = resolveGitUrl(options.repo);
29
-
30
- logger.debug('Init options:', { directory, targetPath, repoUrl });
31
-
32
- this.info(`Initializing project in: ${targetPath}`);
33
- this.info(`Using starter repository: ${repoUrl}`);
34
-
35
- if (await fs.pathExists(targetPath)) {
36
- if ((await fs.readdir(targetPath)).length > 0) {
37
- this.error(`Directory ${directory} is not empty.`);
38
- process.exit(1);
39
- }
40
- } else {
41
- await fs.mkdir(targetPath, { recursive: true });
42
- }
43
-
44
- try {
45
- this.info('Cloning core repository...');
46
- await git.clone(repoUrl, targetPath, { recursive: true });
47
-
48
- this.info('Updating submodules...');
49
- await git.updateSubmodules(targetPath);
50
-
51
- this.info('Installing dependencies...');
52
- await runCommand('npm install', targetPath);
53
-
54
- this.info('Setting up upstream remote...');
55
- await git.renameRemote('origin', 'upstream', targetPath);
56
-
57
- // Ensure module directory
58
- await fs.ensureDir(path.join(targetPath, 'modules'));
59
-
60
- // Check for nexical.yaml, if not present create a default one
61
- const configPath = path.join(targetPath, 'nexical.yaml');
62
- if (!await fs.pathExists(configPath)) {
63
- this.info('Creating default nexical.yaml...');
64
- await fs.writeFile(configPath, 'name: ' + path.basename(targetPath) + '\nmodules: []\n');
65
- }
66
-
67
- // Create VERSION file
68
- const versionPath = path.join(targetPath, 'VERSION');
69
- // Check if version file exists, if not create it
70
- if (!await fs.pathExists(versionPath)) {
71
- this.info('Creating VERSION file with 0.1.0...');
72
- await fs.writeFile(versionPath, '0.1.0');
73
- }
74
-
75
- await git.addAll(targetPath);
76
- await git.commit('Initial site commit', targetPath);
77
-
78
- this.success(`Project initialized successfully in ${directory}!`);
8
+ static usage = 'init';
9
+ static description = 'Initialize a new Nexical project.';
10
+ static requiresProject = false;
11
+
12
+ static args: CommandDefinition = {
13
+ args: [
14
+ { name: 'directory', required: true, description: 'Directory to initialize the project in' },
15
+ ],
16
+ options: [
17
+ {
18
+ name: '--repo <url>',
19
+ description: 'Starter repository URL (supports gh@owner/repo syntax)',
20
+ default: 'gh@nexical/app-starter',
21
+ },
22
+ ],
23
+ };
24
+
25
+ async run(options: { directory: string; repo: string }) {
26
+ const directory = options.directory;
27
+ const targetPath = path.resolve(process.cwd(), directory);
28
+ const repoUrl = resolveGitUrl(options.repo);
29
+
30
+ logger.debug('Init options:', { directory, targetPath, repoUrl });
31
+
32
+ this.info(`Initializing project in: ${targetPath}`);
33
+ this.info(`Using starter repository: ${repoUrl}`);
34
+
35
+ if (await fs.pathExists(targetPath)) {
36
+ if ((await fs.readdir(targetPath)).length > 0) {
37
+ this.error(`Directory ${directory} is not empty.`);
38
+ process.exit(1);
39
+ }
40
+ } else {
41
+ await fs.mkdir(targetPath, { recursive: true });
42
+ }
79
43
 
80
- } catch (error: any) {
81
- this.error(`Failed to initialize project: ${error.message}`);
82
- process.exit(1);
83
- }
44
+ try {
45
+ this.info('Cloning starter repository...');
46
+ await git.clone(repoUrl, targetPath, { recursive: true });
47
+
48
+ this.info('Updating submodules...');
49
+ await git.updateSubmodules(targetPath);
50
+
51
+ this.info('Installing dependencies...');
52
+ await runCommand('npm install', targetPath);
53
+
54
+ this.info('Setting up upstream remote...');
55
+ await git.renameRemote('origin', 'upstream', targetPath);
56
+
57
+ // Run setup script
58
+ this.info('Running project setup...');
59
+ await runCommand('npm run setup', targetPath);
60
+
61
+ // Check for nexical.yaml, if not present create a default one
62
+ const configPath = path.join(targetPath, 'nexical.yaml');
63
+ if (!(await fs.pathExists(configPath))) {
64
+ this.info('Creating default nexical.yaml...');
65
+ await fs.writeFile(configPath, 'name: ' + path.basename(targetPath) + '\nmodules: []\n');
66
+ }
67
+
68
+ // Create VERSION file
69
+ const versionPath = path.join(targetPath, 'VERSION');
70
+ // Check if version file exists, if not create it
71
+ if (!(await fs.pathExists(versionPath))) {
72
+ this.info('Creating VERSION file with 0.1.0...');
73
+ await fs.writeFile(versionPath, '0.1.0');
74
+ }
75
+
76
+ await git.addAll(targetPath);
77
+ await git.commit('Initial site commit', targetPath);
78
+
79
+ this.success(`Project initialized successfully in ${directory}!`);
80
+ } catch (error: unknown) {
81
+ if (error instanceof Error) {
82
+ this.error(`Failed to initialize project: ${error.message}`);
83
+ } else {
84
+ this.error(`Failed to initialize project: ${String(error)}`);
85
+ }
86
+ process.exit(1);
84
87
  }
88
+ }
85
89
  }
@@ -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
- static usage = 'module add <url>';
10
- static description = 'Add a module and its dependencies as git submodules.';
11
- static requiresProject = true;
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
- static args: CommandDefinition = {
14
- args: [
15
- { name: 'url', required: true, description: 'Git repository URL or gh@org/repo' }
16
- ]
17
- };
13
+ static args: CommandDefinition = {
14
+ args: [{ name: 'url', required: true, description: 'Git repository URL or gh@org/repo' }],
15
+ };
18
16
 
19
- private visited = new Set<string>();
17
+ private visited = new Set<string>();
20
18
 
21
- async run(options: any) {
22
- const projectRoot = this.projectRoot as string;
23
- let { url } = options;
19
+ async run(options: { url: string }) {
20
+ const projectRoot = this.projectRoot as string;
21
+ const { url } = options;
24
22
 
25
- if (!url) {
26
- this.error('Please specify a repository URL.');
27
- return;
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
- try {
31
- await this.installModule(url);
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
- this.info('Syncing workspace dependencies...');
34
- await runCommand('npm install', projectRoot);
44
+ private async installModule(url: string) {
45
+ const projectRoot = this.projectRoot as string;
35
46
 
36
- this.success('All modules installed successfully.');
37
- } catch (e: any) {
38
- this.error(`Failed to add module: ${e.message}`);
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
- private async installModule(url: string) {
43
- const projectRoot = this.projectRoot as string;
44
-
45
- // Resolve URL using utility
46
- url = resolveGitUrl(url);
47
-
48
- const [repoUrl, subPath] = url.split('.git//');
49
- const cleanUrl = subPath ? repoUrl + '.git' : url;
50
-
51
- if (this.visited.has(cleanUrl)) {
52
- logger.debug(`Already visited ${cleanUrl}, skipping.`);
53
- return;
54
- }
55
- this.visited.add(cleanUrl);
56
-
57
- this.info(`Inspecting ${cleanUrl}...`);
58
-
59
- // Stage 1: Inspect (Temp Clone)
60
- const stagingDir = path.resolve(projectRoot!, '.nexical', 'cache', `staging-${Date.now()}-${Math.random().toString(36).substring(7)}`);
61
- let moduleName = '';
62
- let dependencies: string[] = [];
63
-
64
- try {
65
- await fs.ensureDir(stagingDir);
66
-
67
- // Shallow clone to inspect
68
- await clone(cleanUrl, stagingDir, { depth: 1 });
69
-
70
- // Read module.yaml
71
- const searchPath = subPath ? path.join(stagingDir, subPath) : stagingDir;
72
- const moduleYamlPath = path.join(searchPath, 'module.yaml');
73
- const moduleYmlPath = path.join(searchPath, 'module.yml');
74
-
75
- let configPath = '';
76
- if (await fs.pathExists(moduleYamlPath)) configPath = moduleYamlPath;
77
- else if (await fs.pathExists(moduleYmlPath)) configPath = moduleYmlPath;
78
- else {
79
- throw new Error(`No module.yaml found in ${cleanUrl}${subPath ? '//' + subPath : ''}`);
80
- }
81
-
82
- const configContent = await fs.readFile(configPath, 'utf8');
83
- const config = YAML.parse(configContent);
84
-
85
- if (!config.name) {
86
- throw new Error(`Module at ${url} is missing 'name' in module.yaml`);
87
- }
88
- moduleName = config.name;
89
- dependencies = config.dependencies || [];
90
-
91
- // Normalize dependencies to array if object (though spec says list of strings, defensiveness is good)
92
- if (dependencies && !Array.isArray(dependencies)) {
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
- private async addToConfig(moduleName: string) {
145
- const projectRoot = this.projectRoot as string;
146
- const configPath = path.join(projectRoot, 'nexical.yaml');
147
-
148
- if (!await fs.pathExists(configPath)) {
149
- // Not strictly required to exist for all operations, but good to have if we are tracking modules.
150
- logger.warn('nexical.yaml not found, skipping module list update.');
151
- return;
152
- }
153
-
154
- try {
155
- const content = await fs.readFile(configPath, 'utf8');
156
- let config = YAML.parse(content) || {};
157
-
158
- if (!config.modules) config.modules = [];
159
-
160
- if (!config.modules.includes(moduleName)) {
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
- static usage = 'module list';
8
- static description = 'List installed modules.';
9
- static requiresProject = true;
7
+ static usage = 'module list';
8
+ static description = 'List installed modules.';
9
+ static requiresProject = true;
10
10
 
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}`);
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
- if (!(await fs.pathExists(modulesDir))) {
17
- this.info('No modules installed (modules directory missing).');
18
- return;
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
- const pkgJsonPath = path.join(modulePath, 'package.json');
32
- const moduleYamlPath = path.join(modulePath, 'module.yaml');
33
- const moduleYmlPath = path.join(modulePath, 'module.yml');
21
+ try {
22
+ const modules = await fs.readdir(modulesDir);
23
+ const validModules: { name: string; version: string; description: string }[] = [];
34
24
 
35
- let pkg: any = {};
36
- let modConfig: any = {};
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
- if (await fs.pathExists(pkgJsonPath)) {
39
- try {
40
- pkg = await fs.readJson(pkgJsonPath);
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
- if (await fs.pathExists(moduleYamlPath) || await fs.pathExists(moduleYmlPath)) {
45
- try {
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
- version = pkg.version || 'unknown';
53
- description = modConfig.description || pkg.description || '';
54
- // Optionally use display name from module.yaml if present, but strictly list is usually dir name.
55
- // Let's stick to dir name for "name" column, but description from module.yaml is good.
56
- validModules.push({ name: moduleName, version, description });
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
- if (validModules.length === 0) {
61
- this.info('No modules installed.');
62
- } else {
63
- console.table(validModules);
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
- } catch (error: any) {
66
- this.error(`Failed to list modules: ${error.message}`);
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
  }