@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,126 @@
1
+ import type { Dirent } from "node:fs"
2
+ import { readdirSync } from "node:fs"
3
+ import path from "node:path"
4
+ import { confirm, intro, isCancel, log, outro, select, text } from "@clack/prompts"
5
+ import { defineCommand } from "citty"
6
+ import pc from "picocolors"
7
+ import { BUNDLE_FILE, BUNDLES_DIR } from "../consts.ts"
8
+ import { loadContext } from "../context.ts"
9
+ import { scaffoldFunction } from "../scaffold.ts"
10
+ import type { BundleConfig, FunctionKind } from "../types.ts"
11
+ import { isValidSlug, readJson, unwrap } from "../utils.ts"
12
+
13
+ async function listBundles(root: string): Promise<string[]> {
14
+ const dir = path.join(root, BUNDLES_DIR)
15
+ let entries: Dirent[]
16
+ try {
17
+ entries = readdirSync(dir, { withFileTypes: true }) as unknown as Dirent[]
18
+ } catch {
19
+ return []
20
+ }
21
+ return entries
22
+ .filter((e) => e.isDirectory() && !e.name.startsWith("."))
23
+ .filter((e) => {
24
+ try {
25
+ return Bun.file(path.join(dir, e.name, BUNDLE_FILE)).size > 0
26
+ } catch {
27
+ return false
28
+ }
29
+ })
30
+ .map((e) => e.name)
31
+ .sort()
32
+ }
33
+
34
+ const KIND_OPTIONS: { value: FunctionKind; label: string; hint: string }[] = [
35
+ { value: "function", label: "function", hint: "同步请求-响应" },
36
+ { value: "streamFunction", label: "streamFunction", hint: "同步流式" },
37
+ { value: "asyncFunction", label: "asyncFunction", hint: "异步入队,请求-响应" },
38
+ { value: "asyncStreamFunction", label: "asyncStreamFunction", hint: "异步入队,流式" },
39
+ ]
40
+
41
+ const createCommand = defineCommand({
42
+ meta: { name: "create", description: "在函数包下创建函数" },
43
+ args: {
44
+ slug: { type: "positional", required: false, description: "函数名" },
45
+ bundle: { type: "string", description: "指定 bundle 名(跳过交互选择)" },
46
+ kind: { type: "string", description: "函数类型:function / streamFunction / asyncFunction / asyncStreamFunction" },
47
+ },
48
+ async run({ args }) {
49
+ intro(pc.cyan("创建函数"))
50
+ const ctx = await loadContext()
51
+ if (!ctx) {
52
+ log.error("未找到 atomfunctions.json")
53
+ process.exit(1)
54
+ }
55
+
56
+ let bundle: string
57
+ if (typeof args.bundle === "string") {
58
+ bundle = args.bundle
59
+ const bundleDir = path.join(ctx.root, BUNDLES_DIR, bundle)
60
+ const cfg = await readJson<BundleConfig>(path.join(bundleDir, BUNDLE_FILE))
61
+ if (!cfg) {
62
+ log.error(`bundle ${bundle} 不存在或不是有效的函数包`)
63
+ process.exit(1)
64
+ }
65
+ } else {
66
+ const bundles = await listBundles(ctx.root)
67
+ if (bundles.length === 0) {
68
+ log.error("尚未创建任何 bundle,请先运行 atomfn bundle create")
69
+ process.exit(1)
70
+ }
71
+ bundle = unwrap(
72
+ await select({
73
+ message: "选择函数包",
74
+ options: bundles.map((b) => ({ value: b, label: b })),
75
+ }),
76
+ ) as string
77
+ }
78
+
79
+ const slug =
80
+ args.slug ??
81
+ unwrap(
82
+ await text({
83
+ message: "函数名",
84
+ validate: (v) => {
85
+ const s = v?.trim()
86
+ if (!s) return "请输入函数名"
87
+ return isValidSlug(s) ? undefined : "需以小写字母开头,仅含小写字母、数字、连字符"
88
+ },
89
+ }),
90
+ ).trim()
91
+
92
+ if (!isValidSlug(slug)) {
93
+ log.error("函数名需以小写字母开头,仅含小写字母、数字、连字符")
94
+ process.exit(1)
95
+ }
96
+
97
+ const kind: FunctionKind =
98
+ typeof args.kind === "string"
99
+ ? (args.kind as FunctionKind)
100
+ : (unwrap(
101
+ await select({
102
+ message: "选择函数类型",
103
+ options: KIND_OPTIONS,
104
+ initialValue: "function",
105
+ }),
106
+ ) as FunctionKind)
107
+
108
+ const bundleDir = path.join(ctx.root, BUNDLES_DIR, bundle)
109
+ const fnDir = path.join(bundleDir, slug)
110
+ if (await Bun.file(path.join(fnDir, "function.ts")).exists()) {
111
+ const ok = await confirm({ message: `函数 ${bundle}/${slug} 已存在,是否覆盖?`, initialValue: false })
112
+ if (isCancel(ok) || !ok) {
113
+ outro(pc.yellow("已取消"))
114
+ return
115
+ }
116
+ }
117
+ await scaffoldFunction(bundleDir, slug, "bun", kind)
118
+ log.success(`已创建${KIND_OPTIONS.find((k) => k.value === kind)?.label}函数 ${bundle}/${slug}`)
119
+ outro(pc.green("完成,运行 atomfunctions deploy 进行部署"))
120
+ },
121
+ })
122
+
123
+ export const functionCommand = defineCommand({
124
+ meta: { name: "function", description: "管理函数" },
125
+ subCommands: { create: createCommand },
126
+ })
@@ -0,0 +1,122 @@
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 { AdminClient } from "../client.ts"
6
+ import {
7
+ GITIGNORE_BODY,
8
+ LOCAL_DIR,
9
+ MANIFEST_FILE,
10
+ MANIFEST_SCHEMA,
11
+ MANIFEST_SCHEMA_FILE,
12
+ REMOTE_DIR,
13
+ SCHEMAS_DIR,
14
+ VIEWS_DIR,
15
+ } from "../consts.ts"
16
+ import { getCredential } from "../creds.ts"
17
+ import type { Manifest } from "../types.ts"
18
+ import { ensureDir, hostOf, isValidSlug, readJson, unwrap, writeJson } from "../utils.ts"
19
+
20
+ export const initCommand = defineCommand({
21
+ meta: { name: "init", description: "在当前目录初始化函数项目" },
22
+ args: {
23
+ server: { type: "string", description: "控制面服务器地址" },
24
+ project: { type: "string", description: "项目标识 slug(可选)" },
25
+ },
26
+ async run({ args }) {
27
+ intro(pc.cyan("初始化函数项目"))
28
+ const root = process.cwd()
29
+ const existing = await readJson<Manifest>(path.join(root, MANIFEST_FILE))
30
+
31
+ const server = (
32
+ args.server ??
33
+ unwrap(
34
+ await text({
35
+ message: "控制面服务器地址",
36
+ initialValue: existing?.server ?? "",
37
+ placeholder: "https://fn.example.com",
38
+ validate: (v) => {
39
+ const s = v?.trim()
40
+ if (!s) return "请输入服务器地址"
41
+ try {
42
+ new URL(s)
43
+ } catch {
44
+ return "请输入合法的 URL"
45
+ }
46
+ },
47
+ }),
48
+ )
49
+ ).trim()
50
+
51
+ if (args.server) {
52
+ try {
53
+ new URL(server)
54
+ } catch {
55
+ log.error("--server 不是合法的 URL")
56
+ process.exit(1)
57
+ }
58
+ }
59
+
60
+ let slug: string | undefined
61
+
62
+ if (args.project) {
63
+ if (!isValidSlug(args.project)) {
64
+ log.error("项目标识需以小写字母开头,仅含小写字母、数字、连字符")
65
+ process.exit(1)
66
+ }
67
+ slug = args.project
68
+ } else {
69
+ const host = hostOf(server)
70
+ const cred = await getCredential(host)
71
+ if (cred) {
72
+ const client = new AdminClient(server, cred.token)
73
+ try {
74
+ const { projects } = await client.get<{ projects: { slug: string; name: string }[] }>("/projects")
75
+ if (projects.length > 0) {
76
+ const options = [
77
+ ...projects.map((p) => ({ value: p.slug, label: p.slug, hint: p.name })),
78
+ { value: "__skip__", label: "暂不绑定项目" },
79
+ ]
80
+ const chosen = unwrap(await select({ message: "绑定项目", options })) as string
81
+ if (chosen !== "__skip__") slug = chosen
82
+ } else {
83
+ log.info("服务器上暂无项目可供绑定")
84
+ }
85
+ } catch {
86
+ log.warn("无法获取项目列表,跳过项目绑定")
87
+ }
88
+ } else {
89
+ const input = unwrap(
90
+ await text({
91
+ message: "项目标识(尚未创建项目可留空)",
92
+ initialValue: existing?.project?.slug ?? "",
93
+ placeholder: "留空",
94
+ validate: (v) => {
95
+ const s = v?.trim()
96
+ if (!s) return undefined
97
+ return isValidSlug(s) ? undefined : "需以小写字母开头,仅含小写字母、数字、连字符"
98
+ },
99
+ }),
100
+ ).trim()
101
+ if (input) slug = input
102
+ }
103
+ }
104
+
105
+ const manifest: Manifest = {
106
+ $schema: `./${SCHEMAS_DIR}/${MANIFEST_SCHEMA_FILE}`,
107
+ server,
108
+ ...(slug ? { project: { slug } } : {}),
109
+ }
110
+ await writeJson(path.join(root, MANIFEST_FILE), manifest)
111
+ ensureDir(path.join(root, SCHEMAS_DIR))
112
+ await Bun.write(path.join(root, SCHEMAS_DIR, MANIFEST_SCHEMA_FILE), MANIFEST_SCHEMA)
113
+ await Bun.write(path.join(root, ".gitignore"), GITIGNORE_BODY)
114
+ for (const sub of [`${LOCAL_DIR}/project`, REMOTE_DIR, VIEWS_DIR]) {
115
+ ensureDir(path.join(root, sub))
116
+ }
117
+
118
+ outro(
119
+ slug ? pc.green(`完成,已绑定项目 ${slug}`) : pc.green("完成,可通过 atomfunctions project link <slug> 绑定项目"),
120
+ )
121
+ },
122
+ })
@@ -0,0 +1,40 @@
1
+ import { intro, log, outro } from "@clack/prompts"
2
+ import { defineCommand } from "citty"
3
+ import pc from "picocolors"
4
+ import { pickTarget, requireProjectContext } from "../context.ts"
5
+ import { parseRef } from "../utils.ts"
6
+
7
+ export const invokeCommand = defineCommand({
8
+ meta: { name: "invoke", description: "调用函数(调试用)" },
9
+ args: {
10
+ ref: { type: "positional", description: "<bundle>/<function>", required: true },
11
+ server: { type: "string", description: "指定机器(默认首台)" },
12
+ data: { type: "string", description: "JSON 请求体(不填则从 stdin 读取)" },
13
+ },
14
+ async run({ args }) {
15
+ const { bundle, fn } = parseRef(args.ref)
16
+ if (!fn) {
17
+ log.error("请使用 <bundle>/<function> 格式")
18
+ process.exit(1)
19
+ }
20
+
21
+ intro(pc.cyan(`调用 ${bundle}/${fn}`))
22
+ const ctx = await requireProjectContext()
23
+ const target = pickTarget(ctx, args.server)
24
+
25
+ const body = args.data ?? ((await Bun.stdin.text()) || "{}")
26
+ const url = `${target.server.replace(/\/+$/, "")}/${ctx.project}/${bundle}/${fn}`
27
+
28
+ let res: Response
29
+ try {
30
+ res = await fetch(url, { method: "POST", headers: { "content-type": "application/json" }, body })
31
+ } catch {
32
+ log.error(`无法连接到 ${url}`)
33
+ process.exit(1)
34
+ }
35
+
36
+ const text = await res.text()
37
+ process.stdout.write(`${text}\n`)
38
+ outro(res.ok ? pc.green(`HTTP ${res.status}`) : pc.red(`HTTP ${res.status}`))
39
+ },
40
+ })
@@ -0,0 +1,32 @@
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 { pickTarget, requireProjectContext } from "../context.ts"
6
+
7
+ export const listCommand = defineCommand({
8
+ meta: { name: "list", description: "列出已部署的函数包" },
9
+ args: {
10
+ server: { type: "string", description: "指定查询的机器(默认首台)" },
11
+ },
12
+ async run({ args }) {
13
+ intro(pc.cyan("已部署函数包"))
14
+ const ctx = await requireProjectContext()
15
+ const target = pickTarget(ctx, args.server)
16
+ const client = new AdminClient(target.server, target.token)
17
+ const bundles = await client.list(ctx.project)
18
+
19
+ if (bundles.length === 0) {
20
+ log.warn("当前项目暂无已部署的函数包")
21
+ outro(pc.yellow("已结束"))
22
+ return
23
+ }
24
+
25
+ for (const b of bundles) {
26
+ log.info(
27
+ `${pc.bold(`${b.project}/${b.bundle}`)} ${pc.dim(`${b.artifact}:${b.version}`)}\n 函数:${b.functions.join("、")}`,
28
+ )
29
+ }
30
+ outro(pc.green(`共 ${bundles.length} 个函数包`))
31
+ },
32
+ })
@@ -0,0 +1,105 @@
1
+ import path from "node:path"
2
+ import { intro, log, outro, password as passwordPrompt, spinner, text } from "@clack/prompts"
3
+ import { defineCommand } from "citty"
4
+ import pc from "picocolors"
5
+ import { MANIFEST_FILE } from "../consts.ts"
6
+ import { getSavedPassword, savePassword, setCredential } from "../creds.ts"
7
+ import type { Manifest } from "../types.ts"
8
+ import { hostOf, readJson, unwrap } from "../utils.ts"
9
+
10
+ export const loginCommand = defineCommand({
11
+ meta: { name: "login", description: "登录控制面" },
12
+ args: {
13
+ server: { type: "string", description: "服务器地址(默认读取 atomfunctions.json)" },
14
+ account: { type: "string", description: "邮箱或用户名" },
15
+ password: { type: "string", description: "密码" },
16
+ },
17
+ async run({ args }) {
18
+ intro(pc.cyan("atomfunctions 登录"))
19
+ const root = process.cwd()
20
+ const manifest = await readJson<Manifest>(path.join(root, MANIFEST_FILE))
21
+ let server = args.server ?? manifest?.server
22
+ if (!server) {
23
+ server = unwrap(
24
+ await text({
25
+ message: "控制面服务器地址",
26
+ placeholder: "http://localhost:4922",
27
+ validate: (v) => (v?.trim() ? undefined : "请输入服务器地址"),
28
+ }),
29
+ ).trim()
30
+ } else {
31
+ server = server.trim()
32
+ }
33
+
34
+ try {
35
+ new URL(server)
36
+ } catch {
37
+ log.error("请输入合法的 URL")
38
+ process.exit(1)
39
+ }
40
+
41
+ const host = hostOf(server)
42
+ const baseUrl = server.replace(/\/+$/, "")
43
+ const savedPassword = await getSavedPassword(host)
44
+
45
+ const account = args.account
46
+ ? args.account
47
+ : unwrap(
48
+ await text({
49
+ message: "账号(用户名或邮箱)",
50
+ validate: (v) => (v?.trim() ? undefined : "请输入账号"),
51
+ }),
52
+ ).trim()
53
+
54
+ async function attemptLogin(pw: string) {
55
+ const res = await fetch(`${baseUrl}/v1/auth/login`, {
56
+ method: "POST",
57
+ headers: { "content-type": "application/json" },
58
+ body: JSON.stringify({ account, password: pw }),
59
+ })
60
+ if (!res.ok) {
61
+ const body = (await res.json().catch(() => ({}))) as { error?: { message?: string } }
62
+ throw new Error(body.error?.message ?? `登录失败(${res.status})`)
63
+ }
64
+ return (await res.json()) as { token: string; user: { email: string; name: string } }
65
+ }
66
+
67
+ const spin = spinner()
68
+
69
+ if (!args.password && savedPassword) {
70
+ spin.start("使用已保存的密码登录")
71
+ try {
72
+ const res = await attemptLogin(savedPassword)
73
+ await setCredential(host, { token: res.token, account: res.user.email })
74
+ spin.stop(pc.green(`已登录为 ${res.user.name}`))
75
+ outro(`凭证已保存(${host})`)
76
+ return
77
+ } catch {
78
+ spin.stop(pc.yellow("已保存的密码已失效,请重新输入"))
79
+ }
80
+ }
81
+
82
+ const secret = args.password
83
+ ? args.password
84
+ : unwrap(
85
+ await passwordPrompt({
86
+ message: "密码",
87
+ validate: (v) => (v && v.length > 0 ? undefined : "请输入密码"),
88
+ }),
89
+ )
90
+
91
+ spin.start("正在登录")
92
+ try {
93
+ const res = await attemptLogin(secret)
94
+ await setCredential(host, { token: res.token, account: res.user.email })
95
+ await savePassword(host, secret)
96
+ spin.stop(pc.green(`已登录为 ${res.user.name}`))
97
+ } catch (err) {
98
+ spin.stop(pc.red("登录失败"))
99
+ log.error(err instanceof Error ? err.message : "未知错误")
100
+ process.exit(1)
101
+ }
102
+
103
+ outro(`凭证已保存(${host})`)
104
+ },
105
+ })
@@ -0,0 +1,26 @@
1
+ import { intro, outro } from "@clack/prompts"
2
+ import { defineCommand } from "citty"
3
+ import pc from "picocolors"
4
+ import { loadContext } from "../context.ts"
5
+ import { clearCredential } from "../creds.ts"
6
+ import { hostOf } from "../utils.ts"
7
+
8
+ export const logoutCommand = defineCommand({
9
+ meta: { name: "logout", description: "退出登录,移除本地凭证" },
10
+ args: {
11
+ server: { type: "string", description: "服务器地址(默认读取 atomfunctions.json)" },
12
+ },
13
+ async run({ args }) {
14
+ intro(pc.cyan("atomfunctions 退出登录"))
15
+ const ctx = await loadContext()
16
+ if (!ctx) process.exit(1)
17
+ const server = args.server ?? ctx.manifest.server
18
+ if (!server) {
19
+ outro(pc.red("无法确定服务器地址,请用 --server 指定"))
20
+ process.exit(1)
21
+ }
22
+ const host = hostOf(server)
23
+ const removed = await clearCredential(host)
24
+ outro(removed ? pc.green(`已退出 ${host}`) : `未找到 ${host} 的登录凭证`)
25
+ },
26
+ })
@@ -0,0 +1,29 @@
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 { pickTarget, requireProjectContext } from "../context.ts"
6
+
7
+ export const logsCommand = defineCommand({
8
+ meta: { name: "logs", description: "查看函数包日志(指定机器)" },
9
+ args: {
10
+ bundle: { type: "positional", description: "bundle 名", required: true },
11
+ server: { type: "string", description: "指定机器(默认首台,日志按机器区分)" },
12
+ tail: { type: "string", description: "末尾行数(默认 200)" },
13
+ },
14
+ async run({ args }) {
15
+ intro(pc.cyan(`日志 ${args.bundle}`))
16
+ const ctx = await requireProjectContext()
17
+ const target = pickTarget(ctx, args.server)
18
+ const client = new AdminClient(target.server, target.token)
19
+ const tail = Number(args.tail ?? "200")
20
+ const text = await client.logs(ctx.project, args.bundle, Number.isFinite(tail) ? tail : 200)
21
+
22
+ if (!text.trim()) {
23
+ log.warn("暂无日志")
24
+ } else {
25
+ process.stdout.write(`${text}\n`)
26
+ }
27
+ outro(pc.green("已结束"))
28
+ },
29
+ })
@@ -0,0 +1,140 @@
1
+ import { intro, multiselect, outro } from "@clack/prompts"
2
+ import { defineCommand } from "citty"
3
+ import pc from "picocolors"
4
+ import { ClientError } from "../client.ts"
5
+ import { authedClient, requireProjectContext, requireServerContext } from "../context.ts"
6
+ import { unwrap } from "../utils.ts"
7
+ import { writeView } from "../views.ts"
8
+
9
+ async function fail(err: unknown): Promise<never> {
10
+ const msg = err instanceof Error ? err.message : String(err)
11
+ outro(pc.red(msg))
12
+ process.exit(1)
13
+ }
14
+
15
+ const listCommand = defineCommand({
16
+ meta: { name: "list", description: "查看项目成员" },
17
+ args: {
18
+ project: { type: "string", description: "项目标识(默认读取 atomfunctions.json)" },
19
+ },
20
+ async run({ args }) {
21
+ intro(pc.cyan("项目成员"))
22
+ const ctx = await requireProjectContext()
23
+ const client = await authedClient(ctx)
24
+ const slug = args.project ?? ctx.project
25
+ try {
26
+ const data = await client.get<{ members: unknown[] }>(`/projects/${slug}/members`)
27
+ const file = await writeView(ctx, "members.list.json", data)
28
+ outro(pc.green(`已生成 ${file}`))
29
+ } catch (err) {
30
+ await fail(err)
31
+ }
32
+ },
33
+ })
34
+
35
+ const addCommand = defineCommand({
36
+ meta: { name: "add", description: "拉用户进项目(角色设为 admin)" },
37
+ args: {
38
+ project: { type: "string", description: "项目标识(默认读取 atomfunctions.json)" },
39
+ },
40
+ async run({ args }) {
41
+ intro(pc.cyan("添加成员"))
42
+ const ctx = await requireProjectContext()
43
+ const client = await authedClient(ctx)
44
+ const slug = args.project ?? ctx.project
45
+
46
+ let selectedAccounts: string[] = []
47
+ try {
48
+ const [usersData, membersData] = await Promise.all([
49
+ client.get<{ users: { name: string; email: string; isSuperAdmin: boolean }[] }>("/users"),
50
+ client.get<{ members: { name: string }[] }>(`/projects/${slug}/members`),
51
+ ])
52
+ const existing = new Set(membersData.members.map((m) => m.name))
53
+ const candidates = usersData.users.filter((u) => !u.isSuperAdmin && !existing.has(u.name))
54
+ if (candidates.length === 0) {
55
+ outro("所有用户已在项目中")
56
+ return
57
+ }
58
+ selectedAccounts = unwrap(
59
+ await multiselect({
60
+ message: "选择要添加的用户",
61
+ options: candidates.map((u) => ({ value: u.name, label: u.name, hint: u.email })),
62
+ required: true,
63
+ }),
64
+ ) as string[]
65
+ } catch (err) {
66
+ await fail(err)
67
+ }
68
+
69
+ let done = 0
70
+ let failed = 0
71
+ for (const account of selectedAccounts) {
72
+ try {
73
+ await client.post(`/projects/${slug}/members`, { account })
74
+ done++
75
+ } catch {
76
+ failed++
77
+ }
78
+ }
79
+ outro(pc.green(`已添加 ${done} 人${failed > 0 ? `,${failed} 人失败` : ""}`))
80
+ },
81
+ })
82
+
83
+ const removeCommand = defineCommand({
84
+ meta: { name: "remove", description: "移除项目成员" },
85
+ args: {
86
+ project: { type: "string", description: "项目标识(默认读取 atomfunctions.json)" },
87
+ account: { type: "positional", required: false, description: "用户邮箱或用户名" },
88
+ },
89
+ async run({ args }) {
90
+ intro(pc.cyan("移除成员"))
91
+ const ctx = await requireProjectContext()
92
+ const client = await authedClient(ctx)
93
+ const slug = args.project ?? ctx.project
94
+
95
+ let selectedAccounts: string[]
96
+ if (args.account) {
97
+ selectedAccounts = [args.account]
98
+ } else {
99
+ let members: { name: string; role: string; email: string }[] = []
100
+ try {
101
+ const data = await client.get<{ members: { name: string; role: string; email: string }[] }>(
102
+ `/projects/${slug}/members`,
103
+ )
104
+ members = data.members
105
+ } catch (err) {
106
+ await fail(err)
107
+ }
108
+ if (members.length === 0) {
109
+ outro("该项目暂无成员")
110
+ return
111
+ }
112
+ selectedAccounts = unwrap(
113
+ await multiselect({
114
+ message: "选择要移除的成员",
115
+ options: members.map((m) => ({ value: m.name, label: m.name, hint: `${m.email} ${m.role}` })),
116
+ required: true,
117
+ }),
118
+ ) as string[]
119
+ }
120
+
121
+ let done = 0
122
+ let failed = 0
123
+ for (const account of selectedAccounts) {
124
+ try {
125
+ await client.del(`/projects/${slug}/members/${encodeURIComponent(account)}`)
126
+ done += 1
127
+ } catch (err) {
128
+ failed += 1
129
+ if (err instanceof ClientError) outro(pc.red(`✗ ${account}:${err.message}`))
130
+ }
131
+ }
132
+ outro(failed === 0 ? pc.green(`已移除 ${done} 人`) : pc.yellow(`移除 ${done} 人,失败 ${failed} 人`))
133
+ if (failed > 0) process.exit(1)
134
+ },
135
+ })
136
+
137
+ export const memberCommand = defineCommand({
138
+ meta: { name: "member", description: "管理项目成员" },
139
+ subCommands: { list: listCommand, add: addCommand, remove: removeCommand },
140
+ })
@@ -0,0 +1,68 @@
1
+ import path from "node:path"
2
+ import { intro, log, outro, select } from "@clack/prompts"
3
+ import { defineCommand } from "citty"
4
+ import pc from "picocolors"
5
+ import { BUNDLE_FILE } from "../consts.ts"
6
+ import { loadContext } from "../context.ts"
7
+ import { scaffoldBundle, scaffoldContainerfile, scaffoldFunction } from "../scaffold.ts"
8
+ import type { BundleConfig, Language } from "../types.ts"
9
+ import { isValidSlug, parseRef, readJson, unwrap } from "../utils.ts"
10
+
11
+ export const newCommand = defineCommand({
12
+ meta: { name: "new", description: "创建函数包(bundle)或函数(function)" },
13
+ args: {
14
+ ref: { type: "positional", description: "<bundle> 或 <bundle>/<function>", required: true },
15
+ lang: { type: "string", description: "语言:bun(默认)" },
16
+ "with-containerfile": { type: "boolean", description: "生成逃生舱 Containerfile 模板" },
17
+ },
18
+ async run({ args }) {
19
+ intro(pc.cyan("创建脚手架"))
20
+ const ctx = await loadContext()
21
+ if (!ctx) {
22
+ log.error("未找到 atomfunctions.json")
23
+ process.exit(1)
24
+ }
25
+ const { bundle, fn } = parseRef(args.ref)
26
+
27
+ if (!isValidSlug(bundle)) {
28
+ log.error("bundle 名需以小写字母开头,仅含小写字母、数字、连字符")
29
+ process.exit(1)
30
+ }
31
+
32
+ const bundleDir = path.join(ctx.root, bundle)
33
+ const existingConfig = await readJson<BundleConfig>(path.join(bundleDir, BUNDLE_FILE))
34
+
35
+ let language: Language = existingConfig?.language ?? "bun"
36
+ if (!existingConfig) {
37
+ language = (args.lang ??
38
+ unwrap(
39
+ await select({
40
+ message: "选择语言",
41
+ options: [
42
+ { value: "bun", label: "Bun (TypeScript)" },
43
+ { value: "rust", label: "Rust" },
44
+ ],
45
+ initialValue: "bun",
46
+ }),
47
+ )) as Language
48
+ await scaffoldBundle(bundleDir, { language })
49
+ log.success(`已创建 bundle ${bundle}`)
50
+ }
51
+
52
+ if (args["with-containerfile"]) {
53
+ await scaffoldContainerfile(bundleDir)
54
+ log.success("已生成逃生舱 Containerfile")
55
+ }
56
+
57
+ if (fn) {
58
+ if (!isValidSlug(fn)) {
59
+ log.error("function 名需以小写字母开头,仅含小写字母、数字、连字符")
60
+ process.exit(1)
61
+ }
62
+ await scaffoldFunction(bundleDir, fn, language)
63
+ log.success(`已创建函数 ${bundle}/${fn}`)
64
+ }
65
+
66
+ outro(pc.green("完成,运行 atomfunctions deploy 进行部署"))
67
+ },
68
+ })