@solidxai/core 0.1.10-beta.25 → 0.1.10-beta.26

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 (225) hide show
  1. package/dist/controllers/action-metadata.controller.d.ts +1 -20
  2. package/dist/controllers/action-metadata.controller.d.ts.map +1 -1
  3. package/dist/controllers/agent-event.controller.d.ts +1 -20
  4. package/dist/controllers/agent-event.controller.d.ts.map +1 -1
  5. package/dist/controllers/agent-session.controller.d.ts +1 -20
  6. package/dist/controllers/agent-session.controller.d.ts.map +1 -1
  7. package/dist/controllers/ai-interaction.controller.d.ts +1 -20
  8. package/dist/controllers/ai-interaction.controller.d.ts.map +1 -1
  9. package/dist/controllers/chatter-message-details.controller.d.ts +1 -20
  10. package/dist/controllers/chatter-message-details.controller.d.ts.map +1 -1
  11. package/dist/controllers/chatter-message.controller.d.ts +1 -20
  12. package/dist/controllers/chatter-message.controller.d.ts.map +1 -1
  13. package/dist/controllers/dashboard-user-layout.controller.d.ts +1 -20
  14. package/dist/controllers/dashboard-user-layout.controller.d.ts.map +1 -1
  15. package/dist/controllers/email-template.controller.d.ts +1 -20
  16. package/dist/controllers/email-template.controller.d.ts.map +1 -1
  17. package/dist/controllers/export-template.controller.d.ts +1 -20
  18. package/dist/controllers/export-template.controller.d.ts.map +1 -1
  19. package/dist/controllers/export-transaction.controller.d.ts +1 -20
  20. package/dist/controllers/export-transaction.controller.d.ts.map +1 -1
  21. package/dist/controllers/import-transaction-error-log.controller.d.ts +1 -20
  22. package/dist/controllers/import-transaction-error-log.controller.d.ts.map +1 -1
  23. package/dist/controllers/import-transaction.controller.d.ts +1 -20
  24. package/dist/controllers/import-transaction.controller.d.ts.map +1 -1
  25. package/dist/controllers/list-of-values.controller.d.ts +1 -20
  26. package/dist/controllers/list-of-values.controller.d.ts.map +1 -1
  27. package/dist/controllers/locale.controller.d.ts +1 -20
  28. package/dist/controllers/locale.controller.d.ts.map +1 -1
  29. package/dist/controllers/mcp-audit-log.controller.d.ts +1 -20
  30. package/dist/controllers/mcp-audit-log.controller.d.ts.map +1 -1
  31. package/dist/controllers/media.controller.d.ts +1 -20
  32. package/dist/controllers/media.controller.d.ts.map +1 -1
  33. package/dist/controllers/menu-item-metadata.controller.d.ts +1 -20
  34. package/dist/controllers/menu-item-metadata.controller.d.ts.map +1 -1
  35. package/dist/controllers/model-metadata.controller.d.ts +10 -30
  36. package/dist/controllers/model-metadata.controller.d.ts.map +1 -1
  37. package/dist/controllers/model-metadata.controller.js +2 -2
  38. package/dist/controllers/model-sequence.controller.d.ts +1 -20
  39. package/dist/controllers/model-sequence.controller.d.ts.map +1 -1
  40. package/dist/controllers/module-metadata-explorer.controller.d.ts +171 -0
  41. package/dist/controllers/module-metadata-explorer.controller.d.ts.map +1 -0
  42. package/dist/controllers/module-metadata-explorer.controller.js +143 -0
  43. package/dist/controllers/module-metadata-explorer.controller.js.map +1 -0
  44. package/dist/controllers/module-metadata.controller.d.ts +5 -0
  45. package/dist/controllers/module-metadata.controller.d.ts.map +1 -1
  46. package/dist/controllers/module-metadata.controller.js +12 -0
  47. package/dist/controllers/module-metadata.controller.js.map +1 -1
  48. package/dist/controllers/module-package.controller.d.ts +622 -0
  49. package/dist/controllers/module-package.controller.d.ts.map +1 -0
  50. package/dist/controllers/module-package.controller.js +150 -0
  51. package/dist/controllers/module-package.controller.js.map +1 -0
  52. package/dist/controllers/mq-message-queue.controller.d.ts +1 -20
  53. package/dist/controllers/mq-message-queue.controller.d.ts.map +1 -1
  54. package/dist/controllers/mq-message.controller.d.ts +1 -20
  55. package/dist/controllers/mq-message.controller.d.ts.map +1 -1
  56. package/dist/controllers/permission-metadata.controller.d.ts +1 -20
  57. package/dist/controllers/permission-metadata.controller.d.ts.map +1 -1
  58. package/dist/controllers/role-metadata.controller.d.ts +1 -20
  59. package/dist/controllers/role-metadata.controller.d.ts.map +1 -1
  60. package/dist/controllers/saved-filters.controller.d.ts +1 -20
  61. package/dist/controllers/saved-filters.controller.d.ts.map +1 -1
  62. package/dist/controllers/scheduled-job.controller.d.ts +1 -20
  63. package/dist/controllers/scheduled-job.controller.d.ts.map +1 -1
  64. package/dist/controllers/security-rule.controller.d.ts +1 -20
  65. package/dist/controllers/security-rule.controller.d.ts.map +1 -1
  66. package/dist/controllers/sms-template.controller.d.ts +1 -20
  67. package/dist/controllers/sms-template.controller.d.ts.map +1 -1
  68. package/dist/controllers/user-activity-history.controller.d.ts +1 -20
  69. package/dist/controllers/user-activity-history.controller.d.ts.map +1 -1
  70. package/dist/controllers/user-view-metadata.controller.d.ts +1 -20
  71. package/dist/controllers/user-view-metadata.controller.d.ts.map +1 -1
  72. package/dist/controllers/user.controller.d.ts +1 -20
  73. package/dist/controllers/user.controller.d.ts.map +1 -1
  74. package/dist/controllers/view-metadata.controller.d.ts +1 -20
  75. package/dist/controllers/view-metadata.controller.d.ts.map +1 -1
  76. package/dist/dtos/confirm-module-package-import.dto.d.ts +4 -0
  77. package/dist/dtos/confirm-module-package-import.dto.d.ts.map +1 -0
  78. package/dist/dtos/confirm-module-package-import.dto.js +34 -0
  79. package/dist/dtos/confirm-module-package-import.dto.js.map +1 -0
  80. package/dist/dtos/metadata-explorer-references-query.dto.d.ts +10 -0
  81. package/dist/dtos/metadata-explorer-references-query.dto.d.ts.map +1 -0
  82. package/dist/dtos/metadata-explorer-references-query.dto.js +69 -0
  83. package/dist/dtos/metadata-explorer-references-query.dto.js.map +1 -0
  84. package/dist/dtos/metadata-explorer-search-query.dto.d.ts +8 -0
  85. package/dist/dtos/metadata-explorer-search-query.dto.d.ts.map +1 -0
  86. package/dist/dtos/metadata-explorer-search-query.dto.js +55 -0
  87. package/dist/dtos/metadata-explorer-search-query.dto.js.map +1 -0
  88. package/dist/dtos/metadata-explorer-write.dto.d.ts +4 -0
  89. package/dist/dtos/metadata-explorer-write.dto.d.ts.map +1 -0
  90. package/dist/dtos/metadata-explorer-write.dto.js +30 -0
  91. package/dist/dtos/metadata-explorer-write.dto.js.map +1 -0
  92. package/dist/dtos/run-module-package-build.dto.d.ts +5 -0
  93. package/dist/dtos/run-module-package-build.dto.d.ts.map +1 -0
  94. package/dist/dtos/run-module-package-build.dto.js +38 -0
  95. package/dist/dtos/run-module-package-build.dto.js.map +1 -0
  96. package/dist/dtos/run-module-package-seed.dto.d.ts +4 -0
  97. package/dist/dtos/run-module-package-seed.dto.d.ts.map +1 -0
  98. package/dist/dtos/run-module-package-seed.dto.js +34 -0
  99. package/dist/dtos/run-module-package-seed.dto.js.map +1 -0
  100. package/dist/helpers/command.service.js +1 -1
  101. package/dist/helpers/command.service.js.map +1 -1
  102. package/dist/helpers/module-metadata-helper.service.d.ts +2 -0
  103. package/dist/helpers/module-metadata-helper.service.d.ts.map +1 -1
  104. package/dist/helpers/module-metadata-helper.service.js +14 -12
  105. package/dist/helpers/module-metadata-helper.service.js.map +1 -1
  106. package/dist/helpers/module.helper.d.ts.map +1 -1
  107. package/dist/helpers/module.helper.js +11 -6
  108. package/dist/helpers/module.helper.js.map +1 -1
  109. package/dist/helpers/typeorm-migration-helpers.d.ts +12 -0
  110. package/dist/helpers/typeorm-migration-helpers.d.ts.map +1 -0
  111. package/dist/helpers/typeorm-migration-helpers.js +119 -0
  112. package/dist/helpers/typeorm-migration-helpers.js.map +1 -0
  113. package/dist/index.d.ts +8 -0
  114. package/dist/index.d.ts.map +1 -1
  115. package/dist/index.js +8 -0
  116. package/dist/index.js.map +1 -1
  117. package/dist/seeders/module-metadata-seeder.service.d.ts +1 -0
  118. package/dist/seeders/module-metadata-seeder.service.d.ts.map +1 -1
  119. package/dist/seeders/module-metadata-seeder.service.js +57 -38
  120. package/dist/seeders/module-metadata-seeder.service.js.map +1 -1
  121. package/dist/seeders/module-test-data.service.js +1 -1
  122. package/dist/seeders/module-test-data.service.js.map +1 -1
  123. package/dist/seeders/seed-data/solid-core-metadata.json +2364 -1106
  124. package/dist/services/crud-helper.service.d.ts +18 -0
  125. package/dist/services/crud-helper.service.d.ts.map +1 -1
  126. package/dist/services/crud-helper.service.js +59 -0
  127. package/dist/services/crud-helper.service.js.map +1 -1
  128. package/dist/services/crud.service.d.ts +1 -21
  129. package/dist/services/crud.service.d.ts.map +1 -1
  130. package/dist/services/crud.service.js +9 -75
  131. package/dist/services/crud.service.js.map +1 -1
  132. package/dist/services/genai/ingest-metadata.service.js +2 -2
  133. package/dist/services/genai/ingest-metadata.service.js.map +1 -1
  134. package/dist/services/media.service.d.ts +1 -20
  135. package/dist/services/media.service.d.ts.map +1 -1
  136. package/dist/services/mediaStorageProviders/file-s3-storage-provider.d.ts.map +1 -1
  137. package/dist/services/mediaStorageProviders/file-s3-storage-provider.js +31 -2
  138. package/dist/services/mediaStorageProviders/file-s3-storage-provider.js.map +1 -1
  139. package/dist/services/mediaStorageProviders/file-storage-provider.d.ts.map +1 -1
  140. package/dist/services/mediaStorageProviders/file-storage-provider.js +5 -1
  141. package/dist/services/mediaStorageProviders/file-storage-provider.js.map +1 -1
  142. package/dist/services/model-metadata.service.d.ts +15 -30
  143. package/dist/services/model-metadata.service.d.ts.map +1 -1
  144. package/dist/services/model-metadata.service.js +249 -29
  145. package/dist/services/model-metadata.service.js.map +1 -1
  146. package/dist/services/module-metadata-explorer.service.d.ts +161 -0
  147. package/dist/services/module-metadata-explorer.service.d.ts.map +1 -0
  148. package/dist/services/module-metadata-explorer.service.js +485 -0
  149. package/dist/services/module-metadata-explorer.service.js.map +1 -0
  150. package/dist/services/module-metadata.service.d.ts +7 -0
  151. package/dist/services/module-metadata.service.d.ts.map +1 -1
  152. package/dist/services/module-metadata.service.js +121 -2
  153. package/dist/services/module-metadata.service.js.map +1 -1
  154. package/dist/services/module-package.service.d.ts +266 -0
  155. package/dist/services/module-package.service.d.ts.map +1 -0
  156. package/dist/services/module-package.service.js +980 -0
  157. package/dist/services/module-package.service.js.map +1 -0
  158. package/dist/solid-core.module.d.ts.map +1 -1
  159. package/dist/solid-core.module.js +10 -2
  160. package/dist/solid-core.module.js.map +1 -1
  161. package/dist-tests/api/authenticate.spec.js +119 -0
  162. package/dist-tests/api/authenticate.spec.js.map +1 -0
  163. package/dist-tests/api/crud-service.findOne.cityMaster.spec.js +97 -0
  164. package/dist-tests/api/crud-service.findOne.cityMaster.spec.js.map +1 -0
  165. package/dist-tests/api/ping.spec.js +21 -0
  166. package/dist-tests/api/ping.spec.js.map +1 -0
  167. package/dist-tests/helpers/auth.js +41 -0
  168. package/dist-tests/helpers/auth.js.map +1 -0
  169. package/dist-tests/helpers/env.js +11 -0
  170. package/dist-tests/helpers/env.js.map +1 -0
  171. package/docs/dashboards/AGENTIC_DASHBOARD_IMPLEMENTATION_PLAN.md +438 -0
  172. package/docs/dashboards/dashboard-curl-smoke-tests.txt +146 -0
  173. package/docs/dashboards/delete-legacy-dashboard-metadata.sql +172 -0
  174. package/docs/grouping-enhancements.md +89 -0
  175. package/docs/java-spring/README.md +3 -0
  176. package/docs/java-spring/solid-core-module-deep-dive-report.md +1317 -0
  177. package/docs/module-package-import-handoff.md +691 -0
  178. package/docs/seed-changes.md +65 -0
  179. package/docs/test-data-workflow.md +200 -0
  180. package/docs/type-declaration-import-issue.md +24 -0
  181. package/package.json +2 -1
  182. package/src/controllers/module-metadata-explorer.controller.ts +83 -0
  183. package/src/controllers/module-metadata.controller.ts +6 -0
  184. package/src/controllers/module-package.controller.ts +102 -0
  185. package/src/dtos/confirm-module-package-import.dto.ts +12 -0
  186. package/src/dtos/metadata-explorer-references-query.dto.ts +39 -0
  187. package/src/dtos/metadata-explorer-search-query.dto.ts +28 -0
  188. package/src/dtos/metadata-explorer-write.dto.ts +11 -0
  189. package/src/dtos/run-module-package-build.dto.ts +14 -0
  190. package/src/dtos/run-module-package-seed.dto.ts +12 -0
  191. package/src/helpers/command.service.ts +1 -1
  192. package/src/helpers/module-metadata-helper.service.ts +19 -21
  193. package/src/helpers/module.helper.ts +12 -9
  194. package/src/helpers/typeorm-migration-helpers.ts +163 -0
  195. package/src/index.ts +8 -0
  196. package/src/seeders/module-metadata-seeder.service.ts +63 -38
  197. package/src/seeders/module-test-data.service.ts +1 -1
  198. package/src/seeders/seed-data/solid-core-metadata.json +2365 -1107
  199. package/src/services/crud-helper.service.ts +72 -0
  200. package/src/services/crud.service.ts +11 -104
  201. package/src/services/dashboard-providers/README.md +1 -1
  202. package/src/services/genai/ingest-metadata.service.ts +3 -3
  203. package/src/services/mediaStorageProviders/file-s3-storage-provider.ts +37 -2
  204. package/src/services/mediaStorageProviders/file-storage-provider.ts +5 -1
  205. package/src/services/model-metadata.service.ts +302 -98
  206. package/src/services/module-metadata-explorer.service.ts +670 -0
  207. package/src/services/module-metadata.service.ts +136 -10
  208. package/src/services/module-package.service.ts +1171 -0
  209. package/src/solid-core.module.ts +10 -2
  210. package/.claude/settings.local.json +0 -15
  211. package/CLAUDE.md +0 -26
  212. package/dist/services/database/database-bootstrap.service.d.ts +0 -12
  213. package/dist/services/database/database-bootstrap.service.d.ts.map +0 -1
  214. package/dist/services/database/database-bootstrap.service.js +0 -115
  215. package/dist/services/database/database-bootstrap.service.js.map +0 -1
  216. package/sql/default/mariadb/proc_CleanupModelMetadata.sql +0 -153
  217. package/sql/default/mariadb/proc_CleanupModuleMetadata.sql +0 -56
  218. package/sql/default/mssql/proc_CleanupModuleMetadata.sql +0 -78
  219. package/sql/default/mysql/proc_CleanupModelMetadata.sql +0 -153
  220. package/sql/default/mysql/proc_CleanupModuleMetadata.sql +0 -56
  221. package/sql/default/postgres/proc_CleanupModelMetadata.sql +0 -148
  222. package/sql/default/postgres/proc_CleanupModuleMetadata.sql +0 -50
  223. package/sql/default/postgres/scratchpad.sql.txt +0 -12
  224. package/src/services/1.js +0 -6
  225. package/src/services/database/database-bootstrap.service.ts +0 -91
