@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.
Files changed (58) hide show
  1. package/README.md +117 -4
  2. package/dist/bundle/app-bundle.service.d.ts +27 -0
  3. package/dist/bundle/app-bundle.service.js +133 -0
  4. package/dist/bundle/app-bundle.types.d.ts +24 -0
  5. package/dist/cli/app-runtime-cli.service.d.ts +27 -0
  6. package/dist/cli/app-runtime-cli.service.js +281 -0
  7. package/dist/cli/app-runtime-options.service.d.ts +60 -0
  8. package/dist/cli/app-runtime-options.service.js +215 -0
  9. package/dist/commands/create.controller.d.ts +14 -0
  10. package/dist/commands/grant.controller.d.ts +16 -0
  11. package/dist/commands/grant.controller.js +25 -0
  12. package/dist/commands/info.controller.d.ts +14 -0
  13. package/dist/commands/info.controller.js +27 -0
  14. package/dist/commands/install.controller.d.ts +15 -0
  15. package/dist/commands/install.controller.js +24 -0
  16. package/dist/commands/list.controller.d.ts +13 -0
  17. package/dist/commands/list.controller.js +25 -0
  18. package/dist/commands/pack.controller.d.ts +15 -0
  19. package/dist/commands/pack.controller.js +25 -0
  20. package/dist/commands/permissions.controller.d.ts +14 -0
  21. package/dist/commands/permissions.controller.js +26 -0
  22. package/dist/commands/registry.controller.d.ts +16 -0
  23. package/dist/commands/registry.controller.js +27 -0
  24. package/dist/commands/revoke.controller.d.ts +15 -0
  25. package/dist/commands/revoke.controller.js +24 -0
  26. package/dist/commands/run.controller.js +14 -6
  27. package/dist/commands/uninstall.controller.d.ts +15 -0
  28. package/dist/commands/uninstall.controller.js +23 -0
  29. package/dist/commands/update.controller.d.ts +16 -0
  30. package/dist/commands/update.controller.js +29 -0
  31. package/dist/host/app-instance.service.d.ts +3 -1
  32. package/dist/host/app-instance.service.js +2 -2
  33. package/dist/index.d.ts +26 -2
  34. package/dist/index.js +23 -2
  35. package/dist/install/app-installation.service.d.ts +37 -0
  36. package/dist/install/app-installation.service.js +255 -0
  37. package/dist/install/app-installation.types.d.ts +62 -0
  38. package/dist/main.js +5 -132
  39. package/dist/package.js +1 -1
  40. package/dist/paths/app-home.service.d.ts +16 -0
  41. package/dist/paths/app-home.service.js +42 -0
  42. package/dist/permissions/app-grant.service.d.ts +22 -0
  43. package/dist/permissions/app-grant.service.js +63 -0
  44. package/dist/permissions/app-permissions.service.d.ts +3 -1
  45. package/dist/permissions/app-permissions.service.js +12 -5
  46. package/dist/permissions/app-permissions.types.d.ts +28 -1
  47. package/dist/registry/app-registry-config.service.d.ts +16 -0
  48. package/dist/registry/app-registry-config.service.js +84 -0
  49. package/dist/registry/app-registry.service.d.ts +38 -0
  50. package/dist/registry/app-registry.service.js +115 -0
  51. package/dist/registry/app-registry.types.d.ts +33 -0
  52. package/dist/registry/app-remote-registry-client.service.d.ts +28 -0
  53. package/dist/registry/app-remote-registry-client.service.js +114 -0
  54. package/dist/registry/app-remote-registry.types.d.ts +52 -0
  55. package/dist/registry/app-remote-registry.types.js +4 -0
  56. package/dist/scaffold/app-scaffold.service.d.ts +19 -0
  57. package/dist/scaffold/app-scaffold.service.js +1 -1
  58. package/package.json +4 -2
package/dist/main.js CHANGED
@@ -1,136 +1,9 @@
1
1
  #!/usr/bin/env node
2
- import { version } from "./package.js";
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
- var AppRuntimeCli = class {
9
- run = async () => {
10
- const [command, appDirectory, ...restArgs] = process.argv.slice(2);
11
- if (command === "--help" || command === "help") {
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
@@ -1,4 +1,4 @@
1
1
  //#region package.json
2
- var version = "0.2.0";
2
+ var version = "0.3.0";
3
3
  //#endregion
4
4
  export { version };
@@ -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) => Promise<ResolvedPermissions>;
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) throw new Error(`缺少 documentAccess 授权:${scope.id}`);
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
- await access(normalizedPath);
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 };