@nextclaw/app-runtime 0.14.1 → 0.15.0

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.
Files changed (28) hide show
  1. package/dist/index.d.ts +3 -2
  2. package/dist/index.js +2 -1
  3. package/dist/package.js +1 -1
  4. package/dist/services/app-build.service.d.ts +1 -0
  5. package/dist/services/app-build.service.d.ts.map +1 -1
  6. package/dist/services/app-build.service.js +37 -2
  7. package/dist/services/app-build.service.js.map +1 -1
  8. package/dist/services/app-runtime-cli.service.js +1 -1
  9. package/dist/services/app-runtime-cli.service.js.map +1 -1
  10. package/dist/services/app-runtime-options.service.d.ts.map +1 -1
  11. package/dist/services/app-runtime-options.service.js +2 -2
  12. package/dist/services/app-runtime-options.service.js.map +1 -1
  13. package/dist/services/app-runtime-toolchain.service.d.ts +5 -2
  14. package/dist/services/app-runtime-toolchain.service.d.ts.map +1 -1
  15. package/dist/services/app-runtime-toolchain.service.js +36 -4
  16. package/dist/services/app-runtime-toolchain.service.js.map +1 -1
  17. package/dist/services/app-rust-wasi-scaffold-template.service.d.ts +24 -0
  18. package/dist/services/app-rust-wasi-scaffold-template.service.d.ts.map +1 -0
  19. package/dist/services/app-rust-wasi-scaffold-template.service.js +357 -0
  20. package/dist/services/app-rust-wasi-scaffold-template.service.js.map +1 -0
  21. package/dist/services/app-scaffold.service.d.ts +4 -2
  22. package/dist/services/app-scaffold.service.d.ts.map +1 -1
  23. package/dist/services/app-scaffold.service.js +20 -4
  24. package/dist/services/app-scaffold.service.js.map +1 -1
  25. package/dist/services/platform-auth-state.service.js +1 -1
  26. package/package.json +2 -1
  27. package/resources/rust-wasi/Cargo.lock +454 -0
  28. package/resources/wit/portable-service.wit +47 -0
