@nexical/cli 0.12.2 → 0.12.3

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.
@@ -0,0 +1,69 @@
1
+ import { createRequire } from "module"; const require = createRequire(import.meta.url);
2
+ import {
3
+ init_esm_shims
4
+ } from "./chunk-6DE5Q66O.js";
5
+
6
+ // src/deploy/utils.ts
7
+ init_esm_shims();
8
+ import { exec, spawn } from "child_process";
9
+ import { promisify } from "util";
10
+ import { createWriteStream } from "fs";
11
+ var execAsync = promisify(exec);
12
+ async function checkCommand(command) {
13
+ try {
14
+ await execAsync(command);
15
+ return true;
16
+ } catch {
17
+ return false;
18
+ }
19
+ }
20
+ async function spawnAsync(command, args, options = {}) {
21
+ const { cwd, env, logFile, debug } = options;
22
+ return new Promise((resolve, reject) => {
23
+ const output = [];
24
+ const fullCommand = args.length > 0 ? `${command} ${args.join(" ")}` : command;
25
+ const child = spawn(fullCommand, {
26
+ cwd,
27
+ env,
28
+ shell: true,
29
+ stdio: ["ignore", "pipe", "pipe"]
30
+ });
31
+ const logStream = !debug && logFile ? createWriteStream(logFile, { flags: "a" }) : null;
32
+ child.stdout?.on("data", (data) => {
33
+ const str = data.toString();
34
+ output.push(str);
35
+ if (debug) process.stdout.write(data);
36
+ if (logStream) logStream.write(data);
37
+ });
38
+ child.stderr?.on("data", (data) => {
39
+ const str = data.toString();
40
+ output.push(str);
41
+ if (debug) process.stderr.write(data);
42
+ if (logStream) logStream.write(data);
43
+ });
44
+ child.on("close", (code) => {
45
+ if (logStream) logStream.end();
46
+ if (code === 0) {
47
+ resolve();
48
+ } else {
49
+ const fullOutput = output.join("");
50
+ const error = new Error(
51
+ `Command failed with code ${code}: ${fullCommand}`
52
+ );
53
+ error.output = fullOutput;
54
+ error.code = code;
55
+ reject(error);
56
+ }
57
+ });
58
+ child.on("error", (err) => {
59
+ reject(err);
60
+ });
61
+ });
62
+ }
63
+
64
+ export {
65
+ execAsync,
66
+ checkCommand,
67
+ spawnAsync
68
+ };
69
+ //# sourceMappingURL=chunk-IPBEZWE2.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/deploy/utils.ts"],"sourcesContent":["import { exec, spawn } from 'node:child_process';\nimport { promisify } from 'node:util';\nimport { createWriteStream } from 'node:fs';\nimport { DeploymentError } from './types';\n\nexport const execAsync = promisify(exec);\n\nexport async function checkCommand(command: string): Promise<boolean> {\n try {\n await execAsync(command);\n return true;\n } catch {\n return false;\n }\n}\n\nexport interface SpawnOptions {\n cwd?: string;\n env?: NodeJS.ProcessEnv;\n logFile?: string;\n debug?: boolean;\n}\n\nexport async function spawnAsync(\n command: string,\n args: string[],\n options: SpawnOptions = {},\n): Promise<void> {\n const { cwd, env, logFile, debug } = options;\n\n return new Promise((resolve, reject) => {\n const output: string[] = [];\n const fullCommand = args.length > 0 ? `${command} ${args.join(' ')}` : command;\n const child = spawn(fullCommand, {\n cwd,\n env,\n shell: true,\n stdio: ['ignore', 'pipe', 'pipe'],\n });\n\n const logStream = !debug && logFile ? createWriteStream(logFile, { flags: 'a' }) : null;\n\n child.stdout?.on('data', (data) => {\n const str = data.toString();\n output.push(str);\n if (debug) process.stdout.write(data);\n if (logStream) logStream.write(data);\n });\n\n child.stderr?.on('data', (data) => {\n const str = data.toString();\n output.push(str);\n if (debug) process.stderr.write(data);\n if (logStream) logStream.write(data);\n });\n\n child.on('close', (code) => {\n if (logStream) logStream.end();\n if (code === 0) {\n resolve();\n } else {\n const fullOutput = output.join('');\n const error = new Error(\n `Command failed with code ${code}: ${fullCommand}`,\n ) as DeploymentError;\n error.output = fullOutput;\n error.code = code;\n reject(error);\n }\n });\n\n child.on('error', (err) => {\n reject(err);\n });\n });\n}\n"],"mappings":";;;;;;AAAA;AAAA,SAAS,MAAM,aAAa;AAC5B,SAAS,iBAAiB;AAC1B,SAAS,yBAAyB;AAG3B,IAAM,YAAY,UAAU,IAAI;AAEvC,eAAsB,aAAa,SAAmC;AACpE,MAAI;AACF,UAAM,UAAU,OAAO;AACvB,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AASA,eAAsB,WACpB,SACA,MACA,UAAwB,CAAC,GACV;AACf,QAAM,EAAE,KAAK,KAAK,SAAS,MAAM,IAAI;AAErC,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,UAAM,SAAmB,CAAC;AAC1B,UAAM,cAAc,KAAK,SAAS,IAAI,GAAG,OAAO,IAAI,KAAK,KAAK,GAAG,CAAC,KAAK;AACvE,UAAM,QAAQ,MAAM,aAAa;AAAA,MAC/B;AAAA,MACA;AAAA,MACA,OAAO;AAAA,MACP,OAAO,CAAC,UAAU,QAAQ,MAAM;AAAA,IAClC,CAAC;AAED,UAAM,YAAY,CAAC,SAAS,UAAU,kBAAkB,SAAS,EAAE,OAAO,IAAI,CAAC,IAAI;AAEnF,UAAM,QAAQ,GAAG,QAAQ,CAAC,SAAS;AACjC,YAAM,MAAM,KAAK,SAAS;AAC1B,aAAO,KAAK,GAAG;AACf,UAAI,MAAO,SAAQ,OAAO,MAAM,IAAI;AACpC,UAAI,UAAW,WAAU,MAAM,IAAI;AAAA,IACrC,CAAC;AAED,UAAM,QAAQ,GAAG,QAAQ,CAAC,SAAS;AACjC,YAAM,MAAM,KAAK,SAAS;AAC1B,aAAO,KAAK,GAAG;AACf,UAAI,MAAO,SAAQ,OAAO,MAAM,IAAI;AACpC,UAAI,UAAW,WAAU,MAAM,IAAI;AAAA,IACrC,CAAC;AAED,UAAM,GAAG,SAAS,CAAC,SAAS;AAC1B,UAAI,UAAW,WAAU,IAAI;AAC7B,UAAI,SAAS,GAAG;AACd,gBAAQ;AAAA,MACV,OAAO;AACL,cAAM,aAAa,OAAO,KAAK,EAAE;AACjC,cAAM,QAAQ,IAAI;AAAA,UAChB,4BAA4B,IAAI,KAAK,WAAW;AAAA,QAClD;AACA,cAAM,SAAS;AACf,cAAM,OAAO;AACb,eAAO,KAAK;AAAA,MACd;AAAA,IACF,CAAC;AAED,UAAM,GAAG,SAAS,CAAC,QAAQ;AACzB,aAAO,GAAG;AAAA,IACZ,CAAC;AAAA,EACH,CAAC;AACH;","names":[]}
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.2",
21
+ version: "0.12.3",
22
22
  license: "Apache-2.0",
23
23
  type: "module",
24
24
  bin: {
@@ -79,7 +79,8 @@ var package_default = {
79
79
  tsx: "^4.21.0",
80
80
  typescript: "^5.9.3",
81
81
  "typescript-eslint": "^8.56.0",
82
- vitest: "^4.0.18"
82
+ vitest: "^4.0.18",
83
+ wrangler: "4.68.1"
83
84
  }
84
85
  };
85
86
 
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.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":[]}
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.3\",\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 \"wrangler\": \"4.68.1\"\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,IACV,UAAY;AAAA,EACd;AACF;;;AD7DA,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":[]}
@@ -2,10 +2,10 @@ import { createRequire } from "module"; const require = createRequire(import.met
2
2
  import {
3
3
  ConfigManager
4
4
  } from "../../chunk-HOVS7SCD.js";
5
- import "../../chunk-JGAMEJTL.js";
6
5
  import {
7
6
  ProviderRegistry
8
7
  } from "../../chunk-USP2MI63.js";
8
+ import "../../chunk-JGAMEJTL.js";
9
9
  import {
10
10
  init_esm_shims
11
11
  } from "../../chunk-6DE5Q66O.js";
@@ -87,6 +87,7 @@ PROCESS:
87
87
  }
88
88
  apps = filteredApps;
89
89
  }
90
+ this.info(`Selected applications: ${apps.map((a) => a.name).join(", ")}`);
90
91
  if (apps.length === 0) {
91
92
  this.error("No applications found in nexical.yaml. Please configure [deploy.apps].");
92
93
  }
