@nexical/cli 0.11.5 → 0.11.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/src/commands/deploy.d.ts +4 -0
- package/dist/src/commands/deploy.js +5 -0
- package/dist/src/commands/deploy.js.map +1 -1
- package/dist/src/deploy/providers/cloudflare.js +11 -5
- package/dist/src/deploy/providers/cloudflare.js.map +1 -1
- package/dist/src/deploy/providers/railway.d.ts +1 -0
- package/dist/src/deploy/providers/railway.js +60 -33
- package/dist/src/deploy/providers/railway.js.map +1 -1
- package/package.json +1 -1
- package/src/commands/deploy.ts +5 -0
- package/src/deploy/providers/cloudflare.ts +13 -5
- package/src/deploy/providers/railway.ts +88 -45
package/dist/index.js
CHANGED
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../index.ts","../package.json"],"sourcesContent":["#!/usr/bin/env node\nimport { CLI, findProjectRoot } from '@nexical/cli-core';\nimport { fileURLToPath } from 'node:url';\nimport { discoverCommandDirectories } from './src/utils/discovery.js';\nimport pkg from './package.json';\nimport path from 'node:path';\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\nconst commandName = 'nexical';\nconst projectRoot = (await findProjectRoot(commandName, process.cwd())) || process.cwd();\nconst coreCommandsDir = path.resolve(__dirname, './src/commands');\nconst additionalCommands = discoverCommandDirectories(projectRoot);\n\n// Filter out duplicate core commands and source versions\nconst filteredAdditional = additionalCommands.filter((dir) => {\n const resolvedDir = path.resolve(dir);\n const resolvedCore = path.resolve(coreCommandsDir);\n\n if (resolvedDir === resolvedCore) return false;\n\n // Check if this is another instance of the core CLI commands (by checking path suffix)\n const coreSuffix = path.join('@nexical', 'cli', 'dist', 'src', 'commands');\n const coreSuffixSrc = path.join('packages', 'cli', 'dist', 'src', 'commands');\n const coreSuffixRawSrc = path.join('packages', 'cli', 'src', 'commands');\n\n if (\n resolvedDir.endsWith(coreSuffix) ||\n resolvedDir.endsWith(coreSuffixSrc) ||\n resolvedDir.endsWith(coreSuffixRawSrc)\n ) {\n return false;\n }\n\n // Handle mismatch between dist/src and src/\n if (resolvedCore.includes(path.join(path.sep, 'dist', 'src', 'commands'))) {\n const srcVersion = resolvedCore.replace(\n path.join(path.sep, 'dist', 'src', 'commands'),\n path.join(path.sep, 'src', 'commands'),\n );\n if (resolvedDir === srcVersion) return false;\n }\n\n return true;\n});\n\nconst app = new CLI({\n version: pkg.version,\n commandName: commandName,\n searchDirectories: [...new Set([coreCommandsDir, ...filteredAdditional])],\n});\napp.start();\n","{\n \"name\": \"@nexical/cli\",\n \"version\": \"0.11.
|
|
1
|
+
{"version":3,"sources":["../index.ts","../package.json"],"sourcesContent":["#!/usr/bin/env node\nimport { CLI, findProjectRoot } from '@nexical/cli-core';\nimport { fileURLToPath } from 'node:url';\nimport { discoverCommandDirectories } from './src/utils/discovery.js';\nimport pkg from './package.json';\nimport path from 'node:path';\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\nconst commandName = 'nexical';\nconst projectRoot = (await findProjectRoot(commandName, process.cwd())) || process.cwd();\nconst coreCommandsDir = path.resolve(__dirname, './src/commands');\nconst additionalCommands = discoverCommandDirectories(projectRoot);\n\n// Filter out duplicate core commands and source versions\nconst filteredAdditional = additionalCommands.filter((dir) => {\n const resolvedDir = path.resolve(dir);\n const resolvedCore = path.resolve(coreCommandsDir);\n\n if (resolvedDir === resolvedCore) return false;\n\n // Check if this is another instance of the core CLI commands (by checking path suffix)\n const coreSuffix = path.join('@nexical', 'cli', 'dist', 'src', 'commands');\n const coreSuffixSrc = path.join('packages', 'cli', 'dist', 'src', 'commands');\n const coreSuffixRawSrc = path.join('packages', 'cli', 'src', 'commands');\n\n if (\n resolvedDir.endsWith(coreSuffix) ||\n resolvedDir.endsWith(coreSuffixSrc) ||\n resolvedDir.endsWith(coreSuffixRawSrc)\n ) {\n return false;\n }\n\n // Handle mismatch between dist/src and src/\n if (resolvedCore.includes(path.join(path.sep, 'dist', 'src', 'commands'))) {\n const srcVersion = resolvedCore.replace(\n path.join(path.sep, 'dist', 'src', 'commands'),\n path.join(path.sep, 'src', 'commands'),\n );\n if (resolvedDir === srcVersion) return false;\n }\n\n return true;\n});\n\nconst app = new CLI({\n version: pkg.version,\n commandName: commandName,\n searchDirectories: [...new Set([coreCommandsDir, ...filteredAdditional])],\n});\napp.start();\n","{\n \"name\": \"@nexical/cli\",\n \"version\": \"0.11.7\",\n \"type\": \"module\",\n \"bin\": {\n \"nexical\": \"./dist/index.js\"\n },\n \"scripts\": {\n \"build\": \"tsup\",\n \"dev\": \"tsup --watch\",\n \"start\": \"node dist/index.js\",\n \"test\": \"npm run test:unit && npm run test:integration && npm run test:e2e\",\n \"test:unit\": \"vitest run --config vitest.config.ts --coverage\",\n \"test:integration\": \"vitest run --config vitest.integration.config.ts\",\n \"test:e2e\": \"npm run build && vitest run --config vitest.e2e.config.ts\",\n \"test:watch\": \"vitest\",\n \"format\": \"prettier --write .\",\n \"lint\": \"eslint .\",\n \"lint:fix\": \"eslint . --fix\",\n \"prepare\": \"husky\"\n },\n \"lint-staged\": {\n \"**/*\": [\n \"prettier --write --ignore-unknown\"\n ],\n \"**/*.{js,jsx,ts,tsx,astro}\": [\n \"eslint --fix\"\n ]\n },\n \"dependencies\": {\n \"@nexical/cli-core\": \"^0.1.12\",\n \"dotenv\": \"^17.3.1\",\n \"fast-glob\": \"^3.3.3\",\n \"jiti\": \"^2.6.1\",\n \"yaml\": \"^2.3.4\"\n },\n \"devDependencies\": {\n \"@eslint/js\": \"^9.39.2\",\n \"@types/fs-extra\": \"^11.0.4\",\n \"@types/node\": \"^20.10.0\",\n \"@vitest/coverage-v8\": \"^4.0.15\",\n \"eslint\": \"^9.39.2\",\n \"eslint-config-prettier\": \"^10.1.8\",\n \"eslint-plugin-astro\": \"^1.5.0\",\n \"eslint-plugin-jsx-a11y\": \"^6.10.2\",\n \"eslint-plugin-react\": \"^7.37.5\",\n \"eslint-plugin-react-hooks\": \"^7.0.1\",\n \"execa\": \"^9.6.1\",\n \"fs-extra\": \"^11.3.2\",\n \"globals\": \"^17.2.0\",\n \"husky\": \"^9.1.7\",\n \"lint-staged\": \"^16.2.7\",\n \"prettier\": \"^3.8.1\",\n \"tsup\": \"^8.0.1\",\n \"tsx\": \"^4.21.0\",\n \"typescript\": \"^5.3.3\",\n \"typescript-eslint\": \"^8.54.0\",\n \"vitest\": \"^4.0.15\"\n }\n}\n"],"mappings":";;;;;;;;;;AAAA;AACA,SAAS,KAAK,uBAAuB;AACrC,SAAS,qBAAqB;;;ACF9B;AAAA,EACE,MAAQ;AAAA,EACR,SAAW;AAAA,EACX,MAAQ;AAAA,EACR,KAAO;AAAA,IACL,SAAW;AAAA,EACb;AAAA,EACA,SAAW;AAAA,IACT,OAAS;AAAA,IACT,KAAO;AAAA,IACP,OAAS;AAAA,IACT,MAAQ;AAAA,IACR,aAAa;AAAA,IACb,oBAAoB;AAAA,IACpB,YAAY;AAAA,IACZ,cAAc;AAAA,IACd,QAAU;AAAA,IACV,MAAQ;AAAA,IACR,YAAY;AAAA,IACZ,SAAW;AAAA,EACb;AAAA,EACA,eAAe;AAAA,IACb,QAAQ;AAAA,MACN;AAAA,IACF;AAAA,IACA,8BAA8B;AAAA,MAC5B;AAAA,IACF;AAAA,EACF;AAAA,EACA,cAAgB;AAAA,IACd,qBAAqB;AAAA,IACrB,QAAU;AAAA,IACV,aAAa;AAAA,IACb,MAAQ;AAAA,IACR,MAAQ;AAAA,EACV;AAAA,EACA,iBAAmB;AAAA,IACjB,cAAc;AAAA,IACd,mBAAmB;AAAA,IACnB,eAAe;AAAA,IACf,uBAAuB;AAAA,IACvB,QAAU;AAAA,IACV,0BAA0B;AAAA,IAC1B,uBAAuB;AAAA,IACvB,0BAA0B;AAAA,IAC1B,uBAAuB;AAAA,IACvB,6BAA6B;AAAA,IAC7B,OAAS;AAAA,IACT,YAAY;AAAA,IACZ,SAAW;AAAA,IACX,OAAS;AAAA,IACT,eAAe;AAAA,IACf,UAAY;AAAA,IACZ,MAAQ;AAAA,IACR,KAAO;AAAA,IACP,YAAc;AAAA,IACd,qBAAqB;AAAA,IACrB,QAAU;AAAA,EACZ;AACF;;;ADtDA,OAAO,UAAU;AAEjB,IAAM,YAAY,KAAK,QAAQ,cAAc,YAAY,GAAG,CAAC;AAE7D,IAAM,cAAc;AACpB,IAAM,cAAe,MAAM,gBAAgB,aAAa,QAAQ,IAAI,CAAC,KAAM,QAAQ,IAAI;AACvF,IAAM,kBAAkB,KAAK,QAAQ,WAAW,gBAAgB;AAChE,IAAM,qBAAqB,2BAA2B,WAAW;AAGjE,IAAM,qBAAqB,mBAAmB,OAAO,CAAC,QAAQ;AAC5D,QAAM,cAAc,KAAK,QAAQ,GAAG;AACpC,QAAM,eAAe,KAAK,QAAQ,eAAe;AAEjD,MAAI,gBAAgB,aAAc,QAAO;AAGzC,QAAM,aAAa,KAAK,KAAK,YAAY,OAAO,QAAQ,OAAO,UAAU;AACzE,QAAM,gBAAgB,KAAK,KAAK,YAAY,OAAO,QAAQ,OAAO,UAAU;AAC5E,QAAM,mBAAmB,KAAK,KAAK,YAAY,OAAO,OAAO,UAAU;AAEvE,MACE,YAAY,SAAS,UAAU,KAC/B,YAAY,SAAS,aAAa,KAClC,YAAY,SAAS,gBAAgB,GACrC;AACA,WAAO;AAAA,EACT;AAGA,MAAI,aAAa,SAAS,KAAK,KAAK,KAAK,KAAK,QAAQ,OAAO,UAAU,CAAC,GAAG;AACzE,UAAM,aAAa,aAAa;AAAA,MAC9B,KAAK,KAAK,KAAK,KAAK,QAAQ,OAAO,UAAU;AAAA,MAC7C,KAAK,KAAK,KAAK,KAAK,OAAO,UAAU;AAAA,IACvC;AACA,QAAI,gBAAgB,WAAY,QAAO;AAAA,EACzC;AAEA,SAAO;AACT,CAAC;AAED,IAAM,MAAM,IAAI,IAAI;AAAA,EAClB,SAAS,gBAAI;AAAA,EACb;AAAA,EACA,mBAAmB,CAAC,GAAG,oBAAI,IAAI,CAAC,iBAAiB,GAAG,kBAAkB,CAAC,CAAC;AAC1E,CAAC;AACD,IAAI,MAAM;","names":[]}
|
|
@@ -51,6 +51,11 @@ PROCESS:
|
|
|
51
51
|
name: "--repo <provider>",
|
|
52
52
|
description: "Override repositroy provider"
|
|
53
53
|
},
|
|
54
|
+
{
|
|
55
|
+
name: "--env <environment>",
|
|
56
|
+
description: "Deployment environment (e.g. production, staging)",
|
|
57
|
+
default: "production"
|
|
58
|
+
},
|
|
54
59
|
{
|
|
55
60
|
name: "--dry-run",
|
|
56
61
|
description: "Simulate the deployment process",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/commands/deploy.ts"],"sourcesContent":["import path from 'node:path';\nimport dotenv from 'dotenv';\nimport { BaseCommand } from '@nexical/cli-core';\nimport { ConfigManager } from '../deploy/config-manager';\nimport { ProviderRegistry } from '../deploy/registry';\nimport { DeploymentContext } from '../deploy/types';\n\nexport default class DeployCommand extends BaseCommand {\n static description = `Deploy the application based on nexical.yaml configuration.\n\nThis command orchestrates the deployment of your frontend and backend applications \nby interacting with the providers specified in your configuration file.\n\nCONFIGURATION:\n- Requires a 'nexical.yaml' file in the project root.\n- If the file or specific sections are missing, the CLI will prompt you to run an interactive setup \n and save the configuration for future uses.\n- Supports loading environment variables from a .env file in the project root.\n\nPROVIDERS:\n- Backend: Railway, etc.\n- Frontend: Cloudflare Pages, etc.\n- Repository: GitHub, GitLab, etc.\n\nPROCESS:\n1. Loads environment variables from '.env'.\n2. Loads configuration from 'nexical.yaml'.\n3. Provisions resources via the selected providers.\n4. Configures the repository (secrets/variables) for CI/CD.\n5. Generates CI/CD workflow files.`;\n\n static args = {\n options: [\n {\n name: '--backend <provider>',\n description: 'Override backend provider',\n },\n {\n name: '--frontend <provider>',\n description: 'Override frontend provider',\n },\n {\n name: '--repo <provider>',\n description: 'Override repositroy provider',\n },\n {\n name: '--dry-run',\n description: 'Simulate the deployment process',\n default: false,\n },\n ],\n };\n\n async run(options: Record<string, unknown>) {\n this.info('Starting Nexical Deployment...');\n\n // Load environment variables from .env\n dotenv.config({ path: path.join(process.cwd(), '.env') });\n\n const configManager = new ConfigManager(process.cwd());\n const config = await configManager.load();\n const registry = new ProviderRegistry();\n\n // Register core and local providers\n await registry.loadCoreProviders();\n await registry.loadLocalProviders(process.cwd());\n\n // Resolve providers (CLI flags > Config > Error)\n const backendProviderName =\n (options.backend as string | undefined) || config.deploy?.backend?.provider;\n if (!backendProviderName) {\n this.error(\n \"Backend provider not specified. Use --backend flag or configure 'deploy.backend.provider' in nexical.yaml.\",\n );\n }\n\n const frontendProviderName =\n (options.frontend as string | undefined) || config.deploy?.frontend?.provider;\n if (!frontendProviderName) {\n this.error(\n \"Frontend provider not specified. Use --frontend flag or configure 'deploy.frontend.provider' in nexical.yaml.\",\n );\n }\n\n const repoProviderName =\n (options.repo as string | undefined) || config.deploy?.repository?.provider;\n if (!repoProviderName) {\n this.error(\n \"Repository provider not specified. Use --repo flag or configure 'deploy.repository.provider' in nexical.yaml.\",\n );\n }\n\n const backendProvider = registry.getDeploymentProvider(backendProviderName!);\n const frontendProvider = registry.getDeploymentProvider(frontendProviderName!);\n const repoProvider = registry.getRepositoryProvider(repoProviderName!);\n\n if (!backendProvider) throw new Error(`Backend provider '${backendProviderName}' not found.`);\n if (!frontendProvider)\n throw new Error(`Frontend provider '${frontendProviderName}' not found.`);\n if (!repoProvider) throw new Error(`Repository provider '${repoProviderName}' not found.`);\n\n const context: DeploymentContext = {\n cwd: process.cwd(),\n config,\n options,\n };\n\n // Provision\n this.info(`Provisioning Backend with ${backendProvider.name}...`);\n await backendProvider.provision(context);\n\n this.info(`Provisioning Frontend with ${frontendProvider.name}...`);\n await frontendProvider.provision(context);\n\n // Configure Repo\n this.info(`Configuring Repository with ${repoProvider.name}...`);\n\n const secrets: Record<string, string> = {};\n\n // Collect secrets from Backend Provider\n this.info(`Resolving secrets from ${backendProvider.name}...`);\n try {\n const backendSecrets = await backendProvider.getSecrets(context);\n Object.assign(secrets, backendSecrets);\n } catch (e: unknown) {\n const message = e instanceof Error ? e.message : String(e);\n this.error(`Failed to resolve secrets for ${backendProvider.name}: ${message}`);\n }\n\n // Collect secrets from Frontend Provider\n this.info(`Resolving secrets from ${frontendProvider.name}...`);\n try {\n const frontendSecrets = await frontendProvider.getSecrets(context);\n Object.assign(secrets, frontendSecrets);\n } catch (e: unknown) {\n const message = e instanceof Error ? e.message : String(e);\n this.error(`Failed to resolve secrets for ${frontendProvider.name}: ${message}`);\n }\n\n await repoProvider.configureSecrets(context, secrets);\n\n const variables: Record<string, string> = {};\n\n // Collect variables from Backend Provider\n try {\n const backendVars = await backendProvider.getVariables(context);\n Object.assign(variables, backendVars);\n } catch (e: unknown) {\n const message = e instanceof Error ? e.message : String(e);\n this.error(`Failed to resolve variables for ${backendProvider.name}: ${message}`);\n }\n\n // Collect variables from Frontend Provider\n try {\n const frontendVars = await frontendProvider.getVariables(context);\n Object.assign(variables, frontendVars);\n } catch (e: unknown) {\n const message = e instanceof Error ? e.message : String(e);\n this.error(`Failed to resolve variables for ${frontendProvider.name}: ${message}`);\n }\n\n await repoProvider.configureVariables(context, variables);\n\n // Generate Workflows\n this.info('Generating CI/CD Workflows...');\n await repoProvider.generateWorkflow(context, [backendProvider, frontendProvider]);\n\n this.success('Deployment configuration complete!');\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/commands/deploy.ts"],"sourcesContent":["import path from 'node:path';\nimport dotenv from 'dotenv';\nimport { BaseCommand } from '@nexical/cli-core';\nimport { ConfigManager } from '../deploy/config-manager';\nimport { ProviderRegistry } from '../deploy/registry';\nimport { DeploymentContext } from '../deploy/types';\n\nexport default class DeployCommand extends BaseCommand {\n static description = `Deploy the application based on nexical.yaml configuration.\n\nThis command orchestrates the deployment of your frontend and backend applications \nby interacting with the providers specified in your configuration file.\n\nCONFIGURATION:\n- Requires a 'nexical.yaml' file in the project root.\n- If the file or specific sections are missing, the CLI will prompt you to run an interactive setup \n and save the configuration for future uses.\n- Supports loading environment variables from a .env file in the project root.\n\nPROVIDERS:\n- Backend: Railway, etc.\n- Frontend: Cloudflare Pages, etc.\n- Repository: GitHub, GitLab, etc.\n\nPROCESS:\n1. Loads environment variables from '.env'.\n2. Loads configuration from 'nexical.yaml'.\n3. Provisions resources via the selected providers.\n4. Configures the repository (secrets/variables) for CI/CD.\n5. Generates CI/CD workflow files.`;\n\n static args = {\n options: [\n {\n name: '--backend <provider>',\n description: 'Override backend provider',\n },\n {\n name: '--frontend <provider>',\n description: 'Override frontend provider',\n },\n {\n name: '--repo <provider>',\n description: 'Override repositroy provider',\n },\n {\n name: '--env <environment>',\n description: 'Deployment environment (e.g. production, staging)',\n default: 'production',\n },\n {\n name: '--dry-run',\n description: 'Simulate the deployment process',\n default: false,\n },\n ],\n };\n\n async run(options: Record<string, unknown>) {\n this.info('Starting Nexical Deployment...');\n\n // Load environment variables from .env\n dotenv.config({ path: path.join(process.cwd(), '.env') });\n\n const configManager = new ConfigManager(process.cwd());\n const config = await configManager.load();\n const registry = new ProviderRegistry();\n\n // Register core and local providers\n await registry.loadCoreProviders();\n await registry.loadLocalProviders(process.cwd());\n\n // Resolve providers (CLI flags > Config > Error)\n const backendProviderName =\n (options.backend as string | undefined) || config.deploy?.backend?.provider;\n if (!backendProviderName) {\n this.error(\n \"Backend provider not specified. Use --backend flag or configure 'deploy.backend.provider' in nexical.yaml.\",\n );\n }\n\n const frontendProviderName =\n (options.frontend as string | undefined) || config.deploy?.frontend?.provider;\n if (!frontendProviderName) {\n this.error(\n \"Frontend provider not specified. Use --frontend flag or configure 'deploy.frontend.provider' in nexical.yaml.\",\n );\n }\n\n const repoProviderName =\n (options.repo as string | undefined) || config.deploy?.repository?.provider;\n if (!repoProviderName) {\n this.error(\n \"Repository provider not specified. Use --repo flag or configure 'deploy.repository.provider' in nexical.yaml.\",\n );\n }\n\n const backendProvider = registry.getDeploymentProvider(backendProviderName!);\n const frontendProvider = registry.getDeploymentProvider(frontendProviderName!);\n const repoProvider = registry.getRepositoryProvider(repoProviderName!);\n\n if (!backendProvider) throw new Error(`Backend provider '${backendProviderName}' not found.`);\n if (!frontendProvider)\n throw new Error(`Frontend provider '${frontendProviderName}' not found.`);\n if (!repoProvider) throw new Error(`Repository provider '${repoProviderName}' not found.`);\n\n const context: DeploymentContext = {\n cwd: process.cwd(),\n config,\n options,\n };\n\n // Provision\n this.info(`Provisioning Backend with ${backendProvider.name}...`);\n await backendProvider.provision(context);\n\n this.info(`Provisioning Frontend with ${frontendProvider.name}...`);\n await frontendProvider.provision(context);\n\n // Configure Repo\n this.info(`Configuring Repository with ${repoProvider.name}...`);\n\n const secrets: Record<string, string> = {};\n\n // Collect secrets from Backend Provider\n this.info(`Resolving secrets from ${backendProvider.name}...`);\n try {\n const backendSecrets = await backendProvider.getSecrets(context);\n Object.assign(secrets, backendSecrets);\n } catch (e: unknown) {\n const message = e instanceof Error ? e.message : String(e);\n this.error(`Failed to resolve secrets for ${backendProvider.name}: ${message}`);\n }\n\n // Collect secrets from Frontend Provider\n this.info(`Resolving secrets from ${frontendProvider.name}...`);\n try {\n const frontendSecrets = await frontendProvider.getSecrets(context);\n Object.assign(secrets, frontendSecrets);\n } catch (e: unknown) {\n const message = e instanceof Error ? e.message : String(e);\n this.error(`Failed to resolve secrets for ${frontendProvider.name}: ${message}`);\n }\n\n await repoProvider.configureSecrets(context, secrets);\n\n const variables: Record<string, string> = {};\n\n // Collect variables from Backend Provider\n try {\n const backendVars = await backendProvider.getVariables(context);\n Object.assign(variables, backendVars);\n } catch (e: unknown) {\n const message = e instanceof Error ? e.message : String(e);\n this.error(`Failed to resolve variables for ${backendProvider.name}: ${message}`);\n }\n\n // Collect variables from Frontend Provider\n try {\n const frontendVars = await frontendProvider.getVariables(context);\n Object.assign(variables, frontendVars);\n } catch (e: unknown) {\n const message = e instanceof Error ? e.message : String(e);\n this.error(`Failed to resolve variables for ${frontendProvider.name}: ${message}`);\n }\n\n await repoProvider.configureVariables(context, variables);\n\n // Generate Workflows\n this.info('Generating CI/CD Workflows...');\n await repoProvider.generateWorkflow(context, [backendProvider, frontendProvider]);\n\n this.success('Deployment configuration complete!');\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -13,12 +13,14 @@ var CloudflareProvider = class {
|
|
|
13
13
|
name = "cloudflare";
|
|
14
14
|
type = "frontend";
|
|
15
15
|
async provision(context) {
|
|
16
|
-
const
|
|
17
|
-
|
|
16
|
+
const env = context.options.env || "production";
|
|
17
|
+
const baseProjectName = context.config.deploy?.frontend?.projectName;
|
|
18
|
+
if (!baseProjectName) {
|
|
18
19
|
throw new Error(
|
|
19
20
|
"Cloudflare project name not found in nexical.yaml. Please configure 'deploy.frontend.projectName'."
|
|
20
21
|
);
|
|
21
22
|
}
|
|
23
|
+
const projectName = env === "production" ? baseProjectName : `${baseProjectName}-${env}`;
|
|
22
24
|
const options = context.config.deploy?.frontend?.options || {};
|
|
23
25
|
const apiTokenEnvVar = typeof options.apiTokenEnvVar === "string" ? options.apiTokenEnvVar : void 0;
|
|
24
26
|
const apiToken = (typeof context.options.cloudflareToken === "string" ? context.options.cloudflareToken : void 0) || (apiTokenEnvVar ? process.env[apiTokenEnvVar] : void 0) || process.env.CLOUDFLARE_API_TOKEN;
|
|
@@ -26,7 +28,9 @@ var CloudflareProvider = class {
|
|
|
26
28
|
const accountId = (typeof context.options.cloudflareAccount === "string" ? context.options.cloudflareAccount : void 0) || (accountIdEnvVar ? process.env[accountIdEnvVar] : void 0) || process.env.CLOUDFLARE_ACCOUNT_ID;
|
|
27
29
|
logger.info("Configuring Cloudflare Pages...");
|
|
28
30
|
if (context.options.dryRun) {
|
|
29
|
-
logger.info(
|
|
31
|
+
logger.info(
|
|
32
|
+
`[Dry Run] Would check Cloudflare Pages project "${projectName}" and create if missing.`
|
|
33
|
+
);
|
|
30
34
|
return;
|
|
31
35
|
}
|
|
32
36
|
if (!apiToken || !accountId) {
|
|
@@ -78,12 +82,14 @@ var CloudflareProvider = class {
|
|
|
78
82
|
return secrets;
|
|
79
83
|
}
|
|
80
84
|
async getVariables(context) {
|
|
81
|
-
const
|
|
82
|
-
|
|
85
|
+
const env = context.options.env || "production";
|
|
86
|
+
const baseProjectName = context.config.deploy?.frontend?.projectName;
|
|
87
|
+
if (!baseProjectName) {
|
|
83
88
|
throw new Error(
|
|
84
89
|
"Cloudflare project name not found in nexical.yaml. Please configure 'deploy.frontend.projectName'."
|
|
85
90
|
);
|
|
86
91
|
}
|
|
92
|
+
const projectName = env === "production" ? baseProjectName : `${baseProjectName}-${env}`;
|
|
87
93
|
return {
|
|
88
94
|
CLOUDFLARE_PROJECT_NAME: projectName
|
|
89
95
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/deploy/providers/cloudflare.ts"],"sourcesContent":["import { logger } from '@nexical/cli-core';\nimport { DeploymentProvider, DeploymentContext, CIConfig } from '../types';\nimport { execAsync } from '../utils';\n\nexport class CloudflareProvider implements DeploymentProvider {\n name = 'cloudflare';\n type = 'frontend' as const;\n\n async provision(context: DeploymentContext): Promise<void> {\n const
|
|
1
|
+
{"version":3,"sources":["../../../../src/deploy/providers/cloudflare.ts"],"sourcesContent":["import { logger } from '@nexical/cli-core';\nimport { DeploymentProvider, DeploymentContext, CIConfig } from '../types';\nimport { execAsync } from '../utils';\n\nexport class CloudflareProvider implements DeploymentProvider {\n name = 'cloudflare';\n type = 'frontend' as const;\n\n async provision(context: DeploymentContext): Promise<void> {\n const env = (context.options.env as string) || 'production';\n const baseProjectName = context.config.deploy?.frontend?.projectName;\n\n if (!baseProjectName) {\n throw new Error(\n \"Cloudflare project name not found in nexical.yaml. Please configure 'deploy.frontend.projectName'.\",\n );\n }\n\n const projectName = env === 'production' ? baseProjectName : `${baseProjectName}-${env}`;\n\n const options = context.config.deploy?.frontend?.options || {};\n\n // Resolve credentials:\n // 1. CLI flag (options)\n // 2. Env var defined in config (options.apiTokenEnvVar)\n // 3. Default env var (CLOUDFLARE_API_TOKEN)\n const apiTokenEnvVar =\n typeof options.apiTokenEnvVar === 'string' ? options.apiTokenEnvVar : undefined;\n const apiToken =\n (typeof context.options.cloudflareToken === 'string'\n ? context.options.cloudflareToken\n : undefined) ||\n (apiTokenEnvVar ? process.env[apiTokenEnvVar] : undefined) ||\n process.env.CLOUDFLARE_API_TOKEN;\n\n const accountIdEnvVar =\n typeof options.accountIdEnvVar === 'string' ? options.accountIdEnvVar : undefined;\n const accountId =\n (typeof context.options.cloudflareAccount === 'string'\n ? context.options.cloudflareAccount\n : undefined) ||\n (accountIdEnvVar ? process.env[accountIdEnvVar] : undefined) ||\n process.env.CLOUDFLARE_ACCOUNT_ID;\n\n logger.info('Configuring Cloudflare Pages...');\n\n if (context.options.dryRun) {\n logger.info(\n `[Dry Run] Would check Cloudflare Pages project \"${projectName}\" and create if missing.`,\n );\n return;\n }\n\n if (!apiToken || !accountId) {\n logger.warn('Cloudflare credentials missing. Skipping automated Cloudflare setup.');\n logger.info('You can manually set up Cloudflare Pages and add the secrets to GitHub.');\n return;\n }\n\n try {\n logger.info(`Ensuring Cloudflare Pages project \"${projectName}\" exists...`);\n try {\n await execAsync(`wrangler pages project create ${projectName} --production-branch main`, {\n env: {\n ...process.env,\n CLOUDFLARE_API_TOKEN: apiToken,\n CLOUDFLARE_ACCOUNT_ID: accountId,\n },\n });\n } catch {\n logger.info('Cloudflare project might already exist.');\n }\n } catch (e: unknown) {\n logger.warn('Cloudflare setup failed.');\n throw e;\n }\n }\n\n async getSecrets(context: DeploymentContext): Promise<Record<string, string>> {\n const options = context.config.deploy?.frontend?.options || {};\n const secrets: Record<string, string> = {};\n\n // Resolve API Token\n const apiTokenEnvVar =\n typeof options.apiTokenEnvVar === 'string' ? options.apiTokenEnvVar : undefined;\n const apiToken =\n (apiTokenEnvVar ? process.env[apiTokenEnvVar] : undefined) ||\n process.env.CLOUDFLARE_API_TOKEN;\n\n if (!apiToken) {\n throw new Error(\n `Cloudflare API Token not found. Please provide it via:\\n` +\n `1. Configuring 'deploy.frontend.options.apiTokenEnvVar' in nexical.yaml and setting that env var in .env\\n` +\n `2. Setting CLOUDFLARE_API_TOKEN in .env`,\n );\n }\n secrets['CLOUDFLARE_API_TOKEN'] = apiToken;\n\n // Resolve Account ID\n const accountIdEnvVar =\n typeof options.accountIdEnvVar === 'string' ? options.accountIdEnvVar : undefined;\n const accountId =\n (accountIdEnvVar ? process.env[accountIdEnvVar] : undefined) ||\n process.env.CLOUDFLARE_ACCOUNT_ID;\n\n if (!accountId) {\n throw new Error(\n `Cloudflare Account ID not found. Please provide it via:\\n` +\n `1. Configuring 'deploy.frontend.options.accountIdEnvVar' in nexical.yaml and setting that env var in .env\\n` +\n `2. Setting CLOUDFLARE_ACCOUNT_ID in .env`,\n );\n }\n secrets['CLOUDFLARE_ACCOUNT_ID'] = accountId;\n\n return secrets;\n }\n\n async getVariables(context: DeploymentContext): Promise<Record<string, string>> {\n const env = (context.options.env as string) || 'production';\n const baseProjectName = context.config.deploy?.frontend?.projectName;\n\n if (!baseProjectName) {\n throw new Error(\n \"Cloudflare project name not found in nexical.yaml. Please configure 'deploy.frontend.projectName'.\",\n );\n }\n\n const projectName = env === 'production' ? baseProjectName : `${baseProjectName}-${env}`;\n return {\n CLOUDFLARE_PROJECT_NAME: projectName,\n };\n }\n\n getCIConfig(): CIConfig {\n return {\n secrets: ['CLOUDFLARE_API_TOKEN', 'CLOUDFLARE_ACCOUNT_ID'],\n variables: ['CLOUDFLARE_PROJECT_NAME'],\n deploySteps: [], // Handled by action\n githubActionStep: {\n name: 'Deploy to Cloudflare Pages',\n uses: 'cloudflare/wrangler-action@v3',\n with: {\n apiToken: '${{ secrets.CLOUDFLARE_API_TOKEN }}',\n accountId: '${{ secrets.CLOUDFLARE_ACCOUNT_ID }}',\n command: 'pages deploy dist --project-name=${{ vars.CLOUDFLARE_PROJECT_NAME }}',\n workingDirectory: 'apps/frontend',\n },\n },\n };\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -4,6 +4,7 @@ declare class RailwayProvider implements DeploymentProvider {
|
|
|
4
4
|
name: string;
|
|
5
5
|
type: "backend";
|
|
6
6
|
provision(context: DeploymentContext): Promise<void>;
|
|
7
|
+
private resolveToken;
|
|
7
8
|
getSecrets(context: DeploymentContext): Promise<Record<string, string>>;
|
|
8
9
|
getVariables(context: DeploymentContext): Promise<Record<string, string>>;
|
|
9
10
|
getCIConfig(): CIConfig;
|
|
@@ -15,71 +15,98 @@ var RailwayProvider = class {
|
|
|
15
15
|
type = "backend";
|
|
16
16
|
async provision(context) {
|
|
17
17
|
const backendDir = path.join(context.cwd, "apps/backend");
|
|
18
|
-
const
|
|
19
|
-
|
|
18
|
+
const env = context.options.env || "production";
|
|
19
|
+
const baseProjectName = context.config.deploy?.backend?.projectName;
|
|
20
|
+
if (!baseProjectName) {
|
|
20
21
|
throw new Error(
|
|
21
22
|
"Railway project name not found in nexical.yaml. Please configure 'deploy.backend.projectName'."
|
|
22
23
|
);
|
|
23
24
|
}
|
|
24
|
-
const
|
|
25
|
+
const railwayName = env === "production" ? baseProjectName : `${baseProjectName}-${env}`;
|
|
25
26
|
logger.info("Configuring Railway...");
|
|
26
27
|
if (context.options.dryRun) {
|
|
27
|
-
logger.info(
|
|
28
|
+
logger.info(`[Dry Run] Would check Railway status and init project "${railwayName}".`);
|
|
28
29
|
return;
|
|
29
30
|
}
|
|
30
31
|
try {
|
|
32
|
+
const env2 = { ...process.env };
|
|
33
|
+
delete env2.RAILWAY_API_TOKEN;
|
|
34
|
+
delete env2.RAILWAY_TOKEN;
|
|
35
|
+
logger.info("Using local Railway CLI credentials (environment variables stripped).");
|
|
31
36
|
try {
|
|
32
|
-
await execAsync("railway status", { cwd: backendDir });
|
|
33
|
-
} catch {
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
+
await execAsync("railway status", { cwd: backendDir, env: env2 });
|
|
38
|
+
} catch (error) {
|
|
39
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
40
|
+
const stderr = error.stderr || "";
|
|
41
|
+
const stdout = error.stdout || "";
|
|
42
|
+
const fullError = `${errMsg} ${stderr} ${stdout}`;
|
|
43
|
+
if (fullError.includes("Project not found") || fullError.includes("No project") || fullError.includes("Project is deleted")) {
|
|
44
|
+
if (fullError.includes("Project is deleted")) {
|
|
45
|
+
logger.info("[Railway] Project is deleted. Unlinking...");
|
|
46
|
+
await execAsync("railway unlink", { cwd: backendDir }).catch(() => {
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
const initCmd = `railway init --name ${railwayName}`;
|
|
50
|
+
logger.info(`No active Railway project linked. Initializing with: ${initCmd}`);
|
|
51
|
+
await execAsync(initCmd, { cwd: backendDir, env: env2 });
|
|
52
|
+
} else if (fullError.includes("Invalid RAILWAY_API_TOKEN") || fullError.includes("Unauthorized")) {
|
|
53
|
+
throw new Error("Railway authentication failed during status check.");
|
|
54
|
+
} else {
|
|
55
|
+
logger.warn(`Railway status check failed: ${errMsg}. Proceeding.`);
|
|
56
|
+
}
|
|
37
57
|
}
|
|
38
|
-
logger.info(`Adding PostgreSQL service if missing for "${
|
|
39
|
-
const { stdout: status } = await execAsync("railway status", { cwd: backendDir })
|
|
58
|
+
logger.info(`Adding PostgreSQL service if missing for "${railwayName}"...`);
|
|
59
|
+
const { stdout: status } = await execAsync("railway status", { cwd: backendDir, env: env2 }).catch(
|
|
60
|
+
() => ({ stdout: "" })
|
|
61
|
+
);
|
|
40
62
|
if (!status.includes("postgres")) {
|
|
41
|
-
|
|
63
|
+
try {
|
|
64
|
+
await execAsync("railway add --database postgres", { cwd: backendDir, env: env2 });
|
|
65
|
+
} catch {
|
|
66
|
+
logger.warn("Failed to auto-add PostgreSQL.");
|
|
67
|
+
}
|
|
42
68
|
}
|
|
43
69
|
} catch (e) {
|
|
70
|
+
const errMsg = e instanceof Error ? e.message : String(e);
|
|
71
|
+
const stderr = e.stderr || "";
|
|
72
|
+
const stdout = e.stdout || "";
|
|
73
|
+
if (errMsg.includes("Railway authentication failed")) throw e;
|
|
74
|
+
logger.error(`Railway setup failed with error: ${errMsg}`);
|
|
75
|
+
if (stderr) logger.error(`[Railway stderr]: ${stderr}`);
|
|
76
|
+
if (stdout) logger.info(`[Railway stdout]: ${stdout}`);
|
|
44
77
|
logger.warn(
|
|
45
|
-
"Railway setup encountered an issue. Ensure you are logged in
|
|
78
|
+
"Railway setup encountered an issue. Ensure you are logged in or have a valid token."
|
|
46
79
|
);
|
|
47
|
-
throw e;
|
|
48
80
|
}
|
|
49
81
|
}
|
|
50
|
-
|
|
82
|
+
resolveToken(context) {
|
|
51
83
|
const options = context.config.deploy?.backend?.options || {};
|
|
52
|
-
const secrets = {};
|
|
53
84
|
const tokenEnvVar = typeof options.tokenEnvVar === "string" ? options.tokenEnvVar : void 0;
|
|
54
|
-
|
|
85
|
+
return process.env.RAILWAY_API_TOKEN?.trim() || (tokenEnvVar ? process.env[tokenEnvVar]?.trim() : void 0) || process.env.RAILWAY_TOKEN?.trim();
|
|
86
|
+
}
|
|
87
|
+
async getSecrets(context) {
|
|
88
|
+
const token = this.resolveToken(context);
|
|
89
|
+
const secrets = {};
|
|
55
90
|
if (!token) {
|
|
56
91
|
throw new Error(
|
|
57
92
|
`Railway Token not found. Please provide it via:
|
|
58
|
-
1.
|
|
59
|
-
2.
|
|
93
|
+
1. Setting RAILWAY_API_TOKEN in .env (Recommended)
|
|
94
|
+
2. Configuring 'deploy.backend.options.tokenEnvVar' in nexical.yaml
|
|
95
|
+
3. Setting RAILWAY_TOKEN in .env`
|
|
60
96
|
);
|
|
61
97
|
}
|
|
62
|
-
secrets["
|
|
98
|
+
secrets["RAILWAY_API_TOKEN"] = token;
|
|
63
99
|
return secrets;
|
|
64
100
|
}
|
|
65
101
|
async getVariables(context) {
|
|
66
|
-
|
|
67
|
-
if (!railwayName) {
|
|
68
|
-
throw new Error(
|
|
69
|
-
"Railway project name not found in nexical.yaml. Please configure 'deploy.backend.projectName'."
|
|
70
|
-
);
|
|
71
|
-
}
|
|
72
|
-
const serviceName = railwayName || "nexical-backend";
|
|
73
|
-
return {
|
|
74
|
-
RAILWAY_SERVICE_NAME: serviceName
|
|
75
|
-
};
|
|
102
|
+
return {};
|
|
76
103
|
}
|
|
77
104
|
getCIConfig() {
|
|
78
105
|
return {
|
|
79
|
-
secrets: ["
|
|
80
|
-
variables: [
|
|
106
|
+
secrets: ["RAILWAY_API_TOKEN"],
|
|
107
|
+
variables: [],
|
|
81
108
|
installSteps: ["npm install -g @railway/cli"],
|
|
82
|
-
deploySteps: ["railway up --
|
|
109
|
+
deploySteps: ["railway up --detach"]
|
|
83
110
|
};
|
|
84
111
|
}
|
|
85
112
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/deploy/providers/railway.ts"],"sourcesContent":["import path from 'node:path';\nimport { logger } from '@nexical/cli-core';\nimport { DeploymentProvider, DeploymentContext, CIConfig } from '../types';\nimport { execAsync } from '../utils';\n\nexport class RailwayProvider implements DeploymentProvider {\n name = 'railway';\n type = 'backend' as const;\n\n async provision(context: DeploymentContext): Promise<void> {\n const backendDir = path.join(context.cwd, 'apps/backend');\n
|
|
1
|
+
{"version":3,"sources":["../../../../src/deploy/providers/railway.ts"],"sourcesContent":["import path from 'node:path';\nimport { logger } from '@nexical/cli-core';\nimport { DeploymentProvider, DeploymentContext, CIConfig } from '../types';\nimport { execAsync } from '../utils';\n\nexport class RailwayProvider implements DeploymentProvider {\n name = 'railway';\n type = 'backend' as const;\n\n async provision(context: DeploymentContext): Promise<void> {\n const backendDir = path.join(context.cwd, 'apps/backend');\n const env = (context.options.env as string) || 'production';\n const baseProjectName = context.config.deploy?.backend?.projectName;\n\n if (!baseProjectName) {\n throw new Error(\n \"Railway project name not found in nexical.yaml. Please configure 'deploy.backend.projectName'.\",\n );\n }\n\n const railwayName = env === 'production' ? baseProjectName : `${baseProjectName}-${env}`;\n\n logger.info('Configuring Railway...');\n\n if (context.options.dryRun) {\n logger.info(`[Dry Run] Would check Railway status and init project \"${railwayName}\".`);\n return;\n }\n\n try {\n // We consciously DO NOT pass any RAILWAY_API_TOKEN to the subprocess.\n // The user may have an invalid token in their .env file (which process.env inherits).\n // We want to force the Railway CLI to use the locally logged-in user's credentials.\n const env = { ...process.env };\n delete env.RAILWAY_API_TOKEN;\n delete env.RAILWAY_TOKEN;\n\n logger.info('Using local Railway CLI credentials (environment variables stripped).');\n\n // Check status to see if we are linked to a project\n try {\n await execAsync('railway status', { cwd: backendDir, env });\n } catch (error: unknown) {\n const errMsg = error instanceof Error ? error.message : String(error);\n const stderr = (error as { stderr?: string }).stderr || '';\n const stdout = (error as { stdout?: string }).stdout || '';\n const fullError = `${errMsg} ${stderr} ${stdout}`;\n\n // If status fails, likely project doesn't exist locally or we aren't linked.\n if (\n fullError.includes('Project not found') ||\n fullError.includes('No project') ||\n fullError.includes('Project is deleted')\n ) {\n if (fullError.includes('Project is deleted')) {\n logger.info('[Railway] Project is deleted. Unlinking...');\n // If it's deleted, we might need to unlink first to clean up local config\n await execAsync('railway unlink', { cwd: backendDir }).catch(() => {});\n }\n const initCmd = `railway init --name ${railwayName}`;\n logger.info(`No active Railway project linked. Initializing with: ${initCmd}`);\n await execAsync(initCmd, { cwd: backendDir, env });\n } else if (\n fullError.includes('Invalid RAILWAY_API_TOKEN') ||\n fullError.includes('Unauthorized')\n ) {\n throw new Error('Railway authentication failed during status check.');\n } else {\n // Some other error (e.g. timeout), warn and try to proceed\n logger.warn(`Railway status check failed: ${errMsg}. Proceeding.`);\n }\n }\n\n logger.info(`Adding PostgreSQL service if missing for \"${railwayName}\"...`);\n const { stdout: status } = await execAsync('railway status', { cwd: backendDir, env }).catch(\n () => ({ stdout: '' }),\n );\n if (!status.includes('postgres')) {\n try {\n await execAsync('railway add --database postgres', { cwd: backendDir, env });\n } catch {\n logger.warn('Failed to auto-add PostgreSQL.');\n }\n }\n } catch (e: unknown) {\n // Rethrow explicit auth errors, otherwise warn\n const errMsg = e instanceof Error ? e.message : String(e);\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const stderr = (e as any).stderr || '';\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const stdout = (e as any).stdout || '';\n\n if (errMsg.includes('Railway authentication failed')) throw e;\n\n logger.error(`Railway setup failed with error: ${errMsg}`);\n if (stderr) logger.error(`[Railway stderr]: ${stderr}`);\n if (stdout) logger.info(`[Railway stdout]: ${stdout}`);\n\n logger.warn(\n 'Railway setup encountered an issue. Ensure you are logged in or have a valid token.',\n );\n }\n }\n\n private resolveToken(context: DeploymentContext): string | undefined {\n const options = context.config.deploy?.backend?.options || {};\n const tokenEnvVar = typeof options.tokenEnvVar === 'string' ? options.tokenEnvVar : undefined;\n return (\n process.env.RAILWAY_API_TOKEN?.trim() ||\n (tokenEnvVar ? process.env[tokenEnvVar]?.trim() : undefined) ||\n process.env.RAILWAY_TOKEN?.trim()\n );\n }\n\n async getSecrets(context: DeploymentContext): Promise<Record<string, string>> {\n const token = this.resolveToken(context);\n const secrets: Record<string, string> = {};\n\n if (!token) {\n // Strict check: Error if token is missing\n throw new Error(\n `Railway Token not found. Please provide it via:\\n` +\n `1. Setting RAILWAY_API_TOKEN in .env (Recommended)\\n` +\n `2. Configuring 'deploy.backend.options.tokenEnvVar' in nexical.yaml\\n` +\n `3. Setting RAILWAY_TOKEN in .env`,\n );\n }\n\n secrets['RAILWAY_API_TOKEN'] = token;\n return secrets;\n }\n\n async getVariables(context: DeploymentContext): Promise<Record<string, string>> {\n return {};\n }\n\n getCIConfig(): CIConfig {\n return {\n secrets: ['RAILWAY_API_TOKEN'],\n variables: [],\n installSteps: ['npm install -g @railway/cli'],\n deploySteps: ['railway up --detach'],\n };\n }\n}\n"],"mappings":"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","names":["env"]}
|
package/package.json
CHANGED
package/src/commands/deploy.ts
CHANGED
|
@@ -43,6 +43,11 @@ PROCESS:
|
|
|
43
43
|
name: '--repo <provider>',
|
|
44
44
|
description: 'Override repositroy provider',
|
|
45
45
|
},
|
|
46
|
+
{
|
|
47
|
+
name: '--env <environment>',
|
|
48
|
+
description: 'Deployment environment (e.g. production, staging)',
|
|
49
|
+
default: 'production',
|
|
50
|
+
},
|
|
46
51
|
{
|
|
47
52
|
name: '--dry-run',
|
|
48
53
|
description: 'Simulate the deployment process',
|
|
@@ -7,14 +7,17 @@ export class CloudflareProvider implements DeploymentProvider {
|
|
|
7
7
|
type = 'frontend' as const;
|
|
8
8
|
|
|
9
9
|
async provision(context: DeploymentContext): Promise<void> {
|
|
10
|
-
const
|
|
10
|
+
const env = (context.options.env as string) || 'production';
|
|
11
|
+
const baseProjectName = context.config.deploy?.frontend?.projectName;
|
|
11
12
|
|
|
12
|
-
if (!
|
|
13
|
+
if (!baseProjectName) {
|
|
13
14
|
throw new Error(
|
|
14
15
|
"Cloudflare project name not found in nexical.yaml. Please configure 'deploy.frontend.projectName'.",
|
|
15
16
|
);
|
|
16
17
|
}
|
|
17
18
|
|
|
19
|
+
const projectName = env === 'production' ? baseProjectName : `${baseProjectName}-${env}`;
|
|
20
|
+
|
|
18
21
|
const options = context.config.deploy?.frontend?.options || {};
|
|
19
22
|
|
|
20
23
|
// Resolve credentials:
|
|
@@ -42,7 +45,9 @@ export class CloudflareProvider implements DeploymentProvider {
|
|
|
42
45
|
logger.info('Configuring Cloudflare Pages...');
|
|
43
46
|
|
|
44
47
|
if (context.options.dryRun) {
|
|
45
|
-
logger.info(
|
|
48
|
+
logger.info(
|
|
49
|
+
`[Dry Run] Would check Cloudflare Pages project "${projectName}" and create if missing.`,
|
|
50
|
+
);
|
|
46
51
|
return;
|
|
47
52
|
}
|
|
48
53
|
|
|
@@ -111,13 +116,16 @@ export class CloudflareProvider implements DeploymentProvider {
|
|
|
111
116
|
}
|
|
112
117
|
|
|
113
118
|
async getVariables(context: DeploymentContext): Promise<Record<string, string>> {
|
|
114
|
-
const
|
|
119
|
+
const env = (context.options.env as string) || 'production';
|
|
120
|
+
const baseProjectName = context.config.deploy?.frontend?.projectName;
|
|
115
121
|
|
|
116
|
-
if (!
|
|
122
|
+
if (!baseProjectName) {
|
|
117
123
|
throw new Error(
|
|
118
124
|
"Cloudflare project name not found in nexical.yaml. Please configure 'deploy.frontend.projectName'.",
|
|
119
125
|
);
|
|
120
126
|
}
|
|
127
|
+
|
|
128
|
+
const projectName = env === 'production' ? baseProjectName : `${baseProjectName}-${env}`;
|
|
121
129
|
return {
|
|
122
130
|
CLOUDFLARE_PROJECT_NAME: projectName,
|
|
123
131
|
};
|
|
@@ -9,95 +9,138 @@ export class RailwayProvider implements DeploymentProvider {
|
|
|
9
9
|
|
|
10
10
|
async provision(context: DeploymentContext): Promise<void> {
|
|
11
11
|
const backendDir = path.join(context.cwd, 'apps/backend');
|
|
12
|
-
|
|
13
|
-
const
|
|
12
|
+
const env = (context.options.env as string) || 'production';
|
|
13
|
+
const baseProjectName = context.config.deploy?.backend?.projectName;
|
|
14
14
|
|
|
15
|
-
if (!
|
|
15
|
+
if (!baseProjectName) {
|
|
16
16
|
throw new Error(
|
|
17
17
|
"Railway project name not found in nexical.yaml. Please configure 'deploy.backend.projectName'.",
|
|
18
18
|
);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
const serviceName = railwayName || 'nexical-backend';
|
|
23
|
-
// Note: Token is usually handled by `railway login` for CLI, but for CI we need it.
|
|
24
|
-
// The provider might not need to know the token for `provision` if we rely on CLI auth.
|
|
25
|
-
// However, we might need to export it for GitHub secrets.
|
|
21
|
+
const railwayName = env === 'production' ? baseProjectName : `${baseProjectName}-${env}`;
|
|
26
22
|
|
|
27
23
|
logger.info('Configuring Railway...');
|
|
28
24
|
|
|
29
25
|
if (context.options.dryRun) {
|
|
30
|
-
logger.info(
|
|
26
|
+
logger.info(`[Dry Run] Would check Railway status and init project "${railwayName}".`);
|
|
31
27
|
return;
|
|
32
28
|
}
|
|
33
29
|
|
|
34
30
|
try {
|
|
31
|
+
// We consciously DO NOT pass any RAILWAY_API_TOKEN to the subprocess.
|
|
32
|
+
// The user may have an invalid token in their .env file (which process.env inherits).
|
|
33
|
+
// We want to force the Railway CLI to use the locally logged-in user's credentials.
|
|
34
|
+
const env = { ...process.env };
|
|
35
|
+
delete env.RAILWAY_API_TOKEN;
|
|
36
|
+
delete env.RAILWAY_TOKEN;
|
|
37
|
+
|
|
38
|
+
logger.info('Using local Railway CLI credentials (environment variables stripped).');
|
|
39
|
+
|
|
40
|
+
// Check status to see if we are linked to a project
|
|
35
41
|
try {
|
|
36
|
-
await execAsync('railway status', { cwd: backendDir });
|
|
37
|
-
} catch {
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
42
|
+
await execAsync('railway status', { cwd: backendDir, env });
|
|
43
|
+
} catch (error: unknown) {
|
|
44
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
45
|
+
const stderr = (error as { stderr?: string }).stderr || '';
|
|
46
|
+
const stdout = (error as { stdout?: string }).stdout || '';
|
|
47
|
+
const fullError = `${errMsg} ${stderr} ${stdout}`;
|
|
48
|
+
|
|
49
|
+
// If status fails, likely project doesn't exist locally or we aren't linked.
|
|
50
|
+
if (
|
|
51
|
+
fullError.includes('Project not found') ||
|
|
52
|
+
fullError.includes('No project') ||
|
|
53
|
+
fullError.includes('Project is deleted')
|
|
54
|
+
) {
|
|
55
|
+
if (fullError.includes('Project is deleted')) {
|
|
56
|
+
logger.info('[Railway] Project is deleted. Unlinking...');
|
|
57
|
+
// If it's deleted, we might need to unlink first to clean up local config
|
|
58
|
+
await execAsync('railway unlink', { cwd: backendDir }).catch(() => {});
|
|
59
|
+
}
|
|
60
|
+
const initCmd = `railway init --name ${railwayName}`;
|
|
61
|
+
logger.info(`No active Railway project linked. Initializing with: ${initCmd}`);
|
|
62
|
+
await execAsync(initCmd, { cwd: backendDir, env });
|
|
63
|
+
} else if (
|
|
64
|
+
fullError.includes('Invalid RAILWAY_API_TOKEN') ||
|
|
65
|
+
fullError.includes('Unauthorized')
|
|
66
|
+
) {
|
|
67
|
+
throw new Error('Railway authentication failed during status check.');
|
|
68
|
+
} else {
|
|
69
|
+
// Some other error (e.g. timeout), warn and try to proceed
|
|
70
|
+
logger.warn(`Railway status check failed: ${errMsg}. Proceeding.`);
|
|
71
|
+
}
|
|
41
72
|
}
|
|
42
73
|
|
|
43
|
-
logger.info(`Adding PostgreSQL service if missing for "${
|
|
44
|
-
const { stdout: status } = await execAsync('railway status', { cwd: backendDir })
|
|
74
|
+
logger.info(`Adding PostgreSQL service if missing for "${railwayName}"...`);
|
|
75
|
+
const { stdout: status } = await execAsync('railway status', { cwd: backendDir, env }).catch(
|
|
76
|
+
() => ({ stdout: '' }),
|
|
77
|
+
);
|
|
45
78
|
if (!status.includes('postgres')) {
|
|
46
|
-
|
|
79
|
+
try {
|
|
80
|
+
await execAsync('railway add --database postgres', { cwd: backendDir, env });
|
|
81
|
+
} catch {
|
|
82
|
+
logger.warn('Failed to auto-add PostgreSQL.');
|
|
83
|
+
}
|
|
47
84
|
}
|
|
48
85
|
} catch (e: unknown) {
|
|
86
|
+
// Rethrow explicit auth errors, otherwise warn
|
|
87
|
+
const errMsg = e instanceof Error ? e.message : String(e);
|
|
88
|
+
|
|
89
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
90
|
+
const stderr = (e as any).stderr || '';
|
|
91
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
92
|
+
const stdout = (e as any).stdout || '';
|
|
93
|
+
|
|
94
|
+
if (errMsg.includes('Railway authentication failed')) throw e;
|
|
95
|
+
|
|
96
|
+
logger.error(`Railway setup failed with error: ${errMsg}`);
|
|
97
|
+
if (stderr) logger.error(`[Railway stderr]: ${stderr}`);
|
|
98
|
+
if (stdout) logger.info(`[Railway stdout]: ${stdout}`);
|
|
99
|
+
|
|
49
100
|
logger.warn(
|
|
50
|
-
'Railway setup encountered an issue. Ensure you are logged in
|
|
101
|
+
'Railway setup encountered an issue. Ensure you are logged in or have a valid token.',
|
|
51
102
|
);
|
|
52
|
-
throw e;
|
|
53
103
|
}
|
|
54
104
|
}
|
|
55
105
|
|
|
56
|
-
|
|
106
|
+
private resolveToken(context: DeploymentContext): string | undefined {
|
|
57
107
|
const options = context.config.deploy?.backend?.options || {};
|
|
58
|
-
const secrets: Record<string, string> = {};
|
|
59
|
-
|
|
60
|
-
// Resolve Railway Token
|
|
61
|
-
// Priority: Configured Env Var > Default Env Var
|
|
62
108
|
const tokenEnvVar = typeof options.tokenEnvVar === 'string' ? options.tokenEnvVar : undefined;
|
|
63
|
-
|
|
109
|
+
return (
|
|
110
|
+
process.env.RAILWAY_API_TOKEN?.trim() ||
|
|
111
|
+
(tokenEnvVar ? process.env[tokenEnvVar]?.trim() : undefined) ||
|
|
112
|
+
process.env.RAILWAY_TOKEN?.trim()
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async getSecrets(context: DeploymentContext): Promise<Record<string, string>> {
|
|
117
|
+
const token = this.resolveToken(context);
|
|
118
|
+
const secrets: Record<string, string> = {};
|
|
64
119
|
|
|
65
120
|
if (!token) {
|
|
66
121
|
// Strict check: Error if token is missing
|
|
67
122
|
throw new Error(
|
|
68
123
|
`Railway Token not found. Please provide it via:\n` +
|
|
69
|
-
`1.
|
|
70
|
-
`2.
|
|
124
|
+
`1. Setting RAILWAY_API_TOKEN in .env (Recommended)\n` +
|
|
125
|
+
`2. Configuring 'deploy.backend.options.tokenEnvVar' in nexical.yaml\n` +
|
|
126
|
+
`3. Setting RAILWAY_TOKEN in .env`,
|
|
71
127
|
);
|
|
72
128
|
}
|
|
73
129
|
|
|
74
|
-
secrets['
|
|
130
|
+
secrets['RAILWAY_API_TOKEN'] = token;
|
|
75
131
|
return secrets;
|
|
76
132
|
}
|
|
77
133
|
|
|
78
134
|
async getVariables(context: DeploymentContext): Promise<Record<string, string>> {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
if (!railwayName) {
|
|
82
|
-
throw new Error(
|
|
83
|
-
"Railway project name not found in nexical.yaml. Please configure 'deploy.backend.projectName'.",
|
|
84
|
-
);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Service name defaults to project name
|
|
88
|
-
const serviceName = railwayName || 'nexical-backend';
|
|
89
|
-
|
|
90
|
-
return {
|
|
91
|
-
RAILWAY_SERVICE_NAME: serviceName,
|
|
92
|
-
};
|
|
135
|
+
return {};
|
|
93
136
|
}
|
|
94
137
|
|
|
95
138
|
getCIConfig(): CIConfig {
|
|
96
139
|
return {
|
|
97
|
-
secrets: ['
|
|
98
|
-
variables: [
|
|
140
|
+
secrets: ['RAILWAY_API_TOKEN'],
|
|
141
|
+
variables: [],
|
|
99
142
|
installSteps: ['npm install -g @railway/cli'],
|
|
100
|
-
deploySteps: ['railway up --
|
|
143
|
+
deploySteps: ['railway up --detach'],
|
|
101
144
|
};
|
|
102
145
|
}
|
|
103
146
|
}
|