@@ -0,0 +1,1171 @@
1
+ import {
2
+ BadRequestException,
3
+ Injectable,
4
+ Logger,
5
+ NotFoundException,
6
+ } from '@nestjs/common';
7
+ import { createHash } from 'crypto';
8
+ import archiver from 'archiver';
9
+ import { createWriteStream } from 'fs';
10
+ import { DisallowInProduction } from 'src/decorators/disallow-in-production.decorator';
11
+ import { ConfirmModulePackageImportDto } from 'src/dtos/confirm-module-package-import.dto';
12
+ import { RunModulePackageBuildDto } from 'src/dtos/run-module-package-build.dto';
13
+ import { RunModulePackageSeedDto } from 'src/dtos/run-module-package-seed.dto';
14
+ import { CommandService } from 'src/helpers/command.service';
15
+ import { ModuleMetadataHelperService } from 'src/helpers/module-metadata-helper.service';
16
+ import * as fs from 'fs/promises';
17
+ import * as path from 'path';
18
+ import { v4 as uuidv4 } from 'uuid';
19
+
20
+ type ModulePackageManifest = {
21
+ schemaVersion: string;
22
+ packageType: string;
23
+ exportedAt?: string;
24
+ generatedBy?: {
25
+ name: string;
26
+ version?: string;
27
+ };
28
+ module: {
29
+ name: string;
30
+ displayName?: string;
31
+ description?: string;
32
+ };
33
+ contents: {
34
+ metadataPath: string;
35
+ apiModulePath: string;
36
+ uiModulePath: string;
37
+ };
38
+ compatibility?: Record<string, unknown>;
39
+ postImport?: Record<string, unknown>;
40
+ checksums?: Record<string, string>;
41
+ };
42
+
43
+ type ValidationPayload = {
44
+ valid: boolean;
45
+ errors: string[];
46
+ warnings: string[];
47
+ };
48
+
49
+ type PreviewPayload = {
50
+ module: {
51
+ name: string;
52
+ displayName?: string;
53
+ description?: string;
54
+ };
55
+ manifest: ModulePackageManifest;
56
+ contentsSummary: {
57
+ totalEntries: number;
58
+ solidApiEntries: number;
59
+ solidUiEntries: number;
60
+ };
61
+ requiredPaths: {
62
+ metadataPath: string;
63
+ apiModulePath: string;
64
+ uiModulePath: string;
65
+ };
66
+ conflicts: string[];
67
+ fileTree: string[];
68
+ nextActions: string[];
69
+ };
70
+
71
+ type ParsedArchive = {
72
+ entries: string[];
73
+ archiveRootPrefix: string | null;
74
+ manifest: ModulePackageManifest;
75
+ metadataDocument: any;
76
+ validation: ValidationPayload;
77
+ preview: PreviewPayload;
78
+ };
79
+
80
+ type ModulePackageStatusFile = {
81
+ transactionKey: string;
82
+ status: string;
83
+ currentStep: string;
84
+ moduleName: string | null;
85
+ moduleDisplayName: string | null;
86
+ archiveFileName: string | null;
87
+ archiveFilePath: string | null;
88
+ extractDirPath: string | null;
89
+ archiveRootPrefix: string | null;
90
+ manifest: ModulePackageManifest | null;
91
+ metadataDocument: any;
92
+ preview: PreviewPayload | null;
93
+ validation: ValidationPayload | null;
94
+ conflicts: string[];
95
+ outputs: {
96
+ import: string | null;
97
+ build: string | null;
98
+ seed: string | null;
99
+ };
100
+ errorMessage: string | null;
101
+ createdAt: string;
102
+ updatedAt: string;
103
+ };
104
+
105
+ type ModulePackageExportFile = {
106
+ fileName: string;
107
+ filePath: string;
108
+ mimeType: string;
109
+ };
110
+
111
+ type ModulePackageRuntimeCleanupResult = {
112
+ runtimeRoot: string;
113
+ removedImportTransactions: number;
114
+ removedExportTransactions: number;
115
+ removedImportLooseEntries: number;
116
+ removedExportLooseEntries: number;
117
+ clearedActiveTransactionPointer: boolean;
118
+ clearedAt: string;
119
+ };
120
+
121
+ type ModulePackageActiveTransactionFile = {
122
+ transactionKey: string;
123
+ updatedAt: string;
124
+ };
125
+
126
+ enum ModulePackageStatus {
127
+ uploaded = 'uploaded',
128
+ validated = 'validated',
129
+ awaiting_confirmation = 'awaiting_confirmation',
130
+ import_running = 'import_running',
131
+ awaiting_restart = 'awaiting_restart',
132
+ build_running = 'build_running',
133
+ build_failed = 'build_failed',
134
+ build_succeeded = 'build_succeeded',
135
+ seed_running = 'seed_running',
136
+ seed_failed = 'seed_failed',
137
+ completed = 'completed',
138
+ failed = 'failed',
139
+ }
140
+
141
+ @Injectable()
142
+ export class ModulePackageService {
143
+ private readonly logger = new Logger(ModulePackageService.name);
144
+ private static readonly SUPPORTED_SCHEMA_VERSION = '1.0';
145
+ private static readonly SUPPORTED_PACKAGE_TYPE = 'solidx-module';
146
+
147
+ constructor(
148
+ private readonly commandService: CommandService,
149
+ private readonly moduleMetadataHelperService: ModuleMetadataHelperService,
150
+ ) { }
151
+
152
+ @DisallowInProduction()
153
+ async validateUpload(file: Express.Multer.File) {
154
+ if (!file) {
155
+ throw new BadRequestException('A .sldx archive file is required.');
156
+ }
157
+
158
+ await this.cleanupModulePackageTransactions();
159
+
160
+ const transactionKey = uuidv4();
161
+ const workingDir = await this.createWorkingDir(transactionKey);
162
+ const archiveFileName = file.originalname || path.basename(file.path);
163
+ const archiveFilePath = path.join(workingDir, archiveFileName);
164
+ const extractDirPath = path.join(workingDir, 'extract');
165
+
166
+ await fs.mkdir(extractDirPath, { recursive: true });
167
+ await fs.rename(file.path, archiveFilePath);
168
+
169
+ const parsed = await this.parseArchive(archiveFilePath);
170
+ await this.extractArchive(archiveFilePath, extractDirPath);
171
+
172
+ const transaction: ModulePackageStatusFile = {
173
+ transactionKey,
174
+ status: parsed.validation.valid ? ModulePackageStatus.awaiting_confirmation : ModulePackageStatus.failed,
175
+ currentStep: 'preview',
176
+ moduleName: parsed.manifest.module.name,
177
+ moduleDisplayName: parsed.manifest.module.displayName ?? null,
178
+ archiveFileName,
179
+ archiveFilePath,
180
+ extractDirPath,
181
+ archiveRootPrefix: parsed.archiveRootPrefix,
182
+ manifest: parsed.manifest,
183
+ metadataDocument: parsed.metadataDocument,
184
+ preview: parsed.preview,
185
+ validation: parsed.validation,
186
+ conflicts: parsed.preview.conflicts,
187
+ outputs: {
188
+ import: null,
189
+ build: null,
190
+ seed: null,
191
+ },
192
+ errorMessage: parsed.validation.valid ? null : parsed.validation.errors.join('\n'),
193
+ createdAt: new Date().toISOString(),
194
+ updatedAt: new Date().toISOString(),
195
+ };
196
+
197
+ await this.writeStatusFile(transactionKey, transaction);
198
+ return this.toStatusResponse(transaction);
199
+ }
200
+
201
+ async getStatus(transactionKey: string) {
202
+ const transaction = await this.loadTransaction(transactionKey);
203
+ return this.toStatusResponse(transaction);
204
+ }
205
+
206
+ @DisallowInProduction()
207
+ async getLatestResumableImport() {
208
+ await this.cleanupModulePackageTransactions();
209
+ const transaction = await this.resolveLatestResumableTransaction();
210
+ return transaction ? this.toStatusResponse(transaction) : null;
211
+ }
212
+
213
+ @DisallowInProduction()
214
+ async dismissImport(transactionKey: string) {
215
+ const transaction = await this.loadTransaction(transactionKey);
216
+ await this.clearActiveTransactionIfMatches(transaction.transactionKey);
217
+ return this.toStatusResponse(transaction);
218
+ }
219
+
220
+ @DisallowInProduction()
221
+ async clearPackageRuntime(): Promise<ModulePackageRuntimeCleanupResult> {
222
+ const runtimeRoot = this.getModulePackageRuntimeRoot();
223
+ const importsRoot = this.getModulePackageImportsRoot();
224
+ const exportsRoot = this.getModulePackageExportsRoot();
225
+ const snapshot = await this.collectRuntimeCleanupSnapshot();
226
+
227
+ await fs.rm(runtimeRoot, { recursive: true, force: true });
228
+ await fs.mkdir(importsRoot, { recursive: true });
229
+ await fs.mkdir(exportsRoot, { recursive: true });
230
+
231
+ return {
232
+ runtimeRoot,
233
+ ...snapshot,
234
+ clearedAt: new Date().toISOString(),
235
+ };
236
+ }
237
+
238
+ @DisallowInProduction()
239
+ async exportModulePackage(moduleNameInput: string): Promise<ModulePackageExportFile> {
240
+ const moduleName = (moduleNameInput ?? '').trim();
241
+ if (!moduleName) {
242
+ throw new BadRequestException('A module name is required to export a module package.');
243
+ }
244
+
245
+ const solidApiModulePath = this.getSolidApiModuleTargetPath(moduleName);
246
+ const solidUiModulePath = this.getSolidUiModuleTargetPath(moduleName);
247
+ const metadataFilePath = await this.moduleMetadataHelperService.getModuleMetadataFilePath(moduleName);
248
+
249
+ await this.assertPathExists(solidApiModulePath, 'solid-api module path');
250
+ await this.assertPathExists(solidUiModulePath, 'solid-ui module path');
251
+ await this.assertPathExists(metadataFilePath, 'module metadata file');
252
+
253
+ const metadataDocument = this.parseMetadataDocument(
254
+ await fs.readFile(metadataFilePath, 'utf-8'),
255
+ [],
256
+ );
257
+
258
+ const transactionKey = uuidv4();
259
+ const exportRoot = path.join(this.getModulePackageExportsRoot(), transactionKey);
260
+ const stagingDir = path.join(exportRoot, 'staging');
261
+ const archiveFileName = `${moduleName}.sldx`;
262
+ const archiveFilePath = path.join(exportRoot, archiveFileName);
263
+
264
+ await fs.mkdir(path.join(stagingDir, 'solid-api', 'src'), { recursive: true });
265
+ await fs.mkdir(path.join(stagingDir, 'solid-ui', 'src'), { recursive: true });
266
+
267
+ await fs.cp(solidApiModulePath, path.join(stagingDir, 'solid-api', 'src', moduleName), { recursive: true });
268
+ await fs.cp(solidUiModulePath, path.join(stagingDir, 'solid-ui', 'src', moduleName), { recursive: true });
269
+
270
+ const manifest = await this.buildExportManifest(moduleName, metadataDocument, stagingDir);
271
+ await fs.writeFile(
272
+ path.join(stagingDir, 'manifest.json'),
273
+ JSON.stringify(manifest, null, 2),
274
+ 'utf-8',
275
+ );
276
+
277
+ await this.createArchiveFromDirectory(stagingDir, archiveFilePath);
278
+
279
+ return {
280
+ fileName: archiveFileName,
281
+ filePath: archiveFilePath,
282
+ mimeType: 'application/zip',
283
+ };
284
+ }
285
+
286
+ @DisallowInProduction()
287
+ async confirmImport(transactionKey: string, dto: ConfirmModulePackageImportDto = {}) {
288
+ const transaction = await this.loadTransaction(transactionKey);
289
+ const preview = transaction.preview;
290
+ const validation = transaction.validation;
291
+
292
+ if (!preview || !validation) {
293
+ throw new BadRequestException('The module package transaction is missing preview or validation data.');
294
+ }
295
+
296
+ if (!validation.valid) {
297
+ throw new BadRequestException({
298
+ message: 'The uploaded module package is invalid and cannot be imported.',
299
+ validation,
300
+ });
301
+ }
302
+
303
+ const overwriteExisting = dto.overwriteExisting ?? false;
304
+ if (preview.conflicts.length > 0 && !overwriteExisting) {
305
+ throw new BadRequestException({
306
+ message: 'Conflicts were detected. Re-submit with overwriteExisting=true to continue.',
307
+ conflicts: preview.conflicts,
308
+ });
309
+ }
310
+
311
+ transaction.status = ModulePackageStatus.import_running;
312
+ transaction.currentStep = 'import';
313
+ transaction.errorMessage = null;
314
+ await this.writeStatusFile(transactionKey, transaction);
315
+
316
+ try {
317
+ const moduleName = preview.module.name;
318
+ const solidApiTarget = this.getSolidApiModuleTargetPath(moduleName);
319
+ const solidUiTarget = this.getSolidUiModuleTargetPath(moduleName);
320
+ const extractRoot = transaction.archiveRootPrefix
321
+ ? path.join(transaction.extractDirPath as string, transaction.archiveRootPrefix.replace(/\/$/, ''))
322
+ : transaction.extractDirPath as string;
323
+ const solidApiSource = path.join(extractRoot, 'solid-api', 'src', moduleName);
324
+ const solidUiSource = path.join(extractRoot, 'solid-ui', 'src', moduleName);
325
+
326
+ await this.assertPathExists(solidApiSource, 'solid-api module source');
327
+ await this.assertPathExists(solidUiSource, 'solid-ui module source');
328
+
329
+ if (overwriteExisting) {
330
+ await fs.rm(solidApiTarget, { recursive: true, force: true });
331
+ await fs.rm(solidUiTarget, { recursive: true, force: true });
332
+ }
333
+
334
+ await fs.mkdir(path.dirname(solidApiTarget), { recursive: true });
335
+ await fs.mkdir(path.dirname(solidUiTarget), { recursive: true });
336
+
337
+ await fs.cp(solidApiSource, solidApiTarget, { recursive: true, force: overwriteExisting });
338
+ await fs.cp(solidUiSource, solidUiTarget, { recursive: true, force: overwriteExisting });
339
+
340
+ if (transaction.extractDirPath) {
341
+ await fs.rm(transaction.extractDirPath, { recursive: true, force: true });
342
+ transaction.extractDirPath = null;
343
+ }
344
+
345
+ transaction.status = ModulePackageStatus.awaiting_restart;
346
+ transaction.currentStep = 'restart';
347
+ transaction.outputs.import = [
348
+ `Placed solid-api module at ${solidApiTarget}`,
349
+ `Placed solid-ui module at ${solidUiTarget}`,
350
+ `Metadata file placed at ${preview.requiredPaths.metadataPath}`,
351
+ ].join('\n');
352
+ await this.writeStatusFile(transactionKey, transaction);
353
+ await this.syncActiveTransactionPointer(transaction);
354
+
355
+ return this.toStatusResponse(transaction);
356
+ } catch (error: any) {
357
+ transaction.status = ModulePackageStatus.failed;
358
+ transaction.currentStep = 'import';
359
+ transaction.errorMessage = error.message ?? 'Failed to import the module package.';
360
+ await this.writeStatusFile(transactionKey, transaction);
361
+ await this.syncActiveTransactionPointer(transaction);
362
+ throw error;
363
+ }
364
+ }
365
+
366
+ @DisallowInProduction()
367
+ async runBuild(transactionKey: string, dto: RunModulePackageBuildDto = {}) {
368
+ const transaction = await this.loadTransaction(transactionKey);
369
+ transaction.status = ModulePackageStatus.build_running;
370
+ transaction.currentStep = 'build';
371
+ transaction.errorMessage = null;
372
+ await this.writeStatusFile(transactionKey, transaction);
373
+
374
+ try {
375
+ const buildSolidApi = dto.buildSolidApi ?? true;
376
+ const buildSolidUi = dto.buildSolidUi ?? true;
377
+ const outputs: string[] = [];
378
+ const failedTargets: string[] = [];
379
+ const projectRoot = this.getProjectRoot();
380
+
381
+ if (buildSolidApi) {
382
+ try {
383
+ const output = await this.commandService.executeCommandWithArgs({
384
+ command: 'npx',
385
+ args: ['-y', '@solidxai/solidctl@latest', 'build'],
386
+ cwd: projectRoot,
387
+ });
388
+ outputs.push(`solidctl build [success]\n${output}`.trim());
389
+ } catch (error: any) {
390
+ failedTargets.push('solidctl build');
391
+ outputs.push(`solidctl build [failed]\n${error?.message ?? 'Build failed.'}`.trim());
392
+ }
393
+ }
394
+
395
+ if (buildSolidUi) {
396
+ try {
397
+ const output = await this.commandService.executeCommandWithArgs({
398
+ command: 'npx',
399
+ args: ['-y', '@solidxai/solidctl@latest', 'build', '--ui-only'],
400
+ cwd: projectRoot,
401
+ });
402
+ outputs.push(`solidctl build --ui-only [success]\n${output}`.trim());
403
+ } catch (error: any) {
404
+ failedTargets.push('solidctl build --ui-only');
405
+ outputs.push(`solidctl build --ui-only [failed]\n${error?.message ?? 'Build failed.'}`.trim());
406
+ }
407
+ }
408
+
409
+ transaction.status = failedTargets.length > 0
410
+ ? ModulePackageStatus.build_failed
411
+ : ModulePackageStatus.build_succeeded;
412
+ transaction.currentStep = 'build';
413
+ transaction.outputs.build = outputs.join('\n\n');
414
+ transaction.errorMessage = failedTargets.length > 0
415
+ ? `Build completed with errors in: ${failedTargets.join(', ')}`
416
+ : null;
417
+ await this.writeStatusFile(transactionKey, transaction);
418
+ await this.syncActiveTransactionPointer(transaction);
419
+
420
+ return this.toStatusResponse(transaction);
421
+ } catch (error: any) {
422
+ transaction.status = ModulePackageStatus.build_failed;
423
+ transaction.currentStep = 'build';
424
+ transaction.errorMessage = error.message ?? 'Build failed.';
425
+ transaction.outputs.build = error.message ?? '';
426
+ await this.writeStatusFile(transactionKey, transaction);
427
+ await this.syncActiveTransactionPointer(transaction);
428
+ throw error;
429
+ }
430
+ }
431
+
432
+ @DisallowInProduction()
433
+ async runSeed(transactionKey: string, dto: RunModulePackageSeedDto = {}) {
434
+ const transaction = await this.loadTransaction(transactionKey);
435
+ const moduleName = transaction.moduleName;
436
+
437
+ if (!moduleName) {
438
+ throw new BadRequestException('Unable to seed because the transaction is missing moduleName.');
439
+ }
440
+
441
+ transaction.status = ModulePackageStatus.seed_running;
442
+ transaction.currentStep = 'seed';
443
+ transaction.errorMessage = null;
444
+ await this.writeStatusFile(transactionKey, transaction);
445
+
446
+ try {
447
+ const output = await this.commandService.executeCommandWithArgs({
448
+ command: 'npx',
449
+ args: ['-y', '@solidxai/solidctl@latest', 'seed', '--modules-to-seed', moduleName],
450
+ cwd: this.getProjectRoot(),
451
+ });
452
+ transaction.status = ModulePackageStatus.completed;
453
+ transaction.currentStep = 'done';
454
+ transaction.outputs.seed = [
455
+ `solidctl seed --modules-to-seed ${moduleName} [success]`,
456
+ output,
457
+ ].filter(Boolean).join('\n');
458
+ await this.writeStatusFile(transactionKey, transaction);
459
+ await this.syncActiveTransactionPointer(transaction);
460
+
461
+ return this.toStatusResponse(transaction);
462
+ } catch (error: any) {
463
+ transaction.status = ModulePackageStatus.seed_failed;
464
+ transaction.currentStep = 'seed';
465
+ transaction.errorMessage = error.message ?? 'Seed failed.';
466
+ transaction.outputs.seed = error.message ?? '';
467
+ await this.writeStatusFile(transactionKey, transaction);
468
+ await this.syncActiveTransactionPointer(transaction);
469
+ return this.toStatusResponse(transaction);
470
+ }
471
+ }
472
+
473
+ private async parseArchive(archiveFilePath: string): Promise<ParsedArchive> {
474
+ const entries = await this.listArchiveEntries(archiveFilePath);
475
+ const archiveRootPrefix = this.detectArchiveRootPrefix(entries);
476
+ const normalizedEntries = this.normalizeArchiveEntries(entries, archiveRootPrefix);
477
+ const validationErrors: string[] = [];
478
+ const validationWarnings: string[] = [];
479
+
480
+ if (path.extname(archiveFilePath).toLowerCase() !== '.sldx') {
481
+ validationErrors.push('Only .sldx archives are supported.');
482
+ }
483
+
484
+ const unsafeEntries = normalizedEntries.filter((entry) => this.isUnsafeArchivePath(entry));
485
+ if (unsafeEntries.length > 0) {
486
+ validationErrors.push(`Archive contains unsafe paths: ${unsafeEntries.join(', ')}`);
487
+ }
488
+
489
+ if (!normalizedEntries.includes('manifest.json')) {
490
+ validationErrors.push('Archive is missing manifest.json at the root.');
491
+ }
492
+
493
+ const manifestArchivePath = this.toArchiveEntryPath(archiveRootPrefix, 'manifest.json');
494
+ const manifestText = normalizedEntries.includes('manifest.json')
495
+ ? await this.readArchiveEntry(archiveFilePath, manifestArchivePath)
496
+ : '';
497
+ const manifest = this.parseManifest(manifestText, validationErrors);
498
+
499
+ if (!manifest) {
500
+ return {
501
+ entries: normalizedEntries,
502
+ archiveRootPrefix,
503
+ manifest: this.getFallbackManifest(),
504
+ metadataDocument: null,
505
+ validation: {
506
+ valid: false,
507
+ errors: validationErrors,
508
+ warnings: validationWarnings,
509
+ },
510
+ preview: {
511
+ module: { name: '', displayName: '' },
512
+ manifest: this.getFallbackManifest(),
513
+ contentsSummary: {
514
+ totalEntries: normalizedEntries.length,
515
+ solidApiEntries: normalizedEntries.filter((entry) => entry.startsWith('solid-api/')).length,
516
+ solidUiEntries: normalizedEntries.filter((entry) => entry.startsWith('solid-ui/')).length,
517
+ },
518
+ requiredPaths: {
519
+ metadataPath: '',
520
+ apiModulePath: '',
521
+ uiModulePath: '',
522
+ },
523
+ conflicts: [],
524
+ fileTree: normalizedEntries,
525
+ nextActions: ['Fix archive structure and upload again.'],
526
+ },
527
+ };
528
+ }
529
+
530
+ if (manifest.packageType !== ModulePackageService.SUPPORTED_PACKAGE_TYPE) {
531
+ validationErrors.push(`manifest.json packageType must be "${ModulePackageService.SUPPORTED_PACKAGE_TYPE}".`);
532
+ }
533
+ if (manifest.schemaVersion !== ModulePackageService.SUPPORTED_SCHEMA_VERSION) {
534
+ validationErrors.push(`manifest.json schemaVersion must be "${ModulePackageService.SUPPORTED_SCHEMA_VERSION}".`);
535
+ }
536
+ if (!manifest.module?.name) {
537
+ validationErrors.push('manifest.json module.name is required.');
538
+ }
539
+
540
+ const moduleName = manifest.module?.name ?? '';
541
+ const expectedPaths = this.buildExpectedPaths(moduleName);
542
+
543
+ if (manifest.contents?.metadataPath !== expectedPaths.metadataPath) {
544
+ validationErrors.push(`manifest.json contents.metadataPath must be "${expectedPaths.metadataPath}".`);
545
+ }
546
+ if (manifest.contents?.apiModulePath !== expectedPaths.apiModulePath) {
547
+ validationErrors.push(`manifest.json contents.apiModulePath must be "${expectedPaths.apiModulePath}".`);
548
+ }
549
+ if (manifest.contents?.uiModulePath !== expectedPaths.uiModulePath) {
550
+ validationErrors.push(`manifest.json contents.uiModulePath must be "${expectedPaths.uiModulePath}".`);
551
+ }
552
+
553
+ if (!normalizedEntries.includes(expectedPaths.metadataPath)) {
554
+ validationErrors.push(`Archive is missing ${expectedPaths.metadataPath}.`);
555
+ }
556
+ if (!normalizedEntries.includes(expectedPaths.apiModulePath)) {
557
+ validationErrors.push(`Archive is missing ${expectedPaths.apiModulePath}.`);
558
+ }
559
+ if (!normalizedEntries.includes(expectedPaths.uiModulePath)) {
560
+ validationErrors.push(`Archive is missing ${expectedPaths.uiModulePath}.`);
561
+ }
562
+
563
+ const metadataDocument = normalizedEntries.includes(expectedPaths.metadataPath)
564
+ ? this.parseMetadataDocument(
565
+ await this.readArchiveEntry(archiveFilePath, this.toArchiveEntryPath(archiveRootPrefix, expectedPaths.metadataPath)),
566
+ validationErrors,
567
+ )
568
+ : null;
569
+
570
+ const metadataModuleName = metadataDocument?.moduleMetadata?.name;
571
+ if (metadataModuleName && metadataModuleName !== moduleName) {
572
+ validationErrors.push(`Metadata module name "${metadataModuleName}" does not match manifest module name "${moduleName}".`);
573
+ }
574
+
575
+ if (!manifest.checksums || Object.keys(manifest.checksums).length === 0) {
576
+ validationWarnings.push('manifest.json does not include checksums. Checksums are recommended.');
577
+ }
578
+
579
+ const conflicts = await this.detectConflicts(moduleName);
580
+ if (conflicts.length > 0) {
581
+ validationWarnings.push('The target module already exists locally and will require overwrite confirmation.');
582
+ }
583
+
584
+ const validation: ValidationPayload = {
585
+ valid: validationErrors.length === 0,
586
+ errors: validationErrors,
587
+ warnings: validationWarnings,
588
+ };
589
+
590
+ const preview: PreviewPayload = {
591
+ module: manifest.module,
592
+ manifest,
593
+ contentsSummary: {
594
+ totalEntries: normalizedEntries.length,
595
+ solidApiEntries: normalizedEntries.filter((entry) => entry.startsWith('solid-api/')).length,
596
+ solidUiEntries: normalizedEntries.filter((entry) => entry.startsWith('solid-ui/')).length,
597
+ },
598
+ requiredPaths: expectedPaths,
599
+ conflicts,
600
+ fileTree: normalizedEntries,
601
+ nextActions: [
602
+ 'Review archive preview.',
603
+ 'Confirm import to place files locally.',
604
+ 'Wait for service restart, then run build and seed.',
605
+ ],
606
+ };
607
+
608
+ return {
609
+ entries: normalizedEntries,
610
+ archiveRootPrefix,
611
+ manifest,
612
+ metadataDocument,
613
+ validation,
614
+ preview,
615
+ };
616
+ }
617
+
618
+ private parseManifest(manifestText: string, validationErrors: string[]): ModulePackageManifest | null {
619
+ if (!manifestText) {
620
+ return null;
621
+ }
622
+
623
+ try {
624
+ return JSON.parse(manifestText) as ModulePackageManifest;
625
+ } catch (error) {
626
+ validationErrors.push('manifest.json is not valid JSON.');
627
+ return null;
628
+ }
629
+ }
630
+
631
+ private parseMetadataDocument(documentText: string, validationErrors: string[]) {
632
+ try {
633
+ return JSON.parse(documentText);
634
+ } catch (error) {
635
+ validationErrors.push('Module metadata JSON is not valid JSON.');
636
+ return null;
637
+ }
638
+ }
639
+
640
+ private async listArchiveEntries(archiveFilePath: string): Promise<string[]> {
641
+ const output = await this.commandService.executeCommandWithArgs({
642
+ command: 'unzip',
643
+ args: ['-Z1', archiveFilePath],
644
+ });
645
+
646
+ return output
647
+ .split(/\r?\n/)
648
+ .map((entry) => entry.trim())
649
+ .filter(Boolean);
650
+ }
651
+
652
+ private async readArchiveEntry(archiveFilePath: string, entryPath: string): Promise<string> {
653
+ return this.commandService.executeCommandWithArgs({
654
+ command: 'unzip',
655
+ args: ['-p', archiveFilePath, entryPath],
656
+ });
657
+ }
658
+
659
+ private async extractArchive(archiveFilePath: string, extractDirPath: string) {
660
+ await this.commandService.executeCommandWithArgs({
661
+ command: 'unzip',
662
+ args: ['-o', archiveFilePath, '-d', extractDirPath],
663
+ });
664
+ }
665
+
666
+ private async detectConflicts(moduleName: string): Promise<string[]> {
667
+ const conflicts: string[] = [];
668
+ const solidApiTarget = this.getSolidApiModuleTargetPath(moduleName);
669
+ const solidUiTarget = this.getSolidUiModuleTargetPath(moduleName);
670
+ const metadataFilePath = await this.moduleMetadataHelperService.getModuleMetadataFilePath(moduleName);
671
+
672
+ if (await this.pathExists(solidApiTarget)) {
673
+ conflicts.push(`solid-api module already exists at ${solidApiTarget}`);
674
+ }
675
+ if (await this.pathExists(solidUiTarget)) {
676
+ conflicts.push(`solid-ui module already exists at ${solidUiTarget}`);
677
+ }
678
+ if (await this.pathExists(metadataFilePath)) {
679
+ conflicts.push(`module metadata already exists at ${metadataFilePath}`);
680
+ }
681
+
682
+ return conflicts;
683
+ }
684
+
685
+ private buildExpectedPaths(moduleName: string) {
686
+ return {
687
+ metadataPath: `solid-api/src/${moduleName}/metadata/${moduleName}-metadata.json`,
688
+ apiModulePath: `solid-api/src/${moduleName}/${moduleName}.module.ts`,
689
+ uiModulePath: `solid-ui/src/${moduleName}/${moduleName}.ui-module.ts`,
690
+ };
691
+ }
692
+
693
+ private detectArchiveRootPrefix(entries: string[]): string | null {
694
+ const normalized = entries
695
+ .map((entry) => entry.trim())
696
+ .filter(Boolean)
697
+ .filter((entry) => !this.isMacOsMetadataEntry(entry));
698
+
699
+ if (normalized.length === 0) {
700
+ return null;
701
+ }
702
+
703
+ const topLevelSegments = normalized
704
+ .map((entry) => entry.split('/')[0])
705
+ .filter(Boolean);
706
+
707
+ const uniqueTopLevelSegments = Array.from(new Set(topLevelSegments));
708
+ if (uniqueTopLevelSegments.length !== 1) {
709
+ return null;
710
+ }
711
+
712
+ const onlySegment = uniqueTopLevelSegments[0];
713
+ const hasNestedEntries = normalized.some((entry) => entry.startsWith(`${onlySegment}/`));
714
+ return hasNestedEntries ? `${onlySegment}/` : null;
715
+ }
716
+
717
+ private normalizeArchiveEntries(entries: string[], archiveRootPrefix: string | null) {
718
+ const filteredEntries = entries.filter((entry) => !this.isMacOsMetadataEntry(entry));
719
+
720
+ if (!archiveRootPrefix) {
721
+ return filteredEntries;
722
+ }
723
+
724
+ return filteredEntries
725
+ .map((entry) => entry.startsWith(archiveRootPrefix) ? entry.slice(archiveRootPrefix.length) : entry)
726
+ .filter(Boolean);
727
+ }
728
+
729
+ private toArchiveEntryPath(archiveRootPrefix: string | null, normalizedEntryPath: string) {
730
+ return archiveRootPrefix ? `${archiveRootPrefix}${normalizedEntryPath}` : normalizedEntryPath;
731
+ }
732
+
733
+ private isUnsafeArchivePath(entry: string) {
734
+ return entry.startsWith('/') || entry.includes('..') || /^[A-Za-z]:/.test(entry);
735
+ }
736
+
737
+ private isMacOsMetadataEntry(entry: string) {
738
+ const normalizedEntry = entry.trim();
739
+ if (!normalizedEntry) {
740
+ return false;
741
+ }
742
+
743
+ const segments = normalizedEntry.split('/').filter(Boolean);
744
+ return segments.some((segment) => segment === '__MACOSX' || segment.startsWith('._'));
745
+ }
746
+
747
+ private getFallbackManifest(): ModulePackageManifest {
748
+ return {
749
+ schemaVersion: ModulePackageService.SUPPORTED_SCHEMA_VERSION,
750
+ packageType: ModulePackageService.SUPPORTED_PACKAGE_TYPE,
751
+ module: { name: '', displayName: '' },
752
+ contents: {
753
+ metadataPath: '',
754
+ apiModulePath: '',
755
+ uiModulePath: '',
756
+ },
757
+ };
758
+ }
759
+
760
+ private async loadTransaction(transactionKey: string) {
761
+ this.assertSafeTransactionKey(transactionKey);
762
+ const statusFilePath = this.getStatusFilePath(transactionKey);
763
+
764
+ if (!(await this.pathExists(statusFilePath))) {
765
+ throw new NotFoundException(`Module package transaction ${transactionKey} was not found.`);
766
+ }
767
+
768
+ return JSON.parse(await fs.readFile(statusFilePath, 'utf-8')) as ModulePackageStatusFile;
769
+ }
770
+
771
+ private isResumableStatus(status: string | null | undefined) {
772
+ return [
773
+ ModulePackageStatus.import_running,
774
+ ModulePackageStatus.awaiting_restart,
775
+ ModulePackageStatus.build_running,
776
+ ModulePackageStatus.build_failed,
777
+ ModulePackageStatus.build_succeeded,
778
+ ModulePackageStatus.seed_running,
779
+ ModulePackageStatus.seed_failed,
780
+ ].includes(status as ModulePackageStatus);
781
+ }
782
+
783
+ private async createWorkingDir(transactionKey: string) {
784
+ const baseDir = path.join(
785
+ this.getModulePackageImportsRoot(),
786
+ transactionKey,
787
+ );
788
+
789
+ await fs.mkdir(baseDir, { recursive: true });
790
+ return baseDir;
791
+ }
792
+
793
+ private getSolidApiRoot() {
794
+ return process.cwd();
795
+ }
796
+
797
+ private getProjectRoot() {
798
+ return path.resolve(this.getSolidApiRoot(), '..');
799
+ }
800
+
801
+ private getSolidUiRoot() {
802
+ return path.join(this.getProjectRoot(), 'solid-ui');
803
+ }
804
+
805
+ private getModulePackageImportsRoot() {
806
+ return path.join(
807
+ this.getModulePackageRuntimeRoot(),
808
+ 'module-package-imports',
809
+ );
810
+ }
811
+
812
+ private getModulePackageExportsRoot() {
813
+ return path.join(
814
+ this.getModulePackageRuntimeRoot(),
815
+ 'module-package-exports',
816
+ );
817
+ }
818
+
819
+ private getModulePackageRuntimeRoot() {
820
+ const configuredRuntimeRoot = process.env.SOLIDX_MODULE_PACKAGE_RUNTIME_DIR?.trim();
821
+
822
+ if (configuredRuntimeRoot) {
823
+ return path.resolve(configuredRuntimeRoot);
824
+ }
825
+
826
+ return path.join(
827
+ this.getProjectRoot(),
828
+ '.solidx-runtime',
829
+ );
830
+ }
831
+
832
+ private getSolidApiModuleTargetPath(moduleName: string) {
833
+ return path.join(this.getSolidApiRoot(), 'src', moduleName);
834
+ }
835
+
836
+ private getSolidUiModuleTargetPath(moduleName: string) {
837
+ return path.join(this.getSolidUiRoot(), 'src', moduleName);
838
+ }
839
+
840
+ private async pathExists(targetPath: string) {
841
+ try {
842
+ await fs.access(targetPath);
843
+ return true;
844
+ } catch (error) {
845
+ return false;
846
+ }
847
+ }
848
+
849
+ private async assertPathExists(targetPath: string, label: string) {
850
+ if (!(await this.pathExists(targetPath))) {
851
+ throw new BadRequestException(`Expected ${label} at ${targetPath}, but it was not found in the extracted archive.`);
852
+ }
853
+ }
854
+
855
+ private async readDirSafe(targetPath: string) {
856
+ try {
857
+ return await fs.readdir(targetPath, { withFileTypes: true });
858
+ } catch (error) {
859
+ return [];
860
+ }
861
+ }
862
+
863
+ private assertSafeTransactionKey(transactionKey: string) {
864
+ if (!/^[a-zA-Z0-9-]+$/.test(transactionKey)) {
865
+ throw new BadRequestException('Invalid module package transaction key.');
866
+ }
867
+ }
868
+
869
+ private getStatusFilePath(transactionKey: string) {
870
+ this.assertSafeTransactionKey(transactionKey);
871
+ return path.join(this.getModulePackageImportsRoot(), transactionKey, 'status.json');
872
+ }
873
+
874
+ private getActiveTransactionFilePath() {
875
+ return path.join(this.getModulePackageImportsRoot(), 'active-transaction.json');
876
+ }
877
+
878
+ private async writeStatusFile(transactionKey: string, payload: ModulePackageStatusFile) {
879
+ payload.updatedAt = new Date().toISOString();
880
+ const statusFilePath = this.getStatusFilePath(transactionKey);
881
+ await fs.mkdir(path.dirname(statusFilePath), { recursive: true });
882
+ await fs.writeFile(statusFilePath, JSON.stringify(payload, null, 2));
883
+ }
884
+
885
+ private async markActiveTransaction(transactionKey: string) {
886
+ const activeTransactionFilePath = this.getActiveTransactionFilePath();
887
+ await fs.mkdir(path.dirname(activeTransactionFilePath), { recursive: true });
888
+ const payload: ModulePackageActiveTransactionFile = {
889
+ transactionKey,
890
+ updatedAt: new Date().toISOString(),
891
+ };
892
+ await fs.writeFile(activeTransactionFilePath, JSON.stringify(payload, null, 2));
893
+ }
894
+
895
+ private async clearActiveTransactionIfMatches(transactionKey: string) {
896
+ const activeTransaction = await this.loadActiveTransactionPointer();
897
+ if (!activeTransaction || activeTransaction.transactionKey !== transactionKey) {
898
+ return;
899
+ }
900
+
901
+ await fs.rm(this.getActiveTransactionFilePath(), { force: true });
902
+ }
903
+
904
+ private async loadActiveTransactionPointer(): Promise<ModulePackageActiveTransactionFile | null> {
905
+ try {
906
+ return JSON.parse(await fs.readFile(this.getActiveTransactionFilePath(), 'utf-8')) as ModulePackageActiveTransactionFile;
907
+ } catch (error) {
908
+ return null;
909
+ }
910
+ }
911
+
912
+ private async syncActiveTransactionPointer(transaction: ModulePackageStatusFile) {
913
+ if (this.isResumableStatus(transaction.status)) {
914
+ await this.markActiveTransaction(transaction.transactionKey);
915
+ return;
916
+ }
917
+
918
+ await this.clearActiveTransactionIfMatches(transaction.transactionKey);
919
+ }
920
+
921
+ private async collectRuntimeCleanupSnapshot() {
922
+ const importEntries = await this.readDirSafe(this.getModulePackageImportsRoot());
923
+ const exportEntries = await this.readDirSafe(this.getModulePackageExportsRoot());
924
+
925
+ const importTransactionEntries = importEntries.filter(
926
+ (entry) => entry.isDirectory() && /^[a-zA-Z0-9-]+$/.test(entry.name),
927
+ );
928
+ const exportTransactionEntries = exportEntries.filter(
929
+ (entry) => entry.isDirectory() && /^[a-zA-Z0-9-]+$/.test(entry.name),
930
+ );
931
+
932
+ const importLooseEntries = importEntries.filter(
933
+ (entry) => !(entry.isDirectory() && /^[a-zA-Z0-9-]+$/.test(entry.name)),
934
+ );
935
+ const exportLooseEntries = exportEntries.filter(
936
+ (entry) => !(entry.isDirectory() && /^[a-zA-Z0-9-]+$/.test(entry.name)),
937
+ );
938
+
939
+ return {
940
+ removedImportTransactions: importTransactionEntries.length,
941
+ removedExportTransactions: exportTransactionEntries.length,
942
+ removedImportLooseEntries: importLooseEntries.length,
943
+ removedExportLooseEntries: exportLooseEntries.length,
944
+ clearedActiveTransactionPointer: importEntries.some((entry) => entry.name === 'active-transaction.json'),
945
+ };
946
+ }
947
+
948
+ private getCompletedTransactionRetentionMs() {
949
+ const retentionDays = Number(process.env.SOLIDX_MODULE_PACKAGE_RETENTION_DAYS ?? 7);
950
+ const safeDays = Number.isFinite(retentionDays) && retentionDays > 0 ? retentionDays : 7;
951
+ return safeDays * 24 * 60 * 60 * 1000;
952
+ }
953
+
954
+ private getPendingTransactionRetentionMs() {
955
+ const retentionHours = Number(process.env.SOLIDX_MODULE_PACKAGE_PENDING_RETENTION_HOURS ?? 24);
956
+ const safeHours = Number.isFinite(retentionHours) && retentionHours > 0 ? retentionHours : 24;
957
+ return safeHours * 60 * 60 * 1000;
958
+ }
959
+
960
+ private async cleanupModulePackageTransactions() {
961
+ const importsRoot = this.getModulePackageImportsRoot();
962
+ const activePointer = await this.loadActiveTransactionPointer();
963
+ const now = Date.now();
964
+
965
+ try {
966
+ await fs.mkdir(importsRoot, { recursive: true });
967
+ const entries = await fs.readdir(importsRoot, { withFileTypes: true });
968
+
969
+ for (const entry of entries) {
970
+ if (!entry.isDirectory()) {
971
+ continue;
972
+ }
973
+
974
+ const transactionKey = entry.name;
975
+ if (!/^[a-zA-Z0-9-]+$/.test(transactionKey)) {
976
+ continue;
977
+ }
978
+
979
+ const statusFilePath = this.getStatusFilePath(transactionKey);
980
+ let transaction: ModulePackageStatusFile | null = null;
981
+
982
+ try {
983
+ transaction = JSON.parse(await fs.readFile(statusFilePath, 'utf-8')) as ModulePackageStatusFile;
984
+ } catch (error) {
985
+ continue;
986
+ }
987
+
988
+ const updatedAtMs = transaction.updatedAt ? new Date(transaction.updatedAt).getTime() : 0;
989
+ const ageMs = updatedAtMs > 0 ? now - updatedAtMs : Number.MAX_SAFE_INTEGER;
990
+ const isResumable = this.isResumableStatus(transaction.status);
991
+ const maxAgeMs = isResumable
992
+ ? this.getPendingTransactionRetentionMs()
993
+ : this.getCompletedTransactionRetentionMs();
994
+ const shouldDelete = ageMs > maxAgeMs;
995
+
996
+ if (!shouldDelete) {
997
+ continue;
998
+ }
999
+
1000
+ await fs.rm(path.join(importsRoot, transactionKey), { recursive: true, force: true });
1001
+
1002
+ if (activePointer?.transactionKey === transactionKey) {
1003
+ await fs.rm(this.getActiveTransactionFilePath(), { force: true });
1004
+ }
1005
+ }
1006
+ } catch (error) {
1007
+ this.logger.warn(`Failed to clean up module package transactions: ${String(error)}`);
1008
+ }
1009
+ }
1010
+
1011
+ private async resolveLatestResumableTransaction(): Promise<ModulePackageStatusFile | null> {
1012
+ const activePointer = await this.loadActiveTransactionPointer();
1013
+ if (activePointer?.transactionKey) {
1014
+ try {
1015
+ const activeTransaction = await this.loadTransaction(activePointer.transactionKey);
1016
+ if (this.isResumableStatus(activeTransaction.status)) {
1017
+ return activeTransaction;
1018
+ }
1019
+
1020
+ await this.clearActiveTransactionIfMatches(activePointer.transactionKey);
1021
+ } catch (error) {
1022
+ await this.clearActiveTransactionIfMatches(activePointer.transactionKey);
1023
+ }
1024
+ }
1025
+
1026
+ const importsRoot = this.getModulePackageImportsRoot();
1027
+ const candidates: ModulePackageStatusFile[] = [];
1028
+
1029
+ try {
1030
+ const entries = await fs.readdir(importsRoot, { withFileTypes: true });
1031
+
1032
+ for (const entry of entries) {
1033
+ if (!entry.isDirectory()) {
1034
+ continue;
1035
+ }
1036
+
1037
+ const transactionKey = entry.name;
1038
+ if (!/^[a-zA-Z0-9-]+$/.test(transactionKey)) {
1039
+ continue;
1040
+ }
1041
+
1042
+ try {
1043
+ const transaction = await this.loadTransaction(transactionKey);
1044
+ if (this.isResumableStatus(transaction.status)) {
1045
+ candidates.push(transaction);
1046
+ }
1047
+ } catch (error) {
1048
+ continue;
1049
+ }
1050
+ }
1051
+ } catch (error) {
1052
+ return null;
1053
+ }
1054
+
1055
+ candidates.sort((left, right) => {
1056
+ const leftUpdatedAt = new Date(left.updatedAt ?? left.createdAt ?? 0).getTime();
1057
+ const rightUpdatedAt = new Date(right.updatedAt ?? right.createdAt ?? 0).getTime();
1058
+ return rightUpdatedAt - leftUpdatedAt;
1059
+ });
1060
+
1061
+ const latest = candidates[0] ?? null;
1062
+ if (latest) {
1063
+ await this.markActiveTransaction(latest.transactionKey);
1064
+ }
1065
+ return latest;
1066
+ }
1067
+
1068
+ private toStatusResponse(transaction: ModulePackageStatusFile) {
1069
+ return {
1070
+ transactionKey: transaction.transactionKey,
1071
+ status: transaction.status,
1072
+ currentStep: transaction.currentStep,
1073
+ moduleName: transaction.moduleName,
1074
+ moduleDisplayName: transaction.moduleDisplayName,
1075
+ archiveFileName: transaction.archiveFileName,
1076
+ manifest: transaction.manifest,
1077
+ preview: transaction.preview,
1078
+ validation: transaction.validation,
1079
+ conflicts: transaction.conflicts,
1080
+ outputs: transaction.outputs,
1081
+ errorMessage: transaction.errorMessage,
1082
+ createdAt: transaction.createdAt,
1083
+ updatedAt: transaction.updatedAt,
1084
+ };
1085
+ }
1086
+
1087
+ private async buildExportManifest(moduleName: string, metadataDocument: any, stagingDir: string): Promise<ModulePackageManifest> {
1088
+ const expectedPaths = this.buildExpectedPaths(moduleName);
1089
+ const checksums = await this.computeDirectoryChecksums(stagingDir, ['manifest.json']);
1090
+
1091
+ return {
1092
+ schemaVersion: ModulePackageService.SUPPORTED_SCHEMA_VERSION,
1093
+ packageType: ModulePackageService.SUPPORTED_PACKAGE_TYPE,
1094
+ exportedAt: new Date().toISOString(),
1095
+ generatedBy: {
1096
+ name: '@solidxai/core',
1097
+ },
1098
+ module: {
1099
+ name: moduleName,
1100
+ displayName: metadataDocument?.moduleMetadata?.displayName,
1101
+ description: metadataDocument?.moduleMetadata?.description,
1102
+ },
1103
+ contents: expectedPaths,
1104
+ postImport: {
1105
+ recommendedSteps: ['restart', 'build', 'seed'],
1106
+ },
1107
+ checksums,
1108
+ };
1109
+ }
1110
+
1111
+ private async computeDirectoryChecksums(rootDir: string, excludedRelativePaths: string[] = []): Promise<Record<string, string>> {
1112
+ const excludedPaths = new Set(excludedRelativePaths.map((entry) => entry.replace(/\\/g, '/')));
1113
+ const filePaths = await this.collectFilePathsRecursively(rootDir);
1114
+ const checksums: Record<string, string> = {};
1115
+
1116
+ for (const filePath of filePaths) {
1117
+ const relativePath = path.relative(rootDir, filePath).replace(/\\/g, '/');
1118
+ if (excludedPaths.has(relativePath)) {
1119
+ continue;
1120
+ }
1121
+
1122
+ checksums[relativePath] = await this.computeSha256(filePath);
1123
+ }
1124
+
1125
+ return checksums;
1126
+ }
1127
+
1128
+ private async collectFilePathsRecursively(rootDir: string): Promise<string[]> {
1129
+ const entries = await fs.readdir(rootDir, { withFileTypes: true });
1130
+ const nestedPaths = await Promise.all(entries.map(async (entry) => {
1131
+ const entryPath = path.join(rootDir, entry.name);
1132
+ if (entry.isDirectory()) {
1133
+ return this.collectFilePathsRecursively(entryPath);
1134
+ }
1135
+
1136
+ return [entryPath];
1137
+ }));
1138
+
1139
+ return nestedPaths.flat();
1140
+ }
1141
+
1142
+ private async createArchiveFromDirectory(sourceDir: string, archiveFilePath: string): Promise<void> {
1143
+ await fs.mkdir(path.dirname(archiveFilePath), { recursive: true });
1144
+
1145
+ await new Promise<void>((resolve, reject) => {
1146
+ const output = createWriteStream(archiveFilePath);
1147
+ const archive = archiver('zip', {
1148
+ zlib: { level: 9 },
1149
+ });
1150
+
1151
+ const rejectOnce = (error: Error) => {
1152
+ output.destroy();
1153
+ reject(error);
1154
+ };
1155
+
1156
+ output.on('close', () => resolve());
1157
+ output.on('error', rejectOnce);
1158
+ archive.on('warning', rejectOnce);
1159
+ archive.on('error', rejectOnce);
1160
+
1161
+ archive.pipe(output);
1162
+ archive.directory(sourceDir, false);
1163
+ void archive.finalize();
1164
+ });
1165
+ }
1166
+
1167
+ private async computeSha256(filePath: string): Promise<string> {
1168
+ const fileBuffer = await fs.readFile(filePath);
1169
+ return createHash('sha256').update(fileBuffer).digest('hex');
1170
+ }
1171
+ }