@nexical/cli 0.11.3 → 0.11.5

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 (43) hide show
  1. package/dist/chunk-2FKDEDDE.js +39 -0
  2. package/dist/chunk-2FKDEDDE.js.map +1 -0
  3. package/dist/chunk-2JW5BYZW.js +24 -0
  4. package/dist/chunk-2JW5BYZW.js.map +1 -0
  5. package/dist/chunk-EKCOW7FM.js +118 -0
  6. package/dist/chunk-EKCOW7FM.js.map +1 -0
  7. package/dist/index.js +13 -11
  8. package/dist/index.js.map +1 -1
  9. package/dist/src/commands/deploy.d.ts +3 -12
  10. package/dist/src/commands/deploy.js +106 -108
  11. package/dist/src/commands/deploy.js.map +1 -1
  12. package/dist/src/commands/init.js +3 -3
  13. package/dist/src/commands/module/add.js +3 -3
  14. package/dist/src/deploy/config-manager.d.ts +11 -0
  15. package/dist/src/deploy/config-manager.js +9 -0
  16. package/dist/src/deploy/config-manager.js.map +1 -0
  17. package/dist/src/deploy/providers/cloudflare.d.ts +12 -0
  18. package/dist/src/deploy/providers/cloudflare.js +113 -0
  19. package/dist/src/deploy/providers/cloudflare.js.map +1 -0
  20. package/dist/src/deploy/providers/github.d.ts +10 -0
  21. package/dist/src/deploy/providers/github.js +121 -0
  22. package/dist/src/deploy/providers/github.js.map +1 -0
  23. package/dist/src/deploy/providers/railway.d.ts +12 -0
  24. package/dist/src/deploy/providers/railway.js +89 -0
  25. package/dist/src/deploy/providers/railway.js.map +1 -0
  26. package/dist/src/deploy/registry.d.ts +15 -0
  27. package/dist/src/deploy/registry.js +9 -0
  28. package/dist/src/deploy/registry.js.map +1 -0
  29. package/dist/src/deploy/types.d.ts +47 -0
  30. package/dist/src/deploy/types.js +8 -0
  31. package/dist/src/deploy/types.js.map +1 -0
  32. package/dist/src/deploy/utils.d.ts +6 -0
  33. package/dist/src/deploy/utils.js +11 -0
  34. package/dist/src/deploy/utils.js.map +1 -0
  35. package/package.json +13 -11
  36. package/src/commands/deploy.ts +128 -144
  37. package/src/deploy/config-manager.ts +41 -0
  38. package/src/deploy/providers/cloudflare.ts +143 -0
  39. package/src/deploy/providers/github.ts +135 -0
  40. package/src/deploy/providers/railway.ts +103 -0
  41. package/src/deploy/registry.ts +136 -0
  42. package/src/deploy/types.ts +63 -0
  43. package/src/deploy/utils.ts +13 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/deploy/types.ts"],"sourcesContent":["export interface CIConfig {\n secrets: string[];\n variables: string[];\n installSteps?: string[];\n buildSteps?: string[];\n deploySteps?: string[];\n // Platform specific overrides (e.g. uses: action/...)\n githubActionStep?: Record<string, unknown>;\n}\n\nexport interface DeploymentContext {\n cwd: string;\n config: NexicalConfig;\n options: Record<string, unknown>;\n}\n\nexport interface DeploymentProvider {\n name: string;\n type: 'frontend' | 'backend';\n\n // Interactive or automatic setup of the provider resources\n provision(context: DeploymentContext): Promise<void>;\n\n // Returns the CI configuration for this provider\n getCIConfig(repoType: 'github' | 'gitlab'): CIConfig;\n\n // Returns a map of secrets to be set in the repository (e.g. tokens, account IDs)\n // The provider is responsible for resolving these from config/env and throwing if missing.\n getSecrets(context: DeploymentContext): Promise<Record<string, string>>;\n\n // Returns a map of variables to be set in the repository (e.g. project names, service names)\n getVariables(context: DeploymentContext): Promise<Record<string, string>>;\n}\n\nexport interface RepositoryProvider {\n name: string;\n\n // Sets secrets/variables in the repo\n configureSecrets(context: DeploymentContext, secrets: Record<string, string>): Promise<void>;\n configureVariables(context: DeploymentContext, variables: Record<string, string>): Promise<void>;\n\n // Generates and writes the CI workflow files\n generateWorkflow(context: DeploymentContext, targets: DeploymentProvider[]): Promise<void>;\n}\n\nexport interface NexicalConfig {\n deploy?: {\n backend?: {\n provider: string;\n projectName?: string;\n options?: Record<string, unknown>;\n };\n frontend?: {\n provider: string;\n projectName?: string;\n options?: Record<string, unknown>;\n };\n repository?: {\n provider: string;\n options?: Record<string, unknown>;\n };\n };\n}\n"],"mappings":";;;;;;AAAA;","names":[]}
@@ -0,0 +1,6 @@
1
+ import { exec } from 'node:child_process';
2
+
3
+ declare const execAsync: typeof exec.__promisify__;
4
+ declare function checkCommand(command: string): Promise<boolean>;
5
+
6
+ export { checkCommand, execAsync };
@@ -0,0 +1,11 @@
1
+ import { createRequire } from "module"; const require = createRequire(import.meta.url);
2
+ import {
3
+ checkCommand,
4
+ execAsync
5
+ } from "../../chunk-2JW5BYZW.js";
6
+ import "../../chunk-OYFWMYPG.js";
7
+ export {
8
+ checkCommand,
9
+ execAsync
10
+ };
11
+ //# sourceMappingURL=utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nexical/cli",
3
- "version": "0.11.3",
3
+ "version": "0.11.5",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "nexical": "./dist/index.js"
@@ -29,30 +29,32 @@
29
29
  },
