@nexical/cli 0.11.2 → 0.11.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -15,7 +15,7 @@ import { fileURLToPath } from "url";
15
15
  // package.json
16
16
  var package_default = {
17
17
  name: "@nexical/cli",
18
- version: "0.11.2",
18
+ version: "0.11.4",
19
19
  type: "module",
20
20
  bin: {
21
21
  nexical: "./dist/index.js"
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../index.ts","../package.json"],"sourcesContent":["#!/usr/bin/env node\nimport { CLI, findProjectRoot } from '@nexical/cli-core';\nimport { fileURLToPath } from 'node:url';\nimport { discoverCommandDirectories } from './src/utils/discovery.js';\nimport pkg from './package.json';\nimport path from 'node:path';\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\nconst commandName = 'nexical';\nconst projectRoot = (await findProjectRoot(commandName, process.cwd())) || process.cwd();\nconst coreCommandsDir = path.resolve(__dirname, './src/commands');\nconst additionalCommands = discoverCommandDirectories(projectRoot);\n\n// Filter out duplicate core commands and source versions\nconst filteredAdditional = additionalCommands.filter((dir) => {\n const resolvedDir = path.resolve(dir);\n const resolvedCore = path.resolve(coreCommandsDir);\n\n if (resolvedDir === resolvedCore) return false;\n\n // Check if this is another instance of the core CLI commands (by checking path suffix)\n const coreSuffix = path.join('@nexical', 'cli', 'dist', 'src', 'commands');\n const coreSuffixSrc = path.join('packages', 'cli', 'dist', 'src', 'commands');\n const coreSuffixRawSrc = path.join('packages', 'cli', 'src', 'commands');\n\n if (\n resolvedDir.endsWith(coreSuffix) ||\n resolvedDir.endsWith(coreSuffixSrc) ||\n resolvedDir.endsWith(coreSuffixRawSrc)\n ) {\n return false;\n }\n\n // Handle mismatch between dist/src and src/\n if (resolvedCore.includes(path.join(path.sep, 'dist', 'src', 'commands'))) {\n const srcVersion = resolvedCore.replace(\n path.join(path.sep, 'dist', 'src', 'commands'),\n path.join(path.sep, 'src', 'commands'),\n );\n if (resolvedDir === srcVersion) return false;\n }\n\n return true;\n});\n\nconst app = new CLI({\n version: pkg.version,\n commandName: commandName,\n searchDirectories: [...new Set([coreCommandsDir, ...filteredAdditional])],\n});\napp.start();\n","{\n \"name\": \"@nexical/cli\",\n \"version\": \"0.11.2\",\n \"type\": \"module\",\n \"bin\": {\n \"nexical\": \"./dist/index.js\"\n },\n \"scripts\": {\n \"build\": \"tsup\",\n \"dev\": \"tsup --watch\",\n \"start\": \"node dist/index.js\",\n \"test\": \"npm run test:unit && npm run test:integration && npm run test:e2e\",\n \"test:unit\": \"vitest run --config vitest.config.ts --coverage\",\n \"test:integration\": \"vitest run --config vitest.integration.config.ts\",\n \"test:e2e\": \"npm run build && vitest run --config vitest.e2e.config.ts\",\n \"test:watch\": \"vitest\",\n \"format\": \"prettier --write .\",\n \"lint\": \"eslint .\",\n \"lint:fix\": \"eslint . --fix\",\n \"prepare\": \"husky\"\n },\n \"lint-staged\": {\n \"**/*\": [\n \"prettier --write --ignore-unknown\"\n ],\n \"**/*.{js,jsx,ts,tsx,astro}\": [\n \"eslint --fix\"\n ]\n },\n \"dependencies\": {\n \"@nexical/cli-core\": \"^0.1.12\",\n \"yaml\": \"^2.3.4\",\n \"fast-glob\": \"^3.3.3\"\n },\n \"devDependencies\": {\n \"@types/fs-extra\": \"^11.0.4\",\n \"@types/node\": \"^20.10.0\",\n \"@vitest/coverage-v8\": \"^4.0.15\",\n \"execa\": \"^9.6.1\",\n \"fs-extra\": \"^11.3.2\",\n \"tsup\": \"^8.0.1\",\n \"tsx\": \"^4.21.0\",\n \"typescript\": \"^5.3.3\",\n \"vitest\": \"^4.0.15\",\n \"@eslint/js\": \"^9.39.2\",\n \"eslint\": \"^9.39.2\",\n \"eslint-config-prettier\": \"^10.1.8\",\n \"eslint-plugin-astro\": \"^1.5.0\",\n \"eslint-plugin-jsx-a11y\": \"^6.10.2\",\n \"eslint-plugin-react\": \"^7.37.5\",\n \"eslint-plugin-react-hooks\": \"^7.0.1\",\n \"globals\": \"^17.2.0\",\n \"husky\": \"^9.1.7\",\n \"lint-staged\": \"^16.2.7\",\n \"prettier\": \"^3.8.1\",\n \"typescript-eslint\": \"^8.54.0\"\n }\n}\n"],"mappings":";;;;;;;;;;AAAA;AACA,SAAS,KAAK,uBAAuB;AACrC,SAAS,qBAAqB;;;ACF9B;AAAA,EACE,MAAQ;AAAA,EACR,SAAW;AAAA,EACX,MAAQ;AAAA,EACR,KAAO;AAAA,IACL,SAAW;AAAA,EACb;AAAA,EACA,SAAW;AAAA,IACT,OAAS;AAAA,IACT,KAAO;AAAA,IACP,OAAS;AAAA,IACT,MAAQ;AAAA,IACR,aAAa;AAAA,IACb,oBAAoB;AAAA,IACpB,YAAY;AAAA,IACZ,cAAc;AAAA,IACd,QAAU;AAAA,IACV,MAAQ;AAAA,IACR,YAAY;AAAA,IACZ,SAAW;AAAA,EACb;AAAA,EACA,eAAe;AAAA,IACb,QAAQ;AAAA,MACN;AAAA,IACF;AAAA,IACA,8BAA8B;AAAA,MAC5B;AAAA,IACF;AAAA,EACF;AAAA,EACA,cAAgB;AAAA,IACd,qBAAqB;AAAA,IACrB,MAAQ;AAAA,IACR,aAAa;AAAA,EACf;AAAA,EACA,iBAAmB;AAAA,IACjB,mBAAmB;AAAA,IACnB,eAAe;AAAA,IACf,uBAAuB;AAAA,IACvB,OAAS;AAAA,IACT,YAAY;AAAA,IACZ,MAAQ;AAAA,IACR,KAAO;AAAA,IACP,YAAc;AAAA,IACd,QAAU;AAAA,IACV,cAAc;AAAA,IACd,QAAU;AAAA,IACV,0BAA0B;AAAA,IAC1B,uBAAuB;AAAA,IACvB,0BAA0B;AAAA,IAC1B,uBAAuB;AAAA,IACvB,6BAA6B;AAAA,IAC7B,SAAW;AAAA,IACX,OAAS;AAAA,IACT,eAAe;AAAA,IACf,UAAY;AAAA,IACZ,qBAAqB;AAAA,EACvB;AACF;;;ADpDA,OAAO,UAAU;AAEjB,IAAM,YAAY,KAAK,QAAQ,cAAc,YAAY,GAAG,CAAC;AAE7D,IAAM,cAAc;AACpB,IAAM,cAAe,MAAM,gBAAgB,aAAa,QAAQ,IAAI,CAAC,KAAM,QAAQ,IAAI;AACvF,IAAM,kBAAkB,KAAK,QAAQ,WAAW,gBAAgB;AAChE,IAAM,qBAAqB,2BAA2B,WAAW;AAGjE,IAAM,qBAAqB,mBAAmB,OAAO,CAAC,QAAQ;AAC5D,QAAM,cAAc,KAAK,QAAQ,GAAG;AACpC,QAAM,eAAe,KAAK,QAAQ,eAAe;AAEjD,MAAI,gBAAgB,aAAc,QAAO;AAGzC,QAAM,aAAa,KAAK,KAAK,YAAY,OAAO,QAAQ,OAAO,UAAU;AACzE,QAAM,gBAAgB,KAAK,KAAK,YAAY,OAAO,QAAQ,OAAO,UAAU;AAC5E,QAAM,mBAAmB,KAAK,KAAK,YAAY,OAAO,OAAO,UAAU;AAEvE,MACE,YAAY,SAAS,UAAU,KAC/B,YAAY,SAAS,aAAa,KAClC,YAAY,SAAS,gBAAgB,GACrC;AACA,WAAO;AAAA,EACT;AAGA,MAAI,aAAa,SAAS,KAAK,KAAK,KAAK,KAAK,QAAQ,OAAO,UAAU,CAAC,GAAG;AACzE,UAAM,aAAa,aAAa;AAAA,MAC9B,KAAK,KAAK,KAAK,KAAK,QAAQ,OAAO,UAAU;AAAA,MAC7C,KAAK,KAAK,KAAK,KAAK,OAAO,UAAU;AAAA,IACvC;AACA,QAAI,gBAAgB,WAAY,QAAO;AAAA,EACzC;AAEA,SAAO;AACT,CAAC;AAED,IAAM,MAAM,IAAI,IAAI;AAAA,EAClB,SAAS,gBAAI;AAAA,EACb;AAAA,EACA,mBAAmB,CAAC,GAAG,oBAAI,IAAI,CAAC,iBAAiB,GAAG,kBAAkB,CAAC,CAAC;AAC1E,CAAC;AACD,IAAI,MAAM;","names":[]}
1
+ {"version":3,"sources":["../index.ts","../package.json"],"sourcesContent":["#!/usr/bin/env node\nimport { CLI, findProjectRoot } from '@nexical/cli-core';\nimport { fileURLToPath } from 'node:url';\nimport { discoverCommandDirectories } from './src/utils/discovery.js';\nimport pkg from './package.json';\nimport path from 'node:path';\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\nconst commandName = 'nexical';\nconst projectRoot = (await findProjectRoot(commandName, process.cwd())) || process.cwd();\nconst coreCommandsDir = path.resolve(__dirname, './src/commands');\nconst additionalCommands = discoverCommandDirectories(projectRoot);\n\n// Filter out duplicate core commands and source versions\nconst filteredAdditional = additionalCommands.filter((dir) => {\n const resolvedDir = path.resolve(dir);\n const resolvedCore = path.resolve(coreCommandsDir);\n\n if (resolvedDir === resolvedCore) return false;\n\n // Check if this is another instance of the core CLI commands (by checking path suffix)\n const coreSuffix = path.join('@nexical', 'cli', 'dist', 'src', 'commands');\n const coreSuffixSrc = path.join('packages', 'cli', 'dist', 'src', 'commands');\n const coreSuffixRawSrc = path.join('packages', 'cli', 'src', 'commands');\n\n if (\n resolvedDir.endsWith(coreSuffix) ||\n resolvedDir.endsWith(coreSuffixSrc) ||\n resolvedDir.endsWith(coreSuffixRawSrc)\n ) {\n return false;\n }\n\n // Handle mismatch between dist/src and src/\n if (resolvedCore.includes(path.join(path.sep, 'dist', 'src', 'commands'))) {\n const srcVersion = resolvedCore.replace(\n path.join(path.sep, 'dist', 'src', 'commands'),\n path.join(path.sep, 'src', 'commands'),\n );\n if (resolvedDir === srcVersion) return false;\n }\n\n return true;\n});\n\nconst app = new CLI({\n version: pkg.version,\n commandName: commandName,\n searchDirectories: [...new Set([coreCommandsDir, ...filteredAdditional])],\n});\napp.start();\n","{\n \"name\": \"@nexical/cli\",\n \"version\": \"0.11.4\",\n \"type\": \"module\",\n \"bin\": {\n \"nexical\": \"./dist/index.js\"\n },\n \"scripts\": {\n \"build\": \"tsup\",\n \"dev\": \"tsup --watch\",\n \"start\": \"node dist/index.js\",\n \"test\": \"npm run test:unit && npm run test:integration && npm run test:e2e\",\n \"test:unit\": \"vitest run --config vitest.config.ts --coverage\",\n \"test:integration\": \"vitest run --config vitest.integration.config.ts\",\n \"test:e2e\": \"npm run build && vitest run --config vitest.e2e.config.ts\",\n \"test:watch\": \"vitest\",\n \"format\": \"prettier --write .\",\n \"lint\": \"eslint .\",\n \"lint:fix\": \"eslint . --fix\",\n \"prepare\": \"husky\"\n },\n \"lint-staged\": {\n \"**/*\": [\n \"prettier --write --ignore-unknown\"\n ],\n \"**/*.{js,jsx,ts,tsx,astro}\": [\n \"eslint --fix\"\n ]\n },\n \"dependencies\": {\n \"@nexical/cli-core\": \"^0.1.12\",\n \"yaml\": \"^2.3.4\",\n \"fast-glob\": \"^3.3.3\"\n },\n \"devDependencies\": {\n \"@types/fs-extra\": \"^11.0.4\",\n \"@types/node\": \"^20.10.0\",\n \"@vitest/coverage-v8\": \"^4.0.15\",\n \"execa\": \"^9.6.1\",\n \"fs-extra\": \"^11.3.2\",\n \"tsup\": \"^8.0.1\",\n \"tsx\": \"^4.21.0\",\n \"typescript\": \"^5.3.3\",\n \"vitest\": \"^4.0.15\",\n \"@eslint/js\": \"^9.39.2\",\n \"eslint\": \"^9.39.2\",\n \"eslint-config-prettier\": \"^10.1.8\",\n \"eslint-plugin-astro\": \"^1.5.0\",\n \"eslint-plugin-jsx-a11y\": \"^6.10.2\",\n \"eslint-plugin-react\": \"^7.37.5\",\n \"eslint-plugin-react-hooks\": \"^7.0.1\",\n \"globals\": \"^17.2.0\",\n \"husky\": \"^9.1.7\",\n \"lint-staged\": \"^16.2.7\",\n \"prettier\": \"^3.8.1\",\n \"typescript-eslint\": \"^8.54.0\"\n }\n}\n"],"mappings":";;;;;;;;;;AAAA;AACA,SAAS,KAAK,uBAAuB;AACrC,SAAS,qBAAqB;;;ACF9B;AAAA,EACE,MAAQ;AAAA,EACR,SAAW;AAAA,EACX,MAAQ;AAAA,EACR,KAAO;AAAA,IACL,SAAW;AAAA,EACb;AAAA,EACA,SAAW;AAAA,IACT,OAAS;AAAA,IACT,KAAO;AAAA,IACP,OAAS;AAAA,IACT,MAAQ;AAAA,IACR,aAAa;AAAA,IACb,oBAAoB;AAAA,IACpB,YAAY;AAAA,IACZ,cAAc;AAAA,IACd,QAAU;AAAA,IACV,MAAQ;AAAA,IACR,YAAY;AAAA,IACZ,SAAW;AAAA,EACb;AAAA,EACA,eAAe;AAAA,IACb,QAAQ;AAAA,MACN;AAAA,IACF;AAAA,IACA,8BAA8B;AAAA,MAC5B;AAAA,IACF;AAAA,EACF;AAAA,EACA,cAAgB;AAAA,IACd,qBAAqB;AAAA,IACrB,MAAQ;AAAA,IACR,aAAa;AAAA,EACf;AAAA,EACA,iBAAmB;AAAA,IACjB,mBAAmB;AAAA,IACnB,eAAe;AAAA,IACf,uBAAuB;AAAA,IACvB,OAAS;AAAA,IACT,YAAY;AAAA,IACZ,MAAQ;AAAA,IACR,KAAO;AAAA,IACP,YAAc;AAAA,IACd,QAAU;AAAA,IACV,cAAc;AAAA,IACd,QAAU;AAAA,IACV,0BAA0B;AAAA,IAC1B,uBAAuB;AAAA,IACvB,0BAA0B;AAAA,IAC1B,uBAAuB;AAAA,IACvB,6BAA6B;AAAA,IAC7B,SAAW;AAAA,IACX,OAAS;AAAA,IACT,eAAe;AAAA,IACf,UAAY;AAAA,IACZ,qBAAqB;AAAA,EACvB;AACF;;;ADpDA,OAAO,UAAU;AAEjB,IAAM,YAAY,KAAK,QAAQ,cAAc,YAAY,GAAG,CAAC;AAE7D,IAAM,cAAc;AACpB,IAAM,cAAe,MAAM,gBAAgB,aAAa,QAAQ,IAAI,CAAC,KAAM,QAAQ,IAAI;AACvF,IAAM,kBAAkB,KAAK,QAAQ,WAAW,gBAAgB;AAChE,IAAM,qBAAqB,2BAA2B,WAAW;AAGjE,IAAM,qBAAqB,mBAAmB,OAAO,CAAC,QAAQ;AAC5D,QAAM,cAAc,KAAK,QAAQ,GAAG;AACpC,QAAM,eAAe,KAAK,QAAQ,eAAe;AAEjD,MAAI,gBAAgB,aAAc,QAAO;AAGzC,QAAM,aAAa,KAAK,KAAK,YAAY,OAAO,QAAQ,OAAO,UAAU;AACzE,QAAM,gBAAgB,KAAK,KAAK,YAAY,OAAO,QAAQ,OAAO,UAAU;AAC5E,QAAM,mBAAmB,KAAK,KAAK,YAAY,OAAO,OAAO,UAAU;AAEvE,MACE,YAAY,SAAS,UAAU,KAC/B,YAAY,SAAS,aAAa,KAClC,YAAY,SAAS,gBAAgB,GACrC;AACA,WAAO;AAAA,EACT;AAGA,MAAI,aAAa,SAAS,KAAK,KAAK,KAAK,KAAK,QAAQ,OAAO,UAAU,CAAC,GAAG;AACzE,UAAM,aAAa,aAAa;AAAA,MAC9B,KAAK,KAAK,KAAK,KAAK,QAAQ,OAAO,UAAU;AAAA,MAC7C,KAAK,KAAK,KAAK,KAAK,OAAO,UAAU;AAAA,IACvC;AACA,QAAI,gBAAgB,WAAY,QAAO;AAAA,EACzC;AAEA,SAAO;AACT,CAAC;AAED,IAAM,MAAM,IAAI,IAAI;AAAA,EAClB,SAAS,gBAAI;AAAA,EACb;AAAA,EACA,mBAAmB,CAAC,GAAG,oBAAI,IAAI,CAAC,iBAAiB,GAAG,kBAAkB,CAAC,CAAC;AAC1E,CAAC;AACD,IAAI,MAAM;","names":[]}
@@ -0,0 +1,35 @@
1
+ import { BaseCommand } from '@nexical/cli-core';
2
+
3
+ interface DeployOptions {
4
+ dryRun: boolean;
5
+ railwayToken?: string;
6
+ railwayName?: string;
7
+ backendName: string;
8
+ frontendName: string;
9
+ cloudflareToken?: string;
10
+ cloudflareAccount?: string;
11
+ }
12
+ declare class DeployCommand extends BaseCommand {
13
+ static description: string;
14
+ static args: {
15
+ options: ({
16
+ name: string;
17
+ description: string;
18
+ default: boolean;
19
+ } | {
20
+ name: string;
21
+ description: string;
22
+ default?: undefined;
23
+ } | {
24
+ name: string;
25
+ description: string;
26
+ default: string;
27
+ })[];
28
+ };
29
+ run(options: DeployOptions): Promise<void>;
30
+ private setupRailway;
31
+ private setupCloudflare;
32
+ private setupGitHubConfig;
33
+ }
34
+
35
+ export { DeployCommand as default };
@@ -0,0 +1,196 @@
1
+ import { createRequire } from "module"; const require = createRequire(import.meta.url);
2
+ import {
3
+ init_esm_shims
4
+ } from "../../chunk-OYFWMYPG.js";
5
+
6
+ // src/commands/deploy.ts
7
+ init_esm_shims();
8
+ import { BaseCommand } from "@nexical/cli-core";
9
+ import { exec } from "child_process";
10
+ import { promisify } from "util";
11
+ import path from "path";
12
+ var execAsync = promisify(exec);
13
+ var DeployCommand = class extends BaseCommand {
14
+ static description = `Deploy the application to Railway and Cloudflare.
15
+
16
+ ENVIRONMENT SETUP & PREREQUISITES:
17
+ 1. Install Required CLIs:
18
+ - Railway CLI: npm i -g @railway/cli
19
+ - Wrangler (Cloudflare): npm i -g wrangler
20
+ - GitHub CLI: https://cli.github.com/
21
+
22
+ 2. Authentication:
23
+ - Railway: Run 'railway login'
24
+ - GitHub: Run 'gh auth login'
25
+ - Cloudflare: Obtain an API Token (with Pages edit permissions) and your Account ID from the dashboard.
26
+
27
+ 3. Configuration:
28
+ Run this command with --cloudflare-token and --cloudflare-account to automate the full setup.
29
+ Optional: Use --railway-token if you prefer not to use the interactive login.
30
+ Optional: Use --railway-name to specify a custom Railway project name.
31
+ Optional: Use --backend-name to specify the Railway service name (default: nexical-backend).
32
+ Optional: Use --frontend-name to specify the Cloudflare Pages project name (default: nexical-frontend).
33
+
34
+ PROCESS:
35
+ - Provisions a PostgreSQL database on Railway (if missing).
36
+ - Creates a Cloudflare Pages project for the frontend.
37
+ - Syncs all necessary deployment secrets and variables to GitHub for CI/CD automation.`;
38
+ static args = {
39
+ options: [
40
+ {
41
+ name: "--dry-run",
42
+ description: "Simulate the deployment process without making changes.",
43
+ default: false
44
+ },
45
+ {
46
+ name: "--railway-token <token>",
47
+ description: "Railway Project Token (optional if already logged in)."
48
+ },
49
+ {
50
+ name: "--railway-name <name>",
51
+ description: "Railway Project Name (used during initialization)."
52
+ },
53
+ {
54
+ name: "--backend-name <name>",
55
+ description: "Backend service name on Railway.",
56
+ default: "nexical-backend"
57
+ },
58
+ {
59
+ name: "--frontend-name <name>",
60
+ description: "Frontend project name on Cloudflare.",
61
+ default: "nexical-frontend"
62
+ },
63
+ {
64
+ name: "--cloudflare-token <token>",
65
+ description: "Cloudflare API Token."
66
+ },
67
+ {
68
+ name: "--cloudflare-account <id>",
69
+ description: "Cloudflare Account ID."
70
+ }
71
+ ]
72
+ };
73
+ async run(options) {
74
+ this.info("Starting Nexical Deployment Automation...");
75
+ if (options.dryRun) {
76
+ this.notice("DRY RUN MODE ENABLED");
77
+ }
78
+ try {
79
+ await this.setupRailway(options);
80
+ await this.setupCloudflare(options);
81
+ await this.setupGitHubConfig(options);
82
+ this.success("Deployment setup complete! Your application is being deployed.");
83
+ } catch (error) {
84
+ if (error instanceof Error) {
85
+ this.error(`Deployment failed: ${error.message}`);
86
+ } else {
87
+ this.error(`Deployment failed: ${String(error)}`);
88
+ }
89
+ process.exit(1);
90
+ }
91
+ }
92
+ async setupRailway(options) {
93
+ this.info("Configuring Railway...");
94
+ if (options.dryRun) {
95
+ const initCmd = options.railwayName ? `railway init --name ${options.railwayName}` : "railway init";
96
+ this.info(`[Dry Run] Would run: ${initCmd}`);
97
+ this.info("[Dry Run] Would run: railway add --database postgres");
98
+ return;
99
+ }
100
+ try {
101
+ this.info("Ensuring Railway project is linked...");
102
+ const backendDir = path.join(process.cwd(), "apps/backend");
103
+ try {
104
+ await execAsync("railway status", { cwd: backendDir });
105
+ } catch {
106
+ const initCmd = options.railwayName ? `railway init --name ${options.railwayName}` : "railway init";
107
+ this.info(`No Railway project detected in apps/backend. Initializing with: ${initCmd}`);
108
+ await execAsync(initCmd, { cwd: backendDir });
109
+ }
110
+ this.info(`Adding PostgreSQL service if missing for "${options.backendName}"...`);
111
+ const { stdout: status } = await execAsync("railway status", { cwd: backendDir });
112
+ if (!status.includes("postgres")) {
113
+ await execAsync("railway add --database postgres", { cwd: backendDir });
114
+ }
115
+ } catch (e) {
116
+ this.warn(
117
+ "Railway setup encountered an issue. Ensure you are logged in with `railway login`."
118
+ );
119
+ throw e;
120
+ }
121
+ }
122
+ async setupCloudflare(options) {
123
+ this.info("Configuring Cloudflare Pages...");
124
+ if (options.dryRun) {
125
+ this.info(`[Dry Run] Would run: wrangler pages project create ${options.frontendName}`);
126
+ return;
127
+ }
128
+ if (!options.cloudflareToken || !options.cloudflareAccount) {
129
+ this.warn("Cloudflare credentials missing. Skipping automated Cloudflare setup.");
130
+ this.info("You can manually set up Cloudflare Pages and add the secrets to GitHub.");
131
+ return;
132
+ }
133
+ try {
134
+ const projectName = options.frontendName;
135
+ this.info(`Ensuring Cloudflare Pages project "${projectName}" exists...`);
136
+ try {
137
+ await execAsync(`wrangler pages project create ${projectName} --production-branch main`, {
138
+ env: {
139
+ ...process.env,
140
+ CLOUDFLARE_API_TOKEN: options.cloudflareToken,
141
+ CLOUDFLARE_ACCOUNT_ID: options.cloudflareAccount
142
+ }
143
+ });
144
+ } catch {
145
+ this.info("Cloudflare project might already exist.");
146
+ }
147
+ } catch (e) {
148
+ this.warn("Cloudflare setup failed.");
149
+ throw e;
150
+ }
151
+ }
152
+ async setupGitHubConfig(options) {
153
+ this.info("Configuring GitHub Secrets and Variables...");
154
+ if (options.dryRun) {
155
+ this.info("[Dry Run] Would run: gh secret set RAILWAY_TOKEN");
156
+ this.info("[Dry Run] Would run: gh secret set CLOUDFLARE_API_TOKEN");
157
+ this.info("[Dry Run] Would run: gh secret set CLOUDFLARE_ACCOUNT_ID");
158
+ this.info(
159
+ `[Dry Run] Would run: gh variable set RAILWAY_SERVICE_NAME --body "${options.backendName}"`
160
+ );
161
+ this.info(
162
+ `[Dry Run] Would run: gh variable set CLOUDFLARE_PROJECT_NAME --body "${options.frontendName}"`
163
+ );
164
+ return;
165
+ }
166
+ try {
167
+ if (options.railwayToken) {
168
+ this.info("Setting RAILWAY_TOKEN in GitHub...");
169
+ await execAsync(`gh secret set RAILWAY_TOKEN --body "${options.railwayToken}"`);
170
+ }
171
+ if (options.cloudflareToken) {
172
+ this.info("Setting CLOUDFLARE_API_TOKEN in GitHub...");
173
+ await execAsync(`gh secret set CLOUDFLARE_API_TOKEN --body "${options.cloudflareToken}"`);
174
+ }
175
+ if (options.cloudflareAccount) {
176
+ this.info("Setting CLOUDFLARE_ACCOUNT_ID in GitHub...");
177
+ await execAsync(
178
+ `gh secret set CLOUDFLARE_ACCOUNT_ID --body "${options.cloudflareAccount}"`
179
+ );
180
+ }
181
+ this.info(`Setting RAILWAY_SERVICE_NAME to "${options.backendName}" in GitHub...`);
182
+ await execAsync(`gh variable set RAILWAY_SERVICE_NAME --body "${options.backendName}"`);
183
+ this.info(`Setting CLOUDFLARE_PROJECT_NAME to "${options.frontendName}" in GitHub...`);
184
+ await execAsync(`gh variable set CLOUDFLARE_PROJECT_NAME --body "${options.frontendName}"`);
185
+ } catch (e) {
186
+ this.warn(
187
+ "GitHub configuration failed. Ensure you have the GitHub CLI (gh) installed and are logged in."
188
+ );
189
+ throw e;
190
+ }
191
+ }
192
+ };
193
+ export {
194
+ DeployCommand as default
195
+ };
196
+ //# sourceMappingURL=deploy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/commands/deploy.ts"],"sourcesContent":["import { BaseCommand } from '@nexical/cli-core';\nimport { exec } from 'node:child_process';\nimport { promisify } from 'node:util';\nimport path from 'node:path';\n\nconst execAsync = promisify(exec);\n\ninterface DeployOptions {\n dryRun: boolean;\n railwayToken?: string;\n railwayName?: string;\n backendName: string;\n frontendName: string;\n cloudflareToken?: string;\n cloudflareAccount?: string;\n}\n\nexport default class DeployCommand extends BaseCommand {\n static description = `Deploy the application to Railway and Cloudflare.\n\nENVIRONMENT SETUP & PREREQUISITES:\n1. Install Required CLIs:\n - Railway CLI: npm i -g @railway/cli\n - Wrangler (Cloudflare): npm i -g wrangler\n - GitHub CLI: https://cli.github.com/\n\n2. Authentication:\n - Railway: Run 'railway login'\n - GitHub: Run 'gh auth login'\n - Cloudflare: Obtain an API Token (with Pages edit permissions) and your Account ID from the dashboard.\n\n3. Configuration:\n Run this command with --cloudflare-token and --cloudflare-account to automate the full setup.\n Optional: Use --railway-token if you prefer not to use the interactive login.\n Optional: Use --railway-name to specify a custom Railway project name.\n Optional: Use --backend-name to specify the Railway service name (default: nexical-backend).\n Optional: Use --frontend-name to specify the Cloudflare Pages project name (default: nexical-frontend).\n\nPROCESS:\n- Provisions a PostgreSQL database on Railway (if missing).\n- Creates a Cloudflare Pages project for the frontend.\n- Syncs all necessary deployment secrets and variables to GitHub for CI/CD automation.`;\n\n static args = {\n options: [\n {\n name: '--dry-run',\n description: 'Simulate the deployment process without making changes.',\n default: false,\n },\n {\n name: '--railway-token <token>',\n description: 'Railway Project Token (optional if already logged in).',\n },\n {\n name: '--railway-name <name>',\n description: 'Railway Project Name (used during initialization).',\n },\n {\n name: '--backend-name <name>',\n description: 'Backend service name on Railway.',\n default: 'nexical-backend',\n },\n {\n name: '--frontend-name <name>',\n description: 'Frontend project name on Cloudflare.',\n default: 'nexical-frontend',\n },\n {\n name: '--cloudflare-token <token>',\n description: 'Cloudflare API Token.',\n },\n {\n name: '--cloudflare-account <id>',\n description: 'Cloudflare Account ID.',\n },\n ],\n };\n\n async run(options: DeployOptions) {\n this.info('Starting Nexical Deployment Automation...');\n\n if (options.dryRun) {\n this.notice('DRY RUN MODE ENABLED');\n }\n\n try {\n // 1. Railway Setup\n await this.setupRailway(options);\n\n // 2. Cloudflare Setup\n await this.setupCloudflare(options);\n\n // 3. GitHub Configuration (Secrets & Variables)\n await this.setupGitHubConfig(options);\n\n this.success('Deployment setup complete! Your application is being deployed.');\n } catch (error: unknown) {\n if (error instanceof Error) {\n this.error(`Deployment failed: ${error.message}`);\n } else {\n this.error(`Deployment failed: ${String(error)}`);\n }\n process.exit(1);\n }\n }\n\n private async setupRailway(options: DeployOptions) {\n this.info('Configuring Railway...');\n\n if (options.dryRun) {\n const initCmd = options.railwayName\n ? `railway init --name ${options.railwayName}`\n : 'railway init';\n this.info(`[Dry Run] Would run: ${initCmd}`);\n this.info('[Dry Run] Would run: railway add --database postgres');\n return;\n }\n\n try {\n // Check if railway project exists or init\n // Note: railway init might be interactive, so we might need to handle that or assume user has linked.\n // For now, let's assume we use 'railway link' if they passed a token or have it set.\n\n this.info('Ensuring Railway project is linked...');\n // Note: We intentionally DO NOT set process.env.RAILWAY_TOKEN here.\n // Management commands (init, status, add) require an interactive session (railway login).\n // The provided --railway-token is reserved for GitHub Secrets setup (CI/CD).\n\n const backendDir = path.join(process.cwd(), 'apps/backend');\n\n try {\n await execAsync('railway status', { cwd: backendDir });\n } catch {\n const initCmd = options.railwayName\n ? `railway init --name ${options.railwayName}`\n : 'railway init';\n this.info(`No Railway project detected in apps/backend. Initializing with: ${initCmd}`);\n await execAsync(initCmd, { cwd: backendDir });\n }\n\n this.info(`Adding PostgreSQL service if missing for \"${options.backendName}\"...`);\n const { stdout: status } = await execAsync('railway status', { cwd: backendDir });\n if (!status.includes('postgres')) {\n await execAsync('railway add --database postgres', { cwd: backendDir });\n }\n } catch (e: unknown) {\n this.warn(\n 'Railway setup encountered an issue. Ensure you are logged in with `railway login`.',\n );\n throw e;\n }\n }\n\n private async setupCloudflare(options: DeployOptions) {\n this.info('Configuring Cloudflare Pages...');\n\n if (options.dryRun) {\n this.info(`[Dry Run] Would run: wrangler pages project create ${options.frontendName}`);\n return;\n }\n\n if (!options.cloudflareToken || !options.cloudflareAccount) {\n this.warn('Cloudflare credentials missing. Skipping automated Cloudflare setup.');\n this.info('You can manually set up Cloudflare Pages and add the secrets to GitHub.');\n return;\n }\n\n try {\n // Use wrangler to create project if it doesn't exist\n const projectName = options.frontendName;\n this.info(`Ensuring Cloudflare Pages project \"${projectName}\" exists...`);\n\n try {\n await execAsync(`wrangler pages project create ${projectName} --production-branch main`, {\n env: {\n ...process.env,\n CLOUDFLARE_API_TOKEN: options.cloudflareToken,\n CLOUDFLARE_ACCOUNT_ID: options.cloudflareAccount,\n },\n });\n } catch {\n this.info('Cloudflare project might already exist.');\n }\n } catch (e: unknown) {\n this.warn('Cloudflare setup failed.');\n throw e;\n }\n }\n\n private async setupGitHubConfig(options: DeployOptions) {\n this.info('Configuring GitHub Secrets and Variables...');\n\n if (options.dryRun) {\n this.info('[Dry Run] Would run: gh secret set RAILWAY_TOKEN');\n this.info('[Dry Run] Would run: gh secret set CLOUDFLARE_API_TOKEN');\n this.info('[Dry Run] Would run: gh secret set CLOUDFLARE_ACCOUNT_ID');\n this.info(\n `[Dry Run] Would run: gh variable set RAILWAY_SERVICE_NAME --body \"${options.backendName}\"`,\n );\n this.info(\n `[Dry Run] Would run: gh variable set CLOUDFLARE_PROJECT_NAME --body \"${options.frontendName}\"`,\n );\n return;\n }\n\n try {\n if (options.railwayToken) {\n this.info('Setting RAILWAY_TOKEN in GitHub...');\n await execAsync(`gh secret set RAILWAY_TOKEN --body \"${options.railwayToken}\"`);\n }\n\n if (options.cloudflareToken) {\n this.info('Setting CLOUDFLARE_API_TOKEN in GitHub...');\n await execAsync(`gh secret set CLOUDFLARE_API_TOKEN --body \"${options.cloudflareToken}\"`);\n }\n\n if (options.cloudflareAccount) {\n this.info('Setting CLOUDFLARE_ACCOUNT_ID in GitHub...');\n await execAsync(\n `gh secret set CLOUDFLARE_ACCOUNT_ID --body \"${options.cloudflareAccount}\"`,\n );\n }\n\n // Set variables\n this.info(`Setting RAILWAY_SERVICE_NAME to \"${options.backendName}\" in GitHub...`);\n await execAsync(`gh variable set RAILWAY_SERVICE_NAME --body \"${options.backendName}\"`);\n\n this.info(`Setting CLOUDFLARE_PROJECT_NAME to \"${options.frontendName}\" in GitHub...`);\n await execAsync(`gh variable set CLOUDFLARE_PROJECT_NAME --body \"${options.frontendName}\"`);\n } catch (e: unknown) {\n this.warn(\n 'GitHub configuration failed. Ensure you have the GitHub CLI (gh) installed and are logged in.',\n );\n throw e;\n }\n }\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nexical/cli",
3
- "version": "0.11.2",
3
+ "version": "0.11.4",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "nexical": "./dist/index.js"
@@ -0,0 +1,238 @@
1
+ import { BaseCommand } from '@nexical/cli-core';
2
+ import { exec } from 'node:child_process';
3
+ import { promisify } from 'node:util';
4
+ import path from 'node:path';
5
+
6
+ const execAsync = promisify(exec);
7
+
8
+ interface DeployOptions {
9
+ dryRun: boolean;
10
+ railwayToken?: string;
11
+ railwayName?: string;
12
+ backendName: string;
13
+ frontendName: string;
14
+ cloudflareToken?: string;
15
+ cloudflareAccount?: string;
16
+ }
17
+
18
+ export default class DeployCommand extends BaseCommand {
19
+ static description = `Deploy the application to Railway and Cloudflare.
20
+
21
+ ENVIRONMENT SETUP & PREREQUISITES:
22
+ 1. Install Required CLIs:
23
+ - Railway CLI: npm i -g @railway/cli
24
+ - Wrangler (Cloudflare): npm i -g wrangler
25
+ - GitHub CLI: https://cli.github.com/
26
+
27
+ 2. Authentication:
28
+ - Railway: Run 'railway login'
29
+ - GitHub: Run 'gh auth login'
30
+ - Cloudflare: Obtain an API Token (with Pages edit permissions) and your Account ID from the dashboard.
31
+
32
+ 3. Configuration:
33
+ Run this command with --cloudflare-token and --cloudflare-account to automate the full setup.
34
+ Optional: Use --railway-token if you prefer not to use the interactive login.
35
+ Optional: Use --railway-name to specify a custom Railway project name.
36
+ Optional: Use --backend-name to specify the Railway service name (default: nexical-backend).
37
+ Optional: Use --frontend-name to specify the Cloudflare Pages project name (default: nexical-frontend).
38
+
39
+ PROCESS:
40
+ - Provisions a PostgreSQL database on Railway (if missing).
41
+ - Creates a Cloudflare Pages project for the frontend.
42
+ - Syncs all necessary deployment secrets and variables to GitHub for CI/CD automation.`;
43
+
44
+ static args = {
45
+ options: [
46
+ {
47
+ name: '--dry-run',
48
+ description: 'Simulate the deployment process without making changes.',
49
+ default: false,
50
+ },
51
+ {
52
+ name: '--railway-token <token>',
53
+ description: 'Railway Project Token (optional if already logged in).',
54
+ },
55
+ {
56
+ name: '--railway-name <name>',
57
+ description: 'Railway Project Name (used during initialization).',
58
+ },
59
+ {
60
+ name: '--backend-name <name>',
61
+ description: 'Backend service name on Railway.',
62
+ default: 'nexical-backend',
63
+ },
64
+ {
65
+ name: '--frontend-name <name>',
66
+ description: 'Frontend project name on Cloudflare.',
67
+ default: 'nexical-frontend',
68
+ },
69
+ {
70
+ name: '--cloudflare-token <token>',
71
+ description: 'Cloudflare API Token.',
72
+ },
73
+ {
74
+ name: '--cloudflare-account <id>',
75
+ description: 'Cloudflare Account ID.',
76
+ },
77
+ ],
78
+ };
79
+
80
+ async run(options: DeployOptions) {
81
+ this.info('Starting Nexical Deployment Automation...');
82
+
83
+ if (options.dryRun) {
84
+ this.notice('DRY RUN MODE ENABLED');
85
+ }
86
+
87
+ try {
88
+ // 1. Railway Setup
89
+ await this.setupRailway(options);
90
+
91
+ // 2. Cloudflare Setup
92
+ await this.setupCloudflare(options);
93
+
94
+ // 3. GitHub Configuration (Secrets & Variables)
95
+ await this.setupGitHubConfig(options);
96
+
97
+ this.success('Deployment setup complete! Your application is being deployed.');
98
+ } catch (error: unknown) {
99
+ if (error instanceof Error) {
100
+ this.error(`Deployment failed: ${error.message}`);
101
+ } else {
102
+ this.error(`Deployment failed: ${String(error)}`);
103
+ }
104
+ process.exit(1);
105
+ }
106
+ }
107
+
108
+ private async setupRailway(options: DeployOptions) {
109
+ this.info('Configuring Railway...');
110
+
111
+ if (options.dryRun) {
112
+ const initCmd = options.railwayName
113
+ ? `railway init --name ${options.railwayName}`
114
+ : 'railway init';
115
+ this.info(`[Dry Run] Would run: ${initCmd}`);
116
+ this.info('[Dry Run] Would run: railway add --database postgres');
117
+ return;
118
+ }
119
+
120
+ try {
121
+ // Check if railway project exists or init
122
+ // Note: railway init might be interactive, so we might need to handle that or assume user has linked.
123
+ // For now, let's assume we use 'railway link' if they passed a token or have it set.
124
+
125
+ this.info('Ensuring Railway project is linked...');
126
+ // Note: We intentionally DO NOT set process.env.RAILWAY_TOKEN here.
127
+ // Management commands (init, status, add) require an interactive session (railway login).
128
+ // The provided --railway-token is reserved for GitHub Secrets setup (CI/CD).
129
+
130
+ const backendDir = path.join(process.cwd(), 'apps/backend');
131
+
132
+ try {
133
+ await execAsync('railway status', { cwd: backendDir });
134
+ } catch {
135
+ const initCmd = options.railwayName
136
+ ? `railway init --name ${options.railwayName}`
137
+ : 'railway init';
138
+ this.info(`No Railway project detected in apps/backend. Initializing with: ${initCmd}`);
139
+ await execAsync(initCmd, { cwd: backendDir });
140
+ }
141
+
142
+ this.info(`Adding PostgreSQL service if missing for "${options.backendName}"...`);
143
+ const { stdout: status } = await execAsync('railway status', { cwd: backendDir });
144
+ if (!status.includes('postgres')) {
145
+ await execAsync('railway add --database postgres', { cwd: backendDir });
146
+ }
147
+ } catch (e: unknown) {
148
+ this.warn(
149
+ 'Railway setup encountered an issue. Ensure you are logged in with `railway login`.',
150
+ );
151
+ throw e;
152
+ }
153
+ }
154
+
155
+ private async setupCloudflare(options: DeployOptions) {
156
+ this.info('Configuring Cloudflare Pages...');
157
+
158
+ if (options.dryRun) {
159
+ this.info(`[Dry Run] Would run: wrangler pages project create ${options.frontendName}`);
160
+ return;
161
+ }
162
+
163
+ if (!options.cloudflareToken || !options.cloudflareAccount) {
164
+ this.warn('Cloudflare credentials missing. Skipping automated Cloudflare setup.');
165
+ this.info('You can manually set up Cloudflare Pages and add the secrets to GitHub.');
166
+ return;
167
+ }
168
+
169
+ try {
170
+ // Use wrangler to create project if it doesn't exist
171
+ const projectName = options.frontendName;
172
+ this.info(`Ensuring Cloudflare Pages project "${projectName}" exists...`);
173
+
174
+ try {
175
+ await execAsync(`wrangler pages project create ${projectName} --production-branch main`, {
176
+ env: {
177
+ ...process.env,
178
+ CLOUDFLARE_API_TOKEN: options.cloudflareToken,
179
+ CLOUDFLARE_ACCOUNT_ID: options.cloudflareAccount,
180
+ },
181
+ });
182
+ } catch {
183
+ this.info('Cloudflare project might already exist.');
184
+ }
185
+ } catch (e: unknown) {
186
+ this.warn('Cloudflare setup failed.');
187
+ throw e;
188
+ }
189
+ }
190
+
191
+ private async setupGitHubConfig(options: DeployOptions) {
192
+ this.info('Configuring GitHub Secrets and Variables...');
193
+
194
+ if (options.dryRun) {
195
+ this.info('[Dry Run] Would run: gh secret set RAILWAY_TOKEN');
196
+ this.info('[Dry Run] Would run: gh secret set CLOUDFLARE_API_TOKEN');
197
+ this.info('[Dry Run] Would run: gh secret set CLOUDFLARE_ACCOUNT_ID');
198
+ this.info(
199
+ `[Dry Run] Would run: gh variable set RAILWAY_SERVICE_NAME --body "${options.backendName}"`,
200
+ );
201
+ this.info(
202
+ `[Dry Run] Would run: gh variable set CLOUDFLARE_PROJECT_NAME --body "${options.frontendName}"`,
203
+ );
204
+ return;
205
+ }
206
+
207
+ try {
208
+ if (options.railwayToken) {
209
+ this.info('Setting RAILWAY_TOKEN in GitHub...');
210
+ await execAsync(`gh secret set RAILWAY_TOKEN --body "${options.railwayToken}"`);
211
+ }
212
+
213
+ if (options.cloudflareToken) {
214
+ this.info('Setting CLOUDFLARE_API_TOKEN in GitHub...');
215
+ await execAsync(`gh secret set CLOUDFLARE_API_TOKEN --body "${options.cloudflareToken}"`);
216
+ }
217
+
218
+ if (options.cloudflareAccount) {
219
+ this.info('Setting CLOUDFLARE_ACCOUNT_ID in GitHub...');
220
+ await execAsync(
221
+ `gh secret set CLOUDFLARE_ACCOUNT_ID --body "${options.cloudflareAccount}"`,
222
+ );
223
+ }
224
+
225
+ // Set variables
226
+ this.info(`Setting RAILWAY_SERVICE_NAME to "${options.backendName}" in GitHub...`);
227
+ await execAsync(`gh variable set RAILWAY_SERVICE_NAME --body "${options.backendName}"`);
228
+
229
+ this.info(`Setting CLOUDFLARE_PROJECT_NAME to "${options.frontendName}" in GitHub...`);
230
+ await execAsync(`gh variable set CLOUDFLARE_PROJECT_NAME --body "${options.frontendName}"`);
231
+ } catch (e: unknown) {
232
+ this.warn(
233
+ 'GitHub configuration failed. Ensure you have the GitHub CLI (gh) installed and are logged in.',
234
+ );
235
+ throw e;
236
+ }
237
+ }
238
+ }