@nextclaw/app-runtime 0.10.0 → 0.12.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 (56) hide show
  1. package/README.md +19 -2
  2. package/dist/index.d.ts +7 -2
  3. package/dist/index.js +6 -1
  4. package/dist/package.js +1 -1
  5. package/dist/services/app-home.service.d.ts +4 -0
  6. package/dist/services/app-home.service.d.ts.map +1 -1
  7. package/dist/services/app-home.service.js +15 -1
  8. package/dist/services/app-home.service.js.map +1 -1
  9. package/dist/services/app-install-source.service.js +14 -1
  10. package/dist/services/app-install-source.service.js.map +1 -1
  11. package/dist/services/app-installation-filesystem.service.js +68 -0
  12. package/dist/services/app-installation-filesystem.service.js.map +1 -0
  13. package/dist/services/app-installation-integrity.service.d.ts +20 -0
  14. package/dist/services/app-installation-integrity.service.d.ts.map +1 -0
  15. package/dist/services/app-installation-integrity.service.js +84 -0
  16. package/dist/services/app-installation-integrity.service.js.map +1 -0
  17. package/dist/services/app-installation-lifecycle.service.js +110 -0
  18. package/dist/services/app-installation-lifecycle.service.js.map +1 -0
  19. package/dist/services/app-installation.service.d.ts +21 -11
  20. package/dist/services/app-installation.service.d.ts.map +1 -1
  21. package/dist/services/app-installation.service.js +262 -280
  22. package/dist/services/app-installation.service.js.map +1 -1
  23. package/dist/services/app-instance-inventory.service.d.ts +31 -0
  24. package/dist/services/app-instance-inventory.service.d.ts.map +1 -0
  25. package/dist/services/app-instance-inventory.service.js +136 -0
  26. package/dist/services/app-instance-inventory.service.js.map +1 -0
  27. package/dist/services/app-instance-storage.service.d.ts +55 -0
  28. package/dist/services/app-instance-storage.service.d.ts.map +1 -0
  29. package/dist/services/app-instance-storage.service.js +269 -0
  30. package/dist/services/app-instance-storage.service.js.map +1 -0
  31. package/dist/services/app-manifest.service.d.ts +5 -1
  32. package/dist/services/app-manifest.service.d.ts.map +1 -1
  33. package/dist/services/app-manifest.service.js +59 -3
  34. package/dist/services/app-manifest.service.js.map +1 -1
  35. package/dist/services/app-publish.service.js +1 -1
  36. package/dist/services/app-publish.service.js.map +1 -1
  37. package/dist/services/app-registry.service.d.ts +13 -3
  38. package/dist/services/app-registry.service.d.ts.map +1 -1
  39. package/dist/services/app-registry.service.js +143 -13
  40. package/dist/services/app-registry.service.js.map +1 -1
  41. package/dist/services/file-lock.service.d.ts +20 -0
  42. package/dist/services/file-lock.service.d.ts.map +1 -0
  43. package/dist/services/file-lock.service.js +156 -0
  44. package/dist/services/file-lock.service.js.map +1 -0
  45. package/dist/types/app-installation.types.d.ts +7 -0
  46. package/dist/types/app-installation.types.d.ts.map +1 -1
  47. package/dist/types/app-manifest.types.d.ts +22 -1
  48. package/dist/types/app-manifest.types.d.ts.map +1 -1
  49. package/dist/types/app-manifest.types.js.map +1 -1
  50. package/dist/types/app-registry.types.d.ts +7 -1
  51. package/dist/types/app-registry.types.d.ts.map +1 -1
  52. package/dist/types/app-storage.types.d.ts +64 -0
  53. package/dist/types/app-storage.types.d.ts.map +1 -0
  54. package/dist/types/app-storage.types.js +7 -0
  55. package/dist/types/app-storage.types.js.map +1 -0
  56. package/package.json +1 -1
package/README.md CHANGED
@@ -15,7 +15,7 @@
15
15
  - `napp publish <app-dir>`:把应用目录发布到官方 apps registry;独立应用默认使用 `source`,组合包默认使用 `bundle`
16
16
  - `napp install <app-dir|bundle.napp|app-id[@version]>`:从本地或 registry 安装应用
17
17
  - `napp update <app-id>`:更新已安装应用
18
- - `napp uninstall <app-id>`:卸载已安装应用
18
+ - `napp uninstall <app-id>`:卸载已安装应用,默认保留受管实例数据;增加 `--purge-data` 才会同时删除
19
19
  - `napp list`:列出已安装应用
20
20
  - `napp info <app-id>`:查看已安装应用详情
21
21
  - `napp registry [get|set|reset]`:查看或切换 registry
@@ -61,7 +61,9 @@ assets/
61
61
  - 当前 Wasm 执行底座:Node 原生 `WebAssembly` 与 Wasmtime `serve`
62
62
  - 分发包形态:`.napp`(底层为 zip,支持 `source` / `bundle` 双模式)
63
63
  - 安装目录:`~/.nextclaw/apps/packages/<app-id>/<version>/`
64
- - 用户数据目录:`~/.nextclaw/apps/data/<app-id>/`
64
+ - 受管实例目录:`~/.nextclaw/apps/instances/<app-id>/default/`
65
+ - 受管实例分类:`data/`、`config/`、`state/`、`cache/`、`tmp/`、`logs/`,并由 `metadata.json` 记录稳定身份
66
+ - 旧用户数据目录:`~/.nextclaw/apps/data/<app-id>/`(首次使用时原子迁移到受管实例)
65
67
  - 本地 registry:`~/.nextclaw/apps/registry.json`
66
68
  - 本地 config:`~/.nextclaw/apps/config.json`
67
69
  - 默认 registry:`https://apps-registry.nextclaw.io/api/v1/apps/registry/`
@@ -119,6 +121,21 @@ napp uninstall nextclaw.my-first-napp
119
121
  napp uninstall nextclaw.my-first-napp --purge-data
120
122
  ```
121
123
 
124
+ 不带 `--purge-data` 时只卸载应用代码和注册记录,受管实例会保留,以便重装后继续使用。带上该选项后,卸载事务才会同时移除该 App 的默认实例。宿主只管理上述实例目录;通过文档授权访问的外部文件或目录不属于 App 实例,卸载和清理数据都不会删除它们。
125
+
126
+ 运行中的 NextClaw 还提供统一的数据清单和残留数据清理入口:
127
+
128
+ ```bash
129
+ nextclaw app data list --json
130
+ nextclaw app data delete <data-id> --confirm <app-id> --json
131
+ ```
132
+
133
+ `data-id` 必须取自最新的 `list` 结果;删除命令只接受已经卸载或已经从 workspace 移除的 `retained` 实例,并要求 `--confirm` 与目标 App id 完全一致。开发 Service App 时,可以显式重置隔离的开发实例:
134
+
135
+ ```bash
136
+ nextclaw app dev <service-app-dir> --reset-data --confirm <app-id> --json
137
+ ```
138
+
122
139
  ## 示例
123
140
 
124
141
  ```bash
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { AppBundleChecksums, AppBundleExtractResult, AppBundleMetadata, AppBundlePackResult, AppDistributionMode } from "./types/app-bundle.types.js";
2
2
  import { AppArtifactValidationExpectation, AppArtifactValidationResult, AppArtifactValidationService } from "./services/app-artifact-validation.service.js";
3
- import { AppComponentKind, AppComponentManifest, AppComponentManifestBundle, AppComponentManifestSummary, AppComponentReference, AppCoreWasmMainManifest, AppDocumentAccessMode, AppDocumentAccessScope, AppMainManifest, AppManifest, AppManifestBundle, AppManifestSummary, AppPermissions, AppResolvedComponent, AppStandaloneManifest, AppStandaloneManifestBundle, AppStandaloneManifestSummary, AppUiManifest, AppWasiHttpComponentMainManifest, isAppComponentManifestBundle, isAppStandaloneManifestBundle } from "./types/app-manifest.types.js";
3
+ import { AppComponentKind, AppComponentManifest, AppComponentManifestBundle, AppComponentManifestSummary, AppComponentReference, AppCoreWasmMainManifest, AppDocumentAccessMode, AppDocumentAccessScope, AppMainManifest, AppManifest, AppManifestBundle, AppManifestSummary, AppPermissions, AppPlatformSecuritySummary, AppResolvedComponent, AppRuntimeDeclaration, AppRuntimeIsolation, AppRuntimeProfile, AppStandaloneManifest, AppStandaloneManifestBundle, AppStandaloneManifestSummary, AppStorageDeclaration, AppUiManifest, AppWasiHttpComponentMainManifest, isAppComponentManifestBundle, isAppStandaloneManifestBundle } from "./types/app-manifest.types.js";
4
4
  import { AppManifestService } from "./services/app-manifest.service.js";
5
5
  import { AppBundleService } from "./services/app-bundle.service.js";
6
6
  import { AppDocumentGrantMap, AppDocumentGrantMutationResult, AppDocumentGrantState, AppInstalledPermissionState, AppPermissionSummary, ResolvedDocumentGrant, ResolvedPermissions } from "./types/app-permissions.types.js";
