@atomservice/cli 0.1.0
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/bin/atom.ts +58 -0
- package/package.json +40 -0
- package/src/add/add.cmd.ts +67 -0
- package/src/cli.consts.ts +14 -0
- package/src/cli.link.ts +41 -0
- package/src/init/init.cmd.ts +72 -0
- package/src/run/index.ts +13 -0
- package/src/run/run.down.ts +36 -0
- package/src/run/run.status.ts +70 -0
- package/src/run/run.up.ts +38 -0
- package/src/run/run.utils.ts +76 -0
- package/src/service/index.ts +1 -0
- package/src/service/service.cmds.ts +34 -0
package/bin/atom.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { UserError } from "@atomservice/core"
|
|
3
|
+
import { type ArgsDef, type CommandDef, defineCommand, renderUsage, runCommand, runMain } from "citty"
|
|
4
|
+
import pc from "picocolors"
|
|
5
|
+
import { addCmd } from "../src/add/add.cmd.ts"
|
|
6
|
+
import { initCmd } from "../src/init/init.cmd.ts"
|
|
7
|
+
import { runCmd } from "../src/run/index.ts"
|
|
8
|
+
import { buildServiceSubCommands } from "../src/service/index.ts"
|
|
9
|
+
|
|
10
|
+
async function showUsage<T extends ArgsDef>(cmd: CommandDef<T>, parent?: CommandDef<T>) {
|
|
11
|
+
const raw = await renderUsage(cmd, parent)
|
|
12
|
+
const zh = raw
|
|
13
|
+
.replace(/USAGE/g, "用法")
|
|
14
|
+
.replace(/COMMANDS/g, "命令")
|
|
15
|
+
.replace(/ARGUMENTS/g, "参数")
|
|
16
|
+
.replace(/OPTIONS/g, "选项")
|
|
17
|
+
.replace(/Use .+ for more information about a command\./g, "运行 atom <命令> --help 查看命令详情。")
|
|
18
|
+
console.log(`${zh}\n`)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const main = defineCommand({
|
|
22
|
+
meta: { name: "atom", version: "0.0.1", description: "原子化自托管服务编排工具" },
|
|
23
|
+
subCommands: async () => ({
|
|
24
|
+
init: initCmd,
|
|
25
|
+
add: addCmd,
|
|
26
|
+
run: runCmd,
|
|
27
|
+
...(await buildServiceSubCommands()),
|
|
28
|
+
}),
|
|
29
|
+
run: async () => {
|
|
30
|
+
const subcmd = process.argv.slice(2).find((a) => !a.startsWith("-"))
|
|
31
|
+
if (!subcmd) await showUsage(main)
|
|
32
|
+
},
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
const argv = process.argv.slice(2)
|
|
36
|
+
const wantsHelpOrVersion =
|
|
37
|
+
argv.includes("--help") || argv.includes("-h") || (argv.length === 1 && argv[0] === "--version")
|
|
38
|
+
|
|
39
|
+
if (wantsHelpOrVersion) {
|
|
40
|
+
// help / version 由 citty 自带逻辑处理
|
|
41
|
+
await runMain(main, { showUsage })
|
|
42
|
+
} else {
|
|
43
|
+
try {
|
|
44
|
+
await runCommand(main, { rawArgs: argv })
|
|
45
|
+
} catch (err) {
|
|
46
|
+
if (err instanceof UserError) {
|
|
47
|
+
console.error(`\n${pc.red("✗")} ${err.message}`)
|
|
48
|
+
if (err.hint) console.error(pc.dim(` ${err.hint}`))
|
|
49
|
+
} else {
|
|
50
|
+
console.error(`\n${pc.red("✗")} ${err instanceof Error ? err.message : String(err)}`)
|
|
51
|
+
console.error(pc.dim(" 如果这看起来是个 bug,可加 --verbose 查看详细堆栈"))
|
|
52
|
+
if (process.env.ATOMSERVICE_VERBOSE && err instanceof Error && err.stack) {
|
|
53
|
+
console.error(pc.dim(err.stack))
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
process.exit(1)
|
|
57
|
+
}
|
|
58
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@atomservice/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "atomservice 命令行工具(atom):init / run / add / service",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"author": "openorson",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/openorson/atomservice.git",
|
|
10
|
+
"directory": "packages/cli"
|
|
11
|
+
},
|
|
12
|
+
"bugs": "https://github.com/openorson/atomservice/issues",
|
|
13
|
+
"homepage": "https://github.com/openorson/atomservice/tree/main/packages/cli#readme",
|
|
14
|
+
"keywords": [
|
|
15
|
+
"atomservice",
|
|
16
|
+
"cli",
|
|
17
|
+
"self-hosted",
|
|
18
|
+
"podman",
|
|
19
|
+
"bun"
|
|
20
|
+
],
|
|
21
|
+
"engines": {
|
|
22
|
+
"bun": ">=1.0.0"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"src",
|
|
26
|
+
"bin"
|
|
27
|
+
],
|
|
28
|
+
"bin": {
|
|
29
|
+
"atom": "./bin/atom.ts"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@clack/prompts": "^1.4.0",
|
|
33
|
+
"@atomservice/core": "workspace:*",
|
|
34
|
+
"citty": "^0.2.2",
|
|
35
|
+
"cli-table3": "^0.6.5",
|
|
36
|
+
"log-update": "^8.0.0",
|
|
37
|
+
"ora": "^9.4.0",
|
|
38
|
+
"picocolors": "^1.1.1"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import fs from "node:fs"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
import { ATOMSERVICE_DIR } from "@atomservice/core"
|
|
4
|
+
import { cancel, confirm, isCancel, multiselect, spinner } from "@clack/prompts"
|
|
5
|
+
import { defineCommand } from "citty"
|
|
6
|
+
import pc from "picocolors"
|
|
7
|
+
import { OFFICIAL_SERVICES } from "../cli.consts.ts"
|
|
8
|
+
import { ensureLocalLink } from "../cli.link.ts"
|
|
9
|
+
|
|
10
|
+
async function installService(name: string) {
|
|
11
|
+
const pkgName = `@atomservice/${name}`
|
|
12
|
+
const symlinkPath = path.join(ATOMSERVICE_DIR, "node_modules", "@atomservice", name)
|
|
13
|
+
|
|
14
|
+
const existing = fs.lstatSync(symlinkPath, { throwIfNoEntry: false })
|
|
15
|
+
if (existing) {
|
|
16
|
+
try {
|
|
17
|
+
const resolved = fs.realpathSync(symlinkPath)
|
|
18
|
+
if (fs.existsSync(path.join(resolved, "package.json"))) return
|
|
19
|
+
} catch {
|
|
20
|
+
fs.rmSync(symlinkPath, { force: true, recursive: true })
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const s = spinner()
|
|
25
|
+
s.start(`安装 ${pkgName}…`)
|
|
26
|
+
|
|
27
|
+
const installed = await Bun.$`bun add --cwd ${ATOMSERVICE_DIR} ${pkgName}`.quiet().nothrow()
|
|
28
|
+
if (installed.exitCode === 0) {
|
|
29
|
+
s.stop(pc.green(`${pkgName} 已安装`))
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const linked = ensureLocalLink(name)
|
|
34
|
+
s.stop(linked ? pc.green(`${pkgName} 已就绪(开发模式)`) : pc.red(`${pkgName} 安装失败`))
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const addCmd = defineCommand({
|
|
38
|
+
meta: { name: "add", description: "安装原子服务" },
|
|
39
|
+
run: async () => {
|
|
40
|
+
const selected = await multiselect({
|
|
41
|
+
message: "选择要安装的原子服务(空格选择,回车确认)",
|
|
42
|
+
options: OFFICIAL_SERVICES,
|
|
43
|
+
required: true,
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
if (isCancel(selected)) {
|
|
47
|
+
cancel("已取消")
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const ok = await confirm({ message: `安装 ${(selected as string[]).length} 个原子服务?` })
|
|
52
|
+
|
|
53
|
+
if (isCancel(ok) || !ok) {
|
|
54
|
+
cancel("已取消")
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
for (const name of selected as string[]) {
|
|
59
|
+
await installService(name)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
console.log("")
|
|
63
|
+
console.log(
|
|
64
|
+
`${pc.green("✔")} 完成,在 ${pc.bold("~/.atomservice/atom.config.ts")} 中引入原子服务后运行 ${pc.cyan("atom run up")}`,
|
|
65
|
+
)
|
|
66
|
+
},
|
|
67
|
+
})
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import path from "node:path"
|
|
2
|
+
import { ATOMSERVICE_DIR } from "@atomservice/core"
|
|
3
|
+
|
|
4
|
+
export const CONFIG_PATH = path.join(ATOMSERVICE_DIR, "atom.config.ts")
|
|
5
|
+
|
|
6
|
+
export const OFFICIAL_SERVICES: Array<{ value: string; label: string; hint: string }> = [
|
|
7
|
+
{ value: "service-redis", label: "service-redis", hint: "Redis 缓存" },
|
|
8
|
+
{ value: "service-postgres", label: "service-postgres", hint: "PostgreSQL 数据库" },
|
|
9
|
+
{ value: "service-gateway", label: "service-gateway", hint: "反向代理 + HTTPS" },
|
|
10
|
+
{ value: "service-gitea", label: "service-gitea", hint: "Gitea Git 服务" },
|
|
11
|
+
{ value: "service-config", label: "service-config", hint: "通用配置中心" },
|
|
12
|
+
{ value: "service-app", label: "service-app", hint: "容器化后端应用部署" },
|
|
13
|
+
{ value: "service-site", label: "service-site", hint: "静态网站部署" },
|
|
14
|
+
]
|
package/src/cli.link.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import fs from "node:fs"
|
|
2
|
+
import os from "node:os"
|
|
3
|
+
import path from "node:path"
|
|
4
|
+
|
|
5
|
+
function findMonorepoRoot(): string | null {
|
|
6
|
+
const scriptDir = import.meta.dirname
|
|
7
|
+
const candidates = [
|
|
8
|
+
path.resolve(scriptDir, "../../.."),
|
|
9
|
+
path.resolve(scriptDir, "../../../.."),
|
|
10
|
+
path.resolve(scriptDir, "../../../../.."),
|
|
11
|
+
]
|
|
12
|
+
for (const candidate of candidates) {
|
|
13
|
+
if (fs.existsSync(path.join(candidate, "packages", "core", "package.json"))) {
|
|
14
|
+
return candidate
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return null
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function ensureLocalLink(packageName: string): boolean {
|
|
21
|
+
const monorepoRoot = findMonorepoRoot()
|
|
22
|
+
if (!monorepoRoot) return false
|
|
23
|
+
|
|
24
|
+
const sourceDir = path.join(monorepoRoot, "packages", packageName)
|
|
25
|
+
if (!fs.existsSync(path.join(sourceDir, "package.json"))) return false
|
|
26
|
+
|
|
27
|
+
const atomserviceDir = path.join(os.homedir(), ".atomservice")
|
|
28
|
+
const targetDir = path.join(atomserviceDir, "node_modules", "@atomservice")
|
|
29
|
+
const symlinkPath = path.join(targetDir, packageName)
|
|
30
|
+
|
|
31
|
+
const existing = fs.lstatSync(symlinkPath, { throwIfNoEntry: false })
|
|
32
|
+
if (existing?.isSymbolicLink() || existing?.isDirectory()) {
|
|
33
|
+
const resolved = fs.realpathSync(symlinkPath)
|
|
34
|
+
if (resolved === sourceDir) return true
|
|
35
|
+
fs.rmSync(symlinkPath, { force: true, recursive: true })
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
fs.mkdirSync(targetDir, { recursive: true })
|
|
39
|
+
fs.symlinkSync(sourceDir, symlinkPath, "dir")
|
|
40
|
+
return true
|
|
41
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import fs from "node:fs"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
import { ATOMSERVICE_DIR } from "@atomservice/core"
|
|
4
|
+
import { spinner } from "@clack/prompts"
|
|
5
|
+
import { defineCommand } from "citty"
|
|
6
|
+
import pc from "picocolors"
|
|
7
|
+
import { CONFIG_PATH } from "../cli.consts.ts"
|
|
8
|
+
import { ensureLocalLink } from "../cli.link.ts"
|
|
9
|
+
|
|
10
|
+
async function ensureDir() {
|
|
11
|
+
fs.mkdirSync(ATOMSERVICE_DIR, { recursive: true })
|
|
12
|
+
|
|
13
|
+
const pkgPath = path.join(ATOMSERVICE_DIR, "package.json")
|
|
14
|
+
if (!fs.existsSync(pkgPath)) {
|
|
15
|
+
await Bun.write(
|
|
16
|
+
pkgPath,
|
|
17
|
+
`${JSON.stringify({ name: "atomservice-config", private: true, type: "module" }, null, 2)}\n`,
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function ensureCore() {
|
|
23
|
+
const symlinkPath = path.join(ATOMSERVICE_DIR, "node_modules", "@atomservice", "core")
|
|
24
|
+
|
|
25
|
+
const existing = fs.lstatSync(symlinkPath, { throwIfNoEntry: false })
|
|
26
|
+
if (existing) {
|
|
27
|
+
try {
|
|
28
|
+
const resolved = fs.realpathSync(symlinkPath)
|
|
29
|
+
if (fs.existsSync(path.join(resolved, "package.json"))) return
|
|
30
|
+
} catch {
|
|
31
|
+
fs.rmSync(symlinkPath, { force: true, recursive: true })
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const s = spinner()
|
|
36
|
+
s.start("安装 @atomservice/core…")
|
|
37
|
+
|
|
38
|
+
const installed = await Bun.$`bun add --cwd ${ATOMSERVICE_DIR} @atomservice/core`.quiet().nothrow()
|
|
39
|
+
if (installed.exitCode === 0) {
|
|
40
|
+
s.stop(pc.green("@atomservice/core 已安装"))
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const linked = ensureLocalLink("core")
|
|
45
|
+
s.stop(linked ? pc.green("@atomservice/core 已就绪(开发模式)") : pc.red("@atomservice/core 安装失败"))
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const initCmd = defineCommand({
|
|
49
|
+
meta: { name: "init", description: `初始化 atomservice 项目(${ATOMSERVICE_DIR})` },
|
|
50
|
+
run: async () => {
|
|
51
|
+
await ensureDir()
|
|
52
|
+
await ensureCore()
|
|
53
|
+
|
|
54
|
+
if (fs.existsSync(CONFIG_PATH)) {
|
|
55
|
+
console.log(pc.dim("已就绪,无需重复初始化"))
|
|
56
|
+
console.log(pc.dim(`配置文件:${CONFIG_PATH}`))
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const content = `import { defineConfig } from "@atomservice/core"
|
|
61
|
+
|
|
62
|
+
export default defineConfig({
|
|
63
|
+
root: "${ATOMSERVICE_DIR}",
|
|
64
|
+
services: [],
|
|
65
|
+
})
|
|
66
|
+
`
|
|
67
|
+
|
|
68
|
+
await Bun.write(CONFIG_PATH, content)
|
|
69
|
+
console.log(`${pc.green("✔")} 已初始化 ${pc.bold(ATOMSERVICE_DIR)}`)
|
|
70
|
+
console.log(pc.dim(" 编辑 atom.config.ts 添加原子服务后运行 atom run up"))
|
|
71
|
+
},
|
|
72
|
+
})
|
package/src/run/index.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { defineCommand } from "citty"
|
|
2
|
+
import { downCmd } from "./run.down.ts"
|
|
3
|
+
import { statusCmd } from "./run.status.ts"
|
|
4
|
+
import { upCmd } from "./run.up.ts"
|
|
5
|
+
|
|
6
|
+
export const runCmd = defineCommand({
|
|
7
|
+
meta: { name: "run", description: "管理原子服务生命周期" },
|
|
8
|
+
subCommands: {
|
|
9
|
+
up: upCmd,
|
|
10
|
+
down: downCmd,
|
|
11
|
+
status: statusCmd,
|
|
12
|
+
},
|
|
13
|
+
})
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { initForCli, loadConfig, selectServices } from "@atomservice/core"
|
|
2
|
+
import { defineCommand } from "citty"
|
|
3
|
+
import pc from "picocolors"
|
|
4
|
+
import { runAtom } from "./run.utils.ts"
|
|
5
|
+
|
|
6
|
+
export const downCmd = defineCommand({
|
|
7
|
+
meta: { name: "down", description: "反序停止原子服务" },
|
|
8
|
+
args: {
|
|
9
|
+
services: {
|
|
10
|
+
type: "positional",
|
|
11
|
+
required: false,
|
|
12
|
+
description: "要停止的服务名(可多个),不填则停止全部",
|
|
13
|
+
},
|
|
14
|
+
only: { type: "boolean", description: "仅停止指定服务,不自动停止其下游", default: false },
|
|
15
|
+
verbose: { type: "boolean", alias: "v", description: "显示详细命令输出", default: false },
|
|
16
|
+
},
|
|
17
|
+
run: async ({ args }) => {
|
|
18
|
+
if (args.verbose) process.env.ATOMSERVICE_VERBOSE = "1"
|
|
19
|
+
const config = await loadConfig()
|
|
20
|
+
let services = await initForCli(config)
|
|
21
|
+
|
|
22
|
+
if (services.length === 0) {
|
|
23
|
+
console.log(pc.dim("没有配置原子服务,请编辑 ~/.atomservice/atom.config.ts"))
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const names = (args._ ?? []).filter((a): a is string => typeof a === "string" && a.length > 0)
|
|
28
|
+
if (names.length > 0) {
|
|
29
|
+
services = selectServices(services, names, { direction: "dependents", only: args.only })
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
for (const { serviceId, state, logger } of [...services].reverse()) {
|
|
33
|
+
await runAtom(serviceId, state, logger, (s) => s.downHandlers)
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
})
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { HealthStatus } from "@atomservice/core"
|
|
2
|
+
import { initForCli, loadConfig } from "@atomservice/core"
|
|
3
|
+
import { defineCommand } from "citty"
|
|
4
|
+
import Table from "cli-table3"
|
|
5
|
+
import logUpdate from "log-update"
|
|
6
|
+
import pc from "picocolors"
|
|
7
|
+
|
|
8
|
+
function renderTable(results: Map<string, HealthStatus>): string {
|
|
9
|
+
const table = new Table({
|
|
10
|
+
head: [pc.bold("原子服务"), pc.bold("状态"), pc.bold("信息")],
|
|
11
|
+
})
|
|
12
|
+
for (const [id, health] of results) {
|
|
13
|
+
const icon =
|
|
14
|
+
health.status === "healthy"
|
|
15
|
+
? pc.green("✓ healthy")
|
|
16
|
+
: health.status === "unhealthy"
|
|
17
|
+
? pc.red("✗ unhealthy")
|
|
18
|
+
: pc.yellow("? unknown")
|
|
19
|
+
table.push([id, icon, health.message ?? ""])
|
|
20
|
+
}
|
|
21
|
+
return table.toString()
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const statusCmd = defineCommand({
|
|
25
|
+
meta: { name: "status", description: "查看所有原子服务健康状态" },
|
|
26
|
+
args: {
|
|
27
|
+
watch: {
|
|
28
|
+
type: "boolean",
|
|
29
|
+
description: "持续监控,每 5 秒刷新",
|
|
30
|
+
default: false,
|
|
31
|
+
alias: "w",
|
|
32
|
+
},
|
|
33
|
+
verbose: { type: "boolean", alias: "v", description: "显示详细命令输出", default: false },
|
|
34
|
+
},
|
|
35
|
+
run: async ({ args }) => {
|
|
36
|
+
if (args.verbose) process.env.ATOMSERVICE_VERBOSE = "1"
|
|
37
|
+
const config = await loadConfig()
|
|
38
|
+
const services = await initForCli(config)
|
|
39
|
+
|
|
40
|
+
if (services.length === 0) {
|
|
41
|
+
console.log(pc.dim("没有配置原子服务,请编辑 ~/.atomservice/atom.config.ts"))
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const refresh = async (): Promise<string> => {
|
|
46
|
+
const results = new Map<string, HealthStatus>()
|
|
47
|
+
await Promise.all(
|
|
48
|
+
services.map(async ({ serviceId, state }) => {
|
|
49
|
+
const status = state.healthHandler ? await state.healthHandler() : ({ status: "unknown" } as const)
|
|
50
|
+
results.set(serviceId, status)
|
|
51
|
+
}),
|
|
52
|
+
)
|
|
53
|
+
return renderTable(results)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (args.watch) {
|
|
57
|
+
logUpdate(await refresh())
|
|
58
|
+
const interval = setInterval(async () => {
|
|
59
|
+
logUpdate(await refresh())
|
|
60
|
+
}, 5000)
|
|
61
|
+
process.on("SIGINT", () => {
|
|
62
|
+
clearInterval(interval)
|
|
63
|
+
logUpdate.done()
|
|
64
|
+
process.exit(0)
|
|
65
|
+
})
|
|
66
|
+
} else {
|
|
67
|
+
console.log(await refresh())
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
})
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { initForCli, loadConfig, selectServices } from "@atomservice/core"
|
|
2
|
+
import { defineCommand } from "citty"
|
|
3
|
+
import pc from "picocolors"
|
|
4
|
+
import { runAtom } from "./run.utils.ts"
|
|
5
|
+
|
|
6
|
+
export const upCmd = defineCommand({
|
|
7
|
+
meta: { name: "up", description: "按依赖顺序启动原子服务" },
|
|
8
|
+
args: {
|
|
9
|
+
services: {
|
|
10
|
+
type: "positional",
|
|
11
|
+
required: false,
|
|
12
|
+
description: "要启动的服务名(可多个),不填则启动全部",
|
|
13
|
+
},
|
|
14
|
+
only: { type: "boolean", description: "仅启动指定服务,不自动启动其依赖", default: false },
|
|
15
|
+
verbose: { type: "boolean", alias: "v", description: "显示详细命令输出", default: false },
|
|
16
|
+
},
|
|
17
|
+
run: async ({ args }) => {
|
|
18
|
+
if (args.verbose) process.env.ATOMSERVICE_VERBOSE = "1"
|
|
19
|
+
const config = await loadConfig()
|
|
20
|
+
let services = await initForCli(config)
|
|
21
|
+
|
|
22
|
+
if (services.length === 0) {
|
|
23
|
+
console.log(pc.dim("没有配置原子服务,请编辑 ~/.atomservice/atom.config.ts"))
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const names = (args._ ?? []).filter((a): a is string => typeof a === "string" && a.length > 0)
|
|
28
|
+
if (names.length > 0) {
|
|
29
|
+
services = selectServices(services, names, { direction: "deps", only: args.only })
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
await Bun.$`podman network create atomservice`.quiet().nothrow()
|
|
33
|
+
|
|
34
|
+
for (const { serviceId, state, logger } of services) {
|
|
35
|
+
await runAtom(serviceId, state, logger, (s) => s.upHandlers)
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
})
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { Logger, ServiceLifecycle } from "@atomservice/core"
|
|
2
|
+
import { UserError } from "@atomservice/core"
|
|
3
|
+
import { spinner } from "@clack/prompts"
|
|
4
|
+
import pc from "picocolors"
|
|
5
|
+
|
|
6
|
+
export function elapsed(ms: number): string {
|
|
7
|
+
return `${(ms / 1000).toFixed(1)}s`
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function runAtom(
|
|
11
|
+
serviceId: string,
|
|
12
|
+
state: ServiceLifecycle,
|
|
13
|
+
logger: Logger,
|
|
14
|
+
handlers: (lifecycle: ServiceLifecycle) => (() => Bun.MaybePromise<void>)[],
|
|
15
|
+
): Promise<void> {
|
|
16
|
+
const verbose = !!process.env.ATOMSERVICE_VERBOSE
|
|
17
|
+
const t0 = Date.now()
|
|
18
|
+
let finalMsg = serviceId
|
|
19
|
+
|
|
20
|
+
if (verbose) {
|
|
21
|
+
const RESET = "\x1b[0m"
|
|
22
|
+
const BOLD = "\x1b[1m"
|
|
23
|
+
const cyan = (s: string) => (Bun.color("#22d3ee", "ansi") ?? "") + s + RESET
|
|
24
|
+
const green = (s: string) => (Bun.color("#4ade80", "ansi") ?? "") + s + RESET
|
|
25
|
+
const blue = (s: string) => (Bun.color("#7dd3fc", "ansi") ?? "") + s + RESET
|
|
26
|
+
const yellow = (s: string) => (Bun.color("#fbbf24", "ansi") ?? "") + s + RESET
|
|
27
|
+
const red = (s: string) => (Bun.color("#f87171", "ansi") ?? "") + s + RESET
|
|
28
|
+
const bar = (s: string) => (Bun.color("#52525b", "ansi") ?? "") + s + RESET
|
|
29
|
+
console.log(`${cyan("◇")} ${BOLD}${serviceId}${RESET}`)
|
|
30
|
+
logger.info = (msg) => {
|
|
31
|
+
console.log(`${bar("│")} ${blue("›")} ${msg}`)
|
|
32
|
+
}
|
|
33
|
+
logger.success = (msg) => {
|
|
34
|
+
finalMsg = msg
|
|
35
|
+
}
|
|
36
|
+
logger.warn = (msg) => {
|
|
37
|
+
console.log(`${bar("│")} ${yellow("⚠")} ${msg}`)
|
|
38
|
+
}
|
|
39
|
+
logger.error = (msg) => {
|
|
40
|
+
console.log(`${bar("│")} ${red("✗")} ${msg}`)
|
|
41
|
+
finalMsg = msg
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
for (const fn of handlers(state)) await fn()
|
|
45
|
+
console.log(`${green("└")} ${green(finalMsg)} ${bar(elapsed(Date.now() - t0))}`)
|
|
46
|
+
console.log()
|
|
47
|
+
} catch (err) {
|
|
48
|
+
console.log(`${red("└")} ${red(serviceId)} ${bar(elapsed(Date.now() - t0))}`)
|
|
49
|
+
console.error(red(` ${err instanceof Error ? err.message : String(err)}`))
|
|
50
|
+
if (err instanceof UserError && err.hint) console.error(bar(` ${err.hint}`))
|
|
51
|
+
process.exit(1)
|
|
52
|
+
}
|
|
53
|
+
} else {
|
|
54
|
+
const s = spinner()
|
|
55
|
+
s.start(serviceId)
|
|
56
|
+
logger.info = (msg) => s.message(msg)
|
|
57
|
+
logger.success = (msg) => {
|
|
58
|
+
finalMsg = msg
|
|
59
|
+
s.message(msg)
|
|
60
|
+
}
|
|
61
|
+
logger.warn = (msg) => s.message(pc.yellow(msg))
|
|
62
|
+
logger.error = (msg) => {
|
|
63
|
+
finalMsg = msg
|
|
64
|
+
s.message(pc.red(msg))
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
for (const fn of handlers(state)) await fn()
|
|
68
|
+
s.stop(`${pc.green(finalMsg)} ${pc.dim(elapsed(Date.now() - t0))}`)
|
|
69
|
+
} catch (err) {
|
|
70
|
+
s.stop(`${pc.red(serviceId)} ${pc.dim(elapsed(Date.now() - t0))}`)
|
|
71
|
+
console.error(pc.red(` ${err instanceof Error ? err.message : String(err)}`))
|
|
72
|
+
if (err instanceof UserError && err.hint) console.error(pc.dim(` ${err.hint}`))
|
|
73
|
+
process.exit(1)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { buildServiceSubCommands } from "./service.cmds.ts"
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { initForCli, loadConfig } from "@atomservice/core"
|
|
2
|
+
import { defineCommand } from "citty"
|
|
3
|
+
|
|
4
|
+
export async function buildServiceSubCommands(): Promise<Record<string, ReturnType<typeof defineCommand>>> {
|
|
5
|
+
let config: Awaited<ReturnType<typeof loadConfig>>
|
|
6
|
+
try {
|
|
7
|
+
config = await loadConfig()
|
|
8
|
+
} catch {
|
|
9
|
+
return {}
|
|
10
|
+
}
|
|
11
|
+
const services = await initForCli(config)
|
|
12
|
+
const subCommands: Record<string, ReturnType<typeof defineCommand>> = {}
|
|
13
|
+
|
|
14
|
+
for (const { serviceId, state } of services) {
|
|
15
|
+
if (!state.commands.length) continue
|
|
16
|
+
|
|
17
|
+
const cmds: Record<string, ReturnType<typeof defineCommand>> = {}
|
|
18
|
+
for (const cmd of state.commands) {
|
|
19
|
+
cmds[cmd.name] = defineCommand({
|
|
20
|
+
meta: { name: cmd.name, description: cmd.description },
|
|
21
|
+
run: async ({ args }) => {
|
|
22
|
+
await cmd.handler(args as Record<string, string>)
|
|
23
|
+
},
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
subCommands[serviceId] = defineCommand({
|
|
28
|
+
meta: { name: serviceId, description: `${serviceId} 原子服务命令` },
|
|
29
|
+
subCommands: cmds,
|
|
30
|
+
})
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return subCommands
|
|
34
|
+
}
|