@nextclaw/app-runtime 0.2.0 → 0.3.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 +117 -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 +27 -0
- package/dist/cli/app-runtime-cli.service.js +281 -0
- package/dist/cli/app-runtime-options.service.d.ts +60 -0
- package/dist/cli/app-runtime-options.service.js +215 -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/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 +26 -2
- package/dist/index.js +23 -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/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 +19 -0
- package/dist/scaffold/app-scaffold.service.js +1 -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,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 };
|