@@ -0,0 +1,357 @@
1
+ import { readFileSync } from "node:fs";
2
+ //#region src/services/app-rust-wasi-scaffold-template.service.ts
3
+ const RUST_WASI_GUEST_CRATE_NAME = "nextclaw-rust-wasi-guest";
4
+ const PORTABLE_SERVICE_WIT = readFileSync(new URL("../../resources/wit/portable-service.wit", import.meta.url), "utf8");
5
+ const RUST_WASI_CARGO_LOCK = readFileSync(new URL("../../resources/rust-wasi/Cargo.lock", import.meta.url), "utf8");
6
+ var AppRustWasiScaffoldTemplateService = class {
7
+ buildFiles = (params) => {
8
+ const { appId, appName } = params;
9
+ const packageSlug = appId.replace(/[^a-z0-9]+/g, "-");
10
+ const panelId = `${packageSlug}-panel`;
11
+ const serviceId = `${packageSlug}-service`;
12
+ return [
13
+ {
14
+ relativePath: "manifest.json",
15
+ content: `${JSON.stringify(this.buildManifest(appId, appName, panelId, serviceId), null, 2)}\n`
16
+ },
17
+ {
18
+ relativePath: "marketplace.json",
19
+ content: `${JSON.stringify(this.buildMarketplaceMetadata(appName), null, 2)}\n`
20
+ },
21
+ {
22
+ relativePath: "README.md",
23
+ content: this.buildReadme(appName, serviceId)
24
+ },
25
+ {
26
+ relativePath: `panels/${panelId}.panel/panel-app.json`,
27
+ content: `${JSON.stringify(this.buildPanelManifest(panelId, appName, serviceId), null, 2)}\n`
28
+ },
29
+ {
30
+ relativePath: `panels/${panelId}.panel/index.html`,
31
+ content: this.buildPanelHtml(appName)
32
+ },
33
+ {
34
+ relativePath: `panels/${panelId}.panel/app.js`,
35
+ content: this.buildPanelScript(serviceId)
36
+ },
37
+ {
38
+ relativePath: `service-components/${serviceId}/service-app.json`,
39
+ content: `${JSON.stringify(this.buildServiceManifest(serviceId), null, 2)}\n`
40
+ },
41
+ {
42
+ relativePath: "guest/Cargo.toml",
43
+ content: this.buildCargoToml()
44
+ },
45
+ {
46
+ relativePath: "guest/Cargo.lock",
47
+ content: RUST_WASI_CARGO_LOCK
48
+ },
49
+ {
50
+ relativePath: "guest/src/lib.rs",
51
+ content: this.buildRustSource()
52
+ },
53
+ {
54
+ relativePath: "guest/wit/portable-service.wit",
55
+ content: PORTABLE_SERVICE_WIT
56
+ },
57
+ {
58
+ relativePath: "tests/service-smoke.json",
59
+ content: `${JSON.stringify(this.buildServiceSmokeFixture(serviceId), null, 2)}\n`
60
+ },
61
+ {
62
+ relativePath: "assets/icon.svg",
63
+ content: this.buildIconSvg()
64
+ }
65
+ ];
66
+ };
67
+ buildManifest = (appId, appName, panelId, serviceId) => ({
68
+ schemaVersion: 2,
69
+ id: appId,
70
+ name: appName,
71
+ version: "0.1.0",
72
+ description: `${appName},由 Rust/WASI Component 保存持久计数。`,
73
+ icon: "assets/icon.svg",
74
+ engines: { nextclaw: ">=0.45.4" },
75
+ presentation: { primaryPanel: panelId },
76
+ runtime: { profile: "wasi" },
77
+ distribution: { mode: "universal" },
78
+ storage: {
79
+ scope: "global",
80
+ schemaVersion: 1
81
+ },
82
+ permissions: { storage: { namespace: appId.replace(/\./g, "-") } },
83
+ components: [{
84
+ kind: "panel",
85
+ path: `panels/${panelId}.panel`
86
+ }, {
87
+ kind: "service",
88
+ path: `service-components/${serviceId}`
89
+ }]
90
+ });
91
+ buildPanelManifest = (panelId, appName, serviceId) => ({
92
+ id: panelId,
93
+ title: appName,
94
+ description: "读取并增加由 Rust/WASI Component 持久保存的计数。",
95
+ icon: "🦀",
96
+ entry: "index.html",
97
+ actions: [`${serviceId}.counter_read`, `${serviceId}.counter_increment`]
98
+ });
99
+ buildServiceManifest = (serviceId) => ({
100
+ id: serviceId,
101
+ title: "Rust/WASI 持久计数组件",
102
+ description: "通过宿主 KV 读取和增加持久计数。",
103
+ protocol: "wasi-component",
104
+ component: { entry: "service.wasm" },
105
+ actions: {
106
+ counter_read: {
107
+ risk: "read",
108
+ title: "读取持久计数"
109
+ },
110
+ counter_increment: {
111
+ risk: "write",
112
+ title: "增加持久计数",
113
+ inputSchema: {
114
+ type: "object",
115
+ properties: { step: {
116
+ type: "integer",
117
+ minimum: 1,
118
+ maximum: 100
119
+ } },
120
+ additionalProperties: false
121
+ }
122
+ }
123
+ }
124
+ });
125
+ buildServiceSmokeFixture = (serviceId) => ({
126
+ schemaVersion: 1,
127
+ component: serviceId,
128
+ resetData: true,
129
+ steps: [{
130
+ action: "counter_increment",
131
+ input: { step: 3 },
132
+ expect: {
133
+ counter: 3,
134
+ persistedBy: "host.kv"
135
+ }
136
+ }, {
137
+ action: "counter_read",
138
+ input: {},
139
+ expect: {
140
+ counter: 3,
141
+ persistedBy: "host.kv"
142
+ }
143
+ }]
144
+ });
145
+ buildCargoToml = () => `[package]
146
+ name = "${RUST_WASI_GUEST_CRATE_NAME}"
147
+ version = "0.1.0"
148
+ edition = "2024"
149
+ publish = false
150
+
151
+ [dependencies]
152
+ serde_json = "1.0"
153
+ wit-bindgen = "0.44.0"
154
+
155
+ [lib]
156
+ crate-type = ["cdylib"]
157
+ `;
158
+ buildRustSource = () => `wit_bindgen::generate!({
159
+ path: "wit",
160
+ world: "service-app",
161
+ });
162
+
163
+ use exports::nextclaw::portable_service::service::{Action, Guest};
164
+ use nextclaw::portable_service::host;
165
+ use serde_json::{Value, json};
166
+
167
+ struct Component;
168
+
169
+ impl Guest for Component {
170
+ fn list_actions() -> Vec<Action> {
171
+ vec![
172
+ Action {
173
+ name: "counter_read".into(),
174
+ title: "读取持久计数".into(),
175
+ description: "从宿主管理的 KV 存储读取计数。".into(),
176
+ },
177
+ Action {
178
+ name: "counter_increment".into(),
179
+ title: "增加持久计数".into(),
180
+ description: "在 Rust/WASM 中计算,并通过宿主 KV 持久化。".into(),
181
+ },
182
+ ]
183
+ }
184
+
185
+ fn invoke(action: String, input_json: String) -> Result<String, String> {
186
+ host::log(host::LogLevel::Info, &format!("invoking {action}"));
187
+ let input: Value = serde_json::from_str(&input_json).unwrap_or_else(|_| json!({}));
188
+ match action.as_str() {
189
+ "counter_read" => Ok(counter_result(read_counter()?)),
190
+ "counter_increment" => {
191
+ let step = input.get("step").and_then(Value::as_i64).unwrap_or(1);
192
+ if !(1..=100).contains(&step) {
193
+ return Err("INVALID_INPUT: step must be between 1 and 100".into());
194
+ }
195
+ let counter = read_counter()?.saturating_add(step);
196
+ host::kv_set("counter", &counter.to_string())?;
197
+ Ok(counter_result(counter))
198
+ }
199
+ _ => Err(format!("UNKNOWN_ACTION: {action}")),
200
+ }
201
+ }
202
+
203
+ fn start(_config_json: String) -> Result<String, String> {
204
+ Ok(json!({ "started": true, "mode": "action" }).to_string())
205
+ }
206
+
207
+ fn handle_event(_event_json: String) -> Result<String, String> {
208
+ Err("UNSUPPORTED_LIFECYCLE: action component does not accept resident events".into())
209
+ }
210
+
211
+ fn stop(_reason_json: String) -> Result<String, String> {
212
+ Ok(json!({ "stopped": true, "mode": "action" }).to_string())
213
+ }
214
+ }
215
+
216
+ fn read_counter() -> Result<i64, String> {
217
+ Ok(host::kv_get("counter")?
218
+ .and_then(|value| value.parse().ok())
219
+ .unwrap_or(0))
220
+ }
221
+
222
+ fn counter_result(counter: i64) -> String {
223
+ json!({ "counter": counter, "persistedBy": "host.kv" }).to_string()
224
+ }
225
+
226
+ export!(Component with_types_in self);
227
+ `;
228
+ buildReadme = (appName, serviceId) => `# ${appName}
229
+
230
+ 这是一个可以独立构建的 Rust/WASI Component App。Panel 和 Agent 调用同一组 Service Action,计数通过 NextClaw 宿主 KV 持久保存。
231
+
232
+ ## 1. 准备 Rust 工具链
233
+
234
+ \`\`\`bash
235
+ rustup target add wasm32-wasip2
236
+ \`\`\`
237
+
238
+ ## 2. 构建 Component
239
+
240
+ \`\`\`bash
241
+ cd guest
242
+ cargo build --release --target wasm32-wasip2
243
+ cp target/wasm32-wasip2/release/nextclaw_rust_wasi_guest.wasm ../service-components/${serviceId}/service.wasm
244
+ cd ..
245
+ \`\`\`
246
+
247
+ 项目内的 \`guest/wit/portable-service.wit\` 是当前 Service Action 与宿主能力合同;不需要 NextClaw 源码仓库。
248
+
249
+ ## 3. 检查和调试
250
+
251
+ \`\`\`bash
252
+ nextclaw app check .
253
+ nextclaw app dev .
254
+ nextclaw app call . counter_read
255
+ nextclaw app call . counter_increment --input '{"step":2}'
256
+ \`\`\`
257
+
258
+ 如果一个包包含多个 Service Component,请增加 \`--component <component-id>\`。
259
+
260
+ ## 4. 打包和安装
261
+
262
+ \`\`\`bash
263
+ nextclaw app pack . --target universal --out ${this.normalizeSlug(appName)}.napp
264
+ nextclaw app install ./${this.normalizeSlug(appName)}.napp
265
+ \`\`\`
266
+ `;
267
+ buildPanelHtml = (appName) => `<!doctype html>
268
+ <html lang="zh-CN">
269
+ <head>
270
+ <meta charset="utf-8" />
271
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
272
+ <title>${appName}</title>
273
+ <style>
274
+ :root { font-family: ui-sans-serif, system-ui, sans-serif; color: #172033; background: #f5f7fb; }
275
+ body { margin: 0; min-height: 100vh; display: grid; place-items: center; }
276
+ main { width: min(420px, calc(100vw - 40px)); padding: 28px; border-radius: 24px; background: white; box-shadow: 0 18px 60px #24324a1f; }
277
+ p { color: #667085; }
278
+ output { display: block; margin: 24px 0; font-size: 64px; font-weight: 700; }
279
+ button { border: 0; border-radius: 999px; padding: 12px 18px; background: #172033; color: white; cursor: pointer; }
280
+ #error { color: #b42318; }
281
+ </style>
282
+ </head>
283
+ <body>
284
+ <main>
285
+ <h1>${appName}</h1>
286
+ <p>这个数字由 Rust/WASI Component 计算,并通过宿主 KV 持久保存。</p>
287
+ <output id="counter">…</output>
288
+ <button id="increment" type="button">增加 1</button>
289
+ <p id="error" role="alert"></p>
290
+ </main>
291
+ <script src="app.js"><\/script>
292
+ </body>
293
+ </html>
294
+ `;
295
+ buildPanelScript = (serviceId) => `const counter = document.querySelector("#counter");
296
+ const error = document.querySelector("#error");
297
+ const increment = document.querySelector("#increment");
298
+
299
+ async function readCounter() {
300
+ error.textContent = "";
301
+ try {
302
+ const result = await window.nextclaw.serviceActions.invoke("${serviceId}.counter_read", {});
303
+ counter.textContent = String(result.counter ?? 0);
304
+ } catch (cause) {
305
+ error.textContent = cause instanceof Error ? cause.message : String(cause);
306
+ }
307
+ }
308
+
309
+ async function incrementCounter() {
310
+ error.textContent = "";
311
+ try {
312
+ const result = await window.nextclaw.serviceActions.invoke("${serviceId}.counter_increment", { step: 1 });
313
+ counter.textContent = String(result.counter ?? 0);
314
+ } catch (cause) {
315
+ error.textContent = cause instanceof Error ? cause.message : String(cause);
316
+ }
317
+ }
318
+
319
+ increment.addEventListener("click", incrementCounter);
320
+ void readCounter();
321
+ `;
322
+ buildMarketplaceMetadata = (appName) => ({
323
+ slug: this.normalizeSlug(appName),
324
+ summary: `${appName} Rust/WASI Component 示例。`,
325
+ summaryI18n: {
326
+ zh: `${appName} Rust/WASI Component 示例。`,
327
+ en: `${appName}, a Rust/WASI Component example.`
328
+ },
329
+ description: "A minimal Rust/WASI Component App with a Panel and host-managed persistent KV.",
330
+ descriptionI18n: {
331
+ zh: "一个包含 Panel 和宿主持久 KV 的最小 Rust/WASI Component App。",
332
+ en: "A minimal Rust/WASI Component App with a Panel and host-managed persistent KV."
333
+ },
334
+ author: "NextClaw",
335
+ tags: [
336
+ "starter",
337
+ "rust",
338
+ "wasi-component",
339
+ "official"
340
+ ],
341
+ sourceRepo: "https://github.com/Peiiii/nextclaw",
342
+ homepage: "https://nextclaw.io",
343
+ featured: false
344
+ });
345
+ normalizeSlug = (value) => value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "rust-wasi-app";
346
+ buildIconSvg = () => `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
347
+ <rect width="128" height="128" rx="30" fill="#172033"/>
348
+ <path d="M31 40h66v48H31z" fill="#fff" opacity=".12"/>
349
+ <path d="M43 52h42v8H43zm0 16h28v8H43z" fill="#fff"/>
350
+ <circle cx="88" cy="76" r="12" fill="#f59e0b"/>
351
+ </svg>
352
+ `;
353
+ };
354
+ //#endregion
355
+ export { AppRustWasiScaffoldTemplateService };
356
+
357
+ //# sourceMappingURL=app-rust-wasi-scaffold-template.service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app-rust-wasi-scaffold-template.service.js","names":[],"sources":["../../src/services/app-rust-wasi-scaffold-template.service.ts"],"sourcesContent":["import { readFileSync } from \"node:fs\";\nimport type { AppScaffoldFile } from \"./app-ts-http-scaffold-template.service.js\";\n\nconst RUST_WASI_GUEST_CRATE_NAME = \"nextclaw-rust-wasi-guest\";\nconst PORTABLE_SERVICE_WIT = readFileSync(\n new URL(\"../../resources/wit/portable-service.wit\", import.meta.url),\n \"utf8\",\n);\nconst RUST_WASI_CARGO_LOCK = readFileSync(\n new URL(\"../../resources/rust-wasi/Cargo.lock\", import.meta.url),\n \"utf8\",\n);\n\nexport class AppRustWasiScaffoldTemplateService {\n buildFiles = (params: { appId: string; appName: string }): AppScaffoldFile[] => {\n const { appId, appName } = params;\n const packageSlug = appId.replace(/[^a-z0-9]+/g, \"-\");\n const panelId = `${packageSlug}-panel`;\n const serviceId = `${packageSlug}-service`;\n return [\n {\n relativePath: \"manifest.json\",\n content: `${JSON.stringify(this.buildManifest(appId, appName, panelId, serviceId), null, 2)}\\n`,\n },\n {\n relativePath: \"marketplace.json\",\n content: `${JSON.stringify(this.buildMarketplaceMetadata(appName), null, 2)}\\n`,\n },\n { relativePath: \"README.md\", content: this.buildReadme(appName, serviceId) },\n {\n relativePath: `panels/${panelId}.panel/panel-app.json`,\n content: `${JSON.stringify(this.buildPanelManifest(panelId, appName, serviceId), null, 2)}\\n`,\n },\n { relativePath: `panels/${panelId}.panel/index.html`, content: this.buildPanelHtml(appName) },\n { relativePath: `panels/${panelId}.panel/app.js`, content: this.buildPanelScript(serviceId) },\n {\n relativePath: `service-components/${serviceId}/service-app.json`,\n content: `${JSON.stringify(this.buildServiceManifest(serviceId), null, 2)}\\n`,\n },\n { relativePath: \"guest/Cargo.toml\", content: this.buildCargoToml() },\n { relativePath: \"guest/Cargo.lock\", content: RUST_WASI_CARGO_LOCK },\n { relativePath: \"guest/src/lib.rs\", content: this.buildRustSource() },\n { relativePath: \"guest/wit/portable-service.wit\", content: PORTABLE_SERVICE_WIT },\n {\n relativePath: \"tests/service-smoke.json\",\n content: `${JSON.stringify(this.buildServiceSmokeFixture(serviceId), null, 2)}\\n`,\n },\n { relativePath: \"assets/icon.svg\", content: this.buildIconSvg() },\n ];\n };\n\n private buildManifest = (\n appId: string,\n appName: string,\n panelId: string,\n serviceId: string,\n ) => ({\n schemaVersion: 2,\n id: appId,\n name: appName,\n version: \"0.1.0\",\n description: `${appName},由 Rust/WASI Component 保存持久计数。`,\n icon: \"assets/icon.svg\",\n engines: { nextclaw: \">=0.45.4\" },\n presentation: { primaryPanel: panelId },\n runtime: { profile: \"wasi\" },\n distribution: { mode: \"universal\" },\n storage: { scope: \"global\", schemaVersion: 1 },\n permissions: { storage: { namespace: appId.replace(/\\./g, \"-\") } },\n components: [\n { kind: \"panel\", path: `panels/${panelId}.panel` },\n { kind: \"service\", path: `service-components/${serviceId}` },\n ],\n });\n\n private buildPanelManifest = (panelId: string, appName: string, serviceId: string) => ({\n id: panelId,\n title: appName,\n description: \"读取并增加由 Rust/WASI Component 持久保存的计数。\",\n icon: \"🦀\",\n entry: \"index.html\",\n actions: [`${serviceId}.counter_read`, `${serviceId}.counter_increment`],\n });\n\n private buildServiceManifest = (serviceId: string) => ({\n id: serviceId,\n title: \"Rust/WASI 持久计数组件\",\n description: \"通过宿主 KV 读取和增加持久计数。\",\n protocol: \"wasi-component\",\n component: { entry: \"service.wasm\" },\n actions: {\n counter_read: { risk: \"read\", title: \"读取持久计数\" },\n counter_increment: {\n risk: \"write\",\n title: \"增加持久计数\",\n inputSchema: {\n type: \"object\",\n properties: { step: { type: \"integer\", minimum: 1, maximum: 100 } },\n additionalProperties: false,\n },\n },\n },\n });\n\n private buildServiceSmokeFixture = (serviceId: string) => ({\n schemaVersion: 1,\n component: serviceId,\n resetData: true,\n steps: [\n {\n action: \"counter_increment\",\n input: { step: 3 },\n expect: { counter: 3, persistedBy: \"host.kv\" },\n },\n {\n action: \"counter_read\",\n input: {},\n expect: { counter: 3, persistedBy: \"host.kv\" },\n },\n ],\n });\n\n private buildCargoToml = (): string => `[package]\nname = \"${RUST_WASI_GUEST_CRATE_NAME}\"\nversion = \"0.1.0\"\nedition = \"2024\"\npublish = false\n\n[dependencies]\nserde_json = \"1.0\"\nwit-bindgen = \"0.44.0\"\n\n[lib]\ncrate-type = [\"cdylib\"]\n`;\n\n private buildRustSource = (): string => `wit_bindgen::generate!({\n path: \"wit\",\n world: \"service-app\",\n});\n\nuse exports::nextclaw::portable_service::service::{Action, Guest};\nuse nextclaw::portable_service::host;\nuse serde_json::{Value, json};\n\nstruct Component;\n\nimpl Guest for Component {\n fn list_actions() -> Vec<Action> {\n vec![\n Action {\n name: \"counter_read\".into(),\n title: \"读取持久计数\".into(),\n description: \"从宿主管理的 KV 存储读取计数。\".into(),\n },\n Action {\n name: \"counter_increment\".into(),\n title: \"增加持久计数\".into(),\n description: \"在 Rust/WASM 中计算,并通过宿主 KV 持久化。\".into(),\n },\n ]\n }\n\n fn invoke(action: String, input_json: String) -> Result<String, String> {\n host::log(host::LogLevel::Info, &format!(\"invoking {action}\"));\n let input: Value = serde_json::from_str(&input_json).unwrap_or_else(|_| json!({}));\n match action.as_str() {\n \"counter_read\" => Ok(counter_result(read_counter()?)),\n \"counter_increment\" => {\n let step = input.get(\"step\").and_then(Value::as_i64).unwrap_or(1);\n if !(1..=100).contains(&step) {\n return Err(\"INVALID_INPUT: step must be between 1 and 100\".into());\n }\n let counter = read_counter()?.saturating_add(step);\n host::kv_set(\"counter\", &counter.to_string())?;\n Ok(counter_result(counter))\n }\n _ => Err(format!(\"UNKNOWN_ACTION: {action}\")),\n }\n }\n\n fn start(_config_json: String) -> Result<String, String> {\n Ok(json!({ \"started\": true, \"mode\": \"action\" }).to_string())\n }\n\n fn handle_event(_event_json: String) -> Result<String, String> {\n Err(\"UNSUPPORTED_LIFECYCLE: action component does not accept resident events\".into())\n }\n\n fn stop(_reason_json: String) -> Result<String, String> {\n Ok(json!({ \"stopped\": true, \"mode\": \"action\" }).to_string())\n }\n}\n\nfn read_counter() -> Result<i64, String> {\n Ok(host::kv_get(\"counter\")?\n .and_then(|value| value.parse().ok())\n .unwrap_or(0))\n}\n\nfn counter_result(counter: i64) -> String {\n json!({ \"counter\": counter, \"persistedBy\": \"host.kv\" }).to_string()\n}\n\nexport!(Component with_types_in self);\n`;\n\n private buildReadme = (appName: string, serviceId: string): string => `# ${appName}\n\n这是一个可以独立构建的 Rust/WASI Component App。Panel 和 Agent 调用同一组 Service Action,计数通过 NextClaw 宿主 KV 持久保存。\n\n## 1. 准备 Rust 工具链\n\n\\`\\`\\`bash\nrustup target add wasm32-wasip2\n\\`\\`\\`\n\n## 2. 构建 Component\n\n\\`\\`\\`bash\ncd guest\ncargo build --release --target wasm32-wasip2\ncp target/wasm32-wasip2/release/nextclaw_rust_wasi_guest.wasm ../service-components/${serviceId}/service.wasm\ncd ..\n\\`\\`\\`\n\n项目内的 \\`guest/wit/portable-service.wit\\` 是当前 Service Action 与宿主能力合同;不需要 NextClaw 源码仓库。\n\n## 3. 检查和调试\n\n\\`\\`\\`bash\nnextclaw app check .\nnextclaw app dev .\nnextclaw app call . counter_read\nnextclaw app call . counter_increment --input '{\"step\":2}'\n\\`\\`\\`\n\n如果一个包包含多个 Service Component,请增加 \\`--component <component-id>\\`。\n\n## 4. 打包和安装\n\n\\`\\`\\`bash\nnextclaw app pack . --target universal --out ${this.normalizeSlug(appName)}.napp\nnextclaw app install ./${this.normalizeSlug(appName)}.napp\n\\`\\`\\`\n`;\n\n private buildPanelHtml = (appName: string): string => `<!doctype html>\n<html lang=\"zh-CN\">\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <title>${appName}</title>\n <style>\n :root { font-family: ui-sans-serif, system-ui, sans-serif; color: #172033; background: #f5f7fb; }\n body { margin: 0; min-height: 100vh; display: grid; place-items: center; }\n main { width: min(420px, calc(100vw - 40px)); padding: 28px; border-radius: 24px; background: white; box-shadow: 0 18px 60px #24324a1f; }\n p { color: #667085; }\n output { display: block; margin: 24px 0; font-size: 64px; font-weight: 700; }\n button { border: 0; border-radius: 999px; padding: 12px 18px; background: #172033; color: white; cursor: pointer; }\n #error { color: #b42318; }\n </style>\n </head>\n <body>\n <main>\n <h1>${appName}</h1>\n <p>这个数字由 Rust/WASI Component 计算,并通过宿主 KV 持久保存。</p>\n <output id=\"counter\">…</output>\n <button id=\"increment\" type=\"button\">增加 1</button>\n <p id=\"error\" role=\"alert\"></p>\n </main>\n <script src=\"app.js\"></script>\n </body>\n</html>\n`;\n\n private buildPanelScript = (serviceId: string): string => `const counter = document.querySelector(\"#counter\");\nconst error = document.querySelector(\"#error\");\nconst increment = document.querySelector(\"#increment\");\n\nasync function readCounter() {\n error.textContent = \"\";\n try {\n const result = await window.nextclaw.serviceActions.invoke(\"${serviceId}.counter_read\", {});\n counter.textContent = String(result.counter ?? 0);\n } catch (cause) {\n error.textContent = cause instanceof Error ? cause.message : String(cause);\n }\n}\n\nasync function incrementCounter() {\n error.textContent = \"\";\n try {\n const result = await window.nextclaw.serviceActions.invoke(\"${serviceId}.counter_increment\", { step: 1 });\n counter.textContent = String(result.counter ?? 0);\n } catch (cause) {\n error.textContent = cause instanceof Error ? cause.message : String(cause);\n }\n}\n\nincrement.addEventListener(\"click\", incrementCounter);\nvoid readCounter();\n`;\n\n private buildMarketplaceMetadata = (appName: string) => ({\n slug: this.normalizeSlug(appName),\n summary: `${appName} Rust/WASI Component 示例。`,\n summaryI18n: {\n zh: `${appName} Rust/WASI Component 示例。`,\n en: `${appName}, a Rust/WASI Component example.`,\n },\n description: \"A minimal Rust/WASI Component App with a Panel and host-managed persistent KV.\",\n descriptionI18n: {\n zh: \"一个包含 Panel 和宿主持久 KV 的最小 Rust/WASI Component App。\",\n en: \"A minimal Rust/WASI Component App with a Panel and host-managed persistent KV.\",\n },\n author: \"NextClaw\",\n tags: [\"starter\", \"rust\", \"wasi-component\", \"official\"],\n sourceRepo: \"https://github.com/Peiiii/nextclaw\",\n homepage: \"https://nextclaw.io\",\n featured: false,\n });\n\n private normalizeSlug = (value: string): string => value\n .trim()\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\") || \"rust-wasi-app\";\n\n private buildIconSvg = (): string => `<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 128 128\">\n <rect width=\"128\" height=\"128\" rx=\"30\" fill=\"#172033\"/>\n <path d=\"M31 40h66v48H31z\" fill=\"#fff\" opacity=\".12\"/>\n <path d=\"M43 52h42v8H43zm0 16h28v8H43z\" fill=\"#fff\"/>\n <circle cx=\"88\" cy=\"76\" r=\"12\" fill=\"#f59e0b\"/>\n</svg>\n`;\n}\n"],"mappings":"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"}
@@ -1,8 +1,9 @@
1
1
  import { AppTsHttpScaffoldTemplateService } from "./app-ts-http-scaffold-template.service.js";