30
30
  "dependencies": {
31
31
  "@nexical/cli-core": "^0.1.12",
32
- "yaml": "^2.3.4",
33
- "fast-glob": "^3.3.3"
32
+ "dotenv": "^17.3.1",
33
+ "fast-glob": "^3.3.3",
34
+ "jiti": "^2.6.1",
35
+ "yaml": "^2.3.4"
34
36
  },
35
37
  "devDependencies": {
38
+ "@eslint/js": "^9.39.2",
36
39
  "@types/fs-extra": "^11.0.4",
37
40
  "@types/node": "^20.10.0",
38
41
  "@vitest/coverage-v8": "^4.0.15",
39
- "execa": "^9.6.1",
40
- "fs-extra": "^11.3.2",
41
- "tsup": "^8.0.1",
42
- "tsx": "^4.21.0",
43
- "typescript": "^5.3.3",
44
- "vitest": "^4.0.15",
45
- "@eslint/js": "^9.39.2",
46
42
  "eslint": "^9.39.2",
47
43
  "eslint-config-prettier": "^10.1.8",
48
44
  "eslint-plugin-astro": "^1.5.0",
49
45
  "eslint-plugin-jsx-a11y": "^6.10.2",
50
46
  "eslint-plugin-react": "^7.37.5",
51
47
  "eslint-plugin-react-hooks": "^7.0.1",
48
+ "execa": "^9.6.1",
49
+ "fs-extra": "^11.3.2",
52
50
  "globals": "^17.2.0",
53
51
  "husky": "^9.1.7",
54
52
  "lint-staged": "^16.2.7",
55
53
  "prettier": "^3.8.1",
56
- "typescript-eslint": "^8.54.0"
54
+ "tsup": "^8.0.1",
55
+ "tsx": "^4.21.0",
56
+ "typescript": "^5.3.3",
57
+ "typescript-eslint": "^8.54.0",
58
+ "vitest": "^4.0.15"
57
59
  }
58
60
  }
@@ -1,186 +1,170 @@
1
- import { BaseCommand, runCommand } from '@nexical/cli-core';
2
- import { exec } from 'node:child_process';
3
- import { promisify } from 'node:util';
1
+ import path from 'node:path';
2
+ import dotenv from 'dotenv';
3
+ import { BaseCommand } from '@nexical/cli-core';
4
+ import { ConfigManager } from '../deploy/config-manager';
5
+ import { ProviderRegistry } from '../deploy/registry';
6
+ import { DeploymentContext } from '../deploy/types';
4
7
 
