@nexical/cli 0.11.23 → 0.12.0

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 (91) hide show
  1. package/README.md +90 -235
  2. package/dist/{chunk-OYFWMYPG.js → chunk-6DE5Q66O.js} +6 -1
  3. package/dist/{chunk-OYFWMYPG.js.map → chunk-6DE5Q66O.js.map} +1 -1
  4. package/dist/chunk-G66GMEFE.js +31 -0
  5. package/dist/chunk-G66GMEFE.js.map +1 -0
  6. package/dist/{chunk-2FKDEDDE.js → chunk-HOVS7SCD.js} +16 -3
  7. package/dist/chunk-HOVS7SCD.js.map +1 -0
  8. package/dist/{chunk-GUUPSHWC.js → chunk-JEMIKBGX.js} +3 -3
  9. package/dist/chunk-JGAMEJTL.js +4101 -0
  10. package/dist/chunk-JGAMEJTL.js.map +1 -0
  11. package/dist/{chunk-OUGA4CB4.js → chunk-JS6WL5NS.js} +2 -2
  12. package/dist/{chunk-GEESHGE4.js → chunk-L2RUXOL4.js} +2 -2
  13. package/dist/{chunk-54HY52LH.js → chunk-QTJIGPQ3.js} +2 -2
  14. package/dist/{chunk-EKCOW7FM.js → chunk-USP2MI63.js} +41 -23
  15. package/dist/chunk-USP2MI63.js.map +1 -0
  16. package/dist/{chunk-2JW5BYZW.js → chunk-VKE7R2EZ.js} +2 -2
  17. package/dist/{chunk-AC4B3HPJ.js → chunk-XONR27KC.js} +2 -2
  18. package/dist/{chunk-PJIOCW2A.js → chunk-ZWNIZB3Q.js} +2 -2
  19. package/dist/index.js +5 -5
  20. package/dist/index.js.map +1 -1
  21. package/dist/src/commands/deploy.d.ts +3 -3
  22. package/dist/src/commands/deploy.js +134 -78
  23. package/dist/src/commands/deploy.js.map +1 -1
  24. package/dist/src/commands/init.js +5 -5
  25. package/dist/src/commands/module/add.js +4 -4
  26. package/dist/src/commands/module/list.js +2 -2
  27. package/dist/src/commands/module/remove.js +2 -2
  28. package/dist/src/commands/module/update.js +2 -2
  29. package/dist/src/commands/prompt.js +2 -2
  30. package/dist/src/commands/run.js +2 -2
  31. package/dist/src/commands/setup.js +3 -3
  32. package/dist/src/deploy/config-manager.js +3 -2
  33. package/dist/src/deploy/providers/cloudflare.d.ts +13 -8
  34. package/dist/src/deploy/providers/cloudflare.js +161 -52
  35. package/dist/src/deploy/providers/cloudflare.js.map +1 -1
  36. package/dist/src/deploy/providers/dns-cloudflare.d.ts +9 -0
  37. package/dist/src/deploy/providers/dns-cloudflare.js +123 -0
  38. package/dist/src/deploy/providers/dns-cloudflare.js.map +1 -0
  39. package/dist/src/deploy/providers/github.d.ts +6 -2
  40. package/dist/src/deploy/providers/github.js +37 -45
  41. package/dist/src/deploy/providers/github.js.map +1 -1
  42. package/dist/src/deploy/providers/railway.d.ts +17 -8
  43. package/dist/src/deploy/providers/railway.js +106 -45
  44. package/dist/src/deploy/providers/railway.js.map +1 -1
  45. package/dist/src/deploy/registry.d.ts +7 -4
  46. package/dist/src/deploy/registry.js +2 -2
  47. package/dist/src/deploy/schema.d.ts +188 -0
  48. package/dist/src/deploy/schema.js +11 -0
  49. package/dist/src/deploy/schema.js.map +1 -0
  50. package/dist/src/deploy/template-manager.d.ts +12 -0
  51. package/dist/src/deploy/template-manager.js +9 -0
  52. package/dist/src/deploy/template-manager.js.map +1 -0
  53. package/dist/src/deploy/types.d.ts +42 -17
  54. package/dist/src/deploy/types.js +1 -1
  55. package/dist/src/deploy/types.js.map +1 -1
  56. package/dist/src/deploy/utils.js +2 -2
  57. package/dist/src/utils/discovery.js +2 -2
  58. package/dist/src/utils/filter.js +2 -2
  59. package/dist/src/utils/git.js +2 -2
  60. package/dist/src/utils/url-resolver.js +2 -2
  61. package/dist/templates/github-workflow.yaml +23 -0
  62. package/package.json +2 -2
  63. package/src/commands/deploy.ts +157 -93
  64. package/src/deploy/config-manager.ts +14 -1
  65. package/src/deploy/providers/cloudflare.ts +203 -80
  66. package/src/deploy/providers/dns-cloudflare.ts +134 -0
  67. package/src/deploy/providers/github.ts +44 -47
  68. package/src/deploy/providers/railway.ts +135 -55
  69. package/src/deploy/registry.ts +49 -28
  70. package/src/deploy/schema.ts +39 -0
  71. package/src/deploy/template-manager.ts +32 -0
  72. package/src/deploy/templates/github-workflow.yaml +23 -0
  73. package/src/deploy/types.ts +48 -16
  74. package/test/integration/commands/deploy.integration.test.ts +79 -3
  75. package/test/unit/commands/deploy.test.ts +96 -198
  76. package/test/unit/deploy/config-manager.test.ts +9 -5
  77. package/test/unit/deploy/providers/cloudflare.test.ts +95 -96
  78. package/test/unit/deploy/providers/dns-cloudflare.test.ts +148 -0
  79. package/test/unit/deploy/providers/github.test.ts +43 -47
  80. package/test/unit/deploy/providers/railway.test.ts +50 -261
  81. package/test/unit/deploy/registry.test.ts +20 -17
  82. package/tsup.config.ts +3 -0
  83. package/dist/chunk-2FKDEDDE.js.map +0 -1
  84. package/dist/chunk-EKCOW7FM.js.map +0 -1
  85. /package/dist/{chunk-GUUPSHWC.js.map → chunk-JEMIKBGX.js.map} +0 -0
  86. /package/dist/{chunk-OUGA4CB4.js.map → chunk-JS6WL5NS.js.map} +0 -0
  87. /package/dist/{chunk-GEESHGE4.js.map → chunk-L2RUXOL4.js.map} +0 -0
  88. /package/dist/{chunk-54HY52LH.js.map → chunk-QTJIGPQ3.js.map} +0 -0
  89. /package/dist/{chunk-2JW5BYZW.js.map → chunk-VKE7R2EZ.js.map} +0 -0
  90. /package/dist/{chunk-AC4B3HPJ.js.map → chunk-XONR27KC.js.map} +0 -0
  91. /package/dist/{chunk-PJIOCW2A.js.map → chunk-ZWNIZB3Q.js.map} +0 -0
