@nextclaw/app-runtime 0.2.0 → 0.4.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 (68) hide show
  1. package/README.md +125 -4
  2. package/dist/bundle/app-bundle.service.d.ts +27 -0
  3. package/dist/bundle/app-bundle.service.js +133 -0
  4. package/dist/bundle/app-bundle.types.d.ts +24 -0
  5. package/dist/cli/app-runtime-cli.service.d.ts +28 -0
  6. package/dist/cli/app-runtime-cli.service.js +298 -0
  7. package/dist/cli/app-runtime-options.service.d.ts +67 -0
  8. package/dist/cli/app-runtime-options.service.js +242 -0
  9. package/dist/commands/create.controller.d.ts +14 -0
  10. package/dist/commands/grant.controller.d.ts +16 -0
  11. package/dist/commands/grant.controller.js +25 -0
  12. package/dist/commands/info.controller.d.ts +14 -0
  13. package/dist/commands/info.controller.js +27 -0
  14. package/dist/commands/install.controller.d.ts +15 -0
  15. package/dist/commands/install.controller.js +24 -0
  16. package/dist/commands/list.controller.d.ts +13 -0
  17. package/dist/commands/list.controller.js +25 -0
  18. package/dist/commands/pack.controller.d.ts +15 -0
  19. package/dist/commands/pack.controller.js +25 -0
  20. package/dist/commands/permissions.controller.d.ts +14 -0
  21. package/dist/commands/permissions.controller.js +26 -0
  22. package/dist/commands/publish.controller.d.ts +17 -0
  23. package/dist/commands/publish.controller.js +29 -0
  24. package/dist/commands/registry.controller.d.ts +16 -0
  25. package/dist/commands/registry.controller.js +27 -0
  26. package/dist/commands/revoke.controller.d.ts +15 -0
  27. package/dist/commands/revoke.controller.js +24 -0
  28. package/dist/commands/run.controller.js +14 -6
  29. package/dist/commands/uninstall.controller.d.ts +15 -0
  30. package/dist/commands/uninstall.controller.js +23 -0
  31. package/dist/commands/update.controller.d.ts +16 -0
  32. package/dist/commands/update.controller.js +29 -0
  33. package/dist/host/app-instance.service.d.ts +3 -1
  34. package/dist/host/app-instance.service.js +2 -2
  35. package/dist/index.d.ts +31 -2
  36. package/dist/index.js +28 -2
  37. package/dist/install/app-installation.service.d.ts +37 -0
  38. package/dist/install/app-installation.service.js +255 -0
  39. package/dist/install/app-installation.types.d.ts +62 -0
  40. package/dist/main.js +5 -132
  41. package/dist/package.js +1 -1
  42. package/dist/paths/app-home.service.d.ts +16 -0
  43. package/dist/paths/app-home.service.js +42 -0
  44. package/dist/permissions/app-grant.service.d.ts +22 -0
  45. package/dist/permissions/app-grant.service.js +63 -0
  46. package/dist/permissions/app-permissions.service.d.ts +3 -1
  47. package/dist/permissions/app-permissions.service.js +12 -5
  48. package/dist/permissions/app-permissions.types.d.ts +28 -1
  49. package/dist/publish/app-marketplace-client.service.d.ts +13 -0
  50. package/dist/publish/app-marketplace-client.service.js +31 -0
  51. package/dist/publish/app-marketplace-metadata.service.d.ts +27 -0
  52. package/dist/publish/app-marketplace-metadata.service.js +84 -0
  53. package/dist/publish/app-publish.service.d.ts +22 -0
  54. package/dist/publish/app-publish.service.js +74 -0
  55. package/dist/publish/app-publish.types.d.ts +66 -0
  56. package/dist/publish/app-publish.types.js +4 -0
  57. package/dist/registry/app-registry-config.service.d.ts +16 -0
  58. package/dist/registry/app-registry-config.service.js +84 -0
  59. package/dist/registry/app-registry.service.d.ts +38 -0
  60. package/dist/registry/app-registry.service.js +115 -0
  61. package/dist/registry/app-registry.types.d.ts +33 -0
  62. package/dist/registry/app-remote-registry-client.service.d.ts +28 -0
  63. package/dist/registry/app-remote-registry-client.service.js +114 -0
  64. package/dist/registry/app-remote-registry.types.d.ts +52 -0
  65. package/dist/registry/app-remote-registry.types.js +4 -0
  66. package/dist/scaffold/app-scaffold.service.d.ts +21 -0
  67. package/dist/scaffold/app-scaffold.service.js +55 -1
  68. package/package.json +4 -2