@@ -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, HostingProvider, AppConfig, DnsRecord } from '../deploy/types';\n\nexport default class DeployCommand extends BaseCommand {\n static usage = 'deploy';\n static description = 'Deploy the application based on nexical.yaml configuration.';\n static help = `This command orchestrates the deployment of your applications \nby interacting with the providers specified in your configuration file.\n\nCONFIGURATION:\n- Requires a 'nexical.yaml' file in the project root.\n- Supports definition of multiple applications under 'deploy.apps'.\n- Supports loading environment variables from a .env file in the project root.\n\nPROCESS:\n1. Loads environment variables from '.env'.\n2. Loads configuration from 'nexical.yaml'.\n3. Provisions resources for each application.\n4. Configures the repository (secrets/variables) for CI/CD.\n5. Generates CI/CD workflow files for each application.`;\n\n static args = {\n options: [\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 name: '--apps <apps>',\n description: 'Comma separated list of applications to deploy',\n },\n {\n name: '--manual',\n description: 'Perform a direct build and deployment from the local machine',\n default: false,\n },\n {\n name: '--repo <provider>',\n description: 'Repository provider to use (e.g. github, gitlab)',\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'), quiet: true });\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 Applications\n const appsMap = config.deploy?.apps || {};\n let apps: AppConfig[] = Object.entries(appsMap).map(([name, appConfig]) => {\n const app: AppConfig = {\n ...(appConfig as unknown as AppConfig),\n name,\n };\n return app;\n });\n\n // Filter applications if --apps is specified\n const selectedApps = options.apps as string | undefined;\n if (selectedApps) {\n const appNames = selectedApps.split(',').map((s) => s.trim());\n const filteredApps = apps.filter((app) => appNames.includes(app.name));\n\n // Validation: Ensure all specified apps exist\n const missingApps = appNames.filter((name) => !apps.find((app) => app.name === name));\n if (missingApps.length > 0) {\n this.error(\n `The following applications were not found in nexical.yaml: ${missingApps.join(', ')}`,\n );\n }\n\n apps = filteredApps;\n }\n\n if (apps.length === 0) {\n this.error('No applications found in nexical.yaml. Please configure [deploy.apps].');\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 repoProvider = registry.getRepositoryProvider(repoProviderName!);\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 const activeApps: { provider: HostingProvider; app: AppConfig }[] = [];\n const secrets: Record<string, string> = {};\n const variables: Record<string, string> = {};\n\n this.info(`Deploying ${apps.length} applications in parallel...`);\n\n const isManual = !!options.manual;\n\n await Promise.all(\n apps.map(async (app) => {\n this.info(`Processing application: ${app.name}...`);\n const provider = registry.getHostingProvider(app.provider);\n if (!provider) {\n this.error(`Provider '${app.provider}' not found for application '${app.name}'.`);\n return;\n }\n\n // Build\n if (isManual && app.buildCommand) {\n this.info(` Building ${app.name} locally...`);\n\n const buildEnv: Record<string, string> = {\n ...(process.env as Record<string, string>),\n ...(app.env || {}),\n };\n\n if (app.domain) {\n const domain = Array.isArray(app.domain) ? app.domain[0] : app.domain;\n buildEnv.SITE = `https://${domain}`;\n buildEnv.BASE = '/';\n }\n\n if (context.options.dryRun) {\n this.info(` [Dry Run] Would run build: ${app.buildCommand}`);\n if (buildEnv.SITE) {\n this.info(\n ` [Dry Run] Environment override: SITE=${buildEnv.SITE} BASE=${buildEnv.BASE}`,\n );\n }\n } else {\n try {\n const { execAsync } = await import('../deploy/utils');\n await execAsync(app.buildCommand, { env: buildEnv });\n } catch (e: unknown) {\n const message = e instanceof Error ? e.message : String(e);\n this.error(`Build failed for ${app.name}: ${message}`);\n return;\n }\n }\n }\n\n // Provision\n this.info(` Provisioning ${app.name} with ${provider.name}...`);\n await provider.provision(context, app);\n\n // Direct Deploy\n if (isManual && provider.deploy) {\n this.info(` Performing direct deployment for ${app.name}...`);\n await provider.deploy(context, app);\n }\n\n // Collect secrets\n this.info(` Resolving secrets for ${app.name} from ${provider.name}...`);\n try {\n const appSecrets = await provider.getSecrets(context, app);\n Object.assign(secrets, appSecrets);\n } catch (e: unknown) {\n const message = e instanceof Error ? e.message : String(e);\n this.error(`Failed to resolve secrets for ${app.name} (${provider.name}): ${message}`);\n }\n\n // Collect variables\n this.info(` Resolving variables for ${app.name} from ${provider.name}...`);\n try {\n const appVars = await provider.getVariables(context, app);\n Object.assign(variables, appVars);\n } catch (e: unknown) {\n const message = e instanceof Error ? e.message : String(e);\n this.error(`Failed to resolve variables for ${app.name} (${provider.name}): ${message}`);\n }\n\n activeApps.push({ provider, app });\n }),\n );\n\n // Configure Repo\n this.info(`Configuring Repository with ${repoProvider.name}...`);\n await repoProvider.configureSecrets(context, secrets);\n await repoProvider.configureVariables(context, variables);\n\n // Generate Workflows\n this.info('Generating CI/CD Workflows...');\n await repoProvider.generateWorkflow(context, activeApps);\n\n // DNS Provisioning\n const dnsConfig = config.deploy?.dns;\n if (dnsConfig?.provider) {\n const dnsProvider = registry.getDnsProvider(dnsConfig.provider);\n if (!dnsProvider) {\n this.error(`DNS provider '${dnsConfig.provider}' not found.`);\n return;\n }\n\n const dnsRecords: DnsRecord[] = [];\n for (const { app, provider } of activeApps) {\n const target =\n app.dnsTarget ||\n (provider.getDefaultDnsTarget ? provider.getDefaultDnsTarget(app) : undefined);\n\n if (app.domain && target) {\n const domains = Array.isArray(app.domain) ? app.domain : [app.domain];\n for (const domain of domains) {\n const isIp = /^(?:[0-9]{1,3}\\.){3}[0-9]{1,3}$/.test(target);\n dnsRecords.push({\n type: isIp ? 'A' : 'CNAME',\n name: domain,\n content: target,\n proxied: true,\n });\n }\n } else if (app.domain && !target) {\n this.warn(\n `App '${app.name}' specifies domain(s) but no 'dnsTarget' could be inferred. Skipping DNS auto-provisioning.`,\n );\n }\n }\n\n if (dnsRecords.length > 0) {\n this.info(`Configuring DNS with ${dnsProvider.name}...`);\n try {\n await dnsProvider.provision(context, dnsRecords);\n } catch (e: unknown) {\n const message = e instanceof Error ? e.message : String(e);\n this.warn(`DNS provisioning failed: ${message}`);\n }\n }\n }\n\n this.success('Deployment configuration complete!');\n }\n}\n"],"mappings":";;;;;;;;;;;;;AAAA;AAAA,OAAO,UAAU;AACjB,OAAO,YAAY;AACnB,SAAS,mBAAmB;AAK5B,IAAqB,gBAArB,cAA2C,YAAY;AAAA,EACrD,OAAO,QAAQ;AAAA,EACf,OAAO,cAAc;AAAA,EACrB,OAAO,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAed,OAAO,OAAO;AAAA,IACZ,SAAS;AAAA,MACP;AAAA,QACE,MAAM;AAAA,QACN,aAAa;AAAA,QACb,SAAS;AAAA,MACX;AAAA,MACA;AAAA,QACE,MAAM;AAAA,QACN,aAAa;AAAA,QACb,SAAS;AAAA,MACX;AAAA,MACA;AAAA,QACE,MAAM;AAAA,QACN,aAAa;AAAA,MACf;AAAA,MACA;AAAA,QACE,MAAM;AAAA,QACN,aAAa;AAAA,QACb,SAAS;AAAA,MACX;AAAA,MACA;AAAA,QACE,MAAM;AAAA,QACN,aAAa;AAAA,MACf;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAM,IAAI,SAAkC;AAC1C,SAAK,KAAK,gCAAgC;AAG1C,WAAO,OAAO,EAAE,MAAM,KAAK,KAAK,QAAQ,IAAI,GAAG,MAAM,GAAG,OAAO,KAAK,CAAC;AAErE,UAAM,gBAAgB,IAAI,cAAc,QAAQ,IAAI,CAAC;AACrD,UAAM,SAAS,MAAM,cAAc,KAAK;AACxC,UAAM,WAAW,IAAI,iBAAiB;AAGtC,UAAM,SAAS,kBAAkB;AACjC,UAAM,SAAS,mBAAmB,QAAQ,IAAI,CAAC;AAG/C,UAAM,UAAU,OAAO,QAAQ,QAAQ,CAAC;AACxC,QAAI,OAAoB,OAAO,QAAQ,OAAO,EAAE,IAAI,CAAC,CAAC,MAAM,SAAS,MAAM;AACzE,YAAM,MAAiB;AAAA,QACrB,GAAI;AAAA,QACJ;AAAA,MACF;AACA,aAAO;AAAA,IACT,CAAC;AAGD,UAAM,eAAe,QAAQ;AAC7B,QAAI,cAAc;AAChB,YAAM,WAAW,aAAa,MAAM,GAAG,EAAE,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC;AAC5D,YAAM,eAAe,KAAK,OAAO,CAAC,QAAQ,SAAS,SAAS,IAAI,IAAI,CAAC;AAGrE,YAAM,cAAc,SAAS,OAAO,CAAC,SAAS,CAAC,KAAK,KAAK,CAAC,QAAQ,IAAI,SAAS,IAAI,CAAC;AACpF,UAAI,YAAY,SAAS,GAAG;AAC1B,aAAK;AAAA,UACH,8DAA8D,YAAY,KAAK,IAAI,CAAC;AAAA,QACtF;AAAA,MACF;AAEA,aAAO;AAAA,IACT;AAEA,QAAI,KAAK,WAAW,GAAG;AACrB,WAAK,MAAM,wEAAwE;AAAA,IACrF;AAEA,UAAM,mBACH,QAAQ,QAA+B,OAAO,QAAQ,YAAY;AACrE,QAAI,CAAC,kBAAkB;AACrB,WAAK;AAAA,QACH;AAAA,MACF;AAAA,IACF;AAEA,UAAM,eAAe,SAAS,sBAAsB,gBAAiB;AACrE,QAAI,CAAC,aAAc,OAAM,IAAI,MAAM,wBAAwB,gBAAgB,cAAc;AAEzF,UAAM,UAA6B;AAAA,MACjC,KAAK,QAAQ,IAAI;AAAA,MACjB;AAAA,MACA;AAAA,IACF;AAEA,UAAM,aAA8D,CAAC;AACrE,UAAM,UAAkC,CAAC;AACzC,UAAM,YAAoC,CAAC;AAE3C,SAAK,KAAK,aAAa,KAAK,MAAM,8BAA8B;AAEhE,UAAM,WAAW,CAAC,CAAC,QAAQ;AAE3B,UAAM,QAAQ;AAAA,MACZ,KAAK,IAAI,OAAO,QAAQ;AACtB,aAAK,KAAK,2BAA2B,IAAI,IAAI,KAAK;AAClD,cAAM,WAAW,SAAS,mBAAmB,IAAI,QAAQ;AACzD,YAAI,CAAC,UAAU;AACb,eAAK,MAAM,aAAa,IAAI,QAAQ,gCAAgC,IAAI,IAAI,IAAI;AAChF;AAAA,QACF;AAGA,YAAI,YAAY,IAAI,cAAc;AAChC,eAAK,KAAK,cAAc,IAAI,IAAI,aAAa;AAE7C,gBAAM,WAAmC;AAAA,YACvC,GAAI,QAAQ;AAAA,YACZ,GAAI,IAAI,OAAO,CAAC;AAAA,UAClB;AAEA,cAAI,IAAI,QAAQ;AACd,kBAAM,SAAS,MAAM,QAAQ,IAAI,MAAM,IAAI,IAAI,OAAO,CAAC,IAAI,IAAI;AAC/D,qBAAS,OAAO,WAAW,MAAM;AACjC,qBAAS,OAAO;AAAA,UAClB;AAEA,cAAI,QAAQ,QAAQ,QAAQ;AAC1B,iBAAK,KAAK,gCAAgC,IAAI,YAAY,EAAE;AAC5D,gBAAI,SAAS,MAAM;AACjB,mBAAK;AAAA,gBACH,0CAA0C,SAAS,IAAI,SAAS,SAAS,IAAI;AAAA,cAC/E;AAAA,YACF;AAAA,UACF,OAAO;AACL,gBAAI;AACF,oBAAM,EAAE,UAAU,IAAI,MAAM,OAAO,oBAAiB;AACpD,oBAAM,UAAU,IAAI,cAAc,EAAE,KAAK,SAAS,CAAC;AAAA,YACrD,SAAS,GAAY;AACnB,oBAAM,UAAU,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC;AACzD,mBAAK,MAAM,oBAAoB,IAAI,IAAI,KAAK,OAAO,EAAE;AACrD;AAAA,YACF;AAAA,UACF;AAAA,QACF;AAGA,aAAK,KAAK,kBAAkB,IAAI,IAAI,SAAS,SAAS,IAAI,KAAK;AAC/D,cAAM,SAAS,UAAU,SAAS,GAAG;AAGrC,YAAI,YAAY,SAAS,QAAQ;AAC/B,eAAK,KAAK,sCAAsC,IAAI,IAAI,KAAK;AAC7D,gBAAM,SAAS,OAAO,SAAS,GAAG;AAAA,QACpC;AAGA,aAAK,KAAK,2BAA2B,IAAI,IAAI,SAAS,SAAS,IAAI,KAAK;AACxE,YAAI;AACF,gBAAM,aAAa,MAAM,SAAS,WAAW,SAAS,GAAG;AACzD,iBAAO,OAAO,SAAS,UAAU;AAAA,QACnC,SAAS,GAAY;AACnB,gBAAM,UAAU,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC;AACzD,eAAK,MAAM,iCAAiC,IAAI,IAAI,KAAK,SAAS,IAAI,MAAM,OAAO,EAAE;AAAA,QACvF;AAGA,aAAK,KAAK,6BAA6B,IAAI,IAAI,SAAS,SAAS,IAAI,KAAK;AAC1E,YAAI;AACF,gBAAM,UAAU,MAAM,SAAS,aAAa,SAAS,GAAG;AACxD,iBAAO,OAAO,WAAW,OAAO;AAAA,QAClC,SAAS,GAAY;AACnB,gBAAM,UAAU,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC;AACzD,eAAK,MAAM,mCAAmC,IAAI,IAAI,KAAK,SAAS,IAAI,MAAM,OAAO,EAAE;AAAA,QACzF;AAEA,mBAAW,KAAK,EAAE,UAAU,IAAI,CAAC;AAAA,MACnC,CAAC;AAAA,IACH;AAGA,SAAK,KAAK,+BAA+B,aAAa,IAAI,KAAK;AAC/D,UAAM,aAAa,iBAAiB,SAAS,OAAO;AACpD,UAAM,aAAa,mBAAmB,SAAS,SAAS;AAGxD,SAAK,KAAK,+BAA+B;AACzC,UAAM,aAAa,iBAAiB,SAAS,UAAU;AAGvD,UAAM,YAAY,OAAO,QAAQ;AACjC,QAAI,WAAW,UAAU;AACvB,YAAM,cAAc,SAAS,eAAe,UAAU,QAAQ;AAC9D,UAAI,CAAC,aAAa;AAChB,aAAK,MAAM,iBAAiB,UAAU,QAAQ,cAAc;AAC5D;AAAA,MACF;AAEA,YAAM,aAA0B,CAAC;AACjC,iBAAW,EAAE,KAAK,SAAS,KAAK,YAAY;AAC1C,cAAM,SACJ,IAAI,cACH,SAAS,sBAAsB,SAAS,oBAAoB,GAAG,IAAI;AAEtE,YAAI,IAAI,UAAU,QAAQ;AACxB,gBAAM,UAAU,MAAM,QAAQ,IAAI,MAAM,IAAI,IAAI,SAAS,CAAC,IAAI,MAAM;AACpE,qBAAW,UAAU,SAAS;AAC5B,kBAAM,OAAO,kCAAkC,KAAK,MAAM;AAC1D,uBAAW,KAAK;AAAA,cACd,MAAM,OAAO,MAAM;AAAA,cACnB,MAAM;AAAA,cACN,SAAS;AAAA,cACT,SAAS;AAAA,YACX,CAAC;AAAA,UACH;AAAA,QACF,WAAW,IAAI,UAAU,CAAC,QAAQ;AAChC,eAAK;AAAA,YACH,QAAQ,IAAI,IAAI;AAAA,UAClB;AAAA,QACF;AAAA,MACF;AAEA,UAAI,WAAW,SAAS,GAAG;AACzB,aAAK,KAAK,wBAAwB,YAAY,IAAI,KAAK;AACvD,YAAI;AACF,gBAAM,YAAY,UAAU,SAAS,UAAU;AAAA,QACjD,SAAS,GAAY;AACnB,gBAAM,UAAU,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC;AACzD,eAAK,KAAK,4BAA4B,OAAO,EAAE;AAAA,QACjD;AAAA,MACF;AAAA,IACF;AAEA,SAAK,QAAQ,oCAAoC;AAAA,EACnD;AACF;","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, HostingProvider, AppConfig, DnsRecord } from '../deploy/types';\n\nexport default class DeployCommand extends BaseCommand {\n static usage = 'deploy';\n static description = 'Deploy the application based on nexical.yaml configuration.';\n static help = `This command orchestrates the deployment of your applications \nby interacting with the providers specified in your configuration file.\n\nCONFIGURATION:\n- Requires a 'nexical.yaml' file in the project root.\n- Supports definition of multiple applications under 'deploy.apps'.\n- Supports loading environment variables from a .env file in the project root.\n\nPROCESS:\n1. Loads environment variables from '.env'.\n2. Loads configuration from 'nexical.yaml'.\n3. Provisions resources for each application.\n4. Configures the repository (secrets/variables) for CI/CD.\n5. Generates CI/CD workflow files for each application.`;\n\n static args = {\n options: [\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 name: '--apps <apps>',\n description: 'Comma separated list of applications to deploy',\n },\n {\n name: '--manual',\n description: 'Perform a direct build and deployment from the local machine',\n default: false,\n },\n {\n name: '--repo <provider>',\n description: 'Repository provider to use (e.g. github, gitlab)',\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'), quiet: true });\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 Applications\n const appsMap = config.deploy?.apps || {};\n let apps: AppConfig[] = Object.entries(appsMap).map(([name, appConfig]) => {\n const app: AppConfig = {\n ...(appConfig as unknown as AppConfig),\n name,\n };\n return app;\n });\n\n // Filter applications if --apps is specified\n const selectedApps = options.apps as string | undefined;\n if (selectedApps) {\n const appNames = selectedApps.split(',').map((s) => s.trim());\n const filteredApps = apps.filter((app) => appNames.includes(app.name));\n\n // Validation: Ensure all specified apps exist\n const missingApps = appNames.filter((name) => !apps.find((app) => app.name === name));\n if (missingApps.length > 0) {\n this.error(\n `The following applications were not found in nexical.yaml: ${missingApps.join(', ')}`,\n );\n }\n\n apps = filteredApps;\n }\n\n this.info(`Selected applications: ${apps.map((a) => a.name).join(', ')}`);\n\n if (apps.length === 0) {\n this.error('No applications found in nexical.yaml. Please configure [deploy.apps].');\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 repoProvider = registry.getRepositoryProvider(repoProviderName!);\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 const activeApps: { provider: HostingProvider; app: AppConfig }[] = [];\n const secrets: Record<string, string> = {};\n const variables: Record<string, string> = {};\n\n this.info(`Deploying ${apps.length} applications in parallel...`);\n\n const isManual = !!options.manual;\n\n await Promise.all(\n apps.map(async (app) => {\n this.info(`Processing application: ${app.name}...`);\n const provider = registry.getHostingProvider(app.provider);\n if (!provider) {\n this.error(`Provider '${app.provider}' not found for application '${app.name}'.`);\n return;\n }\n\n // Build\n if (isManual && app.buildCommand) {\n this.info(` Building ${app.name} locally...`);\n\n const buildEnv: Record<string, string> = {\n ...(process.env as Record<string, string>),\n ...(app.env || {}),\n };\n\n if (app.domain) {\n const domain = Array.isArray(app.domain) ? app.domain[0] : app.domain;\n buildEnv.SITE = `https://${domain}`;\n buildEnv.BASE = '/';\n }\n\n if (context.options.dryRun) {\n this.info(` [Dry Run] Would run build: ${app.buildCommand}`);\n if (buildEnv.SITE) {\n this.info(\n ` [Dry Run] Environment override: SITE=${buildEnv.SITE} BASE=${buildEnv.BASE}`,\n );\n }\n } else {\n try {\n const { execAsync } = await import('../deploy/utils');\n await execAsync(app.buildCommand, { env: buildEnv });\n } catch (e: unknown) {\n const message = e instanceof Error ? e.message : String(e);\n this.error(`Build failed for ${app.name}: ${message}`);\n return;\n }\n }\n }\n\n // Provision\n this.info(` Provisioning ${app.name} with ${provider.name}...`);\n await provider.provision(context, app);\n\n // Direct Deploy\n if (isManual && provider.deploy) {\n this.info(` Performing direct deployment for ${app.name}...`);\n await provider.deploy(context, app);\n }\n\n // Collect secrets\n this.info(` Resolving secrets for ${app.name} from ${provider.name}...`);\n try {\n const appSecrets = await provider.getSecrets(context, app);\n Object.assign(secrets, appSecrets);\n } catch (e: unknown) {\n const message = e instanceof Error ? e.message : String(e);\n this.error(`Failed to resolve secrets for ${app.name} (${provider.name}): ${message}`);\n }\n\n // Collect variables\n this.info(` Resolving variables for ${app.name} from ${provider.name}...`);\n try {\n const appVars = await provider.getVariables(context, app);\n Object.assign(variables, appVars);\n } catch (e: unknown) {\n const message = e instanceof Error ? e.message : String(e);\n this.error(`Failed to resolve variables for ${app.name} (${provider.name}): ${message}`);\n }\n\n activeApps.push({ provider, app });\n }),\n );\n\n // Configure Repo\n this.info(`Configuring Repository with ${repoProvider.name}...`);\n await repoProvider.configureSecrets(context, secrets);\n await repoProvider.configureVariables(context, variables);\n\n // Generate Workflows\n this.info('Generating CI/CD Workflows...');\n await repoProvider.generateWorkflow(context, activeApps);\n\n // DNS Provisioning\n const dnsConfig = config.deploy?.dns;\n if (dnsConfig?.provider) {\n const dnsProvider = registry.getDnsProvider(dnsConfig.provider);\n if (!dnsProvider) {\n this.error(`DNS provider '${dnsConfig.provider}' not found.`);\n return;\n }\n\n const dnsRecords: DnsRecord[] = [];\n for (const { app, provider } of activeApps) {\n const target =\n app.dnsTarget ||\n (provider.getDefaultDnsTarget ? provider.getDefaultDnsTarget(app) : undefined);\n\n if (app.domain && target) {\n const domains = Array.isArray(app.domain) ? app.domain : [app.domain];\n for (const domain of domains) {\n const isIp = /^(?:[0-9]{1,3}\\.){3}[0-9]{1,3}$/.test(target);\n dnsRecords.push({\n type: isIp ? 'A' : 'CNAME',\n name: domain,\n content: target,\n proxied: true,\n });\n }\n } else if (app.domain && !target) {\n this.warn(\n `App '${app.name}' specifies domain(s) but no 'dnsTarget' could be inferred. Skipping DNS auto-provisioning.`,\n );\n }\n }\n\n if (dnsRecords.length > 0) {\n this.info(`Configuring DNS with ${dnsProvider.name}...`);\n try {\n await dnsProvider.provision(context, dnsRecords);\n } catch (e: unknown) {\n const message = e instanceof Error ? e.message : String(e);\n this.warn(`DNS provisioning failed: ${message}`);\n }\n }\n }\n\n this.success('Deployment configuration complete!');\n }\n}\n"],"mappings":"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","names":[]}
@@ -6,6 +6,7 @@ interface CloudflareConfig {
6
6
  }
