@atomservice/functions-cli 0.1.6 → 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/bundle.ts +25 -1
- package/src/commands/deploy.ts +277 -42
- package/src/commands/dev.ts +160 -18
- 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 +3 -0
- package/src/context.ts +11 -6
- package/src/index.ts +0 -2
- package/src/scaffold.ts +58 -42
- package/src/types.ts +3 -1
- package/src/utils.ts +5 -0
- package/src/commands/new.ts +0 -68
package/src/context.ts
CHANGED
|
@@ -57,12 +57,17 @@ export async function requireServerContext(): Promise<CliContext> {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
const servers = resolveServers(ctx.manifest)
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
60
|
+
const creds = await Promise.all(
|
|
61
|
+
servers.map(async (server) => {
|
|
62
|
+
const host = hostOf(server)
|
|
63
|
+
const cred = await getCredential(host)
|
|
64
|
+
return { server, host, token: cred?.token ?? null }
|
|
65
|
+
}),
|
|
66
|
+
)
|
|
67
|
+
const fallbackToken = creds.find((c) => c.token)?.token
|
|
68
|
+
const targets: ServerTarget[] = creds
|
|
69
|
+
.filter((c) => c.token || fallbackToken)
|
|
70
|
+
.map((c) => ({ server: c.server, host: c.host, token: (c.token || fallbackToken)! }))
|
|
66
71
|
|
|
67
72
|
return { ...ctx, targets }
|
|
68
73
|
}
|
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/scaffold.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from "node:path"
|
|
2
|
-
import {
|
|
2
|
+
import { snakeCase } from "change-case"
|
|
3
|
+
import { BUNDLE_FILE, FUNCTION_CONFIG_FILE, FUNCTION_FILE, RUST_SDK_VERSION, SDK_PACKAGE } from "./consts.ts"
|
|
3
4
|
import type { BundleConfig, FunctionKind, Language } from "./types.ts"
|
|
4
5
|
import { ensureDir, writeJson } from "./utils.ts"
|
|
5
6
|
|
|
@@ -87,9 +88,12 @@ export async function scaffoldFunction(
|
|
|
87
88
|
kind: FunctionKind = "function",
|
|
88
89
|
): Promise<void> {
|
|
89
90
|
if (language === "rust") {
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
91
|
+
const fnDir = path.join(bundleDir, "src", "functions", fn)
|
|
92
|
+
const fnPath = path.join(fnDir, "function.rs")
|
|
93
|
+
ensureDir(fnDir)
|
|
94
|
+
await Bun.write(fnPath, rustFunctionSource(snakeCase(fn)))
|
|
95
|
+
await writeJson(path.join(fnDir, FUNCTION_CONFIG_FILE), {})
|
|
96
|
+
await rustAutoRegister(bundleDir, fn)
|
|
93
97
|
return
|
|
94
98
|
}
|
|
95
99
|
const fnDir = path.join(bundleDir, fn)
|
|
@@ -109,47 +113,30 @@ name = ${JSON.stringify(bundle)}
|
|
|
109
113
|
path = "src/main.rs"
|
|
110
114
|
|
|
111
115
|
[dependencies]
|
|
112
|
-
|
|
116
|
+
atomfn = "${RUST_SDK_VERSION}"
|
|
113
117
|
serde = { version = "1", features = ["derive"] }
|
|
114
118
|
serde_json = "1"
|
|
119
|
+
validator = { version = "0.19", features = ["derive"] }
|
|
115
120
|
`
|
|
116
121
|
}
|
|
117
122
|
|
|
118
123
|
function rustMainSource(): string {
|
|
119
|
-
return `
|
|
120
|
-
use
|
|
121
|
-
|
|
122
|
-
#[derive(Deserialize)]
|
|
123
|
-
struct GreetInput {
|
|
124
|
-
name: String,
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
#[derive(Serialize)]
|
|
128
|
-
struct GreetOutput {
|
|
129
|
-
greeting: String,
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
fn greet(input: GreetInput, ctx: &Ctx) -> Result<GreetOutput, FunctionError> {
|
|
133
|
-
ctx.info("greeting");
|
|
134
|
-
Ok(GreetOutput { greeting: format!("hello, {}", input.name) })
|
|
135
|
-
}
|
|
124
|
+
return `mod functions;
|
|
125
|
+
use atomfn::serve;
|
|
126
|
+
use functions::*;
|
|
136
127
|
|
|
137
|
-
|
|
138
|
-
let mut registry = Registry::new();
|
|
139
|
-
registry.function("greet", greet);
|
|
140
|
-
serve(registry);
|
|
141
|
-
}
|
|
128
|
+
serve! {}
|
|
142
129
|
`
|
|
143
130
|
}
|
|
144
131
|
|
|
145
|
-
function rustFunctionSource(
|
|
146
|
-
return
|
|
147
|
-
// 并在 ${BUNDLE_FILE} 的 functions 数组中加入 "${fn}"。
|
|
148
|
-
use atomfunctions::{Ctx, FunctionError};
|
|
132
|
+
function rustFunctionSource(rustName: string): string {
|
|
133
|
+
return `use atomfn::{Ctx, FunctionError};
|
|
149
134
|
use serde::{Deserialize, Serialize};
|
|
135
|
+
use validator::Validate;
|
|
150
136
|
|
|
151
|
-
#[derive(Deserialize)]
|
|
137
|
+
#[derive(Deserialize, Validate)]
|
|
152
138
|
pub struct Input {
|
|
139
|
+
#[validate(length(min = 1, max = 100))]
|
|
153
140
|
pub name: String,
|
|
154
141
|
}
|
|
155
142
|
|
|
@@ -158,33 +145,62 @@ pub struct Output {
|
|
|
158
145
|
pub greeting: String,
|
|
159
146
|
}
|
|
160
147
|
|
|
161
|
-
pub fn ${
|
|
148
|
+
pub fn ${rustName}(input: Input, _ctx: &Ctx) -> Result<Output, FunctionError> {
|
|
162
149
|
Ok(Output { greeting: format!("hello, {}", input.name) })
|
|
163
150
|
}
|
|
164
151
|
`
|
|
165
152
|
}
|
|
166
153
|
|
|
167
|
-
function
|
|
168
|
-
|
|
169
|
-
WORKDIR /build
|
|
170
|
-
COPY . .
|
|
171
|
-
RUN cargo build --release
|
|
154
|
+
async function rustAutoRegister(bundleDir: string, slug: string): Promise<void> {
|
|
155
|
+
const rustName = snakeCase(slug)
|
|
172
156
|
|
|
173
|
-
|
|
157
|
+
const modPath = path.join(bundleDir, "src", "functions", "mod.rs")
|
|
158
|
+
const modContent = await Bun.file(modPath)
|
|
159
|
+
.text()
|
|
160
|
+
.catch(() => "")
|
|
161
|
+
const modDecl = `#[path = "${slug}/function.rs"]
|
|
162
|
+
pub mod ${rustName};
|
|
163
|
+
pub use ${rustName}::*;`
|
|
164
|
+
if (!modContent.includes(`pub mod ${rustName};`)) {
|
|
165
|
+
const newMod = modContent.trimEnd() ? `${modContent.trimEnd()}\n${modDecl}\n` : `${modDecl}\n`
|
|
166
|
+
await Bun.write(modPath, newMod)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const mainPath = path.join(bundleDir, "src", "main.rs")
|
|
170
|
+
const mainContent = await Bun.file(mainPath).text()
|
|
171
|
+
const macroMatch = mainContent.match(/serve!\s*\{([^}]*)\}/)
|
|
172
|
+
if (!macroMatch) return
|
|
173
|
+
|
|
174
|
+
const body = macroMatch[1] ?? ""
|
|
175
|
+
const entryRegex = /(stream\s+)?([a-z][a-z0-9_]*)/g
|
|
176
|
+
for (let match = entryRegex.exec(body); match !== null; match = entryRegex.exec(body)) {
|
|
177
|
+
if (match[2] === rustName) return
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const entries = body.trim() ? `${body.trim()}, ${rustName}` : rustName
|
|
181
|
+
const newMain = mainContent.replace(/serve!\s*\{[^}]*\}/, `serve! { ${entries} }`)
|
|
182
|
+
await Bun.write(mainPath, newMain)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function rustContainerfile(): string {
|
|
186
|
+
return `FROM debian:bookworm-slim
|
|
174
187
|
RUN apt-get update \\
|
|
175
188
|
&& apt-get install -y --no-install-recommends ca-certificates \\
|
|
176
189
|
&& rm -rf /var/lib/apt/lists/*
|
|
177
|
-
|
|
190
|
+
WORKDIR /app
|
|
191
|
+
COPY bundle /app/bundle
|
|
192
|
+
COPY functions.config.json /app/functions.config.json
|
|
178
193
|
ENTRYPOINT ["/app/bundle"]
|
|
179
194
|
`
|
|
180
195
|
}
|
|
181
196
|
|
|
182
197
|
async function scaffoldRustBundle(bundleDir: string, config: BundleConfig): Promise<void> {
|
|
183
198
|
const bundle = path.basename(bundleDir)
|
|
184
|
-
await writeJson(path.join(bundleDir, BUNDLE_FILE),
|
|
199
|
+
await writeJson(path.join(bundleDir, BUNDLE_FILE), config)
|
|
185
200
|
await Bun.write(path.join(bundleDir, "Cargo.toml"), rustCargoToml(bundle))
|
|
186
|
-
await Bun.write(path.join(bundleDir, "Containerfile"), rustContainerfile(
|
|
201
|
+
await Bun.write(path.join(bundleDir, "Containerfile"), rustContainerfile())
|
|
187
202
|
await Bun.write(path.join(bundleDir, "src", "main.rs"), rustMainSource())
|
|
203
|
+
await Bun.write(path.join(bundleDir, "src", "functions", "mod.rs"), "")
|
|
188
204
|
await Bun.write(path.join(bundleDir, ".gitignore"), "target/\n.atomfn-*\n")
|
|
189
205
|
}
|
|
190
206
|
|
package/src/types.ts
CHANGED
|
@@ -45,13 +45,14 @@ export interface BundleConfig {
|
|
|
45
45
|
cpus?: number
|
|
46
46
|
maxConcurrency?: number
|
|
47
47
|
defaults?: FunctionFileConfig
|
|
48
|
-
functions?: string[]
|
|
49
48
|
env?: Record<string, string>
|
|
49
|
+
dev?: { port?: number }
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
export interface DeployManifest {
|
|
53
53
|
project: string
|
|
54
54
|
bundle: string
|
|
55
|
+
arch: string
|
|
55
56
|
version: string
|
|
56
57
|
artifact: "binary" | "image"
|
|
57
58
|
functions: string[]
|
|
@@ -70,6 +71,7 @@ export interface BundleRecord {
|
|
|
70
71
|
host: string
|
|
71
72
|
port: number
|
|
72
73
|
functions: string[]
|
|
74
|
+
arch: string
|
|
73
75
|
version: string
|
|
74
76
|
artifact: "binary" | "image"
|
|
75
77
|
image: string
|
package/src/utils.ts
CHANGED
|
@@ -26,6 +26,11 @@ export function isValidSlug(slug: string): boolean {
|
|
|
26
26
|
return SLUG_RE.test(slug)
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
export function bunTarget(arch: string): string {
|
|
30
|
+
if (arch === "arm64") return "bun-linux-arm64"
|
|
31
|
+
return "bun-linux-x64"
|
|
32
|
+
}
|
|
33
|
+
|
|
29
34
|
export function unwrap<T>(value: T | symbol): T {
|
|
30
35
|
if (isCancel(value)) {
|
|
31
36
|
cancel("已取消")
|
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
|
-
})
|