This commit is contained in:
SrGooglo 2025-03-25 23:04:18 +00:00
parent 536d7b82d5
commit 8cd34fafcb
14 changed files with 422 additions and 0 deletions

28
packages/cli/package.json Normal file
View File

@ -0,0 +1,28 @@
{
"name": "@ragestudio/comty-cli",
"description": "Command line interface for Comty Services",
"version": "0.1.2",
"publishConfig": {
"access": "public"
},
"main": "./src/index.js",
"type": "module",
"license": "MIT",
"bin": {
"comty-cli": "./src/index.js"
},
"scripts": {
"start": "node ./src/index.js"
},
"dependencies": {
"7zip-min": "^2.0.0",
"@inquirer/prompts": "^7.4.0",
"axios": "^1.8.3",
"commander": "^13.1.0",
"comty.js": "^0.60.7",
"form-data": "^4.0.2",
"formdata-node": "^6.0.3",
"glob": "^11.0.1",
"yocto-spinner": "^0.2.1"
}
}

View File

@ -0,0 +1,30 @@
import path from "node:path"
import fs from "node:fs"
import Config from "./config.js"
export default class Cache {
static cachePath = path.resolve(Config.appWorkdir, "cache")
static async initialize() {
if (!fs.existsSync(this.cachePath)) {
await fs.promises.mkdir(this.cachePath, { recursive: true })
}
}
static async destroyTemporalDir(tempDirId) {
const tempDir = path.resolve(this.cachePath, tempDirId)
await fs.promises.rm(tempDir, {
recursive: true,
})
}
static async createTemporalDir() {
const tempDir = path.join(this.cachePath, `temp-${Date.now()}`)
await fs.promises.mkdir(tempDir, { recursive: true })
return [tempDir, async () => await Cache.destroyTemporalDir(tempDir)]
}
}

View File

@ -0,0 +1,51 @@
import fs from "node:fs"
import path from "node:path"
import os from "node:os"
export default class Config {
static appWorkdir = path.resolve(os.homedir(), ".comty-cli")
static configFilePath = path.resolve(Config.appWorkdir, "config.json")
data = {}
async initialize() {
if (!fs.existsSync(path.dirname(Config.configFilePath))) {
fs.mkdirSync(path.dirname(Config.configFilePath), {
recursive: true,
})
}
if (!fs.existsSync(Config.configFilePath)) {
fs.writeFileSync(Config.configFilePath, JSON.stringify({}))
}
this.data = JSON.parse(
await fs.promises.readFile(Config.configFilePath, "utf8"),
)
}
async write() {
await fs.promises.writeFile(
Config.configFilePath,
JSON.stringify(this.data),
)
}
get(key) {
return this.data[key]
}
async set(key, value) {
this.data[key] = value
await this.write()
return value
}
async delete(key) {
delete this.data[key]
await this.write()
}
}

View File

@ -0,0 +1,8 @@
import authorizeAccount from "../../utils/authorizeAccount.js"
export default {
cmd: "auth",
fn: async () => {
await authorizeAccount()
},
}

View File

@ -0,0 +1,100 @@
import fs from "node:fs"
import path from "node:path"
import yoctoSpinner from "yocto-spinner"
import { fileFromPath } from "formdata-node/file-from-path"
import FormData from "form-data"
import Cache from "../../classes/cache.js"
import getUploadsPaths from "../../utils/getUploadsPaths.js"
import compressFiles from "../../utils/compressFiles.js"
import Request from "comty.js/dist/request.js"
export default {
cmd: "publish",
arguments: [
{
argument: "<cwd>",
description: "Set the current working directory",
},
],
fn: async (customCwd) => {
const token = global.config.get("auth").token
const projectFolder = customCwd ?? process.cwd()
const pkgJsonPath = path.join(projectFolder, "package.json")
let pkgJSON = null
if (!fs.existsSync(pkgJsonPath)) {
console.error("package.json not found")
return 1
}
pkgJSON = JSON.parse(fs.readFileSync(pkgJsonPath, "utf8"))
console.log(`⚙️ Publishing ${pkgJSON.name}@${pkgJSON.version}`)
const [temporalDir, destroyTemporalDir] =
await Cache.createTemporalDir()
const spinner = yoctoSpinner({ text: "Loading…" }).start()
try {
spinner.text = "Reading files..."
const paths = await getUploadsPaths(pkgJsonPath)
const originPath = path.join(temporalDir, "origin")
const bundlePath = path.join(temporalDir, "bundle.7z")
// copy files and dirs to origin path
spinner.text = "Copying files and directories"
for await (const file of paths) {
await fs.promises.cp(
file,
path.join(originPath, path.basename(file)),
{ recursive: true },
)
}
spinner.text = "Compressing files"
await compressFiles(`${originPath}/*`, bundlePath)
// PUT to registry
const bodyData = new FormData()
//bodyData.append("pkg", JSON.stringify(pkgJSON))
bodyData.append("bundle", fs.createReadStream(bundlePath))
spinner.text = "Publishing extension"
const response = await Request.default({
method: "PUT",
url: "/extensions/publish",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "multipart/form-data",
pkg: JSON.stringify(pkgJSON),
},
data: bodyData,
}).catch((error) => {
throw new Error(
`Failed to publish extension: ${error.response?.data?.error ?? error.message}`,
)
})
// cleanup
spinner.text = "Cleaning up"
await destroyTemporalDir()
spinner.success(`Ok!`)
console.log(response.data)
return 0
} catch (error) {
await destroyTemporalDir()
spinner.error(`${error.message}`)
console.error(error)
return 1
}
},
}

