@atomservice/functions-cli 0.1.6 → 0.1.7
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 +1 -1
- package/src/commands/bundle.ts +25 -1
- package/src/commands/dev.ts +160 -18
- package/src/consts.ts +1 -0
- package/src/index.ts +0 -2
- package/src/types.ts +1 -0
- package/src/commands/new.ts +0 -68
package/package.json
CHANGED
package/src/commands/bundle.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import path from "node:path"
|
|
2
|
+
import { $ } from "bun"
|
|
2
3
|
import { confirm, intro, isCancel, log, outro, select, text } from "@clack/prompts"
|
|
3
4
|
import { defineCommand } from "citty"
|
|
4
5
|
import pc from "picocolors"
|
|
5
6
|
import { BUNDLE_FILE, BUNDLES_DIR } from "../consts.ts"
|
|
6
7
|
import { loadContext } from "../context.ts"
|
|
7
|
-
import { scaffoldBundle } from "../scaffold.ts"
|
|
8
|
+
import { scaffoldBundle, scaffoldContainerfile } from "../scaffold.ts"
|
|
8
9
|
import type { BundleConfig, Language } from "../types.ts"
|
|
9
10
|
import { isValidSlug, readJson, unwrap } from "../utils.ts"
|
|
10
11
|
|
|
@@ -13,6 +14,7 @@ const createCommand = defineCommand({
|
|
|
13
14
|
args: {
|
|
14
15
|
slug: { type: "positional", required: false, description: "bundle 名" },
|
|
15
16
|
lang: { type: "string", description: "语言:bun(默认)" },
|
|
17
|
+
"with-containerfile": { type: "boolean", description: "生成逃生舱 Containerfile 模板" },
|
|
16
18
|
},
|
|
17
19
|
async run({ args }) {
|
|
18
20
|
intro(pc.cyan("创建函数包"))
|
|
@@ -66,6 +68,28 @@ const createCommand = defineCommand({
|
|
|
66
68
|
|
|
67
69
|
await scaffoldBundle(bundleDir, { language })
|
|
68
70
|
log.success(`已创建 bundle ${slug}`)
|
|
71
|
+
|
|
72
|
+
const withContainerfile =
|
|
73
|
+
args["with-containerfile"] === true ||
|
|
74
|
+
(await confirm({ message: "是否生成 Containerfile 模板?", initialValue: false }))
|
|
75
|
+
if (!isCancel(withContainerfile) && withContainerfile) {
|
|
76
|
+
await scaffoldContainerfile(bundleDir)
|
|
77
|
+
log.success("已生成 Containerfile")
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (language === "bun") {
|
|
81
|
+
const install = await confirm({ message: "是否安装依赖包?", initialValue: true })
|
|
82
|
+
if (!isCancel(install) && install) {
|
|
83
|
+
log.info("正在安装依赖...")
|
|
84
|
+
const result = await $`bun install`.cwd(bundleDir).quiet().nothrow()
|
|
85
|
+
if (result.exitCode === 0) {
|
|
86
|
+
log.success("依赖安装完成")
|
|
87
|
+
} else {
|
|
88
|
+
log.warn("依赖安装失败,请稍后手动执行 bun install")
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
69
93
|
outro(pc.green(`完成,路径 ${path.relative(ctx.root, bundleDir)}`))
|
|
70
94
|
},
|
|
71
95
|
})
|
package/src/commands/dev.ts
CHANGED
|
@@ -1,32 +1,80 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { existsSync, readdirSync } from "node:fs"
|
|
2
|
+
import type { Dirent } from "node:fs"
|
|
3
|
+
import { rm, writeFile } from "node:fs/promises"
|
|
4
|
+
import { connect } from "node:net"
|
|
2
5
|
import path from "node:path"
|
|
3
|
-
import { intro, log } from "@clack/prompts"
|
|
6
|
+
import { intro, log, outro, select, text } from "@clack/prompts"
|
|
4
7
|
import { defineCommand } from "citty"
|
|
5
8
|
import pc from "picocolors"
|
|
6
9
|
import { resolveFunctions, writeEntry } from "../build.ts"
|
|
7
|
-
import { BUNDLE_FILE, DEFAULT_DEV_PORT } from "../consts.ts"
|
|
10
|
+
import { BUNDLES_DIR, BUNDLE_FILE, DEFAULT_DEV_PORT, DEV_PID_FILE } from "../consts.ts"
|
|
8
11
|
import { loadContext } from "../context.ts"
|
|
9
12
|
import type { BundleConfig } from "../types.ts"
|
|
10
|
-
import { readJson } from "../utils.ts"
|
|
13
|
+
import { isValidSlug, readJson, unwrap, writeJson } from "../utils.ts"
|
|
11
14
|
|
|
12
|
-
|
|
13
|
-
|
|
15
|
+
function pidFile(bundleDir: string): string {
|
|
16
|
+
return path.join(bundleDir, DEV_PID_FILE)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function isPortInUse(port: number): Promise<boolean> {
|
|
20
|
+
return new Promise((resolve) => {
|
|
21
|
+
const sock = connect({ port, host: "127.0.0.1" }, () => {
|
|
22
|
+
sock.destroy()
|
|
23
|
+
resolve(true)
|
|
24
|
+
})
|
|
25
|
+
sock.on("error", () => resolve(false))
|
|
26
|
+
})
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function listBundles(root: string): Promise<string[]> {
|
|
30
|
+
const dir = path.join(root, BUNDLES_DIR)
|
|
31
|
+
let entries: Dirent[]
|
|
32
|
+
try {
|
|
33
|
+
entries = readdirSync(dir, { withFileTypes: true }) as unknown as Dirent[]
|
|
34
|
+
} catch {
|
|
35
|
+
return []
|
|
36
|
+
}
|
|
37
|
+
return entries
|
|
38
|
+
.filter((e) => e.isDirectory() && !e.name.startsWith("."))
|
|
39
|
+
.filter((e) => existsSync(path.join(dir, e.name, BUNDLE_FILE)))
|
|
40
|
+
.map((e) => e.name)
|
|
41
|
+
.sort()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const startCommand = defineCommand({
|
|
45
|
+
meta: { name: "start", description: "启动本地开发服务器" },
|
|
14
46
|
args: {
|
|
15
|
-
bundle: { type: "positional", description: "bundle 名"
|
|
16
|
-
port: { type: "string", description:
|
|
47
|
+
bundle: { type: "positional", required: false, description: "bundle 名" },
|
|
48
|
+
port: { type: "string", description: `本地端口(默认从 atombundle.json 读取,否则 ${DEFAULT_DEV_PORT})` },
|
|
17
49
|
},
|
|
18
50
|
async run({ args }) {
|
|
19
|
-
intro(pc.cyan(`本地运行 ${args.bundle}`))
|
|
20
51
|
const ctx = await loadContext()
|
|
21
52
|
if (!ctx) {
|
|
22
53
|
log.error("未找到 atomfunctions.json")
|
|
23
54
|
process.exit(1)
|
|
24
55
|
}
|
|
25
|
-
const bundleDir = path.join(ctx.root, args.bundle)
|
|
26
56
|
|
|
27
|
-
|
|
57
|
+
let bundle: string
|
|
58
|
+
if (typeof args.bundle === "string") {
|
|
59
|
+
if (!isValidSlug(args.bundle)) {
|
|
60
|
+
log.error("bundle 名需以小写字母开头,仅含小写字母、数字、连字符")
|
|
61
|
+
process.exit(1)
|
|
62
|
+
}
|
|
63
|
+
bundle = args.bundle
|
|
64
|
+
} else {
|
|
65
|
+
const bundles = await listBundles(ctx.root)
|
|
66
|
+
if (bundles.length === 0) {
|
|
67
|
+
log.error("尚未创建任何 bundle,请先运行 atomfn bundle create")
|
|
68
|
+
process.exit(1)
|
|
69
|
+
}
|
|
70
|
+
bundle = unwrap(await select({ message: "选择函数包", options: bundles.map((b) => ({ value: b, label: b })) })) as string
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const bundleDir = path.join(ctx.root, BUNDLES_DIR, bundle)
|
|
74
|
+
const configPath = path.join(bundleDir, BUNDLE_FILE)
|
|
75
|
+
const config = await readJson<BundleConfig>(configPath)
|
|
28
76
|
if (!config) {
|
|
29
|
-
log.error(`未找到 ${
|
|
77
|
+
log.error(`未找到 ${BUNDLES_DIR}/${bundle}/${BUNDLE_FILE}`)
|
|
30
78
|
process.exit(1)
|
|
31
79
|
}
|
|
32
80
|
if (config.language !== "bun") {
|
|
@@ -34,16 +82,50 @@ export const devCommand = defineCommand({
|
|
|
34
82
|
process.exit(1)
|
|
35
83
|
}
|
|
36
84
|
|
|
85
|
+
const defaultPort = args.port ? Number(args.port) : (config.dev?.port ?? DEFAULT_DEV_PORT)
|
|
86
|
+
if (Number.isNaN(defaultPort) || defaultPort < 1 || defaultPort > 65535) {
|
|
87
|
+
log.error("端口号不合法,范围 1-65535")
|
|
88
|
+
process.exit(1)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const portStr = unwrap(
|
|
92
|
+
await text({
|
|
93
|
+
message: "监听端口",
|
|
94
|
+
initialValue: String(defaultPort),
|
|
95
|
+
validate: (v) => {
|
|
96
|
+
const n = Number(v)
|
|
97
|
+
if (Number.isNaN(n) || n < 1 || n > 65535) return "端口号不合法,范围 1-65535"
|
|
98
|
+
},
|
|
99
|
+
}),
|
|
100
|
+
).trim()
|
|
101
|
+
const port = Number(portStr)
|
|
102
|
+
|
|
103
|
+
const pf = pidFile(bundleDir)
|
|
104
|
+
if (existsSync(pf)) {
|
|
105
|
+
const oldPid = Number(await Bun.file(pf).text().catch(() => ""))
|
|
106
|
+
if (oldPid && isProcessAlive(oldPid)) {
|
|
107
|
+
log.error(`bundle ${bundle} 已在运行中(pid ${oldPid})`)
|
|
108
|
+
process.exit(1)
|
|
109
|
+
}
|
|
110
|
+
await rm(pf, { force: true })
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (await isPortInUse(port)) {
|
|
114
|
+
log.error(`端口 ${port} 已被占用,请更换`)
|
|
115
|
+
process.exit(1)
|
|
116
|
+
}
|
|
117
|
+
|
|
37
118
|
const functions = await resolveFunctions(bundleDir, config.defaults)
|
|
38
119
|
if (functions.length === 0) {
|
|
39
120
|
log.error("该 bundle 内没有函数")
|
|
40
121
|
process.exit(1)
|
|
41
122
|
}
|
|
42
123
|
|
|
43
|
-
|
|
124
|
+
intro(pc.cyan(`本地开发 ${bundle}`))
|
|
125
|
+
|
|
44
126
|
const entryPath = await writeEntry(bundleDir, functions)
|
|
45
127
|
log.info(`函数:${functions.map((f) => f.name).join("、")}`)
|
|
46
|
-
log.info(pc.dim(`http://localhost:${port}/
|
|
128
|
+
log.info(pc.dim(`http://localhost:${port}/invoke/<function>`))
|
|
47
129
|
|
|
48
130
|
const proc = Bun.spawn(["bun", "--watch", entryPath], {
|
|
49
131
|
cwd: bundleDir,
|
|
@@ -52,20 +134,80 @@ export const devCommand = defineCommand({
|
|
|
52
134
|
...process.env,
|
|
53
135
|
...config.env,
|
|
54
136
|
ATOMFN_PROJECT: ctx.project,
|
|
55
|
-
ATOMFN_BUNDLE:
|
|
137
|
+
ATOMFN_BUNDLE: bundle,
|
|
56
138
|
ATOMFN_PORT: String(port),
|
|
57
139
|
},
|
|
58
140
|
})
|
|
59
141
|
|
|
60
|
-
|
|
142
|
+
await writeFile(pf, String(proc.pid))
|
|
143
|
+
|
|
144
|
+
// write back port if changed
|
|
145
|
+
const prevPort = config.dev?.port
|
|
146
|
+
if (port !== prevPort) {
|
|
147
|
+
config.dev = { ...config.dev, port }
|
|
148
|
+
await writeJson(configPath, config)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const onExit = async () => {
|
|
61
152
|
proc.kill()
|
|
62
153
|
await rm(entryPath, { force: true })
|
|
154
|
+
await rm(pf, { force: true })
|
|
63
155
|
process.exit(0)
|
|
64
156
|
}
|
|
65
|
-
process.on("SIGINT",
|
|
66
|
-
process.on("SIGTERM",
|
|
157
|
+
process.on("SIGINT", onExit)
|
|
158
|
+
process.on("SIGTERM", onExit)
|
|
67
159
|
|
|
68
160
|
await proc.exited
|
|
69
161
|
await rm(entryPath, { force: true })
|
|
162
|
+
await rm(pf, { force: true })
|
|
163
|
+
},
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
const stopCommand = defineCommand({
|
|
167
|
+
meta: { name: "stop", description: "停止本地开发服务器" },
|
|
168
|
+
async run() {
|
|
169
|
+
const ctx = await loadContext()
|
|
170
|
+
if (!ctx) {
|
|
171
|
+
log.error("未找到 atomfunctions.json")
|
|
172
|
+
process.exit(1)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const bundles = await listBundles(ctx.root)
|
|
176
|
+
let stopped = 0
|
|
177
|
+
for (const b of bundles) {
|
|
178
|
+
const pf = pidFile(path.join(ctx.root, BUNDLES_DIR, b))
|
|
179
|
+
if (!existsSync(pf)) continue
|
|
180
|
+
const pid = Number(await Bun.file(pf).text().catch(() => ""))
|
|
181
|
+
if (!pid) continue
|
|
182
|
+
if (isProcessAlive(pid)) {
|
|
183
|
+
try {
|
|
184
|
+
process.kill(pid, "SIGTERM")
|
|
185
|
+
stopped++
|
|
186
|
+
} catch {
|
|
187
|
+
// already gone
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
await rm(pf, { force: true })
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (stopped === 0) {
|
|
194
|
+
outro("没有正在运行的 dev 服务")
|
|
195
|
+
} else {
|
|
196
|
+
outro(pc.green(`已停止 ${stopped} 个 dev 服务`))
|
|
197
|
+
}
|
|
70
198
|
},
|
|
71
199
|
})
|
|
200
|
+
|
|
201
|
+
function isProcessAlive(pid: number): boolean {
|
|
202
|
+
try {
|
|
203
|
+
process.kill(pid, 0)
|
|
204
|
+
return true
|
|
205
|
+
} catch {
|
|
206
|
+
return false
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export const devCommand = defineCommand({
|
|
211
|
+
meta: { name: "dev", description: "本地开发服务器" },
|
|
212
|
+
subCommands: { start: startCommand, stop: stopCommand },
|
|
213
|
+
})
|
package/src/consts.ts
CHANGED
|
@@ -5,6 +5,7 @@ export const MANIFEST_FILE = "atomfunctions.json"
|
|
|
5
5
|
export const MANIFEST_SCHEMA_FILE = "atomfunctions.schema.json"
|
|
6
6
|
export const BUNDLE_FILE = "atombundle.json"
|
|
7
7
|
export const BUNDLES_DIR = "bundles"
|
|
8
|
+
export const DEV_PID_FILE = ".atomfn-dev.pid"
|
|
8
9
|
export const SCHEMAS_DIR = "schemas"
|
|
9
10
|
export const FUNCTION_FILE = "function.ts"
|
|
10
11
|
export const FUNCTION_CONFIG_FILE = "function.json"
|
package/src/index.ts
CHANGED
|
@@ -10,7 +10,6 @@ import { loginCommand } from "./commands/login.ts"
|
|
|
10
10
|
import { logoutCommand } from "./commands/logout.ts"
|
|
11
11
|
import { logsCommand } from "./commands/logs.ts"
|
|
12
12
|
import { memberCommand } from "./commands/member.ts"
|
|
13
|
-
import { newCommand } from "./commands/new.ts"
|
|
14
13
|
import { projectCommand } from "./commands/project.ts"
|
|
15
14
|
import { pullCommand } from "./commands/pull.ts"
|
|
16
15
|
import { pushCommand } from "./commands/push.ts"
|
|
@@ -34,7 +33,6 @@ export const mainCommand = defineCommand({
|
|
|
34
33
|
function: functionCommand,
|
|
35
34
|
push: pushCommand,
|
|
36
35
|
pull: pullCommand,
|
|
37
|
-
new: newCommand,
|
|
38
36
|
dev: devCommand,
|
|
39
37
|
deploy: deployCommand,
|
|
40
38
|
list: listCommand,
|
package/src/types.ts
CHANGED
package/src/commands/new.ts
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
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
|
-
})
|