✨ Sync, reload command
This commit is contained in:
parent
f22affc05c
commit
70e5e5eddf
@ -1,6 +1,7 @@
|
|||||||
<component name="InspectionProjectProfileManager">
|
<component name="InspectionProjectProfileManager">
|
||||||
<profile version="1.0">
|
<profile version="1.0">
|
||||||
<option name="myName" value="Project Default" />
|
<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" />
|
<inspection_tool class="ExceptionCaughtLocallyJS" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||||
</profile>
|
</profile>
|
||||||
</component>
|
</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 figlet from "figlet"
|
||||||
import chalk from "chalk"
|
import chalk from "chalk"
|
||||||
|
|
||||||
@ -9,6 +9,8 @@ import { StatusCommand } from "./src/cmd/status.ts"
|
|||||||
import { InfoCommand } from "./src/cmd/info.ts"
|
import { InfoCommand } from "./src/cmd/info.ts"
|
||||||
import { ProcessCommand } from "./src/cmd/process-info.ts"
|
import { ProcessCommand } from "./src/cmd/process-info.ts"
|
||||||
import { DeployCommand } from "./src/cmd/deploy.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
|
const [node, app, ...args] = process.argv
|
||||||
|
|
||||||
@ -26,6 +28,8 @@ const cli = new Cli({
|
|||||||
binaryVersion: `1.0.0`
|
binaryVersion: `1.0.0`
|
||||||
})
|
})
|
||||||
|
|
||||||
|
cli.register(Builtins.VersionCommand)
|
||||||
|
cli.register(Builtins.HelpCommand)
|
||||||
cli.register(LoginCommand)
|
cli.register(LoginCommand)
|
||||||
cli.register(LogoutCommand)
|
cli.register(LogoutCommand)
|
||||||
cli.register(ListServerCommand)
|
cli.register(ListServerCommand)
|
||||||
@ -33,4 +37,6 @@ cli.register(StatusCommand)
|
|||||||
cli.register(InfoCommand)
|
cli.register(InfoCommand)
|
||||||
cli.register(ProcessCommand)
|
cli.register(ProcessCommand)
|
||||||
cli.register(DeployCommand)
|
cli.register(DeployCommand)
|
||||||
|
cli.register(SyncCommand)
|
||||||
|
cli.register(ReloadCommand)
|
||||||
cli.runExit(args)
|
cli.runExit(args)
|
@ -4,6 +4,7 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "latest",
|
"@types/bun": "latest",
|
||||||
|
"@types/cli-progress": "^3.11.6",
|
||||||
"@types/figlet": "^1.5.8"
|
"@types/figlet": "^1.5.8"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
@ -11,6 +12,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chalk": "^5.3.0",
|
"chalk": "^5.3.0",
|
||||||
|
"cli-progress": "^3.12.0",
|
||||||
"cli-table3": "^0.6.5",
|
"cli-table3": "^0.6.5",
|
||||||
"clipanion": "^4.0.0-rc.4",
|
"clipanion": "^4.0.0-rc.4",
|
||||||
"figlet": "^1.7.0",
|
"figlet": "^1.7.0",
|
||||||
|
@ -6,6 +6,7 @@ import * as fs from "node:fs"
|
|||||||
import * as child_process from "node:child_process"
|
import * as child_process from "node:child_process"
|
||||||
import * as path from "node:path"
|
import * as path from "node:path"
|
||||||
import { createAuthHeader } from "../utils/auth.ts"
|
import { createAuthHeader } from "../utils/auth.ts"
|
||||||
|
import { RsLocalConfig } from "../utils/config-local.ts"
|
||||||
|
|
||||||
export class DeployCommand extends Command {
|
export class DeployCommand extends Command {
|
||||||
static paths = [[`deploy`]]
|
static paths = [[`deploy`]]
|
||||||
@ -13,54 +14,53 @@ export class DeployCommand extends Command {
|
|||||||
category: `Building`,
|
category: `Building`,
|
||||||
description: `Deploying App / Static Site onto RoadSign`,
|
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.`,
|
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 })
|
server = Option.String({ required: true })
|
||||||
site = Option.String({ required: true })
|
region = Option.String({ required: false })
|
||||||
upstream = Option.String({ required: true })
|
site = Option.String({ required: false })
|
||||||
input = Option.String({ required: true })
|
input = Option.String({ required: false })
|
||||||
|
|
||||||
async execute() {
|
async deploy(serverLabel: string, region: string, site: string, input: string) {
|
||||||
const config = await RsConfig.getInstance()
|
const cfg = await RsConfig.getInstance()
|
||||||
|
const server = cfg.config.servers.find(item => item.label === serverLabel)
|
||||||
const server = config.config.servers.find(item => item.label === this.server)
|
|
||||||
if (server == null) {
|
if (server == null) {
|
||||||
this.context.stdout.write(chalk.red(`Server with label ${chalk.bold(this.server)} was not found.\n`))
|
this.context.stdout.write(chalk.red(`Server with label ${chalk.bold(this.server)} was not found.\n`))
|
||||||
return
|
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`))
|
this.context.stdout.write(chalk.red(`Input file ${chalk.bold(this.input)} was not found.\n`))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let isDirectory = false
|
let isDirectory = false
|
||||||
if (fs.statSync(this.input).isDirectory()) {
|
if (fs.statSync(input).isDirectory()) {
|
||||||
if (this.input.endsWith("/")) {
|
input = path.join(input, "*")
|
||||||
this.input = this.input.slice(0, -1)
|
|
||||||
}
|
|
||||||
this.input += "/*"
|
|
||||||
|
|
||||||
const compressPrefStart = performance.now()
|
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`
|
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
|
const compressPrefTook = performance.now() - compressPrefStart
|
||||||
compressSpinner.succeed(`Compressing completed in ${(compressPrefTook / 1000).toFixed(2)}s 🎉`)
|
compressSpinner.succeed(`Compressing completed in ${(compressPrefTook / 1000).toFixed(2)}s 🎉`)
|
||||||
this.input = destName
|
input = destName
|
||||||
isDirectory = true
|
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 spinner = ora(`Deploying ${chalk.bold(destBreadcrumb)} to ${chalk.bold(this.server)}...`).start()
|
||||||
|
|
||||||
const prefStart = performance.now()
|
const prefStart = performance.now()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const payload = new FormData()
|
const payload = new FormData()
|
||||||
payload.set("attachments", await fs.openAsBlob(this.input), isDirectory ? "dist.zip" : path.basename(this.input))
|
payload.set("attachments", await fs.openAsBlob(input), isDirectory ? "dist.zip" : path.basename(input))
|
||||||
const res = await fetch(`${server.url}/webhooks/publish/${this.site}/${this.upstream}?mimetype=application/zip`, {
|
const res = await fetch(`${server.url}/webhooks/publish/${region}/${site}?mimetype=application/zip`, {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
body: payload,
|
body: payload,
|
||||||
headers: {
|
headers: {
|
||||||
@ -76,10 +76,37 @@ export class DeployCommand extends Command {
|
|||||||
this.context.stdout.write(`Failed to deploy to remote: ${e}\n`)
|
this.context.stdout.write(`Failed to deploy to remote: ${e}\n`)
|
||||||
spinner.fail(`Server with label ${chalk.bold(this.server)} is not running! 😢`)
|
spinner.fail(`Server with label ${chalk.bold(this.server)} is not running! 😢`)
|
||||||
} finally {
|
} finally {
|
||||||
if (isDirectory && this.input.endsWith(".zip")) {
|
if (isDirectory && input.endsWith(".zip")) {
|
||||||
fs.unlinkSync(this.input)
|
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)
|
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 {
|
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())
|
return fiber.NewError(fiber.ErrInternalServerError.Code, err.Error())
|
||||||
} else {
|
} else {
|
||||||
raw, _ := toml.Marshal(req)
|
var testOut map[string]any
|
||||||
file.Write(raw)
|
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()
|
defer file.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user