@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.
- package/README.md +125 -4
- package/dist/bundle/app-bundle.service.d.ts +27 -0
- package/dist/bundle/app-bundle.service.js +133 -0
- package/dist/bundle/app-bundle.types.d.ts +24 -0
- package/dist/cli/app-runtime-cli.service.d.ts +28 -0
- package/dist/cli/app-runtime-cli.service.js +298 -0
- package/dist/cli/app-runtime-options.service.d.ts +67 -0
- package/dist/cli/app-runtime-options.service.js +242 -0
- package/dist/commands/create.controller.d.ts +14 -0
- package/dist/commands/grant.controller.d.ts +16 -0
- package/dist/commands/grant.controller.js +25 -0
- package/dist/commands/info.controller.d.ts +14 -0
- package/dist/commands/info.controller.js +27 -0
- package/dist/commands/install.controller.d.ts +15 -0
- package/dist/commands/install.controller.js +24 -0
- package/dist/commands/list.controller.d.ts +13 -0
- package/dist/commands/list.controller.js +25 -0
- package/dist/commands/pack.controller.d.ts +15 -0
- package/dist/commands/pack.controller.js +25 -0
- package/dist/commands/permissions.controller.d.ts +14 -0
- package/dist/commands/permissions.controller.js +26 -0
- package/dist/commands/publish.controller.d.ts +17 -0
- package/dist/commands/publish.controller.js +29 -0
- package/dist/commands/registry.controller.d.ts +16 -0
- package/dist/commands/registry.controller.js +27 -0
- package/dist/commands/revoke.controller.d.ts +15 -0
- package/dist/commands/revoke.controller.js +24 -0
- package/dist/commands/run.controller.js +14 -6
- package/dist/commands/uninstall.controller.d.ts +15 -0
- package/dist/commands/uninstall.controller.js +23 -0
- package/dist/commands/update.controller.d.ts +16 -0
- package/dist/commands/update.controller.js +29 -0
- package/dist/host/app-instance.service.d.ts +3 -1
- package/dist/host/app-instance.service.js +2 -2
- package/dist/index.d.ts +31 -2
- package/dist/index.js +28 -2
- package/dist/install/app-installation.service.d.ts +37 -0
- package/dist/install/app-installation.service.js +255 -0
- package/dist/install/app-installation.types.d.ts +62 -0
- package/dist/main.js +5 -132
- package/dist/package.js +1 -1
- package/dist/paths/app-home.service.d.ts +16 -0
- package/dist/paths/app-home.service.js +42 -0
- package/dist/permissions/app-grant.service.d.ts +22 -0
- package/dist/permissions/app-grant.service.js +63 -0
- package/dist/permissions/app-permissions.service.d.ts +3 -1
- package/dist/permissions/app-permissions.service.js +12 -5
- package/dist/permissions/app-permissions.types.d.ts +28 -1
- package/dist/publish/app-marketplace-client.service.d.ts +13 -0
- package/dist/publish/app-marketplace-client.service.js +31 -0
- package/dist/publish/app-marketplace-metadata.service.d.ts +27 -0
- package/dist/publish/app-marketplace-metadata.service.js +84 -0
- package/dist/publish/app-publish.service.d.ts +22 -0
- package/dist/publish/app-publish.service.js +74 -0
- package/dist/publish/app-publish.types.d.ts +66 -0
- package/dist/publish/app-publish.types.js +4 -0
- package/dist/registry/app-registry-config.service.d.ts +16 -0
- package/dist/registry/app-registry-config.service.js +84 -0
- package/dist/registry/app-registry.service.d.ts +38 -0
- package/dist/registry/app-registry.service.js +115 -0
- package/dist/registry/app-registry.types.d.ts +33 -0
- package/dist/registry/app-remote-registry-client.service.d.ts +28 -0
- package/dist/registry/app-remote-registry-client.service.js +114 -0
- package/dist/registry/app-remote-registry.types.d.ts +52 -0
- package/dist/registry/app-remote-registry.types.js +4 -0
- package/dist/scaffold/app-scaffold.service.d.ts +21 -0
- package/dist/scaffold/app-scaffold.service.js +55 -1
- 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,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,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/
|
|
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.
|
|
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",
|