View File

@ -0,0 +1,3 @@
export default {
cmd: "template",
}

63
packages/cli/src/index.js Executable file
View File

@ -0,0 +1,63 @@
#!/usr/bin/env node
import fs from "node:fs"
import path from "node:path"
import { Command } from "commander"
import { createClient } from "comty.js"
import SessionModel from "comty.js/dist/models/session/index.js"
import Cache from "./classes/cache.js"
import Config from "./classes/config.js"
import readCommandsFiles from "./utils/readCommandsFiles.js"
import importDefaults from "./utils/importDefaults.js"
import buildCommands from "./utils/buildCommands.js"
import authorizeAccount from "./utils/authorizeAccount.js"
const commandsPath = path.resolve(import.meta.dirname, "commands")
const packageJsonPath = path.resolve(import.meta.dirname, "../package.json")
// only for development
if (process.env.NODE_ENV === "development") {
process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = 0
}
async function main() {
let packageJson = await fs.promises.readFile(packageJsonPath, "utf8")
packageJson = JSON.parse(packageJson)
global.config = new Config()
global.comtyClient = createClient({
origin:
process.env.NODE_ENV === "production"
? "https://api.comty.app"
: "https://indev.comty.app/api",
})
await global.config.initialize()
await Cache.initialize()
if (!global.config.get("auth")) {
console.log("No auth found, authentication required...")
await authorizeAccount()
}
SessionModel.default.token = global.config.get("auth").token
let program = new Command()
program
.name(packageJson.name)
.description(packageJson.description)
.version(packageJson.version)
let commands = await readCommandsFiles(commandsPath)
commands = await importDefaults(commands)
program = await buildCommands(commands, program)
program.parse()
return 0
}
main()

View File

@ -0,0 +1,25 @@
import * as prompts from "@inquirer/prompts"
import AuthModel from "comty.js/dist/models/auth/index.js"
export default function authorizeAccount() {
return new Promise(async (resolve, reject) => {
const username = await prompts.input({
message: "username or email >",
})
const password = await prompts.password({
message: "password >",
})
try {
const result = await AuthModel.default.login({ username, password })
console.log("✅ Logged in successfully")
await global.config.set("auth", result)
resolve(result)
} catch (error) {
console.error(`⛔ Failed to login: ${error.response.data.error}`)
authorizeAccount()
}
})
}

View File

@ -0,0 +1,30 @@
export default async function buildCommands(commands, program) {
for await (const command of commands) {
if (typeof command.fn !== "function") {
continue
}
const commandInstance = program.command(command.cmd).action(command.fn)
if (command.description) {
commandInstance.description(command.description)
}
if (command.options) {
for (const option of command.options) {
commandInstance.option(option.option, option.description)
}
}
if (command.arguments) {
for (const argument of command.arguments) {
commandInstance.argument(
argument.argument,
argument.description,
)
}
}
}
return program
}

View File

@ -0,0 +1,21 @@
import fs from "node:fs"
import sevenzip from "7zip-min"
export default async function compressFiles(origin, output) {
// check if out file exists
if (fs.existsSync(output)) {
fs.unlinkSync(output)
}
await new Promise((resolve, reject) => {
sevenzip.pack(origin, output, (err) => {
if (err) {
return reject(err)
}
return resolve(output)
})
})
return output
}

View File

@ -0,0 +1,23 @@
import fs from "node:fs"
import path from "node:path"
import { glob } from "glob"
export default async function getUploadsPaths(pkgJsonPath) {
const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, "utf8"))
const projectFolder = path.dirname(pkgJsonPath)
//const gitIgnore = await readGitIgnore(projectFolder)
let globs = []
if (Array.isArray(pkgJson.files)) {
globs.push(...pkgJson.files)
}
globs = globs.map((glob) => path.resolve(projectFolder, glob))
globs = await glob(globs, { cwd: projectFolder })
globs.push(pkgJsonPath)
return globs
}

View File

@ -0,0 +1,10 @@
export default async function importDefaults(commands) {
const result = []
for await (const command of commands) {
const commandModule = await import(command)
result.push(commandModule.default)
}
return result
}

View File

@ -0,0 +1,18 @@
import fs from "node:fs"
import path from "node:path"
export default async function readCommandsFiles(from) {
const result = []
let files = await fs.promises.readdir(from)
for (const file of files) {
const filePath = path.join(from, file)
const stat = await fs.promises.stat(filePath)
if (stat.isDirectory()) {
result.push(path.resolve(from, file, "index.js"))
}
}
return result
}

View File

@ -0,0 +1,12 @@
import fs from "node:fs"
import path from "node:path"
export default async function readGitIgnore(projectFolder) {
const gitIgnorePath = path.join(projectFolder, ".gitignore")
if (!fs.existsSync(gitIgnorePath)) {
return []
}
return fs.readFileSync(gitIgnorePath, "utf8").split("\n").filter(Boolean)
}