✨ Sync, reload command
This commit is contained in:
		
							
								
								
									
										1
									
								
								.idea/inspectionProfiles/Project_Default.xml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1
									
								
								.idea/inspectionProfiles/Project_Default.xml
									
									
									
										generated
									
									
									
								
							| @@ -1,6 +1,7 @@ | ||||
| <component name="InspectionProjectProfileManager"> | ||||
|   <profile version="1.0"> | ||||
|     <option name="myName" value="Project Default" /> | ||||
|     <inspection_tool class="DuplicatedCode" enabled="false" level="WEAK WARNING" enabled_by_default="false" /> | ||||
|     <inspection_tool class="ExceptionCaughtLocallyJS" enabled="false" level="WARNING" enabled_by_default="false" /> | ||||
|   </profile> | ||||
| </component> | ||||
							
								
								
									
										9
									
								
								cli/.roadsignrc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								cli/.roadsignrc
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| { | ||||
|   "deployments": [ | ||||
|     { | ||||
|       "path": "test/static-files", | ||||
|       "region": "static-files", | ||||
|       "site": "static-files-des" | ||||
|     } | ||||
|   ] | ||||
| } | ||||
							
								
								
									
										
											BIN
										
									
								
								cli/bun.lockb
									
									
									
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								cli/bun.lockb
									
									
									
									
									
								
							
										
											Binary file not shown.
										
									
								
							| @@ -1,4 +1,4 @@ | ||||
