@nexical/cli 0.11.3 → 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 +1 -1
- package/dist/index.js.map +1 -1
- package/dist/src/commands/deploy.d.ts +8 -1
- package/dist/src/commands/deploy.js +69 -21
- package/dist/src/commands/deploy.js.map +1 -1
- package/package.json +1 -1
- package/src/commands/deploy.ts +84 -32
package/dist/index.js
CHANGED
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../index.ts","../package.json"],"sourcesContent":["#!/usr/bin/env node\nimport { CLI, findProjectRoot } from '@nexical/cli-core';\nimport { fileURLToPath } from 'node:url';\nimport { discoverCommandDirectories } from './src/utils/discovery.js';\nimport pkg from './package.json';\nimport path from 'node:path';\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\nconst commandName = 'nexical';\nconst projectRoot = (await findProjectRoot(commandName, process.cwd())) || process.cwd();\nconst coreCommandsDir = path.resolve(__dirname, './src/commands');\nconst additionalCommands = discoverCommandDirectories(projectRoot);\n\n// Filter out duplicate core commands and source versions\nconst filteredAdditional = additionalCommands.filter((dir) => {\n const resolvedDir = path.resolve(dir);\n const resolvedCore = path.resolve(coreCommandsDir);\n\n if (resolvedDir === resolvedCore) return false;\n\n // Check if this is another instance of the core CLI commands (by checking path suffix)\n const coreSuffix = path.join('@nexical', 'cli', 'dist', 'src', 'commands');\n const coreSuffixSrc = path.join('packages', 'cli', 'dist', 'src', 'commands');\n const coreSuffixRawSrc = path.join('packages', 'cli', 'src', 'commands');\n\n if (\n resolvedDir.endsWith(coreSuffix) ||\n resolvedDir.endsWith(coreSuffixSrc) ||\n resolvedDir.endsWith(coreSuffixRawSrc)\n ) {\n return false;\n }\n\n // Handle mismatch between dist/src and src/\n if (resolvedCore.includes(path.join(path.sep, 'dist', 'src', 'commands'))) {\n const srcVersion = resolvedCore.replace(\n path.join(path.sep, 'dist', 'src', 'commands'),\n path.join(path.sep, 'src', 'commands'),\n );\n if (resolvedDir === srcVersion) return false;\n }\n\n return true;\n});\n\nconst app = new CLI({\n version: pkg.version,\n commandName: commandName,\n searchDirectories: [...new Set([coreCommandsDir, ...filteredAdditional])],\n});\napp.start();\n","{\n \"name\": \"@nexical/cli\",\n \"version\": \"0.11.
|
|
1
|
+
{"version":3,"sources":["../index.ts","../package.json"],"sourcesContent":["#!/usr/bin/env node\nimport { CLI, findProjectRoot } from '@nexical/cli-core';\nimport { fileURLToPath } from 'node:url';\nimport { discoverCommandDirectories } from './src/utils/discovery.js';\nimport pkg from './package.json';\nimport path from 'node:path';\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\nconst commandName = 'nexical';\nconst projectRoot = (await findProjectRoot(commandName, process.cwd())) || process.cwd();\nconst coreCommandsDir = path.resolve(__dirname, './src/commands');\nconst additionalCommands = discoverCommandDirectories(projectRoot);\n\n// Filter out duplicate core commands and source versions\nconst filteredAdditional = additionalCommands.filter((dir) => {\n const resolvedDir = path.resolve(dir);\n const resolvedCore = path.resolve(coreCommandsDir);\n\n if (resolvedDir === resolvedCore) return false;\n\n // Check if this is another instance of the core CLI commands (by checking path suffix)\n const coreSuffix = path.join('@nexical', 'cli', 'dist', 'src', 'commands');\n const coreSuffixSrc = path.join('packages', 'cli', 'dist', 'src', 'commands');\n const coreSuffixRawSrc = path.join('packages', 'cli', 'src', 'commands');\n\n if (\n resolvedDir.endsWith(coreSuffix) ||\n resolvedDir.endsWith(coreSuffixSrc) ||\n resolvedDir.endsWith(coreSuffixRawSrc)\n ) {\n return false;\n }\n\n // Handle mismatch between dist/src and src/\n if (resolvedCore.includes(path.join(path.sep, 'dist', 'src', 'commands'))) {\n const srcVersion = resolvedCore.replace(\n path.join(path.sep, 'dist', 'src', 'commands'),\n path.join(path.sep, 'src', 'commands'),\n );\n if (resolvedDir === srcVersion) return false;\n }\n\n return true;\n});\n\nconst app = new CLI({\n version: pkg.version,\n commandName: commandName,\n searchDirectories: [...new Set([coreCommandsDir, ...filteredAdditional])],\n});\napp.start();\n","{\n \"name\": \"@nexical/cli\",\n \"version\": \"0.11.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":[]}
|
|
@@ -3,6 +3,9 @@ import { BaseCommand } from '@nexical/cli-core';
|
|
|
3
3
|
interface DeployOptions {
|
|
4
4
|
dryRun: boolean;
|
|
5
5
|
railwayToken?: string;
|
|
6
|
+
railwayName?: string;
|
|
7
|
+
backendName: string;
|
|
8
|
+
frontendName: string;
|
|
6
9
|
cloudflareToken?: string;
|
|
7
10
|
cloudflareAccount?: string;
|
|
8
11
|
}
|
|
@@ -17,12 +20,16 @@ declare class DeployCommand extends BaseCommand {
|
|
|
17
20
|
name: string;
|
|
18
21
|
description: string;
|
|
19
22
|
default?: undefined;
|
|
23
|
+
} | {
|
|
24
|
+
name: string;
|
|
25
|
+
description: string;
|
|
26
|
+
default: string;
|
|
20
27
|
})[];
|
|
21
28
|
};
|
|
22
29
|
run(options: DeployOptions): Promise<void>;
|
|
23
30
|
private setupRailway;
|
|
24
31
|
private setupCloudflare;
|
|
25
|
-
private
|
|
32
|
+
private setupGitHubConfig;
|
|
26
33
|
}
|
|
27
34
|
|
|
28
35
|
export { DeployCommand as default };
|
|
@@ -5,12 +5,36 @@ import {
|
|
|
5
5
|
|
|
6
6
|
// src/commands/deploy.ts
|
|
7
7
|
init_esm_shims();
|
|
8
|
-
import { BaseCommand
|
|
8
|
+
import { BaseCommand } from "@nexical/cli-core";
|
|
9
9
|
import { exec } from "child_process";
|
|
10
10
|
import { promisify } from "util";
|
|
11
|
+
import path from "path";
|
|
11
12
|
var execAsync = promisify(exec);
|
|
12
13
|
var DeployCommand = class extends BaseCommand {
|
|
13
|
-
static description =
|
|
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.`;
|
|
14
38
|
static args = {
|
|
15
39
|
options: [
|
|
16
40
|
{
|
|
@@ -22,6 +46,20 @@ var DeployCommand = class extends BaseCommand {
|
|
|
22
46
|
name: "--railway-token <token>",
|
|
23
47
|
description: "Railway Project Token (optional if already logged in)."
|
|
24
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
|
+
},
|
|
25
63
|
{
|
|
26
64
|
name: "--cloudflare-token <token>",
|
|
27
65
|
description: "Cloudflare API Token."
|
|
@@ -40,7 +78,7 @@ var DeployCommand = class extends BaseCommand {
|
|
|
40
78
|
try {
|
|
41
79
|
await this.setupRailway(options);
|
|
42
80
|
await this.setupCloudflare(options);
|
|
43
|
-
await this.
|
|
81
|
+
await this.setupGitHubConfig(options);
|
|
44
82
|
this.success("Deployment setup complete! Your application is being deployed.");
|
|
45
83
|
} catch (error) {
|
|
46
84
|
if (error instanceof Error) {
|
|
@@ -54,25 +92,25 @@ var DeployCommand = class extends BaseCommand {
|
|
|
54
92
|
async setupRailway(options) {
|
|
55
93
|
this.info("Configuring Railway...");
|
|
56
94
|
if (options.dryRun) {
|
|
57
|
-
|
|
95
|
+
const initCmd = options.railwayName ? `railway init --name ${options.railwayName}` : "railway init";
|
|
96
|
+
this.info(`[Dry Run] Would run: ${initCmd}`);
|
|
58
97
|
this.info("[Dry Run] Would run: railway add --database postgres");
|
|
59
98
|
return;
|
|
60
99
|
}
|
|
61
100
|
try {
|
|
62
101
|
this.info("Ensuring Railway project is linked...");
|
|
63
|
-
|
|
64
|
-
process.env.RAILWAY_TOKEN = options.railwayToken;
|
|
65
|
-
}
|
|
102
|
+
const backendDir = path.join(process.cwd(), "apps/backend");
|
|
66
103
|
try {
|
|
67
|
-
await
|
|
104
|
+
await execAsync("railway status", { cwd: backendDir });
|
|
68
105
|
} catch {
|
|
69
|
-
|
|
70
|
-
|
|
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 });
|
|
71
109
|
}
|
|
72
|
-
this.info(
|
|
73
|
-
const { stdout: status } = await execAsync("railway status");
|
|
110
|
+
this.info(`Adding PostgreSQL service if missing for "${options.backendName}"...`);
|
|
111
|
+
const { stdout: status } = await execAsync("railway status", { cwd: backendDir });
|
|
74
112
|
if (!status.includes("postgres")) {
|
|
75
|
-
await
|
|
113
|
+
await execAsync("railway add --database postgres", { cwd: backendDir });
|
|
76
114
|
}
|
|
77
115
|
} catch (e) {
|
|
78
116
|
this.warn(
|
|
@@ -84,7 +122,7 @@ var DeployCommand = class extends BaseCommand {
|
|
|
84
122
|
async setupCloudflare(options) {
|
|
85
123
|
this.info("Configuring Cloudflare Pages...");
|
|
86
124
|
if (options.dryRun) {
|
|
87
|
-
this.info(
|
|
125
|
+
this.info(`[Dry Run] Would run: wrangler pages project create ${options.frontendName}`);
|
|
88
126
|
return;
|
|
89
127
|
}
|
|
90
128
|
if (!options.cloudflareToken || !options.cloudflareAccount) {
|
|
@@ -93,7 +131,7 @@ var DeployCommand = class extends BaseCommand {
|
|
|
93
131
|
return;
|
|
94
132
|
}
|
|
95
133
|
try {
|
|
96
|
-
const projectName =
|
|
134
|
+
const projectName = options.frontendName;
|
|
97
135
|
this.info(`Ensuring Cloudflare Pages project "${projectName}" exists...`);
|
|
98
136
|
try {
|
|
99
137
|
await execAsync(`wrangler pages project create ${projectName} --production-branch main`, {
|
|
@@ -111,32 +149,42 @@ var DeployCommand = class extends BaseCommand {
|
|
|
111
149
|
throw e;
|
|
112
150
|
}
|
|
113
151
|
}
|
|
114
|
-
async
|
|
115
|
-
this.info("Configuring GitHub Secrets...");
|
|
152
|
+
async setupGitHubConfig(options) {
|
|
153
|
+
this.info("Configuring GitHub Secrets and Variables...");
|
|
116
154
|
if (options.dryRun) {
|
|
117
155
|
this.info("[Dry Run] Would run: gh secret set RAILWAY_TOKEN");
|
|
118
156
|
this.info("[Dry Run] Would run: gh secret set CLOUDFLARE_API_TOKEN");
|
|
119
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
|
+
);
|
|
120
164
|
return;
|
|
121
165
|
}
|
|
122
166
|
try {
|
|
123
167
|
if (options.railwayToken) {
|
|
124
168
|
this.info("Setting RAILWAY_TOKEN in GitHub...");
|
|
125
|
-
await
|
|
169
|
+
await execAsync(`gh secret set RAILWAY_TOKEN --body "${options.railwayToken}"`);
|
|
126
170
|
}
|
|
127
171
|
if (options.cloudflareToken) {
|
|
128
172
|
this.info("Setting CLOUDFLARE_API_TOKEN in GitHub...");
|
|
129
|
-
await
|
|
173
|
+
await execAsync(`gh secret set CLOUDFLARE_API_TOKEN --body "${options.cloudflareToken}"`);
|
|
130
174
|
}
|
|
131
175
|
if (options.cloudflareAccount) {
|
|
132
176
|
this.info("Setting CLOUDFLARE_ACCOUNT_ID in GitHub...");
|
|
133
|
-
await
|
|
177
|
+
await execAsync(
|
|
134
178
|
`gh secret set CLOUDFLARE_ACCOUNT_ID --body "${options.cloudflareAccount}"`
|
|
135
179
|
);
|
|
136
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}"`);
|
|
137
185
|
} catch (e) {
|
|
138
186
|
this.warn(
|
|
139
|
-
"GitHub
|
|
187
|
+
"GitHub configuration failed. Ensure you have the GitHub CLI (gh) installed and are logged in."
|
|
140
188
|
);
|
|
141
189
|
throw e;
|
|
142
190
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/commands/deploy.ts"],"sourcesContent":["import { BaseCommand, runCommand } from '@nexical/cli-core';\nimport { exec } from 'node:child_process';\nimport { promisify } from 'node:util';\n\nconst execAsync = promisify(exec);\n\ninterface DeployOptions {\n dryRun: boolean;\n railwayToken?: 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\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: '--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 Secrets Setup\n await this.setupGitHubSecrets(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 this.info('[Dry Run] Would run: railway init');\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 // If they provided a token, we should probably set it in the environment for subsequent calls\n if (options.railwayToken) {\n process.env.RAILWAY_TOKEN = options.railwayToken;\n }\n\n // Check if we are in a railway project\n try {\n await runCommand('railway status');\n } catch {\n this.info('No Railway project detected. Initializing...');\n await runCommand('railway init');\n }\n\n this.info('Adding PostgreSQL service if missing...');\n // railway add --database postgres is usually safe to run twice but we should check status\n const { stdout: status } = await execAsync('railway status');\n if (!status.includes('postgres')) {\n await runCommand('railway add --database postgres');\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 nexical-frontend');\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 // We assume project name 'nexical-frontend' for now, should be configurable.\n const projectName = 'nexical-frontend';\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 setupGitHubSecrets(options: DeployOptions) {\n this.info('Configuring GitHub Secrets...');\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 return;\n }\n\n try {\n // We need the Railway Project Token.\n // User might have provided it, or we try to get it from railway tokens?\n // Railway CLI doesn't easily expose the project token via CLI easily without a lot of parsing.\n // Usually users generate it in the UI.\n // If they provided it via --railway-token, we use it.\n\n if (options.railwayToken) {\n this.info('Setting RAILWAY_TOKEN in GitHub...');\n await runCommand(`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 runCommand(`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 runCommand(\n `gh secret set CLOUDFLARE_ACCOUNT_ID --body \"${options.cloudflareAccount}\"`,\n );\n }\n } catch (e: unknown) {\n this.warn(\n 'GitHub Secrets setup 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":[]}
|
|
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
package/src/commands/deploy.ts
CHANGED
|
@@ -1,18 +1,45 @@
|
|
|
1
|
-
import { BaseCommand
|
|
1
|
+
import { BaseCommand } from '@nexical/cli-core';
|
|
2
2
|
import { exec } from 'node:child_process';
|
|
3
3
|
import { promisify } from 'node:util';
|
|
4
|
+
import path from 'node:path';
|
|
4
5
|
|
|
5
6
|
const execAsync = promisify(exec);
|
|
6
7
|
|
|
7
8
|
interface DeployOptions {
|
|
8
9
|
dryRun: boolean;
|
|
9
10
|
railwayToken?: string;
|
|
11
|
+
railwayName?: string;
|
|
12
|
+
backendName: string;
|
|
13
|
+
frontendName: string;
|
|
10
14
|
cloudflareToken?: string;
|
|
11
15
|
cloudflareAccount?: string;
|
|
12
16
|
}
|
|
13
17
|
|
|
14
18
|
export default class DeployCommand extends BaseCommand {
|
|
15
|
-
static description =
|
|
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.`;
|
|
16
43
|
|
|
17
44
|
static args = {
|
|
18
45
|
options: [
|
|
@@ -25,6 +52,20 @@ export default class DeployCommand extends BaseCommand {
|
|
|
25
52
|
name: '--railway-token <token>',
|
|
26
53
|
description: 'Railway Project Token (optional if already logged in).',
|
|
27
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
|
+
},
|
|
28
69
|
{
|
|
29
70
|
name: '--cloudflare-token <token>',
|
|
30
71
|
description: 'Cloudflare API Token.',
|
|
@@ -50,8 +91,8 @@ export default class DeployCommand extends BaseCommand {
|
|
|
50
91
|
// 2. Cloudflare Setup
|
|
51
92
|
await this.setupCloudflare(options);
|
|
52
93
|
|
|
53
|
-
// 3. GitHub Secrets
|
|
54
|
-
await this.
|
|
94
|
+
// 3. GitHub Configuration (Secrets & Variables)
|
|
95
|
+
await this.setupGitHubConfig(options);
|
|
55
96
|
|
|
56
97
|
this.success('Deployment setup complete! Your application is being deployed.');
|
|
57
98
|
} catch (error: unknown) {
|
|
@@ -68,7 +109,10 @@ export default class DeployCommand extends BaseCommand {
|
|
|
68
109
|
this.info('Configuring Railway...');
|
|
69
110
|
|
|
70
111
|
if (options.dryRun) {
|
|
71
|
-
|
|
112
|
+
const initCmd = options.railwayName
|
|
113
|
+
? `railway init --name ${options.railwayName}`
|
|
114
|
+
: 'railway init';
|
|
115
|
+
this.info(`[Dry Run] Would run: ${initCmd}`);
|
|
72
116
|
this.info('[Dry Run] Would run: railway add --database postgres');
|
|
73
117
|
return;
|
|
74
118
|
}
|
|
@@ -79,24 +123,26 @@ export default class DeployCommand extends BaseCommand {
|
|
|
79
123
|
// For now, let's assume we use 'railway link' if they passed a token or have it set.
|
|
80
124
|
|
|
81
125
|
this.info('Ensuring Railway project is linked...');
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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');
|
|
86
131
|
|
|
87
|
-
// Check if we are in a railway project
|
|
88
132
|
try {
|
|
89
|
-
await
|
|
133
|
+
await execAsync('railway status', { cwd: backendDir });
|
|
90
134
|
} catch {
|
|
91
|
-
|
|
92
|
-
|
|
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 });
|
|
93
140
|
}
|
|
94
141
|
|
|
95
|
-
this.info(
|
|
96
|
-
|
|
97
|
-
const { stdout: status } = await execAsync('railway status');
|
|
142
|
+
this.info(`Adding PostgreSQL service if missing for "${options.backendName}"...`);
|
|
143
|
+
const { stdout: status } = await execAsync('railway status', { cwd: backendDir });
|
|
98
144
|
if (!status.includes('postgres')) {
|
|
99
|
-
await
|
|
145
|
+
await execAsync('railway add --database postgres', { cwd: backendDir });
|
|
100
146
|
}
|
|
101
147
|
} catch (e: unknown) {
|
|
102
148
|
this.warn(
|
|
@@ -110,7 +156,7 @@ export default class DeployCommand extends BaseCommand {
|
|
|
110
156
|
this.info('Configuring Cloudflare Pages...');
|
|
111
157
|
|
|
112
158
|
if (options.dryRun) {
|
|
113
|
-
this.info(
|
|
159
|
+
this.info(`[Dry Run] Would run: wrangler pages project create ${options.frontendName}`);
|
|
114
160
|
return;
|
|
115
161
|
}
|
|
116
162
|
|
|
@@ -122,8 +168,7 @@ export default class DeployCommand extends BaseCommand {
|
|
|
122
168
|
|
|
123
169
|
try {
|
|
124
170
|
// Use wrangler to create project if it doesn't exist
|
|
125
|
-
|
|
126
|
-
const projectName = 'nexical-frontend';
|
|
171
|
+
const projectName = options.frontendName;
|
|
127
172
|
this.info(`Ensuring Cloudflare Pages project "${projectName}" exists...`);
|
|
128
173
|
|
|
129
174
|
try {
|
|
@@ -143,42 +188,49 @@ export default class DeployCommand extends BaseCommand {
|
|
|
143
188
|
}
|
|
144
189
|
}
|
|
145
190
|
|
|
146
|
-
private async
|
|
147
|
-
this.info('Configuring GitHub Secrets...');
|
|
191
|
+
private async setupGitHubConfig(options: DeployOptions) {
|
|
192
|
+
this.info('Configuring GitHub Secrets and Variables...');
|
|
148
193
|
|
|
149
194
|
if (options.dryRun) {
|
|
150
195
|
this.info('[Dry Run] Would run: gh secret set RAILWAY_TOKEN');
|
|
151
196
|
this.info('[Dry Run] Would run: gh secret set CLOUDFLARE_API_TOKEN');
|
|
152
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
|
+
);
|
|
153
204
|
return;
|
|
154
205
|
}
|
|
155
206
|
|
|
156
207
|
try {
|
|
157
|
-
// We need the Railway Project Token.
|
|
158
|
-
// User might have provided it, or we try to get it from railway tokens?
|
|
159
|
-
// Railway CLI doesn't easily expose the project token via CLI easily without a lot of parsing.
|
|
160
|
-
// Usually users generate it in the UI.
|
|
161
|
-
// If they provided it via --railway-token, we use it.
|
|
162
|
-
|
|
163
208
|
if (options.railwayToken) {
|
|
164
209
|
this.info('Setting RAILWAY_TOKEN in GitHub...');
|
|
165
|
-
await
|
|
210
|
+
await execAsync(`gh secret set RAILWAY_TOKEN --body "${options.railwayToken}"`);
|
|
166
211
|
}
|
|
167
212
|
|
|
168
213
|
if (options.cloudflareToken) {
|
|
169
214
|
this.info('Setting CLOUDFLARE_API_TOKEN in GitHub...');
|
|
170
|
-
await
|
|
215
|
+
await execAsync(`gh secret set CLOUDFLARE_API_TOKEN --body "${options.cloudflareToken}"`);
|
|
171
216
|
}
|
|
172
217
|
|
|
173
218
|
if (options.cloudflareAccount) {
|
|
174
219
|
this.info('Setting CLOUDFLARE_ACCOUNT_ID in GitHub...');
|
|
175
|
-
await
|
|
220
|
+
await execAsync(
|
|
176
221
|
`gh secret set CLOUDFLARE_ACCOUNT_ID --body "${options.cloudflareAccount}"`,
|
|
177
222
|
);
|
|
178
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}"`);
|
|
179
231
|
} catch (e: unknown) {
|
|
180
232
|
this.warn(
|
|
181
|
-
'GitHub
|
|
233
|
+
'GitHub configuration failed. Ensure you have the GitHub CLI (gh) installed and are logged in.',
|
|
182
234
|
);
|
|
183
235
|
throw e;
|
|
184
236
|
}
|