@nexical/cli 0.12.1 → 0.12.2

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 CHANGED
@@ -18,7 +18,7 @@ import { fileURLToPath } from "url";
18
18
  // package.json
19
19
  var package_default = {
20
20
  name: "@nexical/cli",
21
- version: "0.12.1",
21
+ version: "0.12.2",
22
22
  license: "Apache-2.0",
23
23
  type: "module",
24
24
  bin: {
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';\nimport { filterCommandDirectories } from './src/utils/filter.js';\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\nconst filteredAdditional = filterCommandDirectories(additionalCommands, coreCommandsDir);\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.12.1\",\n \"license\": \"Apache-2.0\",\n \"type\": \"module\",\n \"bin\": {\n \"nexical\": \"./dist/index.js\"\n },\n \"scripts\": {\n \"build\": \"tsup && cp -r src/deploy/templates dist/\",\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/ai\": \"^0.1.5\",\n \"@nexical/cli-core\": \"^0.1.16\",\n \"dotenv\": \"^17.3.1\",\n \"fast-glob\": \"^3.3.3\",\n \"glob\": \"^13.0.5\",\n \"jiti\": \"^2.6.1\",\n \"minimist\": \"^1.2.8\",\n \"yaml\": \"^2.8.2\"\n },\n \"devDependencies\": {\n \"@eslint/js\": \"^9.39.2\",\n \"@types/fs-extra\": \"^11.0.4\",\n \"@types/minimist\": \"^1.2.5\",\n \"@types/node\": \"^25.3.0\",\n \"@types/nunjucks\": \"^3.2.6\",\n \"@vitest/coverage-v8\": \"^4.0.18\",\n \"eslint\": \"^9.39.2\",\n \"eslint-config-prettier\": \"^10.1.8\",\n \"eslint-plugin-astro\": \"^1.6.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.3\",\n \"globals\": \"^17.3.0\",\n \"husky\": \"^9.1.7\",\n \"lint-staged\": \"^16.2.7\",\n \"prettier\": \"^3.8.1\",\n \"tsup\": \"^8.5.1\",\n \"tsx\": \"^4.21.0\",\n \"typescript\": \"^5.9.3\",\n \"typescript-eslint\": \"^8.56.0\",\n \"vitest\": \"^4.0.18\"\n }\n}\n"],"mappings":";;;;;;;;;;;;;AAAA;AACA,SAAS,KAAK,uBAAuB;AACrC,SAAS,qBAAqB;;;ACF9B;AAAA,EACE,MAAQ;AAAA,EACR,SAAW;AAAA,EACX,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,eAAe;AAAA,IACf,qBAAqB;AAAA,IACrB,QAAU;AAAA,IACV,aAAa;AAAA,IACb,MAAQ;AAAA,IACR,MAAQ;AAAA,IACR,UAAY;AAAA,IACZ,MAAQ;AAAA,EACV;AAAA,EACA,iBAAmB;AAAA,IACjB,cAAc;AAAA,IACd,mBAAmB;AAAA,IACnB,mBAAmB;AAAA,IACnB,eAAe;AAAA,IACf,mBAAmB;AAAA,IACnB,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;;;AD5DA,OAAO,UAAU;AAGjB,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;AAEjE,IAAM,qBAAqB,yBAAyB,oBAAoB,eAAe;AAEvF,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":[]}
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';\nimport { filterCommandDirectories } from './src/utils/filter.js';\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\nconst filteredAdditional = filterCommandDirectories(additionalCommands, coreCommandsDir);\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.12.2\",\n \"license\": \"Apache-2.0\",\n \"type\": \"module\",\n \"bin\": {\n \"nexical\": \"./dist/index.js\"\n },\n \"scripts\": {\n \"build\": \"tsup && cp -r src/deploy/templates dist/\",\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/ai\": \"^0.1.5\",\n \"@nexical/cli-core\": \"^0.1.16\",\n \"dotenv\": \"^17.3.1\",\n \"fast-glob\": \"^3.3.3\",\n \"glob\": \"^13.0.5\",\n \"jiti\": \"^2.6.1\",\n \"minimist\": \"^1.2.8\",\n \"yaml\": \"^2.8.2\"\n },\n \"devDependencies\": {\n \"@eslint/js\": \"^9.39.2\",\n \"@types/fs-extra\": \"^11.0.4\",\n \"@types/minimist\": \"^1.2.5\",\n \"@types/node\": \"^25.3.0\",\n \"@types/nunjucks\": \"^3.2.6\",\n \"@vitest/coverage-v8\": \"^4.0.18\",\n \"eslint\": \"^9.39.2\",\n \"eslint-config-prettier\": \"^10.1.8\",\n \"eslint-plugin-astro\": \"^1.6.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.3\",\n \"globals\": \"^17.3.0\",\n \"husky\": \"^9.1.7\",\n \"lint-staged\": \"^16.2.7\",\n \"prettier\": \"^3.8.1\",\n \"tsup\": \"^8.5.1\",\n \"tsx\": \"^4.21.0\",\n \"typescript\": \"^5.9.3\",\n \"typescript-eslint\": \"^8.56.0\",\n \"vitest\": \"^4.0.18\"\n }\n}\n"],"mappings":";;;;;;;;;;;;;AAAA;AACA,SAAS,KAAK,uBAAuB;AACrC,SAAS,qBAAqB;;;ACF9B;AAAA,EACE,MAAQ;AAAA,EACR,SAAW;AAAA,EACX,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,eAAe;AAAA,IACf,qBAAqB;AAAA,IACrB,QAAU;AAAA,IACV,aAAa;AAAA,IACb,MAAQ;AAAA,IACR,MAAQ;AAAA,IACR,UAAY;AAAA,IACZ,MAAQ;AAAA,EACV;AAAA,EACA,iBAAmB;AAAA,IACjB,cAAc;AAAA,IACd,mBAAmB;AAAA,IACnB,mBAAmB;AAAA,IACnB,eAAe;AAAA,IACf,mBAAmB;AAAA,IACnB,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;;;AD5DA,OAAO,UAAU;AAGjB,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;AAEjE,IAAM,qBAAqB,yBAAyB,oBAAoB,eAAe;AAEvF,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":[]}
@@ -75,7 +75,7 @@ var CloudflareProvider = class {
75
75
  logger.warn(`Failed to fetch existing linked domains: ${errorText}`);
76
76
  } else {
77
77
  const listJson = await listRes.json();
78
- const existingDomains = listJson.success ? listJson.result.map((d) => d.domain) : [];
78
+ const existingDomains = listJson.success ? listJson.result.map((d) => d.name) : [];
79
79
  for (const domain of domains) {
80
80
  if (existingDomains.includes(domain)) {
81
81
  logger.info(`[Cloudflare Pages] Domain ${domain} is already linked.`);
@@ -97,7 +97,7 @@ var CloudflareProvider = class {
97
97
  );
98
98
  if (!linkRes.ok) {
99
99
  const errorText = await linkRes.text();
100
- if (errorText.includes("already exists") || errorText.includes("1008")) {
100
+ if (errorText.includes("already exists") || errorText.includes("already added") || errorText.includes("1008") || errorText.includes("8000018")) {
101
101
  logger.info(`[Cloudflare Pages] Domain ${domain} already linked.`);
102
102
  } else {
103
103
  logger.warn(`[Cloudflare Pages] Failed to link domain ${domain}: ${errorText}`);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/deploy/providers/cloudflare.ts"],"sourcesContent":["import path from 'node:path';\nimport { logger } from '@nexical/cli-core';\nimport { HostingProvider, DeploymentContext, CIConfig, AppConfig } from '../types';\nimport { execAsync } from '../utils';\n\nexport interface CloudflareConfig {\n token?: string;\n account?: string;\n}\n\nexport class CloudflareProvider implements HostingProvider {\n name = 'cloudflare';\n\n async provision(context: DeploymentContext, app: AppConfig): Promise<void> {\n const env = (context.options.env as string) || 'production';\n const baseProjectName = app.projectName;\n\n if (!baseProjectName) {\n throw new Error(\n `Cloudflare project name not found for ${app.name}. Please configure 'projectName'.`,\n );\n }\n\n const projectName = env === 'production' ? baseProjectName : `${baseProjectName}-${env}`;\n\n logger.info(`Configuring Cloudflare Pages for ${app.name}...`);\n\n if (context.options.dryRun) {\n logger.info(\n `[Dry Run] Would check Cloudflare status and provision project \"${projectName}\".`,\n );\n return;\n }\n\n try {\n const secrets = await this.getSecrets(context, app).catch(() => undefined);\n if (!secrets) {\n logger.warn(\n `Cloudflare credentials missing for ${app.name}. Skipping provisioning. ` +\n 'Ensure CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID are set.',\n );\n return;\n }\n\n const processEnv = {\n ...process.env,\n ...secrets,\n NODE_OPTIONS: `${process.env.NODE_OPTIONS || ''} --dns-result-order=ipv4first`.trim(),\n };\n logger.info(`Ensuring Cloudflare Pages project \"${projectName}\" exists...`);\n try {\n await execAsync(`wrangler pages project create ${projectName} --production-branch main`, {\n env: processEnv,\n });\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : String(err);\n if (message.includes('already exists')) {\n logger.info('Cloudflare project already exists.');\n } else {\n throw err;\n }\n }\n\n // Handle Linked Domains\n if (app.domain) {\n const domains = Array.isArray(app.domain) ? app.domain : [app.domain];\n logger.info(\n `Linking ${domains.length} domains to Cloudflare Pages project \"${projectName}\"...`,\n );\n\n const apiToken = secrets.CLOUDFLARE_API_TOKEN;\n const accountId = secrets.CLOUDFLARE_ACCOUNT_ID;\n\n // Fetch existing domains to avoid redundant calls\n const listRes = await fetch(\n `https://api.cloudflare.com/client/v4/accounts/${accountId}/pages/projects/${projectName}/domains`,\n {\n headers: {\n Authorization: `Bearer ${apiToken}`,\n 'Content-Type': 'application/json',\n },\n },\n );\n\n if (!listRes.ok) {\n const errorText = await listRes.text();\n logger.warn(`Failed to fetch existing linked domains: ${errorText}`);\n } else {\n const listJson = (await listRes.json()) as {\n success: boolean;\n result: { domain: string }[];\n };\n const existingDomains = listJson.success ? listJson.result.map((d) => d.domain) : [];\n\n for (const domain of domains) {\n if (existingDomains.includes(domain)) {\n logger.info(`[Cloudflare Pages] Domain ${domain} is already linked.`);\n continue;\n }\n\n logger.info(`[Cloudflare Pages] Linking domain ${domain}...`);\n const linkRes = await fetch(\n `https://api.cloudflare.com/client/v4/accounts/${accountId}/pages/projects/${projectName}/domains`,\n {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${apiToken}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ name: domain }), // Pages API uses 'name' for the domain string in some versions, but docs suggest 'name' or just object. Let's verify 'name' vs 'domain'.\n // Correction: The API docs say POST body should be { \"name\": \"example.com\" }\n },\n );\n\n if (!linkRes.ok) {\n const errorText = await linkRes.text();\n // If it failed because it exists but wasn't in list (unlikely but safe)\n if (errorText.includes('already exists') || errorText.includes('1008')) {\n logger.info(`[Cloudflare Pages] Domain ${domain} already linked.`);\n } else {\n logger.warn(`[Cloudflare Pages] Failed to link domain ${domain}: ${errorText}`);\n }\n } else {\n logger.success(`[Cloudflare Pages] Linked domain ${domain}.`);\n }\n }\n }\n }\n } catch (e: unknown) {\n logger.warn('Cloudflare setup failed.');\n throw e;\n }\n }\n\n async getSecrets(context: DeploymentContext, app: AppConfig): Promise<Record<string, string>> {\n const cfConfig = (app.cloudflare as CloudflareConfig) || {};\n const apiTokenEnvVar = cfConfig.token;\n const accountIdEnvVar = cfConfig.account;\n\n const apiToken =\n process.env.CLOUDFLARE_API_TOKEN?.trim() ||\n (apiTokenEnvVar ? process.env[apiTokenEnvVar]?.trim() : undefined);\n const accountId =\n process.env.CLOUDFLARE_ACCOUNT_ID?.trim() ||\n (accountIdEnvVar ? process.env[accountIdEnvVar]?.trim() : undefined);\n\n if (!apiToken) {\n throw new Error(\n `Cloudflare API Token not found for ${app.name}. Please provide it via:\\n` +\n `1. Setting CLOUDFLARE_API_TOKEN in .env (Recommended)\\n` +\n `2. Configuring 'cloudflare.token' and setting that env var in .env`,\n );\n }\n\n if (!accountId) {\n throw new Error(\n `Cloudflare Account ID not found for ${app.name}. Please provide it via:\\n` +\n `1. Setting CLOUDFLARE_ACCOUNT_ID in .env (Recommended)\\n` +\n `2. Configuring 'cloudflare.account' and setting that env var in .env`,\n );\n }\n\n const secrets: Record<string, string> = {\n CLOUDFLARE_API_TOKEN: apiToken,\n CLOUDFLARE_ACCOUNT_ID: accountId,\n };\n\n // Custom mapped secrets\n if (app.secrets) {\n for (const [key, envVar] of Object.entries(app.secrets)) {\n const value = process.env[envVar];\n if (!value) {\n throw new Error(`Custom secret '${key}' mapping failed: Env var '${envVar}' not found.`);\n }\n secrets[key] = value;\n }\n }\n\n return secrets;\n }\n\n async getVariables(context: DeploymentContext, app: AppConfig): Promise<Record<string, string>> {\n const env = (context.options.env as string) || 'production';\n const baseProjectName = app.projectName;\n\n if (!baseProjectName) {\n throw new Error(`Cloudflare project name not found for ${app.name}.`);\n }\n\n const projectName = env === 'production' ? baseProjectName : `${baseProjectName}-${env}`;\n const varName = `CLOUDFLARE_PROJECT_NAME_${app.name.toUpperCase().replace(/-/g, '_')}`;\n const result: Record<string, string> = {\n [varName]: projectName,\n };\n\n // Custom mapped variables\n if (app.env) {\n for (const [key, value] of Object.entries(app.env)) {\n // If it looks like an env var, try to resolve it, otherwise use literal\n const resolvedValue = process.env[value] || value;\n result[key] = resolvedValue;\n }\n }\n\n return result;\n }\n\n getCIConfig(repoType: 'github' | 'gitlab', app: AppConfig): CIConfig {\n const varName = `CLOUDFLARE_PROJECT_NAME_${app.name.toUpperCase().replace(/-/g, '_')}`;\n const artifactPath = app.artifactPath || 'dist';\n return {\n secrets: ['CLOUDFLARE_API_TOKEN', 'CLOUDFLARE_ACCOUNT_ID'],\n variables: [varName],\n deploySteps: [], // Handled by action\n githubActionStep: {\n name: `Deploy ${app.name} 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 ${artifactPath} --project-name=\\${{ vars.${varName} }}`,\n workingDirectory: app.target || '.',\n },\n },\n };\n }\n\n async deploy(context: DeploymentContext, app: AppConfig): Promise<void> {\n const env = (context.options.env as string) || 'production';\n const baseProjectName = app.projectName;\n const artifactPath = app.artifactPath || 'dist';\n const targetDir = app.target ? path.resolve(context.cwd, app.target) : context.cwd;\n\n if (!baseProjectName) {\n throw new Error(`Cloudflare project name not found for ${app.name}.`);\n }\n\n const projectName = env === 'production' ? baseProjectName : `${baseProjectName}-${env}`;\n\n logger.info(`Deploying ${app.name} to Cloudflare Pages project \"${projectName}\"...`);\n\n if (context.options.dryRun) {\n logger.info(\n `[Dry Run] Would deploy directory \"${artifactPath}\" to Cloudflare project \"${projectName}\".`,\n );\n return;\n }\n\n const secrets = await this.getSecrets(context, app);\n const processEnv = {\n ...process.env,\n ...secrets,\n NODE_OPTIONS: `${process.env.NODE_OPTIONS || ''} --dns-result-order=ipv4first`.trim(),\n };\n\n await execAsync(`wrangler pages deploy ${artifactPath} --project-name=${projectName}`, {\n cwd: targetDir,\n env: processEnv,\n });\n\n logger.success(`Successfully deployed ${app.name} to Cloudflare Pages.`);\n }\n\n getDefaultDnsTarget(app: AppConfig): string | undefined {\n // Cloudflare pages gives a predictable .pages.dev alias\n // Note: This does not take environment into account for custom domains usually,\n // custom domains are typically linked to the production project alias or a specific branch alias.\n // For standard custom domain linkage, we return the production project alias.\n if (app.projectName) {\n return `${app.projectName}.pages.dev`;\n }\n return undefined;\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../../src/deploy/providers/cloudflare.ts"],"sourcesContent":["import path from 'node:path';\nimport { logger } from '@nexical/cli-core';\nimport { HostingProvider, DeploymentContext, CIConfig, AppConfig } from '../types';\nimport { execAsync } from '../utils';\n\nexport interface CloudflareConfig {\n token?: string;\n account?: string;\n}\n\nexport class CloudflareProvider implements HostingProvider {\n name = 'cloudflare';\n\n async provision(context: DeploymentContext, app: AppConfig): Promise<void> {\n const env = (context.options.env as string) || 'production';\n const baseProjectName = app.projectName;\n\n if (!baseProjectName) {\n throw new Error(\n `Cloudflare project name not found for ${app.name}. Please configure 'projectName'.`,\n );\n }\n\n const projectName = env === 'production' ? baseProjectName : `${baseProjectName}-${env}`;\n\n logger.info(`Configuring Cloudflare Pages for ${app.name}...`);\n\n if (context.options.dryRun) {\n logger.info(\n `[Dry Run] Would check Cloudflare status and provision project \"${projectName}\".`,\n );\n return;\n }\n\n try {\n const secrets = await this.getSecrets(context, app).catch(() => undefined);\n if (!secrets) {\n logger.warn(\n `Cloudflare credentials missing for ${app.name}. Skipping provisioning. ` +\n 'Ensure CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID are set.',\n );\n return;\n }\n\n const processEnv = {\n ...process.env,\n ...secrets,\n NODE_OPTIONS: `${process.env.NODE_OPTIONS || ''} --dns-result-order=ipv4first`.trim(),\n };\n logger.info(`Ensuring Cloudflare Pages project \"${projectName}\" exists...`);\n try {\n await execAsync(`wrangler pages project create ${projectName} --production-branch main`, {\n env: processEnv,\n });\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : String(err);\n if (message.includes('already exists')) {\n logger.info('Cloudflare project already exists.');\n } else {\n throw err;\n }\n }\n\n // Handle Linked Domains\n if (app.domain) {\n const domains = Array.isArray(app.domain) ? app.domain : [app.domain];\n logger.info(\n `Linking ${domains.length} domains to Cloudflare Pages project \"${projectName}\"...`,\n );\n\n const apiToken = secrets.CLOUDFLARE_API_TOKEN;\n const accountId = secrets.CLOUDFLARE_ACCOUNT_ID;\n\n // Fetch existing domains to avoid redundant calls\n const listRes = await fetch(\n `https://api.cloudflare.com/client/v4/accounts/${accountId}/pages/projects/${projectName}/domains`,\n {\n headers: {\n Authorization: `Bearer ${apiToken}`,\n 'Content-Type': 'application/json',\n },\n },\n );\n\n if (!listRes.ok) {\n const errorText = await listRes.text();\n logger.warn(`Failed to fetch existing linked domains: ${errorText}`);\n } else {\n const listJson = (await listRes.json()) as {\n success: boolean;\n result: { name: string }[];\n };\n const existingDomains = listJson.success ? listJson.result.map((d) => d.name) : [];\n\n for (const domain of domains) {\n if (existingDomains.includes(domain)) {\n logger.info(`[Cloudflare Pages] Domain ${domain} is already linked.`);\n continue;\n }\n\n logger.info(`[Cloudflare Pages] Linking domain ${domain}...`);\n const linkRes = await fetch(\n `https://api.cloudflare.com/client/v4/accounts/${accountId}/pages/projects/${projectName}/domains`,\n {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${apiToken}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ name: domain }), // Pages API uses 'name' for the domain string in some versions, but docs suggest 'name' or just object. Let's verify 'name' vs 'domain'.\n // Correction: The API docs say POST body should be { \"name\": \"example.com\" }\n },\n );\n\n if (!linkRes.ok) {\n const errorText = await linkRes.text();\n // If it failed because it exists but wasn't in list (unlikely but safe)\n if (\n errorText.includes('already exists') ||\n errorText.includes('already added') ||\n errorText.includes('1008') ||\n errorText.includes('8000018')\n ) {\n logger.info(`[Cloudflare Pages] Domain ${domain} already linked.`);\n } else {\n logger.warn(`[Cloudflare Pages] Failed to link domain ${domain}: ${errorText}`);\n }\n } else {\n logger.success(`[Cloudflare Pages] Linked domain ${domain}.`);\n }\n }\n }\n }\n } catch (e: unknown) {\n logger.warn('Cloudflare setup failed.');\n throw e;\n }\n }\n\n async getSecrets(context: DeploymentContext, app: AppConfig): Promise<Record<string, string>> {\n const cfConfig = (app.cloudflare as CloudflareConfig) || {};\n const apiTokenEnvVar = cfConfig.token;\n const accountIdEnvVar = cfConfig.account;\n\n const apiToken =\n process.env.CLOUDFLARE_API_TOKEN?.trim() ||\n (apiTokenEnvVar ? process.env[apiTokenEnvVar]?.trim() : undefined);\n const accountId =\n process.env.CLOUDFLARE_ACCOUNT_ID?.trim() ||\n (accountIdEnvVar ? process.env[accountIdEnvVar]?.trim() : undefined);\n\n if (!apiToken) {\n throw new Error(\n `Cloudflare API Token not found for ${app.name}. Please provide it via:\\n` +\n `1. Setting CLOUDFLARE_API_TOKEN in .env (Recommended)\\n` +\n `2. Configuring 'cloudflare.token' and setting that env var in .env`,\n );\n }\n\n if (!accountId) {\n throw new Error(\n `Cloudflare Account ID not found for ${app.name}. Please provide it via:\\n` +\n `1. Setting CLOUDFLARE_ACCOUNT_ID in .env (Recommended)\\n` +\n `2. Configuring 'cloudflare.account' and setting that env var in .env`,\n );\n }\n\n const secrets: Record<string, string> = {\n CLOUDFLARE_API_TOKEN: apiToken,\n CLOUDFLARE_ACCOUNT_ID: accountId,\n };\n\n // Custom mapped secrets\n if (app.secrets) {\n for (const [key, envVar] of Object.entries(app.secrets)) {\n const value = process.env[envVar];\n if (!value) {\n throw new Error(`Custom secret '${key}' mapping failed: Env var '${envVar}' not found.`);\n }\n secrets[key] = value;\n }\n }\n\n return secrets;\n }\n\n async getVariables(context: DeploymentContext, app: AppConfig): Promise<Record<string, string>> {\n const env = (context.options.env as string) || 'production';\n const baseProjectName = app.projectName;\n\n if (!baseProjectName) {\n throw new Error(`Cloudflare project name not found for ${app.name}.`);\n }\n\n const projectName = env === 'production' ? baseProjectName : `${baseProjectName}-${env}`;\n const varName = `CLOUDFLARE_PROJECT_NAME_${app.name.toUpperCase().replace(/-/g, '_')}`;\n const result: Record<string, string> = {\n [varName]: projectName,\n };\n\n // Custom mapped variables\n if (app.env) {\n for (const [key, value] of Object.entries(app.env)) {\n // If it looks like an env var, try to resolve it, otherwise use literal\n const resolvedValue = process.env[value] || value;\n result[key] = resolvedValue;\n }\n }\n\n return result;\n }\n\n getCIConfig(repoType: 'github' | 'gitlab', app: AppConfig): CIConfig {\n const varName = `CLOUDFLARE_PROJECT_NAME_${app.name.toUpperCase().replace(/-/g, '_')}`;\n const artifactPath = app.artifactPath || 'dist';\n return {\n secrets: ['CLOUDFLARE_API_TOKEN', 'CLOUDFLARE_ACCOUNT_ID'],\n variables: [varName],\n deploySteps: [], // Handled by action\n githubActionStep: {\n name: `Deploy ${app.name} 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 ${artifactPath} --project-name=\\${{ vars.${varName} }}`,\n workingDirectory: app.target || '.',\n },\n },\n };\n }\n\n async deploy(context: DeploymentContext, app: AppConfig): Promise<void> {\n const env = (context.options.env as string) || 'production';\n const baseProjectName = app.projectName;\n const artifactPath = app.artifactPath || 'dist';\n const targetDir = app.target ? path.resolve(context.cwd, app.target) : context.cwd;\n\n if (!baseProjectName) {\n throw new Error(`Cloudflare project name not found for ${app.name}.`);\n }\n\n const projectName = env === 'production' ? baseProjectName : `${baseProjectName}-${env}`;\n\n logger.info(`Deploying ${app.name} to Cloudflare Pages project \"${projectName}\"...`);\n\n if (context.options.dryRun) {\n logger.info(\n `[Dry Run] Would deploy directory \"${artifactPath}\" to Cloudflare project \"${projectName}\".`,\n );\n return;\n }\n\n const secrets = await this.getSecrets(context, app);\n const processEnv = {\n ...process.env,\n ...secrets,\n NODE_OPTIONS: `${process.env.NODE_OPTIONS || ''} --dns-result-order=ipv4first`.trim(),\n };\n\n await execAsync(`wrangler pages deploy ${artifactPath} --project-name=${projectName}`, {\n cwd: targetDir,\n env: processEnv,\n });\n\n logger.success(`Successfully deployed ${app.name} to Cloudflare Pages.`);\n }\n\n getDefaultDnsTarget(app: AppConfig): string | undefined {\n // Cloudflare pages gives a predictable .pages.dev alias\n // Note: This does not take environment into account for custom domains usually,\n // custom domains are typically linked to the production project alias or a specific branch alias.\n // For standard custom domain linkage, we return the production project alias.\n if (app.projectName) {\n return `${app.projectName}.pages.dev`;\n }\n return undefined;\n }\n}\n"],"mappings":"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","names":[]}
@@ -94,13 +94,6 @@ declare const DeploymentSchema: z.ZodObject<{
94
94
  dnsTarget: z.ZodOptional<z.ZodString>;
95
95
  }, z.ZodTypeAny, "passthrough">>>>;
96
96
  }, "strip", z.ZodTypeAny, {
97
- dns?: z.objectOutputType<{
98
- provider: z.ZodString;
99
- }, z.ZodTypeAny, "passthrough"> | undefined;
100
- repository?: {
101
- provider: string;
102
- options?: Record<string, any> | undefined;
103
- } | undefined;
104
97
  apps?: Record<string, z.objectOutputType<{
105
98
  provider: z.ZodString;
106
99
  projectName: z.ZodOptional<z.ZodString>;
@@ -114,14 +107,14 @@ declare const DeploymentSchema: z.ZodObject<{
114
107
  domain: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodArray<z.ZodString, "many">]>>;
115
108
  dnsTarget: z.ZodOptional<z.ZodString>;
116
109
  }, z.ZodTypeAny, "passthrough">> | undefined;
117
- }, {
118
- dns?: z.objectInputType<{
110
+ dns?: z.objectOutputType<{
119
111
  provider: z.ZodString;
120
112
  }, z.ZodTypeAny, "passthrough"> | undefined;
121
113
  repository?: {
122
114
  provider: string;
123
115
  options?: Record<string, any> | undefined;
124
116
  } | undefined;
117
+ }, {
125
118
  apps?: Record<string, z.objectInputType<{
126
119
  provider: z.ZodString;
127
120
  projectName: z.ZodOptional<z.ZodString>;
@@ -135,16 +128,16 @@ declare const DeploymentSchema: z.ZodObject<{
135
128
  domain: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodArray<z.ZodString, "many">]>>;
136
129
  dnsTarget: z.ZodOptional<z.ZodString>;
137
130
  }, z.ZodTypeAny, "passthrough">> | undefined;
138
- }>>;
139
- }, "strip", z.ZodTypeAny, {
140
- deploy?: {
141
- dns?: z.objectOutputType<{
131
+ dns?: z.objectInputType<{
142
132
  provider: z.ZodString;
143
133
  }, z.ZodTypeAny, "passthrough"> | undefined;
144
134
  repository?: {
145
135
  provider: string;
146
136
  options?: Record<string, any> | undefined;
147
137
  } | undefined;
138
+ }>>;
139
+ }, "strip", z.ZodTypeAny, {
140
+ deploy?: {
148
141
  apps?: Record<string, z.objectOutputType<{
149
142
  provider: z.ZodString;
150
143
  projectName: z.ZodOptional<z.ZodString>;
@@ -158,16 +151,16 @@ declare const DeploymentSchema: z.ZodObject<{
158
151
  domain: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodArray<z.ZodString, "many">]>>;
159
152
  dnsTarget: z.ZodOptional<z.ZodString>;
160
153
  }, z.ZodTypeAny, "passthrough">> | undefined;
161
- } | undefined;
162
- }, {
163
- deploy?: {
164
- dns?: z.objectInputType<{
154
+ dns?: z.objectOutputType<{
165
155
  provider: z.ZodString;
166
156
  }, z.ZodTypeAny, "passthrough"> | undefined;
167
157
  repository?: {
168
158
  provider: string;
169
159
  options?: Record<string, any> | undefined;
170
160
  } | undefined;
161
+ } | undefined;
162
+ }, {
163
+ deploy?: {
171
164
  apps?: Record<string, z.objectInputType<{
172
165
  provider: z.ZodString;
173
166
  projectName: z.ZodOptional<z.ZodString>;
@@ -181,6 +174,13 @@ declare const DeploymentSchema: z.ZodObject<{
181
174
  domain: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodArray<z.ZodString, "many">]>>;
182
175
  dnsTarget: z.ZodOptional<z.ZodString>;
183
176
  }, z.ZodTypeAny, "passthrough">> | undefined;
177
+ dns?: z.objectInputType<{
178
+ provider: z.ZodString;
179
+ }, z.ZodTypeAny, "passthrough"> | undefined;
180
+ repository?: {
181
+ provider: string;
182
+ options?: Record<string, any> | undefined;
183
+ } | undefined;
184
184
  } | undefined;
185
185
  }>;
186
186
  type ValidatedNexicalConfig = z.infer<typeof DeploymentSchema>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nexical/cli",
3
- "version": "0.12.1",
3
+ "version": "0.12.2",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "bin": {
@@ -88,9 +88,9 @@ export class CloudflareProvider implements HostingProvider {
88
88
  } else {
89
89
  const listJson = (await listRes.json()) as {
90
90
  success: boolean;
91
- result: { domain: string }[];
91
+ result: { name: string }[];
92
92
  };
93
- const existingDomains = listJson.success ? listJson.result.map((d) => d.domain) : [];
93
+ const existingDomains = listJson.success ? listJson.result.map((d) => d.name) : [];
94
94
 
95
95
  for (const domain of domains) {
96
96
  if (existingDomains.includes(domain)) {
@@ -115,7 +115,12 @@ export class CloudflareProvider implements HostingProvider {
115
115
  if (!linkRes.ok) {
116
116
  const errorText = await linkRes.text();
117
117
  // If it failed because it exists but wasn't in list (unlikely but safe)
118
- if (errorText.includes('already exists') || errorText.includes('1008')) {
118
+ if (
119
+ errorText.includes('already exists') ||
120
+ errorText.includes('already added') ||
121
+ errorText.includes('1008') ||
122
+ errorText.includes('8000018')
123
+ ) {
119
124
  logger.info(`[Cloudflare Pages] Domain ${domain} already linked.`);
120
125
  } else {
121
126
  logger.warn(`[Cloudflare Pages] Failed to link domain ${domain}: ${errorText}`);
@@ -120,7 +120,7 @@ describe('CloudflareProvider', () => {
120
120
  ok: true,
121
121
  json: async () => ({
122
122
  success: true,
123
- result: [{ domain: 'already-linked.com' }],
123
+ result: [{ name: 'already-linked.com' }],
124
124
  }),
125
125
  });
126
126
 
@@ -162,6 +162,51 @@ describe('CloudflareProvider', () => {
162
162
 
163
163
  vi.unstubAllGlobals();
164
164
  });
165
+
166
+ it('should suppress "already added" error during domain linking', async () => {
167
+ process.env.CLOUDFLARE_API_TOKEN = 'tok';
168
+ process.env.CLOUDFLARE_ACCOUNT_ID = 'acc';
169
+
170
+ const mockFetch = vi.fn();
171
+ vi.stubGlobal('fetch', mockFetch);
172
+
173
+ // First call (GET) - return empty existing domains
174
+ mockFetch.mockResolvedValueOnce({
175
+ ok: true,
176
+ json: async () => ({
177
+ success: true,
178
+ result: [],
179
+ }),
180
+ });
181
+
182
+ // Second call (POST) - return "already added" error
183
+ mockFetch.mockResolvedValueOnce({
184
+ ok: false,
185
+ text: async () =>
186
+ JSON.stringify({
187
+ success: false,
188
+ errors: [{ code: 8000018, message: 'You have already added this custom domain.' }],
189
+ }),
190
+ });
191
+
192
+ const app = {
193
+ name: 'frontend',
194
+ provider: 'cloudflare',
195
+ projectName: 'my-app',
196
+ domain: ['existing.com'],
197
+ } as AppConfig;
198
+
199
+ await provider.provision(mockContext, app);
200
+
201
+ expect(logger.info).toHaveBeenCalledWith(
202
+ expect.stringContaining('Domain existing.com already linked'),
203
+ );
204
+ expect(logger.warn).not.toHaveBeenCalledWith(
205
+ expect.stringContaining('Failed to link domain'),
206
+ );
207
+
208
+ vi.unstubAllGlobals();
209
+ });
165
210
  });
166
211
 
167
212
  describe('getSecrets', () => {