@@ -3,46 +3,28 @@ import dotenv from 'dotenv';
3
3
  import { BaseCommand } from '@nexical/cli-core';
4
4
  import { ConfigManager } from '../deploy/config-manager';
5
5
  import { ProviderRegistry } from '../deploy/registry';
6
- import { DeploymentContext } from '../deploy/types';
6
+ import { DeploymentContext, HostingProvider, AppConfig, DnsRecord } from '../deploy/types';
7
7
 
8
8
  export default class DeployCommand extends BaseCommand {
9
9
  static usage = 'deploy';
10
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
11
+ static help = `This command orchestrates the deployment of your applications
12
12
  by interacting with the providers specified in your configuration file.
13
13
 
14
14
  CONFIGURATION:
15
15
  - Requires a 'nexical.yaml' file in the project root.
16
- - If the file or specific sections are missing, the CLI will prompt you to run an interactive setup
17
- and save the configuration for future uses.
16
+ - Supports definition of multiple applications under 'deploy.apps'.
18
17
  - Supports loading environment variables from a .env file in the project root.
19
18
 
20
- PROVIDERS:
21
- - Backend: Railway, etc.
22
- - Frontend: Cloudflare Pages, etc.
23
- - Repository: GitHub, GitLab, etc.
24
-
25
19
  PROCESS:
26
20
  1. Loads environment variables from '.env'.
27
21
  2. Loads configuration from 'nexical.yaml'.
28
- 3. Provisions resources via the selected providers.
22
+ 3. Provisions resources for each application.
29
23
  4. Configures the repository (secrets/variables) for CI/CD.
30
- 5. Generates CI/CD workflow files.`;
24
+ 5. Generates CI/CD workflow files for each application.`;
31
25
 
32
26
  static args = {
33
27
  options: [
34
- {
35
- name: '--backend <provider>',
36
- description: 'Override backend provider',
37
- },
38
- {
39
- name: '--frontend <provider>',
40
- description: 'Override frontend provider',
41
- },
42
- {
43
- name: '--repo <provider>',
44
- description: 'Override repositroy provider',
45
- },
46
28
  {
47
29
  name: '--env <environment>',
48
30
  description: 'Deployment environment (e.g. production, staging)',
@@ -53,6 +35,19 @@ PROCESS:
53
35
  description: 'Simulate the deployment process',
54
36
  default: false,
55
37
  },
38
+ {
39
+ name: '--apps <apps>',
40
+ description: 'Comma separated list of applications to deploy',
41
+ },
42
+ {
43
+ name: '--manual',
44
+ description: 'Perform a direct build and deployment from the local machine',
45
+ default: false,
46
+ },
47
+ {
48
+ name: '--repo <provider>',
49
+ description: 'Repository provider to use (e.g. github, gitlab)',
50
+ },
56
51
  ],
57
52
  };
58
53
 
@@ -60,7 +55,7 @@ PROCESS:
60
55
  this.info('Starting Nexical Deployment...');
61
56
 
62
57
  // Load environment variables from .env
63
- dotenv.config({ path: path.join(process.cwd(), '.env') });
58
+ dotenv.config({ path: path.join(process.cwd(), '.env'), quiet: true });
64
59
 
65
60
  const configManager = new ConfigManager(process.cwd());
66
61
  const config = await configManager.load();
@@ -70,21 +65,35 @@ PROCESS:
70
65
  await registry.loadCoreProviders();
71
66
  await registry.loadLocalProviders(process.cwd());
72
67
 
73
- // Resolve providers (CLI flags > Config > Error)
74
- const backendProviderName =
75
- (options.backend as string | undefined) || config.deploy?.backend?.provider;
76
- if (!backendProviderName) {
77
- this.error(
78
- "Backend provider not specified. Use --backend flag or configure 'deploy.backend.provider' in nexical.yaml.",
79
- );
68
+ // Resolve Applications
69
+ const appsMap = config.deploy?.apps || {};
70
+ let apps: AppConfig[] = Object.entries(appsMap).map(([name, appConfig]) => {
71
+ const app: AppConfig = {
72
+ ...(appConfig as unknown as AppConfig),
73
+ name,
74
+ };
75
+ return app;
76
+ });
77
+
78
+ // Filter applications if --apps is specified
79
+ const selectedApps = options.apps as string | undefined;
80
+ if (selectedApps) {
81
+ const appNames = selectedApps.split(',').map((s) => s.trim());
82
+ const filteredApps = apps.filter((app) => appNames.includes(app.name));
83
+
84
+ // Validation: Ensure all specified apps exist
85
+ const missingApps = appNames.filter((name) => !apps.find((app) => app.name === name));
86
+ if (missingApps.length > 0) {
87
+ this.error(
88
+ `The following applications were not found in nexical.yaml: ${missingApps.join(', ')}`,
89
+ );
90
+ }
91
+
92
+ apps = filteredApps;
80
93
  }
81
94
 
82
- const frontendProviderName =
83
- (options.frontend as string | undefined) || config.deploy?.frontend?.provider;
84
- if (!frontendProviderName) {
85
- this.error(
86
- "Frontend provider not specified. Use --frontend flag or configure 'deploy.frontend.provider' in nexical.yaml.",
87
- );
95
+ if (apps.length === 0) {
96
+ this.error('No applications found in nexical.yaml. Please configure [deploy.apps].');
88
97
  }
89
98
 
90
99
  const repoProviderName =
@@ -95,13 +104,7 @@ PROCESS:
95
104
  );
96
105
  }
97
106
 
98
- const backendProvider = registry.getDeploymentProvider(backendProviderName!);
99
- const frontendProvider = registry.getDeploymentProvider(frontendProviderName!);
100
107
  const repoProvider = registry.getRepositoryProvider(repoProviderName!);
101
-
102
- if (!backendProvider) throw new Error(`Backend provider '${backendProviderName}' not found.`);
103
- if (!frontendProvider)
104
- throw new Error(`Frontend provider '${frontendProviderName}' not found.`);
105
108
  if (!repoProvider) throw new Error(`Repository provider '${repoProviderName}' not found.`);
106
109
 
107
110
  const context: DeploymentContext = {
@@ -110,65 +113,126 @@ PROCESS:
110
113
  options,
111
114
  };
112
115
 
113
- // Provision
114
- this.info(`Provisioning Backend with ${backendProvider.name}...`);
115
- await backendProvider.provision(context);
116
+ const activeApps: { provider: HostingProvider; app: AppConfig }[] = [];
117
+ const secrets: Record<string, string> = {};
118
+ const variables: Record<string, string> = {};
116
119
 
117
- this.info(`Provisioning Frontend with ${frontendProvider.name}...`);
118
- await frontendProvider.provision(context);
120
+ this.info(`Deploying ${apps.length} applications in parallel...`);
121
+
122
+ const isManual = !!options.manual;
123
+
124
+ await Promise.all(
125
+ apps.map(async (app) => {
126
+ this.info(`Processing application: ${app.name}...`);
127
+ const provider = registry.getHostingProvider(app.provider);
128
+ if (!provider) {
129
+ this.error(`Provider '${app.provider}' not found for application '${app.name}'.`);
130
+ return;
131
+ }
132
+
133
+ // Build
134
+ if (isManual && app.buildCommand) {
135
+ this.info(` Building ${app.name} locally...`);
136
+ if (context.options.dryRun) {
137
+ this.info(` [Dry Run] Would run build: ${app.buildCommand}`);
138
+ } else {
139
+ try {
140
+ const { execAsync } = await import('../deploy/utils');
141
+ await execAsync(app.buildCommand);
142
+ } catch (e: unknown) {
143
+ const message = e instanceof Error ? e.message : String(e);
144
+ this.error(`Build failed for ${app.name}: ${message}`);
145
+ return;
146
+ }
147
+ }
148
+ }
149
+
150
+ // Provision
151
+ this.info(` Provisioning ${app.name} with ${provider.name}...`);
152
+ await provider.provision(context, app);
153
+
154
+ // Direct Deploy
155
+ if (isManual && provider.deploy) {
156
+ this.info(` Performing direct deployment for ${app.name}...`);
157
+ await provider.deploy(context, app);
158
+ }
159
+
160
+ // Collect secrets
161
+ this.info(` Resolving secrets for ${app.name} from ${provider.name}...`);
162
+ try {
163
+ const appSecrets = await provider.getSecrets(context, app);
164
+ Object.assign(secrets, appSecrets);
165
+ } catch (e: unknown) {
166
+ const message = e instanceof Error ? e.message : String(e);
167
+ this.error(`Failed to resolve secrets for ${app.name} (${provider.name}): ${message}`);
168
+ }
169
+
170
+ // Collect variables
171
+ this.info(` Resolving variables for ${app.name} from ${provider.name}...`);
172
+ try {
173
+ const appVars = await provider.getVariables(context, app);
174
+ Object.assign(variables, appVars);
175
+ } catch (e: unknown) {
176
+ const message = e instanceof Error ? e.message : String(e);
177
+ this.error(`Failed to resolve variables for ${app.name} (${provider.name}): ${message}`);
178
+ }
179
+
180
+ activeApps.push({ provider, app });
181
+ }),
182
+ );
119
183
 
120
184
  // Configure Repo
121
185
  this.info(`Configuring Repository with ${repoProvider.name}...`);
122
-
123
- const secrets: Record<string, string> = {};
124
-
125
- // Collect secrets from Backend Provider
126
- this.info(`Resolving secrets from ${backendProvider.name}...`);
127
- try {
128
- const backendSecrets = await backendProvider.getSecrets(context);
129
- Object.assign(secrets, backendSecrets);
130
- } catch (e: unknown) {
131
- const message = e instanceof Error ? e.message : String(e);
132
- this.error(`Failed to resolve secrets for ${backendProvider.name}: ${message}`);
133
- }
134
-
135
- // Collect secrets from Frontend Provider
136
- this.info(`Resolving secrets from ${frontendProvider.name}...`);
137
- try {
138
- const frontendSecrets = await frontendProvider.getSecrets(context);
139
- Object.assign(secrets, frontendSecrets);
140
- } catch (e: unknown) {
141
- const message = e instanceof Error ? e.message : String(e);
142
- this.error(`Failed to resolve secrets for ${frontendProvider.name}: ${message}`);
143
- }
144
-
145
186
  await repoProvider.configureSecrets(context, secrets);
146
-
147
- const variables: Record<string, string> = {};
148
-
149
- // Collect variables from Backend Provider
150
- try {
151
- const backendVars = await backendProvider.getVariables(context);
152
- Object.assign(variables, backendVars);
153
- } catch (e: unknown) {
154
- const message = e instanceof Error ? e.message : String(e);
155
- this.error(`Failed to resolve variables for ${backendProvider.name}: ${message}`);
156
- }
157
-
158
- // Collect variables from Frontend Provider
159
- try {
160
- const frontendVars = await frontendProvider.getVariables(context);
161
- Object.assign(variables, frontendVars);
162
- } catch (e: unknown) {
163
- const message = e instanceof Error ? e.message : String(e);
164
- this.error(`Failed to resolve variables for ${frontendProvider.name}: ${message}`);
165
- }
166
-
167
187
  await repoProvider.configureVariables(context, variables);
168
188
 
169
189
  // Generate Workflows
170
190
  this.info('Generating CI/CD Workflows...');
171
- await repoProvider.generateWorkflow(context, [backendProvider, frontendProvider]);
191
+ await repoProvider.generateWorkflow(context, activeApps);
192
+
193
+ // DNS Provisioning
194
+ const dnsConfig = config.deploy?.dns;
195
+ if (dnsConfig?.provider) {
196
+ const dnsProvider = registry.getDnsProvider(dnsConfig.provider);
197
+ if (!dnsProvider) {
198
+ this.error(`DNS provider '${dnsConfig.provider}' not found.`);
199
+ return;
200
+ }
201
+
202
+ const dnsRecords: DnsRecord[] = [];
203
+ for (const { app, provider } of activeApps) {
204
+ const target =
205
+ app.dnsTarget ||
206
+ (provider.getDefaultDnsTarget ? provider.getDefaultDnsTarget(app) : undefined);
207
+
208
+ if (app.domain && target) {
209
+ const domains = Array.isArray(app.domain) ? app.domain : [app.domain];
210
+ for (const domain of domains) {
211
+ const isIp = /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/.test(target);
212
+ dnsRecords.push({
213
+ type: isIp ? 'A' : 'CNAME',
214
+ name: domain,
215
+ content: target,
216
+ proxied: true,
217
+ });
218
+ }
219
+ } else if (app.domain && !target) {
220
+ this.warn(
221
+ `App '${app.name}' specifies domain(s) but no 'dnsTarget' could be inferred. Skipping DNS auto-provisioning.`,
222
+ );
223
+ }
224
+ }
225
+
226
+ if (dnsRecords.length > 0) {
227
+ this.info(`Configuring DNS with ${dnsProvider.name}...`);
228
+ try {
229
+ await dnsProvider.provision(context, dnsRecords);
230
+ } catch (e: unknown) {
231
+ const message = e instanceof Error ? e.message : String(e);
232
+ this.warn(`DNS provisioning failed: ${message}`);
233
+ }
234
+ }
235
+ }
172
236
 
173
237
  this.success('Deployment configuration complete!');
174
238
  }
@@ -2,6 +2,8 @@ import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import YAML from 'yaml';
4
4
  import { NexicalConfig } from './types';
5
+ import { DeploymentSchema } from './schema';
6
+ import { logger } from '@nexical/cli-core';
5
7
 
6
8
  export class ConfigManager {
7
9
  private configPath: string;
@@ -13,7 +15,18 @@ export class ConfigManager {
13
15
  async load(): Promise<NexicalConfig> {
14
16
  try {
15
17
  const content = await fs.readFile(this.configPath, 'utf-8');
16
- return YAML.parse(content) as NexicalConfig;
18
+ const parsed = YAML.parse(content);
19
+
20
+ const result = DeploymentSchema.safeParse(parsed);
21
+ if (!result.success) {
22
+ logger.error('Invalid nexical.yaml configuration:');
23
+ result.error.issues.forEach((err) => {
24
+ logger.error(` - ${err.path.join('.')}: ${err.message}`);
25
+ });
26
+ throw new Error('Configuration validation failed.');
27
+ }
28
+
29
+ return result.data as NexicalConfig;
17
30
  } catch (error: unknown) {
18
31
  if (
19
32
  error &&