@nexical/cli 0.11.2 → 0.11.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.
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/src/commands/deploy.d.ts +28 -0
- package/dist/src/commands/deploy.js +148 -0
- package/dist/src/commands/deploy.js.map +1 -0
- package/package.json +1 -1
- package/src/commands/deploy.ts +186 -0
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.3\",\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,28 @@
|
|
|
1
|
+
import { BaseCommand } from '@nexical/cli-core';
|
|
2
|
+
|
|
3
|
+
interface DeployOptions {
|
|
4
|
+
dryRun: boolean;
|
|
5
|
+
railwayToken?: string;
|
|
6
|
+
cloudflareToken?: string;
|
|
7
|
+
cloudflareAccount?: string;
|
|
8
|
+
}
|
|
9
|
+
declare class DeployCommand extends BaseCommand {
|
|
10
|
+
static description: string;
|
|
11
|
+
static args: {
|
|
12
|
+
options: ({
|
|
13
|
+
name: string;
|
|
14
|
+
description: string;
|
|
15
|
+
default: boolean;
|
|
16
|
+
} | {
|
|
17
|
+
name: string;
|
|
18
|
+
description: string;
|
|
19
|
+
default?: undefined;
|
|
20
|
+
})[];
|
|
21
|
+
};
|
|
22
|
+
run(options: DeployOptions): Promise<void>;
|
|
23
|
+
private setupRailway;
|
|
24
|
+
private setupCloudflare;
|
|
25
|
+
private setupGitHubSecrets;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export { DeployCommand as default };
|
|
@@ -0,0 +1,148 @@
|
|
|
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, runCommand } from "@nexical/cli-core";
|
|
9
|
+
import { exec } from "child_process";
|
|
10
|
+
import { promisify } from "util";
|
|
11
|
+
var execAsync = promisify(exec);
|
|
12
|
+
var DeployCommand = class extends BaseCommand {
|
|
13
|
+
static description = "Deploy the application to Railway and Cloudflare.";
|
|
14
|
+
static args = {
|
|
15
|
+
options: [
|
|
16
|
+
{
|
|
17
|
+
name: "--dry-run",
|
|
18
|
+
description: "Simulate the deployment process without making changes.",
|
|
19
|
+
default: false
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
name: "--railway-token <token>",
|
|
23
|
+
description: "Railway Project Token (optional if already logged in)."
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: "--cloudflare-token <token>",
|
|
27
|
+
description: "Cloudflare API Token."
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: "--cloudflare-account <id>",
|
|
31
|
+
description: "Cloudflare Account ID."
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
};
|
|
35
|
+
async run(options) {
|
|
36
|
+
this.info("Starting Nexical Deployment Automation...");
|
|
37
|
+
if (options.dryRun) {
|
|
38
|
+
this.notice("DRY RUN MODE ENABLED");
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
await this.setupRailway(options);
|
|
42
|
+
await this.setupCloudflare(options);
|
|
43
|
+
await this.setupGitHubSecrets(options);
|
|
44
|
+
this.success("Deployment setup complete! Your application is being deployed.");
|
|
45
|
+
} catch (error) {
|
|
46
|
+
if (error instanceof Error) {
|
|
47
|
+
this.error(`Deployment failed: ${error.message}`);
|
|
48
|
+
} else {
|
|
49
|
+
this.error(`Deployment failed: ${String(error)}`);
|
|
50
|
+
}
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
async setupRailway(options) {
|
|
55
|
+
this.info("Configuring Railway...");
|
|
56
|
+
if (options.dryRun) {
|
|
57
|
+
this.info("[Dry Run] Would run: railway init");
|
|
58
|
+
this.info("[Dry Run] Would run: railway add --database postgres");
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
this.info("Ensuring Railway project is linked...");
|
|
63
|
+
if (options.railwayToken) {
|
|
64
|
+
process.env.RAILWAY_TOKEN = options.railwayToken;
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
await runCommand("railway status");
|
|
68
|
+
} catch {
|
|
69
|
+
this.info("No Railway project detected. Initializing...");
|
|
70
|
+
await runCommand("railway init");
|
|
71
|
+
}
|
|
72
|
+
this.info("Adding PostgreSQL service if missing...");
|
|
73
|
+
const { stdout: status } = await execAsync("railway status");
|
|
74
|
+
if (!status.includes("postgres")) {
|
|
75
|
+
await runCommand("railway add --database postgres");
|
|
76
|
+
}
|
|
77
|
+
} catch (e) {
|
|
78
|
+
this.warn(
|
|
79
|
+
"Railway setup encountered an issue. Ensure you are logged in with `railway login`."
|
|
80
|
+
);
|
|
81
|
+
throw e;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
async setupCloudflare(options) {
|
|
85
|
+
this.info("Configuring Cloudflare Pages...");
|
|
86
|
+
if (options.dryRun) {
|
|
87
|
+
this.info("[Dry Run] Would run: wrangler pages project create nexical-frontend");
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (!options.cloudflareToken || !options.cloudflareAccount) {
|
|
91
|
+
this.warn("Cloudflare credentials missing. Skipping automated Cloudflare setup.");
|
|
92
|
+
this.info("You can manually set up Cloudflare Pages and add the secrets to GitHub.");
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
const projectName = "nexical-frontend";
|
|
97
|
+
this.info(`Ensuring Cloudflare Pages project "${projectName}" exists...`);
|
|
98
|
+
try {
|
|
99
|
+
await execAsync(`wrangler pages project create ${projectName} --production-branch main`, {
|
|
100
|
+
env: {
|
|
101
|
+
...process.env,
|
|
102
|
+
CLOUDFLARE_API_TOKEN: options.cloudflareToken,
|
|
103
|
+
CLOUDFLARE_ACCOUNT_ID: options.cloudflareAccount
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
} catch {
|
|
107
|
+
this.info("Cloudflare project might already exist.");
|
|
108
|
+
}
|
|
109
|
+
} catch (e) {
|
|
110
|
+
this.warn("Cloudflare setup failed.");
|
|
111
|
+
throw e;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
async setupGitHubSecrets(options) {
|
|
115
|
+
this.info("Configuring GitHub Secrets...");
|
|
116
|
+
if (options.dryRun) {
|
|
117
|
+
this.info("[Dry Run] Would run: gh secret set RAILWAY_TOKEN");
|
|
118
|
+
this.info("[Dry Run] Would run: gh secret set CLOUDFLARE_API_TOKEN");
|
|
119
|
+
this.info("[Dry Run] Would run: gh secret set CLOUDFLARE_ACCOUNT_ID");
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
if (options.railwayToken) {
|
|
124
|
+
this.info("Setting RAILWAY_TOKEN in GitHub...");
|
|
125
|
+
await runCommand(`gh secret set RAILWAY_TOKEN --body "${options.railwayToken}"`);
|
|
126
|
+
}
|
|
127
|
+
if (options.cloudflareToken) {
|
|
128
|
+
this.info("Setting CLOUDFLARE_API_TOKEN in GitHub...");
|
|
129
|
+
await runCommand(`gh secret set CLOUDFLARE_API_TOKEN --body "${options.cloudflareToken}"`);
|
|
130
|
+
}
|
|
131
|
+
if (options.cloudflareAccount) {
|
|
132
|
+
this.info("Setting CLOUDFLARE_ACCOUNT_ID in GitHub...");
|
|
133
|
+
await runCommand(
|
|
134
|
+
`gh secret set CLOUDFLARE_ACCOUNT_ID --body "${options.cloudflareAccount}"`
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
} catch (e) {
|
|
138
|
+
this.warn(
|
|
139
|
+
"GitHub Secrets setup failed. Ensure you have the GitHub CLI (gh) installed and are logged in."
|
|
140
|
+
);
|
|
141
|
+
throw e;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
export {
|
|
146
|
+
DeployCommand as default
|
|
147
|
+
};
|
|
148
|
+
//# sourceMappingURL=deploy.js.map
|
|
@@ -0,0 +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":[]}
|
package/package.json
CHANGED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { BaseCommand, runCommand } from '@nexical/cli-core';
|
|
2
|
+
import { exec } from 'node:child_process';
|
|
3
|
+
import { promisify } from 'node:util';
|
|
4
|
+
|
|
5
|
+
const execAsync = promisify(exec);
|
|
6
|
+
|
|
7
|
+
interface DeployOptions {
|
|
8
|
+
dryRun: boolean;
|
|
9
|
+
railwayToken?: string;
|
|
10
|
+
cloudflareToken?: string;
|
|
11
|
+
cloudflareAccount?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default class DeployCommand extends BaseCommand {
|
|
15
|
+
static description = 'Deploy the application to Railway and Cloudflare.';
|
|
16
|
+
|
|
17
|
+
static args = {
|
|
18
|
+
options: [
|
|
19
|
+
{
|
|
20
|
+
name: '--dry-run',
|
|
21
|
+
description: 'Simulate the deployment process without making changes.',
|
|
22
|
+
default: false,
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: '--railway-token <token>',
|
|
26
|
+
description: 'Railway Project Token (optional if already logged in).',
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: '--cloudflare-token <token>',
|
|
30
|
+
description: 'Cloudflare API Token.',
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: '--cloudflare-account <id>',
|
|
34
|
+
description: 'Cloudflare Account ID.',
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
async run(options: DeployOptions) {
|
|
40
|
+
this.info('Starting Nexical Deployment Automation...');
|
|
41
|
+
|
|
42
|
+
if (options.dryRun) {
|
|
43
|
+
this.notice('DRY RUN MODE ENABLED');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
// 1. Railway Setup
|
|
48
|
+
await this.setupRailway(options);
|
|
49
|
+
|
|
50
|
+
// 2. Cloudflare Setup
|
|
51
|
+
await this.setupCloudflare(options);
|
|
52
|
+
|
|
53
|
+
// 3. GitHub Secrets Setup
|
|
54
|
+
await this.setupGitHubSecrets(options);
|
|
55
|
+
|
|
56
|
+
this.success('Deployment setup complete! Your application is being deployed.');
|
|
57
|
+
} catch (error: unknown) {
|
|
58
|
+
if (error instanceof Error) {
|
|
59
|
+
this.error(`Deployment failed: ${error.message}`);
|
|
60
|
+
} else {
|
|
61
|
+
this.error(`Deployment failed: ${String(error)}`);
|
|
62
|
+
}
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private async setupRailway(options: DeployOptions) {
|
|
68
|
+
this.info('Configuring Railway...');
|
|
69
|
+
|
|
70
|
+
if (options.dryRun) {
|
|
71
|
+
this.info('[Dry Run] Would run: railway init');
|
|
72
|
+
this.info('[Dry Run] Would run: railway add --database postgres');
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
// Check if railway project exists or init
|
|
78
|
+
// Note: railway init might be interactive, so we might need to handle that or assume user has linked.
|
|
79
|
+
// For now, let's assume we use 'railway link' if they passed a token or have it set.
|
|
80
|
+
|
|
81
|
+
this.info('Ensuring Railway project is linked...');
|
|
82
|
+
// If they provided a token, we should probably set it in the environment for subsequent calls
|
|
83
|
+
if (options.railwayToken) {
|
|
84
|
+
process.env.RAILWAY_TOKEN = options.railwayToken;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Check if we are in a railway project
|
|
88
|
+
try {
|
|
89
|
+
await runCommand('railway status');
|
|
90
|
+
} catch {
|
|
91
|
+
this.info('No Railway project detected. Initializing...');
|
|
92
|
+
await runCommand('railway init');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
this.info('Adding PostgreSQL service if missing...');
|
|
96
|
+
// railway add --database postgres is usually safe to run twice but we should check status
|
|
97
|
+
const { stdout: status } = await execAsync('railway status');
|
|
98
|
+
if (!status.includes('postgres')) {
|
|
99
|
+
await runCommand('railway add --database postgres');
|
|
100
|
+
}
|
|
101
|
+
} catch (e: unknown) {
|
|
102
|
+
this.warn(
|
|
103
|
+
'Railway setup encountered an issue. Ensure you are logged in with `railway login`.',
|
|
104
|
+
);
|
|
105
|
+
throw e;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private async setupCloudflare(options: DeployOptions) {
|
|
110
|
+
this.info('Configuring Cloudflare Pages...');
|
|
111
|
+
|
|
112
|
+
if (options.dryRun) {
|
|
113
|
+
this.info('[Dry Run] Would run: wrangler pages project create nexical-frontend');
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (!options.cloudflareToken || !options.cloudflareAccount) {
|
|
118
|
+
this.warn('Cloudflare credentials missing. Skipping automated Cloudflare setup.');
|
|
119
|
+
this.info('You can manually set up Cloudflare Pages and add the secrets to GitHub.');
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
// Use wrangler to create project if it doesn't exist
|
|
125
|
+
// We assume project name 'nexical-frontend' for now, should be configurable.
|
|
126
|
+
const projectName = 'nexical-frontend';
|
|
127
|
+
this.info(`Ensuring Cloudflare Pages project "${projectName}" exists...`);
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
await execAsync(`wrangler pages project create ${projectName} --production-branch main`, {
|
|
131
|
+
env: {
|
|
132
|
+
...process.env,
|
|
133
|
+
CLOUDFLARE_API_TOKEN: options.cloudflareToken,
|
|
134
|
+
CLOUDFLARE_ACCOUNT_ID: options.cloudflareAccount,
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
} catch {
|
|
138
|
+
this.info('Cloudflare project might already exist.');
|
|
139
|
+
}
|
|
140
|
+
} catch (e: unknown) {
|
|
141
|
+
this.warn('Cloudflare setup failed.');
|
|
142
|
+
throw e;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private async setupGitHubSecrets(options: DeployOptions) {
|
|
147
|
+
this.info('Configuring GitHub Secrets...');
|
|
148
|
+
|
|
149
|
+
if (options.dryRun) {
|
|
150
|
+
this.info('[Dry Run] Would run: gh secret set RAILWAY_TOKEN');
|
|
151
|
+
this.info('[Dry Run] Would run: gh secret set CLOUDFLARE_API_TOKEN');
|
|
152
|
+
this.info('[Dry Run] Would run: gh secret set CLOUDFLARE_ACCOUNT_ID');
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
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
|
+
if (options.railwayToken) {
|
|
164
|
+
this.info('Setting RAILWAY_TOKEN in GitHub...');
|
|
165
|
+
await runCommand(`gh secret set RAILWAY_TOKEN --body "${options.railwayToken}"`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (options.cloudflareToken) {
|
|
169
|
+
this.info('Setting CLOUDFLARE_API_TOKEN in GitHub...');
|
|
170
|
+
await runCommand(`gh secret set CLOUDFLARE_API_TOKEN --body "${options.cloudflareToken}"`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (options.cloudflareAccount) {
|
|
174
|
+
this.info('Setting CLOUDFLARE_ACCOUNT_ID in GitHub...');
|
|
175
|
+
await runCommand(
|
|
176
|
+
`gh secret set CLOUDFLARE_ACCOUNT_ID --body "${options.cloudflareAccount}"`,
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
} catch (e: unknown) {
|
|
180
|
+
this.warn(
|
|
181
|
+
'GitHub Secrets setup failed. Ensure you have the GitHub CLI (gh) installed and are logged in.',
|
|
182
|
+
);
|
|
183
|
+
throw e;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|