@nexical/cli 0.11.23 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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 +148 -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 +169 -88
  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,143 @@ 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...`);
119
121
 
120
- // Configure Repo
121
- this.info(`Configuring Repository with ${repoProvider.name}...`);
122
+ const isManual = !!options.manual;
122
123
 
123
- const secrets: Record<string, string> = {};
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
+ }
124
132
 
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
- }
133
+ // Build
134
+ if (isManual && app.buildCommand) {
135
+ this.info(` Building ${app.name} locally...`);
134
136
 
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
- }
137
+ const buildEnv: Record<string, string> = {
138
+ ...(process.env as Record<string, string>),
139
+ ...(app.env || {}),
140
+ };
144
141
 
145
- await repoProvider.configureSecrets(context, secrets);
142
+ if (app.domain) {
143
+ const domain = Array.isArray(app.domain) ? app.domain[0] : app.domain;
144
+ buildEnv.SITE = `https://${domain}`;
145
+ buildEnv.BASE = '/';
146
+ }
146
147
 
147
- const variables: Record<string, string> = {};
148
+ if (context.options.dryRun) {
149
+ this.info(` [Dry Run] Would run build: ${app.buildCommand}`);
150
+ if (buildEnv.SITE) {
151
+ this.info(
152
+ ` [Dry Run] Environment override: SITE=${buildEnv.SITE} BASE=${buildEnv.BASE}`,
153
+ );
154
+ }
155
+ } else {
156
+ try {
157
+ const { execAsync } = await import('../deploy/utils');
158
+ await execAsync(app.buildCommand, { env: buildEnv });
159
+ } catch (e: unknown) {
160
+ const message = e instanceof Error ? e.message : String(e);
161
+ this.error(`Build failed for ${app.name}: ${message}`);
162
+ return;
163
+ }
164
+ }
165
+ }
148
166
 
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
- }
167
+ // Provision
168
+ this.info(` Provisioning ${app.name} with ${provider.name}...`);
169
+ await provider.provision(context, app);
157
170
 
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
- }
171
+ // Direct Deploy
172
+ if (isManual && provider.deploy) {
173
+ this.info(` Performing direct deployment for ${app.name}...`);
174
+ await provider.deploy(context, app);
175
+ }
166
176
 
177
+ // Collect secrets
178
+ this.info(` Resolving secrets for ${app.name} from ${provider.name}...`);
179
+ try {
180
+ const appSecrets = await provider.getSecrets(context, app);
181
+ Object.assign(secrets, appSecrets);
182
+ } catch (e: unknown) {
183
+ const message = e instanceof Error ? e.message : String(e);
184
+ this.error(`Failed to resolve secrets for ${app.name} (${provider.name}): ${message}`);
185
+ }
186
+
187
+ // Collect variables
188
+ this.info(` Resolving variables for ${app.name} from ${provider.name}...`);
189
+ try {
190
+ const appVars = await provider.getVariables(context, app);
191
+ Object.assign(variables, appVars);
192
+ } catch (e: unknown) {
193
+ const message = e instanceof Error ? e.message : String(e);
194
+ this.error(`Failed to resolve variables for ${app.name} (${provider.name}): ${message}`);
195
+ }
196
+
197
+ activeApps.push({ provider, app });
198
+ }),
199
+ );
200
+
201
+ // Configure Repo
202
+ this.info(`Configuring Repository with ${repoProvider.name}...`);
203
+ await repoProvider.configureSecrets(context, secrets);
167
204
  await repoProvider.configureVariables(context, variables);
168
205
 
169
206
  // Generate Workflows
170
207
  this.info('Generating CI/CD Workflows...');
171
- await repoProvider.generateWorkflow(context, [backendProvider, frontendProvider]);
208
+ await repoProvider.generateWorkflow(context, activeApps);
209
+
210
+ // DNS Provisioning
211
+ const dnsConfig = config.deploy?.dns;
212
+ if (dnsConfig?.provider) {
213
+ const dnsProvider = registry.getDnsProvider(dnsConfig.provider);
214
+ if (!dnsProvider) {
215
+ this.error(`DNS provider '${dnsConfig.provider}' not found.`);
216
+ return;
217
+ }
218
+
219
+ const dnsRecords: DnsRecord[] = [];
220
+ for (const { app, provider } of activeApps) {
221
+ const target =
222
+ app.dnsTarget ||
223
+ (provider.getDefaultDnsTarget ? provider.getDefaultDnsTarget(app) : undefined);
224
+
225
+ if (app.domain && target) {
226
+ const domains = Array.isArray(app.domain) ? app.domain : [app.domain];
227
+ for (const domain of domains) {
228
+ const isIp = /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/.test(target);
229
+ dnsRecords.push({
230
+ type: isIp ? 'A' : 'CNAME',
231
+ name: domain,
232
+ content: target,
233
+ proxied: true,
234
+ });
235
+ }
236
+ } else if (app.domain && !target) {
237
+ this.warn(
238
+ `App '${app.name}' specifies domain(s) but no 'dnsTarget' could be inferred. Skipping DNS auto-provisioning.`,
239
+ );
240
+ }
241
+ }
242
+
243
+ if (dnsRecords.length > 0) {
244
+ this.info(`Configuring DNS with ${dnsProvider.name}...`);
245
+ try {
246
+ await dnsProvider.provision(context, dnsRecords);
247
+ } catch (e: unknown) {
248
+ const message = e instanceof Error ? e.message : String(e);
249
+ this.warn(`DNS provisioning failed: ${message}`);
250
+ }
251
+ }
252
+ }
172
253
 
173
254
  this.success('Deployment configuration complete!');
174
255
  }
@@ -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 &&