@nexical/cli 0.11.8 → 0.11.10
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/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/src/commands/deploy.d.ts +2 -0
- package/dist/src/commands/deploy.js +3 -3
- package/dist/src/commands/deploy.js.map +1 -1
- package/dist/src/commands/init.js +3 -3
- package/dist/src/commands/module/add.js +53 -22
- package/dist/src/commands/module/add.js.map +1 -1
- package/dist/src/commands/module/list.d.ts +1 -0
- package/dist/src/commands/module/list.js +54 -45
- package/dist/src/commands/module/list.js.map +1 -1
- package/dist/src/commands/module/remove.js +37 -12
- package/dist/src/commands/module/remove.js.map +1 -1
- package/dist/src/commands/module/update.js +15 -3
- package/dist/src/commands/module/update.js.map +1 -1
- package/dist/src/commands/run.js +18 -1
- package/dist/src/commands/run.js.map +1 -1
- package/package.json +2 -2
- package/src/commands/deploy.ts +3 -3
- package/src/commands/module/add.ts +74 -31
- package/src/commands/module/list.ts +80 -57
- package/src/commands/module/remove.ts +50 -14
- package/src/commands/module/update.ts +19 -5
- package/src/commands/run.ts +21 -1
- package/test/e2e/lifecycle.e2e.test.ts +3 -2
- package/test/integration/commands/deploy.integration.test.ts +102 -0
- package/test/integration/commands/init.integration.test.ts +16 -1
- package/test/integration/commands/module.integration.test.ts +81 -55
- package/test/integration/commands/run.integration.test.ts +69 -74
- package/test/integration/commands/setup.integration.test.ts +53 -0
- package/test/unit/commands/deploy.test.ts +285 -0
- package/test/unit/commands/init.test.ts +15 -0
- package/test/unit/commands/module/add.test.ts +363 -254
- package/test/unit/commands/module/list.test.ts +100 -99
- package/test/unit/commands/module/remove.test.ts +143 -58
- package/test/unit/commands/module/update.test.ts +45 -62
- package/test/unit/commands/run.test.ts +16 -1
- package/test/unit/commands/setup.test.ts +25 -66
- package/test/unit/deploy/config-manager.test.ts +65 -0
- package/test/unit/deploy/providers/cloudflare.test.ts +210 -0
- package/test/unit/deploy/providers/github.test.ts +139 -0
- package/test/unit/deploy/providers/railway.test.ts +328 -0
- package/test/unit/deploy/registry.test.ts +227 -0
- package/test/unit/deploy/utils.test.ts +30 -0
- package/test/unit/utils/command-discovery.test.ts +145 -142
- package/test/unit/utils/git_utils.test.ts +49 -0
|
@@ -26,12 +26,24 @@ var ModuleUpdateCommand = class extends BaseCommand {
|
|
|
26
26
|
logger.debug("Update context:", { name, projectRoot });
|
|
27
27
|
try {
|
|
28
28
|
if (name) {
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
const locations = [
|
|
30
|
+
{ type: "backend", path: `apps/backend/modules/${name}` },
|
|
31
|
+
{ type: "frontend", path: `apps/frontend/modules/${name}` },
|
|
32
|
+
{ type: "legacy", path: `modules/${name}` }
|
|
33
|
+
];
|
|
34
|
+
let targetLoc = null;
|
|
35
|
+
for (const loc of locations) {
|
|
36
|
+
const absPath = path.resolve(projectRoot, loc.path);
|
|
37
|
+
if (await import_fs_extra.default.pathExists(absPath)) {
|
|
38
|
+
targetLoc = loc;
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (!targetLoc) {
|
|
32
43
|
this.error(`Module ${name} not found.`);
|
|
33
44
|
return;
|
|
34
45
|
}
|
|
46
|
+
const relativePath = targetLoc.path;
|
|
35
47
|
await runCommand(`git submodule update --remote --merge ${relativePath}`, projectRoot);
|
|
36
48
|
} else {
|
|
37
49
|
await runCommand("git submodule update --remote --merge", projectRoot);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/commands/module/update.ts"],"sourcesContent":["import { type CommandDefinition, BaseCommand, logger, runCommand } from '@nexical/cli-core';\nimport fs from 'fs-extra';\nimport path from 'path';\n\nexport default class ModuleUpdateCommand extends BaseCommand {\n static usage = 'module update [name]';\n static description = 'Update a specific module or all modules.';\n static requiresProject = true;\n\n static args: CommandDefinition = {\n args: [{ name: 'name', required: false, description: 'Name of the module to update' }],\n };\n\n async run(options: { name?: string }) {\n const projectRoot = this.projectRoot as string;\n const { name } = options;\n\n this.info(name ? `Updating module ${name}...` : 'Updating all modules...');\n logger.debug('Update context:', { name, projectRoot: projectRoot });\n\n try {\n if (name) {\n const
|
|
1
|
+
{"version":3,"sources":["../../../../src/commands/module/update.ts"],"sourcesContent":["import { type CommandDefinition, BaseCommand, logger, runCommand } from '@nexical/cli-core';\nimport fs from 'fs-extra';\nimport path from 'path';\n\nexport default class ModuleUpdateCommand extends BaseCommand {\n static usage = 'module update [name]';\n static description = 'Update a specific module or all modules.';\n static requiresProject = true;\n\n static args: CommandDefinition = {\n args: [{ name: 'name', required: false, description: 'Name of the module to update' }],\n };\n\n async run(options: { name?: string }) {\n const projectRoot = this.projectRoot as string;\n const { name } = options;\n\n this.info(name ? `Updating module ${name}...` : 'Updating all modules...');\n logger.debug('Update context:', { name, projectRoot: projectRoot });\n\n try {\n if (name) {\n // Check locations\n const locations = [\n { type: 'backend', path: `apps/backend/modules/${name}` },\n { type: 'frontend', path: `apps/frontend/modules/${name}` },\n { type: 'legacy', path: `modules/${name}` },\n ];\n\n let targetLoc: { type: string; path: string } | null = null;\n\n for (const loc of locations) {\n const absPath = path.resolve(projectRoot, loc.path);\n if (await fs.pathExists(absPath)) {\n targetLoc = loc;\n break;\n }\n }\n\n if (!targetLoc) {\n this.error(`Module ${name} not found.`);\n return;\n }\n\n const relativePath = targetLoc.path;\n\n // Update specific module\n await runCommand(`git submodule update --remote --merge ${relativePath}`, projectRoot);\n } else {\n // Update all\n await runCommand('git submodule update --remote --merge', projectRoot);\n }\n\n this.info('Syncing workspace dependencies...');\n await runCommand('npm install', projectRoot);\n\n this.success('Modules updated successfully.');\n } catch (e: unknown) {\n if (e instanceof Error) {\n this.error(`Failed to update modules: ${e.message}`);\n } else {\n this.error(`Failed to update modules: ${String(e)}`);\n }\n }\n }\n}\n"],"mappings":";;;;;;;;;;AAAA;AACA,sBAAe;AADf,SAAiC,aAAa,QAAQ,kBAAkB;AAExE,OAAO,UAAU;AAEjB,IAAqB,sBAArB,cAAiD,YAAY;AAAA,EAC3D,OAAO,QAAQ;AAAA,EACf,OAAO,cAAc;AAAA,EACrB,OAAO,kBAAkB;AAAA,EAEzB,OAAO,OAA0B;AAAA,IAC/B,MAAM,CAAC,EAAE,MAAM,QAAQ,UAAU,OAAO,aAAa,+BAA+B,CAAC;AAAA,EACvF;AAAA,EAEA,MAAM,IAAI,SAA4B;AACpC,UAAM,cAAc,KAAK;AACzB,UAAM,EAAE,KAAK,IAAI;AAEjB,SAAK,KAAK,OAAO,mBAAmB,IAAI,QAAQ,yBAAyB;AACzE,WAAO,MAAM,mBAAmB,EAAE,MAAM,YAAyB,CAAC;AAElE,QAAI;AACF,UAAI,MAAM;AAER,cAAM,YAAY;AAAA,UAChB,EAAE,MAAM,WAAW,MAAM,wBAAwB,IAAI,GAAG;AAAA,UACxD,EAAE,MAAM,YAAY,MAAM,yBAAyB,IAAI,GAAG;AAAA,UAC1D,EAAE,MAAM,UAAU,MAAM,WAAW,IAAI,GAAG;AAAA,QAC5C;AAEA,YAAI,YAAmD;AAEvD,mBAAW,OAAO,WAAW;AAC3B,gBAAM,UAAU,KAAK,QAAQ,aAAa,IAAI,IAAI;AAClD,cAAI,MAAM,gBAAAA,QAAG,WAAW,OAAO,GAAG;AAChC,wBAAY;AACZ;AAAA,UACF;AAAA,QACF;AAEA,YAAI,CAAC,WAAW;AACd,eAAK,MAAM,UAAU,IAAI,aAAa;AACtC;AAAA,QACF;AAEA,cAAM,eAAe,UAAU;AAG/B,cAAM,WAAW,yCAAyC,YAAY,IAAI,WAAW;AAAA,MACvF,OAAO;AAEL,cAAM,WAAW,yCAAyC,WAAW;AAAA,MACvE;AAEA,WAAK,KAAK,mCAAmC;AAC7C,YAAM,WAAW,eAAe,WAAW;AAE3C,WAAK,QAAQ,+BAA+B;AAAA,IAC9C,SAAS,GAAY;AACnB,UAAI,aAAa,OAAO;AACtB,aAAK,MAAM,6BAA6B,EAAE,OAAO,EAAE;AAAA,MACrD,OAAO;AACL,aAAK,MAAM,6BAA6B,OAAO,CAAC,CAAC,EAAE;AAAA,MACrD;AAAA,IACF;AAAA,EACF;AACF;","names":["fs"]}
|
package/dist/src/commands/run.js
CHANGED
|
@@ -41,8 +41,25 @@ var RunCommand = class extends BaseCommand {
|
|
|
41
41
|
let scriptName = script;
|
|
42
42
|
if (script.includes(":")) {
|
|
43
43
|
const [moduleName, name] = script.split(":");
|
|
44
|
-
execPath = path.resolve(projectRoot, "modules", moduleName);
|
|
45
44
|
scriptName = name;
|
|
45
|
+
const locations = [
|
|
46
|
+
{ type: "backend", path: `apps/backend/modules/${moduleName}` },
|
|
47
|
+
{ type: "frontend", path: `apps/frontend/modules/${moduleName}` },
|
|
48
|
+
{ type: "legacy", path: `modules/${moduleName}` }
|
|
49
|
+
];
|
|
50
|
+
let found = false;
|
|
51
|
+
for (const loc of locations) {
|
|
52
|
+
const absPath = path.resolve(projectRoot, loc.path);
|
|
53
|
+
if (await import_fs_extra.default.pathExists(absPath)) {
|
|
54
|
+
execPath = absPath;
|
|
55
|
+
found = true;
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (!found) {
|
|
60
|
+
this.error(`Module ${moduleName} not found.`);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
46
63
|
logger.debug(`Resolving module script: ${moduleName}:${scriptName} at ${execPath}`);
|
|
47
64
|
} else {
|
|
48
65
|
logger.debug(`Resolving core script: ${scriptName} at ${execPath}`);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/commands/run.ts"],"sourcesContent":["import { type CommandDefinition, BaseCommand, logger } from '@nexical/cli-core';\nimport fs from 'fs-extra';\nimport path from 'path';\nimport { spawn } from 'child_process';\nimport process from 'node:process';\n\nexport default class RunCommand extends BaseCommand {\n static usage = 'run <script> [args...]';\n static description = 'Run a script inside the Nexical environment.';\n static requiresProject = true;\n\n static args: CommandDefinition = {\n args: [\n {\n name: 'script',\n required: true,\n description: 'The script to run (script-name OR module:script-name)',\n },\n { name: 'args...', required: false, description: 'Arguments for the script' },\n ],\n };\n\n async run(options: { script: string; args?: string[] }) {\n const projectRoot = this.projectRoot as string;\n const script = options.script;\n const scriptArgs = options.args || [];\n\n if (!script) {\n this.error('Please specify a script to run.');\n return;\n }\n\n logger.debug('Run command context:', { script, args: scriptArgs, projectRoot });\n\n let execPath = projectRoot;\n let scriptName = script;\n\n // Handle module:script syntax\n if (script.includes(':')) {\n const [moduleName, name] = script.split(':');\n
|
|
1
|
+
{"version":3,"sources":["../../../src/commands/run.ts"],"sourcesContent":["import { type CommandDefinition, BaseCommand, logger } from '@nexical/cli-core';\nimport fs from 'fs-extra';\nimport path from 'path';\nimport { spawn } from 'child_process';\nimport process from 'node:process';\n\nexport default class RunCommand extends BaseCommand {\n static usage = 'run <script> [args...]';\n static description = 'Run a script inside the Nexical environment.';\n static requiresProject = true;\n\n static args: CommandDefinition = {\n args: [\n {\n name: 'script',\n required: true,\n description: 'The script to run (script-name OR module:script-name)',\n },\n { name: 'args...', required: false, description: 'Arguments for the script' },\n ],\n };\n\n async run(options: { script: string; args?: string[] }) {\n const projectRoot = this.projectRoot as string;\n const script = options.script;\n const scriptArgs = options.args || [];\n\n if (!script) {\n this.error('Please specify a script to run.');\n return;\n }\n\n logger.debug('Run command context:', { script, args: scriptArgs, projectRoot });\n\n let execPath = projectRoot;\n let scriptName = script;\n\n // Handle module:script syntax\n if (script.includes(':')) {\n const [moduleName, name] = script.split(':');\n scriptName = name;\n\n const locations = [\n { type: 'backend', path: `apps/backend/modules/${moduleName}` },\n { type: 'frontend', path: `apps/frontend/modules/${moduleName}` },\n { type: 'legacy', path: `modules/${moduleName}` },\n ];\n\n let found = false;\n for (const loc of locations) {\n const absPath = path.resolve(projectRoot, loc.path);\n if (await fs.pathExists(absPath)) {\n execPath = absPath;\n found = true;\n break;\n }\n }\n\n if (!found) {\n this.error(`Module ${moduleName} not found.`);\n return;\n }\n\n logger.debug(`Resolving module script: ${moduleName}:${scriptName} at ${execPath}`);\n } else {\n logger.debug(`Resolving core script: ${scriptName} at ${execPath}`);\n }\n\n // Validate script existence\n const pkgJsonPath = path.join(execPath, 'package.json');\n if (!(await fs.pathExists(pkgJsonPath))) {\n this.error(`Failed to find package.json at ${execPath}`);\n return;\n }\n\n try {\n const pkg = await fs.readJson(pkgJsonPath);\n if (!pkg.scripts || !pkg.scripts[scriptName]) {\n const type = script.includes(':') ? `module ${script.split(':')[0]}` : 'Nexical core';\n this.error(`Script \"${scriptName}\" does not exist in ${type}`);\n return;\n }\n } catch (e: unknown) {\n if (e instanceof Error) {\n this.error(`Failed to read package.json at ${execPath}: ${e.message}`);\n } else {\n this.error(`Failed to read package.json at ${execPath}: ${String(e)}`);\n }\n return;\n }\n\n const finalArgs = ['run', scriptName, '--', ...scriptArgs];\n logger.debug(`Executing: npm ${finalArgs.join(' ')} in ${execPath}`);\n\n const child = spawn('npm', finalArgs, {\n cwd: execPath,\n stdio: 'inherit',\n env: {\n ...process.env,\n FORCE_COLOR: '1',\n },\n });\n\n // Handle process termination to kill child\n const cleanup = () => {\n child.kill();\n process.exit();\n };\n\n process.on('SIGINT', cleanup);\n process.on('SIGTERM', cleanup);\n\n await new Promise<void>((resolve) => {\n child.on('close', (code) => {\n // Remove listeners to prevent memory leaks if this command is run multiple times in-process (e.g. tests)\n process.off('SIGINT', cleanup);\n process.off('SIGTERM', cleanup);\n\n if (code !== 0) {\n process.exit(code || 1);\n }\n resolve();\n });\n });\n }\n}\n"],"mappings":";;;;;;;;;;AAAA;AACA,sBAAe;AADf,SAAiC,aAAa,cAAc;AAE5D,OAAO,UAAU;AACjB,SAAS,aAAa;AACtB,OAAO,aAAa;AAEpB,IAAqB,aAArB,cAAwC,YAAY;AAAA,EAClD,OAAO,QAAQ;AAAA,EACf,OAAO,cAAc;AAAA,EACrB,OAAO,kBAAkB;AAAA,EAEzB,OAAO,OAA0B;AAAA,IAC/B,MAAM;AAAA,MACJ;AAAA,QACE,MAAM;AAAA,QACN,UAAU;AAAA,QACV,aAAa;AAAA,MACf;AAAA,MACA,EAAE,MAAM,WAAW,UAAU,OAAO,aAAa,2BAA2B;AAAA,IAC9E;AAAA,EACF;AAAA,EAEA,MAAM,IAAI,SAA8C;AACtD,UAAM,cAAc,KAAK;AACzB,UAAM,SAAS,QAAQ;AACvB,UAAM,aAAa,QAAQ,QAAQ,CAAC;AAEpC,QAAI,CAAC,QAAQ;AACX,WAAK,MAAM,iCAAiC;AAC5C;AAAA,IACF;AAEA,WAAO,MAAM,wBAAwB,EAAE,QAAQ,MAAM,YAAY,YAAY,CAAC;AAE9E,QAAI,WAAW;AACf,QAAI,aAAa;AAGjB,QAAI,OAAO,SAAS,GAAG,GAAG;AACxB,YAAM,CAAC,YAAY,IAAI,IAAI,OAAO,MAAM,GAAG;AAC3C,mBAAa;AAEb,YAAM,YAAY;AAAA,QAChB,EAAE,MAAM,WAAW,MAAM,wBAAwB,UAAU,GAAG;AAAA,QAC9D,EAAE,MAAM,YAAY,MAAM,yBAAyB,UAAU,GAAG;AAAA,QAChE,EAAE,MAAM,UAAU,MAAM,WAAW,UAAU,GAAG;AAAA,MAClD;AAEA,UAAI,QAAQ;AACZ,iBAAW,OAAO,WAAW;AAC3B,cAAM,UAAU,KAAK,QAAQ,aAAa,IAAI,IAAI;AAClD,YAAI,MAAM,gBAAAA,QAAG,WAAW,OAAO,GAAG;AAChC,qBAAW;AACX,kBAAQ;AACR;AAAA,QACF;AAAA,MACF;AAEA,UAAI,CAAC,OAAO;AACV,aAAK,MAAM,UAAU,UAAU,aAAa;AAC5C;AAAA,MACF;AAEA,aAAO,MAAM,4BAA4B,UAAU,IAAI,UAAU,OAAO,QAAQ,EAAE;AAAA,IACpF,OAAO;AACL,aAAO,MAAM,0BAA0B,UAAU,OAAO,QAAQ,EAAE;AAAA,IACpE;AAGA,UAAM,cAAc,KAAK,KAAK,UAAU,cAAc;AACtD,QAAI,CAAE,MAAM,gBAAAA,QAAG,WAAW,WAAW,GAAI;AACvC,WAAK,MAAM,kCAAkC,QAAQ,EAAE;AACvD;AAAA,IACF;AAEA,QAAI;AACF,YAAM,MAAM,MAAM,gBAAAA,QAAG,SAAS,WAAW;AACzC,UAAI,CAAC,IAAI,WAAW,CAAC,IAAI,QAAQ,UAAU,GAAG;AAC5C,cAAM,OAAO,OAAO,SAAS,GAAG,IAAI,UAAU,OAAO,MAAM,GAAG,EAAE,CAAC,CAAC,KAAK;AACvE,aAAK,MAAM,WAAW,UAAU,uBAAuB,IAAI,EAAE;AAC7D;AAAA,MACF;AAAA,IACF,SAAS,GAAY;AACnB,UAAI,aAAa,OAAO;AACtB,aAAK,MAAM,kCAAkC,QAAQ,KAAK,EAAE,OAAO,EAAE;AAAA,MACvE,OAAO;AACL,aAAK,MAAM,kCAAkC,QAAQ,KAAK,OAAO,CAAC,CAAC,EAAE;AAAA,MACvE;AACA;AAAA,IACF;AAEA,UAAM,YAAY,CAAC,OAAO,YAAY,MAAM,GAAG,UAAU;AACzD,WAAO,MAAM,kBAAkB,UAAU,KAAK,GAAG,CAAC,OAAO,QAAQ,EAAE;AAEnE,UAAM,QAAQ,MAAM,OAAO,WAAW;AAAA,MACpC,KAAK;AAAA,MACL,OAAO;AAAA,MACP,KAAK;AAAA,QACH,GAAG,QAAQ;AAAA,QACX,aAAa;AAAA,MACf;AAAA,IACF,CAAC;AAGD,UAAM,UAAU,MAAM;AACpB,YAAM,KAAK;AACX,cAAQ,KAAK;AAAA,IACf;AAEA,YAAQ,GAAG,UAAU,OAAO;AAC5B,YAAQ,GAAG,WAAW,OAAO;AAE7B,UAAM,IAAI,QAAc,CAAC,YAAY;AACnC,YAAM,GAAG,SAAS,CAAC,SAAS;AAE1B,gBAAQ,IAAI,UAAU,OAAO;AAC7B,gBAAQ,IAAI,WAAW,OAAO;AAE9B,YAAI,SAAS,GAAG;AACd,kBAAQ,KAAK,QAAQ,CAAC;AAAA,QACxB;AACA,gBAAQ;AAAA,MACV,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AACF;","names":["fs"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nexical/cli",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.10",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"nexical": "./dist/index.js"
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
]
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@nexical/cli-core": "^0.1.
|
|
31
|
+
"@nexical/cli-core": "^0.1.15",
|
|
32
32
|
"dotenv": "^17.3.1",
|
|
33
33
|
"fast-glob": "^3.3.3",
|
|
34
34
|
"jiti": "^2.6.1",
|
package/src/commands/deploy.ts
CHANGED
|
@@ -6,9 +6,9 @@ import { ProviderRegistry } from '../deploy/registry';
|
|
|
6
6
|
import { DeploymentContext } from '../deploy/types';
|
|
7
7
|
|
|
8
8
|
export default class DeployCommand extends BaseCommand {
|
|
9
|
-
static
|
|
10
|
-
|
|
11
|
-
This command orchestrates the deployment of your frontend and backend applications
|
|
9
|
+
static usage = 'deploy';
|
|
10
|
+
static description = 'Deploy the application based on nexical.yaml configuration.';
|
|
11
|
+
static help = `This command orchestrates the deployment of your frontend and backend applications
|
|
12
12
|
by interacting with the providers specified in your configuration file.
|
|
13
13
|
|
|
14
14
|
CONFIGURATION:
|
|
@@ -66,6 +66,7 @@ export default class ModuleAddCommand extends BaseCommand {
|
|
|
66
66
|
`staging-${Date.now()}-${Math.random().toString(36).substring(7)}`,
|
|
67
67
|
);
|
|
68
68
|
let moduleName = '';
|
|
69
|
+
let moduleType: 'backend' | 'frontend' = 'backend'; // Default to backend if uncertain, but we should detect.
|
|
69
70
|
let dependencies: string[] = [];
|
|
70
71
|
|
|
71
72
|
try {
|
|
@@ -74,28 +75,68 @@ export default class ModuleAddCommand extends BaseCommand {
|
|
|
74
75
|
// Shallow clone to inspect
|
|
75
76
|
await clone(cleanUrl, stagingDir, { depth: 1 });
|
|
76
77
|
|
|
77
|
-
//
|
|
78
|
+
// Search path handling
|
|
78
79
|
const searchPath = subPath ? path.join(stagingDir, subPath) : stagingDir;
|
|
80
|
+
|
|
81
|
+
// 1. Detect Module Name & Dependencies
|
|
79
82
|
const moduleYamlPath = path.join(searchPath, 'module.yaml');
|
|
80
83
|
const moduleYmlPath = path.join(searchPath, 'module.yml');
|
|
84
|
+
const pkgJsonPath = path.join(searchPath, 'package.json');
|
|
81
85
|
|
|
82
86
|
let configPath = '';
|
|
83
87
|
if (await fs.pathExists(moduleYamlPath)) configPath = moduleYamlPath;
|
|
84
88
|
else if (await fs.pathExists(moduleYmlPath)) configPath = moduleYmlPath;
|
|
85
|
-
|
|
86
|
-
|
|
89
|
+
|
|
90
|
+
// Try to get name from module.yaml/yml
|
|
91
|
+
if (configPath) {
|
|
92
|
+
const configContent = await fs.readFile(configPath, 'utf8');
|
|
93
|
+
const config = YAML.parse(configContent);
|
|
94
|
+
if (config.name) moduleName = config.name;
|
|
95
|
+
dependencies = config.dependencies || [];
|
|
87
96
|
}
|
|
88
97
|
|
|
89
|
-
|
|
90
|
-
|
|
98
|
+
// If no name yet, try package.json
|
|
99
|
+
if (!moduleName && (await fs.pathExists(pkgJsonPath))) {
|
|
100
|
+
try {
|
|
101
|
+
const pkg = await fs.readJson(pkgJsonPath);
|
|
102
|
+
if (pkg.name) {
|
|
103
|
+
// Handle scoped packages @modules/name -> name
|
|
104
|
+
moduleName = pkg.name.startsWith('@modules/') ? pkg.name.split('/')[1] : pkg.name;
|
|
105
|
+
}
|
|
106
|
+
} catch {
|
|
107
|
+
/* ignore */
|
|
108
|
+
}
|
|
109
|
+
}
|
|
91
110
|
|
|
92
|
-
if
|
|
93
|
-
|
|
111
|
+
// Fallback to git repo name if still no name
|
|
112
|
+
if (!moduleName) {
|
|
113
|
+
moduleName = path.basename(cleanUrl, '.git');
|
|
94
114
|
}
|
|
95
|
-
moduleName = config.name;
|
|
96
|
-
dependencies = config.dependencies || [];
|
|
97
115
|
|
|
98
|
-
//
|
|
116
|
+
// 2. Detect Module Type
|
|
117
|
+
// Frontend indicators: ui.yaml, or specifically typed in module.config.mjs (harder to parse statically), or package.json dependencies like 'react'/'astro' (maybe too broad).
|
|
118
|
+
// Backend indicators: models.yaml, api.yaml, access.yaml.
|
|
119
|
+
|
|
120
|
+
const hasUiYaml = await fs.pathExists(path.join(searchPath, 'ui.yaml'));
|
|
121
|
+
const hasModelsYaml = await fs.pathExists(path.join(searchPath, 'models.yaml'));
|
|
122
|
+
const hasApiYaml = await fs.pathExists(path.join(searchPath, 'api.yaml'));
|
|
123
|
+
|
|
124
|
+
if (hasUiYaml) {
|
|
125
|
+
moduleType = 'frontend';
|
|
126
|
+
} else if (hasModelsYaml || hasApiYaml) {
|
|
127
|
+
moduleType = 'backend';
|
|
128
|
+
} else {
|
|
129
|
+
// Fallback: Check checking package.json for "auth-astro" which is common in both, but maybe "react" or "vue" for frontend?
|
|
130
|
+
// Let's assume Backend default if ambiguous for now, or check for specific folder structure?
|
|
131
|
+
// Let's look for `src/components` vs `src/services`.
|
|
132
|
+
if (await fs.pathExists(path.join(searchPath, 'src', 'components'))) {
|
|
133
|
+
moduleType = 'frontend';
|
|
134
|
+
} else {
|
|
135
|
+
moduleType = 'backend';
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Normalize dependencies
|
|
99
140
|
if (dependencies && !Array.isArray(dependencies)) {
|
|
100
141
|
dependencies = Object.keys(dependencies);
|
|
101
142
|
}
|
|
@@ -104,39 +145,34 @@ export default class ModuleAddCommand extends BaseCommand {
|
|
|
104
145
|
await fs.remove(stagingDir);
|
|
105
146
|
}
|
|
106
147
|
|
|
107
|
-
// Stage 2: Conflict Detection
|
|
108
|
-
const
|
|
109
|
-
|
|
148
|
+
// Stage 2: Conflict Detection & Path Resolution
|
|
149
|
+
const modulesBaseDir =
|
|
150
|
+
moduleType === 'frontend' ? 'apps/frontend/modules' : 'apps/backend/modules';
|
|
151
|
+
const relativeTargetDir = path.join(modulesBaseDir, moduleName);
|
|
152
|
+
const targetDir = path.join(projectRoot!, relativeTargetDir);
|
|
110
153
|
|
|
111
154
|
if (await fs.pathExists(targetDir)) {
|
|
112
155
|
// Check origin
|
|
113
156
|
const existingRemote = await getRemoteUrl(targetDir);
|
|
114
|
-
// We compare cleanUrl (the repo root).
|
|
115
|
-
// normalize both
|
|
116
157
|
const normExisting = existingRemote.replace(/\.git$/, '');
|
|
117
158
|
const normNew = cleanUrl.replace(/\.git$/, '');
|
|
118
159
|
|
|
119
160
|
if (normExisting !== normNew && existingRemote !== '') {
|
|
120
161
|
throw new Error(
|
|
121
|
-
`Dependency Conflict! Module '${moduleName}' exists but remote '${existingRemote}' does not match '${cleanUrl}'.`,
|
|
162
|
+
`Dependency Conflict! Module '${moduleName}' exists in ${moduleType} but remote '${existingRemote}' does not match '${cleanUrl}'.`,
|
|
122
163
|
);
|
|
123
164
|
}
|
|
124
165
|
|
|
125
|
-
this.info(`Module ${moduleName} already installed.`);
|
|
126
|
-
// Proceed to recurse, but skip add
|
|
166
|
+
this.info(`Module ${moduleName} already installed in ${moduleType}.`);
|
|
127
167
|
} else {
|
|
128
168
|
// Stage 3: Submodule Add
|
|
129
|
-
this.info(`Installing ${moduleName} to ${relativeTargetDir}...`);
|
|
130
|
-
|
|
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.
|
|
169
|
+
this.info(`Installing ${moduleName} (${moduleType}) to ${relativeTargetDir}...`);
|
|
170
|
+
await fs.ensureDir(path.dirname(targetDir)); // Ensure apps/backend/modules exists
|
|
135
171
|
await runCommand(`git submodule add ${cleanUrl} ${relativeTargetDir}`, projectRoot!);
|
|
136
172
|
}
|
|
137
173
|
|
|
138
174
|
// Update nexical.yaml
|
|
139
|
-
await this.addToConfig(moduleName);
|
|
175
|
+
await this.addToConfig(moduleName, moduleType);
|
|
140
176
|
|
|
141
177
|
// Stage 4: Recurse
|
|
142
178
|
if (dependencies.length > 0) {
|
|
@@ -147,12 +183,11 @@ export default class ModuleAddCommand extends BaseCommand {
|
|
|
147
183
|
}
|
|
148
184
|
}
|
|
149
185
|
|
|
150
|
-
private async addToConfig(moduleName: string) {
|
|
186
|
+
private async addToConfig(moduleName: string, type: 'backend' | 'frontend') {
|
|
151
187
|
const projectRoot = this.projectRoot as string;
|
|
152
188
|
const configPath = path.join(projectRoot, 'nexical.yaml');
|
|
153
189
|
|
|
154
190
|
if (!(await fs.pathExists(configPath))) {
|
|
155
|
-
// Not strictly required to exist for all operations, but good to have if we are tracking modules.
|
|
156
191
|
logger.warn('nexical.yaml not found, skipping module list update.');
|
|
157
192
|
return;
|
|
158
193
|
}
|
|
@@ -161,12 +196,20 @@ export default class ModuleAddCommand extends BaseCommand {
|
|
|
161
196
|
const content = await fs.readFile(configPath, 'utf8');
|
|
162
197
|
const config = YAML.parse(content) || {};
|
|
163
198
|
|
|
164
|
-
if (!config.modules) config.modules =
|
|
199
|
+
if (!config.modules) config.modules = {};
|
|
200
|
+
|
|
201
|
+
// Migration: If modules is array, convert to object
|
|
202
|
+
if (Array.isArray(config.modules)) {
|
|
203
|
+
const oldModules = config.modules;
|
|
204
|
+
config.modules = { backend: oldModules, frontend: [] }; // Assume old were backend? Or just move them to backend for safety.
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (!config.modules[type]) config.modules[type] = [];
|
|
165
208
|
|
|
166
|
-
if (!config.modules.includes(moduleName)) {
|
|
167
|
-
config.modules.push(moduleName);
|
|
209
|
+
if (!config.modules[type].includes(moduleName)) {
|
|
210
|
+
config.modules[type].push(moduleName);
|
|
168
211
|
await fs.writeFile(configPath, YAML.stringify(config));
|
|
169
|
-
logger.debug(`Added ${moduleName} to nexical.yaml modules list.`);
|
|
212
|
+
logger.debug(`Added ${moduleName} to nexical.yaml modules.${type} list.`);
|
|
170
213
|
}
|
|
171
214
|
} catch (e: unknown) {
|
|
172
215
|
if (e instanceof Error) {
|
|
@@ -1,8 +1,15 @@
|
|
|
1
|
-
import { BaseCommand
|
|
1
|
+
import { BaseCommand } from '@nexical/cli-core';
|
|
2
2
|
import fs from 'fs-extra';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import YAML from 'yaml';
|
|
5
5
|
|
|
6
|
+
interface ModuleInfo {
|
|
7
|
+
name: string;
|
|
8
|
+
version: string;
|
|
9
|
+
description: string;
|
|
10
|
+
type: 'backend' | 'frontend' | 'legacy';
|
|
11
|
+
}
|
|
12
|
+
|
|
6
13
|
export default class ModuleListCommand extends BaseCommand {
|
|
7
14
|
static usage = 'module list';
|
|
8
15
|
static description = 'List installed modules.';
|
|
@@ -10,71 +17,87 @@ export default class ModuleListCommand extends BaseCommand {
|
|
|
10
17
|
|
|
11
18
|
async run() {
|
|
12
19
|
const projectRoot = this.projectRoot as string;
|
|
13
|
-
const modulesDir = path.resolve(projectRoot, 'modules');
|
|
14
|
-
logger.debug(`Scanning for modules in: ${modulesDir}`);
|
|
15
20
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
21
|
+
// Define locations to scan
|
|
22
|
+
const builtInLocations = [
|
|
23
|
+
{ type: 'backend', path: path.join(projectRoot, 'apps/backend/modules') },
|
|
24
|
+
{ type: 'frontend', path: path.join(projectRoot, 'apps/frontend/modules') },
|
|
25
|
+
// Check legacy `modules` folder just in case?
|
|
26
|
+
{ type: 'legacy', path: path.join(projectRoot, 'modules') },
|
|
27
|
+
];
|
|
20
28
|
|
|
21
|
-
|
|
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 = '';
|
|
30
|
-
|
|
31
|
-
const pkgJsonPath = path.join(modulePath, 'package.json');
|
|
32
|
-
const moduleYamlPath = path.join(modulePath, 'module.yaml');
|
|
33
|
-
const moduleYmlPath = path.join(modulePath, 'module.yml');
|
|
34
|
-
|
|
35
|
-
let pkg: Record<string, unknown> = {};
|
|
36
|
-
let modConfig: Record<string, unknown> = {};
|
|
37
|
-
|
|
38
|
-
if (await fs.pathExists(pkgJsonPath)) {
|
|
39
|
-
try {
|
|
40
|
-
pkg = await fs.readJson(pkgJsonPath);
|
|
41
|
-
} catch {
|
|
42
|
-
/* ignore */
|
|
43
|
-
}
|
|
44
|
-
}
|
|
29
|
+
const allModules: ModuleInfo[] = [];
|
|
45
30
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
? moduleYamlPath
|
|
50
|
-
: moduleYmlPath;
|
|
51
|
-
const content = await fs.readFile(configPath, 'utf8');
|
|
52
|
-
modConfig = YAML.parse(content) || {};
|
|
53
|
-
} catch {
|
|
54
|
-
/* ignore */
|
|
55
|
-
}
|
|
56
|
-
}
|
|
31
|
+
for (const loc of builtInLocations) {
|
|
32
|
+
if (await fs.pathExists(loc.path)) {
|
|
33
|
+
const modules = await fs.readdir(loc.path);
|
|
57
34
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
35
|
+
for (const moduleName of modules) {
|
|
36
|
+
const modulePath = path.join(loc.path, moduleName);
|
|
37
|
+
if ((await fs.stat(modulePath)).isDirectory()) {
|
|
38
|
+
const info = await this.getModuleInfo(
|
|
39
|
+
modulePath,
|
|
40
|
+
moduleName,
|
|
41
|
+
loc.type as 'backend' | 'frontend' | 'legacy',
|
|
42
|
+
);
|
|
43
|
+
allModules.push(info);
|
|
44
|
+
}
|
|
63
45
|
}
|
|
64
46
|
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (allModules.length === 0) {
|
|
50
|
+
this.info('No modules installed.');
|
|
51
|
+
} else {
|
|
52
|
+
// Sort by type then name
|
|
53
|
+
allModules.sort((a, b) => {
|
|
54
|
+
if (a.type !== b.type) return a.type.localeCompare(b.type);
|
|
55
|
+
return a.name.localeCompare(b.name);
|
|
56
|
+
});
|
|
57
|
+
// eslint-disable-next-line no-console
|
|
58
|
+
console.table(allModules);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private async getModuleInfo(
|
|
63
|
+
modulePath: string,
|
|
64
|
+
dirName: string,
|
|
65
|
+
type: 'backend' | 'frontend' | 'legacy',
|
|
66
|
+
): Promise<ModuleInfo> {
|
|
67
|
+
let version = 'unknown';
|
|
68
|
+
let description = '';
|
|
69
|
+
|
|
70
|
+
const pkgJsonPath = path.join(modulePath, 'package.json');
|
|
71
|
+
const moduleYamlPath = path.join(modulePath, 'module.yaml');
|
|
72
|
+
const moduleYmlPath = path.join(modulePath, 'module.yml');
|
|
65
73
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
74
|
+
let pkg: Record<string, unknown> = {};
|
|
75
|
+
let modConfig: Record<string, unknown> = {};
|
|
76
|
+
|
|
77
|
+
if (await fs.pathExists(pkgJsonPath)) {
|
|
78
|
+
try {
|
|
79
|
+
pkg = (await fs.readJson(pkgJsonPath)) || {};
|
|
80
|
+
} catch {
|
|
81
|
+
/* ignore */
|
|
71
82
|
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if ((await fs.pathExists(moduleYamlPath)) || (await fs.pathExists(moduleYmlPath))) {
|
|
86
|
+
try {
|
|
87
|
+
const configPath = (await fs.pathExists(moduleYamlPath)) ? moduleYamlPath : moduleYmlPath;
|
|
88
|
+
const content = await fs.readFile(configPath, 'utf8');
|
|
89
|
+
modConfig = YAML.parse(content) || {};
|
|
90
|
+
} catch {
|
|
91
|
+
/* ignore */
|
|
77
92
|
}
|
|
78
93
|
}
|
|
94
|
+
|
|
95
|
+
version = (pkg.version as string) || (modConfig.version as string) || 'unknown';
|
|
96
|
+
description = (pkg.description as string) || (modConfig.description as string) || '';
|
|
97
|
+
|
|
98
|
+
// Use config name if available, else dirName
|
|
99
|
+
const name = (modConfig.name as string) || dirName;
|
|
100
|
+
|
|
101
|
+
return { name, version, description, type };
|
|
79
102
|
}
|
|
80
103
|
}
|
|
@@ -16,27 +16,44 @@ export default class ModuleRemoveCommand extends BaseCommand {
|
|
|
16
16
|
const projectRoot = this.projectRoot as string;
|
|
17
17
|
const { name } = options;
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
// Check locations
|
|
20
|
+
const locations = [
|
|
21
|
+
{ type: 'backend', path: `apps/backend/modules/${name}` },
|
|
22
|
+
{ type: 'frontend', path: `apps/frontend/modules/${name}` },
|
|
23
|
+
{ type: 'legacy', path: `modules/${name}` },
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
let targetLoc: { type: string; path: string } | null = null;
|
|
27
|
+
let fullPath = '';
|
|
28
|
+
|
|
29
|
+
for (const loc of locations) {
|
|
30
|
+
const absPath = path.resolve(projectRoot, loc.path);
|
|
31
|
+
if (await fs.pathExists(absPath)) {
|
|
32
|
+
targetLoc = loc;
|
|
33
|
+
fullPath = absPath;
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
23
37
|
|
|
24
|
-
if (!
|
|
25
|
-
this.error(`Module ${name} not found
|
|
38
|
+
if (!targetLoc) {
|
|
39
|
+
this.error(`Module ${name} not found in any standard location.`);
|
|
26
40
|
return;
|
|
27
41
|
}
|
|
28
42
|
|
|
29
|
-
|
|
43
|
+
const relativePath = targetLoc.path;
|
|
44
|
+
|
|
45
|
+
logger.debug('Removing module at:', fullPath);
|
|
46
|
+
this.info(`Removing module ${name} (${targetLoc.type})...`);
|
|
30
47
|
|
|
31
48
|
try {
|
|
32
49
|
await runCommand(`git submodule deinit -f ${relativePath}`, projectRoot);
|
|
33
50
|
await runCommand(`git rm -f ${relativePath}`, projectRoot);
|
|
34
51
|
|
|
35
|
-
// Clean up .git/modules
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
52
|
+
// Clean up .git/modules if needed (git rm often handles this but sometimes leaves stale dirs in .git/modules)
|
|
53
|
+
// The path in .git/modules depends on how it was added.
|
|
54
|
+
// Usually .git/modules/apps/backend/modules/name
|
|
55
|
+
// We'll leave strict git cleanup to git, manually removing can be risky if path structure varies.
|
|
56
|
+
// But we can check for the directory itself just in case.
|
|
40
57
|
|
|
41
58
|
this.info('Syncing workspace dependencies...');
|
|
42
59
|
await runCommand('npm install', projectRoot);
|
|
@@ -63,8 +80,27 @@ export default class ModuleRemoveCommand extends BaseCommand {
|
|
|
63
80
|
const content = await fs.readFile(configPath, 'utf8');
|
|
64
81
|
const config = YAML.parse(content) || {};
|
|
65
82
|
|
|
66
|
-
|
|
67
|
-
|
|
83
|
+
let changed = false;
|
|
84
|
+
|
|
85
|
+
if (config.modules) {
|
|
86
|
+
// Check if object
|
|
87
|
+
if (!Array.isArray(config.modules)) {
|
|
88
|
+
for (const key of Object.keys(config.modules)) {
|
|
89
|
+
if (Array.isArray(config.modules[key]) && config.modules[key].includes(moduleName)) {
|
|
90
|
+
config.modules[key] = config.modules[key].filter((m: string) => m !== moduleName);
|
|
91
|
+
changed = true;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
} else {
|
|
95
|
+
// Legacy array
|
|
96
|
+
if (config.modules.includes(moduleName)) {
|
|
97
|
+
config.modules = config.modules.filter((m: string) => m !== moduleName);
|
|
98
|
+
changed = true;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (changed) {
|
|
68
104
|
await fs.writeFile(configPath, YAML.stringify(config));
|
|
69
105
|
logger.debug(`Removed ${moduleName} from nexical.yaml modules list.`);
|
|
70
106
|
}
|
|
@@ -20,17 +20,31 @@ export default class ModuleUpdateCommand extends BaseCommand {
|
|
|
20
20
|
|
|
21
21
|
try {
|
|
22
22
|
if (name) {
|
|
23
|
-
|
|
24
|
-
const
|
|
23
|
+
// Check locations
|
|
24
|
+
const locations = [
|
|
25
|
+
{ type: 'backend', path: `apps/backend/modules/${name}` },
|
|
26
|
+
{ type: 'frontend', path: `apps/frontend/modules/${name}` },
|
|
27
|
+
{ type: 'legacy', path: `modules/${name}` },
|
|
28
|
+
];
|
|
25
29
|
|
|
26
|
-
|
|
30
|
+
let targetLoc: { type: string; path: string } | null = null;
|
|
31
|
+
|
|
32
|
+
for (const loc of locations) {
|
|
33
|
+
const absPath = path.resolve(projectRoot, loc.path);
|
|
34
|
+
if (await fs.pathExists(absPath)) {
|
|
35
|
+
targetLoc = loc;
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!targetLoc) {
|
|
27
41
|
this.error(`Module ${name} not found.`);
|
|
28
42
|
return;
|
|
29
43
|
}
|
|
30
44
|
|
|
45
|
+
const relativePath = targetLoc.path;
|
|
46
|
+
|
|
31
47
|
// Update specific module
|
|
32
|
-
// We enter the directory and pull? Or generic submodule update?
|
|
33
|
-
// Generic submodule update --remote src/modules/name
|
|
34
48
|
await runCommand(`git submodule update --remote --merge ${relativePath}`, projectRoot);
|
|
35
49
|
} else {
|
|
36
50
|
// Update all
|
package/src/commands/run.ts
CHANGED
|
@@ -38,9 +38,29 @@ export default class RunCommand extends BaseCommand {
|
|
|
38
38
|
// Handle module:script syntax
|
|
39
39
|
if (script.includes(':')) {
|
|
40
40
|
const [moduleName, name] = script.split(':');
|
|
41
|
-
execPath = path.resolve(projectRoot, 'modules', moduleName);
|
|
42
41
|
scriptName = name;
|
|
43
42
|
|
|
43
|
+
const locations = [
|
|
44
|
+
{ type: 'backend', path: `apps/backend/modules/${moduleName}` },
|
|
45
|
+
{ type: 'frontend', path: `apps/frontend/modules/${moduleName}` },
|
|
46
|
+
{ type: 'legacy', path: `modules/${moduleName}` },
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
let found = false;
|
|
50
|
+
for (const loc of locations) {
|
|
51
|
+
const absPath = path.resolve(projectRoot, loc.path);
|
|
52
|
+
if (await fs.pathExists(absPath)) {
|
|
53
|
+
execPath = absPath;
|
|
54
|
+
found = true;
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!found) {
|
|
60
|
+
this.error(`Module ${moduleName} not found.`);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
44
64
|
logger.debug(`Resolving module script: ${moduleName}:${scriptName} at ${execPath}`);
|
|
45
65
|
} else {
|
|
46
66
|
logger.debug(`Resolving core script: ${scriptName} at ${execPath}`);
|
|
@@ -122,7 +122,7 @@ if (args[0] === 'build') {
|
|
|
122
122
|
'module',
|
|
123
123
|
'add',
|
|
124
124
|
moduleDir,
|
|
125
|
-
|
|
125
|
+
// Name is inferred from module.yaml
|
|
126
126
|
],
|
|
127
127
|
projectDir,
|
|
128
128
|
{ env },
|
|
@@ -132,7 +132,8 @@ if (args[0] === 'build') {
|
|
|
132
132
|
console.error('Module Add Failed:', modResult.stderr || modResult.stdout);
|
|
133
133
|
}
|
|
134
134
|
expect(modResult.exitCode).toBe(0);
|
|
135
|
-
|
|
135
|
+
// Defaults to backend module
|
|
136
|
+
expect(fs.existsSync(path.join(projectDir, 'apps/backend/modules/my-test-module'))).toBe(true);
|
|
136
137
|
|
|
137
138
|
// --- STEP 3: BUILD ---
|
|
138
139
|
// Run: nexical run build
|