7
7
  declare class CloudflareProvider implements HostingProvider {
8
8
  name: string;
9
+ private runWrangler;
9
10
  provision(context: DeploymentContext, app: AppConfig): Promise<void>;
10
11
  getSecrets(context: DeploymentContext, app: AppConfig): Promise<Record<string, string>>;
11
12
  getVariables(context: DeploymentContext, app: AppConfig): Promise<Record<string, string>>;
@@ -1,7 +1,7 @@
1
1
  import { createRequire } from "module"; const require = createRequire(import.meta.url);
2
2
  import {
3
- execAsync
4
- } from "../../../chunk-VKE7R2EZ.js";
3
+ spawnAsync
4
+ } from "../../../chunk-IPBEZWE2.js";
5
5
  import {
6
6
  init_esm_shims
7
7
  } from "../../../chunk-6DE5Q66O.js";
@@ -9,9 +9,59 @@ import {
9
9
  // src/deploy/providers/cloudflare.ts
10
10
  init_esm_shims();
11
11
  import path from "path";
12
+ import fs from "fs";
12
13
  import { logger } from "@nexical/cli-core";
13
14
  var CloudflareProvider = class {
14
15
  name = "cloudflare";
16
+ async runWrangler(context, app, args, options, retries = 5) {
17
+ const debug = !!context.options.debug;
18
+ const logsDir = path.resolve(context.cwd, "logs");
19
+ if (!debug) {
20
+ await fs.promises.mkdir(logsDir, { recursive: true });
21
+ }
22
+ const logFile = path.resolve(logsDir, `cloudflare-${app.name}-${options.commandName}.log`);
23
+ if (!debug) {
24
+ logger.info(`Log file: ${logFile}`);
25
+ }
26
+ let attempt = 0;
27
+ while (attempt <= retries) {
28
+ try {
29
+ await spawnAsync("wrangler", args, {
30
+ cwd: options.cwd || context.cwd,
31
+ env: options.env,
32
+ debug,
33
+ logFile
34
+ });
35
+ return;
36
+ } catch (err) {
37
+ attempt++;
38
+ const error = err;
39
+ const message = error.message || String(err);
40
+ const output = error.output || "";
41
+ const combined = `${message}
42
+ ${output}`;
43
+ const transientRegex = /503|connection termination|upstream connect error|reset by peer|malformed response|Service Unavailable/i;
44
+ const isTransient = transientRegex.test(combined);
45
+ if (debug) {
46
+ logger.info(`Command failed on attempt ${attempt}. Checking for transient error...`);
47
+ logger.info(`Transient match: ${isTransient}`);
48
+ if (!isTransient) {
49
+ logger.debug(`Full failed output context:
50
+ ${combined}`);
51
+ }
52
+ }
53
+ if (isTransient && attempt <= retries) {
54
+ const delay = Math.min(Math.pow(2, attempt) * 2e3, 3e4);
55
+ logger.warn(
56
+ `Cloudflare API returned transient error (Attempt ${attempt}/${retries}). Retrying in ${delay / 1e3}s...`
57
+ );
58
+ await new Promise((resolve) => setTimeout(resolve, delay));
59
+ continue;
60
+ }
61
+ throw err;
62
+ }
63
+ }
64
+ }
15
65
  async provision(context, app) {
16
66
  const env = context.options.env || "production";
17
67
  const baseProjectName = app.projectName;
@@ -41,31 +91,57 @@ var CloudflareProvider = class {
41
91
  ...secrets,
42
92
  NODE_OPTIONS: `${process.env.NODE_OPTIONS || ""} --dns-result-order=ipv4first`.trim()
43
93
  };
44
- logger.info(`Ensuring Cloudflare Pages project "${projectName}" exists...`);
45
- try {
46
- await execAsync(`wrangler pages project create ${projectName} --production-branch main`, {
47
- env: processEnv
48
- });
49
- } catch (err) {
50
- const message = err instanceof Error ? err.message : String(err);
51
- if (message.includes("already exists")) {
52
- logger.info("Cloudflare project already exists.");
53
- } else {
54
- throw err;
94
+ const projectName2 = env === "production" ? baseProjectName : `${baseProjectName}-${env}`;
95
+ const apiToken = secrets.CLOUDFLARE_API_TOKEN;
96
+ const accountId = secrets.CLOUDFLARE_ACCOUNT_ID;
97
+ logger.info(`Ensuring Cloudflare Pages project "${projectName2}" exists...`);
98
+ const projectRes = await fetch(
99
+ `https://api.cloudflare.com/client/v4/accounts/${accountId}/pages/projects/${projectName2}`,
100
+ {
101
+ headers: {
102
+ Authorization: `Bearer ${apiToken}`,
103
+ "Content-Type": "application/json"
104
+ }
105
+ }
106
+ );
107
+ if (projectRes.ok) {
108
+ logger.info(`Cloudflare project "${projectName2}" already exists.`);
109
+ } else {
110
+ try {
111
+ await this.runWrangler(
112
+ context,
113
+ app,
114
+ ["pages", "project", "create", projectName2, "--production-branch", "main"],
115
+ {
116
+ env: processEnv,
117
+ commandName: "provision"
118
+ }
119
+ );
120
+ } catch (err) {
121
+ const error = err;
122
+ const message = error.message || String(err);
123
+ const output = error.output || "";
124
+ const combined = `${message}
125
+ ${output}`.toLowerCase();
126
+ if (combined.includes("already exists") || combined.includes("already_exists")) {
127
+ logger.info("Cloudflare project already exists (detected after creation attempt).");
128
+ } else {
129
+ throw err;
130
+ }
55
131
  }
56
132
  }
57
133
  if (app.domain) {
58
134
  const domains = Array.isArray(app.domain) ? app.domain : [app.domain];
59
135
  logger.info(
60
- `Linking ${domains.length} domains to Cloudflare Pages project "${projectName}"...`
136
+ `Linking ${domains.length} domains to Cloudflare Pages project "${projectName2}"...`
61
137
  );
62
- const apiToken = secrets.CLOUDFLARE_API_TOKEN;
63
- const accountId = secrets.CLOUDFLARE_ACCOUNT_ID;
138
+ const apiToken2 = secrets.CLOUDFLARE_API_TOKEN;
139
+ const accountId2 = secrets.CLOUDFLARE_ACCOUNT_ID;
64
140
  const listRes = await fetch(
65
- `https://api.cloudflare.com/client/v4/accounts/${accountId}/pages/projects/${projectName}/domains`,
141
+ `https://api.cloudflare.com/client/v4/accounts/${accountId2}/pages/projects/${projectName2}/domains`,
66
142
  {
67
143
  headers: {
68
- Authorization: `Bearer ${apiToken}`,
144
+ Authorization: `Bearer ${apiToken2}`,
69
145
  "Content-Type": "application/json"
70
146
  }
71
147
  }
@@ -83,11 +159,11 @@ var CloudflareProvider = class {
83
159
  }
84
160
  logger.info(`[Cloudflare Pages] Linking domain ${domain}...`);
85
161
  const linkRes = await fetch(
86
- `https://api.cloudflare.com/client/v4/accounts/${accountId}/pages/projects/${projectName}/domains`,
162
+ `https://api.cloudflare.com/client/v4/accounts/${accountId2}/pages/projects/${projectName2}/domains`,
87
163
  {
88
164
  method: "POST",
89
165
  headers: {
90
- Authorization: `Bearer ${apiToken}`,
166
+ Authorization: `Bearer ${apiToken2}`,
91
167
  "Content-Type": "application/json"
92
168
  },
93
169
  body: JSON.stringify({ name: domain })
@@ -209,10 +285,16 @@ var CloudflareProvider = class {
209
285
  ...secrets,
210
286
  NODE_OPTIONS: `${process.env.NODE_OPTIONS || ""} --dns-result-order=ipv4first`.trim()
211
287
  };
212
- await execAsync(`wrangler pages deploy ${artifactPath} --project-name=${projectName}`, {
213
- cwd: targetDir,
214
- env: processEnv
215
- });
288
+ await this.runWrangler(
289
+ context,
290
+ app,
291
+ ["pages", "deploy", artifactPath, `--project-name=${projectName}`],
292
+ {
293
+ cwd: targetDir,
294
+ env: processEnv,
295
+ commandName: "deploy"
296
+ }
297
+ );
216
298
  logger.success(`Successfully deployed ${app.name} to Cloudflare Pages.`);
217
299
  }
218
300
  getDefaultDnsTarget(app) {
@@ -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: { 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":[]}
1
+ {"version":3,"sources":["../../../../src/deploy/providers/cloudflare.ts"],"sourcesContent":["import path from 'node:path';\nimport fs from 'node:fs';\nimport { logger } from '@nexical/cli-core';\nimport { HostingProvider, DeploymentContext, CIConfig, AppConfig, DeploymentError } from '../types';\nimport { spawnAsync } from '../utils';\n\nexport interface CloudflareConfig {\n token?: string;\n account?: string;\n}\n\nexport class CloudflareProvider implements HostingProvider {\n name = 'cloudflare';\n\n private async runWrangler(\n context: DeploymentContext,\n app: AppConfig,\n args: string[],\n options: { cwd?: string; env?: NodeJS.ProcessEnv; commandName: string },\n retries = 5,\n ): Promise<void> {\n const debug = !!context.options.debug;\n const logsDir = path.resolve(context.cwd, 'logs');\n\n if (!debug) {\n await fs.promises.mkdir(logsDir, { recursive: true });\n }\n\n const logFile = path.resolve(logsDir, `cloudflare-${app.name}-${options.commandName}.log`);\n\n if (!debug) {\n logger.info(`Log file: ${logFile}`);\n }\n\n let attempt = 0;\n while (attempt <= retries) {\n try {\n await spawnAsync('wrangler', args, {\n cwd: options.cwd || context.cwd,\n env: options.env,\n debug,\n logFile,\n });\n return;\n } catch (err: unknown) {\n attempt++;\n const error = err as DeploymentError;\n const message = error.message || String(err);\n const output = error.output || '';\n const combined = `${message}\\n${output}`;\n\n // Use regex for more robust detection, ignoring case and potential ANSI codes\n const transientRegex =\n /503|connection termination|upstream connect error|reset by peer|malformed response|Service Unavailable/i;\n const isTransient = transientRegex.test(combined);\n\n if (debug) {\n logger.info(`Command failed on attempt ${attempt}. Checking for transient error...`);\n logger.info(`Transient match: ${isTransient}`);\n if (!isTransient) {\n logger.debug(`Full failed output context:\\n${combined}`);\n }\n }\n\n if (isTransient && attempt <= retries) {\n const delay = Math.min(Math.pow(2, attempt) * 2000, 30000); // Max 30s delay\n logger.warn(\n `Cloudflare API returned transient error (Attempt ${attempt}/${retries}). Retrying in ${delay / 1000}s...`,\n );\n await new Promise((resolve) => setTimeout(resolve, delay));\n continue;\n }\n throw err;\n }\n }\n }\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 const projectName = env === 'production' ? baseProjectName : `${baseProjectName}-${env}`;\n const apiToken = secrets.CLOUDFLARE_API_TOKEN;\n const accountId = secrets.CLOUDFLARE_ACCOUNT_ID;\n\n logger.info(`Ensuring Cloudflare Pages project \"${projectName}\" exists...`);\n\n // Check if project exists via API first to avoid unnecessary creation attempts\n const projectRes = await fetch(\n `https://api.cloudflare.com/client/v4/accounts/${accountId}/pages/projects/${projectName}`,\n {\n headers: {\n Authorization: `Bearer ${apiToken}`,\n 'Content-Type': 'application/json',\n },\n },\n );\n\n if (projectRes.ok) {\n logger.info(`Cloudflare project \"${projectName}\" already exists.`);\n } else {\n try {\n await this.runWrangler(\n context,\n app,\n ['pages', 'project', 'create', projectName, '--production-branch', 'main'],\n {\n env: processEnv,\n commandName: 'provision',\n },\n );\n } catch (err: unknown) {\n const error = err as DeploymentError;\n const message = error.message || String(err);\n const output = error.output || '';\n const combined = `${message}\\n${output}`.toLowerCase();\n\n if (combined.includes('already exists') || combined.includes('already_exists')) {\n logger.info('Cloudflare project already exists (detected after creation attempt).');\n } else {\n throw err;\n }\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 this.runWrangler(\n context,\n app,\n ['pages', 'deploy', artifactPath, `--project-name=${projectName}`],\n {\n cwd: targetDir,\n env: processEnv,\n commandName: 'deploy',\n },\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":["projectName","apiToken","accountId"]}
@@ -4,7 +4,7 @@ import {
4
4
  } from "../../../chunk-G66GMEFE.js";
5
5
  import {
6
6
  execAsync
7
- } from "../../../chunk-VKE7R2EZ.js";
7
+ } from "../../../chunk-IPBEZWE2.js";
8
8
  import {
9
9
  init_esm_shims
10
10
  } from "../../../chunk-6DE5Q66O.js";
@@ -1,7 +1,7 @@
1
1
  import { createRequire } from "module"; const require = createRequire(import.meta.url);
2
2
  import {
3
3
  execAsync
4
- } from "../../../chunk-VKE7R2EZ.js";
4
+ } from "../../../chunk-IPBEZWE2.js";
5
5
  import {
6
6
  init_esm_shims
7
7
  } from "../../../chunk-6DE5Q66O.js";
@@ -94,6 +94,13 @@ 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;
97
104
  apps?: Record<string, z.objectOutputType<{
98
105
  provider: z.ZodString;
99
106
  projectName: z.ZodOptional<z.ZodString>;
@@ -107,14 +114,14 @@ declare const DeploymentSchema: z.ZodObject<{
107
114
  domain: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodArray<z.ZodString, "many">]>>;
108
115
  dnsTarget: z.ZodOptional<z.ZodString>;
109
116
  }, z.ZodTypeAny, "passthrough">> | undefined;
110
- dns?: z.objectOutputType<{
117
+ }, {
118
+ dns?: z.objectInputType<{
111
119
  provider: z.ZodString;
112
120
  }, z.ZodTypeAny, "passthrough"> | undefined;
113
121
  repository?: {
114
122
  provider: string;
115
123
  options?: Record<string, any> | undefined;
116
124
  } | undefined;
117
- }, {
118
125
  apps?: Record<string, z.objectInputType<{
119
126
  provider: z.ZodString;
120
127
  projectName: z.ZodOptional<z.ZodString>;
@@ -128,16 +135,16 @@ declare const DeploymentSchema: z.ZodObject<{
128
135
  domain: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodArray<z.ZodString, "many">]>>;
129
136
  dnsTarget: z.ZodOptional<z.ZodString>;
130
137
  }, z.ZodTypeAny, "passthrough">> | undefined;
131
- dns?: z.objectInputType<{
138
+ }>>;
139
+ }, "strip", z.ZodTypeAny, {
140
+ deploy?: {
141
+ dns?: z.objectOutputType<{
132
142
  provider: z.ZodString;
133
143
  }, z.ZodTypeAny, "passthrough"> | undefined;
134
144
  repository?: {
135
145
  provider: string;
136
146
  options?: Record<string, any> | undefined;
137
147
  } | undefined;
138
- }>>;
139
- }, "strip", z.ZodTypeAny, {
140
- deploy?: {
141
148
  apps?: Record<string, z.objectOutputType<{
142
149
  provider: z.ZodString;
143
150
  projectName: z.ZodOptional<z.ZodString>;
@@ -151,16 +158,16 @@ declare const DeploymentSchema: z.ZodObject<{
151
158
  domain: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodArray<z.ZodString, "many">]>>;
152
159
  dnsTarget: z.ZodOptional<z.ZodString>;
153
160
  }, z.ZodTypeAny, "passthrough">> | undefined;
154
- dns?: z.objectOutputType<{
161
+ } | undefined;
162
+ }, {
163
+ deploy?: {
164
+ dns?: z.objectInputType<{
155
165
  provider: z.ZodString;
156
166
  }, z.ZodTypeAny, "passthrough"> | undefined;
157
167
  repository?: {
158
168
  provider: string;
159
169
  options?: Record<string, any> | undefined;
160
170
  } | undefined;
161
- } | undefined;
162
- }, {
163
- deploy?: {
164
171
  apps?: Record<string, z.objectInputType<{
165
172
  provider: z.ZodString;
166
173
  projectName: z.ZodOptional<z.ZodString>;
@@ -174,13 +181,6 @@ declare const DeploymentSchema: z.ZodObject<{
174
181
  domain: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodArray<z.ZodString, "many">]>>;
175
182
  dnsTarget: z.ZodOptional<z.ZodString>;
176
183
  }, 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>;
@@ -1,3 +1,7 @@
1
+ interface DeploymentError extends Error {
2
+ output?: string;
3
+ code?: number | null;
4
+ }
1
5
  interface CIConfig {
2
6
  secrets: string[];
3
7
  variables: string[];
@@ -69,4 +73,4 @@ interface DnsProvider {
69
73
  provision(context: DeploymentContext, records: DnsRecord[]): Promise<void>;
70
74
  }
71
75
 
72
- export type { AppConfig, CIConfig, DeploymentContext, DnsProvider, DnsRecord, HostingProvider, NexicalConfig, RepositoryProvider };
76
+ export type { AppConfig, CIConfig, DeploymentContext, DeploymentError, DnsProvider, DnsRecord, HostingProvider, NexicalConfig, RepositoryProvider };
@@ -1 +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 AppConfig {\n name: string;\n provider: string;\n projectName?: string;\n target?: string;\n buildCommand?: string;\n artifactPath?: string;\n paths?: string[];\n options?: Record<string, unknown>;\n env?: Record<string, string>;\n secrets?: Record<string, string>;\n domain?: string | string[];\n dnsTarget?: string;\n [key: string]: unknown;\n}\n\nexport interface DeploymentContext {\n cwd: string;\n config: NexicalConfig;\n options: Record<string, unknown>;\n}\n\nexport interface HostingProvider {\n name: string;\n\n // Interactive or automatic setup of the provider resources\n provision(context: DeploymentContext, app: AppConfig): Promise<void>;\n\n // Returns the CI configuration for this provider\n getCIConfig(repoType: 'github' | 'gitlab', app: AppConfig): 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, app: AppConfig): 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, app: AppConfig): Promise<Record<string, string>>;\n\n // Performs a manual build/deployment from the local machine\n deploy?(context: DeploymentContext, app: AppConfig): Promise<void>;\n\n // Optional: Automatically infer the DnsTarget from the hosting configuration\n getDefaultDnsTarget?(app: AppConfig): string | undefined;\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(\n context: DeploymentContext,\n targets: { provider: HostingProvider; app: AppConfig }[],\n ): Promise<void>;\n}\n\nexport interface NexicalConfig {\n deploy?: {\n apps?: Record<string, Omit<AppConfig, 'name'>>;\n repository?: {\n provider: string;\n options?: Record<string, unknown>;\n };\n dns?: {\n provider: string;\n [key: string]: unknown;\n };\n };\n}\n\nexport interface DnsRecord {\n type: string;\n name: string;\n content: string;\n proxied?: boolean;\n}\n\nexport interface DnsProvider {\n name: string;\n type?: 'dns';\n provision(context: DeploymentContext, records: DnsRecord[]): Promise<void>;\n}\n"],"mappings":";;;;;;AAAA;","names":[]}
1
+ {"version":3,"sources":["../../../src/deploy/types.ts"],"sourcesContent":["export interface DeploymentError extends Error {\n output?: string;\n code?: number | null;\n}\n\nexport 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 AppConfig {\n name: string;\n provider: string;\n projectName?: string;\n target?: string;\n buildCommand?: string;\n artifactPath?: string;\n paths?: string[];\n options?: Record<string, unknown>;\n env?: Record<string, string>;\n secrets?: Record<string, string>;\n domain?: string | string[];\n dnsTarget?: string;\n [key: string]: unknown;\n}\n\nexport interface DeploymentContext {\n cwd: string;\n config: NexicalConfig;\n options: Record<string, unknown>;\n}\n\nexport interface HostingProvider {\n name: string;\n\n // Interactive or automatic setup of the provider resources\n provision(context: DeploymentContext, app: AppConfig): Promise<void>;\n\n // Returns the CI configuration for this provider\n getCIConfig(repoType: 'github' | 'gitlab', app: AppConfig): 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, app: AppConfig): 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, app: AppConfig): Promise<Record<string, string>>;\n\n // Performs a manual build/deployment from the local machine\n deploy?(context: DeploymentContext, app: AppConfig): Promise<void>;\n\n // Optional: Automatically infer the DnsTarget from the hosting configuration\n getDefaultDnsTarget?(app: AppConfig): string | undefined;\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(\n context: DeploymentContext,\n targets: { provider: HostingProvider; app: AppConfig }[],\n ): Promise<void>;\n}\n\nexport interface NexicalConfig {\n deploy?: {\n apps?: Record<string, Omit<AppConfig, 'name'>>;\n repository?: {\n provider: string;\n options?: Record<string, unknown>;\n };\n dns?: {\n provider: string;\n [key: string]: unknown;\n };\n };\n}\n\nexport interface DnsRecord {\n type: string;\n name: string;\n content: string;\n proxied?: boolean;\n}\n\nexport interface DnsProvider {\n name: string;\n type?: 'dns';\n provision(context: DeploymentContext, records: DnsRecord[]): Promise<void>;\n}\n"],"mappings":";;;;;;AAAA;","names":[]}
@@ -2,5 +2,12 @@ import { exec } from 'node:child_process';
2
2
 
3
3
  declare const execAsync: typeof exec.__promisify__;
4
4
  declare function checkCommand(command: string): Promise<boolean>;
5
+ interface SpawnOptions {
6
+ cwd?: string;
7
+ env?: NodeJS.ProcessEnv;
8
+ logFile?: string;
9
+ debug?: boolean;
10
+ }
11
+ declare function spawnAsync(command: string, args: string[], options?: SpawnOptions): Promise<void>;
5
12
 
6
- export { checkCommand, execAsync };
13
+ export { type SpawnOptions, checkCommand, execAsync, spawnAsync };
@@ -1,11 +1,13 @@
1
1
  import { createRequire } from "module"; const require = createRequire(import.meta.url);
2
2
  import {
3
3
  checkCommand,
4
- execAsync
5
- } from "../../chunk-VKE7R2EZ.js";
4
+ execAsync,
5
+ spawnAsync
6
+ } from "../../chunk-IPBEZWE2.js";
6
7
  import "../../chunk-6DE5Q66O.js";
7
8
  export {
8
9
  checkCommand,
9
- execAsync
10
+ execAsync,
11
+ spawnAsync
10
12
  };
11
13
  //# sourceMappingURL=utils.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nexical/cli",
3
- "version": "0.12.2",
3
+ "version": "0.12.3",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "bin": {
@@ -61,6 +61,7 @@
61
61
  "tsx": "^4.21.0",
62
62
  "typescript": "^5.9.3",
63
63
  "typescript-eslint": "^8.56.0",
64
- "vitest": "^4.0.18"
64
+ "vitest": "^4.0.18",
65
+ "wrangler": "4.68.1"
65
66
  }
66
67
  }
@@ -92,6 +92,8 @@ PROCESS:
92
92
  apps = filteredApps;
93
93
  }
94
94
 
95
+ this.info(`Selected applications: ${apps.map((a) => a.name).join(', ')}`);
96
+
95
97
  if (apps.length === 0) {
96
98
  this.error('No applications found in nexical.yaml. Please configure [deploy.apps].');
97
99
  }
@@ -1,7 +1,8 @@
1
1
  import path from 'node:path';
2
+ import fs from 'node:fs';
2
3
  import { logger } from '@nexical/cli-core';
3
- import { HostingProvider, DeploymentContext, CIConfig, AppConfig } from '../types';
4
- import { execAsync } from '../utils';
4
+ import { HostingProvider, DeploymentContext, CIConfig, AppConfig, DeploymentError } from '../types';
5
+ import { spawnAsync } from '../utils';
5
6
 
6
7
  export interface CloudflareConfig {
7
8
  token?: string;
@@ -11,6 +12,69 @@ export interface CloudflareConfig {
11
12
  export class CloudflareProvider implements HostingProvider {
12
13
  name = 'cloudflare';
13
14
 
15
+ private async runWrangler(
16
+ context: DeploymentContext,
17
+ app: AppConfig,
18
+ args: string[],
19
+ options: { cwd?: string; env?: NodeJS.ProcessEnv; commandName: string },
20
+ retries = 5,
21
+ ): Promise<void> {
22
+ const debug = !!context.options.debug;
23
+ const logsDir = path.resolve(context.cwd, 'logs');
24
+
25
+ if (!debug) {
26
+ await fs.promises.mkdir(logsDir, { recursive: true });
27
+ }
28
+
29
+ const logFile = path.resolve(logsDir, `cloudflare-${app.name}-${options.commandName}.log`);
30
+
31
+ if (!debug) {
32
+ logger.info(`Log file: ${logFile}`);
33
+ }
34
+
35
+ let attempt = 0;
36
+ while (attempt <= retries) {
37
+ try {
38
+ await spawnAsync('wrangler', args, {
39
+ cwd: options.cwd || context.cwd,
40
+ env: options.env,
41
+ debug,
42
+ logFile,
43
+ });
44
+ return;
45
+ } catch (err: unknown) {
46
+ attempt++;
47
+ const error = err as DeploymentError;
48
+ const message = error.message || String(err);
49
+ const output = error.output || '';
50
+ const combined = `${message}\n${output}`;
51
+
52
+ // Use regex for more robust detection, ignoring case and potential ANSI codes
53
+ const transientRegex =
54
+ /503|connection termination|upstream connect error|reset by peer|malformed response|Service Unavailable/i;
55
+ const isTransient = transientRegex.test(combined);
56
+
57
+ if (debug) {
58
+ logger.info(`Command failed on attempt ${attempt}. Checking for transient error...`);
59
+ logger.info(`Transient match: ${isTransient}`);
60
+ if (!isTransient) {
61
+ logger.debug(`Full failed output context:\n${combined}`);
62
+ }
63
+ }
64
+
65
+ if (isTransient && attempt <= retries) {
66
+ const delay = Math.min(Math.pow(2, attempt) * 2000, 30000); // Max 30s delay
67
+ logger.warn(
68
+ `Cloudflare API returned transient error (Attempt ${attempt}/${retries}). Retrying in ${delay / 1000}s...`,
69
+ );
70
+ await new Promise((resolve) => setTimeout(resolve, delay));
71
+ continue;
72
+ }
73
+ throw err;
74
+ }
75
+ }
76
+ }
77
+
14
78
  async provision(context: DeploymentContext, app: AppConfig): Promise<void> {
15
79
  const env = (context.options.env as string) || 'production';
16
80
  const baseProjectName = app.projectName;
@@ -47,17 +111,47 @@ export class CloudflareProvider implements HostingProvider {
47
111
  ...secrets,
48
112
  NODE_OPTIONS: `${process.env.NODE_OPTIONS || ''} --dns-result-order=ipv4first`.trim(),
49
113
  };
114
+ const projectName = env === 'production' ? baseProjectName : `${baseProjectName}-${env}`;
115
+ const apiToken = secrets.CLOUDFLARE_API_TOKEN;
116
+ const accountId = secrets.CLOUDFLARE_ACCOUNT_ID;
117
+
50
118
  logger.info(`Ensuring Cloudflare Pages project "${projectName}" exists...`);
51
- try {
52
- await execAsync(`wrangler pages project create ${projectName} --production-branch main`, {
53
- env: processEnv,
54
- });
55
- } catch (err: unknown) {
56
- const message = err instanceof Error ? err.message : String(err);
57
- if (message.includes('already exists')) {
58
- logger.info('Cloudflare project already exists.');
59
- } else {
60
- throw err;
119
+
120
+ // Check if project exists via API first to avoid unnecessary creation attempts
121
+ const projectRes = await fetch(
122
+ `https://api.cloudflare.com/client/v4/accounts/${accountId}/pages/projects/${projectName}`,
123
+ {
124
+ headers: {
125
+ Authorization: `Bearer ${apiToken}`,
126
+ 'Content-Type': 'application/json',
127
+ },
128
+ },
129
+ );
130
+
131
+ if (projectRes.ok) {
132
+ logger.info(`Cloudflare project "${projectName}" already exists.`);
133
+ } else {
134
+ try {
135
+ await this.runWrangler(
136
+ context,
137
+ app,
138
+ ['pages', 'project', 'create', projectName, '--production-branch', 'main'],
139
+ {
140
+ env: processEnv,
141
+ commandName: 'provision',
142
+ },
143
+ );
144
+ } catch (err: unknown) {
145
+ const error = err as DeploymentError;
146
+ const message = error.message || String(err);
147
+ const output = error.output || '';
148
+ const combined = `${message}\n${output}`.toLowerCase();
149
+
150
+ if (combined.includes('already exists') || combined.includes('already_exists')) {
151
+ logger.info('Cloudflare project already exists (detected after creation attempt).');
152
+ } else {
153
+ throw err;
154
+ }
61
155
  }
62
156
  }
63
157
 
@@ -258,10 +352,16 @@ export class CloudflareProvider implements HostingProvider {
258
352
  NODE_OPTIONS: `${process.env.NODE_OPTIONS || ''} --dns-result-order=ipv4first`.trim(),
259
353
  };
260
354
 
261
- await execAsync(`wrangler pages deploy ${artifactPath} --project-name=${projectName}`, {
262
- cwd: targetDir,
263
- env: processEnv,
264
- });
355
+ await this.runWrangler(
356
+ context,
357
+ app,
358
+ ['pages', 'deploy', artifactPath, `--project-name=${projectName}`],
359
+ {
360
+ cwd: targetDir,
361
+ env: processEnv,
362
+ commandName: 'deploy',
363
+ },
364
+ );
265
365
 
266
366
  logger.success(`Successfully deployed ${app.name} to Cloudflare Pages.`);
267
367
  }
@@ -1,3 +1,8 @@
1
+ export interface DeploymentError extends Error {
2
+ output?: string;
3
+ code?: number | null;
4
+ }
5
+
1
6
  export interface CIConfig {
2
7
  secrets: string[];
3
8
  variables: string[];
@@ -1,5 +1,7 @@
1
- import { exec } from 'node:child_process';
1
+ import { exec, spawn } from 'node:child_process';
2
2
  import { promisify } from 'node:util';
3
+ import { createWriteStream } from 'node:fs';
4
+ import { DeploymentError } from './types';
3
5
 
4
6
  export const execAsync = promisify(exec);
5
7
 
@@ -11,3 +13,64 @@ export async function checkCommand(command: string): Promise<boolean> {
11
13
  return false;
12
14
  }
13
15
  }
16
+
17
+ export interface SpawnOptions {
18
+ cwd?: string;
19
+ env?: NodeJS.ProcessEnv;
20
+ logFile?: string;
21
+ debug?: boolean;
22
+ }
23
+
24
+ export async function spawnAsync(
25
+ command: string,
26
+ args: string[],
27
+ options: SpawnOptions = {},
28
+ ): Promise<void> {
29
+ const { cwd, env, logFile, debug } = options;
30
+
31
+ return new Promise((resolve, reject) => {
32
+ const output: string[] = [];
33
+ const fullCommand = args.length > 0 ? `${command} ${args.join(' ')}` : command;
34
+ const child = spawn(fullCommand, {
35
+ cwd,
36
+ env,
37
+ shell: true,
38
+ stdio: ['ignore', 'pipe', 'pipe'],
39
+ });
40
+
41
+ const logStream = !debug && logFile ? createWriteStream(logFile, { flags: 'a' }) : null;
42
+
43
+ child.stdout?.on('data', (data) => {
44
+ const str = data.toString();
45
+ output.push(str);
46
+ if (debug) process.stdout.write(data);
47
+ if (logStream) logStream.write(data);
48
+ });
49
+
50
+ child.stderr?.on('data', (data) => {
51
+ const str = data.toString();
52
+ output.push(str);
53
+ if (debug) process.stderr.write(data);
54
+ if (logStream) logStream.write(data);
55
+ });
56
+
57
+ child.on('close', (code) => {
58
+ if (logStream) logStream.end();
59
+ if (code === 0) {
60
+ resolve();
61
+ } else {
62
+ const fullOutput = output.join('');
63
+ const error = new Error(
64
+ `Command failed with code ${code}: ${fullCommand}`,
65
+ ) as DeploymentError;
66
+ error.output = fullOutput;
67
+ error.code = code;
68
+ reject(error);
69
+ }
70
+ });
71
+
72
+ child.on('error', (err) => {
73
+ reject(err);
74
+ });
75
+ });
76
+ }
@@ -1,10 +1,38 @@
1
1
  import { describe, it, expect, vi, beforeEach, afterEach, type Mock } from 'vitest';
2
2
  import { CloudflareProvider } from '../../../../src/deploy/providers/cloudflare.js';
3
- import { execAsync } from '../../../../src/deploy/utils.js';
3
+ import { execAsync, spawnAsync } from '../../../../src/deploy/utils.js';
4
4
  import { logger } from '@nexical/cli-core';
5
+ import fs from 'node:fs';
5
6
  import { DeploymentContext, AppConfig } from '../../../../src/deploy/types.js';
6
7
 
7
- vi.mock('../../../../src/deploy/utils.js');
8
+ vi.mock('../../../../src/deploy/utils.js', () => ({
9
+ execAsync: vi.fn(),
10
+ spawnAsync: vi.fn(),
11
+ }));
12
+ vi.mock('node:fs', async (importOriginal) => {
13
+ const actual = await importOriginal<typeof import('node:fs')>();
14
+ const mockMkdir = vi.fn();
15
+
16
+ // We need to handle the case where default might or might not exist on the actual object
17
+ // cast to unknown first to safely check/access properties that TS doesn't see on the module type
18
+ const actualObj = actual as unknown as Record<string, unknown>;
19
+ const actualDefault = (actualObj.default as typeof import('node:fs')) || actual;
20
+
21
+ return {
22
+ ...actual,
23
+ promises: {
24
+ ...actual.promises,
25
+ mkdir: mockMkdir,
26
+ },
27
+ default: {
28
+ ...actualDefault,
29
+ promises: {
30
+ ...actual.promises,
31
+ mkdir: mockMkdir,
32
+ },
33
+ },
34
+ };
35
+ });
8
36
  vi.mock('@nexical/cli-core', () => ({
9
37
  logger: {
10
38
  info: vi.fn(),
@@ -33,12 +61,14 @@ describe('CloudflareProvider', () => {
33
61
  },
34
62
  },
35
63
  },
36
- } as any,
64
+ } as unknown as DeploymentContext['config'],
37
65
  };
38
66
  (execAsync as Mock).mockResolvedValue({
39
67
  stdout: '',
40
68
  stderr: '',
41
69
  });
70
+ (spawnAsync as Mock).mockResolvedValue(undefined);
71
+ (fs.promises.mkdir as Mock).mockResolvedValue(undefined);
42
72
  });
43
73
 
44
74
  afterEach(() => {
@@ -60,33 +90,47 @@ describe('CloudflareProvider', () => {
60
90
  const app = { name: 'frontend', provider: 'cloudflare', projectName: 'my-app' } as AppConfig;
61
91
  await provider.provision(mockContext, app);
62
92
  expect(logger.info).toHaveBeenCalledWith(expect.stringContaining('[Dry Run]'));
63
- expect(execAsync).not.toHaveBeenCalled();
93
+ expect(spawnAsync).not.toHaveBeenCalled();
64
94
  });
65
95
 
66
96
  it('should skip if credentials missing', async () => {
67
97
  const app = { name: 'frontend', provider: 'cloudflare', projectName: 'my-app' } as AppConfig;
68
98
  await provider.provision(mockContext, app);
69
99
  expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining('credentials missing'));
70
- expect(execAsync).not.toHaveBeenCalled();
100
+ expect(spawnAsync).not.toHaveBeenCalled();
71
101
  });
72
102
 
73
103
  it('should provision successfully using default env vars', async () => {
74
104
  process.env.CLOUDFLARE_API_TOKEN = 'tok';
75
105
  process.env.CLOUDFLARE_ACCOUNT_ID = 'acc';
106
+ const mockFetch = vi.fn();
107
+ vi.stubGlobal('fetch', mockFetch);
108
+
109
+ // Project check - project does NOT exist
110
+ mockFetch.mockResolvedValueOnce({ ok: false });
111
+
76
112
  const app = { name: 'frontend', provider: 'cloudflare', projectName: 'my-app' } as AppConfig;
77
113
 
78
114
  await provider.provision(mockContext, app);
79
115
 
80
- expect(execAsync).toHaveBeenCalledWith(
81
- expect.stringContaining('wrangler pages project create my-app --production-branch main'),
116
+ expect(spawnAsync).toHaveBeenCalledWith(
117
+ 'wrangler',
118
+ expect.arrayContaining(['pages', 'project', 'create', 'my-app']),
82
119
  expect.anything(),
83
120
  );
121
+ vi.unstubAllGlobals();
84
122
  });
85
123
 
86
124
  it('should swallow "project already exists" error', async () => {
87
125
  process.env.CLOUDFLARE_API_TOKEN = 'tok';
88
126
  process.env.CLOUDFLARE_ACCOUNT_ID = 'acc';
89
- (execAsync as Mock).mockRejectedValueOnce(
127
+ const mockFetch = vi.fn();
128
+ vi.stubGlobal('fetch', mockFetch);
129
+
130
+ // Project check - project does NOT exist (simulated race condition where it's created between check and create)
131
+ mockFetch.mockResolvedValueOnce({ ok: false });
132
+
133
+ (spawnAsync as Mock).mockRejectedValueOnce(
90
134
  new Error('A pages project with this name already exists.'),
91
135
  );
92
136
  const app = { name: 'frontend', provider: 'cloudflare', projectName: 'my-app' } as AppConfig;
@@ -96,16 +140,23 @@ describe('CloudflareProvider', () => {
96
140
  expect(logger.info).toHaveBeenCalledWith(
97
141
  expect.stringContaining('Cloudflare project already exists'),
98
142
  );
143
+ vi.unstubAllGlobals();
99
144
  });
100
145
 
101
146
  it('should rethrow critical provisioning errors', async () => {
102
147
  process.env.CLOUDFLARE_API_TOKEN = 'tok';
103
148
  process.env.CLOUDFLARE_ACCOUNT_ID = 'acc';
149
+ const mockFetch = vi.fn();
150
+ vi.stubGlobal('fetch', mockFetch);
151
+
152
+ // Project check - project does NOT exist
153
+ mockFetch.mockResolvedValueOnce({ ok: false });
104
154
 
105
- (execAsync as Mock).mockRejectedValueOnce(new Error('Critical error'));
155
+ (spawnAsync as Mock).mockRejectedValueOnce(new Error('Critical error'));
106
156
  const app = { name: 'frontend', provider: 'cloudflare', projectName: 'my-app' } as AppConfig;
107
157
 
108
158
  await expect(provider.provision(mockContext, app)).rejects.toThrow('Critical error');
159
+ vi.unstubAllGlobals();
109
160
  });
110
161
 
111
162
  it('should link custom domains during provision', async () => {
@@ -115,7 +166,13 @@ describe('CloudflareProvider', () => {
115
166
  const mockFetch = vi.fn();
116
167
  vi.stubGlobal('fetch', mockFetch);
117
168
 
118
- // First call (GET) - return one existing domain
169
+ // First call (GET project) - project exists
170
+ mockFetch.mockResolvedValueOnce({
171
+ ok: true,
172
+ json: async () => ({ success: true }),
173
+ });
174
+
175
+ // Second call (GET domains) - return one existing domain
119
176
  mockFetch.mockResolvedValueOnce({
120
177
  ok: true,
121
178
  json: async () => ({
@@ -124,7 +181,7 @@ describe('CloudflareProvider', () => {
124
181
  }),
125
182
  });
126
183
 
127
- // Second call (POST) - link new-domain.com
184
+ // Third call (POST domain) - link new-domain.com
128
185
  mockFetch.mockResolvedValueOnce({
129
186
  ok: true,
130
187
  json: async () => ({
@@ -142,14 +199,14 @@ describe('CloudflareProvider', () => {
142
199
 
143
200
  await provider.provision(mockContext, app);
144
201
 
145
- expect(mockFetch).toHaveBeenCalledTimes(2);
202
+ expect(mockFetch).toHaveBeenCalledTimes(3);
146
203
  expect(mockFetch).toHaveBeenNthCalledWith(
147
204
  1,
148
- expect.stringContaining('/pages/projects/my-app/domains'),
205
+ expect.stringContaining('/pages/projects/my-app'),
149
206
  expect.anything(),
150
207
  );
151
208
  expect(mockFetch).toHaveBeenNthCalledWith(
152
- 2,
209
+ 3,
153
210
  expect.stringContaining('/pages/projects/my-app/domains'),
154
211
  expect.objectContaining({
155
212
  method: 'POST',
@@ -170,7 +227,13 @@ describe('CloudflareProvider', () => {
170
227
  const mockFetch = vi.fn();
171
228
  vi.stubGlobal('fetch', mockFetch);
172
229
 
173
- // First call (GET) - return empty existing domains
230
+ // First call (GET project) - project exists
231
+ mockFetch.mockResolvedValueOnce({
232
+ ok: true,
233
+ json: async () => ({ success: true }),
234
+ });
235
+
236
+ // Second call (GET domains) - return empty existing domains
174
237
  mockFetch.mockResolvedValueOnce({
175
238
  ok: true,
176
239
  json: async () => ({
@@ -179,7 +242,7 @@ describe('CloudflareProvider', () => {
179
242
  }),
180
243
  });
181
244
 
182
- // Second call (POST) - return "already added" error
245
+ // Third call (POST domain) - return "already added" error
183
246
  mockFetch.mockResolvedValueOnce({
184
247
  ok: false,
185
248
  text: async () =>
@@ -237,8 +300,9 @@ describe('CloudflareProvider', () => {
237
300
 
238
301
  await provider.deploy(mockContext, app);
239
302
 
240
- expect(execAsync).toHaveBeenCalledWith(
241
- expect.stringContaining('wrangler pages deploy dist --project-name=my-app'),
303
+ expect(spawnAsync).toHaveBeenCalledWith(
304
+ 'wrangler',
305
+ expect.arrayContaining(['pages', 'deploy', 'dist', '--project-name=my-app']),
242
306
  expect.anything(),
243
307
  );
244
308
  });
@@ -248,7 +312,7 @@ describe('CloudflareProvider', () => {
248
312
  const app = { name: 'frontend', provider: 'cloudflare', projectName: 'my-app' } as AppConfig;
249
313
  await provider.deploy(mockContext, app);
250
314
  expect(logger.info).toHaveBeenCalledWith(expect.stringContaining('[Dry Run]'));
251
- expect(execAsync).not.toHaveBeenCalled();
315
+ expect(spawnAsync).not.toHaveBeenCalled();
252
316
  });
253
317
  });
254
318
  });
@@ -1,24 +0,0 @@
1
- import { createRequire } from "module"; const require = createRequire(import.meta.url);
2
- import {
3
- init_esm_shims
4
- } from "./chunk-6DE5Q66O.js";
5
-
6
- // src/deploy/utils.ts
7
- init_esm_shims();
8
- import { exec } from "child_process";
9
- import { promisify } from "util";
10
- var execAsync = promisify(exec);
11
- async function checkCommand(command) {
12
- try {
13
- await execAsync(command);
14
- return true;
15
- } catch {
16
- return false;
17
- }
18
- }
19
-
20
- export {
21
- execAsync,
22
- checkCommand
23
- };
24
- //# sourceMappingURL=chunk-VKE7R2EZ.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/deploy/utils.ts"],"sourcesContent":["import { exec } from 'node:child_process';\nimport { promisify } from 'node:util';\n\nexport const execAsync = promisify(exec);\n\nexport async function checkCommand(command: string): Promise<boolean> {\n try {\n await execAsync(command);\n return true;\n } catch {\n return false;\n }\n}\n"],"mappings":";;;;;;AAAA;AAAA,SAAS,YAAY;AACrB,SAAS,iBAAiB;AAEnB,IAAM,YAAY,UAAU,IAAI;AAEvC,eAAsB,aAAa,SAAmC;AACpE,MAAI;AACF,UAAM,UAAU,OAAO;AACvB,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;","names":[]}