@@ -0,0 +1,66 @@
1
+ import { AppManifest, AppPermissions } from "../manifest/app-manifest.types.js";
2
+ import { AppPublisher } from "../registry/app-remote-registry.types.js";
3
+
4
+ //#region src/publish/app-publish.types.d.ts
5
+ declare const DEFAULT_APP_MARKETPLACE_API_BASE = "https://apps-registry.nextclaw.io";
6
+ type AppMarketplaceMetadata = {
7
+ slug: string;
8
+ summary: string;
9
+ summaryI18n: Record<string, string>;
10
+ description?: string;
11
+ descriptionI18n?: Record<string, string>;
12
+ author: string;
13
+ tags: string[];
14
+ sourceRepo?: string;
15
+ homepage?: string;
16
+ featured?: boolean;
17
+ publisher?: AppPublisher;
18
+ };
19
+ type AppPublishFile = {
20
+ path: string;
21
+ contentBase64: string;
22
+ };
23
+ type AppPublishPayload = {
24
+ slug: string;
25
+ appId: string;
26
+ name: string;
27
+ version: string;
28
+ summary: string;
29
+ summaryI18n: Record<string, string>;
30
+ description?: string;
31
+ descriptionI18n?: Record<string, string>;
32
+ author: string;
33
+ tags: string[];
34
+ sourceRepo?: string;
35
+ homepage?: string;
36
+ featured: boolean;
37
+ publisher: AppPublisher;
38
+ manifest: AppManifest;
39
+ permissions: AppPermissions;
40
+ bundleBase64: string;
41
+ bundleSha256: string;
42
+ files: AppPublishFile[];
43
+ };
44
+ type AppPublishResult = {
45
+ created: boolean;
46
+ item: {
47
+ slug: string;
48
+ appId: string;
49
+ name: string;
50
+ latestVersion: string;
51
+ webUrl?: string;
52
+ install: {
53
+ kind: "registry";
54
+ spec: string;
55
+ command: string;
56
+ registry: string;
57
+ };
58
+ };
59
+ bundle: {
60
+ path: string;
61
+ sha256: string;
62
+ };
63
+ fileCount: number;
64
+ };
65
+ //#endregion
66
+ export { AppMarketplaceMetadata, AppPublishFile, AppPublishPayload, AppPublishResult, DEFAULT_APP_MARKETPLACE_API_BASE };
@@ -0,0 +1,4 @@
1
+ //#region src/publish/app-publish.types.ts
2
+ const DEFAULT_APP_MARKETPLACE_API_BASE = "https://apps-registry.nextclaw.io";
3
+ //#endregion
4
+ export { DEFAULT_APP_MARKETPLACE_API_BASE };
@@ -0,0 +1,16 @@
1
+ import { AppHomeService } from "../paths/app-home.service.js";
2
+ import { AppRegistryConfigSnapshot } from "./app-remote-registry.types.js";
3
+
4
+ //#region src/registry/app-registry-config.service.d.ts
5
+ declare class AppRegistryConfigService {
6
+ private readonly appHomeService;
7
+ constructor(appHomeService?: AppHomeService);
8
+ getSnapshot: () => Promise<AppRegistryConfigSnapshot>;
9
+ setRegistryUrl: (registryUrl: string) => Promise<AppRegistryConfigSnapshot>;
10
+ resetRegistryUrl: () => Promise<AppRegistryConfigSnapshot>;
11
+ private loadOptionalConfig;
12
+ private parseConfig;
13
+ private normalizeRegistryUrl;
14
+ }
15
+ //#endregion
16
+ export { AppRegistryConfigService };
@@ -0,0 +1,84 @@
1
+ import { AppHomeService } from "../paths/app-home.service.js";
2
+ import { DEFAULT_APP_REGISTRY_URL } from "./app-remote-registry.types.js";
3
+ import { readFile, rm, writeFile } from "node:fs/promises";
4
+ //#region src/registry/app-registry-config.service.ts
5
+ var AppRegistryConfigService = class {
6
+ constructor(appHomeService = new AppHomeService()) {
7
+ this.appHomeService = appHomeService;
8
+ }
9
+ getSnapshot = async () => {
10
+ const envUrl = process.env.NEXTCLAW_APP_REGISTRY?.trim();
11
+ if (envUrl) return {
12
+ defaultUrl: DEFAULT_APP_REGISTRY_URL,
13
+ currentUrl: this.normalizeRegistryUrl(envUrl),
14
+ source: "env"
15
+ };
16
+ const config = await this.loadOptionalConfig();
17
+ if (config) return {
18
+ defaultUrl: DEFAULT_APP_REGISTRY_URL,
19
+ currentUrl: this.normalizeRegistryUrl(config.registry.url),
20
+ source: "config"
21
+ };
22
+ return {
23
+ defaultUrl: DEFAULT_APP_REGISTRY_URL,
24
+ currentUrl: DEFAULT_APP_REGISTRY_URL,
25
+ source: "default"
26
+ };
27
+ };
28
+ setRegistryUrl = async (registryUrl) => {
29
+ const normalized = this.normalizeRegistryUrl(registryUrl);
30
+ await this.appHomeService.ensureBaseDirectories();
31
+ await writeFile(this.appHomeService.getConfigPath(), `${JSON.stringify({
32
+ schemaVersion: 1,
33
+ registry: { url: normalized }
34
+ }, null, 2)}\n`, "utf-8");
35
+ return {
36
+ defaultUrl: DEFAULT_APP_REGISTRY_URL,
37
+ currentUrl: normalized,
38
+ source: "config"
39
+ };
40
+ };
41
+ resetRegistryUrl = async () => {
42
+ await rm(this.appHomeService.getConfigPath(), { force: true });
43
+ return {
44
+ defaultUrl: DEFAULT_APP_REGISTRY_URL,
45
+ currentUrl: DEFAULT_APP_REGISTRY_URL,
46
+ source: "default"
47
+ };
48
+ };
49
+ loadOptionalConfig = async () => {
50
+ try {
51
+ const raw = await readFile(this.appHomeService.getConfigPath(), "utf-8");
52
+ return this.parseConfig(JSON.parse(raw));
53
+ } catch (error) {
54
+ if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") return;
55
+ throw error;
56
+ }
57
+ };
58
+ parseConfig = (rawConfig) => {
59
+ if (!rawConfig || typeof rawConfig !== "object" || Array.isArray(rawConfig)) throw new Error("config.json 必须是对象。");
60
+ const candidate = rawConfig;
61
+ if (candidate.schemaVersion !== 1) throw new Error("当前只支持 config schemaVersion = 1。");
62
+ const registry = candidate.registry;
63
+ if (!registry || typeof registry !== "object" || Array.isArray(registry)) throw new Error("config.registry 必须是对象。");
64
+ const registryUrl = registry.url;
65
+ if (typeof registryUrl !== "string" || !registryUrl.trim()) throw new Error("config.registry.url 必须是非空字符串。");
66
+ return {
67
+ schemaVersion: 1,
68
+ registry: { url: this.normalizeRegistryUrl(registryUrl) }
69
+ };
70
+ };
71
+ normalizeRegistryUrl = (registryUrl) => {
72
+ let normalized;
73
+ try {
74
+ normalized = new URL(registryUrl);
75
+ } catch {
76
+ throw new Error(`非法 registry URL:${registryUrl}`);
77
+ }
78
+ if (normalized.protocol !== "http:" && normalized.protocol !== "https:") throw new Error(`registry URL 只支持 http/https:${registryUrl}`);
79
+ const text = normalized.toString();
80
+ return text.endsWith("/") ? text : `${text}/`;
81
+ };
82
+ };
83
+ //#endregion
84
+ export { AppRegistryConfigService };
@@ -0,0 +1,38 @@
1
+ import { AppPermissions } from "../manifest/app-manifest.types.js";
2
+ import { AppDocumentGrantMap } from "../permissions/app-permissions.types.js";
3
+ import { AppHomeService } from "../paths/app-home.service.js";
4
+ import { AppInstallSourceKind, AppRegistry, AppRegistryAppRecord, AppRegistryInstalledVersion } from "./app-registry.types.js";
5
+
6
+ //#region src/registry/app-registry.service.d.ts
7
+ declare class AppRegistryService {
8
+ private readonly appHomeService;
9
+ constructor(appHomeService?: AppHomeService);
10
+ load: () => Promise<AppRegistry>;
11
+ save: (registry: AppRegistry) => Promise<void>;
12
+ listApps: () => Promise<AppRegistryAppRecord[]>;
13
+ getApp: (appId: string) => Promise<AppRegistryAppRecord | undefined>;
14
+ getActiveVersion: (appId: string) => Promise<AppRegistryInstalledVersion | undefined>;
15
+ upsertInstallation: (params: {
16
+ appId: string;
17
+ name: string;
18
+ description?: string;
19
+ version: string;
20
+ installDirectory: string;
21
+ dataDirectory: string;
22
+ sourceKind: AppInstallSourceKind;
23
+ sourceRef: string;
24
+ installedAt: string;
25
+ permissions: AppPermissions;
26
+ registryUrl?: string;
27
+ bundleUrl?: string;
28
+ sha256?: string;
29
+ publisher?: AppRegistryInstalledVersion["publisher"];
30
+ }) => Promise<AppRegistryAppRecord>;
31
+ updateGrants: (appId: string, grants: AppDocumentGrantMap) => Promise<AppRegistryAppRecord>;
32
+ setDocumentGrant: (appId: string, scopeId: string, directoryPath: string) => Promise<AppRegistryAppRecord>;
33
+ removeDocumentGrant: (appId: string, scopeId: string) => Promise<boolean>;
34
+ removeApp: (appId: string) => Promise<AppRegistryAppRecord | undefined>;
35
+ private parseRegistry;
36
+ }
37
+ //#endregion
38
+ export { AppRegistryService };
@@ -0,0 +1,115 @@
1
+ import { AppHomeService } from "../paths/app-home.service.js";
2
+ import { readFile, writeFile } from "node:fs/promises";
3
+ //#region src/registry/app-registry.service.ts
4
+ var AppRegistryService = class {
5
+ constructor(appHomeService = new AppHomeService()) {
6
+ this.appHomeService = appHomeService;
7
+ }
8
+ load = async () => {
9
+ await this.appHomeService.ensureBaseDirectories();
10
+ const registryPath = this.appHomeService.getRegistryPath();
11
+ try {
12
+ const raw = await readFile(registryPath, "utf-8");
13
+ return this.parseRegistry(JSON.parse(raw));
14
+ } catch (error) {
15
+ if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") return {
16
+ schemaVersion: 1,
17
+ apps: {}
18
+ };
19
+ throw error;
20
+ }
21
+ };
22
+ save = async (registry) => {
23
+ await this.appHomeService.ensureBaseDirectories();
24
+ await writeFile(this.appHomeService.getRegistryPath(), `${JSON.stringify(registry, null, 2)}\n`, "utf-8");
25
+ };
26
+ listApps = async () => {
27
+ const registry = await this.load();
28
+ return Object.values(registry.apps).sort((left, right) => left.appId.localeCompare(right.appId));
29
+ };
30
+ getApp = async (appId) => {
31
+ return (await this.load()).apps[appId];
32
+ };
33
+ getActiveVersion = async (appId) => {
34
+ const appRecord = await this.getApp(appId);
35
+ if (!appRecord) return;
36
+ return appRecord.installedVersions[appRecord.activeVersion];
37
+ };
38
+ upsertInstallation = async (params) => {
39
+ const { appId, name, description, version, installDirectory, dataDirectory, sourceKind, sourceRef, installedAt, permissions, registryUrl, bundleUrl, sha256, publisher } = params;
40
+ const registry = await this.load();
41
+ const currentRecord = registry.apps[appId];
42
+ const nextRecord = {
43
+ appId,
44
+ name,
45
+ description,
46
+ activeVersion: version,
47
+ dataDirectory,
48
+ installedVersions: {
49
+ ...currentRecord?.installedVersions ?? {},
50
+ [version]: {
51
+ version,
52
+ installDirectory,
53
+ sourceKind,
54
+ sourceRef,
55
+ installedAt,
56
+ permissions,
57
+ registryUrl,
58
+ bundleUrl,
59
+ sha256,
60
+ publisher
61
+ }
62
+ },
63
+ grants: currentRecord?.grants ?? {}
64
+ };
65
+ registry.apps[appId] = nextRecord;
66
+ await this.save(registry);
67
+ return nextRecord;
68
+ };
69
+ updateGrants = async (appId, grants) => {
70
+ const registry = await this.load();
71
+ const appRecord = registry.apps[appId];
72
+ if (!appRecord) throw new Error(`未找到已安装应用:${appId}`);
73
+ appRecord.grants = {
74
+ ...appRecord.grants,
75
+ ...grants
76
+ };
77
+ registry.apps[appId] = appRecord;
78
+ await this.save(registry);
79
+ return appRecord;
80
+ };
81
+ setDocumentGrant = async (appId, scopeId, directoryPath) => {
82
+ return this.updateGrants(appId, { [scopeId]: directoryPath });
83
+ };
84
+ removeDocumentGrant = async (appId, scopeId) => {
85
+ const registry = await this.load();
86
+ const appRecord = registry.apps[appId];
87
+ if (!appRecord) throw new Error(`未找到已安装应用:${appId}`);
88
+ if (!(scopeId in appRecord.grants)) return false;
89
+ delete appRecord.grants[scopeId];
90
+ registry.apps[appId] = appRecord;
91
+ await this.save(registry);
92
+ return true;
93
+ };
94
+ removeApp = async (appId) => {
95
+ const registry = await this.load();
96
+ const appRecord = registry.apps[appId];
97
+ if (!appRecord) return;
98
+ delete registry.apps[appId];
99
+ await this.save(registry);
100
+ return appRecord;
101
+ };
102
+ parseRegistry = (rawRegistry) => {
103
+ if (!rawRegistry || typeof rawRegistry !== "object" || Array.isArray(rawRegistry)) throw new Error("registry.json 必须是对象。");
104
+ const candidate = rawRegistry;
105
+ if (candidate.schemaVersion !== 1) throw new Error("当前只支持 registry schemaVersion = 1。");
106
+ const apps = candidate.apps;
107
+ if (!apps || typeof apps !== "object" || Array.isArray(apps)) throw new Error("registry.apps 必须是对象。");
108
+ return {
109
+ schemaVersion: 1,
110
+ apps
111
+ };
112
+ };
113
+ };
114
+ //#endregion
115
+ export { AppRegistryService };
@@ -0,0 +1,33 @@
1
+ import { AppPermissions } from "../manifest/app-manifest.types.js";
2
+ import { AppDocumentGrantMap } from "../permissions/app-permissions.types.js";
3
+ import { AppPublisher } from "./app-remote-registry.types.js";
4
+
5
+ //#region src/registry/app-registry.types.d.ts
6
+ type AppInstallSourceKind = "bundle" | "directory" | "registry";
7
+ type AppRegistryInstalledVersion = {
8
+ version: string;
9
+ installDirectory: string;
10
+ sourceKind: AppInstallSourceKind;
11
+ sourceRef: string;
12
+ installedAt: string;
13
+ permissions: AppPermissions;
14
+ registryUrl?: string;
15
+ bundleUrl?: string;
16
+ sha256?: string;
17
+ publisher?: AppPublisher;
18
+ };
19
+ type AppRegistryAppRecord = {
20
+ appId: string;
21
+ name: string;
22
+ description?: string;
23
+ activeVersion: string;
24
+ dataDirectory: string;
25
+ installedVersions: Record<string, AppRegistryInstalledVersion>;
26
+ grants: AppDocumentGrantMap;
27
+ };
28
+ type AppRegistry = {
29
+ schemaVersion: 1;
30
+ apps: Record<string, AppRegistryAppRecord>;
31
+ };
32
+ //#endregion
33
+ export { AppInstallSourceKind, AppRegistry, AppRegistryAppRecord, AppRegistryInstalledVersion };
@@ -0,0 +1,28 @@
1
+ import { AppRemoteRegistryResolution } from "./app-remote-registry.types.js";
2
+ import { AppRegistryConfigService } from "./app-registry-config.service.js";
3
+
4
+ //#region src/registry/app-remote-registry-client.service.d.ts
5
+ declare class AppRemoteRegistryClientService {
6
+ private readonly configService;
7
+ constructor(configService?: AppRegistryConfigService);
8
+ resolve: (params: {
9
+ appId: string;
10
+ version?: string;
11
+ registryUrl?: string;
12
+ }) => Promise<AppRemoteRegistryResolution>;
13
+ downloadBundle: (params: {
14
+ resolution: AppRemoteRegistryResolution;
15
+ targetDirectory: string;
16
+ }) => Promise<{
17
+ bundlePath: string;
18
+ }>;
19
+ private parseDocument;
20
+ private parseVersion;
21
+ private parsePublisher;
22
+ private readRequiredString;
23
+ private readOptionalString;
24
+ private normalizeBundleFileName;
25
+ private normalizeRegistryUrl;
26
+ }
27
+ //#endregion
28
+ export { AppRemoteRegistryClientService };
@@ -0,0 +1,114 @@
1
+ import { AppRegistryConfigService } from "./app-registry-config.service.js";
2
+ import { writeFile } from "node:fs/promises";
3
+ import { createHash } from "node:crypto";
4
+ import path from "node:path";
5
+ //#region src/registry/app-remote-registry-client.service.ts
6
+ var AppRemoteRegistryClientService = class {
7
+ constructor(configService = new AppRegistryConfigService()) {
8
+ this.configService = configService;
9
+ }
10
+ resolve = async (params) => {
11
+ const { appId, version: requestedVersion, registryUrl: overrideRegistryUrl } = params;
12
+ const registryUrl = overrideRegistryUrl ? this.normalizeRegistryUrl(overrideRegistryUrl) : (await this.configService.getSnapshot()).currentUrl;
13
+ const metadataUrl = new URL(encodeURIComponent(appId), registryUrl).toString();
14
+ const response = await fetch(metadataUrl);
15
+ if (!response.ok) throw new Error(`无法从 registry 拉取 ${appId} metadata:${response.status} ${response.statusText}`);
16
+ const document = this.parseDocument(await response.json(), appId);
17
+ const version = requestedVersion ?? document["dist-tags"].latest;
18
+ const versionRecord = document.versions[version];
19
+ if (!versionRecord) throw new Error(`registry ${registryUrl} 未提供 ${appId}@${version}。`);
20
+ return {
21
+ registryUrl,
22
+ metadataUrl,
23
+ appId,
24
+ version,
25
+ description: versionRecord.description ?? document.description,
26
+ publisher: versionRecord.publisher,
27
+ permissions: versionRecord.permissions,
28
+ bundleUrl: new URL(versionRecord.dist.bundle, metadataUrl).toString(),
29
+ sha256: versionRecord.dist.sha256
30
+ };
31
+ };
32
+ downloadBundle = async (params) => {
33
+ const { resolution, targetDirectory } = params;
34
+ const response = await fetch(resolution.bundleUrl);
35
+ if (!response.ok) throw new Error(`无法下载 bundle:${resolution.bundleUrl} (${response.status} ${response.statusText})`);
36
+ const bundleBytes = Buffer.from(await response.arrayBuffer());
37
+ const actualSha256 = createHash("sha256").update(bundleBytes).digest("hex");
38
+ if (actualSha256 !== resolution.sha256) throw new Error(`bundle checksum 校验失败:期望 ${resolution.sha256},实际 ${actualSha256}`);
39
+ const bundlePath = path.join(targetDirectory, `${this.normalizeBundleFileName(resolution.appId)}-${resolution.version}.napp`);
40
+ await writeFile(bundlePath, bundleBytes);
41
+ return { bundlePath };
42
+ };
43
+ parseDocument = (rawDocument, appId) => {
44
+ if (!rawDocument || typeof rawDocument !== "object" || Array.isArray(rawDocument)) throw new Error(`registry ${appId} metadata 必须是对象。`);
45
+ const candidate = rawDocument;
46
+ const name = this.readRequiredString(candidate.name, "name");
47
+ if (name !== appId) throw new Error(`registry 返回的包名与请求不一致:请求 ${appId},收到 ${name}`);
48
+ const distTags = candidate["dist-tags"];
49
+ if (!distTags || typeof distTags !== "object" || Array.isArray(distTags)) throw new Error("registry metadata 缺少 dist-tags。");
50
+ const versions = candidate.versions;
51
+ if (!versions || typeof versions !== "object" || Array.isArray(versions)) throw new Error("registry metadata 缺少 versions。");
52
+ return {
53
+ name,
54
+ description: this.readOptionalString(candidate.description, "description"),
55
+ "dist-tags": { latest: this.readRequiredString(distTags.latest, "dist-tags.latest") },
56
+ versions: Object.fromEntries(Object.entries(versions).map(([version, rawVersion]) => [version, this.parseVersion(rawVersion, name, version)]))
57
+ };
58
+ };
59
+ parseVersion = (rawVersion, appId, version) => {
60
+ if (!rawVersion || typeof rawVersion !== "object" || Array.isArray(rawVersion)) throw new Error(`registry metadata 的 versions.${version} 必须是对象。`);
61
+ const candidate = rawVersion;
62
+ const name = this.readRequiredString(candidate.name, `versions.${version}.name`);
63
+ const parsedVersion = this.readRequiredString(candidate.version, `versions.${version}.version`);
64
+ if (name !== appId || parsedVersion !== version) throw new Error(`registry metadata 的 versions.${version} 与 app id/version 不一致。`);
65
+ const dist = candidate.dist;
66
+ if (!dist || typeof dist !== "object" || Array.isArray(dist)) throw new Error(`registry metadata 的 versions.${version}.dist 必须是对象。`);
67
+ const distCandidate = dist;
68
+ return {
69
+ name,
70
+ version: parsedVersion,
71
+ description: this.readOptionalString(candidate.description, `versions.${version}.description`),
72
+ publisher: this.parsePublisher(candidate.publisher, version),
73
+ permissions: candidate.permissions,
74
+ dist: {
75
+ bundle: this.readRequiredString(distCandidate.bundle, `versions.${version}.dist.bundle`),
76
+ sha256: this.readRequiredString(distCandidate.sha256, `versions.${version}.dist.sha256`)
77
+ }
78
+ };
79
+ };
80
+ parsePublisher = (rawPublisher, version) => {
81
+ if (rawPublisher === void 0) return;
82
+ if (!rawPublisher || typeof rawPublisher !== "object" || Array.isArray(rawPublisher)) throw new Error(`versions.${version}.publisher 必须是对象。`);
83
+ const candidate = rawPublisher;
84
+ return {
85
+ id: this.readRequiredString(candidate.id, `versions.${version}.publisher.id`),
86
+ name: this.readRequiredString(candidate.name, `versions.${version}.publisher.name`),
87
+ url: this.readOptionalString(candidate.url, `versions.${version}.publisher.url`)
88
+ };
89
+ };
90
+ readRequiredString = (value, fieldName) => {
91
+ if (typeof value !== "string" || !value.trim()) throw new Error(`${fieldName} 必须是非空字符串。`);
92
+ return value.trim();
93
+ };
94
+ readOptionalString = (value, fieldName) => {
95
+ if (value === void 0) return;
96
+ return this.readRequiredString(value, fieldName);
97
+ };
98
+ normalizeBundleFileName = (appId) => {
99
+ return appId.replace(/[^a-zA-Z0-9._-]+/g, "-");
100
+ };
101
+ normalizeRegistryUrl = (registryUrl) => {
102
+ let normalized;
103
+ try {
104
+ normalized = new URL(registryUrl);
105
+ } catch {
106
+ throw new Error(`非法 registry URL:${registryUrl}`);
107
+ }
108
+ if (normalized.protocol !== "http:" && normalized.protocol !== "https:") throw new Error(`registry URL 只支持 http/https:${registryUrl}`);
109
+ const text = normalized.toString();
110
+ return text.endsWith("/") ? text : `${text}/`;
111
+ };
112
+ };
113
+ //#endregion
114
+ export { AppRemoteRegistryClientService };
@@ -0,0 +1,52 @@
1
+ import { AppPermissions } from "../manifest/app-manifest.types.js";
2
+
3
+ //#region src/registry/app-remote-registry.types.d.ts
4
+ declare const DEFAULT_APP_REGISTRY_URL = "https://apps-registry.nextclaw.io/api/v1/apps/registry/";
5
+ type AppPublisher = {
6
+ id: string;
7
+ name: string;
8
+ url?: string;
9
+ };
10
+ type AppRegistryConfig = {
11
+ schemaVersion: 1;
12
+ registry: {
13
+ url: string;
14
+ };
15
+ };
16
+ type AppRegistryConfigSnapshot = {
17
+ defaultUrl: string;
18
+ currentUrl: string;
19
+ source: "default" | "config" | "env";
20
+ };
21
+ type AppRemoteRegistryVersion = {
22
+ name: string;
23
+ version: string;
24
+ description?: string;
25
+ publisher?: AppPublisher;
26
+ permissions?: AppPermissions;
27
+ dist: {
28
+ bundle: string;
29
+ sha256: string;
30
+ };
31
+ };
32
+ type AppRemoteRegistryDocument = {
33
+ name: string;
34
+ description?: string;
35
+ "dist-tags": {
36
+ latest: string;
37
+ };
38
+ versions: Record<string, AppRemoteRegistryVersion>;
39
+ };
40
+ type AppRemoteRegistryResolution = {
41
+ registryUrl: string;
42
+ metadataUrl: string;
43
+ appId: string;
44
+ version: string;
45
+ description?: string;
46
+ publisher?: AppPublisher;
47
+ permissions?: AppPermissions;
48
+ bundleUrl: string;
49
+ sha256: string;
50
+ };
51
+ //#endregion
52
+ export { AppPublisher, AppRegistryConfig, AppRegistryConfigSnapshot, AppRemoteRegistryDocument, AppRemoteRegistryResolution, AppRemoteRegistryVersion, DEFAULT_APP_REGISTRY_URL };
@@ -0,0 +1,4 @@
1
+ //#region src/registry/app-remote-registry.types.ts
2
+ const DEFAULT_APP_REGISTRY_URL = "https://apps-registry.nextclaw.io/api/v1/apps/registry/";
3
+ //#endregion
4
+ export { DEFAULT_APP_REGISTRY_URL };
@@ -0,0 +1,21 @@
1
+ //#region src/scaffold/app-scaffold.service.d.ts
2
+ type AppScaffoldResult = {
3
+ appDirectory: string;
4
+ manifestPath: string;
5
+ };
6
+ declare class AppScaffoldService {
7
+ scaffold: (targetDirectory: string) => Promise<AppScaffoldResult>;
8
+ private assertTargetDoesNotExist;
9
+ private buildAppId;
10
+ private buildAppName;
11
+ private normalizeSlug;
12
+ private buildManifest;
13
+ private buildWatSource;
14
+ private buildUiHtml;
15
+ private buildMarketplaceMetadata;
16
+ private buildReadme;
17
+ private buildUiController;
18
+ private buildIconSvg;
19
+ }
20
+ //#endregion
21
+ export { AppScaffoldService };
@@ -13,6 +13,8 @@ var AppScaffoldService = class {
13
13
  const manifestPath = path.join(appDirectory, "manifest.json");
14
14
  await Promise.all([
15
15
  writeFile(manifestPath, `${JSON.stringify(this.buildManifest(appId, appName), null, 2)}\n`, "utf-8"),
16
+ writeFile(path.join(appDirectory, "marketplace.json"), `${JSON.stringify(this.buildMarketplaceMetadata(appName), null, 2)}\n`, "utf-8"),
17
+ writeFile(path.join(appDirectory, "README.md"), this.buildReadme(appName, appId), "utf-8"),
16
18
  writeFile(path.join(appDirectory, "main", "app.wasm"), Buffer.from(APP_WASM_BASE64, "base64")),
17
19
  writeFile(path.join(appDirectory, "main", "app.wat"), this.buildWatSource(), "utf-8"),
18
20
  writeFile(path.join(appDirectory, "ui", "index.html"), this.buildUiHtml(appName), "utf-8"),
@@ -191,6 +193,58 @@ var AppScaffoldService = class {
191
193
  <script type="module" src="./app.controller.js"><\/script>
192
194
  </body>
193
195
  </html>
196
+ `;
197
+ };
198
+ buildMarketplaceMetadata = (appName) => {
199
+ return {
200
+ slug: this.normalizeSlug(appName),
201
+ summary: `A minimal ${appName} app scaffold created by napp.`,
202
+ summaryI18n: {
203
+ en: `A minimal ${appName} app scaffold created by napp.`,
204
+ zh: `一个由 napp 创建的最小 ${appName} 应用骨架。`
205
+ },
206
+ description: `A minimal NextClaw app scaffold for ${appName}, ready for local run and marketplace publish.`,
207
+ descriptionI18n: {
208
+ en: `A minimal NextClaw app scaffold for ${appName}, ready for local run and marketplace publish.`,
209
+ zh: `一个面向 ${appName} 的最小 NextClaw 应用骨架,可直接本地运行并发布到 marketplace。`
210
+ },
211
+ author: "NextClaw",
212
+ tags: [
213
+ "starter",
214
+ "official",
215
+ "local"
216
+ ],
217
+ sourceRepo: "https://github.com/Peiiii/nextclaw",
218
+ homepage: "https://nextclaw.io",
219
+ featured: false,
220
+ publisher: {
221
+ id: "nextclaw",
222
+ name: "NextClaw",
223
+ url: "https://nextclaw.io"
224
+ }
225
+ };
226
+ };
227
+ buildReadme = (appName, appId) => {
228
+ return `# ${appName}
229
+
230
+ This starter app was created by \`napp create\`.
231
+
232
+ ## Local workflow
233
+
234
+ \`\`\`bash
235
+ napp inspect .
236
+ napp run .
237
+ \`\`\`
238
+
239
+ ## Publish workflow
240
+
241
+ \`\`\`bash
242
+ napp publish .
243
+ \`\`\`
244
+
245
+ ## App ID
246
+
247
+ \`${appId}\`
194
248
  `;
195
249
  };
196
250
  buildUiController = () => {
@@ -255,6 +309,6 @@ await loadManifest();
255
309
  `;
256
310
  };
257
311
  };
258
- const APP_WASM_BASE64 = "AGFzbQEAAAABBwFgAn9/AX8DAgEABxMBD3N1bW1hcml6ZV9ub3RlcwAACg0BCwAgACABagsQQQQK";
312
+ const APP_WASM_BASE64 = "AGFzbQEAAAABBwFgAn9/AX8DAgEABxMBD3N1bW1hcml6ZV9ub3RlcwAACg0BCwAgACABakHIAWoL";
259
313
  //#endregion
260
314
  export { AppScaffoldService };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextclaw/app-runtime",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "private": false,
5
5
  "description": "Standalone micro app runtime and CLI for NextClaw apps.",
6
6
  "type": "module",
@@ -38,7 +38,9 @@
38
38
  "dist",
39
39
  "README.md"
40
40
  ],
41
- "dependencies": {},
41
+ "dependencies": {
42
+ "fflate": "^0.8.2"
43
+ },
42
44
  "devDependencies": {
43
45
  "@types/node": "^20.17.6",
44
46
  "prettier": "^3.3.3",