@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
|
@@ -3,48 +3,50 @@ import fs from 'fs-extra';
|
|
|
3
3
|
import path from 'path';
|
|
4
4
|
|
|
5
5
|
export default class ModuleUpdateCommand extends BaseCommand {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
this.error(`Module ${name} not found.`);
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Update specific module
|
|
34
|
-
// We enter the directory and pull? Or generic submodule update?
|
|
35
|
-
// Generic submodule update --remote src/modules/name
|
|
36
|
-
await runCommand(`git submodule update --remote --merge ${relativePath}`, projectRoot);
|
|
37
|
-
} else {
|
|
38
|
-
// Update all
|
|
39
|
-
await runCommand('git submodule update --remote --merge', projectRoot);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
this.info('Syncing workspace dependencies...');
|
|
43
|
-
await runCommand('npm install', projectRoot);
|
|
44
|
-
|
|
45
|
-
this.success('Modules updated successfully.');
|
|
46
|
-
} catch (e: any) {
|
|
47
|
-
this.error(`Failed to update modules: ${e.message}`);
|
|
6
|
+
static usage = 'module update [name]';
|
|
7
|
+
static description = 'Update a specific module or all modules.';
|
|
8
|
+
static requiresProject = true;
|
|
9
|
+
|
|
10
|
+
static args: CommandDefinition = {
|
|
11
|
+
args: [{ name: 'name', required: false, description: 'Name of the module to update' }],
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
async run(options: { name?: string }) {
|
|
15
|
+
const projectRoot = this.projectRoot as string;
|
|
16
|
+
const { name } = options;
|
|
17
|
+
|
|
18
|
+
this.info(name ? `Updating module ${name}...` : 'Updating all modules...');
|
|
19
|
+
logger.debug('Update context:', { name, projectRoot: projectRoot });
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
if (name) {
|
|
23
|
+
const relativePath = `modules/${name}`;
|
|
24
|
+
const fullPath = path.resolve(projectRoot, relativePath);
|
|
25
|
+
|
|
26
|
+
if (!(await fs.pathExists(fullPath))) {
|
|
27
|
+
this.error(`Module ${name} not found.`);
|
|
28
|
+
return;
|
|
48
29
|
}
|
|
30
|
+
|
|
31
|
+
// Update specific module
|
|
32
|
+
// We enter the directory and pull? Or generic submodule update?
|
|
33
|
+
// Generic submodule update --remote src/modules/name
|
|
34
|
+
await runCommand(`git submodule update --remote --merge ${relativePath}`, projectRoot);
|
|
35
|
+
} else {
|
|
36
|
+
// Update all
|
|
37
|
+
await runCommand('git submodule update --remote --merge', projectRoot);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
this.info('Syncing workspace dependencies...');
|
|
41
|
+
await runCommand('npm install', projectRoot);
|
|
42
|
+
|
|
43
|
+
this.success('Modules updated successfully.');
|
|
44
|
+
} catch (e: unknown) {
|
|
45
|
+
if (e instanceof Error) {
|
|
46
|
+
this.error(`Failed to update modules: ${e.message}`);
|
|
47
|
+
} else {
|
|
48
|
+
this.error(`Failed to update modules: ${String(e)}`);
|
|
49
|
+
}
|
|
49
50
|
}
|
|
51
|
+
}
|
|
50
52
|
}
|
package/src/commands/run.ts
CHANGED
|
@@ -5,94 +5,102 @@ import { spawn } from 'child_process';
|
|
|
5
5
|
import process from 'node:process';
|
|
6
6
|
|
|
7
7
|
export default class RunCommand extends BaseCommand {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
8
|
+
static usage = 'run <script> [args...]';
|
|
9
|
+
static description = 'Run a script inside the Nexical environment.';
|
|
10
|
+
static requiresProject = true;
|
|
11
|
+
|
|
12
|
+
static args: CommandDefinition = {
|
|
13
|
+
args: [
|
|
14
|
+
{
|
|
15
|
+
name: 'script',
|
|
16
|
+
required: true,
|
|
17
|
+
description: 'The script to run (script-name OR module:script-name)',
|
|
18
|
+
},
|
|
19
|
+
{ name: 'args...', required: false, description: 'Arguments for the script' },
|
|
20
|
+
],
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
async run(options: { script: string; args?: string[] }) {
|
|
24
|
+
const projectRoot = this.projectRoot as string;
|
|
25
|
+
const script = options.script;
|
|
26
|
+
const scriptArgs = options.args || [];
|
|
27
|
+
|
|
28
|
+
if (!script) {
|
|
29
|
+
this.error('Please specify a script to run.');
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
18
32
|
|
|
19
|
-
|
|
20
|
-
const projectRoot = this.projectRoot as string;
|
|
21
|
-
const script = options.script;
|
|
22
|
-
const scriptArgs = options.args || [];
|
|
33
|
+
logger.debug('Run command context:', { script, args: scriptArgs, projectRoot });
|
|
23
34
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
35
|
+
let execPath = projectRoot;
|
|
36
|
+
let scriptName = script;
|
|
28
37
|
|
|
29
|
-
|
|
38
|
+
// Handle module:script syntax
|
|
39
|
+
if (script.includes(':')) {
|
|
40
|
+
const [moduleName, name] = script.split(':');
|
|
41
|
+
execPath = path.resolve(projectRoot, 'modules', moduleName);
|
|
42
|
+
scriptName = name;
|
|
30
43
|
|
|
31
|
-
|
|
32
|
-
|
|
44
|
+
logger.debug(`Resolving module script: ${moduleName}:${scriptName} at ${execPath}`);
|
|
45
|
+
} else {
|
|
46
|
+
logger.debug(`Resolving core script: ${scriptName} at ${execPath}`);
|
|
47
|
+
}
|
|
33
48
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
49
|
+
// Validate script existence
|
|
50
|
+
const pkgJsonPath = path.join(execPath, 'package.json');
|
|
51
|
+
if (!(await fs.pathExists(pkgJsonPath))) {
|
|
52
|
+
this.error(`Failed to find package.json at ${execPath}`);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
39
55
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
56
|
+
try {
|
|
57
|
+
const pkg = await fs.readJson(pkgJsonPath);
|
|
58
|
+
if (!pkg.scripts || !pkg.scripts[scriptName]) {
|
|
59
|
+
const type = script.includes(':') ? `module ${script.split(':')[0]}` : 'Nexical core';
|
|
60
|
+
this.error(`Script "${scriptName}" does not exist in ${type}`);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
} catch (e: unknown) {
|
|
64
|
+
if (e instanceof Error) {
|
|
65
|
+
this.error(`Failed to read package.json at ${execPath}: ${e.message}`);
|
|
66
|
+
} else {
|
|
67
|
+
this.error(`Failed to read package.json at ${execPath}: ${String(e)}`);
|
|
68
|
+
}
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
44
71
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
72
|
+
const finalArgs = ['run', scriptName, '--', ...scriptArgs];
|
|
73
|
+
logger.debug(`Executing: npm ${finalArgs.join(' ')} in ${execPath}`);
|
|
74
|
+
|
|
75
|
+
const child = spawn('npm', finalArgs, {
|
|
76
|
+
cwd: execPath,
|
|
77
|
+
stdio: 'inherit',
|
|
78
|
+
env: {
|
|
79
|
+
...process.env,
|
|
80
|
+
FORCE_COLOR: '1',
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Handle process termination to kill child
|
|
85
|
+
const cleanup = () => {
|
|
86
|
+
child.kill();
|
|
87
|
+
process.exit();
|
|
88
|
+
};
|
|
51
89
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
if (!pkg.scripts || !pkg.scripts[scriptName]) {
|
|
55
|
-
const type = script.includes(':') ? `module ${script.split(':')[0]}` : 'Nexical core';
|
|
56
|
-
this.error(`Script "${scriptName}" does not exist in ${type}`);
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
} catch (e: any) {
|
|
60
|
-
this.error(`Failed to read package.json at ${execPath}: ${e.message}`);
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
90
|
+
process.on('SIGINT', cleanup);
|
|
91
|
+
process.on('SIGTERM', cleanup);
|
|
63
92
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const cleanup = () => {
|
|
78
|
-
child.kill();
|
|
79
|
-
process.exit();
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
process.on('SIGINT', cleanup);
|
|
83
|
-
process.on('SIGTERM', cleanup);
|
|
84
|
-
|
|
85
|
-
await new Promise<void>((resolve) => {
|
|
86
|
-
child.on('close', (code) => {
|
|
87
|
-
// Remove listeners to prevent memory leaks if this command is run multiple times in-process (e.g. tests)
|
|
88
|
-
process.off('SIGINT', cleanup);
|
|
89
|
-
process.off('SIGTERM', cleanup);
|
|
90
|
-
|
|
91
|
-
if (code !== 0) {
|
|
92
|
-
process.exit(code || 1);
|
|
93
|
-
}
|
|
94
|
-
resolve();
|
|
95
|
-
});
|
|
96
|
-
});
|
|
97
|
-
}
|
|
93
|
+
await new Promise<void>((resolve) => {
|
|
94
|
+
child.on('close', (code) => {
|
|
95
|
+
// Remove listeners to prevent memory leaks if this command is run multiple times in-process (e.g. tests)
|
|
96
|
+
process.off('SIGINT', cleanup);
|
|
97
|
+
process.off('SIGTERM', cleanup);
|
|
98
|
+
|
|
99
|
+
if (code !== 0) {
|
|
100
|
+
process.exit(code || 1);
|
|
101
|
+
}
|
|
102
|
+
resolve();
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
}
|
|
98
106
|
}
|
package/src/commands/setup.ts
CHANGED
|
@@ -3,72 +3,82 @@ import fs from 'fs-extra';
|
|
|
3
3
|
import path from 'path';
|
|
4
4
|
|
|
5
5
|
export default class SetupCommand extends BaseCommand {
|
|
6
|
-
|
|
6
|
+
static description = 'Setup the application environment by symlinking core assets.';
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
13
|
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
// BaseCommand doesn't expose projectRoot directly in current implementation seen in memory, checking source if possible?
|
|
15
|
+
// InitCommand used process.cwd().
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
// Let's assume process.cwd() is project root if run via `npm run setup` from root.
|
|
18
|
+
const rootDir = process.cwd();
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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']; // 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
|
+
}
|
|
25
35
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
}
|
|
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;
|
|
70
45
|
}
|
|
71
46
|
|
|
72
|
-
|
|
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
|
+
fs.lstatSync(dest);
|
|
57
|
+
fs.removeSync(dest);
|
|
58
|
+
} catch (e: unknown) {
|
|
59
|
+
if (
|
|
60
|
+
e &&
|
|
61
|
+
typeof e === 'object' &&
|
|
62
|
+
'code' in e &&
|
|
63
|
+
(e as { code: string }).code !== 'ENOENT'
|
|
64
|
+
)
|
|
65
|
+
throw e;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const relSource = path.relative(destDir, source);
|
|
69
|
+
await fs.symlink(relSource, dest);
|
|
70
|
+
|
|
71
|
+
logger.debug(`Symlinked ${asset} to ${app}`);
|
|
72
|
+
} catch (e: unknown) {
|
|
73
|
+
if (e instanceof Error) {
|
|
74
|
+
this.error(`Failed to symlink ${asset} to ${app}: ${e.message}`);
|
|
75
|
+
} else {
|
|
76
|
+
this.error(`Failed to symlink ${asset} to ${app}: ${String(e)}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
73
80
|
}
|
|
81
|
+
|
|
82
|
+
this.success('Application setup complete.');
|
|
83
|
+
}
|
|
74
84
|
}
|
package/src/utils/discovery.ts
CHANGED
|
@@ -4,131 +4,116 @@ import fs from 'node:fs';
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Discovers command directories to load into the CLI.
|
|
7
|
-
*
|
|
7
|
+
*
|
|
8
8
|
* Scans for:
|
|
9
9
|
* 1. Core commands (projectRoot/src/commands)
|
|
10
10
|
* 2. Module commands (projectRoot/src/modules/ * /src/commands)
|
|
11
|
-
*
|
|
11
|
+
*
|
|
12
12
|
* @param projectRoot - The root directory of the project
|
|
13
13
|
* @returns Array of absolute paths to command directories
|
|
14
14
|
*/
|
|
15
15
|
export function discoverCommandDirectories(projectRoot: string): string[] {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
if (visited.has(distEquivalent1) || visited.has(distEquivalent2)) {
|
|
35
|
-
logger.debug(`Skipping ${resolved} because a dist version is already registered`);
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
16
|
+
const directories: string[] = [];
|
|
17
|
+
const visited = new Set<string>();
|
|
18
|
+
|
|
19
|
+
const isTsEnvironment =
|
|
20
|
+
process.argv[1]?.endsWith('.ts') ||
|
|
21
|
+
process.argv[1]?.endsWith('.mts') ||
|
|
22
|
+
process.execArgv.some(
|
|
23
|
+
(arg) => arg.includes('tsx') || arg.includes('ts-node') || arg.includes('vitest'),
|
|
24
|
+
) ||
|
|
25
|
+
process.env.VITEST === 'true' ||
|
|
26
|
+
process.env.NODE_ENV === 'test';
|
|
27
|
+
|
|
28
|
+
const addDir = (dir: string) => {
|
|
29
|
+
const resolved = path.resolve(dir);
|
|
30
|
+
if (!fs.existsSync(resolved)) {
|
|
31
|
+
logger.debug(`Command directory not found (skipping): ${resolved}`);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
39
34
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
35
|
+
if (visited.has(resolved)) return;
|
|
36
|
+
|
|
37
|
+
// Detect if this is a source command directory
|
|
38
|
+
const srcPattern = path.join(path.sep, 'src', 'commands');
|
|
39
|
+
const distPattern = path.join(path.sep, 'dist');
|
|
40
|
+
const isSrcDir = resolved.endsWith(srcPattern) && !resolved.includes(distPattern);
|
|
41
|
+
|
|
42
|
+
// Strict check: if we are adding a 'src' directory...
|
|
43
|
+
if (isSrcDir) {
|
|
44
|
+
// 1. Check if an equivalent 'dist' exists in the same package
|
|
45
|
+
const distPath1 = resolved.replace(
|
|
46
|
+
srcPattern,
|
|
47
|
+
path.join(path.sep, 'dist', 'src', 'commands'),
|
|
48
|
+
);
|
|
49
|
+
const distPath2 = resolved.replace(srcPattern, path.join(path.sep, 'dist', 'commands'));
|
|
50
|
+
|
|
51
|
+
if (fs.existsSync(distPath1) || fs.existsSync(distPath2)) {
|
|
52
|
+
logger.debug(`Skipping src commands at ${resolved} because dist exists`);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// 2. If no TS loader, skip src/commands entirely IF it's likely to contain .ts
|
|
57
|
+
if (!isTsEnvironment) {
|
|
58
|
+
logger.debug(`Skipping src commands at ${resolved}: no TS loader detected`);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
51
62
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if (fs.
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
];
|
|
92
|
-
|
|
93
|
-
for (const cmdPath of possibleCmdPaths) {
|
|
94
|
-
if (fs.existsSync(cmdPath) && fs.statSync(cmdPath).isDirectory()) {
|
|
95
|
-
addDir(cmdPath);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
} catch (e: any) {
|
|
100
|
-
logger.debug(`Error scanning modules directory ${modulesDir}: ${e.message}`);
|
|
63
|
+
logger.debug(`Found command directory: ${resolved}`);
|
|
64
|
+
directories.push(resolved);
|
|
65
|
+
visited.add(resolved);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// 1. Core commands
|
|
69
|
+
const possibleCorePaths = [path.join(projectRoot, 'src/commands')];
|
|
70
|
+
possibleCorePaths.forEach(addDir);
|
|
71
|
+
|
|
72
|
+
// 2. Module & Package commands
|
|
73
|
+
const searchRoots = [
|
|
74
|
+
path.join(projectRoot, 'modules'),
|
|
75
|
+
path.join(projectRoot, 'src', 'modules'),
|
|
76
|
+
path.join(projectRoot, 'packages'),
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
searchRoots.forEach((root) => {
|
|
80
|
+
if (!fs.existsSync(root)) return;
|
|
81
|
+
try {
|
|
82
|
+
const entries = fs.readdirSync(root);
|
|
83
|
+
for (const entry of entries) {
|
|
84
|
+
if (entry.startsWith('.')) continue;
|
|
85
|
+
const entryPath = path.join(root, entry);
|
|
86
|
+
if (!fs.statSync(entryPath).isDirectory()) continue;
|
|
87
|
+
|
|
88
|
+
// Preference: dist/src/commands > dist/commands > src/commands
|
|
89
|
+
const possiblePaths = [
|
|
90
|
+
path.join(entryPath, 'dist/src/commands'),
|
|
91
|
+
path.join(entryPath, 'dist/commands'),
|
|
92
|
+
path.join(entryPath, 'src/commands'),
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
let foundDist = false;
|
|
96
|
+
for (const p of possiblePaths) {
|
|
97
|
+
if (fs.existsSync(p) && fs.statSync(p).isDirectory()) {
|
|
98
|
+
if (p.includes(path.sep + 'dist' + path.sep)) {
|
|
99
|
+
addDir(p);
|
|
100
|
+
foundDist = true;
|
|
101
|
+
break; // Found a dist version, skip others for this entry
|
|
101
102
|
}
|
|
103
|
+
}
|
|
102
104
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const packages = fs.readdirSync(packagesDir);
|
|
110
|
-
for (const pkg of packages) {
|
|
111
|
-
if (pkg.startsWith('.')) continue;
|
|
112
|
-
|
|
113
|
-
const pkgPath = path.join(packagesDir, pkg);
|
|
114
|
-
if (!fs.statSync(pkgPath).isDirectory()) continue;
|
|
115
|
-
|
|
116
|
-
const possibleCmdPaths = [
|
|
117
|
-
path.join(pkgPath, 'dist/src/commands'),
|
|
118
|
-
path.join(pkgPath, 'dist/commands'),
|
|
119
|
-
path.join(pkgPath, 'src/commands')
|
|
120
|
-
];
|
|
121
|
-
|
|
122
|
-
for (const cmdPath of possibleCmdPaths) {
|
|
123
|
-
if (fs.existsSync(cmdPath) && fs.statSync(cmdPath).isDirectory()) {
|
|
124
|
-
addDir(cmdPath);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
} catch (e: any) {
|
|
129
|
-
logger.debug(`Error scanning packages directory: ${e.message}`);
|
|
105
|
+
|
|
106
|
+
if (!foundDist) {
|
|
107
|
+
const srcPath = path.join(entryPath, 'src/commands');
|
|
108
|
+
if (fs.existsSync(srcPath) && fs.statSync(srcPath).isDirectory()) {
|
|
109
|
+
addDir(srcPath);
|
|
110
|
+
}
|
|
130
111
|
}
|
|
112
|
+
}
|
|
113
|
+
} catch (e: unknown) {
|
|
114
|
+
logger.debug(`Error scanning root ${root}: ${e instanceof Error ? e.message : String(e)}`);
|
|
131
115
|
}
|
|
116
|
+
});
|
|
132
117
|
|
|
133
|
-
|
|
118
|
+
return directories;
|
|
134
119
|
}
|