@@ -18,11 +18,13 @@ import { AppRuntimeOptionsService, BuildCliOptions, CreateCliOptions, GrantCliOp
18
18
  import { AppRuntimeCliService } from "./services/app-runtime-cli.service.js";
19
19
  import { CreateCommand } from "./controllers/create.controller.js";
20
20
  import { AppHomeService } from "./services/app-home.service.js";
21
+ import { APP_STORAGE_LAYOUT_VERSION, AppInstanceInventory, AppInstanceInventoryDiagnostic, AppInstanceInventoryEntry, AppInstanceMetadata, AppInstanceRecord, AppStorageContext, AppStorageLayout, AppStorageUsage, DEFAULT_APP_INSTANCE_ID } from "./types/app-storage.types.js";
21
22
  import { AppPublisher, AppRegistryConfig, AppRegistryConfigSnapshot, AppRemoteRegistryDocument, AppRemoteRegistryResolution, AppRemoteRegistryVersion, DEFAULT_APP_REGISTRY_URL } from "./types/app-remote-registry.types.js";
22
23
  import { AppInstallSourceKind, AppRegistry, AppRegistryAppRecord, AppRegistryInstalledVersion } from "./types/app-registry.types.js";
23
24
  import { AppRegistryService } from "./services/app-registry.service.js";
24
25
  import { AppGrantService } from "./services/app-grant.service.js";
25
26
  import { GrantCommand } from "./controllers/grant.controller.js";
27
+ import { AppInstanceStorageService } from "./services/app-instance-storage.service.js";
26
28
  import { AppRuntimeCommandResult, AppRuntimeDoctorResult, AppRuntimeToolStatus, AppRuntimeToolchainService } from "./services/app-runtime-toolchain.service.js";
27
29
  import { AppBuildResult, AppBuildService } from "./services/app-build.service.js";
28
30
  import { AppRegistryConfigService } from "./services/app-registry-config.service.js";
@@ -48,5 +50,8 @@ import { AppPublishValidationResult, AppPublishValidationService, AppPublishVali
48
50
  import { ValidatePublishCommand } from "./controllers/validate-publish.controller.js";
49
51
  import { WasmtimeWasiHttpComponentHandle, WasmtimeWasiHttpComponentService, WasmtimeWasiHttpComponentStartOptions } from "./services/wasmtime-wasi-http-component.service.js";
50
52
  import { AppHostHandle, AppHostService, AppHostStartOptions } from "./services/app-host.service.js";
53
+ import { AppInstallationIntegrityService } from "./services/app-installation-integrity.service.js";
54
+ import { AppInstanceInventoryService } from "./services/app-instance-inventory.service.js";
55
+ import { FileLockService } from "./services/file-lock.service.js";
51
56
  import { UiServerService } from "./services/ui-server.service.js";
52
- export { AppActivationResult, AppArtifactValidationExpectation, AppArtifactValidationResult, AppArtifactValidationService, AppBuildResult, AppBuildService, AppBundleChecksums, AppBundleExtractResult, AppBundleMetadata, AppBundlePackResult, AppBundleService, AppComponentKind, AppComponentManifest, AppComponentManifestBundle, AppComponentManifestSummary, AppComponentReference, AppCoreWasmMainManifest, AppDistributionMode, AppDocumentAccessMode, AppDocumentAccessScope, AppDocumentGrantMap, AppDocumentGrantMutationResult, AppDocumentGrantState, AppGrantService, AppHomeService, AppHostHandle, AppHostService, AppHostStartOptions, AppInfoResult, AppInstallProgressHandler, AppInstallProgressPhase, AppInstallResult, AppInstallSourceKind, AppInstallationService, AppInstalledPermissionState, AppInstanceService, AppLaunchResolution, AppMainManifest, AppManifest, AppManifestBundle, AppManifestService, AppManifestSummary, AppMarketplaceClientService, AppMarketplaceMetadata, AppMarketplaceMetadataService, AppMarketplaceVisuals, AppPermissionSummary, AppPermissions, AppPermissionsService, AppPublishFile, AppPublishPayload, AppPublishResult, AppPublishService, AppPublishValidationResult, AppPublishValidationService, AppPublishValidationWarning, AppPublishValidationWarningCode, AppPublisher, AppRegistry, AppRegistryAppRecord, AppRegistryConfig, AppRegistryConfigService, AppRegistryConfigSnapshot, AppRegistryInstalledVersion, AppRegistryService, AppRemoteRegistryClientService, AppRemoteRegistryDocument, AppRemoteRegistryResolution, AppRemoteRegistryVersion, AppResolvedComponent, AppRollbackResult, AppRunResult, AppRuntimeCliService, AppRuntimeCommandResult, AppRuntimeDoctorResult, AppRuntimeOptionsService, AppRuntimeToolStatus, AppRuntimeToolchainService, AppScaffoldFile, AppScaffoldResult, AppScaffoldService, AppScaffoldTemplate, AppStandaloneManifest, AppStandaloneManifestBundle, AppStandaloneManifestSummary, AppTsHttpLiteScaffoldTemplateService, AppTsHttpScaffoldTemplateService, AppUiManifest, AppUninstallResult, AppUpdateResult, AppWasiHttpComponentMainManifest, BuildCliOptions, CreateCliOptions, CreateCommand, DEFAULT_APP_MARKETPLACE_API_BASE, DEFAULT_APP_REGISTRY_URL, GrantCliOptions, GrantCommand, HostBridgeServer, InfoCommand, InstallCliOptions, InstallCommand, InstalledAppListItem, JsonOnlyCliOptions, ListCommand, MainRunRequest, MainRunResult, MainRunnerService, PackCliOptions, PackCommand, PermissionsCommand, PlatformAuthStateService, PlatformPublishAuthState, PublishCliOptions, PublishCommand, RegistryCommand, ResolvedDocumentGrant, ResolvedPermissions, RevokeCliOptions, RevokeCommand, RuntimeCliOptions, UiServerService, UninstallCliOptions, UninstallCommand, UpdateCliOptions, UpdateCommand, ValidatePublishCommand, WasmDocumentSummaryInput, WasmMainRunnerService, WasmSidecarClientService, WasmtimeWasiHttpComponentHandle, WasmtimeWasiHttpComponentService, WasmtimeWasiHttpComponentStartOptions, isAppComponentManifestBundle, isAppStandaloneManifestBundle };
57
+ export { APP_STORAGE_LAYOUT_VERSION, AppActivationResult, AppArtifactValidationExpectation, AppArtifactValidationResult, AppArtifactValidationService, AppBuildResult, AppBuildService, AppBundleChecksums, AppBundleExtractResult, AppBundleMetadata, AppBundlePackResult, AppBundleService, AppComponentKind, AppComponentManifest, AppComponentManifestBundle, AppComponentManifestSummary, AppComponentReference, AppCoreWasmMainManifest, AppDistributionMode, AppDocumentAccessMode, AppDocumentAccessScope, AppDocumentGrantMap, AppDocumentGrantMutationResult, AppDocumentGrantState, AppGrantService, AppHomeService, AppHostHandle, AppHostService, AppHostStartOptions, AppInfoResult, AppInstallProgressHandler, AppInstallProgressPhase, AppInstallResult, AppInstallSourceKind, AppInstallationIntegrityService, AppInstallationService, AppInstalledPermissionState, AppInstanceInventory, AppInstanceInventoryDiagnostic, AppInstanceInventoryEntry, AppInstanceInventoryService, AppInstanceMetadata, AppInstanceRecord, AppInstanceService, AppInstanceStorageService, AppLaunchResolution, AppMainManifest, AppManifest, AppManifestBundle, AppManifestService, AppManifestSummary, AppMarketplaceClientService, AppMarketplaceMetadata, AppMarketplaceMetadataService, AppMarketplaceVisuals, AppPermissionSummary, AppPermissions, AppPermissionsService, AppPlatformSecuritySummary, AppPublishFile, AppPublishPayload, AppPublishResult, AppPublishService, AppPublishValidationResult, AppPublishValidationService, AppPublishValidationWarning, AppPublishValidationWarningCode, AppPublisher, AppRegistry, AppRegistryAppRecord, AppRegistryConfig, AppRegistryConfigService, AppRegistryConfigSnapshot, AppRegistryInstalledVersion, AppRegistryService, AppRemoteRegistryClientService, AppRemoteRegistryDocument, AppRemoteRegistryResolution, AppRemoteRegistryVersion, AppResolvedComponent, AppRollbackResult, AppRunResult, AppRuntimeCliService, AppRuntimeCommandResult, AppRuntimeDeclaration, AppRuntimeDoctorResult, AppRuntimeIsolation, AppRuntimeOptionsService, AppRuntimeProfile, AppRuntimeToolStatus, AppRuntimeToolchainService, AppScaffoldFile, AppScaffoldResult, AppScaffoldService, AppScaffoldTemplate, AppStandaloneManifest, AppStandaloneManifestBundle, AppStandaloneManifestSummary, AppStorageContext, AppStorageDeclaration, AppStorageLayout, AppStorageUsage, AppTsHttpLiteScaffoldTemplateService, AppTsHttpScaffoldTemplateService, AppUiManifest, AppUninstallResult, AppUpdateResult, AppWasiHttpComponentMainManifest, BuildCliOptions, CreateCliOptions, CreateCommand, DEFAULT_APP_INSTANCE_ID, DEFAULT_APP_MARKETPLACE_API_BASE, DEFAULT_APP_REGISTRY_URL, FileLockService, GrantCliOptions, GrantCommand, HostBridgeServer, InfoCommand, InstallCliOptions, InstallCommand, InstalledAppListItem, JsonOnlyCliOptions, ListCommand, MainRunRequest, MainRunResult, MainRunnerService, PackCliOptions, PackCommand, PermissionsCommand, PlatformAuthStateService, PlatformPublishAuthState, PublishCliOptions, PublishCommand, RegistryCommand, ResolvedDocumentGrant, ResolvedPermissions, RevokeCliOptions, RevokeCommand, RuntimeCliOptions, UiServerService, UninstallCliOptions, UninstallCommand, UpdateCliOptions, UpdateCommand, ValidatePublishCommand, WasmDocumentSummaryInput, WasmMainRunnerService, WasmSidecarClientService, WasmtimeWasiHttpComponentHandle, WasmtimeWasiHttpComponentService, WasmtimeWasiHttpComponentStartOptions, isAppComponentManifestBundle, isAppStandaloneManifestBundle };
package/dist/index.js CHANGED
@@ -15,6 +15,10 @@ import { WasmSidecarClientService } from "./services/wasm-sidecar-client.service
15
15
  import { WasmMainRunnerService } from "./services/wasm-main-runner.service.js";
16
16
  import { AppInstanceService } from "./services/app-instance.service.js";
17
17
  import { AppHomeService } from "./services/app-home.service.js";
18
+ import { FileLockService } from "./services/file-lock.service.js";
19
+ import { APP_STORAGE_LAYOUT_VERSION, DEFAULT_APP_INSTANCE_ID } from "./types/app-storage.types.js";
20
+ import { AppInstanceStorageService } from "./services/app-instance-storage.service.js";
21
+ import { AppInstallationIntegrityService } from "./services/app-installation-integrity.service.js";
18
22
  import { AppRuntimeToolchainService } from "./services/app-runtime-toolchain.service.js";
19
23
  import { AppBuildService } from "./services/app-build.service.js";
20
24
  import { DEFAULT_APP_REGISTRY_URL } from "./types/app-remote-registry.types.js";
@@ -44,4 +48,5 @@ import { AppPublishValidationService } from "./services/app-publish-validation.s
44
48
  import { ValidatePublishCommand } from "./controllers/validate-publish.controller.js";
45
49
  import { AppRuntimeOptionsService } from "./services/app-runtime-options.service.js";
46
50
  import { AppRuntimeCliService } from "./services/app-runtime-cli.service.js";
47
- export { AppArtifactValidationService, AppBuildService, AppBundleService, AppGrantService, AppHomeService, AppHostService, AppInstallationService, AppInstanceService, AppManifestService, AppMarketplaceClientService, AppMarketplaceMetadataService, AppPermissionsService, AppPublishService, AppPublishValidationService, AppRegistryConfigService, AppRegistryService, AppRemoteRegistryClientService, AppRuntimeCliService, AppRuntimeOptionsService, AppRuntimeToolchainService, AppScaffoldService, AppTsHttpLiteScaffoldTemplateService, AppTsHttpScaffoldTemplateService, CreateCommand, DEFAULT_APP_MARKETPLACE_API_BASE, DEFAULT_APP_REGISTRY_URL, GrantCommand, HostBridgeServer, InfoCommand, InstallCommand, ListCommand, MainRunnerService, PackCommand, PermissionsCommand, PlatformAuthStateService, PublishCommand, RegistryCommand, RevokeCommand, UiServerService, UninstallCommand, UpdateCommand, ValidatePublishCommand, WasmMainRunnerService, WasmSidecarClientService, WasmtimeWasiHttpComponentService, isAppComponentManifestBundle, isAppStandaloneManifestBundle };
51
+ import { AppInstanceInventoryService } from "./services/app-instance-inventory.service.js";
52
+ export { APP_STORAGE_LAYOUT_VERSION, AppArtifactValidationService, AppBuildService, AppBundleService, AppGrantService, AppHomeService, AppHostService, AppInstallationIntegrityService, AppInstallationService, AppInstanceInventoryService, AppInstanceService, AppInstanceStorageService, AppManifestService, AppMarketplaceClientService, AppMarketplaceMetadataService, AppPermissionsService, AppPublishService, AppPublishValidationService, AppRegistryConfigService, AppRegistryService, AppRemoteRegistryClientService, AppRuntimeCliService, AppRuntimeOptionsService, AppRuntimeToolchainService, AppScaffoldService, AppTsHttpLiteScaffoldTemplateService, AppTsHttpScaffoldTemplateService, CreateCommand, DEFAULT_APP_INSTANCE_ID, DEFAULT_APP_MARKETPLACE_API_BASE, DEFAULT_APP_REGISTRY_URL, FileLockService, GrantCommand, HostBridgeServer, InfoCommand, InstallCommand, ListCommand, MainRunnerService, PackCommand, PermissionsCommand, PlatformAuthStateService, PublishCommand, RegistryCommand, RevokeCommand, UiServerService, UninstallCommand, UpdateCommand, ValidatePublishCommand, WasmMainRunnerService, WasmSidecarClientService, WasmtimeWasiHttpComponentService, isAppComponentManifestBundle, isAppStandaloneManifestBundle };
package/dist/package.js CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region package.json
2
- var version = "0.10.0";
2
+ var version = "0.12.0";
3
3
  //#endregion
4
4
  export { version };
5
5
 
@@ -5,11 +5,15 @@ declare class AppHomeService {
5
5
  getAppHomeDirectory: () => string;
6
6
  getPackagesDirectory: () => string;
7
7
  getDataDirectory: () => string;
8
+ getInstancesDirectory: () => string;
9
+ getLocksDirectory: () => string;
8
10
  getRegistryPath: () => string;
9
11
  getConfigPath: () => string;
10
12
  getOperationsPath: () => string;
11
13
  getInstallDirectory: (appId: string, version: string) => string;
12
14
  getAppDataDirectory: (appId: string) => string;
15
+ getAppInstanceDirectory: (appId: string, instanceId: string) => string;
16
+ getAppOperationLockPath: (appId: string) => string;
13
17
  ensureBaseDirectories: () => Promise<void>;
14
18
  createTemporaryDirectory: (prefix: string) => Promise<string>;
15
19
  }
@@ -1 +1 @@
1
- {"version":3,"file":"app-home.service.d.ts","names":[],"sources":["../../src/services/app-home.service.ts"],"mappings":";cAIa,cAAA;EAAA,iBAEQ,gBAAA;cAAA,gBAAA;EAKnB,mBAAA;EAIA,oBAAA;EAIA,gBAAA;EAIA,eAAA;EAIA,aAAA;EAIA,iBAAA;EAIA,mBAAA,GAAuB,KAAA,UAAe,OAAA;EAItC,mBAAA,GAAuB,KAAA;EAIvB,qBAAA,QAAkC,OAAA;EAQlC,wBAAA,GAAkC,MAAA,aAAiB,OAAA;AAAA"}
1
+ {"version":3,"file":"app-home.service.d.ts","names":[],"sources":["../../src/services/app-home.service.ts"],"mappings":";cAIa,cAAA;EAAA,iBAEQ,gBAAA;cAAA,gBAAA;EAKnB,mBAAA;EAIA,oBAAA;EAIA,gBAAA;EAIA,qBAAA;EAIA,iBAAA;EAIA,eAAA;EAIA,aAAA;EAIA,iBAAA;EAIA,mBAAA,GAAuB,KAAA,UAAe,OAAA;EAItC,mBAAA,GAAuB,KAAA;EAIvB,uBAAA,GAA2B,KAAA,UAAe,UAAA;EAI1C,uBAAA,GAA2B,KAAA;EAI3B,qBAAA,QAAkC,OAAA;EAUlC,wBAAA,GAAkC,MAAA,aAAiB,OAAA;AAAA"}
@@ -15,6 +15,12 @@ var AppHomeService = class {
15
15
  getDataDirectory = () => {
16
16
  return path.join(this.appHomeDirectory, "data");
17
17
  };
18
+ getInstancesDirectory = () => {
19
+ return path.join(this.appHomeDirectory, "instances");
20
+ };
21
+ getLocksDirectory = () => {
22
+ return path.join(this.appHomeDirectory, "locks");
23
+ };
18
24
  getRegistryPath = () => {
19
25
  return path.join(this.appHomeDirectory, "registry.json");
20
26
  };
@@ -30,11 +36,19 @@ var AppHomeService = class {
30
36
  getAppDataDirectory = (appId) => {
31
37
  return path.join(this.getDataDirectory(), appId);
32
38
  };
39
+ getAppInstanceDirectory = (appId, instanceId) => {
40
+ return path.join(this.getInstancesDirectory(), appId, instanceId);
41
+ };
42
+ getAppOperationLockPath = (appId) => {
43
+ return path.join(this.getLocksDirectory(), "apps", `${appId}.lock`);
44
+ };
33
45
  ensureBaseDirectories = async () => {
34
46
  await Promise.all([
35
47
  mkdir(this.appHomeDirectory, { recursive: true }),
36
48
  mkdir(this.getPackagesDirectory(), { recursive: true }),
37
- mkdir(this.getDataDirectory(), { recursive: true })
49
+ mkdir(this.getDataDirectory(), { recursive: true }),
50
+ mkdir(this.getInstancesDirectory(), { recursive: true }),
51
+ mkdir(this.getLocksDirectory(), { recursive: true })
38
52
  ]);
39
53
  };
40
54
  createTemporaryDirectory = async (prefix) => {
@@ -1 +1 @@
1
- {"version":3,"file":"app-home.service.js","names":[],"sources":["../../src/services/app-home.service.ts"],"sourcesContent":["import { mkdir, mkdtemp } from \"node:fs/promises\";\nimport { tmpdir } from \"node:os\";\nimport path from \"node:path\";\n\nexport class AppHomeService {\n constructor(\n private readonly appHomeDirectory: string = process.env.NEXTCLAW_APP_HOME\n ? path.resolve(process.env.NEXTCLAW_APP_HOME)\n : path.join(process.env.HOME ?? process.cwd(), \".nextclaw\", \"apps\"),\n ) {}\n\n getAppHomeDirectory = (): string => {\n return this.appHomeDirectory;\n };\n\n getPackagesDirectory = (): string => {\n return path.join(this.appHomeDirectory, \"packages\");\n };\n\n getDataDirectory = (): string => {\n return path.join(this.appHomeDirectory, \"data\");\n };\n\n getRegistryPath = (): string => {\n return path.join(this.appHomeDirectory, \"registry.json\");\n };\n\n getConfigPath = (): string => {\n return path.join(this.appHomeDirectory, \"config.json\");\n };\n\n getOperationsPath = (): string => {\n return path.join(this.appHomeDirectory, \"operations.json\");\n };\n\n getInstallDirectory = (appId: string, version: string): string => {\n return path.join(this.getPackagesDirectory(), appId, version);\n };\n\n getAppDataDirectory = (appId: string): string => {\n return path.join(this.getDataDirectory(), appId);\n };\n\n ensureBaseDirectories = async (): Promise<void> => {\n await Promise.all([\n mkdir(this.appHomeDirectory, { recursive: true }),\n mkdir(this.getPackagesDirectory(), { recursive: true }),\n mkdir(this.getDataDirectory(), { recursive: true }),\n ]);\n };\n\n createTemporaryDirectory = async (prefix: string): Promise<string> => {\n return mkdtemp(path.join(tmpdir(), prefix));\n };\n}\n"],"mappings":";;;;AAIA,IAAa,iBAAb,MAA4B;CAC1B,YACE,mBAA4C,QAAQ,IAAI,oBACpD,KAAK,QAAQ,QAAQ,IAAI,kBAAkB,GAC3C,KAAK,KAAK,QAAQ,IAAI,QAAQ,QAAQ,KAAK,EAAE,aAAa,OAAO,EACrE;AAHiB,OAAA,mBAAA;;CAKnB,4BAAoC;AAClC,SAAO,KAAK;;CAGd,6BAAqC;AACnC,SAAO,KAAK,KAAK,KAAK,kBAAkB,WAAW;;CAGrD,yBAAiC;AAC/B,SAAO,KAAK,KAAK,KAAK,kBAAkB,OAAO;;CAGjD,wBAAgC;AAC9B,SAAO,KAAK,KAAK,KAAK,kBAAkB,gBAAgB;;CAG1D,sBAA8B;AAC5B,SAAO,KAAK,KAAK,KAAK,kBAAkB,cAAc;;CAGxD,0BAAkC;AAChC,SAAO,KAAK,KAAK,KAAK,kBAAkB,kBAAkB;;CAG5D,uBAAuB,OAAe,YAA4B;AAChE,SAAO,KAAK,KAAK,KAAK,sBAAsB,EAAE,OAAO,QAAQ;;CAG/D,uBAAuB,UAA0B;AAC/C,SAAO,KAAK,KAAK,KAAK,kBAAkB,EAAE,MAAM;;CAGlD,wBAAwB,YAA2B;AACjD,QAAM,QAAQ,IAAI;GAChB,MAAM,KAAK,kBAAkB,EAAE,WAAW,MAAM,CAAC;GACjD,MAAM,KAAK,sBAAsB,EAAE,EAAE,WAAW,MAAM,CAAC;GACvD,MAAM,KAAK,kBAAkB,EAAE,EAAE,WAAW,MAAM,CAAC;GACpD,CAAC;;CAGJ,2BAA2B,OAAO,WAAoC;AACpE,SAAO,QAAQ,KAAK,KAAK,QAAQ,EAAE,OAAO,CAAC"}
1
+ {"version":3,"file":"app-home.service.js","names":[],"sources":["../../src/services/app-home.service.ts"],"sourcesContent":["import { mkdir, mkdtemp } from \"node:fs/promises\";\nimport { tmpdir } from \"node:os\";\nimport path from \"node:path\";\n\nexport class AppHomeService {\n constructor(\n private readonly appHomeDirectory: string = process.env.NEXTCLAW_APP_HOME\n ? path.resolve(process.env.NEXTCLAW_APP_HOME)\n : path.join(process.env.HOME ?? process.cwd(), \".nextclaw\", \"apps\"),\n ) {}\n\n getAppHomeDirectory = (): string => {\n return this.appHomeDirectory;\n };\n\n getPackagesDirectory = (): string => {\n return path.join(this.appHomeDirectory, \"packages\");\n };\n\n getDataDirectory = (): string => {\n return path.join(this.appHomeDirectory, \"data\");\n };\n\n getInstancesDirectory = (): string => {\n return path.join(this.appHomeDirectory, \"instances\");\n };\n\n getLocksDirectory = (): string => {\n return path.join(this.appHomeDirectory, \"locks\");\n };\n\n getRegistryPath = (): string => {\n return path.join(this.appHomeDirectory, \"registry.json\");\n };\n\n getConfigPath = (): string => {\n return path.join(this.appHomeDirectory, \"config.json\");\n };\n\n getOperationsPath = (): string => {\n return path.join(this.appHomeDirectory, \"operations.json\");\n };\n\n getInstallDirectory = (appId: string, version: string): string => {\n return path.join(this.getPackagesDirectory(), appId, version);\n };\n\n getAppDataDirectory = (appId: string): string => {\n return path.join(this.getDataDirectory(), appId);\n };\n\n getAppInstanceDirectory = (appId: string, instanceId: string): string => {\n return path.join(this.getInstancesDirectory(), appId, instanceId);\n };\n\n getAppOperationLockPath = (appId: string): string => {\n return path.join(this.getLocksDirectory(), \"apps\", `${appId}.lock`);\n };\n\n ensureBaseDirectories = async (): Promise<void> => {\n await Promise.all([\n mkdir(this.appHomeDirectory, { recursive: true }),\n mkdir(this.getPackagesDirectory(), { recursive: true }),\n mkdir(this.getDataDirectory(), { recursive: true }),\n mkdir(this.getInstancesDirectory(), { recursive: true }),\n mkdir(this.getLocksDirectory(), { recursive: true }),\n ]);\n };\n\n createTemporaryDirectory = async (prefix: string): Promise<string> => {\n return mkdtemp(path.join(tmpdir(), prefix));\n };\n}\n"],"mappings":";;;;AAIA,IAAa,iBAAb,MAA4B;CAC1B,YACE,mBAA4C,QAAQ,IAAI,oBACpD,KAAK,QAAQ,QAAQ,IAAI,kBAAkB,GAC3C,KAAK,KAAK,QAAQ,IAAI,QAAQ,QAAQ,KAAK,EAAE,aAAa,OAAO,EACrE;AAHiB,OAAA,mBAAA;;CAKnB,4BAAoC;AAClC,SAAO,KAAK;;CAGd,6BAAqC;AACnC,SAAO,KAAK,KAAK,KAAK,kBAAkB,WAAW;;CAGrD,yBAAiC;AAC/B,SAAO,KAAK,KAAK,KAAK,kBAAkB,OAAO;;CAGjD,8BAAsC;AACpC,SAAO,KAAK,KAAK,KAAK,kBAAkB,YAAY;;CAGtD,0BAAkC;AAChC,SAAO,KAAK,KAAK,KAAK,kBAAkB,QAAQ;;CAGlD,wBAAgC;AAC9B,SAAO,KAAK,KAAK,KAAK,kBAAkB,gBAAgB;;CAG1D,sBAA8B;AAC5B,SAAO,KAAK,KAAK,KAAK,kBAAkB,cAAc;;CAGxD,0BAAkC;AAChC,SAAO,KAAK,KAAK,KAAK,kBAAkB,kBAAkB;;CAG5D,uBAAuB,OAAe,YAA4B;AAChE,SAAO,KAAK,KAAK,KAAK,sBAAsB,EAAE,OAAO,QAAQ;;CAG/D,uBAAuB,UAA0B;AAC/C,SAAO,KAAK,KAAK,KAAK,kBAAkB,EAAE,MAAM;;CAGlD,2BAA2B,OAAe,eAA+B;AACvE,SAAO,KAAK,KAAK,KAAK,uBAAuB,EAAE,OAAO,WAAW;;CAGnE,2BAA2B,UAA0B;AACnD,SAAO,KAAK,KAAK,KAAK,mBAAmB,EAAE,QAAQ,GAAG,MAAM,OAAO;;CAGrE,wBAAwB,YAA2B;AACjD,QAAM,QAAQ,IAAI;GAChB,MAAM,KAAK,kBAAkB,EAAE,WAAW,MAAM,CAAC;GACjD,MAAM,KAAK,sBAAsB,EAAE,EAAE,WAAW,MAAM,CAAC;GACvD,MAAM,KAAK,kBAAkB,EAAE,EAAE,WAAW,MAAM,CAAC;GACnD,MAAM,KAAK,uBAAuB,EAAE,EAAE,WAAW,MAAM,CAAC;GACxD,MAAM,KAAK,mBAAmB,EAAE,EAAE,WAAW,MAAM,CAAC;GACrD,CAAC;;CAGJ,2BAA2B,OAAO,WAAoC;AACpE,SAAO,QAAQ,KAAK,KAAK,QAAQ,EAAE,OAAO,CAAC"}
@@ -2,10 +2,23 @@ import { access } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  //#region src/services/app-install-source.service.ts
4
4
  var AppInstallSourceService = class {
5
- constructor(manifestService, remoteRegistryClient) {
5
+ constructor(manifestService, remoteRegistryClient, bundleService) {
6
6
  this.manifestService = manifestService;
7
7
  this.remoteRegistryClient = remoteRegistryClient;
8
+ this.bundleService = bundleService;
8
9
  }
10
+ materializeBundle = async (source, tempDirectory, onProgress) => {
11
+ if (source.kind === "directory") return (await this.bundleService.packAppDirectory({
12
+ appDirectory: source.appDirectory,
13
+ outputPath: path.join(tempDirectory, "app.napp")
14
+ })).bundlePath;
15
+ if (source.kind === "bundle") return source.bundlePath;
16
+ await onProgress?.("downloading");
17
+ return (await this.remoteRegistryClient.downloadBundle({
18
+ resolution: source.registryResolution,
19
+ targetDirectory: tempDirectory
20
+ })).bundlePath;
21
+ };
9
22
  resolve = async (appSource, registryUrl) => {
10
23
  const localSource = await this.detectLocal(appSource);
11
24
  if (localSource.kind === "directory") return {
@@ -1 +1 @@
1
- {"version":3,"file":"app-install-source.service.js","names":[],"sources":["../../src/services/app-install-source.service.ts"],"sourcesContent":["import { access } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport type { AppManifestService } from \"#app-runtime/services/app-manifest.service.js\";\nimport type { AppRemoteRegistryClientService } from \"#app-runtime/services/app-remote-registry-client.service.js\";\n\ntype LocalAppSource =\n | { kind: \"directory\"; appDirectory: string }\n | { kind: \"bundle\"; bundlePath: string }\n | { kind: \"missing\" };\n\nexport type ResolvedAppInstallSource =\n | {\n kind: \"directory\";\n appDirectory: string;\n sourceRef: string;\n }\n | {\n kind: \"bundle\";\n bundlePath: string;\n sourceRef: string;\n }\n | {\n kind: \"registry\";\n sourceRef: string;\n registryResolution: Awaited<ReturnType<AppRemoteRegistryClientService[\"resolve\"]>>;\n };\n\nexport class AppInstallSourceService {\n constructor(\n private readonly manifestService: AppManifestService,\n private readonly remoteRegistryClient: AppRemoteRegistryClientService,\n ) {}\n\n resolve = async (\n appSource: string,\n registryUrl?: string,\n ): Promise<ResolvedAppInstallSource> => {\n const localSource = await this.detectLocal(appSource);\n if (localSource.kind === \"directory\") {\n return {\n kind: \"directory\",\n appDirectory: localSource.appDirectory,\n sourceRef: localSource.appDirectory,\n };\n }\n if (localSource.kind === \"bundle\") {\n return {\n kind: \"bundle\",\n bundlePath: localSource.bundlePath,\n sourceRef: localSource.bundlePath,\n };\n }\n const registrySpec = this.parseRegistrySpec(appSource);\n if (!registrySpec) {\n if (this.looksLikePath(appSource)) {\n throw new Error(`本地安装源不存在:${appSource}`);\n }\n throw new Error(`无法识别安装源:${appSource}`);\n }\n const registryResolution = await this.remoteRegistryClient.resolve({\n appId: registrySpec.appId,\n version: registrySpec.version,\n registryUrl,\n });\n return {\n kind: \"registry\",\n sourceRef: `${registryResolution.appId}@${registryResolution.version}`,\n registryResolution,\n };\n };\n\n detectLocal = async (\n sourcePath: string,\n allowBundle: boolean = true,\n ): Promise<LocalAppSource> => {\n const normalizedSource = path.resolve(sourcePath);\n try {\n await access(normalizedSource);\n const manifestBundle = await this.manifestService.load(normalizedSource);\n if (manifestBundle.appDirectory) {\n return {\n kind: \"directory\",\n appDirectory: normalizedSource,\n };\n }\n } catch {\n if (allowBundle && normalizedSource.endsWith(\".napp\")) {\n try {\n await access(normalizedSource);\n return {\n kind: \"bundle\",\n bundlePath: normalizedSource,\n };\n } catch {\n return { kind: \"missing\" };\n }\n }\n return { kind: \"missing\" };\n }\n return { kind: \"missing\" };\n };\n\n private parseRegistrySpec = (\n appSource: string,\n ): { appId: string; version?: string } | undefined => {\n const match = /^(?<appId>[a-z0-9][a-z0-9._-]*)(?:@(?<version>[A-Za-z0-9._+-]+))?$/i.exec(\n appSource.trim(),\n );\n if (!match?.groups?.appId) {\n return undefined;\n }\n return {\n appId: match.groups.appId,\n version: match.groups.version,\n };\n };\n\n private looksLikePath = (appSource: string): boolean =>\n appSource.startsWith(\".\") ||\n appSource.startsWith(\"/\") ||\n appSource.includes(path.sep);\n}\n"],"mappings":";;;AA2BA,IAAa,0BAAb,MAAqC;CACnC,YACE,iBACA,sBACA;AAFiB,OAAA,kBAAA;AACA,OAAA,uBAAA;;CAGnB,UAAU,OACR,WACA,gBACsC;EACtC,MAAM,cAAc,MAAM,KAAK,YAAY,UAAU;AACrD,MAAI,YAAY,SAAS,YACvB,QAAO;GACL,MAAM;GACN,cAAc,YAAY;GAC1B,WAAW,YAAY;GACxB;AAEH,MAAI,YAAY,SAAS,SACvB,QAAO;GACL,MAAM;GACN,YAAY,YAAY;GACxB,WAAW,YAAY;GACxB;EAEH,MAAM,eAAe,KAAK,kBAAkB,UAAU;AACtD,MAAI,CAAC,cAAc;AACjB,OAAI,KAAK,cAAc,UAAU,CAC/B,OAAM,IAAI,MAAM,YAAY,YAAY;AAE1C,SAAM,IAAI,MAAM,WAAW,YAAY;;EAEzC,MAAM,qBAAqB,MAAM,KAAK,qBAAqB,QAAQ;GACjE,OAAO,aAAa;GACpB,SAAS,aAAa;GACtB;GACD,CAAC;AACF,SAAO;GACL,MAAM;GACN,WAAW,GAAG,mBAAmB,MAAM,GAAG,mBAAmB;GAC7D;GACD;;CAGH,cAAc,OACZ,YACA,cAAuB,SACK;EAC5B,MAAM,mBAAmB,KAAK,QAAQ,WAAW;AACjD,MAAI;AACF,SAAM,OAAO,iBAAiB;AAE9B,QADuB,MAAM,KAAK,gBAAgB,KAAK,iBAAiB,EACrD,aACjB,QAAO;IACL,MAAM;IACN,cAAc;IACf;UAEG;AACN,OAAI,eAAe,iBAAiB,SAAS,QAAQ,CACnD,KAAI;AACF,UAAM,OAAO,iBAAiB;AAC9B,WAAO;KACL,MAAM;KACN,YAAY;KACb;WACK;AACN,WAAO,EAAE,MAAM,WAAW;;AAG9B,UAAO,EAAE,MAAM,WAAW;;AAE5B,SAAO,EAAE,MAAM,WAAW;;CAG5B,qBACE,cACoD;EACpD,MAAM,QAAQ,sEAAsE,KAClF,UAAU,MAAM,CACjB;AACD,MAAI,CAAC,OAAO,QAAQ,MAClB;AAEF,SAAO;GACL,OAAO,MAAM,OAAO;GACpB,SAAS,MAAM,OAAO;GACvB;;CAGH,iBAAyB,cACvB,UAAU,WAAW,IAAI,IACzB,UAAU,WAAW,IAAI,IACzB,UAAU,SAAS,KAAK,IAAI"}
1
+ {"version":3,"file":"app-install-source.service.js","names":[],"sources":["../../src/services/app-install-source.service.ts"],"sourcesContent":["import { access } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport type { AppManifestService } from \"#app-runtime/services/app-manifest.service.js\";\nimport type { AppRemoteRegistryClientService } from \"#app-runtime/services/app-remote-registry-client.service.js\";\nimport type { AppBundleService } from \"#app-runtime/services/app-bundle.service.js\";\nimport type { AppInstallProgressHandler } from \"#app-runtime/types/app-installation.types.js\";\n\ntype LocalAppSource =\n | { kind: \"directory\"; appDirectory: string }\n | { kind: \"bundle\"; bundlePath: string }\n | { kind: \"missing\" };\n\nexport type ResolvedAppInstallSource =\n | {\n kind: \"directory\";\n appDirectory: string;\n sourceRef: string;\n }\n | {\n kind: \"bundle\";\n bundlePath: string;\n sourceRef: string;\n }\n | {\n kind: \"registry\";\n sourceRef: string;\n registryResolution: Awaited<ReturnType<AppRemoteRegistryClientService[\"resolve\"]>>;\n };\n\nexport class AppInstallSourceService {\n constructor(\n private readonly manifestService: AppManifestService,\n private readonly remoteRegistryClient: AppRemoteRegistryClientService,\n private readonly bundleService: AppBundleService,\n ) {}\n\n materializeBundle = async (\n source: ResolvedAppInstallSource,\n tempDirectory: string,\n onProgress?: AppInstallProgressHandler,\n ): Promise<string> => {\n if (source.kind === \"directory\") {\n return (await this.bundleService.packAppDirectory({\n appDirectory: source.appDirectory,\n outputPath: path.join(tempDirectory, \"app.napp\"),\n })).bundlePath;\n }\n if (source.kind === \"bundle\") {\n return source.bundlePath;\n }\n await onProgress?.(\"downloading\");\n return (await this.remoteRegistryClient.downloadBundle({\n resolution: source.registryResolution,\n targetDirectory: tempDirectory,\n })).bundlePath;\n };\n\n resolve = async (\n appSource: string,\n registryUrl?: string,\n ): Promise<ResolvedAppInstallSource> => {\n const localSource = await this.detectLocal(appSource);\n if (localSource.kind === \"directory\") {\n return {\n kind: \"directory\",\n appDirectory: localSource.appDirectory,\n sourceRef: localSource.appDirectory,\n };\n }\n if (localSource.kind === \"bundle\") {\n return {\n kind: \"bundle\",\n bundlePath: localSource.bundlePath,\n sourceRef: localSource.bundlePath,\n };\n }\n const registrySpec = this.parseRegistrySpec(appSource);\n if (!registrySpec) {\n if (this.looksLikePath(appSource)) {\n throw new Error(`本地安装源不存在:${appSource}`);\n }\n throw new Error(`无法识别安装源:${appSource}`);\n }\n const registryResolution = await this.remoteRegistryClient.resolve({\n appId: registrySpec.appId,\n version: registrySpec.version,\n registryUrl,\n });\n return {\n kind: \"registry\",\n sourceRef: `${registryResolution.appId}@${registryResolution.version}`,\n registryResolution,\n };\n };\n\n detectLocal = async (\n sourcePath: string,\n allowBundle: boolean = true,\n ): Promise<LocalAppSource> => {\n const normalizedSource = path.resolve(sourcePath);\n try {\n await access(normalizedSource);\n const manifestBundle = await this.manifestService.load(normalizedSource);\n if (manifestBundle.appDirectory) {\n return {\n kind: \"directory\",\n appDirectory: normalizedSource,\n };\n }\n } catch {\n if (allowBundle && normalizedSource.endsWith(\".napp\")) {\n try {\n await access(normalizedSource);\n return {\n kind: \"bundle\",\n bundlePath: normalizedSource,\n };\n } catch {\n return { kind: \"missing\" };\n }\n }\n return { kind: \"missing\" };\n }\n return { kind: \"missing\" };\n };\n\n private parseRegistrySpec = (\n appSource: string,\n ): { appId: string; version?: string } | undefined => {\n const match = /^(?<appId>[a-z0-9][a-z0-9._-]*)(?:@(?<version>[A-Za-z0-9._+-]+))?$/i.exec(\n appSource.trim(),\n );\n if (!match?.groups?.appId) {\n return undefined;\n }\n return {\n appId: match.groups.appId,\n version: match.groups.version,\n };\n };\n\n private looksLikePath = (appSource: string): boolean =>\n appSource.startsWith(\".\") ||\n appSource.startsWith(\"/\") ||\n appSource.includes(path.sep);\n}\n"],"mappings":";;;AA6BA,IAAa,0BAAb,MAAqC;CACnC,YACE,iBACA,sBACA,eACA;AAHiB,OAAA,kBAAA;AACA,OAAA,uBAAA;AACA,OAAA,gBAAA;;CAGnB,oBAAoB,OAClB,QACA,eACA,eACoB;AACpB,MAAI,OAAO,SAAS,YAClB,SAAQ,MAAM,KAAK,cAAc,iBAAiB;GAChD,cAAc,OAAO;GACrB,YAAY,KAAK,KAAK,eAAe,WAAW;GACjD,CAAC,EAAE;AAEN,MAAI,OAAO,SAAS,SAClB,QAAO,OAAO;AAEhB,QAAM,aAAa,cAAc;AACjC,UAAQ,MAAM,KAAK,qBAAqB,eAAe;GACrD,YAAY,OAAO;GACnB,iBAAiB;GAClB,CAAC,EAAE;;CAGN,UAAU,OACR,WACA,gBACsC;EACtC,MAAM,cAAc,MAAM,KAAK,YAAY,UAAU;AACrD,MAAI,YAAY,SAAS,YACvB,QAAO;GACL,MAAM;GACN,cAAc,YAAY;GAC1B,WAAW,YAAY;GACxB;AAEH,MAAI,YAAY,SAAS,SACvB,QAAO;GACL,MAAM;GACN,YAAY,YAAY;GACxB,WAAW,YAAY;GACxB;EAEH,MAAM,eAAe,KAAK,kBAAkB,UAAU;AACtD,MAAI,CAAC,cAAc;AACjB,OAAI,KAAK,cAAc,UAAU,CAC/B,OAAM,IAAI,MAAM,YAAY,YAAY;AAE1C,SAAM,IAAI,MAAM,WAAW,YAAY;;EAEzC,MAAM,qBAAqB,MAAM,KAAK,qBAAqB,QAAQ;GACjE,OAAO,aAAa;GACpB,SAAS,aAAa;GACtB;GACD,CAAC;AACF,SAAO;GACL,MAAM;GACN,WAAW,GAAG,mBAAmB,MAAM,GAAG,mBAAmB;GAC7D;GACD;;CAGH,cAAc,OACZ,YACA,cAAuB,SACK;EAC5B,MAAM,mBAAmB,KAAK,QAAQ,WAAW;AACjD,MAAI;AACF,SAAM,OAAO,iBAAiB;AAE9B,QADuB,MAAM,KAAK,gBAAgB,KAAK,iBAAiB,EACrD,aACjB,QAAO;IACL,MAAM;IACN,cAAc;IACf;UAEG;AACN,OAAI,eAAe,iBAAiB,SAAS,QAAQ,CACnD,KAAI;AACF,UAAM,OAAO,iBAAiB;AAC9B,WAAO;KACL,MAAM;KACN,YAAY;KACb;WACK;AACN,WAAO,EAAE,MAAM,WAAW;;AAG9B,UAAO,EAAE,MAAM,WAAW;;AAE5B,SAAO,EAAE,MAAM,WAAW;;CAG5B,qBACE,cACoD;EACpD,MAAM,QAAQ,sEAAsE,KAClF,UAAU,MAAM,CACjB;AACD,MAAI,CAAC,OAAO,QAAQ,MAClB;AAEF,SAAO;GACL,OAAO,MAAM,OAAO;GACpB,SAAS,MAAM,OAAO;GACvB;;CAGH,iBAAyB,cACvB,UAAU,WAAW,IAAI,IACzB,UAAU,WAAW,IAAI,IACzB,UAAU,SAAS,KAAK,IAAI"}
@@ -0,0 +1,68 @@
1
+ import { isAppStandaloneManifestBundle } from "../types/app-manifest.types.js";
2
+ import { randomUUID } from "node:crypto";
3
+ import { cp, mkdir, readdir, rename, rm } from "node:fs/promises";
4
+ import path from "node:path";
5
+ //#region src/services/app-installation-filesystem.service.ts
6
+ var AppInstallationFilesystemService = class {
7
+ constructor(params) {
8
+ this.params = params;
9
+ }
10
+ copyToImmutableInstallDirectory = async (params) => {
11
+ const { appId, appVersion, extractedDirectory, installDirectory } = params;
12
+ if (await this.params.integrityService.pathExists(installDirectory)) throw new Error(`应用版本目录已存在,不能覆盖不可变版本:${appId}@${appVersion}`);
13
+ await mkdir(path.dirname(installDirectory), { recursive: true });
14
+ const stagedInstallDirectory = `${installDirectory}.staging-${randomUUID()}`;
15
+ try {
16
+ await cp(extractedDirectory, stagedInstallDirectory, { recursive: true });
17
+ const stagedManifest = await this.params.manifestService.load(stagedInstallDirectory);
18
+ if (stagedManifest.manifest.id !== appId || stagedManifest.manifest.version !== appVersion) throw new Error("staging manifest 与已验证 bundle 身份不一致。");
19
+ const contentSha256 = await this.params.integrityService.calculateDigest(stagedInstallDirectory);
20
+ await rename(stagedInstallDirectory, installDirectory);
21
+ await this.params.integrityService.protectDirectory(installDirectory);
22
+ return contentSha256;
23
+ } catch (error) {
24
+ await rm(stagedInstallDirectory, {
25
+ recursive: true,
26
+ force: true
27
+ });
28
+ throw error;
29
+ }
30
+ };
31
+ materializeDistribution = async (params) => {
32
+ if (params.distributionMode !== "source") return;
33
+ const manifestBundle = await this.params.manifestService.load(params.appDirectory);
34
+ if (!isAppStandaloneManifestBundle(manifestBundle)) throw new Error("schema v2 组合包不允许运行安装期 build。");
35
+ if (manifestBundle.manifest.main.kind !== "wasi-http-component") return;
36
+ await this.params.buildService.build({
37
+ appDirectory: params.appDirectory,
38
+ install: true
39
+ });
40
+ };
41
+ reconcileGeneratedSiblings = async (parentDirectory, referencedPaths) => {
42
+ for (const entry of await this.readDirectories(parentDirectory)) {
43
+ const entryName = path.basename(entry);
44
+ if (entryName.includes(".staging-")) {
45
+ await this.params.integrityService.removeDirectory(entry);
46
+ continue;
47
+ }
48
+ const markerIndex = entryName.indexOf(".uninstalling-");
49
+ if (markerIndex < 0) continue;
50
+ const originalPath = path.join(parentDirectory, entryName.slice(0, markerIndex));
51
+ if (referencedPaths.has(path.resolve(originalPath)) && !await this.params.integrityService.pathExists(originalPath)) await rename(entry, originalPath);
52
+ else await this.params.integrityService.removeDirectory(entry);
53
+ }
54
+ };
55
+ readDirectories = async (directory) => {
56
+ try {
57
+ return (await readdir(directory, { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => path.join(directory, entry.name));
58
+ } catch (error) {
59
+ if (this.isMissingFileError(error)) return [];
60
+ throw error;
61
+ }
62
+ };
63
+ isMissingFileError = (error) => typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
64
+ };
65
+ //#endregion
66
+ export { AppInstallationFilesystemService };
67
+
68
+ //# sourceMappingURL=app-installation-filesystem.service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app-installation-filesystem.service.js","names":[],"sources":["../../src/services/app-installation-filesystem.service.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport { cp, mkdir, readdir, rename, rm } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport type { AppBuildService } from \"#app-runtime/services/app-build.service.js\";\nimport type { AppInstallationIntegrityService } from \"#app-runtime/services/app-installation-integrity.service.js\";\nimport type { AppManifestService } from \"#app-runtime/services/app-manifest.service.js\";\nimport { isAppStandaloneManifestBundle } from \"#app-runtime/types/app-manifest.types.js\";\nimport type { AppDistributionMode } from \"#app-runtime/types/app-bundle.types.js\";\n\nexport class AppInstallationFilesystemService {\n constructor(private readonly params: {\n buildService: AppBuildService;\n integrityService: AppInstallationIntegrityService;\n manifestService: AppManifestService;\n }) {}\n\n copyToImmutableInstallDirectory = async (params: {\n appId: string;\n appVersion: string;\n extractedDirectory: string;\n installDirectory: string;\n }): Promise<string> => {\n const { appId, appVersion, extractedDirectory, installDirectory } = params;\n if (await this.params.integrityService.pathExists(installDirectory)) {\n throw new Error(`应用版本目录已存在,不能覆盖不可变版本:${appId}@${appVersion}`);\n }\n await mkdir(path.dirname(installDirectory), { recursive: true });\n const stagedInstallDirectory = `${installDirectory}.staging-${randomUUID()}`;\n try {\n await cp(extractedDirectory, stagedInstallDirectory, { recursive: true });\n const stagedManifest = await this.params.manifestService.load(stagedInstallDirectory);\n if (\n stagedManifest.manifest.id !== appId ||\n stagedManifest.manifest.version !== appVersion\n ) {\n throw new Error(\"staging manifest 与已验证 bundle 身份不一致。\");\n }\n const contentSha256 = await this.params.integrityService.calculateDigest(\n stagedInstallDirectory,\n );\n await rename(stagedInstallDirectory, installDirectory);\n await this.params.integrityService.protectDirectory(installDirectory);\n return contentSha256;\n } catch (error) {\n await rm(stagedInstallDirectory, { recursive: true, force: true });\n throw error;\n }\n };\n\n materializeDistribution = async (params: {\n appDirectory: string;\n distributionMode: AppDistributionMode;\n }): Promise<void> => {\n if (params.distributionMode !== \"source\") {\n return;\n }\n const manifestBundle = await this.params.manifestService.load(params.appDirectory);\n if (!isAppStandaloneManifestBundle(manifestBundle)) {\n throw new Error(\"schema v2 组合包不允许运行安装期 build。\");\n }\n if (manifestBundle.manifest.main.kind !== \"wasi-http-component\") {\n return;\n }\n await this.params.buildService.build({\n appDirectory: params.appDirectory,\n install: true,\n });\n };\n\n reconcileGeneratedSiblings = async (\n parentDirectory: string,\n referencedPaths: Set<string>,\n ): Promise<void> => {\n for (const entry of await this.readDirectories(parentDirectory)) {\n const entryName = path.basename(entry);\n if (entryName.includes(\".staging-\")) {\n await this.params.integrityService.removeDirectory(entry);\n continue;\n }\n const markerIndex = entryName.indexOf(\".uninstalling-\");\n if (markerIndex < 0) {\n continue;\n }\n const originalPath = path.join(parentDirectory, entryName.slice(0, markerIndex));\n if (\n referencedPaths.has(path.resolve(originalPath)) &&\n !await this.params.integrityService.pathExists(originalPath)\n ) {\n await rename(entry, originalPath);\n } else {\n await this.params.integrityService.removeDirectory(entry);\n }\n }\n };\n\n readDirectories = async (directory: string): Promise<string[]> => {\n try {\n return (await readdir(directory, { withFileTypes: true }))\n .filter((entry) => entry.isDirectory())\n .map((entry) => path.join(directory, entry.name));\n } catch (error) {\n if (this.isMissingFileError(error)) {\n return [];\n }\n throw error;\n }\n };\n\n private isMissingFileError = (error: unknown): boolean =>\n typeof error === \"object\" && error !== null &&\n \"code\" in error && (error as { code?: unknown }).code === \"ENOENT\";\n}\n"],"mappings":";;;;;AASA,IAAa,mCAAb,MAA8C;CAC5C,YAAY,QAIT;AAJ0B,OAAA,SAAA;;CAM7B,kCAAkC,OAAO,WAKlB;EACrB,MAAM,EAAE,OAAO,YAAY,oBAAoB,qBAAqB;AACpE,MAAI,MAAM,KAAK,OAAO,iBAAiB,WAAW,iBAAiB,CACjE,OAAM,IAAI,MAAM,uBAAuB,MAAM,GAAG,aAAa;AAE/D,QAAM,MAAM,KAAK,QAAQ,iBAAiB,EAAE,EAAE,WAAW,MAAM,CAAC;EAChE,MAAM,yBAAyB,GAAG,iBAAiB,WAAW,YAAY;AAC1E,MAAI;AACF,SAAM,GAAG,oBAAoB,wBAAwB,EAAE,WAAW,MAAM,CAAC;GACzE,MAAM,iBAAiB,MAAM,KAAK,OAAO,gBAAgB,KAAK,uBAAuB;AACrF,OACE,eAAe,SAAS,OAAO,SAC/B,eAAe,SAAS,YAAY,WAEpC,OAAM,IAAI,MAAM,sCAAsC;GAExD,MAAM,gBAAgB,MAAM,KAAK,OAAO,iBAAiB,gBACvD,uBACD;AACD,SAAM,OAAO,wBAAwB,iBAAiB;AACtD,SAAM,KAAK,OAAO,iBAAiB,iBAAiB,iBAAiB;AACrE,UAAO;WACA,OAAO;AACd,SAAM,GAAG,wBAAwB;IAAE,WAAW;IAAM,OAAO;IAAM,CAAC;AAClE,SAAM;;;CAIV,0BAA0B,OAAO,WAGZ;AACnB,MAAI,OAAO,qBAAqB,SAC9B;EAEF,MAAM,iBAAiB,MAAM,KAAK,OAAO,gBAAgB,KAAK,OAAO,aAAa;AAClF,MAAI,CAAC,8BAA8B,eAAe,CAChD,OAAM,IAAI,MAAM,+BAA+B;AAEjD,MAAI,eAAe,SAAS,KAAK,SAAS,sBACxC;AAEF,QAAM,KAAK,OAAO,aAAa,MAAM;GACnC,cAAc,OAAO;GACrB,SAAS;GACV,CAAC;;CAGJ,6BAA6B,OAC3B,iBACA,oBACkB;AAClB,OAAK,MAAM,SAAS,MAAM,KAAK,gBAAgB,gBAAgB,EAAE;GAC/D,MAAM,YAAY,KAAK,SAAS,MAAM;AACtC,OAAI,UAAU,SAAS,YAAY,EAAE;AACnC,UAAM,KAAK,OAAO,iBAAiB,gBAAgB,MAAM;AACzD;;GAEF,MAAM,cAAc,UAAU,QAAQ,iBAAiB;AACvD,OAAI,cAAc,EAChB;GAEF,MAAM,eAAe,KAAK,KAAK,iBAAiB,UAAU,MAAM,GAAG,YAAY,CAAC;AAChF,OACE,gBAAgB,IAAI,KAAK,QAAQ,aAAa,CAAC,IAC/C,CAAC,MAAM,KAAK,OAAO,iBAAiB,WAAW,aAAa,CAE5D,OAAM,OAAO,OAAO,aAAa;OAEjC,OAAM,KAAK,OAAO,iBAAiB,gBAAgB,MAAM;;;CAK/D,kBAAkB,OAAO,cAAyC;AAChE,MAAI;AACF,WAAQ,MAAM,QAAQ,WAAW,EAAE,eAAe,MAAM,CAAC,EACtD,QAAQ,UAAU,MAAM,aAAa,CAAC,CACtC,KAAK,UAAU,KAAK,KAAK,WAAW,MAAM,KAAK,CAAC;WAC5C,OAAO;AACd,OAAI,KAAK,mBAAmB,MAAM,CAChC,QAAO,EAAE;AAEX,SAAM;;;CAIV,sBAA8B,UAC5B,OAAO,UAAU,YAAY,UAAU,QACvC,UAAU,SAAU,MAA6B,SAAS"}
@@ -0,0 +1,20 @@
1
+ import { AppManifestService } from "./app-manifest.service.js";
2
+ import { AppRegistryInstalledVersion } from "../types/app-registry.types.js";
3
+
4
+ //#region src/services/app-installation-integrity.service.d.ts
5
+ declare class AppInstallationIntegrityService {
6
+ private readonly manifestService;
7
+ constructor(manifestService?: AppManifestService);
8
+ calculateDigest: (installDirectory: string) => Promise<string>;
9
+ assertVersion: (params: {
10
+ appId: string;
11
+ version: string;
12
+ versionRecord: AppRegistryInstalledVersion;
13
+ }) => Promise<string>;
14
+ protectDirectory: (installDirectory: string) => Promise<void>;
15
+ removeDirectory: (directory: string) => Promise<void>;
16
+ pathExists: (targetPath: string) => Promise<boolean>;
17
+ }
18
+ //#endregion
19
+ export { AppInstallationIntegrityService };
20
+ //# sourceMappingURL=app-installation-integrity.service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app-installation-integrity.service.d.ts","names":[],"sources":["../../src/services/app-installation-integrity.service.ts"],"mappings":";;;;cAMa,+BAAA;EAAA,iBAEQ,eAAA;cAAA,eAAA,GAAiB,kBAAA;EAGpC,eAAA,GAAyB,gBAAA,aAA2B,OAAA;EAgCpD,aAAA,GAAuB,MAAA;IACrB,KAAA;IACA,OAAA;IACA,aAAA,EAAe,2BAAA;EAAA,MACb,OAAA;EAkBJ,gBAAA,GAA0B,gBAAA,aAA2B,OAAA;EAiBrD,eAAA,GAAyB,SAAA,aAAoB,OAAA;EAoB7C,UAAA,GAAoB,UAAA,aAAqB,OAAA;AAAA"}
@@ -0,0 +1,84 @@
1
+ import { AppManifestService } from "./app-manifest.service.js";
2
+ import { createHash } from "node:crypto";
3
+ import { access, chmod, lstat, readFile, readdir, readlink, rm } from "node:fs/promises";
4
+ import path from "node:path";
5
+ //#region src/services/app-installation-integrity.service.ts
6
+ var AppInstallationIntegrityService = class {
7
+ constructor(manifestService = new AppManifestService()) {
8
+ this.manifestService = manifestService;
9
+ }
10
+ calculateDigest = async (installDirectory) => {
11
+ const digest = createHash("sha256");
12
+ const visit = async (directory) => {
13
+ const entries = (await readdir(directory, { withFileTypes: true })).sort((left, right) => left.name.localeCompare(right.name));
14
+ for (const entry of entries) {
15
+ const entryPath = path.join(directory, entry.name);
16
+ const relativePath = path.relative(installDirectory, entryPath).replace(/\\/g, "/");
17
+ digest.update(relativePath);
18
+ digest.update("\0");
19
+ if (entry.isSymbolicLink()) {
20
+ digest.update(`link:${await readlink(entryPath)}`);
21
+ digest.update("\0");
22
+ continue;
23
+ }
24
+ if (entry.isDirectory()) {
25
+ digest.update("directory\0");
26
+ await visit(entryPath);
27
+ continue;
28
+ }
29
+ if (!entry.isFile()) throw new Error(`应用版本目录只允许普通文件、目录或符号链接:${relativePath}`);
30
+ digest.update("file\0");
31
+ digest.update(await readFile(entryPath));
32
+ digest.update("\0");
33
+ }
34
+ };
35
+ await visit(path.resolve(installDirectory));
36
+ return digest.digest("hex");
37
+ };
38
+ assertVersion = async (params) => {
39
+ const { appId, version, versionRecord } = params;
40
+ const targetManifest = await this.manifestService.load(versionRecord.installDirectory);
41
+ if (targetManifest.manifest.id !== appId || targetManifest.manifest.version !== version) throw new Error(`应用 ${appId}@${version} manifest 身份校验失败。`);
42
+ const contentSha256 = await this.calculateDigest(versionRecord.installDirectory);
43
+ if (versionRecord.contentSha256 && contentSha256 !== versionRecord.contentSha256) throw new Error(`应用 ${appId}@${version} 代码完整性校验失败,已阻止激活。请重新安装该版本。`);
44
+ return contentSha256;
45
+ };
46
+ protectDirectory = async (installDirectory) => {
47
+ const protect = async (targetPath) => {
48
+ const stats = await lstat(targetPath);
49
+ if (stats.isSymbolicLink()) return;
50
+ if (stats.isDirectory()) {
51
+ for (const entry of await readdir(targetPath)) await protect(path.join(targetPath, entry));
52
+ return;
53
+ }
54
+ await chmod(targetPath, stats.mode & -147);
55
+ };
56
+ await protect(path.resolve(installDirectory));
57
+ };
58
+ removeDirectory = async (directory) => {
59
+ if (!await this.pathExists(directory)) return;
60
+ const makeWritable = async (targetPath) => {
61
+ const stats = await lstat(targetPath);
62
+ if (stats.isSymbolicLink()) return;
63
+ await chmod(targetPath, stats.mode | 128 | (stats.isDirectory() ? 448 : 0));
64
+ if (stats.isDirectory()) for (const entry of await readdir(targetPath)) await makeWritable(path.join(targetPath, entry));
65
+ };
66
+ await makeWritable(directory);
67
+ await rm(directory, {
68
+ recursive: true,
69
+ force: true
70
+ });
71
+ };
72
+ pathExists = async (targetPath) => {
73
+ try {
74
+ await access(targetPath);
75
+ return true;
76
+ } catch {
77
+ return false;
78
+ }
79
+ };
80
+ };
81
+ //#endregion
82
+ export { AppInstallationIntegrityService };
83
+
84
+ //# sourceMappingURL=app-installation-integrity.service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app-installation-integrity.service.js","names":[],"sources":["../../src/services/app-installation-integrity.service.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport { access, chmod, lstat, readFile, readdir, readlink, rm } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { AppManifestService } from \"#app-runtime/services/app-manifest.service.js\";\nimport type { AppRegistryInstalledVersion } from \"#app-runtime/types/app-registry.types.js\";\n\nexport class AppInstallationIntegrityService {\n constructor(\n private readonly manifestService: AppManifestService = new AppManifestService(),\n ) {}\n\n calculateDigest = async (installDirectory: string): Promise<string> => {\n const digest = createHash(\"sha256\");\n const visit = async (directory: string): Promise<void> => {\n const entries = (await readdir(directory, { withFileTypes: true }))\n .sort((left, right) => left.name.localeCompare(right.name));\n for (const entry of entries) {\n const entryPath = path.join(directory, entry.name);\n const relativePath = path.relative(installDirectory, entryPath).replace(/\\\\/g, \"/\");\n digest.update(relativePath);\n digest.update(\"\\0\");\n if (entry.isSymbolicLink()) {\n digest.update(`link:${await readlink(entryPath)}`);\n digest.update(\"\\0\");\n continue;\n }\n if (entry.isDirectory()) {\n digest.update(\"directory\\0\");\n await visit(entryPath);\n continue;\n }\n if (!entry.isFile()) {\n throw new Error(`应用版本目录只允许普通文件、目录或符号链接:${relativePath}`);\n }\n digest.update(\"file\\0\");\n digest.update(await readFile(entryPath));\n digest.update(\"\\0\");\n }\n };\n await visit(path.resolve(installDirectory));\n return digest.digest(\"hex\");\n };\n\n assertVersion = async (params: {\n appId: string;\n version: string;\n versionRecord: AppRegistryInstalledVersion;\n }): Promise<string> => {\n const { appId, version, versionRecord } = params;\n const targetManifest = await this.manifestService.load(versionRecord.installDirectory);\n if (\n targetManifest.manifest.id !== appId ||\n targetManifest.manifest.version !== version\n ) {\n throw new Error(`应用 ${appId}@${version} manifest 身份校验失败。`);\n }\n const contentSha256 = await this.calculateDigest(versionRecord.installDirectory);\n if (versionRecord.contentSha256 && contentSha256 !== versionRecord.contentSha256) {\n throw new Error(\n `应用 ${appId}@${version} 代码完整性校验失败,已阻止激活。请重新安装该版本。`,\n );\n }\n return contentSha256;\n };\n\n protectDirectory = async (installDirectory: string): Promise<void> => {\n const protect = async (targetPath: string): Promise<void> => {\n const stats = await lstat(targetPath);\n if (stats.isSymbolicLink()) {\n return;\n }\n if (stats.isDirectory()) {\n for (const entry of await readdir(targetPath)) {\n await protect(path.join(targetPath, entry));\n }\n return;\n }\n await chmod(targetPath, stats.mode & ~0o222);\n };\n await protect(path.resolve(installDirectory));\n };\n\n removeDirectory = async (directory: string): Promise<void> => {\n if (!await this.pathExists(directory)) {\n return;\n }\n const makeWritable = async (targetPath: string): Promise<void> => {\n const stats = await lstat(targetPath);\n if (stats.isSymbolicLink()) {\n return;\n }\n await chmod(targetPath, stats.mode | 0o200 | (stats.isDirectory() ? 0o700 : 0));\n if (stats.isDirectory()) {\n for (const entry of await readdir(targetPath)) {\n await makeWritable(path.join(targetPath, entry));\n }\n }\n };\n await makeWritable(directory);\n await rm(directory, { recursive: true, force: true });\n };\n\n pathExists = async (targetPath: string): Promise<boolean> => {\n try {\n await access(targetPath);\n return true;\n } catch {\n return false;\n }\n };\n}\n"],"mappings":";;;;;AAMA,IAAa,kCAAb,MAA6C;CAC3C,YACE,kBAAuD,IAAI,oBAAoB,EAC/E;AADiB,OAAA,kBAAA;;CAGnB,kBAAkB,OAAO,qBAA8C;EACrE,MAAM,SAAS,WAAW,SAAS;EACnC,MAAM,QAAQ,OAAO,cAAqC;GACxD,MAAM,WAAW,MAAM,QAAQ,WAAW,EAAE,eAAe,MAAM,CAAC,EAC/D,MAAM,MAAM,UAAU,KAAK,KAAK,cAAc,MAAM,KAAK,CAAC;AAC7D,QAAK,MAAM,SAAS,SAAS;IAC3B,MAAM,YAAY,KAAK,KAAK,WAAW,MAAM,KAAK;IAClD,MAAM,eAAe,KAAK,SAAS,kBAAkB,UAAU,CAAC,QAAQ,OAAO,IAAI;AACnF,WAAO,OAAO,aAAa;AAC3B,WAAO,OAAO,KAAK;AACnB,QAAI,MAAM,gBAAgB,EAAE;AAC1B,YAAO,OAAO,QAAQ,MAAM,SAAS,UAAU,GAAG;AAClD,YAAO,OAAO,KAAK;AACnB;;AAEF,QAAI,MAAM,aAAa,EAAE;AACvB,YAAO,OAAO,cAAc;AAC5B,WAAM,MAAM,UAAU;AACtB;;AAEF,QAAI,CAAC,MAAM,QAAQ,CACjB,OAAM,IAAI,MAAM,yBAAyB,eAAe;AAE1D,WAAO,OAAO,SAAS;AACvB,WAAO,OAAO,MAAM,SAAS,UAAU,CAAC;AACxC,WAAO,OAAO,KAAK;;;AAGvB,QAAM,MAAM,KAAK,QAAQ,iBAAiB,CAAC;AAC3C,SAAO,OAAO,OAAO,MAAM;;CAG7B,gBAAgB,OAAO,WAIA;EACrB,MAAM,EAAE,OAAO,SAAS,kBAAkB;EAC1C,MAAM,iBAAiB,MAAM,KAAK,gBAAgB,KAAK,cAAc,iBAAiB;AACtF,MACE,eAAe,SAAS,OAAO,SAC/B,eAAe,SAAS,YAAY,QAEpC,OAAM,IAAI,MAAM,MAAM,MAAM,GAAG,QAAQ,mBAAmB;EAE5D,MAAM,gBAAgB,MAAM,KAAK,gBAAgB,cAAc,iBAAiB;AAChF,MAAI,cAAc,iBAAiB,kBAAkB,cAAc,cACjE,OAAM,IAAI,MACR,MAAM,MAAM,GAAG,QAAQ,4BACxB;AAEH,SAAO;;CAGT,mBAAmB,OAAO,qBAA4C;EACpE,MAAM,UAAU,OAAO,eAAsC;GAC3D,MAAM,QAAQ,MAAM,MAAM,WAAW;AACrC,OAAI,MAAM,gBAAgB,CACxB;AAEF,OAAI,MAAM,aAAa,EAAE;AACvB,SAAK,MAAM,SAAS,MAAM,QAAQ,WAAW,CAC3C,OAAM,QAAQ,KAAK,KAAK,YAAY,MAAM,CAAC;AAE7C;;AAEF,SAAM,MAAM,YAAY,MAAM,OAAO,KAAO;;AAE9C,QAAM,QAAQ,KAAK,QAAQ,iBAAiB,CAAC;;CAG/C,kBAAkB,OAAO,cAAqC;AAC5D,MAAI,CAAC,MAAM,KAAK,WAAW,UAAU,CACnC;EAEF,MAAM,eAAe,OAAO,eAAsC;GAChE,MAAM,QAAQ,MAAM,MAAM,WAAW;AACrC,OAAI,MAAM,gBAAgB,CACxB;AAEF,SAAM,MAAM,YAAY,MAAM,OAAO,OAAS,MAAM,aAAa,GAAG,MAAQ,GAAG;AAC/E,OAAI,MAAM,aAAa,CACrB,MAAK,MAAM,SAAS,MAAM,QAAQ,WAAW,CAC3C,OAAM,aAAa,KAAK,KAAK,YAAY,MAAM,CAAC;;AAItD,QAAM,aAAa,UAAU;AAC7B,QAAM,GAAG,WAAW;GAAE,WAAW;GAAM,OAAO;GAAM,CAAC;;CAGvD,aAAa,OAAO,eAAyC;AAC3D,MAAI;AACF,SAAM,OAAO,WAAW;AACxB,UAAO;UACD;AACN,UAAO"}
@@ -0,0 +1,110 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { rename } from "node:fs/promises";
3
+ //#region src/services/app-installation-lifecycle.service.ts
4
+ var AppInstallationLifecycleService = class {
5
+ constructor(params) {
6
+ this.params = params;
7
+ }
8
+ update = async (appId, options = {}) => {
9
+ const { onProgress, registryUrl: requestedRegistryUrl, version } = options;
10
+ await onProgress?.("resolving");
11
+ const appRecord = await this.params.registryService.getApp(appId);
12
+ if (!appRecord) throw new Error(`未找到已安装应用:${appId}`);
13
+ const activeVersionRecord = appRecord.installedVersions[appRecord.activeVersion];
14
+ const registryUrl = requestedRegistryUrl ?? activeVersionRecord?.registryUrl ?? (await this.params.registryConfigService.getSnapshot()).currentUrl;
15
+ const resolution = await this.params.remoteRegistryClient.resolve({
16
+ appId,
17
+ version,
18
+ registryUrl
19
+ });
20
+ if (resolution.version === appRecord.activeVersion) {
21
+ if (!activeVersionRecord) throw new Error(`已安装应用缺少激活版本:${appId}`);
22
+ return this.toUpdateResult(appRecord, activeVersionRecord, appRecord.activeVersion, false);
23
+ }
24
+ const installedTarget = appRecord.installedVersions[resolution.version];
25
+ if (installedTarget) {
26
+ await onProgress?.("verifying");
27
+ await onProgress?.("installing");
28
+ if (options.activate !== false) await this.params.rollback(appId, resolution.version);
29
+ await onProgress?.("finalizing");
30
+ return this.toUpdateResult(appRecord, installedTarget, resolution.version, true);
31
+ }
32
+ return {
33
+ ...await this.params.install(`${appId}@${resolution.version}`, {
34
+ registryUrl,
35
+ onProgress,
36
+ activate: options.activate
37
+ }),
38
+ previousVersion: appRecord.activeVersion,
39
+ updated: true
40
+ };
41
+ };
42
+ uninstall = async (appId, purgeData) => {
43
+ const appRecord = await this.params.registryService.getApp(appId);
44
+ if (!appRecord) throw new Error(`未找到已安装应用:${appId}`);
45
+ if (!purgeData) await this.params.instanceStorageService.materializeDefaultInstance({
46
+ appId,
47
+ publisherId: appRecord.publisher?.id,
48
+ legacyDataDirectory: appRecord.defaultInstance.storage.layout === "legacy" ? appRecord.dataDirectory : void 0,
49
+ dataSchemaVersion: appRecord.defaultInstance.dataSchemaVersion
50
+ });
51
+ const removedVersions = Object.keys(appRecord.installedVersions).sort((left, right) => left.localeCompare(right));
52
+ const stagedPaths = [];
53
+ try {
54
+ for (const versionRecord of Object.values(appRecord.installedVersions)) await this.stagePath(versionRecord.installDirectory, stagedPaths);
55
+ if (purgeData) await this.stagePath(appRecord.defaultInstance.storage.layout === "instance-v1" ? appRecord.defaultInstance.storage.instanceDirectory : appRecord.dataDirectory, stagedPaths);
56
+ if (!await this.params.registryService.removeApp(appId)) throw new Error(`卸载过程中应用记录已发生变化:${appId}`);
57
+ } catch (error) {
58
+ await this.restoreOrThrow(appId, stagedPaths, error);
59
+ throw error;
60
+ }
61
+ await Promise.all(stagedPaths.map(async (entry) => await this.params.integrityService.removeDirectory(entry.stagedPath)));
62
+ return {
63
+ appId,
64
+ removedVersions,
65
+ dataRemoved: purgeData
66
+ };
67
+ };
68
+ toUpdateResult = (appRecord, versionRecord, version, updated) => ({
69
+ appId: appRecord.appId,
70
+ name: appRecord.name,
71
+ version,
72
+ previousVersion: appRecord.activeVersion,
73
+ installDirectory: versionRecord.installDirectory,
74
+ dataDirectory: appRecord.dataDirectory,
75
+ instance: appRecord.defaultInstance,
76
+ sourceKind: versionRecord.sourceKind,
77
+ distributionMode: versionRecord.distributionMode,
78
+ sourceRef: versionRecord.sourceRef,
79
+ permissions: versionRecord.permissions,
80
+ registryUrl: versionRecord.registryUrl,
81
+ bundleUrl: versionRecord.bundleUrl,
82
+ sha256: versionRecord.sha256,
83
+ publisher: versionRecord.publisher,
84
+ enabled: appRecord.enabled,
85
+ manifestSchemaVersion: versionRecord.manifestSchemaVersion,
86
+ components: versionRecord.components,
87
+ primaryPanelId: versionRecord.primaryPanelId,
88
+ updated
89
+ });
90
+ stagePath = async (originalPath, stagedPaths) => {
91
+ if (!await this.params.integrityService.pathExists(originalPath)) return;
92
+ const stagedPath = `${originalPath}.uninstalling-${randomUUID()}`;
93
+ await rename(originalPath, stagedPath);
94
+ stagedPaths.push({
95
+ originalPath,
96
+ stagedPath
97
+ });
98
+ };
99
+ restoreOrThrow = async (appId, stagedPaths, operationError) => {
100
+ try {
101
+ for (const entry of [...stagedPaths].reverse()) if (await this.params.integrityService.pathExists(entry.stagedPath)) await rename(entry.stagedPath, entry.originalPath);
102
+ } catch (restoreError) {
103
+ throw new AggregateError([operationError, restoreError], `卸载 ${appId} 失败,且无法完全恢复已暂存文件。`);
104
+ }
105
+ };
106
+ };
107
+ //#endregion
108
+ export { AppInstallationLifecycleService };
109
+
110
+ //# sourceMappingURL=app-installation-lifecycle.service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app-installation-lifecycle.service.js","names":[],"sources":["../../src/services/app-installation-lifecycle.service.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport { rename } from \"node:fs/promises\";\nimport type { AppInstallationIntegrityService } from \"#app-runtime/services/app-installation-integrity.service.js\";\nimport type { AppInstanceStorageService } from \"#app-runtime/services/app-instance-storage.service.js\";\nimport type { AppRegistryConfigService } from \"#app-runtime/services/app-registry-config.service.js\";\nimport type { AppRegistryService } from \"#app-runtime/services/app-registry.service.js\";\nimport type { AppRemoteRegistryClientService } from \"#app-runtime/services/app-remote-registry-client.service.js\";\nimport type {\n AppInstallProgressHandler,\n AppInstallResult,\n AppRollbackResult,\n AppUninstallResult,\n AppUpdateResult,\n} from \"#app-runtime/types/app-installation.types.js\";\n\ntype AppUpdateOptions = {\n version?: string;\n registryUrl?: string;\n onProgress?: AppInstallProgressHandler;\n activate?: boolean;\n};\n\nexport class AppInstallationLifecycleService {\n constructor(private readonly params: {\n registryService: AppRegistryService;\n registryConfigService: AppRegistryConfigService;\n remoteRegistryClient: AppRemoteRegistryClientService;\n instanceStorageService: AppInstanceStorageService;\n integrityService: AppInstallationIntegrityService;\n install: (source: string, options: AppUpdateOptions) => Promise<AppInstallResult>;\n rollback: (appId: string, version: string) => Promise<AppRollbackResult>;\n }) {}\n\n update = async (appId: string, options: AppUpdateOptions = {}): Promise<AppUpdateResult> => {\n const { onProgress, registryUrl: requestedRegistryUrl, version } = options;\n await onProgress?.(\"resolving\");\n const appRecord = await this.params.registryService.getApp(appId);\n if (!appRecord) {\n throw new Error(`未找到已安装应用:${appId}`);\n }\n const activeVersionRecord = appRecord.installedVersions[appRecord.activeVersion];\n const registryUrl = requestedRegistryUrl ??\n activeVersionRecord?.registryUrl ??\n (await this.params.registryConfigService.getSnapshot()).currentUrl;\n const resolution = await this.params.remoteRegistryClient.resolve({\n appId,\n version,\n registryUrl,\n });\n if (resolution.version === appRecord.activeVersion) {\n if (!activeVersionRecord) {\n throw new Error(`已安装应用缺少激活版本:${appId}`);\n }\n return this.toUpdateResult(appRecord, activeVersionRecord, appRecord.activeVersion, false);\n }\n const installedTarget = appRecord.installedVersions[resolution.version];\n if (installedTarget) {\n await onProgress?.(\"verifying\");\n await onProgress?.(\"installing\");\n if (options.activate !== false) {\n await this.params.rollback(appId, resolution.version);\n }\n await onProgress?.(\"finalizing\");\n return this.toUpdateResult(appRecord, installedTarget, resolution.version, true);\n }\n const installResult = await this.params.install(`${appId}@${resolution.version}`, {\n registryUrl,\n onProgress,\n activate: options.activate,\n });\n return {\n ...installResult,\n previousVersion: appRecord.activeVersion,\n updated: true,\n };\n };\n\n uninstall = async (appId: string, purgeData: boolean): Promise<AppUninstallResult> => {\n const appRecord = await this.params.registryService.getApp(appId);\n if (!appRecord) {\n throw new Error(`未找到已安装应用:${appId}`);\n }\n if (!purgeData) {\n await this.params.instanceStorageService.materializeDefaultInstance({\n appId,\n publisherId: appRecord.publisher?.id,\n legacyDataDirectory: appRecord.defaultInstance.storage.layout === \"legacy\"\n ? appRecord.dataDirectory\n : undefined,\n dataSchemaVersion: appRecord.defaultInstance.dataSchemaVersion,\n });\n }\n const removedVersions = Object.keys(appRecord.installedVersions).sort((left, right) =>\n left.localeCompare(right));\n const stagedPaths: Array<{ originalPath: string; stagedPath: string }> = [];\n try {\n for (const versionRecord of Object.values(appRecord.installedVersions)) {\n await this.stagePath(versionRecord.installDirectory, stagedPaths);\n }\n if (purgeData) {\n await this.stagePath(\n appRecord.defaultInstance.storage.layout === \"instance-v1\"\n ? appRecord.defaultInstance.storage.instanceDirectory\n : appRecord.dataDirectory,\n stagedPaths,\n );\n }\n if (!await this.params.registryService.removeApp(appId)) {\n throw new Error(`卸载过程中应用记录已发生变化:${appId}`);\n }\n } catch (error) {\n await this.restoreOrThrow(appId, stagedPaths, error);\n throw error;\n }\n await Promise.all(stagedPaths.map(async (entry) =>\n await this.params.integrityService.removeDirectory(entry.stagedPath)));\n return { appId, removedVersions, dataRemoved: purgeData };\n };\n\n private toUpdateResult = (\n appRecord: NonNullable<Awaited<ReturnType<AppRegistryService[\"getApp\"]>>>,\n versionRecord: NonNullable<Awaited<ReturnType<AppRegistryService[\"getApp\"]>>>[\"installedVersions\"][string],\n version: string,\n updated: boolean,\n ): AppUpdateResult => ({\n appId: appRecord.appId,\n name: appRecord.name,\n version,\n previousVersion: appRecord.activeVersion,\n installDirectory: versionRecord.installDirectory,\n dataDirectory: appRecord.dataDirectory,\n instance: appRecord.defaultInstance,\n sourceKind: versionRecord.sourceKind,\n distributionMode: versionRecord.distributionMode,\n sourceRef: versionRecord.sourceRef,\n permissions: versionRecord.permissions,\n registryUrl: versionRecord.registryUrl,\n bundleUrl: versionRecord.bundleUrl,\n sha256: versionRecord.sha256,\n publisher: versionRecord.publisher,\n enabled: appRecord.enabled,\n manifestSchemaVersion: versionRecord.manifestSchemaVersion,\n components: versionRecord.components,\n primaryPanelId: versionRecord.primaryPanelId,\n updated,\n });\n\n private stagePath = async (\n originalPath: string,\n stagedPaths: Array<{ originalPath: string; stagedPath: string }>,\n ): Promise<void> => {\n if (!await this.params.integrityService.pathExists(originalPath)) {\n return;\n }\n const stagedPath = `${originalPath}.uninstalling-${randomUUID()}`;\n await rename(originalPath, stagedPath);\n stagedPaths.push({ originalPath, stagedPath });\n };\n\n private restoreOrThrow = async (\n appId: string,\n stagedPaths: Array<{ originalPath: string; stagedPath: string }>,\n operationError: unknown,\n ): Promise<void> => {\n try {\n for (const entry of [...stagedPaths].reverse()) {\n if (await this.params.integrityService.pathExists(entry.stagedPath)) {\n await rename(entry.stagedPath, entry.originalPath);\n }\n }\n } catch (restoreError) {\n throw new AggregateError(\n [operationError, restoreError],\n `卸载 ${appId} 失败,且无法完全恢复已暂存文件。`,\n );\n }\n };\n}\n"],"mappings":";;;AAsBA,IAAa,kCAAb,MAA6C;CAC3C,YAAY,QAQT;AAR0B,OAAA,SAAA;;CAU7B,SAAS,OAAO,OAAe,UAA4B,EAAE,KAA+B;EAC1F,MAAM,EAAE,YAAY,aAAa,sBAAsB,YAAY;AACnE,QAAM,aAAa,YAAY;EAC/B,MAAM,YAAY,MAAM,KAAK,OAAO,gBAAgB,OAAO,MAAM;AACjE,MAAI,CAAC,UACH,OAAM,IAAI,MAAM,YAAY,QAAQ;EAEtC,MAAM,sBAAsB,UAAU,kBAAkB,UAAU;EAClE,MAAM,cAAc,wBAClB,qBAAqB,gBACpB,MAAM,KAAK,OAAO,sBAAsB,aAAa,EAAE;EAC1D,MAAM,aAAa,MAAM,KAAK,OAAO,qBAAqB,QAAQ;GAChE;GACA;GACA;GACD,CAAC;AACF,MAAI,WAAW,YAAY,UAAU,eAAe;AAClD,OAAI,CAAC,oBACH,OAAM,IAAI,MAAM,eAAe,QAAQ;AAEzC,UAAO,KAAK,eAAe,WAAW,qBAAqB,UAAU,eAAe,MAAM;;EAE5F,MAAM,kBAAkB,UAAU,kBAAkB,WAAW;AAC/D,MAAI,iBAAiB;AACnB,SAAM,aAAa,YAAY;AAC/B,SAAM,aAAa,aAAa;AAChC,OAAI,QAAQ,aAAa,MACvB,OAAM,KAAK,OAAO,SAAS,OAAO,WAAW,QAAQ;AAEvD,SAAM,aAAa,aAAa;AAChC,UAAO,KAAK,eAAe,WAAW,iBAAiB,WAAW,SAAS,KAAK;;AAOlF,SAAO;GACL,GANoB,MAAM,KAAK,OAAO,QAAQ,GAAG,MAAM,GAAG,WAAW,WAAW;IAChF;IACA;IACA,UAAU,QAAQ;IACnB,CAAC;GAGA,iBAAiB,UAAU;GAC3B,SAAS;GACV;;CAGH,YAAY,OAAO,OAAe,cAAoD;EACpF,MAAM,YAAY,MAAM,KAAK,OAAO,gBAAgB,OAAO,MAAM;AACjE,MAAI,CAAC,UACH,OAAM,IAAI,MAAM,YAAY,QAAQ;AAEtC,MAAI,CAAC,UACH,OAAM,KAAK,OAAO,uBAAuB,2BAA2B;GAClE;GACA,aAAa,UAAU,WAAW;GAClC,qBAAqB,UAAU,gBAAgB,QAAQ,WAAW,WAC9D,UAAU,gBACV,KAAA;GACJ,mBAAmB,UAAU,gBAAgB;GAC9C,CAAC;EAEJ,MAAM,kBAAkB,OAAO,KAAK,UAAU,kBAAkB,CAAC,MAAM,MAAM,UAC3E,KAAK,cAAc,MAAM,CAAC;EAC5B,MAAM,cAAmE,EAAE;AAC3E,MAAI;AACF,QAAK,MAAM,iBAAiB,OAAO,OAAO,UAAU,kBAAkB,CACpE,OAAM,KAAK,UAAU,cAAc,kBAAkB,YAAY;AAEnE,OAAI,UACF,OAAM,KAAK,UACT,UAAU,gBAAgB,QAAQ,WAAW,gBACzC,UAAU,gBAAgB,QAAQ,oBAClC,UAAU,eACd,YACD;AAEH,OAAI,CAAC,MAAM,KAAK,OAAO,gBAAgB,UAAU,MAAM,CACrD,OAAM,IAAI,MAAM,kBAAkB,QAAQ;WAErC,OAAO;AACd,SAAM,KAAK,eAAe,OAAO,aAAa,MAAM;AACpD,SAAM;;AAER,QAAM,QAAQ,IAAI,YAAY,IAAI,OAAO,UACvC,MAAM,KAAK,OAAO,iBAAiB,gBAAgB,MAAM,WAAW,CAAC,CAAC;AACxE,SAAO;GAAE;GAAO;GAAiB,aAAa;GAAW;;CAG3D,kBACE,WACA,eACA,SACA,aACqB;EACrB,OAAO,UAAU;EACjB,MAAM,UAAU;EAChB;EACA,iBAAiB,UAAU;EAC3B,kBAAkB,cAAc;EAChC,eAAe,UAAU;EACzB,UAAU,UAAU;EACpB,YAAY,cAAc;EAC1B,kBAAkB,cAAc;EAChC,WAAW,cAAc;EACzB,aAAa,cAAc;EAC3B,aAAa,cAAc;EAC3B,WAAW,cAAc;EACzB,QAAQ,cAAc;EACtB,WAAW,cAAc;EACzB,SAAS,UAAU;EACnB,uBAAuB,cAAc;EACrC,YAAY,cAAc;EAC1B,gBAAgB,cAAc;EAC9B;EACD;CAED,YAAoB,OAClB,cACA,gBACkB;AAClB,MAAI,CAAC,MAAM,KAAK,OAAO,iBAAiB,WAAW,aAAa,CAC9D;EAEF,MAAM,aAAa,GAAG,aAAa,gBAAgB,YAAY;AAC/D,QAAM,OAAO,cAAc,WAAW;AACtC,cAAY,KAAK;GAAE;GAAc;GAAY,CAAC;;CAGhD,iBAAyB,OACvB,OACA,aACA,mBACkB;AAClB,MAAI;AACF,QAAK,MAAM,SAAS,CAAC,GAAG,YAAY,CAAC,SAAS,CAC5C,KAAI,MAAM,KAAK,OAAO,iBAAiB,WAAW,MAAM,WAAW,CACjE,OAAM,OAAO,MAAM,YAAY,MAAM,aAAa;WAG/C,cAAc;AACrB,SAAM,IAAI,eACR,CAAC,gBAAgB,aAAa,EAC9B,MAAM,MAAM,mBACb"}