| import { Cli } from "clipanion" | ||||
| import { Builtins, Cli } from "clipanion" | ||||
| import figlet from "figlet" | ||||
| import chalk from "chalk" | ||||
|  | ||||
| @@ -9,6 +9,8 @@ import { StatusCommand } from "./src/cmd/status.ts" | ||||
| import { InfoCommand } from "./src/cmd/info.ts" | ||||
| import { ProcessCommand } from "./src/cmd/process-info.ts" | ||||
| import { DeployCommand } from "./src/cmd/deploy.ts" | ||||
| import { SyncCommand } from "./src/cmd/sync.ts" | ||||
| import { ReloadCommand } from "./src/cmd/reload.ts" | ||||
|  | ||||
| const [node, app, ...args] = process.argv | ||||
|  | ||||
| @@ -26,6 +28,8 @@ const cli = new Cli({ | ||||
|   binaryVersion: `1.0.0` | ||||
| }) | ||||
|  | ||||
| cli.register(Builtins.VersionCommand) | ||||
| cli.register(Builtins.HelpCommand) | ||||
| cli.register(LoginCommand) | ||||
| cli.register(LogoutCommand) | ||||
| cli.register(ListServerCommand) | ||||
| @@ -33,4 +37,6 @@ cli.register(StatusCommand) | ||||
| cli.register(InfoCommand) | ||||
| cli.register(ProcessCommand) | ||||
| cli.register(DeployCommand) | ||||
| cli.register(SyncCommand) | ||||
| cli.register(ReloadCommand) | ||||
| cli.runExit(args) | ||||
| @@ -4,6 +4,7 @@ | ||||
|   "type": "module", | ||||
|   "devDependencies": { | ||||
|     "@types/bun": "latest", | ||||
|     "@types/cli-progress": "^3.11.6", | ||||
|     "@types/figlet": "^1.5.8" | ||||
|   }, | ||||
|   "peerDependencies": { | ||||
| @@ -11,6 +12,7 @@ | ||||
|   }, | ||||
|   "dependencies": { | ||||
|     "chalk": "^5.3.0", | ||||
|     "cli-progress": "^3.12.0", | ||||
|     "cli-table3": "^0.6.5", | ||||
|     "clipanion": "^4.0.0-rc.4", | ||||
|     "figlet": "^1.7.0", | ||||
|   | ||||
| @@ -6,6 +6,7 @@ import * as fs from "node:fs" | ||||
| import * as child_process from "node:child_process" | ||||
| import * as path from "node:path" | ||||
| import { createAuthHeader } from "../utils/auth.ts" | ||||
| import { RsLocalConfig } from "../utils/config-local.ts" | ||||
|  | ||||
| export class DeployCommand extends Command { | ||||
|   static paths = [[`deploy`]] | ||||
| @@ -13,54 +14,53 @@ export class DeployCommand extends Command { | ||||
|     category: `Building`, | ||||
|     description: `Deploying App / Static Site onto RoadSign`, | ||||
|     details: `Deploying an application or hosting a static site via RoadSign, you need preconfigured the RoadSign, or sync the configurations via sync command.`, | ||||
|     examples: [["Deploying to RoadSign", `deploy <server> <site> <slug> <file / directory>`]] | ||||
|     examples: [ | ||||
|       ["Deploying to RoadSign", `deploy <server> <region> <site> <file / directory>`], | ||||
|       ["Deploying to RoadSign with .roadsignrc file", `deploy <server>`] | ||||
|     ] | ||||
|   } | ||||
|  | ||||
|   server = Option.String({ required: true }) | ||||
|   site = Option.String({ required: true }) | ||||
|   upstream = Option.String({ required: true }) | ||||
|   input = Option.String({ required: true }) | ||||
|   region = Option.String({ required: false }) | ||||
|   site = Option.String({ required: false }) | ||||
|   input = Option.String({ required: false }) | ||||
|  | ||||
|   async execute() { | ||||
|     const config = await RsConfig.getInstance() | ||||
|  | ||||
|     const server = config.config.servers.find(item => item.label === this.server) | ||||
|   async deploy(serverLabel: string, region: string, site: string, input: string) { | ||||
|     const cfg = await RsConfig.getInstance() | ||||
|     const server = cfg.config.servers.find(item => item.label === serverLabel) | ||||
|     if (server == null) { | ||||
|       this.context.stdout.write(chalk.red(`Server with label ${chalk.bold(this.server)} was not found.\n`)) | ||||
|       return | ||||
|     } | ||||
|  | ||||
|     if (!fs.existsSync(this.input)) { | ||||
|     if (!fs.existsSync(input)) { | ||||
|       this.context.stdout.write(chalk.red(`Input file ${chalk.bold(this.input)} was not found.\n`)) | ||||
|       return | ||||
|     } | ||||
|  | ||||
|     let isDirectory = false | ||||
|     if (fs.statSync(this.input).isDirectory()) { | ||||
|       if (this.input.endsWith("/")) { | ||||
|         this.input = this.input.slice(0, -1) | ||||
|       } | ||||
|       this.input += "/*" | ||||
|     if (fs.statSync(input).isDirectory()) { | ||||
|       input = path.join(input, "*") | ||||
|  | ||||
|       const compressPrefStart = performance.now() | ||||
|       const compressSpinner = ora(`Compressing ${chalk.bold(this.input)}...`).start() | ||||
|       const compressSpinner = ora(`Compressing ${chalk.bold(input)}...`).start() | ||||
|       const destName = `${Date.now()}-roadsign-archive.zip` | ||||
|       child_process.execSync(`zip -rj ${destName} ${this.input}`) | ||||
|       child_process.execSync(`zip -rj ${destName} ${input}`) | ||||
|       const compressPrefTook = performance.now() - compressPrefStart | ||||
|       compressSpinner.succeed(`Compressing completed in ${(compressPrefTook / 1000).toFixed(2)}s 🎉`) | ||||
|       this.input = destName | ||||
|       input = destName | ||||
|       isDirectory = true | ||||
|     } | ||||
|  | ||||
|     const destBreadcrumb = [this.site, this.upstream].join(" ➜ ") | ||||
|     const destBreadcrumb = [region, site].join(" ➜ ") | ||||
|     const spinner = ora(`Deploying ${chalk.bold(destBreadcrumb)} to ${chalk.bold(this.server)}...`).start() | ||||
|  | ||||
|     const prefStart = performance.now() | ||||
|  | ||||
|     try { | ||||
|       const payload = new FormData() | ||||
|       payload.set("attachments", await fs.openAsBlob(this.input), isDirectory ? "dist.zip" : path.basename(this.input)) | ||||
|       const res = await fetch(`${server.url}/webhooks/publish/${this.site}/${this.upstream}?mimetype=application/zip`, { | ||||
|       payload.set("attachments", await fs.openAsBlob(input), isDirectory ? "dist.zip" : path.basename(input)) | ||||
|       const res = await fetch(`${server.url}/webhooks/publish/${region}/${site}?mimetype=application/zip`, { | ||||
|         method: "PUT", | ||||
|         body: payload, | ||||
|         headers: { | ||||
| @@ -76,10 +76,37 @@ export class DeployCommand extends Command { | ||||
|       this.context.stdout.write(`Failed to deploy to remote: ${e}\n`) | ||||
|       spinner.fail(`Server with label ${chalk.bold(this.server)} is not running! 😢`) | ||||
|     } finally { | ||||
|       if (isDirectory && this.input.endsWith(".zip")) { | ||||
|         fs.unlinkSync(this.input) | ||||
|       if (isDirectory && input.endsWith(".zip")) { | ||||
|         fs.unlinkSync(input) | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   async execute() { | ||||
|     if (this.region && this.site && this.input) { | ||||
|       await this.deploy(this.server, this.region, this.site, this.input) | ||||
|     } else { | ||||
|       let localCfg: RsLocalConfig | ||||
|       try { | ||||
|         localCfg = await RsLocalConfig.getInstance() | ||||
|       } catch (e) { | ||||
|         this.context.stdout.write(chalk.red(`Unable to load .roadsignrc: ${e}\n`)) | ||||
|         return | ||||
|       } | ||||
|  | ||||
|       if (!localCfg.config.deployments) { | ||||
|         this.context.stdout.write(chalk.red(`No deployments found in .roadsignrc, exiting...\n`)) | ||||
|         return | ||||
|       } | ||||
|  | ||||
|       let idx = 0 | ||||
|       for (const deployment of localCfg.config.deployments ?? []) { | ||||
|         this.context.stdout.write(chalk.cyan(`Deploying ${idx + 1} out of ${localCfg.config.deployments.length} deployments...\n`)) | ||||
|         await this.deploy(this.server, deployment.region, deployment.site, deployment.path) | ||||
|       } | ||||
|  | ||||
|       this.context.stdout.write(chalk.green(`All deployments has been deployed!\n`)) | ||||
|     } | ||||
|  | ||||
|     process.exit(0) | ||||
|   } | ||||
|   | ||||
							
								
								
									
										53
									
								
								cli/src/cmd/reload.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								cli/src/cmd/reload.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,53 @@ | ||||
| import { RsConfig } from "../utils/config.ts" | ||||
| import { Command, Option, type Usage } from "clipanion" | ||||
| import chalk from "chalk" | ||||
| import ora from "ora" | ||||
| import * as fs from "node:fs" | ||||
| import { createAuthHeader } from "../utils/auth.ts" | ||||
| import { RsLocalConfig } from "../utils/config-local.ts" | ||||
|  | ||||
| export class ReloadCommand extends Command { | ||||
|   static paths = [[`reload`]] | ||||
|   static usage: Usage = { | ||||
|     category: `Building`, | ||||
|     description: `Reload configuration on RoadSign`, | ||||
|     details: `Reload configuration on remote RoadSign to make changes applied.`, | ||||
|     examples: [ | ||||
|       ["Reload an connected server", `reload <server>`], | ||||
|     ] | ||||
|   } | ||||
|  | ||||
|   server = Option.String({ required: true }) | ||||
|  | ||||
|   async execute() { | ||||
|     const cfg = await RsConfig.getInstance() | ||||
|     const server = cfg.config.servers.find(item => item.label === this.server) | ||||
|     if (server == null) { | ||||
|       this.context.stdout.write(chalk.red(`Server with label ${chalk.bold(this.server)} was not found.\n`)) | ||||
|       return | ||||
|     } | ||||
|  | ||||
|     const spinner = ora(`Reloading server ${chalk.bold(this.server)}...`).start() | ||||
|  | ||||
|     const prefStart = performance.now() | ||||
|  | ||||
|     try { | ||||
|       const res = await fetch(`${server.url}/cgi/reload`, { | ||||
|         method: "POST", | ||||
|         headers: { | ||||
|           Authorization: createAuthHeader(server.credential) | ||||
|         } | ||||
|       }) | ||||
|       if (res.status !== 200) { | ||||
|         throw new Error(await res.text()) | ||||
|       } | ||||
|       const prefTook = performance.now() - prefStart | ||||
|       spinner.succeed(`Reloading completed in ${(prefTook / 1000).toFixed(2)}s 🎉`) | ||||
|     } catch (e) { | ||||
|       this.context.stdout.write(`Failed to reload remote: ${e}\n`) | ||||
|       spinner.fail(`Server with label ${chalk.bold(this.server)} is not running! 😢`) | ||||
|     } | ||||
|  | ||||
|     process.exit(0) | ||||
|   } | ||||
| } | ||||
							
								
								
									
										87
									
								
								cli/src/cmd/sync.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								cli/src/cmd/sync.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,87 @@ | ||||
| import { RsConfig } from "../utils/config.ts" | ||||
| import { Command, Option, type Usage } from "clipanion" | ||||
| import chalk from "chalk" | ||||
| import ora from "ora" | ||||
| import * as fs from "node:fs" | ||||
| import { createAuthHeader } from "../utils/auth.ts" | ||||
| import { RsLocalConfig } from "../utils/config-local.ts" | ||||
|  | ||||
| export class SyncCommand extends Command { | ||||
|   static paths = [[`sync`]] | ||||
|   static usage: Usage = { | ||||
|     category: `Building`, | ||||
|     description: `Sync configuration to RoadSign over Sideload`, | ||||
|     details: `Update remote RoadSign configuration with local ones.`, | ||||
|     examples: [ | ||||
|       ["Sync to RoadSign", `sync <server> <region> <file>`], | ||||
|       ["Sync to RoadSign with .roadsignrc file", `sync <server>`] | ||||
|     ] | ||||
|   } | ||||
|  | ||||
|   server = Option.String({ required: true }) | ||||
|   region = Option.String({ required: false }) | ||||
|   input = Option.String({ required: false }) | ||||
|  | ||||
|   async sync(serverLabel: string, region: string, input: string) { | ||||
|     const cfg = await RsConfig.getInstance() | ||||
|     const server = cfg.config.servers.find(item => item.label === serverLabel) | ||||
|     if (server == null) { | ||||
|       this.context.stdout.write(chalk.red(`Server with label ${chalk.bold(this.server)} was not found.\n`)) | ||||
|       return | ||||
|     } | ||||
|  | ||||
|     if (!fs.existsSync(input)) { | ||||
|       this.context.stdout.write(chalk.red(`Input file ${chalk.bold(this.input)} was not found.\n`)) | ||||
|       return | ||||
|     } | ||||
|     if (!fs.statSync(input).isFile()) { | ||||
|       this.context.stdout.write(chalk.red(`Input file ${chalk.bold(this.input)} is not a file.\n`)) | ||||
|       return | ||||
|     } | ||||
|  | ||||
|     const spinner = ora(`Syncing ${chalk.bold(region)} to ${chalk.bold(this.server)}...`).start() | ||||
|  | ||||
|     const prefStart = performance.now() | ||||
|  | ||||
|     try { | ||||
|       const res = await fetch(`${server.url}/webhooks/sync/${region}`, { | ||||
|         method: "PUT", | ||||
|         body: fs.readFileSync(input, "utf8"), | ||||
|         headers: { | ||||
|           Authorization: createAuthHeader(server.credential) | ||||
|         } | ||||
|       }) | ||||
|       if (res.status !== 200) { | ||||
|         throw new Error(await res.text()) | ||||
|       } | ||||
|       const prefTook = performance.now() - prefStart | ||||
|       spinner.succeed(`Syncing completed in ${(prefTook / 1000).toFixed(2)}s 🎉`) | ||||
|     } catch (e) { | ||||
|       this.context.stdout.write(`Failed to sync to remote: ${e}\n`) | ||||
|       spinner.fail(`Server with label ${chalk.bold(this.server)} is not running! 😢`) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   async execute() { | ||||
|     if (this.region && this.input) { | ||||
|       await this.sync(this.server, this.region, this.input) | ||||
|     } else { | ||||
|       let localCfg: RsLocalConfig | ||||
|       try { | ||||
|         localCfg = await RsLocalConfig.getInstance() | ||||
|       } catch (e) { | ||||
|         this.context.stdout.write(chalk.red(`Unable to load .roadsignrc: ${e}\n`)) | ||||
|         return | ||||
|       } | ||||
|  | ||||
|       if (!localCfg.config.sync) { | ||||
|         this.context.stdout.write(chalk.red(`No sync configuration found in .roadsignrc, exiting...\n`)) | ||||
|         return | ||||
|       } | ||||
|  | ||||
|       await this.sync(this.server, localCfg.config.sync.region, localCfg.config.sync.configPath) | ||||
|     } | ||||
|  | ||||
|     process.exit(0) | ||||
|   } | ||||
| } | ||||
							
								
								
									
										60
									
								
								cli/src/utils/config-local.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								cli/src/utils/config-local.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,60 @@ | ||||
| import * as path from "node:path" | ||||
| import * as fs from "node:fs/promises" | ||||
|  | ||||
| interface RsLocalConfigData { | ||||
|   sync?: RsLocalConfigSyncData | ||||
|   deployments?: RsLocalConfigDeploymentData[] | ||||
| } | ||||
|  | ||||
| interface RsLocalConfigSyncData { | ||||
|   configPath: string | ||||
|   region: string | ||||
| } | ||||
|  | ||||
| interface RsLocalConfigDeploymentData { | ||||
|   path: string | ||||
|   region: string | ||||
|   site: string | ||||
|   autoBuild?: RsLocalConfigDeploymentAutoBuildData | ||||
| } | ||||
|  | ||||
| interface RsLocalConfigDeploymentAutoBuildData { | ||||
|   command: string | ||||
|   environment?: string[] | ||||
| } | ||||
|  | ||||
| class RsLocalConfig { | ||||
|   private static instance: RsLocalConfig | ||||
|  | ||||
|   public config: RsLocalConfigData = {} | ||||
|  | ||||
|   private constructor() { | ||||
|   } | ||||
|  | ||||
|   public static async getInstance(): Promise<RsLocalConfig> { | ||||
|     if (!RsLocalConfig.instance) { | ||||
|       RsLocalConfig.instance = new RsLocalConfig() | ||||
|       await RsLocalConfig.instance.readConfig() | ||||
|     } | ||||
|     return RsLocalConfig.instance | ||||
|   } | ||||
|  | ||||
|   public async readConfig() { | ||||
|     const basepath = process.cwd() | ||||
|     const filepath = path.join(basepath, ".roadsignrc") | ||||
|     if (!await fs.exists(filepath)) { | ||||
|       throw new Error(`.roadsignrc file was not found at ${filepath}`) | ||||
|     } | ||||
|  | ||||
|     const data = await fs.readFile(filepath, "utf8") | ||||
|     this.config = JSON.parse(data) | ||||
|   } | ||||
|  | ||||
|   public async writeConfig() { | ||||
|     const basepath = process.cwd() | ||||
|     const filepath = path.join(basepath, ".roadsignrc") | ||||
|     await fs.writeFile(filepath, JSON.stringify(this.config)) | ||||
|   } | ||||
| } | ||||
|  | ||||
| export { RsLocalConfig, type RsLocalConfigData } | ||||
							
								
								
									
										9
									
								
								cli/test/static-files.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								cli/test/static-files.toml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| id = "static-files-num2" | ||||
|  | ||||
| [[locations]] | ||||
| id = "static-files-loc-num2" | ||||
| hosts = ["127.0.0.1:8000"] | ||||
| paths = ["/"] | ||||
| [[locations.destinations]] | ||||
| id = "static-files-des-num2" | ||||
| uri = "files://../data/static-files" | ||||
| @@ -38,8 +38,11 @@ func doSync(c *fiber.Ctx) error { | ||||
| 	if file, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755); err != nil { | ||||
| 		return fiber.NewError(fiber.ErrInternalServerError.Code, err.Error()) | ||||
| 	} else { | ||||
| 		raw, _ := toml.Marshal(req) | ||||
| 		file.Write(raw) | ||||
| 		var testOut map[string]any | ||||
| 		if err := toml.Unmarshal([]byte(req), &testOut); err != nil { | ||||
| 			return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("invalid configuration: %v", err)) | ||||
| 		} | ||||
| 		_, _ = file.Write([]byte(req)) | ||||
| 		defer file.Close() | ||||
| 	} | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user