2
2
  import { AppTsHttpLiteScaffoldTemplateService } from "./app-ts-http-lite-scaffold-template.service.js";
3
+ import { AppRustWasiScaffoldTemplateService } from "./app-rust-wasi-scaffold-template.service.js";
3
4
 
4
5
  //#region src/services/app-scaffold.service.d.ts
5
- type AppScaffoldTemplate = "starter" | "ts-http" | "ts-http-lite";
6
+ type AppScaffoldTemplate = "starter" | "ts-http" | "ts-http-lite" | "rust-wasi";
6
7
  type AppScaffoldResult = {
7
8
  appDirectory: string;
8
9
  manifestPath: string;
@@ -11,7 +12,8 @@ type AppScaffoldResult = {
11
12
  declare class AppScaffoldService {
12
13
  private readonly tsHttpTemplateService;
13
14
  private readonly tsHttpLiteTemplateService;
14
- constructor(tsHttpTemplateService?: AppTsHttpScaffoldTemplateService, tsHttpLiteTemplateService?: AppTsHttpLiteScaffoldTemplateService);
15
+ private readonly rustWasiTemplateService;
16
+ constructor(tsHttpTemplateService?: AppTsHttpScaffoldTemplateService, tsHttpLiteTemplateService?: AppTsHttpLiteScaffoldTemplateService, rustWasiTemplateService?: AppRustWasiScaffoldTemplateService);
15
17
  scaffold: (targetDirectory: string, options?: {
16
18
  template?: AppScaffoldTemplate;
17
19
  }) => Promise<AppScaffoldResult>;
@@ -1 +1 @@
1
- {"version":3,"file":"app-scaffold.service.d.ts","names":[],"sources":["../../src/services/app-scaffold.service.ts"],"mappings":";;;;KAKY,mBAAA;AAAA,KAEA,iBAAA;EACV,YAAA;EACA,YAAA;EACA,QAAA,EAAU,mBAAA;AAAA;AAAA,cAGC,kBAAA;EAAA,iBAEQ,qBAAA;EAAA,iBACA,yBAAA;cADA,qBAAA,GAAuB,gCAAA,EACvB,yBAAA,GAA2B,oCAAA;EAG9C,QAAA,GACE,eAAA,UACA,OAAA;IACE,QAAA,GAAW,mBAAA;EAAA,MAEZ,OAAA,CAAQ,iBAAA;EAAA,QAyEH,kBAAA;EAAA,QAaA,wBAAA;EAAA,QASA,UAAA;EAAA,QAKA,YAAA;EAAA,QAKA,aAAA;EAAA,QAYA,aAAA;EAAA,QA4BA,cAAA;EAAA,QAUA,WAAA;EAAA,QAyHA,wBAAA;EAAA,QAqBA,WAAA;EAAA,QAwBA,iBAAA;EAAA,QAqDA,YAAA;AAAA"}
1
+ {"version":3,"file":"app-scaffold.service.d.ts","names":[],"sources":["../../src/services/app-scaffold.service.ts"],"mappings":";;;;;KAMY,mBAAA;AAAA,KAEA,iBAAA;EACV,YAAA;EACA,YAAA;EACA,QAAA,EAAU,mBAAA;AAAA;AAAA,cAGC,kBAAA;EAAA,iBAEQ,qBAAA;EAAA,iBACA,yBAAA;EAAA,iBACA,uBAAA;cAFA,qBAAA,GAAuB,gCAAA,EACvB,yBAAA,GAA2B,oCAAA,EAC3B,uBAAA,GAAyB,kCAAA;EAG5C,QAAA,GACE,eAAA,UACA,OAAA;IACE,QAAA,GAAW,mBAAA;EAAA,MAEZ,OAAA,CAAQ,iBAAA;EAAA,QAmFH,kBAAA;EAAA,QAaA,wBAAA;EAAA,QASA,UAAA;EAAA,QAKA,YAAA;EAAA,QAKA,aAAA;EAAA,QAYA,aAAA;EAAA,QA4BA,cAAA;EAAA,QAUA,WAAA;EAAA,QAyHA,wBAAA;EAAA,QAqBA,WAAA;EAAA,QAwBA,iBAAA;EAAA,QAqDA,YAAA;AAAA"}
@@ -1,23 +1,34 @@
1
1
  import { AppTsHttpScaffoldTemplateService } from "./app-ts-http-scaffold-template.service.js";
2
2
  import { AppTsHttpLiteScaffoldTemplateService } from "./app-ts-http-lite-scaffold-template.service.js";
3
+ import { AppRustWasiScaffoldTemplateService } from "./app-rust-wasi-scaffold-template.service.js";
3
4
  import { access, mkdir, writeFile } from "node:fs/promises";
4
5
  import path from "node:path";
5
6
  //#region src/services/app-scaffold.service.ts
6
7
  var AppScaffoldService = class {
7
- constructor(tsHttpTemplateService = new AppTsHttpScaffoldTemplateService(), tsHttpLiteTemplateService = new AppTsHttpLiteScaffoldTemplateService()) {
8
+ constructor(tsHttpTemplateService = new AppTsHttpScaffoldTemplateService(), tsHttpLiteTemplateService = new AppTsHttpLiteScaffoldTemplateService(), rustWasiTemplateService = new AppRustWasiScaffoldTemplateService()) {
8
9
  this.tsHttpTemplateService = tsHttpTemplateService;
9
10
  this.tsHttpLiteTemplateService = tsHttpLiteTemplateService;
11
+ this.rustWasiTemplateService = rustWasiTemplateService;
10
12
  }
11
13
  scaffold = async (targetDirectory, options) => {
12
14
  const appDirectory = path.resolve(targetDirectory);
13
15
  const template = options?.template ?? "starter";
14
16
  await this.assertTargetDoesNotExist(appDirectory);
15
- await mkdir(path.join(appDirectory, "main"), { recursive: true });
16
- await mkdir(path.join(appDirectory, "ui"), { recursive: true });
17
- await mkdir(path.join(appDirectory, "assets"), { recursive: true });
17
+ await mkdir(appDirectory, { recursive: true });
18
18
  const appName = this.buildAppName(appDirectory);
19
19
  const appId = this.buildAppId(appDirectory);
20
20
  const manifestPath = path.join(appDirectory, "manifest.json");
21
+ if (template === "rust-wasi") {
22
+ await this.writeTemplateFiles(appDirectory, this.rustWasiTemplateService.buildFiles({
23
+ appId,
24
+ appName
25
+ }));
26
+ return {
27
+ appDirectory,
28
+ manifestPath,
29
+ template
30
+ };
31
+ }
21
32
  if (template === "ts-http") {
22
33
  await this.writeTemplateFiles(appDirectory, this.tsHttpTemplateService.buildFiles({
23
34
  appId,
@@ -40,6 +51,11 @@ var AppScaffoldService = class {
40
51
  template
41
52
  };
42
53
  }
54
+ await Promise.all([
55
+ mkdir(path.join(appDirectory, "main"), { recursive: true }),
56
+ mkdir(path.join(appDirectory, "ui"), { recursive: true }),
57
+ mkdir(path.join(appDirectory, "assets"), { recursive: true })
58
+ ]);
43
59
  await Promise.all([
44
60
  writeFile(manifestPath, `${JSON.stringify(this.buildManifest(appId, appName), null, 2)}\n`, "utf-8"),
45
61
  writeFile(path.join(appDirectory, "marketplace.json"), `${JSON.stringify(this.buildMarketplaceMetadata(appName), null, 2)}\n`, "utf-8"),
@@ -1 +1 @@
1
- {"version":3,"file":"app-scaffold.service.js","names":[],"sources":["../../src/services/app-scaffold.service.ts"],"sourcesContent":["import { access, mkdir, writeFile } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { AppTsHttpLiteScaffoldTemplateService } from \"./app-ts-http-lite-scaffold-template.service.js\";\nimport { AppTsHttpScaffoldTemplateService } from \"./app-ts-http-scaffold-template.service.js\";\n\nexport type AppScaffoldTemplate = \"starter\" | \"ts-http\" | \"ts-http-lite\";\n\nexport type AppScaffoldResult = {\n appDirectory: string;\n manifestPath: string;\n template: AppScaffoldTemplate;\n};\n\nexport class AppScaffoldService {\n constructor(\n private readonly tsHttpTemplateService: AppTsHttpScaffoldTemplateService = new AppTsHttpScaffoldTemplateService(),\n private readonly tsHttpLiteTemplateService: AppTsHttpLiteScaffoldTemplateService = new AppTsHttpLiteScaffoldTemplateService(),\n ) {}\n\n scaffold = async (\n targetDirectory: string,\n options?: {\n template?: AppScaffoldTemplate;\n },\n ): Promise<AppScaffoldResult> => {\n const appDirectory = path.resolve(targetDirectory);\n const template = options?.template ?? \"starter\";\n await this.assertTargetDoesNotExist(appDirectory);\n await mkdir(path.join(appDirectory, \"main\"), { recursive: true });\n await mkdir(path.join(appDirectory, \"ui\"), { recursive: true });\n await mkdir(path.join(appDirectory, \"assets\"), { recursive: true });\n\n const appName = this.buildAppName(appDirectory);\n const appId = this.buildAppId(appDirectory);\n const manifestPath = path.join(appDirectory, \"manifest.json\");\n if (template === \"ts-http\") {\n await this.writeTemplateFiles(\n appDirectory,\n this.tsHttpTemplateService.buildFiles({\n appId,\n appName,\n }),\n );\n return {\n appDirectory,\n manifestPath,\n template,\n };\n }\n if (template === \"ts-http-lite\") {\n await this.writeTemplateFiles(\n appDirectory,\n this.tsHttpLiteTemplateService.buildFiles({\n appId,\n appName,\n }),\n );\n return {\n appDirectory,\n manifestPath,\n template,\n };\n }\n\n await Promise.all([\n writeFile(\n manifestPath,\n `${JSON.stringify(this.buildManifest(appId, appName), null, 2)}\\n`,\n \"utf-8\",\n ),\n writeFile(\n path.join(appDirectory, \"marketplace.json\"),\n `${JSON.stringify(this.buildMarketplaceMetadata(appName), null, 2)}\\n`,\n \"utf-8\",\n ),\n writeFile(path.join(appDirectory, \"README.md\"), this.buildReadme(appName, appId), \"utf-8\"),\n writeFile(\n path.join(appDirectory, \"main\", \"app.wasm\"),\n Buffer.from(APP_WASM_BASE64, \"base64\"),\n ),\n writeFile(path.join(appDirectory, \"main\", \"app.wat\"), this.buildWatSource(), \"utf-8\"),\n writeFile(path.join(appDirectory, \"ui\", \"index.html\"), this.buildUiHtml(appName), \"utf-8\"),\n writeFile(\n path.join(appDirectory, \"ui\", \"app.controller.js\"),\n this.buildUiController(),\n \"utf-8\",\n ),\n writeFile(path.join(appDirectory, \"assets\", \"icon.svg\"), this.buildIconSvg(), \"utf-8\"),\n ]);\n\n return {\n appDirectory,\n manifestPath,\n template,\n };\n };\n\n private writeTemplateFiles = async (\n appDirectory: string,\n files: Array<{ relativePath: string; content: string | Buffer }>,\n ): Promise<void> => {\n await Promise.all(\n files.map(async (file) => {\n const filePath = path.join(appDirectory, file.relativePath);\n await mkdir(path.dirname(filePath), { recursive: true });\n await writeFile(filePath, file.content);\n }),\n );\n };\n\n private assertTargetDoesNotExist = async (targetDirectory: string): Promise<void> => {\n try {\n await access(targetDirectory);\n } catch {\n return;\n }\n throw new Error(`目标目录已存在:${targetDirectory}`);\n };\n\n private buildAppId = (targetDirectory: string): string => {\n const slug = this.normalizeSlug(path.basename(targetDirectory));\n return `nextclaw.${slug}`;\n };\n\n private buildAppName = (targetDirectory: string): string => {\n const parts = this.normalizeSlug(path.basename(targetDirectory)).split(\"-\");\n return parts.map((part) => `${part[0]?.toUpperCase() ?? \"\"}${part.slice(1)}`).join(\" \");\n };\n\n private normalizeSlug = (value: string): string => {\n const normalized = value\n .trim()\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\");\n if (!normalized) {\n throw new Error(\"应用目录名无法生成合法的应用标识。\");\n }\n return normalized;\n };\n\n private buildManifest = (appId: string, appName: string) => {\n return {\n schemaVersion: 1,\n id: appId,\n name: appName,\n version: \"0.1.0\",\n description: \"A minimal NextClaw micro app scaffold created by napp.\",\n icon: \"assets/icon.svg\",\n main: {\n kind: \"wasm\",\n entry: \"main/app.wasm\",\n export: \"summarize_notes\",\n action: \"runStarterDemo\",\n },\n ui: {\n entry: \"ui/index.html\",\n },\n permissions: {\n storage: {\n namespace: appId.replace(/\\./g, \"-\"),\n },\n capabilities: {\n hostBridge: true,\n },\n },\n };\n };\n\n private buildWatSource = (): string => {\n return `(module\n (func (export \"summarize_notes\") (param i32 i32) (result i32)\n local.get 0\n local.get 1\n i32.add\n i32.const 200\n i32.add))\\n`;\n };\n\n private buildUiHtml = (appName: string): string => {\n return `<!doctype html>\n<html lang=\"zh-CN\">\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <title>${appName}</title>\n <style>\n :root {\n color-scheme: light;\n font-family: \"IBM Plex Sans\", \"Helvetica Neue\", sans-serif;\n background:\n radial-gradient(circle at top left, rgba(255, 206, 134, 0.24), transparent 30%),\n linear-gradient(165deg, #f7f1e7 0%, #fcfcfe 48%, #e1ebf5 100%);\n color: #11233a;\n }\n\n body {\n margin: 0;\n min-height: 100vh;\n }\n\n main {\n box-sizing: border-box;\n max-width: 760px;\n margin: 0 auto;\n padding: 48px 20px 72px;\n }\n\n .panel {\n background: rgba(255, 255, 255, 0.78);\n border: 1px solid rgba(17, 35, 58, 0.08);\n border-radius: 24px;\n box-shadow: 0 18px 42px rgba(17, 35, 58, 0.12);\n padding: 28px;\n backdrop-filter: blur(18px);\n }\n\n h1 {\n margin: 0 0 12px;\n font-size: clamp(30px, 6vw, 54px);\n line-height: 1;\n }\n\n p {\n line-height: 1.6;\n }\n\n button {\n border: 0;\n border-radius: 999px;\n background: #11233a;\n color: #fff;\n padding: 12px 18px;\n font-size: 15px;\n cursor: pointer;\n }\n\n dl {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));\n gap: 12px;\n margin: 24px 0 0;\n }\n\n dt {\n font-size: 12px;\n color: #556881;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n }\n\n dd {\n margin: 6px 0 0;\n font-size: 24px;\n font-weight: 600;\n }\n\n pre {\n margin-top: 20px;\n padding: 14px;\n border-radius: 16px;\n background: rgba(17, 35, 58, 0.07);\n overflow: auto;\n }\n </style>\n </head>\n <body>\n <main>\n <section class=\"panel\">\n <p>NextClaw Micro App</p>\n <h1 id=\"title\">Loading...</h1>\n <p id=\"description\">A starter scaffold created by napp create.</p>\n <button id=\"run-button\" type=\"button\">Run Starter Demo</button>\n <dl>\n <div>\n <dt>Action</dt>\n <dd id=\"action\">-</dd>\n </div>\n <div>\n <dt>Documents</dt>\n <dd id=\"document-count\">-</dd>\n </div>\n <div>\n <dt>Text Bytes</dt>\n <dd id=\"text-bytes\">-</dd>\n </div>\n <div>\n <dt>Wasm Score</dt>\n <dd id=\"wasm-score\">-</dd>\n </div>\n </dl>\n <pre id=\"details\"></pre>\n </section>\n </main>\n <script type=\"module\" src=\"./app.controller.js\"></script>\n </body>\n</html>\n`;\n };\n\n private buildMarketplaceMetadata = (appName: string) => {\n return {\n slug: this.normalizeSlug(appName),\n summary: `A minimal ${appName} app scaffold created by napp.`,\n summaryI18n: {\n en: `A minimal ${appName} app scaffold created by napp.`,\n zh: `一个由 napp 创建的最小 ${appName} 应用骨架。`,\n },\n description: `A minimal NextClaw app scaffold for ${appName}, ready for local run and marketplace publish.`,\n descriptionI18n: {\n en: `A minimal NextClaw app scaffold for ${appName}, ready for local run and marketplace publish.`,\n zh: `一个面向 ${appName} 的最小 NextClaw 应用骨架,可直接本地运行并发布到 marketplace。`,\n },\n author: \"NextClaw\",\n tags: [\"starter\", \"official\", \"local\"],\n sourceRepo: \"https://github.com/Peiiii/nextclaw\",\n homepage: \"https://nextclaw.io\",\n featured: false,\n };\n };\n\n private buildReadme = (appName: string, appId: string): string => {\n return `# ${appName}\n\nThis starter app was created by \\`napp create\\`.\n\n## Local workflow\n\n\\`\\`\\`bash\nnapp inspect .\nnapp run .\n\\`\\`\\`\n\n## Publish workflow\n\n\\`\\`\\`bash\nnapp publish .\n\\`\\`\\`\n\n## App ID\n\n\\`${appId}\\`\n`;\n };\n\n private buildUiController = (): string => {\n return `const titleNode = document.getElementById(\"title\");\nconst descriptionNode = document.getElementById(\"description\");\nconst actionNode = document.getElementById(\"action\");\nconst documentCountNode = document.getElementById(\"document-count\");\nconst textBytesNode = document.getElementById(\"text-bytes\");\nconst wasmScoreNode = document.getElementById(\"wasm-score\");\nconst detailsNode = document.getElementById(\"details\");\nconst runButton = document.getElementById(\"run-button\");\n\nconst setDetails = (value) => {\n detailsNode.textContent = JSON.stringify(value, null, 2);\n};\n\nconst loadManifest = async () => {\n const response = await fetch(\"/__napp/manifest\");\n const payload = await response.json();\n titleNode.textContent = payload.manifest.name;\n descriptionNode.textContent = payload.manifest.description;\n actionNode.textContent = payload.manifest.main.action;\n setDetails(payload);\n};\n\nconst runDemo = async () => {\n runButton.disabled = true;\n try {\n const response = await fetch(\"/__napp/run\", {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n },\n body: JSON.stringify({\n action: actionNode.textContent,\n }),\n });\n const payload = await response.json();\n documentCountNode.textContent = String(payload.result.input.documentCount);\n textBytesNode.textContent = String(payload.result.input.textBytes);\n wasmScoreNode.textContent = String(payload.result.output.output);\n setDetails(payload);\n } finally {\n runButton.disabled = false;\n }\n};\n\nrunButton.addEventListener(\"click\", () => {\n void runDemo();\n});\n\nawait loadManifest();\n`;\n };\n\n private buildIconSvg = (): string => {\n return `<svg width=\"160\" height=\"160\" viewBox=\"0 0 160 160\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n <rect width=\"160\" height=\"160\" rx=\"40\" fill=\"#11233A\" />\n <path d=\"M42 45H118V58H74V115H58V58H42V45Z\" fill=\"#F8C97E\" />\n <path d=\"M92 74H118V87H92V74Z\" fill=\"#FFFFFF\" fill-opacity=\"0.92\" />\n <path d=\"M92 95H118V108H92V95Z\" fill=\"#FFFFFF\" fill-opacity=\"0.72\" />\n</svg>\n`;\n };\n}\n\nconst APP_WASM_BASE64 =\n \"AGFzbQEAAAABBwFgAn9/AX8DAgEABxMBD3N1bW1hcml6ZV9ub3RlcwAACg0BCwAgACABakHIAWoL\";\n"],"mappings":"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"}
1
+ {"version":3,"file":"app-scaffold.service.js","names":[],"sources":["../../src/services/app-scaffold.service.ts"],"sourcesContent":["import { access, mkdir, writeFile } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { AppTsHttpLiteScaffoldTemplateService } from \"./app-ts-http-lite-scaffold-template.service.js\";\nimport { AppTsHttpScaffoldTemplateService } from \"./app-ts-http-scaffold-template.service.js\";\nimport { AppRustWasiScaffoldTemplateService } from \"./app-rust-wasi-scaffold-template.service.js\";\n\nexport type AppScaffoldTemplate = \"starter\" | \"ts-http\" | \"ts-http-lite\" | \"rust-wasi\";\n\nexport type AppScaffoldResult = {\n appDirectory: string;\n manifestPath: string;\n template: AppScaffoldTemplate;\n};\n\nexport class AppScaffoldService {\n constructor(\n private readonly tsHttpTemplateService: AppTsHttpScaffoldTemplateService = new AppTsHttpScaffoldTemplateService(),\n private readonly tsHttpLiteTemplateService: AppTsHttpLiteScaffoldTemplateService = new AppTsHttpLiteScaffoldTemplateService(),\n private readonly rustWasiTemplateService: AppRustWasiScaffoldTemplateService = new AppRustWasiScaffoldTemplateService(),\n ) {}\n\n scaffold = async (\n targetDirectory: string,\n options?: {\n template?: AppScaffoldTemplate;\n },\n ): Promise<AppScaffoldResult> => {\n const appDirectory = path.resolve(targetDirectory);\n const template = options?.template ?? \"starter\";\n await this.assertTargetDoesNotExist(appDirectory);\n await mkdir(appDirectory, { recursive: true });\n\n const appName = this.buildAppName(appDirectory);\n const appId = this.buildAppId(appDirectory);\n const manifestPath = path.join(appDirectory, \"manifest.json\");\n if (template === \"rust-wasi\") {\n await this.writeTemplateFiles(\n appDirectory,\n this.rustWasiTemplateService.buildFiles({ appId, appName }),\n );\n return { appDirectory, manifestPath, template };\n }\n if (template === \"ts-http\") {\n await this.writeTemplateFiles(\n appDirectory,\n this.tsHttpTemplateService.buildFiles({\n appId,\n appName,\n }),\n );\n return {\n appDirectory,\n manifestPath,\n template,\n };\n }\n if (template === \"ts-http-lite\") {\n await this.writeTemplateFiles(\n appDirectory,\n this.tsHttpLiteTemplateService.buildFiles({\n appId,\n appName,\n }),\n );\n return {\n appDirectory,\n manifestPath,\n template,\n };\n }\n\n await Promise.all([\n mkdir(path.join(appDirectory, \"main\"), { recursive: true }),\n mkdir(path.join(appDirectory, \"ui\"), { recursive: true }),\n mkdir(path.join(appDirectory, \"assets\"), { recursive: true }),\n ]);\n await Promise.all([\n writeFile(\n manifestPath,\n `${JSON.stringify(this.buildManifest(appId, appName), null, 2)}\\n`,\n \"utf-8\",\n ),\n writeFile(\n path.join(appDirectory, \"marketplace.json\"),\n `${JSON.stringify(this.buildMarketplaceMetadata(appName), null, 2)}\\n`,\n \"utf-8\",\n ),\n writeFile(path.join(appDirectory, \"README.md\"), this.buildReadme(appName, appId), \"utf-8\"),\n writeFile(\n path.join(appDirectory, \"main\", \"app.wasm\"),\n Buffer.from(APP_WASM_BASE64, \"base64\"),\n ),\n writeFile(path.join(appDirectory, \"main\", \"app.wat\"), this.buildWatSource(), \"utf-8\"),\n writeFile(path.join(appDirectory, \"ui\", \"index.html\"), this.buildUiHtml(appName), \"utf-8\"),\n writeFile(\n path.join(appDirectory, \"ui\", \"app.controller.js\"),\n this.buildUiController(),\n \"utf-8\",\n ),\n writeFile(path.join(appDirectory, \"assets\", \"icon.svg\"), this.buildIconSvg(), \"utf-8\"),\n ]);\n\n return {\n appDirectory,\n manifestPath,\n template,\n };\n };\n\n private writeTemplateFiles = async (\n appDirectory: string,\n files: Array<{ relativePath: string; content: string | Buffer }>,\n ): Promise<void> => {\n await Promise.all(\n files.map(async (file) => {\n const filePath = path.join(appDirectory, file.relativePath);\n await mkdir(path.dirname(filePath), { recursive: true });\n await writeFile(filePath, file.content);\n }),\n );\n };\n\n private assertTargetDoesNotExist = async (targetDirectory: string): Promise<void> => {\n try {\n await access(targetDirectory);\n } catch {\n return;\n }\n throw new Error(`目标目录已存在:${targetDirectory}`);\n };\n\n private buildAppId = (targetDirectory: string): string => {\n const slug = this.normalizeSlug(path.basename(targetDirectory));\n return `nextclaw.${slug}`;\n };\n\n private buildAppName = (targetDirectory: string): string => {\n const parts = this.normalizeSlug(path.basename(targetDirectory)).split(\"-\");\n return parts.map((part) => `${part[0]?.toUpperCase() ?? \"\"}${part.slice(1)}`).join(\" \");\n };\n\n private normalizeSlug = (value: string): string => {\n const normalized = value\n .trim()\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\");\n if (!normalized) {\n throw new Error(\"应用目录名无法生成合法的应用标识。\");\n }\n return normalized;\n };\n\n private buildManifest = (appId: string, appName: string) => {\n return {\n schemaVersion: 1,\n id: appId,\n name: appName,\n version: \"0.1.0\",\n description: \"A minimal NextClaw micro app scaffold created by napp.\",\n icon: \"assets/icon.svg\",\n main: {\n kind: \"wasm\",\n entry: \"main/app.wasm\",\n export: \"summarize_notes\",\n action: \"runStarterDemo\",\n },\n ui: {\n entry: \"ui/index.html\",\n },\n permissions: {\n storage: {\n namespace: appId.replace(/\\./g, \"-\"),\n },\n capabilities: {\n hostBridge: true,\n },\n },\n };\n };\n\n private buildWatSource = (): string => {\n return `(module\n (func (export \"summarize_notes\") (param i32 i32) (result i32)\n local.get 0\n local.get 1\n i32.add\n i32.const 200\n i32.add))\\n`;\n };\n\n private buildUiHtml = (appName: string): string => {\n return `<!doctype html>\n<html lang=\"zh-CN\">\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <title>${appName}</title>\n <style>\n :root {\n color-scheme: light;\n font-family: \"IBM Plex Sans\", \"Helvetica Neue\", sans-serif;\n background:\n radial-gradient(circle at top left, rgba(255, 206, 134, 0.24), transparent 30%),\n linear-gradient(165deg, #f7f1e7 0%, #fcfcfe 48%, #e1ebf5 100%);\n color: #11233a;\n }\n\n body {\n margin: 0;\n min-height: 100vh;\n }\n\n main {\n box-sizing: border-box;\n max-width: 760px;\n margin: 0 auto;\n padding: 48px 20px 72px;\n }\n\n .panel {\n background: rgba(255, 255, 255, 0.78);\n border: 1px solid rgba(17, 35, 58, 0.08);\n border-radius: 24px;\n box-shadow: 0 18px 42px rgba(17, 35, 58, 0.12);\n padding: 28px;\n backdrop-filter: blur(18px);\n }\n\n h1 {\n margin: 0 0 12px;\n font-size: clamp(30px, 6vw, 54px);\n line-height: 1;\n }\n\n p {\n line-height: 1.6;\n }\n\n button {\n border: 0;\n border-radius: 999px;\n background: #11233a;\n color: #fff;\n padding: 12px 18px;\n font-size: 15px;\n cursor: pointer;\n }\n\n dl {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));\n gap: 12px;\n margin: 24px 0 0;\n }\n\n dt {\n font-size: 12px;\n color: #556881;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n }\n\n dd {\n margin: 6px 0 0;\n font-size: 24px;\n font-weight: 600;\n }\n\n pre {\n margin-top: 20px;\n padding: 14px;\n border-radius: 16px;\n background: rgba(17, 35, 58, 0.07);\n overflow: auto;\n }\n </style>\n </head>\n <body>\n <main>\n <section class=\"panel\">\n <p>NextClaw Micro App</p>\n <h1 id=\"title\">Loading...</h1>\n <p id=\"description\">A starter scaffold created by napp create.</p>\n <button id=\"run-button\" type=\"button\">Run Starter Demo</button>\n <dl>\n <div>\n <dt>Action</dt>\n <dd id=\"action\">-</dd>\n </div>\n <div>\n <dt>Documents</dt>\n <dd id=\"document-count\">-</dd>\n </div>\n <div>\n <dt>Text Bytes</dt>\n <dd id=\"text-bytes\">-</dd>\n </div>\n <div>\n <dt>Wasm Score</dt>\n <dd id=\"wasm-score\">-</dd>\n </div>\n </dl>\n <pre id=\"details\"></pre>\n </section>\n </main>\n <script type=\"module\" src=\"./app.controller.js\"></script>\n </body>\n</html>\n`;\n };\n\n private buildMarketplaceMetadata = (appName: string) => {\n return {\n slug: this.normalizeSlug(appName),\n summary: `A minimal ${appName} app scaffold created by napp.`,\n summaryI18n: {\n en: `A minimal ${appName} app scaffold created by napp.`,\n zh: `一个由 napp 创建的最小 ${appName} 应用骨架。`,\n },\n description: `A minimal NextClaw app scaffold for ${appName}, ready for local run and marketplace publish.`,\n descriptionI18n: {\n en: `A minimal NextClaw app scaffold for ${appName}, ready for local run and marketplace publish.`,\n zh: `一个面向 ${appName} 的最小 NextClaw 应用骨架,可直接本地运行并发布到 marketplace。`,\n },\n author: \"NextClaw\",\n tags: [\"starter\", \"official\", \"local\"],\n sourceRepo: \"https://github.com/Peiiii/nextclaw\",\n homepage: \"https://nextclaw.io\",\n featured: false,\n };\n };\n\n private buildReadme = (appName: string, appId: string): string => {\n return `# ${appName}\n\nThis starter app was created by \\`napp create\\`.\n\n## Local workflow\n\n\\`\\`\\`bash\nnapp inspect .\nnapp run .\n\\`\\`\\`\n\n## Publish workflow\n\n\\`\\`\\`bash\nnapp publish .\n\\`\\`\\`\n\n## App ID\n\n\\`${appId}\\`\n`;\n };\n\n private buildUiController = (): string => {\n return `const titleNode = document.getElementById(\"title\");\nconst descriptionNode = document.getElementById(\"description\");\nconst actionNode = document.getElementById(\"action\");\nconst documentCountNode = document.getElementById(\"document-count\");\nconst textBytesNode = document.getElementById(\"text-bytes\");\nconst wasmScoreNode = document.getElementById(\"wasm-score\");\nconst detailsNode = document.getElementById(\"details\");\nconst runButton = document.getElementById(\"run-button\");\n\nconst setDetails = (value) => {\n detailsNode.textContent = JSON.stringify(value, null, 2);\n};\n\nconst loadManifest = async () => {\n const response = await fetch(\"/__napp/manifest\");\n const payload = await response.json();\n titleNode.textContent = payload.manifest.name;\n descriptionNode.textContent = payload.manifest.description;\n actionNode.textContent = payload.manifest.main.action;\n setDetails(payload);\n};\n\nconst runDemo = async () => {\n runButton.disabled = true;\n try {\n const response = await fetch(\"/__napp/run\", {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n },\n body: JSON.stringify({\n action: actionNode.textContent,\n }),\n });\n const payload = await response.json();\n documentCountNode.textContent = String(payload.result.input.documentCount);\n textBytesNode.textContent = String(payload.result.input.textBytes);\n wasmScoreNode.textContent = String(payload.result.output.output);\n setDetails(payload);\n } finally {\n runButton.disabled = false;\n }\n};\n\nrunButton.addEventListener(\"click\", () => {\n void runDemo();\n});\n\nawait loadManifest();\n`;\n };\n\n private buildIconSvg = (): string => {\n return `<svg width=\"160\" height=\"160\" viewBox=\"0 0 160 160\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n <rect width=\"160\" height=\"160\" rx=\"40\" fill=\"#11233A\" />\n <path d=\"M42 45H118V58H74V115H58V58H42V45Z\" fill=\"#F8C97E\" />\n <path d=\"M92 74H118V87H92V74Z\" fill=\"#FFFFFF\" fill-opacity=\"0.92\" />\n <path d=\"M92 95H118V108H92V95Z\" fill=\"#FFFFFF\" fill-opacity=\"0.72\" />\n</svg>\n`;\n };\n}\n\nconst APP_WASM_BASE64 =\n \"AGFzbQEAAAABBwFgAn9/AX8DAgEABxMBD3N1bW1hcml6ZV9ub3RlcwAACg0BCwAgACABakHIAWoL\";\n"],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  import { resolve } from "node:path";
2
- import { homedir } from "node:os";
3
2
  import { existsSync, readFileSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
4
  //#region src/services/platform-auth-state.service.ts
5
5
  const NEXTCLAW_HOME_ENV_KEY = "NEXTCLAW_HOME";
6
6
  const NEXTCLAW_DEFAULT_HOME_DIR = ".nextclaw";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextclaw/app-runtime",
3
- "version": "0.14.1",
3
+ "version": "0.15.0",
4
4
  "private": false,
5
5
  "description": "Standalone micro app runtime and CLI for NextClaw apps.",
6
6
  "type": "module",
@@ -53,6 +53,7 @@
53
53
  },
54
54
  "files": [
55
55
  "dist",
56
+ "resources",
56
57
  "README.md"
57
58
  ],
58
59
  "dependencies": {