5
- const execAsync = promisify(exec);
8
+ export default class DeployCommand extends BaseCommand {
9
+ static description = `Deploy the application based on nexical.yaml configuration.
6
10
 
7
- interface DeployOptions {
8
- dryRun: boolean;
9
- railwayToken?: string;
10
- cloudflareToken?: string;
11
- cloudflareAccount?: string;
12
- }
11
+ This command orchestrates the deployment of your frontend and backend applications
12
+ by interacting with the providers specified in your configuration file.
13
13
 
14
- export default class DeployCommand extends BaseCommand {
15
- static description = 'Deploy the application to Railway and Cloudflare.';
14
+ CONFIGURATION:
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.
18
+ - Supports loading environment variables from a .env file in the project root.
19
+
20
+ PROVIDERS:
21
+ - Backend: Railway, etc.
22
+ - Frontend: Cloudflare Pages, etc.
23
+ - Repository: GitHub, GitLab, etc.
24
+
25
+ PROCESS:
26
+ 1. Loads environment variables from '.env'.
27
+ 2. Loads configuration from 'nexical.yaml'.
28
+ 3. Provisions resources via the selected providers.
29
+ 4. Configures the repository (secrets/variables) for CI/CD.
30
+ 5. Generates CI/CD workflow files.`;
16
31
 
17
32
  static args = {
18
33
  options: [
19
34
  {
20
- name: '--dry-run',
21
- description: 'Simulate the deployment process without making changes.',
22
- default: false,
35
+ name: '--backend <provider>',
36
+ description: 'Override backend provider',
23
37
  },
24
38
  {
25
- name: '--railway-token <token>',
26
- description: 'Railway Project Token (optional if already logged in).',
39
+ name: '--frontend <provider>',
40
+ description: 'Override frontend provider',
27
41
  },
28
42
  {
29
- name: '--cloudflare-token <token>',
30
- description: 'Cloudflare API Token.',
43
+ name: '--repo <provider>',
44
+ description: 'Override repositroy provider',
31
45
  },
32
46
  {
33
- name: '--cloudflare-account <id>',
34
- description: 'Cloudflare Account ID.',
47
+ name: '--dry-run',
48
+ description: 'Simulate the deployment process',
49
+ default: false,
35
50
  },
36
51
  ],
37
52
  };
38
53
 
39
- async run(options: DeployOptions) {
40
- this.info('Starting Nexical Deployment Automation...');
54
+ async run(options: Record<string, unknown>) {
55
+ this.info('Starting Nexical Deployment...');
41
56
 
42
- if (options.dryRun) {
43
- this.notice('DRY RUN MODE ENABLED');
44
- }
57
+ // Load environment variables from .env
58
+ dotenv.config({ path: path.join(process.cwd(), '.env') });
45
59
 
46
- try {
47
- // 1. Railway Setup
48
- await this.setupRailway(options);
49
-
50
- // 2. Cloudflare Setup
51
- await this.setupCloudflare(options);
52
-
53
- // 3. GitHub Secrets Setup
54
- await this.setupGitHubSecrets(options);
55
-
56
- this.success('Deployment setup complete! Your application is being deployed.');
57
- } catch (error: unknown) {
58
- if (error instanceof Error) {
59
- this.error(`Deployment failed: ${error.message}`);
60
- } else {
61
- this.error(`Deployment failed: ${String(error)}`);
62
- }
63
- process.exit(1);
64
- }
65
- }
60
+ const configManager = new ConfigManager(process.cwd());
61
+ const config = await configManager.load();
62
+ const registry = new ProviderRegistry();
66
63
 
67
- private async setupRailway(options: DeployOptions) {
68
- this.info('Configuring Railway...');
64
+ // Register core and local providers
65
+ await registry.loadCoreProviders();
66
+ await registry.loadLocalProviders(process.cwd());
69
67
 
70
- if (options.dryRun) {
71
- this.info('[Dry Run] Would run: railway init');
72
- this.info('[Dry Run] Would run: railway add --database postgres');
73
- return;
68
+ // Resolve providers (CLI flags > Config > Error)
69
+ const backendProviderName =
70
+ (options.backend as string | undefined) || config.deploy?.backend?.provider;
71
+ if (!backendProviderName) {
72
+ this.error(
73
+ "Backend provider not specified. Use --backend flag or configure 'deploy.backend.provider' in nexical.yaml.",
74
+ );
74
75
  }
75
76
 
76
- try {
77
- // Check if railway project exists or init
78
- // Note: railway init might be interactive, so we might need to handle that or assume user has linked.
79
- // For now, let's assume we use 'railway link' if they passed a token or have it set.
80
-
81
- this.info('Ensuring Railway project is linked...');
82
- // If they provided a token, we should probably set it in the environment for subsequent calls
83
- if (options.railwayToken) {
84
- process.env.RAILWAY_TOKEN = options.railwayToken;
85
- }
86
-
87
- // Check if we are in a railway project
88
- try {
89
- await runCommand('railway status');
90
- } catch {
91
- this.info('No Railway project detected. Initializing...');
92
- await runCommand('railway init');
93
- }
94
-
95
- this.info('Adding PostgreSQL service if missing...');
96
- // railway add --database postgres is usually safe to run twice but we should check status
97
- const { stdout: status } = await execAsync('railway status');
98
- if (!status.includes('postgres')) {
99
- await runCommand('railway add --database postgres');
100
- }
101
- } catch (e: unknown) {
102
- this.warn(
103
- 'Railway setup encountered an issue. Ensure you are logged in with `railway login`.',
77
+ const frontendProviderName =
78
+ (options.frontend as string | undefined) || config.deploy?.frontend?.provider;
79
+ if (!frontendProviderName) {
80
+ this.error(
81
+ "Frontend provider not specified. Use --frontend flag or configure 'deploy.frontend.provider' in nexical.yaml.",
104
82
  );
105
- throw e;
106
83
  }
107
- }
108
-
109
- private async setupCloudflare(options: DeployOptions) {
110
- this.info('Configuring Cloudflare Pages...');
111
84
 
112
- if (options.dryRun) {
113
- this.info('[Dry Run] Would run: wrangler pages project create nexical-frontend');
114
- return;
85
+ const repoProviderName =
86
+ (options.repo as string | undefined) || config.deploy?.repository?.provider;
87
+ if (!repoProviderName) {
88
+ this.error(
89
+ "Repository provider not specified. Use --repo flag or configure 'deploy.repository.provider' in nexical.yaml.",
90
+ );
115
91
  }
116
92
 
117
- if (!options.cloudflareToken || !options.cloudflareAccount) {
118
- this.warn('Cloudflare credentials missing. Skipping automated Cloudflare setup.');
119
- this.info('You can manually set up Cloudflare Pages and add the secrets to GitHub.');
120
- return;
93
+ const backendProvider = registry.getDeploymentProvider(backendProviderName!);
94
+ const frontendProvider = registry.getDeploymentProvider(frontendProviderName!);
95
+ const repoProvider = registry.getRepositoryProvider(repoProviderName!);
96
+
97
+ if (!backendProvider) throw new Error(`Backend provider '${backendProviderName}' not found.`);
98
+ if (!frontendProvider)
99
+ throw new Error(`Frontend provider '${frontendProviderName}' not found.`);
100
+ if (!repoProvider) throw new Error(`Repository provider '${repoProviderName}' not found.`);
101
+
102
+ const context: DeploymentContext = {
103
+ cwd: process.cwd(),
104
+ config,
105
+ options,
106
+ };
107
+
108
+ // Provision
109
+ this.info(`Provisioning Backend with ${backendProvider.name}...`);
110
+ await backendProvider.provision(context);
111
+
112
+ this.info(`Provisioning Frontend with ${frontendProvider.name}...`);
113
+ await frontendProvider.provision(context);
114
+
115
+ // Configure Repo
116
+ this.info(`Configuring Repository with ${repoProvider.name}...`);
117
+
118
+ const secrets: Record<string, string> = {};
119
+
120
+ // Collect secrets from Backend Provider
121
+ this.info(`Resolving secrets from ${backendProvider.name}...`);
122
+ try {
123
+ const backendSecrets = await backendProvider.getSecrets(context);
124
+ Object.assign(secrets, backendSecrets);
125
+ } catch (e: unknown) {
126
+ const message = e instanceof Error ? e.message : String(e);
127
+ this.error(`Failed to resolve secrets for ${backendProvider.name}: ${message}`);
121
128
  }
122
129
 
130
+ // Collect secrets from Frontend Provider
131
+ this.info(`Resolving secrets from ${frontendProvider.name}...`);
123
132
  try {
124
- // Use wrangler to create project if it doesn't exist
125
- // We assume project name 'nexical-frontend' for now, should be configurable.
126
- const projectName = 'nexical-frontend';
127
- this.info(`Ensuring Cloudflare Pages project "${projectName}" exists...`);
128
-
129
- try {
130
- await execAsync(`wrangler pages project create ${projectName} --production-branch main`, {
131
- env: {
132
- ...process.env,
133
- CLOUDFLARE_API_TOKEN: options.cloudflareToken,
134
- CLOUDFLARE_ACCOUNT_ID: options.cloudflareAccount,
135
- },
136
- });
137
- } catch {
138
- this.info('Cloudflare project might already exist.');
139
- }
133
+ const frontendSecrets = await frontendProvider.getSecrets(context);
134
+ Object.assign(secrets, frontendSecrets);
140
135
  } catch (e: unknown) {
141
- this.warn('Cloudflare setup failed.');
142
- throw e;
136
+ const message = e instanceof Error ? e.message : String(e);
137
+ this.error(`Failed to resolve secrets for ${frontendProvider.name}: ${message}`);
143
138
  }
144
- }
145
139
 
146
- private async setupGitHubSecrets(options: DeployOptions) {
147
- this.info('Configuring GitHub Secrets...');
140
+ await repoProvider.configureSecrets(context, secrets);
141
+
142
+ const variables: Record<string, string> = {};
148
143
 
149
- if (options.dryRun) {
150
- this.info('[Dry Run] Would run: gh secret set RAILWAY_TOKEN');
151
- this.info('[Dry Run] Would run: gh secret set CLOUDFLARE_API_TOKEN');
152
- this.info('[Dry Run] Would run: gh secret set CLOUDFLARE_ACCOUNT_ID');
153
- return;
144
+ // Collect variables from Backend Provider
145
+ try {
146
+ const backendVars = await backendProvider.getVariables(context);
147
+ Object.assign(variables, backendVars);
148
+ } catch (e: unknown) {
149
+ const message = e instanceof Error ? e.message : String(e);
150
+ this.error(`Failed to resolve variables for ${backendProvider.name}: ${message}`);
154
151
  }
155
152
 
153
+ // Collect variables from Frontend Provider
156
154
  try {
157
- // We need the Railway Project Token.
158
- // User might have provided it, or we try to get it from railway tokens?
159
- // Railway CLI doesn't easily expose the project token via CLI easily without a lot of parsing.
160
- // Usually users generate it in the UI.
161
- // If they provided it via --railway-token, we use it.
162
-
163
- if (options.railwayToken) {
164
- this.info('Setting RAILWAY_TOKEN in GitHub...');
165
- await runCommand(`gh secret set RAILWAY_TOKEN --body "${options.railwayToken}"`);
166
- }
167
-
168
- if (options.cloudflareToken) {
169
- this.info('Setting CLOUDFLARE_API_TOKEN in GitHub...');
170
- await runCommand(`gh secret set CLOUDFLARE_API_TOKEN --body "${options.cloudflareToken}"`);
171
- }
172
-
173
- if (options.cloudflareAccount) {
174
- this.info('Setting CLOUDFLARE_ACCOUNT_ID in GitHub...');
175
- await runCommand(
176
- `gh secret set CLOUDFLARE_ACCOUNT_ID --body "${options.cloudflareAccount}"`,
177
- );
178
- }
155
+ const frontendVars = await frontendProvider.getVariables(context);
156
+ Object.assign(variables, frontendVars);
179
157
  } catch (e: unknown) {
180
- this.warn(
181
- 'GitHub Secrets setup failed. Ensure you have the GitHub CLI (gh) installed and are logged in.',
182
- );
183
- throw e;
158
+ const message = e instanceof Error ? e.message : String(e);
159
+ this.error(`Failed to resolve variables for ${frontendProvider.name}: ${message}`);
184
160
  }
161
+
162
+ await repoProvider.configureVariables(context, variables);
163
+
164
+ // Generate Workflows
165
+ this.info('Generating CI/CD Workflows...');
166
+ await repoProvider.generateWorkflow(context, [backendProvider, frontendProvider]);
167
+
168
+ this.success('Deployment configuration complete!');
185
169
  }
186
170
  }
@@ -0,0 +1,41 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import YAML from 'yaml';
4
+ import { NexicalConfig } from './types';
5
+
6
+ export class ConfigManager {
7
+ private configPath: string;
8
+
9
+ constructor(cwd: string) {
10
+ this.configPath = path.join(cwd, 'nexical.yaml');
11
+ }
12
+
13
+ async load(): Promise<NexicalConfig> {
14
+ try {
15
+ const content = await fs.readFile(this.configPath, 'utf-8');
16
+ return YAML.parse(content) as NexicalConfig;
17
+ } catch (error: unknown) {
18
+ if (
19
+ error &&
20
+ typeof error === 'object' &&
21
+ 'code' in error &&
22
+ (error as { code: unknown }).code === 'ENOENT'
23
+ ) {
24
+ return {};
25
+ }
26
+ throw error;
27
+ }
28
+ }
29
+
30
+ async save(config: NexicalConfig): Promise<void> {
31
+ const content = YAML.stringify(config);
32
+ await fs.writeFile(this.configPath, content, 'utf-8');
33
+ }
34
+
35
+ exists(): Promise<boolean> {
36
+ return fs
37
+ .access(this.configPath)
38
+ .then(() => true)
39
+ .catch(() => false);
40
+ }
41
+ }
@@ -0,0 +1,143 @@
1
+ import { logger } from '@nexical/cli-core';
2
+ import { DeploymentProvider, DeploymentContext, CIConfig } from '../types';
3
+ import { execAsync } from '../utils';
4
+
5
+ export class CloudflareProvider implements DeploymentProvider {
6
+ name = 'cloudflare';
7
+ type = 'frontend' as const;
8
+
9
+ async provision(context: DeploymentContext): Promise<void> {
10
+ const projectName = context.config.deploy?.frontend?.projectName;
11
+
12
+ if (!projectName) {
13
+ throw new Error(
14
+ "Cloudflare project name not found in nexical.yaml. Please configure 'deploy.frontend.projectName'.",
15
+ );
16
+ }
17
+
18
+ const options = context.config.deploy?.frontend?.options || {};
19
+
20
+ // Resolve credentials:
21
+ // 1. CLI flag (options)
22
+ // 2. Env var defined in config (options.apiTokenEnvVar)
23
+ // 3. Default env var (CLOUDFLARE_API_TOKEN)
24
+ const apiTokenEnvVar =
25
+ typeof options.apiTokenEnvVar === 'string' ? options.apiTokenEnvVar : undefined;
26
+ const apiToken =
27
+ (typeof context.options.cloudflareToken === 'string'
28
+ ? context.options.cloudflareToken
29
+ : undefined) ||
30
+ (apiTokenEnvVar ? process.env[apiTokenEnvVar] : undefined) ||
31
+ process.env.CLOUDFLARE_API_TOKEN;
32
+
33
+ const accountIdEnvVar =
34
+ typeof options.accountIdEnvVar === 'string' ? options.accountIdEnvVar : undefined;
35
+ const accountId =
36
+ (typeof context.options.cloudflareAccount === 'string'
37
+ ? context.options.cloudflareAccount
38
+ : undefined) ||
39
+ (accountIdEnvVar ? process.env[accountIdEnvVar] : undefined) ||
40
+ process.env.CLOUDFLARE_ACCOUNT_ID;
41
+
42
+ logger.info('Configuring Cloudflare Pages...');
43
+
44
+ if (context.options.dryRun) {
45
+ logger.info('[Dry Run] Would check Cloudflare Pages project and create if missing.');
46
+ return;
47
+ }
48
+
49
+ if (!apiToken || !accountId) {
50
+ logger.warn('Cloudflare credentials missing. Skipping automated Cloudflare setup.');
51
+ logger.info('You can manually set up Cloudflare Pages and add the secrets to GitHub.');
52
+ return;
53
+ }
54
+
55
+ try {
56
+ logger.info(`Ensuring Cloudflare Pages project "${projectName}" exists...`);
57
+ try {
58
+ await execAsync(`wrangler pages project create ${projectName} --production-branch main`, {
59
+ env: {
60
+ ...process.env,
61
+ CLOUDFLARE_API_TOKEN: apiToken,
62
+ CLOUDFLARE_ACCOUNT_ID: accountId,
63
+ },
64
+ });
65
+ } catch {
66
+ logger.info('Cloudflare project might already exist.');
67
+ }
68
+ } catch (e: unknown) {
69
+ logger.warn('Cloudflare setup failed.');
70
+ throw e;
71
+ }
72
+ }
73
+
74
+ async getSecrets(context: DeploymentContext): Promise<Record<string, string>> {
75
+ const options = context.config.deploy?.frontend?.options || {};
76
+ const secrets: Record<string, string> = {};
77
+
78
+ // Resolve API Token
79
+ const apiTokenEnvVar =
80
+ typeof options.apiTokenEnvVar === 'string' ? options.apiTokenEnvVar : undefined;
81
+ const apiToken =
82
+ (apiTokenEnvVar ? process.env[apiTokenEnvVar] : undefined) ||
83
+ process.env.CLOUDFLARE_API_TOKEN;
84
+
85
+ if (!apiToken) {
86
+ throw new Error(
87
+ `Cloudflare API Token not found. Please provide it via:\n` +
88
+ `1. Configuring 'deploy.frontend.options.apiTokenEnvVar' in nexical.yaml and setting that env var in .env\n` +
89
+ `2. Setting CLOUDFLARE_API_TOKEN in .env`,
90
+ );
91
+ }
92
+ secrets['CLOUDFLARE_API_TOKEN'] = apiToken;
93
+
94
+ // Resolve Account ID
95
+ const accountIdEnvVar =
96
+ typeof options.accountIdEnvVar === 'string' ? options.accountIdEnvVar : undefined;
97
+ const accountId =
98
+ (accountIdEnvVar ? process.env[accountIdEnvVar] : undefined) ||
99
+ process.env.CLOUDFLARE_ACCOUNT_ID;
100
+
101
+ if (!accountId) {
102
+ throw new Error(
103
+ `Cloudflare Account ID not found. Please provide it via:\n` +
104
+ `1. Configuring 'deploy.frontend.options.accountIdEnvVar' in nexical.yaml and setting that env var in .env\n` +
105
+ `2. Setting CLOUDFLARE_ACCOUNT_ID in .env`,
106
+ );
107
+ }
108
+ secrets['CLOUDFLARE_ACCOUNT_ID'] = accountId;
109
+
110
+ return secrets;
111
+ }
112
+
113
+ async getVariables(context: DeploymentContext): Promise<Record<string, string>> {
114
+ const projectName = context.config.deploy?.frontend?.projectName;
115
+
116
+ if (!projectName) {
117
+ throw new Error(
118
+ "Cloudflare project name not found in nexical.yaml. Please configure 'deploy.frontend.projectName'.",
119
+ );
120
+ }
121
+ return {
122
+ CLOUDFLARE_PROJECT_NAME: projectName,
123
+ };
124
+ }
125
+
126
+ getCIConfig(): CIConfig {
127
+ return {
128
+ secrets: ['CLOUDFLARE_API_TOKEN', 'CLOUDFLARE_ACCOUNT_ID'],
129
+ variables: ['CLOUDFLARE_PROJECT_NAME'],
130
+ deploySteps: [], // Handled by action
131
+ githubActionStep: {
132
+ name: 'Deploy to Cloudflare Pages',
133
+ uses: 'cloudflare/wrangler-action@v3',
134
+ with: {
135
+ apiToken: '${{ secrets.CLOUDFLARE_API_TOKEN }}',
136
+ accountId: '${{ secrets.CLOUDFLARE_ACCOUNT_ID }}',
137
+ command: 'pages deploy dist --project-name=${{ vars.CLOUDFLARE_PROJECT_NAME }}',
138
+ workingDirectory: 'apps/frontend',
139
+ },
140
+ },
141
+ };
142
+ }
143
+ }