@atomservice/functions-cli 0.1.6

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.
@@ -0,0 +1,145 @@
1
+ import path from "node:path"
2
+ import { intro, log, outro, select, text } from "@clack/prompts"
3
+ import { defineCommand } from "citty"
4
+ import pc from "picocolors"
5
+ import { ClientError } from "../client.ts"
6
+ import { LOCAL_DIR, MANIFEST_FILE } from "../consts.ts"
7
+ import { authedClient, requireServerContext } from "../context.ts"
8
+ import { openInEditor } from "../editor.ts"
9
+ import type { Manifest } from "../types.ts"
10
+ import { isValidSlug, readJson, unwrap, writeJson } from "../utils.ts"
11
+ import { writeView } from "../views.ts"
12
+
13
+ function intentFile(ctx: { root: string }, slug: string, action: string): string {
14
+ return path.join(ctx.root, LOCAL_DIR, "project", `${slug}.${action}.json`)
15
+ }
16
+
17
+ async function askSlug(provided: string | undefined): Promise<string> {
18
+ if (provided) {
19
+ if (!isValidSlug(provided)) throw new Error("项目标识需以小写字母开头,仅含小写字母、数字、连字符")
20
+ return provided
21
+ }
22
+ return unwrap(
23
+ await text({
24
+ message: "项目标识",
25
+ validate: (v) =>
26
+ v?.trim() && isValidSlug(v.trim()) ? undefined : "需以小写字母开头,仅含小写字母、数字、连字符",
27
+ }),
28
+ ).trim()
29
+ }
30
+
31
+ const listCommand = defineCommand({
32
+ meta: { name: "list", description: "查看所有项目" },
33
+ async run() {
34
+ intro(pc.cyan("项目列表"))
35
+ const ctx = await requireServerContext()
36
+ const client = await authedClient(ctx)
37
+ try {
38
+ const data = await client.get<{ projects: unknown[] }>("/projects")
39
+ const file = await writeView(ctx, "projects.list.json", data)
40
+ outro(pc.green(`已生成 ${file}`))
41
+ } catch (err) {
42
+ if (err instanceof ClientError) {
43
+ outro(pc.red(err.message))
44
+ process.exit(1)
45
+ }
46
+ throw err
47
+ }
48
+ },
49
+ })
50
+
51
+ const createCommand = defineCommand({
52
+ meta: { name: "create", description: "新建项目意图(仅超管,push 生效)" },
53
+ args: {
54
+ slug: { type: "positional", required: false, description: "项目标识" },
55
+ name: { type: "string", description: "项目名称" },
56
+ description: { type: "string", description: "项目描述" },
57
+ },
58
+ async run({ args }) {
59
+ intro(pc.cyan("新建项目"))
60
+ const ctx = await requireServerContext()
61
+ const slug = await askSlug(args.slug)
62
+ const name =
63
+ args.name ??
64
+ unwrap(
65
+ await text({
66
+ message: "项目名称",
67
+ initialValue: slug,
68
+ validate: (v) => (v?.trim() ? undefined : "请输入名称"),
69
+ }),
70
+ ).trim()
71
+ const payload: Record<string, unknown> = { slug, name }
72
+ if (args.description) payload.description = args.description
73
+
74
+ const file = intentFile(ctx, slug, "create")
75
+ await writeJson(file, payload)
76
+ openInEditor(file)
77
+ outro(pc.green(`已生成意图 ${path.relative(ctx.root, file)},编辑后执行 atomfunctions push`))
78
+ },
79
+ })
80
+
81
+ const linkCommand = defineCommand({
82
+ meta: { name: "link", description: "绑定当前目录到指定项目" },
83
+ args: { slug: { type: "positional", required: false, description: "项目标识" } },
84
+ async run({ args }) {
85
+ intro(pc.cyan("绑定项目"))
86
+ const ctx = await requireServerContext()
87
+ const client = await authedClient(ctx)
88
+
89
+ let slug: string
90
+ if (args.slug) {
91
+ if (!isValidSlug(args.slug)) {
92
+ log.error("项目标识需以小写字母开头,仅含小写字母、数字、连字符")
93
+ process.exit(1)
94
+ }
95
+ slug = args.slug
96
+ } else {
97
+ let projects: { slug: string; name: string }[] = []
98
+ try {
99
+ const data = await client.get<{ projects: { slug: string; name: string }[] }>("/projects")
100
+ projects = data.projects
101
+ } catch {}
102
+ if (projects.length > 0) {
103
+ slug = unwrap(
104
+ await select({
105
+ message: "选择项目",
106
+ options: projects.map((p) => ({ value: p.slug, label: p.slug, hint: p.name })),
107
+ }),
108
+ ) as string
109
+ } else {
110
+ slug = unwrap(
111
+ await text({
112
+ message: "项目标识",
113
+ validate: (v) =>
114
+ v?.trim() && isValidSlug(v.trim()) ? undefined : "需以小写字母开头,仅含小写字母、数字、连字符",
115
+ }),
116
+ ).trim()
117
+ }
118
+ }
119
+
120
+ try {
121
+ await client.get(`/projects/${slug}`)
122
+ } catch (err) {
123
+ if (err instanceof ClientError && err.status === 404) {
124
+ log.error(`项目 "${slug}" 在服务器上不存在`)
125
+ process.exit(1)
126
+ }
127
+ throw err
128
+ }
129
+
130
+ const manifestPath = path.join(ctx.root, MANIFEST_FILE)
131
+ const manifest = await readJson<Manifest>(manifestPath)
132
+ if (!manifest) {
133
+ log.error("读取 atomfunctions.json 失败")
134
+ process.exit(1)
135
+ }
136
+ manifest.project = { slug }
137
+ await writeJson(manifestPath, manifest)
138
+ outro(pc.green(`已绑定项目 ${slug}`))
139
+ },
140
+ })
141
+
142
+ export const projectCommand = defineCommand({
143
+ meta: { name: "project", description: "管理项目" },
144
+ subCommands: { list: listCommand, create: createCommand, link: linkCommand },
145
+ })
@@ -0,0 +1,73 @@
1
+ import { rmSync } from "node:fs"
2
+ import path from "node:path"
3
+ import { intro, outro, spinner } from "@clack/prompts"
4
+ import { defineCommand } from "citty"
5
+ import pc from "picocolors"
6
+ import type { AdminClient } from "../client.ts"
7
+ import { ClientError } from "../client.ts"
8
+ import { REMOTE_DIR } from "../consts.ts"
9
+ import { authedClient, requireProjectContext } from "../context.ts"
10
+ import type { CliContext } from "../types.ts"
11
+ import { ensureDir, writeJson } from "../utils.ts"
12
+
13
+ interface ProjectDto {
14
+ id: string
15
+ slug: string
16
+ name: string
17
+ description: string | null
18
+ createdAt: number
19
+ updatedAt: number
20
+ }
21
+
22
+ interface BundleRecord {
23
+ project: string
24
+ bundle: string
25
+ version: string
26
+ functions: string[]
27
+ updatedAt: string
28
+ }
29
+
30
+ export async function runPull(
31
+ ctx: CliContext & { manifest: { project: { slug: string } } },
32
+ client: AdminClient,
33
+ ): Promise<{ bundles: number }> {
34
+ const project = ctx.manifest.project.slug
35
+ const remoteDir = path.join(ctx.root, REMOTE_DIR)
36
+
37
+ rmSync(path.join(remoteDir, "bundles"), { recursive: true, force: true })
38
+ ensureDir(path.join(remoteDir, "bundles"))
39
+
40
+ const { project: projectDto } = await client.get<{ project: ProjectDto }>(`/projects/${project}`)
41
+ await writeJson(path.join(remoteDir, "project.json"), projectDto)
42
+
43
+ const { bundles } = await client.get<{ bundles: BundleRecord[] }>(`/projects/${project}/bundles`)
44
+ for (const bundle of bundles) {
45
+ await writeJson(path.join(remoteDir, "bundles", `${bundle.bundle}.json`), bundle)
46
+ }
47
+
48
+ return { bundles: bundles.length }
49
+ }
50
+
51
+ export const pullCommand = defineCommand({
52
+ meta: { name: "pull", description: "拉取服务器最新状态到 remote/" },
53
+ async run() {
54
+ intro(pc.cyan("拉取最新状态"))
55
+ const ctx = await requireProjectContext()
56
+ const client = await authedClient(ctx)
57
+ const spin = spinner()
58
+ spin.start("同步中")
59
+ try {
60
+ const stats = await runPull(ctx as Parameters<typeof runPull>[0], client)
61
+ spin.stop(pc.green(`已同步:${stats.bundles} 函数包`))
62
+ outro("remote/ 已刷新")
63
+ } catch (err) {
64
+ spin.stop(pc.red("拉取失败"))
65
+ if (err instanceof ClientError) {
66
+ outro(pc.red(err.message))
67
+ } else {
68
+ outro(pc.red(String(err)))
69
+ }
70
+ process.exit(1)
71
+ }
72
+ },
73
+ })
@@ -0,0 +1,136 @@
1
+ import { readdirSync } from "node:fs"
2
+ import { rm } from "node:fs/promises"
3
+ import path from "node:path"
4
+ import { confirm, intro, isCancel, log, outro, spinner } from "@clack/prompts"
5
+ import { defineCommand } from "citty"
6
+ import pc from "picocolors"
7
+ import { ClientError } from "../client.ts"
8
+ import { LOCAL_DIR } from "../consts.ts"
9
+ import { authedClient, requireServerContext } from "../context.ts"
10
+ import { readJson } from "../utils.ts"
11
+
12
+ type IntentAction = "create" | "update" | "delete"
13
+ type IntentKind = "project"
14
+
15
+ interface Intent {
16
+ kind: IntentKind
17
+ action: IntentAction
18
+ slug: string
19
+ file: string
20
+ payload: Record<string, unknown>
21
+ }
22
+
23
+ function parseName(file: string): { action: IntentAction } | null {
24
+ const match = file.match(/^.+\.(create|update|delete)\.json$/)
25
+ if (!match?.[1]) return null
26
+ return { action: match[1] as IntentAction }
27
+ }
28
+
29
+ async function scanIntents(root: string): Promise<Intent[]> {
30
+ const intents: Intent[] = []
31
+ const dir = path.join(root, LOCAL_DIR, "project")
32
+ let entries: string[]
33
+ try {
34
+ entries = readdirSync(dir)
35
+ } catch {
36
+ return []
37
+ }
38
+ for (const file of entries) {
39
+ const parsed = parseName(file)
40
+ if (!parsed) continue
41
+ const filePath = path.join(dir, file)
42
+ const payload = (await readJson<Record<string, unknown>>(filePath)) ?? {}
43
+ const slug = typeof payload.slug === "string" ? payload.slug : null
44
+ if (!slug) throw new Error(`意图文件 ${file} 缺少 slug 字段`)
45
+ intents.push({ kind: "project", action: parsed.action, slug, file: filePath, payload })
46
+ }
47
+ // create → update → delete
48
+ const rank = (i: Intent) => ["create", "update", "delete"].indexOf(i.action)
49
+ return intents.sort((a, b) => rank(a) - rank(b))
50
+ }
51
+
52
+ function cleanPayload(payload: Record<string, unknown>): Record<string, unknown> {
53
+ const out: Record<string, unknown> = {}
54
+ for (const [key, value] of Object.entries(payload)) {
55
+ if (key === "$schema" || key === "slug") continue
56
+ out[key] = value
57
+ }
58
+ return out
59
+ }
60
+
61
+ const actionLabel: Record<IntentAction, string> = {
62
+ create: pc.green("新增"),
63
+ update: pc.blue("更新"),
64
+ delete: pc.red("删除"),
65
+ }
66
+
67
+ const kindLabel: Record<IntentKind, string> = { project: "项目" }
68
+
69
+ export const pushCommand = defineCommand({
70
+ meta: { name: "push", description: "将本地意图应用到服务器" },
71
+ async run() {
72
+ intro(pc.cyan("推送变更"))
73
+ const ctx = await requireServerContext()
74
+
75
+ let intents: Intent[]
76
+ try {
77
+ intents = await scanIntents(ctx.root)
78
+ } catch (err) {
79
+ outro(pc.red(String(err)))
80
+ process.exit(1)
81
+ }
82
+
83
+ if (intents.length === 0) {
84
+ outro("没有待推送的意图")
85
+ return
86
+ }
87
+
88
+ console.log("")
89
+ console.log(pc.bold("待推送变更:"))
90
+ for (const intent of intents) {
91
+ console.log(` ${actionLabel[intent.action]} ${kindLabel[intent.kind]} ${pc.bold(intent.slug)}`)
92
+ }
93
+ console.log("")
94
+
95
+ const confirmed = await confirm({ message: `确认推送以上 ${intents.length} 项变更?` })
96
+ if (isCancel(confirmed) || !confirmed) {
97
+ outro(pc.yellow("已取消"))
98
+ return
99
+ }
100
+
101
+ const client = await authedClient(ctx)
102
+ let applied = 0
103
+ let failed = 0
104
+
105
+ for (const intent of intents) {
106
+ const spin = spinner()
107
+ spin.start(`${intent.action} ${intent.kind} ${intent.slug}`)
108
+ try {
109
+ const body = cleanPayload(intent.payload)
110
+ if (intent.kind === "project") {
111
+ if (intent.action === "create") {
112
+ await client.post("/projects", { slug: intent.slug, ...body })
113
+ } else if (intent.action === "update") {
114
+ await client.post(`/projects/${intent.slug}`, body)
115
+ } else if (intent.action === "delete") {
116
+ await client.del(`/projects/${intent.slug}`)
117
+ }
118
+ }
119
+ await rm(intent.file, { force: true })
120
+ spin.stop(pc.green(`✓ ${actionLabel[intent.action]} ${kindLabel[intent.kind]} ${intent.slug}`))
121
+ applied += 1
122
+ } catch (err) {
123
+ failed += 1
124
+ if (err instanceof ClientError) {
125
+ spin.stop(pc.red(`✗ ${intent.kind} ${intent.slug}:${err.message}`))
126
+ } else {
127
+ spin.stop(pc.red(`✗ ${intent.kind} ${intent.slug}`))
128
+ log.error(String(err))
129
+ }
130
+ }
131
+ }
132
+
133
+ outro(failed === 0 ? pc.green(`完成:应用 ${applied} 项`) : pc.yellow(`应用 ${applied} 项,失败 ${failed} 项`))
134
+ if (failed > 0) process.exit(1)
135
+ },
136
+ })
@@ -0,0 +1,56 @@
1
+ import { intro, log, outro, select } from "@clack/prompts"
2
+ import { defineCommand } from "citty"
3
+ import pc from "picocolors"
4
+ import { AdminClient } from "../client.ts"
5
+ import { pickTarget, requireProjectContext } from "../context.ts"
6
+ import { unwrap } from "../utils.ts"
7
+
8
+ export const rollbackCommand = defineCommand({
9
+ meta: { name: "rollback", description: "回滚函数包到历史版本(扇出到所有机器)" },
10
+ args: {
11
+ bundle: { type: "positional", description: "bundle 名", required: true },
12
+ version: { type: "string", description: "目标版本(不填则从历史中选择)" },
13
+ },
14
+ async run({ args }) {
15
+ intro(pc.cyan(`回滚 ${args.bundle}`))
16
+ const ctx = await requireProjectContext()
17
+
18
+ const primary = pickTarget(ctx)
19
+ const history = await new AdminClient(primary.server, primary.token).versions(ctx.project, args.bundle)
20
+
21
+ let version = args.version
22
+ if (!version) {
23
+ version = unwrap(
24
+ await select({
25
+ message: "选择回滚到的版本",
26
+ options: history.map((record, index) => ({
27
+ value: record.version,
28
+ label: `${record.version}${index === 0 ? pc.dim(" (当前)") : ""}`,
29
+ hint: record.updatedAt,
30
+ })),
31
+ }),
32
+ ) as string
33
+ }
34
+
35
+ const results = await Promise.all(
36
+ ctx.targets.map(async (t) => {
37
+ try {
38
+ await new AdminClient(t.server, t.token).rollback(ctx.project, args.bundle, version as string)
39
+ return { host: t.host, ok: true }
40
+ } catch (error) {
41
+ return { host: t.host, ok: false, message: error instanceof Error ? error.message : String(error) }
42
+ }
43
+ }),
44
+ )
45
+
46
+ const failed = results.filter((r) => !r.ok)
47
+ if (failed.length === 0) {
48
+ log.success(`已在 ${ctx.targets.length} 台机器回滚到 ${version}`)
49
+ outro(pc.green("完成"))
50
+ } else {
51
+ for (const f of failed) log.error(` ${f.host}: ${f.message}`)
52
+ outro(pc.red("部分机器回滚失败"))
53
+ process.exitCode = 1
54
+ }
55
+ },
56
+ })
@@ -0,0 +1,130 @@
1
+ import { intro, log, outro, select, text } from "@clack/prompts"
2
+ import { defineCommand } from "citty"
3
+ import pc from "picocolors"
4
+ import { ClientError } from "../client.ts"
5
+ import { authedClient, requireProjectContext } from "../context.ts"
6
+ import { unwrap } from "../utils.ts"
7
+ import { writeView } from "../views.ts"
8
+
9
+ interface TokenInfo {
10
+ id: string
11
+ name: string
12
+ project: string
13
+ tokenPrefix: string
14
+ revoked: boolean
15
+ createdAt: number
16
+ }
17
+
18
+ function fmtDate(ts: number): string {
19
+ const d = new Date(ts * 1000)
20
+ return `${d.getFullYear()}/${String(d.getMonth() + 1).padStart(2, "0")}/${String(d.getDate()).padStart(2, "0")}`
21
+ }
22
+
23
+ const listCommand = defineCommand({
24
+ meta: { name: "list", description: "查看当前项目的访问 token" },
25
+ async run() {
26
+ intro(pc.cyan("token 列表"))
27
+ const ctx = await requireProjectContext()
28
+ const client = await authedClient(ctx)
29
+ try {
30
+ const data = await client.get<{ tokens: unknown[] }>(`/projects/${ctx.project}/tokens`)
31
+ const file = await writeView(ctx, "tokens.list.json", data)
32
+ outro(pc.green(`已生成 ${file}`))
33
+ } catch (err) {
34
+ if (err instanceof ClientError) {
35
+ outro(pc.red(err.message))
36
+ process.exit(1)
37
+ }
38
+ throw err
39
+ }
40
+ },
41
+ })
42
+
43
+ const createCommand = defineCommand({
44
+ meta: { name: "create", description: "创建当前项目的访问 token(派发给客户端调用函数)" },
45
+ args: {
46
+ name: { type: "string", description: "令牌备注" },
47
+ },
48
+ async run({ args }) {
49
+ intro(pc.cyan("创建访问 token"))
50
+ const ctx = await requireProjectContext()
51
+ const client = await authedClient(ctx)
52
+ const name = args.name ?? unwrap(await text({ message: "令牌备注(可选)", placeholder: "ci" })).trim()
53
+ try {
54
+ const res = await client.post<{ token: string }>(`/projects/${ctx.project}/tokens`, { name: name || undefined })
55
+ log.warn("以下令牌仅显示一次,请妥善保存:")
56
+ log.message(pc.bold(res.token))
57
+ outro(pc.green("已创建访问 token"))
58
+ } catch (err) {
59
+ if (err instanceof ClientError) {
60
+ outro(pc.red(err.message))
61
+ process.exit(1)
62
+ }
63
+ throw err
64
+ }
65
+ },
66
+ })
67
+
68
+ const revokeCommand = defineCommand({
69
+ meta: { name: "revoke", description: "吊销当前项目的访问 token" },
70
+ args: {
71
+ id: { type: "positional", required: false, description: "token id" },
72
+ },
73
+ async run({ args }) {
74
+ intro(pc.cyan("吊销 token"))
75
+ const ctx = await requireProjectContext()
76
+ const client = await authedClient(ctx)
77
+
78
+ let id: string
79
+ if (args.id) {
80
+ id = args.id
81
+ } else {
82
+ let tokens: TokenInfo[] = []
83
+ try {
84
+ const data = await client.get<{ tokens: TokenInfo[] }>(`/projects/${ctx.project}/tokens`)
85
+ tokens = data.tokens
86
+ } catch (err) {
87
+ if (err instanceof ClientError) {
88
+ outro(pc.red(err.message))
89
+ process.exit(1)
90
+ }
91
+ throw err
92
+ }
93
+ if (tokens.length === 0) {
94
+ outro("当前项目暂无 token")
95
+ return
96
+ }
97
+ const active = tokens.filter((t) => !t.revoked)
98
+ if (active.length === 0) {
99
+ outro("当前项目没有有效的 token")
100
+ return
101
+ }
102
+ id = unwrap(
103
+ await select({
104
+ message: "选择要吊销的 token",
105
+ options: active.map((t) => ({
106
+ value: t.id,
107
+ label: `${t.name || "—"} · ${t.tokenPrefix}`,
108
+ hint: fmtDate(t.createdAt),
109
+ })),
110
+ }),
111
+ ) as string
112
+ }
113
+
114
+ try {
115
+ await client.del(`/projects/${ctx.project}/tokens/${id}`)
116
+ outro(pc.green("已吊销"))
117
+ } catch (err) {
118
+ if (err instanceof ClientError) {
119
+ outro(pc.red(err.message))
120
+ process.exit(1)
121
+ }
122
+ throw err
123
+ }
124
+ },
125
+ })
126
+
127
+ export const tokenCommand = defineCommand({
128
+ meta: { name: "token", description: "管理客户端访问 token" },
129
+ subCommands: { list: listCommand, create: createCommand, revoke: revokeCommand },
130
+ })
@@ -0,0 +1,77 @@
1
+ import { intro, outro, password as passwordPrompt, text } from "@clack/prompts"
2
+ import { defineCommand } from "citty"
3
+ import pc from "picocolors"
4
+ import { ClientError } from "../client.ts"
5
+ import { authedClient, requireServerContext } from "../context.ts"
6
+ import { unwrap } from "../utils.ts"
7
+ import { writeView } from "../views.ts"
8
+
9
+ const listCommand = defineCommand({
10
+ meta: { name: "list", description: "查看所有用户" },
11
+ async run() {
12
+ intro(pc.cyan("用户列表"))
13
+ const ctx = await requireServerContext()
14
+ const client = await authedClient(ctx)
15
+ try {
16
+ const data = await client.get<{ users: unknown[] }>("/users")
17
+ const file = await writeView(ctx, "users.list.json", data)
18
+ outro(pc.green(`已生成 ${file}`))
19
+ } catch (err) {
20
+ if (err instanceof ClientError) {
21
+ outro(pc.red(err.message))
22
+ process.exit(1)
23
+ }
24
+ throw err
25
+ }
26
+ },
27
+ })
28
+
29
+ const createCommand = defineCommand({
30
+ meta: { name: "create", description: "创建用户(仅超管)" },
31
+ args: {
32
+ email: { type: "string" },
33
+ name: { type: "string" },
34
+ password: { type: "string" },
35
+ super: { type: "boolean", description: "授予超级管理员", default: false },
36
+ },
37
+ async run({ args }) {
38
+ intro(pc.cyan("创建用户"))
39
+ const ctx = await requireServerContext()
40
+ const client = await authedClient(ctx)
41
+ const email =
42
+ args.email ??
43
+ unwrap(await text({ message: "邮箱", validate: (v) => (v?.trim() ? undefined : "请输入邮箱") })).trim()
44
+ const name =
45
+ args.name ??
46
+ unwrap(await text({ message: "用户名", validate: (v) => (v?.trim() ? undefined : "请输入用户名") })).trim()
47
+ const secret =
48
+ args.password ??
49
+ unwrap(
50
+ await passwordPrompt({
51
+ message: "初始密码(至少 8 位)",
52
+ validate: (v) => (v && v.length >= 8 ? undefined : "至少 8 位"),
53
+ }),
54
+ )
55
+ const isSuperAdmin = args.super
56
+ try {
57
+ const res = await client.post<{ user: { name: string } }>("/users", {
58
+ email,
59
+ name,
60
+ password: secret,
61
+ isSuperAdmin,
62
+ })
63
+ outro(pc.green(`已创建用户 ${res.user.name}`))
64
+ } catch (err) {
65
+ if (err instanceof ClientError) {
66
+ outro(pc.red(err.message))
67
+ process.exit(1)
68
+ }
69
+ throw err
70
+ }
71
+ },
72
+ })
73
+
74
+ export const userCommand = defineCommand({
75
+ meta: { name: "user", description: "管理用户" },
76
+ subCommands: { list: listCommand, create: createCommand },
77
+ })
@@ -0,0 +1,51 @@
1
+ import { intro, log, outro } from "@clack/prompts"
2
+ import { defineCommand } from "citty"
3
+ import pc from "picocolors"
4
+ import { AdminClient } from "../client.ts"
5
+ import { loadContext } from "../context.ts"
6
+ import { getCredential } from "../creds.ts"
7
+ import { hostOf } from "../utils.ts"
8
+
9
+ interface WhoamiUser {
10
+ id: string
11
+ email: string
12
+ name: string
13
+ isSuperAdmin: boolean
14
+ createdAt: string
15
+ }
16
+
17
+ export const whoamiCommand = defineCommand({
18
+ meta: { name: "whoami", description: "查看当前登录账号信息" },
19
+ args: {
20
+ server: { type: "string", description: "服务器地址(默认读取 atomfunctions.json)" },
21
+ },
22
+ async run({ args }) {
23
+ intro(pc.cyan("atomfunctions whoami"))
24
+ const ctx = await loadContext()
25
+ if (!ctx) process.exit(1)
26
+ const server = args.server ?? ctx.manifest.server
27
+ if (!server) {
28
+ log.error("无法确定服务器地址")
29
+ process.exit(1)
30
+ }
31
+
32
+ const host = hostOf(server)
33
+ const cred = await getCredential(host)
34
+ if (!cred) {
35
+ log.error(`尚未登录 ${host}`)
36
+ process.exit(1)
37
+ }
38
+
39
+ const client = new AdminClient(server, cred.token)
40
+ const res = await client.get<{ user: WhoamiUser }>("/auth/whoami")
41
+ const user = res.user
42
+
43
+ log.info(`服务器:${pc.cyan(host)}`)
44
+ log.info(`账号: ${pc.bold(user.name)} (${user.email})`)
45
+ if (user.isSuperAdmin) {
46
+ log.info(`角色: ${pc.yellow("超级管理员")}`)
47
+ }
48
+
49
+ outro(pc.green("已登录"))
50
+ },
51
+ })