@atomservice/functions-cli 0.1.7 → 0.1.8
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/package.json +4 -2
- package/src/build.ts +200 -35
- package/src/client.ts +4 -4
- package/src/commands/deploy.ts +277 -42
- package/src/commands/dev.ts +1 -1
- package/src/commands/function.ts +24 -9
- package/src/commands/init.ts +1 -0
- package/src/commands/invoke.ts +187 -16
- package/src/commands/list.ts +35 -5
- package/src/commands/logs.ts +88 -12
- package/src/commands/rollback.ts +155 -29
- package/src/commands/whoami.ts +11 -7
- package/src/consts.ts +2 -0
- package/src/context.ts +11 -6
- package/src/scaffold.ts +58 -42
- package/src/types.ts +2 -1
- package/src/utils.ts +5 -0
package/src/commands/deploy.ts
CHANGED
|
@@ -1,20 +1,78 @@
|
|
|
1
1
|
import { readdir, rm } from "node:fs/promises"
|
|
2
2
|
import path from "node:path"
|
|
3
|
-
import { intro, log,
|
|
3
|
+
import { confirm, intro, isCancel, log, multiselect, outro } from "@clack/prompts"
|
|
4
4
|
import { defineCommand } from "citty"
|
|
5
|
+
import { Listr } from "listr2"
|
|
5
6
|
import pc from "picocolors"
|
|
6
7
|
import { buildBundle } from "../build.ts"
|
|
7
8
|
import { AdminClient } from "../client.ts"
|
|
8
|
-
import { BUNDLE_FILE } from "../consts.ts"
|
|
9
|
-
import {
|
|
10
|
-
import type { DeployManifest, ServerTarget } from "../types.ts"
|
|
9
|
+
import { BUNDLE_FILE, BUNDLES_DIR } from "../consts.ts"
|
|
10
|
+
import { requireProjectContext } from "../context.ts"
|
|
11
|
+
import type { BundleConfig, DeployManifest, Manifest, ServerTarget } from "../types.ts"
|
|
12
|
+
import { bunTarget, readJson } from "../utils.ts"
|
|
13
|
+
|
|
14
|
+
class Semaphore {
|
|
15
|
+
private permits: number
|
|
16
|
+
private queue: Array<() => void> = []
|
|
17
|
+
constructor(permits: number) {
|
|
18
|
+
this.permits = permits
|
|
19
|
+
}
|
|
20
|
+
async acquire(): Promise<void> {
|
|
21
|
+
if (this.permits > 0) {
|
|
22
|
+
this.permits--
|
|
23
|
+
return
|
|
24
|
+
}
|
|
25
|
+
await new Promise<void>((resolve) => this.queue.push(resolve))
|
|
26
|
+
}
|
|
27
|
+
release(): void {
|
|
28
|
+
const next = this.queue.shift()
|
|
29
|
+
if (next) {
|
|
30
|
+
next()
|
|
31
|
+
} else {
|
|
32
|
+
this.permits++
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function resolveDeployTargets(
|
|
38
|
+
manifest: Manifest,
|
|
39
|
+
allTargets: ServerTarget[],
|
|
40
|
+
arg: string | true | undefined,
|
|
41
|
+
): Promise<ServerTarget[]> {
|
|
42
|
+
if (arg === undefined || arg === true) return allTargets
|
|
43
|
+
if (arg === "all") return allTargets
|
|
44
|
+
|
|
45
|
+
const urls = [manifest.server, ...(manifest.servers ?? [])].filter(Boolean) as string[]
|
|
46
|
+
|
|
47
|
+
if (/^\d+$/.test(arg)) {
|
|
48
|
+
const idx = Number(arg)
|
|
49
|
+
const url = idx === 0 ? manifest.server : manifest.servers?.[idx - 1]
|
|
50
|
+
if (!url) throw new Error(`索引 ${arg} 超出范围,可用 0-${manifest.servers?.length ?? 0}`)
|
|
51
|
+
const match = allTargets.find((t) => t.server === url)
|
|
52
|
+
if (!match) throw new Error(`服务器 ${url} 未登录,请先执行 atomfunctions login`)
|
|
53
|
+
return [match]
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (arg.includes("://") || arg.includes("localhost")) {
|
|
57
|
+
const parts = arg.split(",").map((s) => s.trim())
|
|
58
|
+
const result: ServerTarget[] = []
|
|
59
|
+
for (const part of parts) {
|
|
60
|
+
const match = allTargets.find((t) => t.server === part || t.host === part)
|
|
61
|
+
if (!match) throw new Error(`服务器 ${part} 不在配置中,可用:${urls.join("、")}`)
|
|
62
|
+
result.push(match)
|
|
63
|
+
}
|
|
64
|
+
return result
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
throw new Error(`无法解析 --server 值:${arg}。支持:all、0/1/2(索引)、URL(逗号分隔)`)
|
|
68
|
+
}
|
|
11
69
|
|
|
12
70
|
async function listLocalBundles(root: string): Promise<string[]> {
|
|
13
|
-
const entries = await readdir(root, { withFileTypes: true })
|
|
71
|
+
const entries = await readdir(path.join(root, BUNDLES_DIR), { withFileTypes: true })
|
|
14
72
|
const bundles: string[] = []
|
|
15
73
|
for (const entry of entries) {
|
|
16
74
|
if (!entry.isDirectory() || entry.name.startsWith(".")) continue
|
|
17
|
-
if (await Bun.file(path.join(root, entry.name, BUNDLE_FILE)).exists()) bundles.push(entry.name)
|
|
75
|
+
if (await Bun.file(path.join(root, BUNDLES_DIR, entry.name, BUNDLE_FILE)).exists()) bundles.push(entry.name)
|
|
18
76
|
}
|
|
19
77
|
return bundles.sort()
|
|
20
78
|
}
|
|
@@ -30,7 +88,8 @@ async function deployToTarget(
|
|
|
30
88
|
if (!force) {
|
|
31
89
|
const remote = await client.list(manifest.project)
|
|
32
90
|
const current = remote.find((r) => r.project === manifest.project && r.bundle === manifest.bundle)
|
|
33
|
-
if (current?.version === manifest.version
|
|
91
|
+
if (current?.version === manifest.version && current?.arch === manifest.arch)
|
|
92
|
+
return { host: target.host, ok: true, skipped: true }
|
|
34
93
|
}
|
|
35
94
|
await client.deploy(manifest.project, manifest, Bun.file(artifactPath))
|
|
36
95
|
return { host: target.host, ok: true, skipped: false }
|
|
@@ -45,15 +104,89 @@ async function deployToTarget(
|
|
|
45
104
|
}
|
|
46
105
|
|
|
47
106
|
export const deployCommand = defineCommand({
|
|
48
|
-
meta: { name: "deploy", description: "
|
|
107
|
+
meta: { name: "deploy", description: "本地构建并部署函数包" },
|
|
49
108
|
args: {
|
|
50
109
|
bundle: { type: "positional", description: "bundle 名(不填则部署全部变更)", required: false },
|
|
51
110
|
force: { type: "boolean", description: "忽略版本一致检查,强制部署" },
|
|
111
|
+
target: { type: "string", description: "编译目标架构:x64 | arm64(默认根据服务器自动选择)" },
|
|
112
|
+
server: { type: "string", description: "部署目标:all(全部)、0/1/2(索引)、URL(逗号分隔)" },
|
|
52
113
|
},
|
|
53
114
|
async run({ args }) {
|
|
54
115
|
intro(pc.cyan("部署函数包"))
|
|
55
116
|
const ctx = await requireProjectContext()
|
|
56
117
|
|
|
118
|
+
let deployTargets: ServerTarget[]
|
|
119
|
+
if (typeof args.server === "string") {
|
|
120
|
+
deployTargets = await resolveDeployTargets(ctx.manifest, ctx.targets, args.server)
|
|
121
|
+
} else {
|
|
122
|
+
const options = ctx.targets.map((t, i) => ({ value: String(i), label: t.host, hint: t.server }))
|
|
123
|
+
const selected = await multiselect({
|
|
124
|
+
message: "选择部署目标",
|
|
125
|
+
options,
|
|
126
|
+
required: true,
|
|
127
|
+
initialValues: ctx.targets.map((_, i) => String(i)),
|
|
128
|
+
})
|
|
129
|
+
if (isCancel(selected)) {
|
|
130
|
+
outro(pc.yellow("已取消"))
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
deployTargets = (selected as string[]).map((i) => ctx.targets[Number(i)]).filter(Boolean) as ServerTarget[]
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const force =
|
|
137
|
+
args.force === true ? true : await confirm({ message: "是否忽略版本一致检查,强制部署?", initialValue: false })
|
|
138
|
+
if (isCancel(force)) {
|
|
139
|
+
outro(pc.yellow("已取消"))
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const targetArches = new Map<string, string | Error>()
|
|
144
|
+
await Promise.all(
|
|
145
|
+
deployTargets.map(async (t) => {
|
|
146
|
+
try {
|
|
147
|
+
const client = new AdminClient(t.server, t.token)
|
|
148
|
+
const health = await client.get<{ arch?: string }>("/health")
|
|
149
|
+
targetArches.set(t.server, health.arch ?? "x64")
|
|
150
|
+
} catch (err) {
|
|
151
|
+
targetArches.set(t.server, err instanceof Error ? err : new Error(String(err)))
|
|
152
|
+
}
|
|
153
|
+
}),
|
|
154
|
+
)
|
|
155
|
+
const bunSemaphore = new Semaphore(10)
|
|
156
|
+
const rustSemaphore = new Semaphore(1)
|
|
157
|
+
const serverSemaphores = new Map<string, Semaphore>()
|
|
158
|
+
|
|
159
|
+
const archErrors = deployTargets.filter((t) => targetArches.get(t.server) instanceof Error)
|
|
160
|
+
if (archErrors.length > 0) {
|
|
161
|
+
for (const t of archErrors) {
|
|
162
|
+
log.error(`${t.host}:无法获取服务器架构 — ${(targetArches.get(t.server) as Error).message}`)
|
|
163
|
+
}
|
|
164
|
+
outro(pc.red("部署中止"))
|
|
165
|
+
process.exitCode = 1
|
|
166
|
+
return
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (typeof args.target === "string") {
|
|
170
|
+
for (const key of targetArches.keys()) targetArches.set(key, args.target)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const archGroups = new Map<string, ServerTarget[]>()
|
|
174
|
+
for (const t of deployTargets) {
|
|
175
|
+
const arch = targetArches.get(t.server) as string
|
|
176
|
+
if (!archGroups.has(arch)) archGroups.set(arch, [])
|
|
177
|
+
const group = archGroups.get(arch) ?? []
|
|
178
|
+
group.push(t)
|
|
179
|
+
archGroups.set(arch, group)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const uniqueArches = [...archGroups.keys()]
|
|
183
|
+
if (uniqueArches.length === 1) {
|
|
184
|
+
log.info(pc.dim(`编译目标:${uniqueArches[0]}${!args.target ? "(服务器自动检测)" : ""}`))
|
|
185
|
+
} else {
|
|
186
|
+
log.info(pc.dim(`多架构部署:${uniqueArches.join("、")}`))
|
|
187
|
+
}
|
|
188
|
+
log.info(pc.dim(`目标机器:${deployTargets.map((t) => t.host).join("、")}`))
|
|
189
|
+
|
|
57
190
|
const targets = args.bundle ? [args.bundle] : await listLocalBundles(ctx.root)
|
|
58
191
|
if (targets.length === 0) {
|
|
59
192
|
log.warn("没有可部署的 bundle")
|
|
@@ -61,43 +194,145 @@ export const deployCommand = defineCommand({
|
|
|
61
194
|
return
|
|
62
195
|
}
|
|
63
196
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
197
|
+
type DeployTask = { bundle: string; target: ServerTarget; arch: string }
|
|
198
|
+
|
|
199
|
+
const deployTasks: DeployTask[] = targets.flatMap((bundle) =>
|
|
200
|
+
[...archGroups].flatMap(([arch, archTargets]) => archTargets.map((t) => ({ bundle, target: t, arch }))),
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
type DeployLine = {
|
|
204
|
+
bundle: string
|
|
205
|
+
host: string
|
|
206
|
+
ok: boolean
|
|
207
|
+
skipped: boolean
|
|
208
|
+
message?: string
|
|
209
|
+
}
|
|
210
|
+
const deployLines: DeployLine[] = []
|
|
211
|
+
let hasError = false
|
|
212
|
+
|
|
213
|
+
const buildCache = new Map<string, Promise<{ manifest: DeployManifest; artifactPath: string }>>()
|
|
214
|
+
const buildRefs = new Map<string, number>()
|
|
215
|
+
const configCache = new Map<string, BundleConfig>()
|
|
216
|
+
|
|
217
|
+
console.log("")
|
|
218
|
+
|
|
219
|
+
const listr = new Listr(
|
|
220
|
+
deployTasks.map(({ bundle, target, arch }) => ({
|
|
221
|
+
title: bundle,
|
|
222
|
+
task: async (_ctx, task) => {
|
|
223
|
+
const config =
|
|
224
|
+
configCache.get(bundle) ??
|
|
225
|
+
(await readJson<BundleConfig>(path.join(ctx.root, BUNDLES_DIR, bundle, BUNDLE_FILE)))
|
|
226
|
+
if (!config) throw new Error(`${bundle} 缺少 ${BUNDLE_FILE}`)
|
|
227
|
+
configCache.set(bundle, config)
|
|
228
|
+
const isRust = config?.language === "rust"
|
|
229
|
+
const isBun = config?.language === "bun"
|
|
230
|
+
const buildKey = `${bundle}-${arch}`
|
|
231
|
+
let semReleased = false
|
|
232
|
+
|
|
233
|
+
try {
|
|
234
|
+
if (isRust) {
|
|
235
|
+
task.title = ` ${bundle} ${target.host} (${arch}) 等待构建...`
|
|
236
|
+
await rustSemaphore.acquire()
|
|
237
|
+
} else if (isBun) {
|
|
238
|
+
await bunSemaphore.acquire()
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
task.title = ` ${bundle} ${target.host} (${arch}) 构建中...`
|
|
242
|
+
|
|
243
|
+
if (!buildCache.has(buildKey)) {
|
|
244
|
+
buildCache.set(buildKey, buildBundle(ctx.root, ctx.project, bundle, bunTarget(arch), arch))
|
|
245
|
+
}
|
|
246
|
+
buildRefs.set(buildKey, (buildRefs.get(buildKey) ?? 0) + 1)
|
|
247
|
+
const cached = buildCache.get(buildKey)
|
|
248
|
+
if (!cached) throw new Error("构建缓存异常")
|
|
249
|
+
const { manifest, artifactPath } = await cached
|
|
250
|
+
const versionLabel = pc.dim(`${manifest.artifact}:${manifest.version}`)
|
|
251
|
+
|
|
252
|
+
if (isRust) rustSemaphore.release()
|
|
253
|
+
else if (isBun) bunSemaphore.release()
|
|
254
|
+
semReleased = true
|
|
255
|
+
|
|
256
|
+
try {
|
|
257
|
+
const sem = serverSemaphores.get(target.server) ?? new Semaphore(1)
|
|
258
|
+
serverSemaphores.set(target.server, sem)
|
|
259
|
+
task.title = ` ${bundle} ${target.host} (${arch}) 部署中... ${versionLabel}`
|
|
260
|
+
await sem.acquire()
|
|
261
|
+
try {
|
|
262
|
+
const r = await deployToTarget(target, manifest, artifactPath, force)
|
|
263
|
+
if (r.skipped) {
|
|
264
|
+
task.title = ` ${bundle} ${target.host} (${arch}) ${pc.dim("无变更")} ${versionLabel}`
|
|
265
|
+
} else if (r.ok) {
|
|
266
|
+
task.title = ` ${bundle} ${target.host} (${arch}) ${pc.green("已部署")} ${versionLabel}`
|
|
267
|
+
} else {
|
|
268
|
+
task.title = ` ${bundle} ${target.host} (${arch}) ${pc.red(r.message ?? "失败")} ${versionLabel}`
|
|
269
|
+
hasError = true
|
|
270
|
+
}
|
|
271
|
+
deployLines.push({ bundle, host: target.host, ok: r.ok, skipped: r.skipped, message: r.message })
|
|
272
|
+
if (!r.ok && !r.skipped) hasError = true
|
|
273
|
+
} finally {
|
|
274
|
+
sem.release()
|
|
275
|
+
}
|
|
276
|
+
} finally {
|
|
277
|
+
const refs = (buildRefs.get(buildKey) ?? 1) - 1
|
|
278
|
+
if (refs <= 0) {
|
|
279
|
+
buildRefs.delete(buildKey)
|
|
280
|
+
await rm(artifactPath, { force: true })
|
|
281
|
+
} else {
|
|
282
|
+
buildRefs.set(buildKey, refs)
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
} catch (error) {
|
|
286
|
+
if (!semReleased) {
|
|
287
|
+
if (isRust) rustSemaphore.release()
|
|
288
|
+
else if (isBun) bunSemaphore.release()
|
|
289
|
+
}
|
|
290
|
+
task.title = ` ${bundle} ${target.host} (${arch}) ${pc.red((error as Error).message.slice(0, 100))}`
|
|
291
|
+
hasError = true
|
|
292
|
+
throw error
|
|
293
|
+
}
|
|
294
|
+
},
|
|
295
|
+
})),
|
|
296
|
+
{
|
|
297
|
+
concurrent: true,
|
|
298
|
+
exitOnError: false,
|
|
299
|
+
rendererOptions: { collapseErrors: false },
|
|
300
|
+
},
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
await listr.run().catch(() => {})
|
|
304
|
+
|
|
305
|
+
let totalOk = 0
|
|
306
|
+
let totalFail = 0
|
|
307
|
+
let totalSkip = 0
|
|
308
|
+
const serverSet = new Set<string>()
|
|
309
|
+
|
|
310
|
+
for (const line of deployLines) {
|
|
311
|
+
serverSet.add(line.host)
|
|
312
|
+
if (line.skipped) {
|
|
313
|
+
totalSkip++
|
|
314
|
+
} else if (line.ok) {
|
|
315
|
+
totalOk++
|
|
316
|
+
} else {
|
|
317
|
+
totalFail++
|
|
98
318
|
}
|
|
99
319
|
}
|
|
100
320
|
|
|
321
|
+
const total = totalOk + totalFail + totalSkip
|
|
322
|
+
if (total > 0) {
|
|
323
|
+
const parts: string[] = []
|
|
324
|
+
if (totalOk > 0) parts.push(pc.green(`${totalOk} 成功`))
|
|
325
|
+
if (totalFail > 0) parts.push(pc.red(`${totalFail} 失败`))
|
|
326
|
+
if (totalSkip > 0) parts.push(pc.dim(`${totalSkip} 跳过`))
|
|
327
|
+
const bundleCount = new Set(deployLines.map((l) => l.bundle)).size
|
|
328
|
+
const msg = `${pc.bold("部署结果")} ${bundleCount} 个 bundle → ${serverSet.size} 台服务器,${parts.join(",")},共 ${total} 次部署`
|
|
329
|
+
if (totalFail === total) log.error(msg)
|
|
330
|
+
else if (totalFail > 0) log.warn(msg)
|
|
331
|
+
else log.success(msg)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (hasError) process.exitCode = 1
|
|
335
|
+
|
|
101
336
|
outro(pc.green("部署流程结束"))
|
|
102
337
|
},
|
|
103
338
|
})
|
package/src/commands/dev.ts
CHANGED
|
@@ -64,7 +64,7 @@ const startCommand = defineCommand({
|
|
|
64
64
|
} else {
|
|
65
65
|
const bundles = await listBundles(ctx.root)
|
|
66
66
|
if (bundles.length === 0) {
|
|
67
|
-
log.error("尚未创建任何 bundle,请先运行
|
|
67
|
+
log.error("尚未创建任何 bundle,请先运行 atomfunctions bundle create")
|
|
68
68
|
process.exit(1)
|
|
69
69
|
}
|
|
70
70
|
bundle = unwrap(await select({ message: "选择函数包", options: bundles.map((b) => ({ value: b, label: b })) })) as string
|
package/src/commands/function.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { BUNDLE_FILE, BUNDLES_DIR } from "../consts.ts"
|
|
|
8
8
|
import { loadContext } from "../context.ts"
|
|
9
9
|
import { scaffoldFunction } from "../scaffold.ts"
|
|
10
10
|
import type { BundleConfig, FunctionKind } from "../types.ts"
|
|
11
|
-
import {
|
|
11
|
+
import { readJson, unwrap } from "../utils.ts"
|
|
12
12
|
|
|
13
13
|
async function listBundles(root: string): Promise<string[]> {
|
|
14
14
|
const dir = path.join(root, BUNDLES_DIR)
|
|
@@ -65,7 +65,7 @@ const createCommand = defineCommand({
|
|
|
65
65
|
} else {
|
|
66
66
|
const bundles = await listBundles(ctx.root)
|
|
67
67
|
if (bundles.length === 0) {
|
|
68
|
-
log.error("尚未创建任何 bundle,请先运行
|
|
68
|
+
log.error("尚未创建任何 bundle,请先运行 atomfunctions bundle create")
|
|
69
69
|
process.exit(1)
|
|
70
70
|
}
|
|
71
71
|
bundle = unwrap(
|
|
@@ -76,6 +76,17 @@ const createCommand = defineCommand({
|
|
|
76
76
|
) as string
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
const bundleDir = path.join(ctx.root, BUNDLES_DIR, bundle)
|
|
80
|
+
const bundleCfg = await readJson<BundleConfig>(path.join(bundleDir, BUNDLE_FILE))
|
|
81
|
+
if (!bundleCfg) {
|
|
82
|
+
log.error(`bundle ${bundle} 不存在或不是有效的函数包`)
|
|
83
|
+
process.exit(1)
|
|
84
|
+
}
|
|
85
|
+
const language = bundleCfg.language
|
|
86
|
+
const isRust = language === "rust"
|
|
87
|
+
const slugRe = /^[a-z][a-z0-9-]*$/
|
|
88
|
+
const slugHint = "需以小写字母开头,仅含小写字母、数字、连字符"
|
|
89
|
+
|
|
79
90
|
const slug =
|
|
80
91
|
args.slug ??
|
|
81
92
|
unwrap(
|
|
@@ -84,13 +95,13 @@ const createCommand = defineCommand({
|
|
|
84
95
|
validate: (v) => {
|
|
85
96
|
const s = v?.trim()
|
|
86
97
|
if (!s) return "请输入函数名"
|
|
87
|
-
return
|
|
98
|
+
return slugRe.test(s) ? undefined : slugHint
|
|
88
99
|
},
|
|
89
100
|
}),
|
|
90
101
|
).trim()
|
|
91
102
|
|
|
92
|
-
if (!
|
|
93
|
-
log.error(
|
|
103
|
+
if (!slugRe.test(slug)) {
|
|
104
|
+
log.error(slugHint)
|
|
94
105
|
process.exit(1)
|
|
95
106
|
}
|
|
96
107
|
|
|
@@ -105,16 +116,20 @@ const createCommand = defineCommand({
|
|
|
105
116
|
}),
|
|
106
117
|
) as FunctionKind)
|
|
107
118
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
119
|
+
let exists = false
|
|
120
|
+
if (isRust) {
|
|
121
|
+
exists = await Bun.file(path.join(bundleDir, "src", "functions", slug, "function.rs")).exists()
|
|
122
|
+
} else {
|
|
123
|
+
exists = await Bun.file(path.join(bundleDir, slug, "function.ts")).exists()
|
|
124
|
+
}
|
|
125
|
+
if (exists) {
|
|
111
126
|
const ok = await confirm({ message: `函数 ${bundle}/${slug} 已存在,是否覆盖?`, initialValue: false })
|
|
112
127
|
if (isCancel(ok) || !ok) {
|
|
113
128
|
outro(pc.yellow("已取消"))
|
|
114
129
|
return
|
|
115
130
|
}
|
|
116
131
|
}
|
|
117
|
-
await scaffoldFunction(bundleDir, slug,
|
|
132
|
+
await scaffoldFunction(bundleDir, slug, language, kind)
|
|
118
133
|
log.success(`已创建${KIND_OPTIONS.find((k) => k.value === kind)?.label}函数 ${bundle}/${slug}`)
|
|
119
134
|
outro(pc.green("完成,运行 atomfunctions deploy 进行部署"))
|
|
120
135
|
},
|