@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
package/dist/main.js
CHANGED
|
@@ -1,136 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import { CreateCommand } from "./commands/create.controller.js";
|
|
4
|
-
import { RunCommand } from "./commands/run.controller.js";
|
|
5
|
-
import { DevCommand } from "./commands/dev.controller.js";
|
|
6
|
-
import { InspectCommand } from "./commands/inspect.controller.js";
|
|
2
|
+
import { AppRuntimeCliService } from "./cli/app-runtime-cli.service.js";
|
|
7
3
|
//#region src/main.ts
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
this.writeUsage();
|
|
13
|
-
return;
|
|
14
|
-
}
|
|
15
|
-
if (command === "--version" || command === "version") {
|
|
16
|
-
this.write(`${version}\n`);
|
|
17
|
-
return;
|
|
18
|
-
}
|
|
19
|
-
if (!command || !appDirectory) {
|
|
20
|
-
this.writeUsage();
|
|
21
|
-
process.exit(1);
|
|
22
|
-
}
|
|
23
|
-
if (command === "create") {
|
|
24
|
-
const options = this.readCreateOptions(restArgs);
|
|
25
|
-
await new CreateCommand().run({
|
|
26
|
-
appDirectory,
|
|
27
|
-
json: options.json,
|
|
28
|
-
write: this.write
|
|
29
|
-
});
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
const options = this.readRuntimeOptions(restArgs);
|
|
33
|
-
if (command === "inspect") {
|
|
34
|
-
await new InspectCommand().run({
|
|
35
|
-
appDirectory,
|
|
36
|
-
json: options.json,
|
|
37
|
-
write: this.write
|
|
38
|
-
});
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
if (command === "run") {
|
|
42
|
-
await new RunCommand().run({
|
|
43
|
-
appDirectory,
|
|
44
|
-
host: options.host,
|
|
45
|
-
port: options.port,
|
|
46
|
-
json: options.json,
|
|
47
|
-
documentGrantMap: options.documentGrantMap,
|
|
48
|
-
write: this.write
|
|
49
|
-
});
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
if (command === "dev") {
|
|
53
|
-
await new DevCommand().run({
|
|
54
|
-
appDirectory,
|
|
55
|
-
host: options.host,
|
|
56
|
-
port: options.port,
|
|
57
|
-
json: options.json,
|
|
58
|
-
documentGrantMap: options.documentGrantMap,
|
|
59
|
-
write: this.write
|
|
60
|
-
});
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
this.writeUsage();
|
|
64
|
-
process.exit(1);
|
|
65
|
-
};
|
|
66
|
-
readCreateOptions = (rawArgs) => {
|
|
67
|
-
const options = { json: false };
|
|
68
|
-
for (const current of rawArgs) {
|
|
69
|
-
if (!current?.startsWith("--")) throw new Error(`未知参数:${current}`);
|
|
70
|
-
switch (current) {
|
|
71
|
-
case "--json":
|
|
72
|
-
options.json = true;
|
|
73
|
-
break;
|
|
74
|
-
default: throw new Error(`未知参数:${current}`);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
return options;
|
|
78
|
-
};
|
|
79
|
-
readRuntimeOptions = (rawArgs) => {
|
|
80
|
-
const options = {
|
|
81
|
-
host: "127.0.0.1",
|
|
82
|
-
port: 3100,
|
|
83
|
-
json: false,
|
|
84
|
-
documentGrantMap: {}
|
|
85
|
-
};
|
|
86
|
-
for (let index = 0; index < rawArgs.length; index += 1) {
|
|
87
|
-
const current = rawArgs[index];
|
|
88
|
-
if (!current?.startsWith("--")) throw new Error(`未知参数:${current}`);
|
|
89
|
-
const nextValue = rawArgs[index + 1];
|
|
90
|
-
switch (current) {
|
|
91
|
-
case "--host":
|
|
92
|
-
options.host = this.requireOptionValue(current, nextValue);
|
|
93
|
-
index += 1;
|
|
94
|
-
break;
|
|
95
|
-
case "--port":
|
|
96
|
-
options.port = Number.parseInt(this.requireOptionValue(current, nextValue), 10);
|
|
97
|
-
if (Number.isNaN(options.port) || options.port < 0) throw new Error("--port 必须是非负整数。");
|
|
98
|
-
index += 1;
|
|
99
|
-
break;
|
|
100
|
-
case "--json":
|
|
101
|
-
options.json = true;
|
|
102
|
-
break;
|
|
103
|
-
case "--document":
|
|
104
|
-
this.assignDocumentGrant(options.documentGrantMap, this.requireOptionValue(current, nextValue));
|
|
105
|
-
index += 1;
|
|
106
|
-
break;
|
|
107
|
-
default: throw new Error(`未知参数:${current}`);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
return options;
|
|
111
|
-
};
|
|
112
|
-
assignDocumentGrant = (documentGrantMap, rawGrant) => {
|
|
113
|
-
const delimiterIndex = rawGrant.indexOf("=");
|
|
114
|
-
if (delimiterIndex < 1) throw new Error("--document 必须使用 scopeId=/absolute/or/relative/path 格式。");
|
|
115
|
-
const scopeId = rawGrant.slice(0, delimiterIndex).trim();
|
|
116
|
-
const directoryPath = rawGrant.slice(delimiterIndex + 1).trim();
|
|
117
|
-
if (!scopeId || !directoryPath) throw new Error("--document 缺少 scopeId 或 path。");
|
|
118
|
-
documentGrantMap[scopeId] = directoryPath;
|
|
119
|
-
};
|
|
120
|
-
requireOptionValue = (flag, nextValue) => {
|
|
121
|
-
if (!nextValue || nextValue.startsWith("--")) throw new Error(`${flag} 缺少值。`);
|
|
122
|
-
return nextValue;
|
|
123
|
-
};
|
|
124
|
-
writeUsage = () => {
|
|
125
|
-
this.write("Usage: napp create <app-dir> [--json]\n");
|
|
126
|
-
this.write(" napp <inspect|run|dev> <app-dir> [--host 127.0.0.1] [--port 3100] [--json] [--document scope=/path]\n");
|
|
127
|
-
this.write(" napp --help\n");
|
|
128
|
-
this.write(" napp --version\n");
|
|
129
|
-
};
|
|
130
|
-
write = (text) => {
|
|
131
|
-
process.stdout.write(text);
|
|
132
|
-
};
|
|
133
|
-
};
|
|
134
|
-
new AppRuntimeCli().run();
|
|
4
|
+
new AppRuntimeCliService().run().catch((error) => {
|
|
5
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
6
|
+
process.exit(1);
|
|
7
|
+
});
|
|
135
8
|
//#endregion
|
|
136
9
|
export {};
|
package/dist/package.js
CHANGED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//#region src/paths/app-home.service.d.ts
|
|
2
|
+
declare class AppHomeService {
|
|
3
|
+
private readonly appHomeDirectory;
|
|
4
|
+
constructor(appHomeDirectory?: string);
|
|
5
|
+
getAppHomeDirectory: () => string;
|
|
6
|
+
getPackagesDirectory: () => string;
|
|
7
|
+
getDataDirectory: () => string;
|
|
8
|
+
getRegistryPath: () => string;
|
|
9
|
+
getConfigPath: () => string;
|
|
10
|
+
getInstallDirectory: (appId: string, version: string) => string;
|
|
11
|
+
getAppDataDirectory: (appId: string) => string;
|
|
12
|
+
ensureBaseDirectories: () => Promise<void>;
|
|
13
|
+
createTemporaryDirectory: (prefix: string) => Promise<string>;
|
|
14
|
+
}
|
|
15
|
+
//#endregion
|
|
16
|
+
export { AppHomeService };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { mkdir, mkdtemp } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
//#region src/paths/app-home.service.ts
|
|
5
|
+
var AppHomeService = class {
|
|
6
|
+
constructor(appHomeDirectory = process.env.NEXTCLAW_APP_HOME ? path.resolve(process.env.NEXTCLAW_APP_HOME) : path.join(process.env.HOME ?? process.cwd(), ".nextclaw", "apps")) {
|
|
7
|
+
this.appHomeDirectory = appHomeDirectory;
|
|
8
|
+
}
|
|
9
|
+
getAppHomeDirectory = () => {
|
|
10
|
+
return this.appHomeDirectory;
|
|
11
|
+
};
|
|
12
|
+
getPackagesDirectory = () => {
|
|
13
|
+
return path.join(this.appHomeDirectory, "packages");
|
|
14
|
+
};
|
|
15
|
+
getDataDirectory = () => {
|
|
16
|
+
return path.join(this.appHomeDirectory, "data");
|
|
17
|
+
};
|
|
18
|
+
getRegistryPath = () => {
|
|
19
|
+
return path.join(this.appHomeDirectory, "registry.json");
|
|
20
|
+
};
|
|
21
|
+
getConfigPath = () => {
|
|
22
|
+
return path.join(this.appHomeDirectory, "config.json");
|
|
23
|
+
};
|
|
24
|
+
getInstallDirectory = (appId, version) => {
|
|
25
|
+
return path.join(this.getPackagesDirectory(), appId, version);
|
|
26
|
+
};
|
|
27
|
+
getAppDataDirectory = (appId) => {
|
|
28
|
+
return path.join(this.getDataDirectory(), appId);
|
|
29
|
+
};
|
|
30
|
+
ensureBaseDirectories = async () => {
|
|
31
|
+
await Promise.all([
|
|
32
|
+
mkdir(this.appHomeDirectory, { recursive: true }),
|
|
33
|
+
mkdir(this.getPackagesDirectory(), { recursive: true }),
|
|
34
|
+
mkdir(this.getDataDirectory(), { recursive: true })
|
|
35
|
+
]);
|
|
36
|
+
};
|
|
37
|
+
createTemporaryDirectory = async (prefix) => {
|
|
38
|
+
return mkdtemp(path.join(tmpdir(), prefix));
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
//#endregion
|
|
42
|
+
export { AppHomeService };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { AppManifestService } from "../manifest/app-manifest.service.js";
|
|
2
|
+
import { AppDocumentGrantMutationResult, AppInstalledPermissionState } from "./app-permissions.types.js";
|
|
3
|
+
import { AppRegistryService } from "../registry/app-registry.service.js";
|
|
4
|
+
|
|
5
|
+
//#region src/permissions/app-grant.service.d.ts
|
|
6
|
+
declare class AppGrantService {
|
|
7
|
+
private readonly registryService;
|
|
8
|
+
private readonly manifestService;
|
|
9
|
+
constructor(registryService?: AppRegistryService, manifestService?: AppManifestService);
|
|
10
|
+
summarize: (appId: string) => Promise<AppInstalledPermissionState>;
|
|
11
|
+
grantDocumentScope: (params: {
|
|
12
|
+
appId: string;
|
|
13
|
+
scopeId: string;
|
|
14
|
+
directoryPath: string;
|
|
15
|
+
}) => Promise<AppDocumentGrantMutationResult>;
|
|
16
|
+
revokeDocumentScope: (params: {
|
|
17
|
+
appId: string;
|
|
18
|
+
scopeId: string;
|
|
19
|
+
}) => Promise<AppDocumentGrantMutationResult>;
|
|
20
|
+
}
|
|
21
|
+
//#endregion
|
|
22
|
+
export { AppGrantService };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { AppManifestService } from "../manifest/app-manifest.service.js";
|
|
2
|
+
import { AppRegistryService } from "../registry/app-registry.service.js";
|
|
3
|
+
import { access } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
//#region src/permissions/app-grant.service.ts
|
|
6
|
+
var AppGrantService = class {
|
|
7
|
+
constructor(registryService = new AppRegistryService(), manifestService = new AppManifestService()) {
|
|
8
|
+
this.registryService = registryService;
|
|
9
|
+
this.manifestService = manifestService;
|
|
10
|
+
}
|
|
11
|
+
summarize = async (appId) => {
|
|
12
|
+
const appRecord = await this.registryService.getApp(appId);
|
|
13
|
+
if (!appRecord) throw new Error(`未找到已安装应用:${appId}`);
|
|
14
|
+
const activeVersion = appRecord.installedVersions[appRecord.activeVersion];
|
|
15
|
+
if (!activeVersion) throw new Error(`已安装应用缺少激活版本:${appId}`);
|
|
16
|
+
const requestedPermissions = (await this.manifestService.load(activeVersion.installDirectory)).manifest.permissions ?? {};
|
|
17
|
+
return {
|
|
18
|
+
appId: appRecord.appId,
|
|
19
|
+
name: appRecord.name,
|
|
20
|
+
activeVersion: appRecord.activeVersion,
|
|
21
|
+
documentAccess: (requestedPermissions.documentAccess ?? []).map((scope) => ({
|
|
22
|
+
id: scope.id,
|
|
23
|
+
mode: scope.mode,
|
|
24
|
+
description: scope.description,
|
|
25
|
+
granted: Boolean(appRecord.grants[scope.id]),
|
|
26
|
+
grantedPath: appRecord.grants[scope.id]
|
|
27
|
+
})),
|
|
28
|
+
allowedDomains: requestedPermissions.allowedDomains ?? [],
|
|
29
|
+
storage: {
|
|
30
|
+
enabled: requestedPermissions.storage !== void 0 && requestedPermissions.storage !== false,
|
|
31
|
+
namespace: typeof requestedPermissions.storage === "object" ? requestedPermissions.storage.namespace : void 0
|
|
32
|
+
},
|
|
33
|
+
capabilities: { hostBridge: requestedPermissions.capabilities?.hostBridge !== false }
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
grantDocumentScope = async (params) => {
|
|
37
|
+
const { appId, scopeId, directoryPath } = params;
|
|
38
|
+
if (!(await this.summarize(appId)).documentAccess.find((scope) => scope.id === scopeId)) throw new Error(`应用 ${appId} 未声明 documentAccess scope:${scopeId}`);
|
|
39
|
+
const normalizedDirectory = path.resolve(directoryPath);
|
|
40
|
+
try {
|
|
41
|
+
await access(normalizedDirectory);
|
|
42
|
+
} catch {
|
|
43
|
+
throw new Error(`授权目录不存在:${normalizedDirectory}`);
|
|
44
|
+
}
|
|
45
|
+
await this.registryService.setDocumentGrant(appId, scopeId, normalizedDirectory);
|
|
46
|
+
return {
|
|
47
|
+
appId,
|
|
48
|
+
scopeId,
|
|
49
|
+
grantedPath: normalizedDirectory
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
revokeDocumentScope = async (params) => {
|
|
53
|
+
const { appId, scopeId } = params;
|
|
54
|
+
await this.summarize(appId);
|
|
55
|
+
return {
|
|
56
|
+
appId,
|
|
57
|
+
scopeId,
|
|
58
|
+
removed: await this.registryService.removeDocumentGrant(appId, scopeId)
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
//#endregion
|
|
63
|
+
export { AppGrantService };
|
|
@@ -3,7 +3,9 @@ import { AppDocumentGrantMap, AppPermissionSummary, ResolvedPermissions } from "
|
|
|
3
3
|
|
|
4
4
|
//#region src/permissions/app-permissions.service.d.ts
|
|
5
5
|
declare class AppPermissionsService {
|
|
6
|
-
resolve: (bundle: AppManifestBundle, documentGrantMap: AppDocumentGrantMap
|
|
6
|
+
resolve: (bundle: AppManifestBundle, documentGrantMap: AppDocumentGrantMap, context?: {
|
|
7
|
+
appId?: string;
|
|
8
|
+
}) => Promise<ResolvedPermissions>;
|
|
7
9
|
summarize: (bundle: AppManifestBundle, permissions: ResolvedPermissions) => AppPermissionSummary;
|
|
8
10
|
private resolveDocumentGrant;
|
|
9
11
|
}
|
|
@@ -2,10 +2,10 @@ import { access } from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
//#region src/permissions/app-permissions.service.ts
|
|
4
4
|
var AppPermissionsService = class {
|
|
5
|
-
resolve = async (bundle, documentGrantMap) => {
|
|
5
|
+
resolve = async (bundle, documentGrantMap, context) => {
|
|
6
6
|
const requestedPermissions = bundle.manifest.permissions ?? {};
|
|
7
7
|
return {
|
|
8
|
-
documentAccess: requestedPermissions.documentAccess === void 0 ? [] : await Promise.all(requestedPermissions.documentAccess.map((scope) => this.resolveDocumentGrant(scope, documentGrantMap))),
|
|
8
|
+
documentAccess: requestedPermissions.documentAccess === void 0 ? [] : await Promise.all(requestedPermissions.documentAccess.map((scope) => this.resolveDocumentGrant(scope, documentGrantMap, context?.appId))),
|
|
9
9
|
allowedDomains: requestedPermissions.allowedDomains ?? [],
|
|
10
10
|
storage: {
|
|
11
11
|
enabled: requestedPermissions.storage !== void 0 && requestedPermissions.storage !== false,
|
|
@@ -23,11 +23,18 @@ var AppPermissionsService = class {
|
|
|
23
23
|
capabilities: permissions.capabilities
|
|
24
24
|
};
|
|
25
25
|
};
|
|
26
|
-
resolveDocumentGrant = async (scope, documentGrantMap) => {
|
|
26
|
+
resolveDocumentGrant = async (scope, documentGrantMap, appId) => {
|
|
27
27
|
const grantedPath = documentGrantMap[scope.id];
|
|
28
|
-
if (!grantedPath)
|
|
28
|
+
if (!grantedPath) {
|
|
29
|
+
if (appId) throw new Error(`缺少 documentAccess 授权:${scope.id}。请先执行 napp grant ${appId} --document ${scope.id}=/absolute/path`);
|
|
30
|
+
throw new Error(`缺少 documentAccess 授权:${scope.id}。请通过 --document ${scope.id}=/absolute/path 提供授权。`);
|
|
31
|
+
}
|
|
29
32
|
const normalizedPath = path.resolve(grantedPath);
|
|
30
|
-
|
|
33
|
+
try {
|
|
34
|
+
await access(normalizedPath);
|
|
35
|
+
} catch {
|
|
36
|
+
throw new Error(`documentAccess 授权路径不存在:${scope.id} -> ${normalizedPath}`);
|
|
37
|
+
}
|
|
31
38
|
return {
|
|
32
39
|
id: scope.id,
|
|
33
40
|
mode: scope.mode,
|
|
@@ -22,5 +22,32 @@ type ResolvedPermissions = {
|
|
|
22
22
|
type AppPermissionSummary = Pick<ResolvedPermissions, "documentAccess" | "allowedDomains" | "storage" | "capabilities"> & {
|
|
23
23
|
requested: AppPermissions;
|
|
24
24
|
};
|
|
25
|
+
type AppDocumentGrantState = {
|
|
26
|
+
id: string;
|
|
27
|
+
mode: "read" | "read-write";
|
|
28
|
+
description?: string;
|
|
29
|
+
granted: boolean;
|
|
30
|
+
grantedPath?: string;
|
|
31
|
+
};
|
|
32
|
+
type AppInstalledPermissionState = {
|
|
33
|
+
appId: string;
|
|
34
|
+
name: string;
|
|
35
|
+
activeVersion: string;
|
|
36
|
+
documentAccess: AppDocumentGrantState[];
|
|
37
|
+
allowedDomains: string[];
|
|
38
|
+
storage: {
|
|
39
|
+
enabled: boolean;
|
|
40
|
+
namespace?: string;
|
|
41
|
+
};
|
|
42
|
+
capabilities: {
|
|
43
|
+
hostBridge: boolean;
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
type AppDocumentGrantMutationResult = {
|
|
47
|
+
appId: string;
|
|
48
|
+
scopeId: string;
|
|
49
|
+
grantedPath?: string;
|
|
50
|
+
removed?: boolean;
|
|
51
|
+
};
|
|
25
52
|
//#endregion
|
|
26
|
-
export { AppDocumentGrantMap, AppPermissionSummary, ResolvedDocumentGrant, ResolvedPermissions };
|
|
53
|
+
export { AppDocumentGrantMap, AppDocumentGrantMutationResult, AppDocumentGrantState, AppInstalledPermissionState, AppPermissionSummary, ResolvedDocumentGrant, ResolvedPermissions };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { AppPublishPayload, AppPublishResult } from "./app-publish.types.js";
|
|
2
|
+
|
|
3
|
+
//#region src/publish/app-marketplace-client.service.d.ts
|
|
4
|
+
declare class AppMarketplaceClientService {
|
|
5
|
+
publish: (params: {
|
|
6
|
+
payload: AppPublishPayload;
|
|
7
|
+
apiBaseUrl?: string;
|
|
8
|
+
token?: string;
|
|
9
|
+
}) => Promise<AppPublishResult>;
|
|
10
|
+
private normalizeApiBase;
|
|
11
|
+
}
|
|
12
|
+
//#endregion
|
|
13
|
+
export { AppMarketplaceClientService };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import "./app-publish.types.js";
|
|
2
|
+
//#region src/publish/app-marketplace-client.service.ts
|
|
3
|
+
var AppMarketplaceClientService = class {
|
|
4
|
+
publish = async (params) => {
|
|
5
|
+
const apiBaseUrl = this.normalizeApiBase(params.apiBaseUrl ?? "https://apps-registry.nextclaw.io");
|
|
6
|
+
const token = params.token?.trim() || process.env.NEXTCLAW_MARKETPLACE_ADMIN_TOKEN?.trim();
|
|
7
|
+
if (!token) throw new Error("缺少 marketplace admin token,请传 --token 或设置 NEXTCLAW_MARKETPLACE_ADMIN_TOKEN。");
|
|
8
|
+
const response = await fetch(`${apiBaseUrl}/api/v1/apps/publish`, {
|
|
9
|
+
method: "POST",
|
|
10
|
+
headers: {
|
|
11
|
+
"content-type": "application/json",
|
|
12
|
+
authorization: `Bearer ${token}`,
|
|
13
|
+
accept: "application/json"
|
|
14
|
+
},
|
|
15
|
+
body: JSON.stringify(params.payload)
|
|
16
|
+
});
|
|
17
|
+
const payload = await response.json().catch(() => null);
|
|
18
|
+
if (!response.ok) {
|
|
19
|
+
const message = typeof payload === "object" && payload && "error" in payload && typeof payload.error?.message === "string" ? payload.error.message : `${response.status} ${response.statusText}`;
|
|
20
|
+
throw new Error(`发布 app 失败:${message}`);
|
|
21
|
+
}
|
|
22
|
+
const data = typeof payload === "object" && payload && "data" in payload && typeof payload.data === "object" ? payload.data : null;
|
|
23
|
+
if (!data) throw new Error("marketplace publish 返回格式无效。");
|
|
24
|
+
return data;
|
|
25
|
+
};
|
|
26
|
+
normalizeApiBase = (apiBaseUrl) => {
|
|
27
|
+
return new URL(apiBaseUrl).toString().replace(/\/+$/, "");
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
//#endregion
|
|
31
|
+
export { AppMarketplaceClientService };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { AppManifest } from "../manifest/app-manifest.types.js";
|
|
2
|
+
import { AppMarketplaceMetadata } from "./app-publish.types.js";
|
|
3
|
+
|
|
4
|
+
//#region src/publish/app-marketplace-metadata.service.d.ts
|
|
5
|
+
declare class AppMarketplaceMetadataService {
|
|
6
|
+
load: (params: {
|
|
7
|
+
appDirectory: string;
|
|
8
|
+
manifest: AppManifest;
|
|
9
|
+
metadataPath?: string;
|
|
10
|
+
}) => Promise<AppMarketplaceMetadata>;
|
|
11
|
+
collectPublishFiles: (params: {
|
|
12
|
+
appDirectory: string;
|
|
13
|
+
metadataPath?: string;
|
|
14
|
+
}) => Promise<Array<{
|
|
15
|
+
path: string;
|
|
16
|
+
bytes: Buffer;
|
|
17
|
+
}>>;
|
|
18
|
+
private parseMetadata;
|
|
19
|
+
private parsePublisher;
|
|
20
|
+
private readRequiredString;
|
|
21
|
+
private readOptionalString;
|
|
22
|
+
private readStringArray;
|
|
23
|
+
private readOptionalBoolean;
|
|
24
|
+
private readLocalizedTextMap;
|
|
25
|
+
}
|
|
26
|
+
//#endregion
|
|
27
|
+
export { AppMarketplaceMetadataService };
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
//#region src/publish/app-marketplace-metadata.service.ts
|
|
5
|
+
var AppMarketplaceMetadataService = class {
|
|
6
|
+
load = async (params) => {
|
|
7
|
+
const { appDirectory, manifest, metadataPath: customMetadataPath } = params;
|
|
8
|
+
const metadataPath = customMetadataPath ? path.resolve(customMetadataPath) : path.join(path.resolve(appDirectory), "marketplace.json");
|
|
9
|
+
const raw = JSON.parse(await readFile(metadataPath, "utf-8"));
|
|
10
|
+
return this.parseMetadata(raw, manifest);
|
|
11
|
+
};
|
|
12
|
+
collectPublishFiles = async (params) => {
|
|
13
|
+
const appDirectory = path.resolve(params.appDirectory);
|
|
14
|
+
const publishFiles = [];
|
|
15
|
+
const metadataPath = params.metadataPath ? path.resolve(params.metadataPath) : path.join(appDirectory, "marketplace.json");
|
|
16
|
+
publishFiles.push({
|
|
17
|
+
path: "marketplace.json",
|
|
18
|
+
bytes: Buffer.from(await readFile(metadataPath))
|
|
19
|
+
});
|
|
20
|
+
const readmePath = path.join(appDirectory, "README.md");
|
|
21
|
+
if (existsSync(readmePath)) publishFiles.push({
|
|
22
|
+
path: "README.md",
|
|
23
|
+
bytes: Buffer.from(await readFile(readmePath))
|
|
24
|
+
});
|
|
25
|
+
return publishFiles;
|
|
26
|
+
};
|
|
27
|
+
parseMetadata = (rawMetadata, manifest) => {
|
|
28
|
+
if (!rawMetadata || typeof rawMetadata !== "object" || Array.isArray(rawMetadata)) throw new Error("marketplace.json 必须是对象。");
|
|
29
|
+
const candidate = rawMetadata;
|
|
30
|
+
const slug = this.readRequiredString(candidate.slug, "slug");
|
|
31
|
+
const summary = this.readRequiredString(candidate.summary, "summary");
|
|
32
|
+
const description = this.readOptionalString(candidate.description, "description");
|
|
33
|
+
const author = this.readOptionalString(candidate.author, "author") ?? manifest.name;
|
|
34
|
+
const publisher = this.parsePublisher(candidate.publisher);
|
|
35
|
+
return {
|
|
36
|
+
slug,
|
|
37
|
+
summary,
|
|
38
|
+
summaryI18n: this.readLocalizedTextMap(candidate.summaryI18n, "summaryI18n", summary),
|
|
39
|
+
description,
|
|
40
|
+
descriptionI18n: description ? this.readLocalizedTextMap(candidate.descriptionI18n, "descriptionI18n", description) : void 0,
|
|
41
|
+
author,
|
|
42
|
+
tags: this.readStringArray(candidate.tags, "tags"),
|
|
43
|
+
sourceRepo: this.readOptionalString(candidate.sourceRepo, "sourceRepo"),
|
|
44
|
+
homepage: this.readOptionalString(candidate.homepage, "homepage"),
|
|
45
|
+
featured: this.readOptionalBoolean(candidate.featured, "featured") ?? false,
|
|
46
|
+
publisher
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
parsePublisher = (rawPublisher) => {
|
|
50
|
+
if (rawPublisher === void 0) return;
|
|
51
|
+
if (!rawPublisher || typeof rawPublisher !== "object" || Array.isArray(rawPublisher)) throw new Error("publisher 必须是对象。");
|
|
52
|
+
const candidate = rawPublisher;
|
|
53
|
+
return {
|
|
54
|
+
id: this.readRequiredString(candidate.id, "publisher.id"),
|
|
55
|
+
name: this.readRequiredString(candidate.name, "publisher.name"),
|
|
56
|
+
url: this.readOptionalString(candidate.url, "publisher.url")
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
readRequiredString = (value, fieldName) => {
|
|
60
|
+
if (typeof value !== "string" || !value.trim()) throw new Error(`${fieldName} 必须是非空字符串。`);
|
|
61
|
+
return value.trim();
|
|
62
|
+
};
|
|
63
|
+
readOptionalString = (value, fieldName) => {
|
|
64
|
+
if (value === void 0) return;
|
|
65
|
+
return this.readRequiredString(value, fieldName);
|
|
66
|
+
};
|
|
67
|
+
readStringArray = (value, fieldName) => {
|
|
68
|
+
if (!Array.isArray(value) || value.length === 0) throw new Error(`${fieldName} 必须是非空字符串数组。`);
|
|
69
|
+
return value.map((item, index) => this.readRequiredString(item, `${fieldName}[${index}]`));
|
|
70
|
+
};
|
|
71
|
+
readOptionalBoolean = (value, fieldName) => {
|
|
72
|
+
if (value === void 0) return;
|
|
73
|
+
if (typeof value !== "boolean") throw new Error(`${fieldName} 必须是布尔值。`);
|
|
74
|
+
return value;
|
|
75
|
+
};
|
|
76
|
+
readLocalizedTextMap = (value, fieldName, fallbackEn) => {
|
|
77
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${fieldName} 必须是对象。`);
|
|
78
|
+
const normalized = Object.fromEntries(Object.entries(value).map(([locale, localeValue]) => [locale, this.readRequiredString(localeValue, `${fieldName}.${locale}`)]));
|
|
79
|
+
if (!normalized.en) normalized.en = fallbackEn;
|
|
80
|
+
return normalized;
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
//#endregion
|
|
84
|
+
export { AppMarketplaceMetadataService };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { AppManifestService } from "../manifest/app-manifest.service.js";
|
|
2
|
+
import { AppBundleService } from "../bundle/app-bundle.service.js";
|
|
3
|
+
import { AppPublishResult } from "./app-publish.types.js";
|
|
4
|
+
import { AppMarketplaceClientService } from "./app-marketplace-client.service.js";
|
|
5
|
+
import { AppMarketplaceMetadataService } from "./app-marketplace-metadata.service.js";
|
|
6
|
+
|
|
7
|
+
//#region src/publish/app-publish.service.d.ts
|
|
8
|
+
declare class AppPublishService {
|
|
9
|
+
private readonly manifestService;
|
|
10
|
+
private readonly bundleService;
|
|
11
|
+
private readonly metadataService;
|
|
12
|
+
private readonly marketplaceClient;
|
|
13
|
+
constructor(manifestService?: AppManifestService, bundleService?: AppBundleService, metadataService?: AppMarketplaceMetadataService, marketplaceClient?: AppMarketplaceClientService);
|
|
14
|
+
publish: (params: {
|
|
15
|
+
appDirectory: string;
|
|
16
|
+
metadataPath?: string;
|
|
17
|
+
apiBaseUrl?: string;
|
|
18
|
+
token?: string;
|
|
19
|
+
}) => Promise<AppPublishResult>;
|
|
20
|
+
}
|
|
21
|
+
//#endregion
|
|
22
|
+
export { AppPublishService };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { AppManifestService } from "../manifest/app-manifest.service.js";
|
|
2
|
+
import { AppBundleService } from "../bundle/app-bundle.service.js";
|
|
3
|
+
import { AppMarketplaceClientService } from "./app-marketplace-client.service.js";
|
|
4
|
+
import { AppMarketplaceMetadataService } from "./app-marketplace-metadata.service.js";
|
|
5
|
+
import { readFile } from "node:fs/promises";
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
//#region src/publish/app-publish.service.ts
|
|
9
|
+
var AppPublishService = class {
|
|
10
|
+
constructor(manifestService = new AppManifestService(), bundleService = new AppBundleService(), metadataService = new AppMarketplaceMetadataService(), marketplaceClient = new AppMarketplaceClientService()) {
|
|
11
|
+
this.manifestService = manifestService;
|
|
12
|
+
this.bundleService = bundleService;
|
|
13
|
+
this.metadataService = metadataService;
|
|
14
|
+
this.marketplaceClient = marketplaceClient;
|
|
15
|
+
}
|
|
16
|
+
publish = async (params) => {
|
|
17
|
+
const { appDirectory: inputAppDirectory, metadataPath, apiBaseUrl, token } = params;
|
|
18
|
+
const appDirectory = path.resolve(inputAppDirectory);
|
|
19
|
+
const manifestBundle = await this.manifestService.load(appDirectory);
|
|
20
|
+
const metadata = await this.metadataService.load({
|
|
21
|
+
appDirectory,
|
|
22
|
+
manifest: manifestBundle.manifest,
|
|
23
|
+
metadataPath
|
|
24
|
+
});
|
|
25
|
+
const bundle = await this.bundleService.packAppDirectory({ appDirectory });
|
|
26
|
+
const bundleBytes = Buffer.from(await readFile(bundle.bundlePath));
|
|
27
|
+
const bundleSha256 = createHash("sha256").update(bundleBytes).digest("hex");
|
|
28
|
+
const publishFiles = await this.metadataService.collectPublishFiles({
|
|
29
|
+
appDirectory,
|
|
30
|
+
metadataPath
|
|
31
|
+
});
|
|
32
|
+
const payload = {
|
|
33
|
+
slug: metadata.slug,
|
|
34
|
+
appId: manifestBundle.manifest.id,
|
|
35
|
+
name: manifestBundle.manifest.name,
|
|
36
|
+
version: manifestBundle.manifest.version,
|
|
37
|
+
summary: metadata.summary,
|
|
38
|
+
summaryI18n: metadata.summaryI18n,
|
|
39
|
+
description: metadata.description ?? manifestBundle.manifest.description,
|
|
40
|
+
descriptionI18n: metadata.descriptionI18n,
|
|
41
|
+
author: metadata.author,
|
|
42
|
+
tags: metadata.tags,
|
|
43
|
+
sourceRepo: metadata.sourceRepo,
|
|
44
|
+
homepage: metadata.homepage,
|
|
45
|
+
featured: metadata.featured ?? false,
|
|
46
|
+
publisher: metadata.publisher ?? {
|
|
47
|
+
id: "nextclaw",
|
|
48
|
+
name: "NextClaw",
|
|
49
|
+
url: "https://nextclaw.io"
|
|
50
|
+
},
|
|
51
|
+
manifest: manifestBundle.manifest,
|
|
52
|
+
permissions: manifestBundle.manifest.permissions ?? {},
|
|
53
|
+
bundleBase64: bundleBytes.toString("base64"),
|
|
54
|
+
bundleSha256,
|
|
55
|
+
files: publishFiles.map((file) => ({
|
|
56
|
+
path: file.path,
|
|
57
|
+
contentBase64: file.bytes.toString("base64")
|
|
58
|
+
}))
|
|
59
|
+
};
|
|
60
|
+
return {
|
|
61
|
+
...await this.marketplaceClient.publish({
|
|
62
|
+
payload,
|
|
63
|
+
apiBaseUrl,
|
|
64
|
+
token
|
|
65
|
+
}),
|
|
66
|
+
bundle: {
|
|
67
|
+
path: bundle.bundlePath,
|
|
68
|
+
sha256: bundleSha256
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
//#endregion
|
|
74
|
+
export { AppPublishService };
|