@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.
Files changed (46) hide show
  1. package/dist/index.js +2 -2
  2. package/dist/index.js.map +1 -1
  3. package/dist/src/commands/deploy.d.ts +2 -0
  4. package/dist/src/commands/deploy.js +3 -3
  5. package/dist/src/commands/deploy.js.map +1 -1
  6. package/dist/src/commands/init.js +3 -3
  7. package/dist/src/commands/module/add.js +53 -22
  8. package/dist/src/commands/module/add.js.map +1 -1
  9. package/dist/src/commands/module/list.d.ts +1 -0
  10. package/dist/src/commands/module/list.js +54 -45
  11. package/dist/src/commands/module/list.js.map +1 -1
  12. package/dist/src/commands/module/remove.js +37 -12
  13. package/dist/src/commands/module/remove.js.map +1 -1
  14. package/dist/src/commands/module/update.js +15 -3
  15. package/dist/src/commands/module/update.js.map +1 -1
  16. package/dist/src/commands/run.js +18 -1
  17. package/dist/src/commands/run.js.map +1 -1
  18. package/package.json +2 -2
  19. package/src/commands/deploy.ts +3 -3
  20. package/src/commands/module/add.ts +74 -31
  21. package/src/commands/module/list.ts +80 -57
  22. package/src/commands/module/remove.ts +50 -14
  23. package/src/commands/module/update.ts +19 -5
  24. package/src/commands/run.ts +21 -1
  25. package/test/e2e/lifecycle.e2e.test.ts +3 -2
  26. package/test/integration/commands/deploy.integration.test.ts +102 -0
  27. package/test/integration/commands/init.integration.test.ts +16 -1
  28. package/test/integration/commands/module.integration.test.ts +81 -55
  29. package/test/integration/commands/run.integration.test.ts +69 -74
  30. package/test/integration/commands/setup.integration.test.ts +53 -0
  31. package/test/unit/commands/deploy.test.ts +285 -0
  32. package/test/unit/commands/init.test.ts +15 -0
  33. package/test/unit/commands/module/add.test.ts +363 -254
  34. package/test/unit/commands/module/list.test.ts +100 -99
  35. package/test/unit/commands/module/remove.test.ts +143 -58
  36. package/test/unit/commands/module/update.test.ts +45 -62
  37. package/test/unit/commands/run.test.ts +16 -1
  38. package/test/unit/commands/setup.test.ts +25 -66
  39. package/test/unit/deploy/config-manager.test.ts +65 -0
  40. package/test/unit/deploy/providers/cloudflare.test.ts +210 -0
  41. package/test/unit/deploy/providers/github.test.ts +139 -0
  42. package/test/unit/deploy/providers/railway.test.ts +328 -0
  43. package/test/unit/deploy/registry.test.ts +227 -0
  44. package/test/unit/deploy/utils.test.ts +30 -0
  45. package/test/unit/utils/command-discovery.test.ts +145 -142
  46. 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 relativePath = `modules/${name}`;
30
- const fullPath = path.resolve(projectRoot, relativePath);
31
- if (!await import_fs_extra.default.pathExists(fullPath)) {
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 relativePath = `modules/${name}`;\n const fullPath = path.resolve(projectRoot, relativePath);\n\n if (!(await fs.pathExists(fullPath))) {\n this.error(`Module ${name} not found.`);\n return;\n }\n\n // Update specific module\n // We enter the directory and pull? Or generic submodule update?\n // Generic submodule update --remote src/modules/name\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;AACR,cAAM,eAAe,WAAW,IAAI;AACpC,cAAM,WAAW,KAAK,QAAQ,aAAa,YAAY;AAEvD,YAAI,CAAE,MAAM,gBAAAA,QAAG,WAAW,QAAQ,GAAI;AACpC,eAAK,MAAM,UAAU,IAAI,aAAa;AACtC;AAAA,QACF;AAKA,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"]}
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"]}
@@ -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 execPath = path.resolve(projectRoot, 'modules', moduleName);\n scriptName = name;\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,iBAAW,KAAK,QAAQ,aAAa,WAAW,UAAU;AAC1D,mBAAa;AAEb,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"]}
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":"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","names":["fs"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nexical/cli",
3
- "version": "0.11.8",
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.12",
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",
@@ -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 description = `Deploy the application based on nexical.yaml configuration.
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
- // Read module.yaml
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
- else {
86
- throw new Error(`No module.yaml found in ${cleanUrl}${subPath ? '//' + subPath : ''}`);
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
- const configContent = await fs.readFile(configPath, 'utf8');
90
- const config = YAML.parse(configContent);
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 (!config.name) {
93
- throw new Error(`Module at ${url} is missing 'name' in module.yaml`);
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
- // Normalize dependencies to array if object (though spec says list of strings, defensiveness is good)
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 targetDir = path.join(projectRoot!, 'modules', moduleName);
109
- const relativeTargetDir = path.relative(projectRoot!, targetDir);
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
- // 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.
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, logger } from '@nexical/cli-core';
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
- if (!(await fs.pathExists(modulesDir))) {
17
- this.info('No modules installed (modules directory missing).');
18
- return;
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
- 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 = '';
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
- 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 */
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
- 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 });
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
- if (validModules.length === 0) {
67
- this.info('No modules installed.');
68
- } else {
69
- // eslint-disable-next-line no-console
70
- console.table(validModules);
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
- } 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)}`);
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
- const relativePath = `modules/${name}`;
20
- const fullPath = path.resolve(projectRoot, relativePath);
21
-
22
- logger.debug('Removing module at:', fullPath);
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 (!(await fs.pathExists(fullPath))) {
25
- this.error(`Module ${name} not found at ${relativePath}.`);
38
+ if (!targetLoc) {
39
+ this.error(`Module ${name} not found in any standard location.`);
26
40
  return;
27
41
  }
28
42
 
29
- this.info(`Removing module ${name}...`);
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
- const gitModulesDir = path.resolve(projectRoot, '.git', 'modules', 'modules', name);
37
- if (await fs.pathExists(gitModulesDir)) {
38
- await fs.remove(gitModulesDir);
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
- if (config.modules && config.modules.includes(moduleName)) {
67
- config.modules = config.modules.filter((m: string) => m !== moduleName);
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
- const relativePath = `modules/${name}`;
24
- const fullPath = path.resolve(projectRoot, relativePath);
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
- if (!(await fs.pathExists(fullPath))) {
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
@@ -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
- 'my-test-module', // Explicit name
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
- expect(fs.existsSync(path.join(projectDir, 'modules/my-test-module'))).toBe(true);
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