@nextclaw/app-runtime 0.4.0 → 0.5.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.
- package/README.md +12 -4
- package/dist/cli/app-runtime-cli.service.d.ts +2 -0
- package/dist/cli/app-runtime-cli.service.js +55 -2
- package/dist/cli/app-runtime-options.service.d.ts +10 -1
- package/dist/cli/app-runtime-options.service.js +39 -2
- package/dist/commands/create.controller.d.ts +2 -1
- package/dist/commands/create.controller.js +3 -2
- package/dist/commands/inspect.controller.js +2 -1
- package/dist/commands/run.controller.js +25 -7
- package/dist/host/app-host.service.d.ts +3 -1
- package/dist/host/app-host.service.js +5 -1
- package/dist/host/app-instance.service.js +1 -0
- package/dist/index.d.ts +9 -3
- package/dist/index.js +7 -1
- package/dist/install/app-installation.service.js +1 -0
- package/dist/install/app-installation.types.d.ts +1 -0
- package/dist/manifest/app-manifest.service.js +9 -3
- package/dist/manifest/app-manifest.types.d.ts +9 -3
- package/dist/package.js +1 -1
- package/dist/publish/app-marketplace-client.service.js +2 -2
- package/dist/publish/app-publish.service.d.ts +8 -1
- package/dist/publish/app-publish.service.js +87 -7
- package/dist/publish/platform-auth-state.service.d.ts +11 -0
- package/dist/publish/platform-auth-state.service.js +26 -0
- package/dist/runtime/app-build.service.d.ts +24 -0
- package/dist/runtime/app-build.service.js +55 -0
- package/dist/runtime/app-runtime-toolchain.service.d.ts +30 -0
- package/dist/runtime/app-runtime-toolchain.service.js +96 -0
- package/dist/runtime/wasm-main-runner.service.js +1 -0
- package/dist/runtime/wasmtime-wasi-http-component.service.d.ts +24 -0
- package/dist/runtime/wasmtime-wasi-http-component.service.js +129 -0
- package/dist/scaffold/app-scaffold.service.d.ts +11 -2
- package/dist/scaffold/app-scaffold.service.js +27 -8
- package/dist/scaffold/app-ts-http-scaffold-template.service.d.ts +25 -0
- package/dist/scaffold/app-ts-http-scaffold-template.service.js +496 -0
- package/package.json +1 -1
|
@@ -3,6 +3,7 @@ import { AppBundleService } from "../bundle/app-bundle.service.js";
|
|
|
3
3
|
import { AppPublishResult } from "./app-publish.types.js";
|
|
4
4
|
import { AppMarketplaceClientService } from "./app-marketplace-client.service.js";
|
|
5
5
|
import { AppMarketplaceMetadataService } from "./app-marketplace-metadata.service.js";
|
|
6
|
+
import { PlatformAuthStateService } from "./platform-auth-state.service.js";
|
|
6
7
|
|
|
7
8
|
//#region src/publish/app-publish.service.d.ts
|
|
8
9
|
declare class AppPublishService {
|
|
@@ -10,13 +11,19 @@ declare class AppPublishService {
|
|
|
10
11
|
private readonly bundleService;
|
|
11
12
|
private readonly metadataService;
|
|
12
13
|
private readonly marketplaceClient;
|
|
13
|
-
|
|
14
|
+
private readonly authStateService;
|
|
15
|
+
constructor(manifestService?: AppManifestService, bundleService?: AppBundleService, metadataService?: AppMarketplaceMetadataService, marketplaceClient?: AppMarketplaceClientService, authStateService?: PlatformAuthStateService);
|
|
14
16
|
publish: (params: {
|
|
15
17
|
appDirectory: string;
|
|
16
18
|
metadataPath?: string;
|
|
17
19
|
apiBaseUrl?: string;
|
|
18
20
|
token?: string;
|
|
19
21
|
}) => Promise<AppPublishResult>;
|
|
22
|
+
private resolvePublishActor;
|
|
23
|
+
private fetchCurrentPlatformUser;
|
|
24
|
+
private resolvePlatformApiBase;
|
|
25
|
+
private buildUserPublisher;
|
|
26
|
+
private buildOfficialPublisher;
|
|
20
27
|
}
|
|
21
28
|
//#endregion
|
|
22
29
|
export { AppPublishService };
|
|
@@ -2,16 +2,19 @@ import { AppManifestService } from "../manifest/app-manifest.service.js";
|
|
|
2
2
|
import { AppBundleService } from "../bundle/app-bundle.service.js";
|
|
3
3
|
import { AppMarketplaceClientService } from "./app-marketplace-client.service.js";
|
|
4
4
|
import { AppMarketplaceMetadataService } from "./app-marketplace-metadata.service.js";
|
|
5
|
+
import { PlatformAuthStateService } from "./platform-auth-state.service.js";
|
|
5
6
|
import { readFile } from "node:fs/promises";
|
|
6
7
|
import { createHash } from "node:crypto";
|
|
7
8
|
import path from "node:path";
|
|
8
9
|
//#region src/publish/app-publish.service.ts
|
|
10
|
+
const DEFAULT_PLATFORM_API_BASE = "https://ai-gateway-api.nextclaw.io";
|
|
9
11
|
var AppPublishService = class {
|
|
10
|
-
constructor(manifestService = new AppManifestService(), bundleService = new AppBundleService(), metadataService = new AppMarketplaceMetadataService(), marketplaceClient = new AppMarketplaceClientService()) {
|
|
12
|
+
constructor(manifestService = new AppManifestService(), bundleService = new AppBundleService(), metadataService = new AppMarketplaceMetadataService(), marketplaceClient = new AppMarketplaceClientService(), authStateService = new PlatformAuthStateService()) {
|
|
11
13
|
this.manifestService = manifestService;
|
|
12
14
|
this.bundleService = bundleService;
|
|
13
15
|
this.metadataService = metadataService;
|
|
14
16
|
this.marketplaceClient = marketplaceClient;
|
|
17
|
+
this.authStateService = authStateService;
|
|
15
18
|
}
|
|
16
19
|
publish = async (params) => {
|
|
17
20
|
const { appDirectory: inputAppDirectory, metadataPath, apiBaseUrl, token } = params;
|
|
@@ -22,6 +25,11 @@ var AppPublishService = class {
|
|
|
22
25
|
manifest: manifestBundle.manifest,
|
|
23
26
|
metadataPath
|
|
24
27
|
});
|
|
28
|
+
const actor = await this.resolvePublishActor({
|
|
29
|
+
apiBaseUrl,
|
|
30
|
+
explicitToken: token,
|
|
31
|
+
appId: manifestBundle.manifest.id
|
|
32
|
+
});
|
|
25
33
|
const bundle = await this.bundleService.packAppDirectory({ appDirectory });
|
|
26
34
|
const bundleBytes = Buffer.from(await readFile(bundle.bundlePath));
|
|
27
35
|
const bundleSha256 = createHash("sha256").update(bundleBytes).digest("hex");
|
|
@@ -43,11 +51,7 @@ var AppPublishService = class {
|
|
|
43
51
|
sourceRepo: metadata.sourceRepo,
|
|
44
52
|
homepage: metadata.homepage,
|
|
45
53
|
featured: metadata.featured ?? false,
|
|
46
|
-
publisher:
|
|
47
|
-
id: "nextclaw",
|
|
48
|
-
name: "NextClaw",
|
|
49
|
-
url: "https://nextclaw.io"
|
|
50
|
-
},
|
|
54
|
+
publisher: actor.publisher,
|
|
51
55
|
manifest: manifestBundle.manifest,
|
|
52
56
|
permissions: manifestBundle.manifest.permissions ?? {},
|
|
53
57
|
bundleBase64: bundleBytes.toString("base64"),
|
|
@@ -61,7 +65,7 @@ var AppPublishService = class {
|
|
|
61
65
|
...await this.marketplaceClient.publish({
|
|
62
66
|
payload,
|
|
63
67
|
apiBaseUrl,
|
|
64
|
-
token
|
|
68
|
+
token: actor.token
|
|
65
69
|
}),
|
|
66
70
|
bundle: {
|
|
67
71
|
path: bundle.bundlePath,
|
|
@@ -69,6 +73,82 @@ var AppPublishService = class {
|
|
|
69
73
|
}
|
|
70
74
|
};
|
|
71
75
|
};
|
|
76
|
+
resolvePublishActor = async (params) => {
|
|
77
|
+
const explicitToken = params.explicitToken?.trim();
|
|
78
|
+
const envAdminToken = process.env.NEXTCLAW_MARKETPLACE_ADMIN_TOKEN?.trim();
|
|
79
|
+
if (explicitToken) {
|
|
80
|
+
const token = explicitToken;
|
|
81
|
+
if (!token) throw new Error("缺少 publish token。");
|
|
82
|
+
const me = await this.fetchCurrentPlatformUser({
|
|
83
|
+
token,
|
|
84
|
+
platformApiBase: this.resolvePlatformApiBase()
|
|
85
|
+
});
|
|
86
|
+
return {
|
|
87
|
+
token,
|
|
88
|
+
publisher: this.buildUserPublisher(me, params.appId)
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
const authState = this.authStateService.readCurrentAuthState();
|
|
92
|
+
const platformToken = authState.token?.trim();
|
|
93
|
+
if (platformToken) {
|
|
94
|
+
const me = await this.fetchCurrentPlatformUser({
|
|
95
|
+
token: platformToken,
|
|
96
|
+
platformApiBase: this.resolvePlatformApiBase(authState.apiBaseUrl)
|
|
97
|
+
});
|
|
98
|
+
return {
|
|
99
|
+
token: platformToken,
|
|
100
|
+
publisher: this.buildUserPublisher(me, params.appId)
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
if (envAdminToken) return {
|
|
104
|
+
token: envAdminToken,
|
|
105
|
+
publisher: this.buildOfficialPublisher()
|
|
106
|
+
};
|
|
107
|
+
throw new Error("发布需要 NextClaw 平台登录态。请先运行 nextclaw login,或传入 --token。");
|
|
108
|
+
};
|
|
109
|
+
fetchCurrentPlatformUser = async (params) => {
|
|
110
|
+
const response = await fetch(`${params.platformApiBase}/platform/auth/me`, { headers: {
|
|
111
|
+
authorization: `Bearer ${params.token}`,
|
|
112
|
+
accept: "application/json"
|
|
113
|
+
} });
|
|
114
|
+
const payload = await response.json().catch(() => null);
|
|
115
|
+
if (!response.ok) {
|
|
116
|
+
const message = typeof payload === "object" && payload && "error" in payload && typeof payload.error?.message === "string" ? payload.error.message : `${response.status} ${response.statusText}`;
|
|
117
|
+
throw new Error(`读取 NextClaw 登录态失败:${message}`);
|
|
118
|
+
}
|
|
119
|
+
const user = typeof payload === "object" && payload && "data" in payload && typeof payload.data?.user === "object" && payload.data.user ? payload.data.user : null;
|
|
120
|
+
const id = typeof user?.id === "string" ? user.id.trim() : "";
|
|
121
|
+
const username = typeof user?.username === "string" ? user.username.trim() : "";
|
|
122
|
+
const role = user?.role === "admin" ? "admin" : "user";
|
|
123
|
+
if (!id) throw new Error("平台登录态缺少用户 id。");
|
|
124
|
+
return {
|
|
125
|
+
id,
|
|
126
|
+
username: username || null,
|
|
127
|
+
role
|
|
128
|
+
};
|
|
129
|
+
};
|
|
130
|
+
resolvePlatformApiBase = (configuredApiBase) => {
|
|
131
|
+
const source = configuredApiBase?.trim() || process.env.NEXTCLAW_PLATFORM_API_BASE?.trim() || DEFAULT_PLATFORM_API_BASE;
|
|
132
|
+
const normalized = new URL(source);
|
|
133
|
+
normalized.pathname = normalized.pathname.replace(/\/v1\/?$/, "/");
|
|
134
|
+
return normalized.toString().replace(/\/+$/, "");
|
|
135
|
+
};
|
|
136
|
+
buildUserPublisher = (user, appId) => {
|
|
137
|
+
if (appId.startsWith("nextclaw.") && user.role === "admin") return this.buildOfficialPublisher();
|
|
138
|
+
if (!user.username) throw new Error("当前 NextClaw 账号还没有 username,无法发布个人 scope app。请先在平台账号页设置用户名。");
|
|
139
|
+
return {
|
|
140
|
+
id: user.username,
|
|
141
|
+
name: user.username,
|
|
142
|
+
url: `https://platform.nextclaw.io/account`
|
|
143
|
+
};
|
|
144
|
+
};
|
|
145
|
+
buildOfficialPublisher = () => {
|
|
146
|
+
return {
|
|
147
|
+
id: "nextclaw",
|
|
148
|
+
name: "NextClaw",
|
|
149
|
+
url: "https://nextclaw.io"
|
|
150
|
+
};
|
|
151
|
+
};
|
|
72
152
|
};
|
|
73
153
|
//#endregion
|
|
74
154
|
export { AppPublishService };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
//#region src/publish/platform-auth-state.service.d.ts
|
|
2
|
+
type PlatformPublishAuthState = {
|
|
3
|
+
token: string | null;
|
|
4
|
+
apiBaseUrl?: string;
|
|
5
|
+
};
|
|
6
|
+
declare class PlatformAuthStateService {
|
|
7
|
+
readCurrentAuthState: () => PlatformPublishAuthState;
|
|
8
|
+
private resolveConfigPath;
|
|
9
|
+
}
|
|
10
|
+
//#endregion
|
|
11
|
+
export { PlatformAuthStateService, PlatformPublishAuthState };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
+
//#region src/publish/platform-auth-state.service.ts
|
|
5
|
+
var PlatformAuthStateService = class {
|
|
6
|
+
readCurrentAuthState = () => {
|
|
7
|
+
const configPath = this.resolveConfigPath();
|
|
8
|
+
if (!existsSync(configPath)) return { token: null };
|
|
9
|
+
try {
|
|
10
|
+
const raw = readFileSync(configPath, "utf-8");
|
|
11
|
+
const provider = JSON.parse(raw).providers?.nextclaw;
|
|
12
|
+
return {
|
|
13
|
+
token: typeof provider?.apiKey === "string" && provider.apiKey.trim().length > 0 ? provider.apiKey.trim() : null,
|
|
14
|
+
apiBaseUrl: typeof provider?.apiBase === "string" && provider.apiBase.trim().length > 0 ? provider.apiBase.trim() : void 0
|
|
15
|
+
};
|
|
16
|
+
} catch {
|
|
17
|
+
return { token: null };
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
resolveConfigPath = () => {
|
|
21
|
+
const nextclawHome = process.env.NEXTCLAW_HOME?.trim();
|
|
22
|
+
return resolve(nextclawHome && nextclawHome.length > 0 ? resolve(nextclawHome) : resolve(homedir(), ".nextclaw"), "config.json");
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
//#endregion
|
|
26
|
+
export { PlatformAuthStateService };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { AppManifestService } from "../manifest/app-manifest.service.js";
|
|
2
|
+
import { AppRuntimeToolchainService } from "./app-runtime-toolchain.service.js";
|
|
3
|
+
|
|
4
|
+
//#region src/runtime/app-build.service.d.ts
|
|
5
|
+
type AppBuildResult = {
|
|
6
|
+
appDirectory: string;
|
|
7
|
+
mainKind: string;
|
|
8
|
+
mainEntryPath: string;
|
|
9
|
+
installedDependencies: boolean;
|
|
10
|
+
built: boolean;
|
|
11
|
+
skippedReason?: string;
|
|
12
|
+
};
|
|
13
|
+
declare class AppBuildService {
|
|
14
|
+
private readonly manifestService;
|
|
15
|
+
private readonly toolchainService;
|
|
16
|
+
constructor(manifestService?: AppManifestService, toolchainService?: AppRuntimeToolchainService);
|
|
17
|
+
build: (params: {
|
|
18
|
+
appDirectory: string;
|
|
19
|
+
install: boolean;
|
|
20
|
+
}) => Promise<AppBuildResult>;
|
|
21
|
+
private pathExists;
|
|
22
|
+
}
|
|
23
|
+
//#endregion
|
|
24
|
+
export { AppBuildResult, AppBuildService };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { AppManifestService } from "../manifest/app-manifest.service.js";
|
|
2
|
+
import { AppRuntimeToolchainService } from "./app-runtime-toolchain.service.js";
|
|
3
|
+
import { access } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
//#region src/runtime/app-build.service.ts
|
|
6
|
+
var AppBuildService = class {
|
|
7
|
+
constructor(manifestService = new AppManifestService(), toolchainService = new AppRuntimeToolchainService()) {
|
|
8
|
+
this.manifestService = manifestService;
|
|
9
|
+
this.toolchainService = toolchainService;
|
|
10
|
+
}
|
|
11
|
+
build = async (params) => {
|
|
12
|
+
const appDirectory = path.resolve(params.appDirectory);
|
|
13
|
+
const bundle = await this.manifestService.load(appDirectory);
|
|
14
|
+
if (bundle.manifest.main.kind !== "wasi-http-component") return {
|
|
15
|
+
appDirectory,
|
|
16
|
+
mainKind: bundle.manifest.main.kind,
|
|
17
|
+
mainEntryPath: bundle.mainEntryPath,
|
|
18
|
+
installedDependencies: false,
|
|
19
|
+
built: false,
|
|
20
|
+
skippedReason: "main.kind=wasm 应用不需要 TS/WASI HTTP 构建。"
|
|
21
|
+
};
|
|
22
|
+
await this.toolchainService.assertReadyForWasiHttpBuild();
|
|
23
|
+
const mainDirectory = path.join(appDirectory, "main");
|
|
24
|
+
await access(path.join(mainDirectory, "package.json"));
|
|
25
|
+
const shouldInstall = params.install || !await this.pathExists(path.join(mainDirectory, "node_modules"));
|
|
26
|
+
if (shouldInstall) await this.toolchainService.runCommand({
|
|
27
|
+
command: "npm",
|
|
28
|
+
args: ["install"],
|
|
29
|
+
cwd: mainDirectory
|
|
30
|
+
});
|
|
31
|
+
await this.toolchainService.runCommand({
|
|
32
|
+
command: "npm",
|
|
33
|
+
args: ["run", "build"],
|
|
34
|
+
cwd: mainDirectory
|
|
35
|
+
});
|
|
36
|
+
await access(bundle.mainEntryPath);
|
|
37
|
+
return {
|
|
38
|
+
appDirectory,
|
|
39
|
+
mainKind: bundle.manifest.main.kind,
|
|
40
|
+
mainEntryPath: bundle.mainEntryPath,
|
|
41
|
+
installedDependencies: shouldInstall,
|
|
42
|
+
built: true
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
pathExists = async (targetPath) => {
|
|
46
|
+
try {
|
|
47
|
+
await access(targetPath);
|
|
48
|
+
return true;
|
|
49
|
+
} catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
//#endregion
|
|
55
|
+
export { AppBuildService };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
//#region src/runtime/app-runtime-toolchain.service.d.ts
|
|
2
|
+
type AppRuntimeToolStatus = {
|
|
3
|
+
name: string;
|
|
4
|
+
command: string;
|
|
5
|
+
ok: boolean;
|
|
6
|
+
version?: string;
|
|
7
|
+
installHint: string;
|
|
8
|
+
error?: string;
|
|
9
|
+
};
|
|
10
|
+
type AppRuntimeDoctorResult = {
|
|
11
|
+
ok: boolean;
|
|
12
|
+
tools: AppRuntimeToolStatus[];
|
|
13
|
+
};
|
|
14
|
+
type AppRuntimeCommandResult = {
|
|
15
|
+
stdout: string;
|
|
16
|
+
stderr: string;
|
|
17
|
+
};
|
|
18
|
+
declare class AppRuntimeToolchainService {
|
|
19
|
+
doctor: () => Promise<AppRuntimeDoctorResult>;
|
|
20
|
+
assertReadyForWasiHttpBuild: () => Promise<void>;
|
|
21
|
+
runCommand: (params: {
|
|
22
|
+
command: string;
|
|
23
|
+
args: string[];
|
|
24
|
+
cwd: string;
|
|
25
|
+
}) => Promise<AppRuntimeCommandResult>;
|
|
26
|
+
private checkTool;
|
|
27
|
+
private collectProcess;
|
|
28
|
+
}
|
|
29
|
+
//#endregion
|
|
30
|
+
export { AppRuntimeCommandResult, AppRuntimeDoctorResult, AppRuntimeToolStatus, AppRuntimeToolchainService };
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
//#region src/runtime/app-runtime-toolchain.service.ts
|
|
3
|
+
const REQUIRED_TOOLS = [
|
|
4
|
+
{
|
|
5
|
+
name: "npm",
|
|
6
|
+
command: "npm",
|
|
7
|
+
args: ["--version"],
|
|
8
|
+
installHint: "请安装 Node.js,或使用 NextClaw 桌面版内置运行环境。"
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
name: "wasmtime",
|
|
12
|
+
command: "wasmtime",
|
|
13
|
+
args: ["--version"],
|
|
14
|
+
installHint: "请安装 Wasmtime,或使用后续内置 Wasmtime 的 NextClaw 发行包。"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
name: "wkg",
|
|
18
|
+
command: "wkg",
|
|
19
|
+
args: ["--version"],
|
|
20
|
+
installHint: "请安装 Bytecode Alliance wkg;有 Rust/Cargo 时可执行:cargo install wkg --locked。"
|
|
21
|
+
}
|
|
22
|
+
];
|
|
23
|
+
var AppRuntimeToolchainService = class {
|
|
24
|
+
doctor = async () => {
|
|
25
|
+
const tools = await Promise.all(REQUIRED_TOOLS.map((tool) => this.checkTool(tool)));
|
|
26
|
+
return {
|
|
27
|
+
ok: tools.every((tool) => tool.ok),
|
|
28
|
+
tools
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
assertReadyForWasiHttpBuild = async () => {
|
|
32
|
+
const missing = (await this.doctor()).tools.filter((tool) => !tool.ok);
|
|
33
|
+
if (missing.length === 0) return;
|
|
34
|
+
throw new Error(["NApp WASI HTTP 开发环境尚未就绪。", ...missing.map((tool) => `- 缺少 ${tool.name}: ${tool.installHint}`)].join("\n"));
|
|
35
|
+
};
|
|
36
|
+
runCommand = async (params) => {
|
|
37
|
+
const { command, args, cwd } = params;
|
|
38
|
+
const child = spawn(command, args, {
|
|
39
|
+
cwd,
|
|
40
|
+
stdio: [
|
|
41
|
+
"ignore",
|
|
42
|
+
"pipe",
|
|
43
|
+
"pipe"
|
|
44
|
+
]
|
|
45
|
+
});
|
|
46
|
+
return await this.collectProcess(child, command);
|
|
47
|
+
};
|
|
48
|
+
checkTool = async (tool) => {
|
|
49
|
+
try {
|
|
50
|
+
const result = await this.runCommand({
|
|
51
|
+
command: tool.command,
|
|
52
|
+
args: tool.args,
|
|
53
|
+
cwd: process.cwd()
|
|
54
|
+
});
|
|
55
|
+
const version = (result.stdout || result.stderr).trim().split(/\r?\n/)[0]?.trim();
|
|
56
|
+
return {
|
|
57
|
+
name: tool.name,
|
|
58
|
+
command: tool.command,
|
|
59
|
+
ok: true,
|
|
60
|
+
version,
|
|
61
|
+
installHint: tool.installHint
|
|
62
|
+
};
|
|
63
|
+
} catch (error) {
|
|
64
|
+
return {
|
|
65
|
+
name: tool.name,
|
|
66
|
+
command: tool.command,
|
|
67
|
+
ok: false,
|
|
68
|
+
installHint: tool.installHint,
|
|
69
|
+
error: error instanceof Error ? error.message : String(error)
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
collectProcess = async (child, command) => {
|
|
74
|
+
const stdoutChunks = [];
|
|
75
|
+
const stderrChunks = [];
|
|
76
|
+
child.stdout.on("data", (chunk) => {
|
|
77
|
+
stdoutChunks.push(chunk);
|
|
78
|
+
});
|
|
79
|
+
child.stderr.on("data", (chunk) => {
|
|
80
|
+
stderrChunks.push(chunk);
|
|
81
|
+
});
|
|
82
|
+
const exitCode = await new Promise((resolve, reject) => {
|
|
83
|
+
child.once("error", reject);
|
|
84
|
+
child.once("exit", resolve);
|
|
85
|
+
});
|
|
86
|
+
const stdout = Buffer.concat(stdoutChunks).toString("utf-8");
|
|
87
|
+
const stderr = Buffer.concat(stderrChunks).toString("utf-8");
|
|
88
|
+
if (exitCode !== 0) throw new Error(`${command} exited with ${exitCode ?? "unknown"}\n${stderr || stdout}`);
|
|
89
|
+
return {
|
|
90
|
+
stdout,
|
|
91
|
+
stderr
|
|
92
|
+
};
|
|
93
|
+
};
|
|
94
|
+
};
|
|
95
|
+
//#endregion
|
|
96
|
+
export { AppRuntimeToolchainService };
|
|
@@ -7,6 +7,7 @@ var WasmMainRunnerService = class extends MainRunnerService {
|
|
|
7
7
|
this.sidecarClient = sidecarClient;
|
|
8
8
|
}
|
|
9
9
|
runDocumentSummary = async (request) => {
|
|
10
|
+
if (request.bundle.manifest.main.kind !== "wasm") throw new Error("runDocumentSummary 只支持 main.kind=wasm。");
|
|
10
11
|
const output = await this.sidecarClient.runExport({
|
|
11
12
|
wasmPath: request.bundle.mainEntryPath,
|
|
12
13
|
exportName: request.bundle.manifest.main.export,
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
|
|
3
|
+
//#region src/runtime/wasmtime-wasi-http-component.service.d.ts
|
|
4
|
+
type WasmtimeWasiHttpComponentStartOptions = {
|
|
5
|
+
wasmPath: string;
|
|
6
|
+
dataDirectory: string;
|
|
7
|
+
};
|
|
8
|
+
type WasmtimeWasiHttpComponentHandle = {
|
|
9
|
+
url: string;
|
|
10
|
+
port: number;
|
|
11
|
+
};
|
|
12
|
+
declare class WasmtimeWasiHttpComponentService {
|
|
13
|
+
private process?;
|
|
14
|
+
private handle?;
|
|
15
|
+
start: (options: WasmtimeWasiHttpComponentStartOptions) => Promise<WasmtimeWasiHttpComponentHandle>;
|
|
16
|
+
stop: () => Promise<void>;
|
|
17
|
+
handleRequest: (request: IncomingMessage, response: ServerResponse) => Promise<boolean>;
|
|
18
|
+
private readBody;
|
|
19
|
+
private methodSupportsBody;
|
|
20
|
+
private findAvailablePort;
|
|
21
|
+
private waitUntilReady;
|
|
22
|
+
}
|
|
23
|
+
//#endregion
|
|
24
|
+
export { WasmtimeWasiHttpComponentHandle, WasmtimeWasiHttpComponentService, WasmtimeWasiHttpComponentStartOptions };
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { mkdir } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { createServer } from "node:http";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
//#region src/runtime/wasmtime-wasi-http-component.service.ts
|
|
6
|
+
var WasmtimeWasiHttpComponentService = class {
|
|
7
|
+
process;
|
|
8
|
+
handle;
|
|
9
|
+
start = async (options) => {
|
|
10
|
+
if (this.process) throw new Error("WASI HTTP component 已经启动。");
|
|
11
|
+
const dataDirectory = path.resolve(options.dataDirectory);
|
|
12
|
+
await mkdir(dataDirectory, { recursive: true });
|
|
13
|
+
const port = await this.findAvailablePort();
|
|
14
|
+
const child = spawn("wasmtime", [
|
|
15
|
+
"serve",
|
|
16
|
+
"-S",
|
|
17
|
+
"cli=y",
|
|
18
|
+
"-S",
|
|
19
|
+
"http=y",
|
|
20
|
+
"-S",
|
|
21
|
+
"inherit-network=y",
|
|
22
|
+
"--addr",
|
|
23
|
+
`127.0.0.1:${port}`,
|
|
24
|
+
"--dir",
|
|
25
|
+
`${dataDirectory}::/data`,
|
|
26
|
+
options.wasmPath
|
|
27
|
+
], { stdio: [
|
|
28
|
+
"ignore",
|
|
29
|
+
"pipe",
|
|
30
|
+
"pipe"
|
|
31
|
+
] });
|
|
32
|
+
this.process = child;
|
|
33
|
+
const handle = {
|
|
34
|
+
url: `http://127.0.0.1:${port}`,
|
|
35
|
+
port
|
|
36
|
+
};
|
|
37
|
+
this.handle = handle;
|
|
38
|
+
await this.waitUntilReady(handle.url, child);
|
|
39
|
+
return handle;
|
|
40
|
+
};
|
|
41
|
+
stop = async () => {
|
|
42
|
+
const child = this.process;
|
|
43
|
+
if (!child) return;
|
|
44
|
+
await new Promise((resolve) => {
|
|
45
|
+
child.once("exit", () => {
|
|
46
|
+
resolve();
|
|
47
|
+
});
|
|
48
|
+
child.kill("SIGTERM");
|
|
49
|
+
setTimeout(() => {
|
|
50
|
+
if (!child.killed) child.kill("SIGKILL");
|
|
51
|
+
resolve();
|
|
52
|
+
}, 1e3).unref();
|
|
53
|
+
});
|
|
54
|
+
this.process = void 0;
|
|
55
|
+
this.handle = void 0;
|
|
56
|
+
};
|
|
57
|
+
handleRequest = async (request, response) => {
|
|
58
|
+
const handle = this.handle;
|
|
59
|
+
if (!handle) return false;
|
|
60
|
+
const requestUrl = new URL(request.url ?? "/", handle.url);
|
|
61
|
+
if (!requestUrl.pathname.startsWith("/api/")) return false;
|
|
62
|
+
const headers = new Headers();
|
|
63
|
+
for (const [key, value] of Object.entries(request.headers)) {
|
|
64
|
+
if (value === void 0 || key.toLowerCase() === "host") continue;
|
|
65
|
+
if (Array.isArray(value)) {
|
|
66
|
+
for (const item of value) headers.append(key, item);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
headers.set(key, value);
|
|
70
|
+
}
|
|
71
|
+
const body = await this.readBody(request);
|
|
72
|
+
const upstreamResponse = await fetch(requestUrl, {
|
|
73
|
+
method: request.method,
|
|
74
|
+
headers,
|
|
75
|
+
body: this.methodSupportsBody(request.method) ? body : void 0
|
|
76
|
+
});
|
|
77
|
+
response.writeHead(upstreamResponse.status, Object.fromEntries(upstreamResponse.headers));
|
|
78
|
+
response.end(Buffer.from(await upstreamResponse.arrayBuffer()));
|
|
79
|
+
return true;
|
|
80
|
+
};
|
|
81
|
+
readBody = async (request) => {
|
|
82
|
+
const chunks = [];
|
|
83
|
+
for await (const chunk of request) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
84
|
+
return Buffer.concat(chunks);
|
|
85
|
+
};
|
|
86
|
+
methodSupportsBody = (method) => {
|
|
87
|
+
return method !== void 0 && method !== "GET" && method !== "HEAD";
|
|
88
|
+
};
|
|
89
|
+
findAvailablePort = async () => {
|
|
90
|
+
const server = createServer();
|
|
91
|
+
await new Promise((resolve, reject) => {
|
|
92
|
+
server.once("error", reject);
|
|
93
|
+
server.listen(0, "127.0.0.1", () => {
|
|
94
|
+
server.off("error", reject);
|
|
95
|
+
resolve();
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
const address = server.address();
|
|
99
|
+
await new Promise((resolve, reject) => {
|
|
100
|
+
server.close((error) => {
|
|
101
|
+
if (error) {
|
|
102
|
+
reject(error);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
resolve();
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
if (!address || typeof address === "string") throw new Error("无法分配 WASI HTTP component 端口。");
|
|
109
|
+
return address.port;
|
|
110
|
+
};
|
|
111
|
+
waitUntilReady = async (url, child) => {
|
|
112
|
+
const errors = [];
|
|
113
|
+
child.stderr.on("data", (chunk) => {
|
|
114
|
+
errors.push(chunk.toString("utf-8"));
|
|
115
|
+
});
|
|
116
|
+
for (let attempt = 0; attempt < 50; attempt += 1) {
|
|
117
|
+
if (child.exitCode !== null) throw new Error(`WASI HTTP component 启动失败:${errors.join("").trim() || `exit ${child.exitCode}`}`);
|
|
118
|
+
try {
|
|
119
|
+
await fetch(url);
|
|
120
|
+
return;
|
|
121
|
+
} catch {
|
|
122
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
throw new Error(`WASI HTTP component 启动超时:${errors.join("").trim()}`);
|
|
126
|
+
};
|
|
127
|
+
};
|
|
128
|
+
//#endregion
|
|
129
|
+
export { WasmtimeWasiHttpComponentService };
|
|
@@ -1,10 +1,19 @@
|
|
|
1
|
+
import { AppTsHttpScaffoldTemplateService } from "./app-ts-http-scaffold-template.service.js";
|
|
2
|
+
|
|
1
3
|
//#region src/scaffold/app-scaffold.service.d.ts
|
|
4
|
+
type AppScaffoldTemplate = "starter" | "ts-http";
|
|
2
5
|
type AppScaffoldResult = {
|
|
3
6
|
appDirectory: string;
|
|
4
7
|
manifestPath: string;
|
|
8
|
+
template: AppScaffoldTemplate;
|
|
5
9
|
};
|
|
6
10
|
declare class AppScaffoldService {
|
|
7
|
-
|
|
11
|
+
private readonly tsHttpTemplateService;
|
|
12
|
+
constructor(tsHttpTemplateService?: AppTsHttpScaffoldTemplateService);
|
|
13
|
+
scaffold: (targetDirectory: string, options?: {
|
|
14
|
+
template?: AppScaffoldTemplate;
|
|
15
|
+
}) => Promise<AppScaffoldResult>;
|
|
16
|
+
private writeTemplateFiles;
|
|
8
17
|
private assertTargetDoesNotExist;
|
|
9
18
|
private buildAppId;
|
|
10
19
|
private buildAppName;
|
|
@@ -18,4 +27,4 @@ declare class AppScaffoldService {
|
|
|
18
27
|
private buildIconSvg;
|
|
19
28
|
}
|
|
20
29
|
//#endregion
|
|
21
|
-
export { AppScaffoldService };
|
|
30
|
+
export { AppScaffoldResult, AppScaffoldService, AppScaffoldTemplate };
|
|
@@ -1,9 +1,14 @@
|
|
|
1
|
+
import { AppTsHttpScaffoldTemplateService } from "./app-ts-http-scaffold-template.service.js";
|
|
1
2
|
import { access, mkdir, writeFile } from "node:fs/promises";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
//#region src/scaffold/app-scaffold.service.ts
|
|
4
5
|
var AppScaffoldService = class {
|
|
5
|
-
|
|
6
|
+
constructor(tsHttpTemplateService = new AppTsHttpScaffoldTemplateService()) {
|
|
7
|
+
this.tsHttpTemplateService = tsHttpTemplateService;
|
|
8
|
+
}
|
|
9
|
+
scaffold = async (targetDirectory, options) => {
|
|
6
10
|
const appDirectory = path.resolve(targetDirectory);
|
|
11
|
+
const template = options?.template ?? "starter";
|
|
7
12
|
await this.assertTargetDoesNotExist(appDirectory);
|
|
8
13
|
await mkdir(path.join(appDirectory, "main"), { recursive: true });
|
|
9
14
|
await mkdir(path.join(appDirectory, "ui"), { recursive: true });
|
|
@@ -11,6 +16,17 @@ var AppScaffoldService = class {
|
|
|
11
16
|
const appName = this.buildAppName(appDirectory);
|
|
12
17
|
const appId = this.buildAppId(appDirectory);
|
|
13
18
|
const manifestPath = path.join(appDirectory, "manifest.json");
|
|
19
|
+
if (template === "ts-http") {
|
|
20
|
+
await this.writeTemplateFiles(appDirectory, this.tsHttpTemplateService.buildFiles({
|
|
21
|
+
appId,
|
|
22
|
+
appName
|
|
23
|
+
}));
|
|
24
|
+
return {
|
|
25
|
+
appDirectory,
|
|
26
|
+
manifestPath,
|
|
27
|
+
template
|
|
28
|
+
};
|
|
29
|
+
}
|
|
14
30
|
await Promise.all([
|
|
15
31
|
writeFile(manifestPath, `${JSON.stringify(this.buildManifest(appId, appName), null, 2)}\n`, "utf-8"),
|
|
16
32
|
writeFile(path.join(appDirectory, "marketplace.json"), `${JSON.stringify(this.buildMarketplaceMetadata(appName), null, 2)}\n`, "utf-8"),
|
|
@@ -23,9 +39,17 @@ var AppScaffoldService = class {
|
|
|
23
39
|
]);
|
|
24
40
|
return {
|
|
25
41
|
appDirectory,
|
|
26
|
-
manifestPath
|
|
42
|
+
manifestPath,
|
|
43
|
+
template
|
|
27
44
|
};
|
|
28
45
|
};
|
|
46
|
+
writeTemplateFiles = async (appDirectory, files) => {
|
|
47
|
+
await Promise.all(files.map(async (file) => {
|
|
48
|
+
const filePath = path.join(appDirectory, file.relativePath);
|
|
49
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
50
|
+
await writeFile(filePath, file.content);
|
|
51
|
+
}));
|
|
52
|
+
};
|
|
29
53
|
assertTargetDoesNotExist = async (targetDirectory) => {
|
|
30
54
|
try {
|
|
31
55
|
await access(targetDirectory);
|
|
@@ -216,12 +240,7 @@ var AppScaffoldService = class {
|
|
|
216
240
|
],
|
|
217
241
|
sourceRepo: "https://github.com/Peiiii/nextclaw",
|
|
218
242
|
homepage: "https://nextclaw.io",
|
|
219
|
-
featured: false
|
|
220
|
-
publisher: {
|
|
221
|
-
id: "nextclaw",
|
|
222
|
-
name: "NextClaw",
|
|
223
|
-
url: "https://nextclaw.io"
|
|
224
|
-
}
|
|
243
|
+
featured: false
|
|
225
244
|
};
|
|
226
245
|
};
|
|
227
246
|
buildReadme = (appName, appId) => {
|