@abtnode/core 1.17.8-beta-20260109-075740-5f484e08 → 1.17.8-beta-20260113-015027-32a1cec4

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 (65) hide show
  1. package/lib/api/team/access-key-manager.js +104 -0
  2. package/lib/api/team/invitation-manager.js +461 -0
  3. package/lib/api/team/notification-manager.js +189 -0
  4. package/lib/api/team/oauth-manager.js +60 -0
  5. package/lib/api/team/org-crud-manager.js +202 -0
  6. package/lib/api/team/org-manager.js +56 -0
  7. package/lib/api/team/org-member-manager.js +403 -0
  8. package/lib/api/team/org-query-manager.js +126 -0
  9. package/lib/api/team/org-resource-manager.js +186 -0
  10. package/lib/api/team/passport-manager.js +670 -0
  11. package/lib/api/team/rbac-manager.js +335 -0
  12. package/lib/api/team/session-manager.js +540 -0
  13. package/lib/api/team/store-manager.js +198 -0
  14. package/lib/api/team/tag-manager.js +230 -0
  15. package/lib/api/team/user-auth-manager.js +132 -0
  16. package/lib/api/team/user-manager.js +78 -0
  17. package/lib/api/team/user-query-manager.js +299 -0
  18. package/lib/api/team/user-social-manager.js +354 -0
  19. package/lib/api/team/user-update-manager.js +224 -0
  20. package/lib/api/team/verify-code-manager.js +161 -0
  21. package/lib/api/team.js +439 -3287
  22. package/lib/blocklet/manager/disk/auth-manager.js +68 -0
  23. package/lib/blocklet/manager/disk/backup-manager.js +288 -0
  24. package/lib/blocklet/manager/disk/cleanup-manager.js +157 -0
  25. package/lib/blocklet/manager/disk/component-manager.js +83 -0
  26. package/lib/blocklet/manager/disk/config-manager.js +191 -0
  27. package/lib/blocklet/manager/disk/controller-manager.js +64 -0
  28. package/lib/blocklet/manager/disk/delete-reset-manager.js +328 -0
  29. package/lib/blocklet/manager/disk/download-manager.js +96 -0
  30. package/lib/blocklet/manager/disk/env-config-manager.js +311 -0
  31. package/lib/blocklet/manager/disk/federated-manager.js +651 -0
  32. package/lib/blocklet/manager/disk/hook-manager.js +124 -0
  33. package/lib/blocklet/manager/disk/install-component-manager.js +95 -0
  34. package/lib/blocklet/manager/disk/install-core-manager.js +448 -0
  35. package/lib/blocklet/manager/disk/install-download-manager.js +313 -0
  36. package/lib/blocklet/manager/disk/install-manager.js +36 -0
  37. package/lib/blocklet/manager/disk/install-upgrade-manager.js +340 -0
  38. package/lib/blocklet/manager/disk/job-manager.js +467 -0
  39. package/lib/blocklet/manager/disk/lifecycle-manager.js +26 -0
  40. package/lib/blocklet/manager/disk/notification-manager.js +343 -0
  41. package/lib/blocklet/manager/disk/query-manager.js +562 -0
  42. package/lib/blocklet/manager/disk/settings-manager.js +507 -0
  43. package/lib/blocklet/manager/disk/start-manager.js +611 -0
  44. package/lib/blocklet/manager/disk/stop-restart-manager.js +292 -0
  45. package/lib/blocklet/manager/disk/update-manager.js +153 -0
  46. package/lib/blocklet/manager/disk.js +669 -5796
  47. package/lib/blocklet/manager/helper/blue-green-start-blocklet.js +5 -0
  48. package/lib/blocklet/manager/lock.js +18 -0
  49. package/lib/event/index.js +28 -24
  50. package/lib/util/blocklet/app-utils.js +192 -0
  51. package/lib/util/blocklet/blocklet-loader.js +258 -0
  52. package/lib/util/blocklet/config-manager.js +232 -0
  53. package/lib/util/blocklet/did-document.js +240 -0
  54. package/lib/util/blocklet/environment.js +555 -0
  55. package/lib/util/blocklet/health-check.js +449 -0
  56. package/lib/util/blocklet/install-utils.js +365 -0
  57. package/lib/util/blocklet/logo.js +57 -0
  58. package/lib/util/blocklet/meta-utils.js +269 -0
  59. package/lib/util/blocklet/port-manager.js +141 -0
  60. package/lib/util/blocklet/process-manager.js +504 -0
  61. package/lib/util/blocklet/runtime-info.js +105 -0
  62. package/lib/util/blocklet/validation.js +418 -0
  63. package/lib/util/blocklet.js +98 -3066
  64. package/lib/util/wallet-app-notification.js +40 -0
  65. package/package.json +22 -22
@@ -0,0 +1,365 @@
1
+ /* eslint-disable no-await-in-loop */
2
+ /**
3
+ * Install Utilities Module
4
+ *
5
+ * Functions for blocklet installation, verification, and bundle management
6
+ * Extracted from blocklet.js for better modularity
7
+ */
8
+
9
+ const fs = require('fs-extra');
10
+ const path = require('node:path');
11
+ const tar = require('tar');
12
+ const get = require('lodash/get');
13
+ const streamToPromise = require('stream-to-promise');
14
+ const { Throttle } = require('stream-throttle');
15
+ const ssri = require('ssri');
16
+ const diff = require('deep-diff');
17
+
18
+ const logger = require('@abtnode/logger')('@abtnode/core:util:blocklet:install-utils');
19
+ const formatBackSlash = require('@abtnode/util/lib/format-back-slash');
20
+ const hashFiles = require('@abtnode/util/lib/hash-files');
21
+ const { BLOCKLET_INSTALL_TYPE } = require('@abtnode/constant');
22
+
23
+ const { BlockletStatus, BlockletSource, fromBlockletStatus } = require('@blocklet/constant');
24
+ const { forEachBlockletSync } = require('@blocklet/meta/lib/util');
25
+
26
+ /**
27
+ * Get version scope string with hash for unique directory naming
28
+ * @param {object} meta - Blocklet meta
29
+ * @returns {string} Version scope string
30
+ */
31
+ const getVersionScope = (meta) => {
32
+ if (meta.dist?.integrity) {
33
+ const safeHash = meta.dist.integrity
34
+ .replace('sha512-', '')
35
+ .slice(0, 8)
36
+ .replace(/[^a-zA-Z0-9]/g, '');
37
+ return `${meta.version}-${safeHash}`;
38
+ }
39
+
40
+ return meta.version;
41
+ };
42
+
43
+ /**
44
+ * Expand tarball to destination directory
45
+ * @param {object} options - Options
46
+ * @param {string} options.source - Source tarball path
47
+ * @param {string} options.dest - Destination directory
48
+ * @param {number} options.strip - Number of leading directories to strip (default: 1)
49
+ * @returns {Promise<string>} Destination path
50
+ */
51
+ const expandTarball = async ({ source, dest, strip = 1 }) => {
52
+ logger.info('expand blocklet', { source, dest });
53
+
54
+ if (!fs.existsSync(source)) {
55
+ throw new Error(`Blocklet tarball ${source} does not exist`);
56
+ }
57
+
58
+ fs.mkdirSync(dest, { recursive: true });
59
+
60
+ await streamToPromise(
61
+ fs
62
+ .createReadStream(source)
63
+ .pipe(new Throttle({ rate: 1024 * 1024 * 20 })) // 20MB
64
+ .pipe(tar.x({ C: dest, strip }))
65
+ );
66
+
67
+ return dest;
68
+ };
69
+
70
+ /**
71
+ * Verify file integrity using SSRI
72
+ * @param {object} options - Options
73
+ * @param {string} options.file - File path
74
+ * @param {string} options.integrity - Expected integrity hash
75
+ * @returns {Promise<boolean>} True if valid
76
+ */
77
+ const verifyIntegrity = async ({ file, integrity: expected }) => {
78
+ const stream = fs.createReadStream(file);
79
+ const result = await ssri.checkStream(stream, ssri.parse(expected));
80
+ logger.debug('verify integrity result', { result });
81
+ stream.destroy();
82
+ return true;
83
+ };
84
+
85
+ /**
86
+ * Get all app directories from install directory
87
+ * @param {string} installDir - Installation directory
88
+ * @returns {Promise<Array<{ key: string, dir: string }>>} App directories
89
+ */
90
+ const getAppDirs = async (installDir) => {
91
+ const appDirs = [];
92
+
93
+ const getNextLevel = (level, name) => {
94
+ if (level === 'root') {
95
+ if (name.startsWith('@')) {
96
+ return 'scope';
97
+ }
98
+ return 'name';
99
+ }
100
+ if (level === 'scope') {
101
+ return 'name';
102
+ }
103
+ if (level === 'name') {
104
+ return 'version';
105
+ }
106
+ throw new Error(`Invalid level ${level}`);
107
+ };
108
+
109
+ const fillAppDirs = async (dir, level = 'root') => {
110
+ if (level === 'version') {
111
+ appDirs.push({
112
+ key: formatBackSlash(path.relative(installDir, dir)),
113
+ dir,
114
+ });
115
+
116
+ return;
117
+ }
118
+
119
+ const nextDirs = [];
120
+ for (const x of await fs.promises.readdir(dir)) {
121
+ if (!fs.lstatSync(path.join(dir, x)).isDirectory()) {
122
+ logger.error('pruneBlockletBundle: invalid file in bundle storage', { dir, file: x });
123
+ // eslint-disable-next-line no-continue
124
+ continue;
125
+ }
126
+ nextDirs.push(x);
127
+ }
128
+
129
+ for (const x of nextDirs) {
130
+ await fillAppDirs(path.join(dir, x), getNextLevel(level, x));
131
+ }
132
+ };
133
+
134
+ await fillAppDirs(installDir, 'root');
135
+
136
+ return appDirs;
137
+ };
138
+
139
+ /**
140
+ * Prune unused blocklet bundles from install directory
141
+ * @param {object} options - Options
142
+ * @param {Array} options.blocklets - Current blocklets
143
+ * @param {string} options.installDir - Installation directory
144
+ * @param {Array} options.blockletSettings - Blocklet settings with children
145
+ */
146
+ const pruneBlockletBundle = async ({ blocklets, installDir, blockletSettings }) => {
147
+ for (const blocklet of blocklets) {
148
+ if (
149
+ [
150
+ BlockletStatus.waiting,
151
+ BlockletStatus.installing,
152
+ BlockletStatus.upgrading,
153
+ BlockletStatus.downloading,
154
+ ].includes(blocklet.status)
155
+ ) {
156
+ logger.info('There are blocklet activities in progress, abort pruning', {
157
+ bundleName: blocklet.meta.bundleName,
158
+ status: fromBlockletStatus(blocklet.status),
159
+ });
160
+ return;
161
+ }
162
+ }
163
+
164
+ // blockletMap: { <[scope/]name/version>: true }
165
+ const blockletMap = {};
166
+ for (const blocklet of blocklets) {
167
+ forEachBlockletSync(blocklet, (component) => {
168
+ blockletMap[`${component.meta.bundleName}/${component.meta.version}`] = true;
169
+ blockletMap[`${component.meta.bundleName}/${getVersionScope(component.meta)}`] = true;
170
+ });
171
+ }
172
+ for (const setting of blockletSettings) {
173
+ for (const child of setting.children || []) {
174
+ if (child.status !== BlockletStatus.deleted) {
175
+ forEachBlockletSync(child, (component) => {
176
+ blockletMap[`${component.meta.bundleName}/${component.meta.version}`] = true;
177
+ blockletMap[`${component.meta.bundleName}/${getVersionScope(component.meta)}`] = true;
178
+ });
179
+ }
180
+ }
181
+ }
182
+
183
+ // fill appDirs
184
+ let appDirs = [];
185
+ try {
186
+ appDirs = await getAppDirs(installDir);
187
+ } catch (error) {
188
+ logger.error('fill app dirs failed', { error });
189
+ }
190
+
191
+ const ensureBundleDirRemoved = async (dir) => {
192
+ const relativeDir = path.relative(installDir, dir);
193
+ const arr = relativeDir.split(path.sep).filter(Boolean);
194
+ const { length } = arr;
195
+ const bundleName = arr[length - 2];
196
+ const scopeName = length > 2 ? arr[length - 3] : '';
197
+ const bundleDir = path.join(installDir, scopeName, bundleName);
198
+ const isDirEmpty = (await fs.promises.readdir(bundleDir)).length === 0;
199
+ if (isDirEmpty) {
200
+ logger.info('Remove bundle folder', { bundleDir });
201
+ await fs.remove(bundleDir);
202
+ }
203
+ if (scopeName) {
204
+ const scopeDir = path.join(installDir, scopeName);
205
+ const isScopeEmpty = (await fs.promises.readdir(scopeDir)).length === 0;
206
+ if (isScopeEmpty) {
207
+ logger.info('Remove scope folder', { scopeDir });
208
+ await fs.remove(scopeDir);
209
+ }
210
+ }
211
+ };
212
+
213
+ // remove trash
214
+ for (const app of appDirs) {
215
+ if (!blockletMap[app.key]) {
216
+ logger.info('Remove app folder', { dir: app.dir });
217
+ await fs.remove(app.dir);
218
+ await ensureBundleDirRemoved(app.dir);
219
+ }
220
+ }
221
+
222
+ logger.info('Blocklet source folder has been pruned');
223
+ };
224
+
225
+ /**
226
+ * Get install type from params
227
+ * @param {object} params - Install params
228
+ * @returns {string} BLOCKLET_INSTALL_TYPE
229
+ */
230
+ const getTypeFromInstallParams = (params) => {
231
+ if (params.type) {
232
+ if (!Object.values(BLOCKLET_INSTALL_TYPE).includes(params.type)) {
233
+ throw new Error(`Can only install blocklet from ${Object.values(BLOCKLET_INSTALL_TYPE).join('/')}`);
234
+ }
235
+ return params.type;
236
+ }
237
+
238
+ if (params.url) {
239
+ return BLOCKLET_INSTALL_TYPE.URL;
240
+ }
241
+
242
+ if (params.file) {
243
+ throw new Error('install from upload is not supported');
244
+ }
245
+
246
+ if (params.did) {
247
+ return BLOCKLET_INSTALL_TYPE.STORE;
248
+ }
249
+
250
+ if (params.title && params.description) {
251
+ return BLOCKLET_INSTALL_TYPE.CREATE;
252
+ }
253
+
254
+ throw new Error(`Can only install blocklet from ${Object.values(BLOCKLET_INSTALL_TYPE).join('/')}`);
255
+ };
256
+
257
+ /**
258
+ * Get diff files between input files and source directory
259
+ * @param {Array} inputFiles - Input files with hash
260
+ * @param {string} sourceDir - Source directory
261
+ * @returns {Promise<{ addSet: Array, changeSet: Array, deleteSet: Array }>}
262
+ */
263
+ const getDiffFiles = async (inputFiles, sourceDir) => {
264
+ if (!fs.existsSync(sourceDir)) {
265
+ throw new Error(`${sourceDir} does not exist`);
266
+ }
267
+
268
+ const files = inputFiles.reduce((obj, item) => {
269
+ obj[item.file] = item.hash;
270
+ return obj;
271
+ }, {});
272
+
273
+ const { files: sourceFiles } = await hashFiles(sourceDir, {
274
+ filter: (x) => x.indexOf('node_modules') === -1,
275
+ concurrentHash: 1,
276
+ });
277
+
278
+ const addSet = [];
279
+ const changeSet = [];
280
+ const deleteSet = [];
281
+
282
+ const diffFiles = diff(sourceFiles, files);
283
+ if (diffFiles) {
284
+ diffFiles.forEach((item) => {
285
+ if (item.kind === 'D') {
286
+ deleteSet.push(item.path[0]);
287
+ }
288
+ if (item.kind === 'E') {
289
+ changeSet.push(item.path[0]);
290
+ }
291
+ if (item.kind === 'N') {
292
+ addSet.push(item.path[0]);
293
+ }
294
+ });
295
+ }
296
+
297
+ return {
298
+ addSet,
299
+ changeSet,
300
+ deleteSet,
301
+ };
302
+ };
303
+
304
+ const checkCompatibleOnce = {};
305
+
306
+ /**
307
+ * Check compatibility with old blocklets that use blocklet.yml
308
+ * @param {string} dir - Directory to check
309
+ * @returns {boolean}
310
+ */
311
+ const compatibleWithOldBlocklets = (dir) => {
312
+ if (checkCompatibleOnce[dir] !== undefined) {
313
+ return checkCompatibleOnce[dir];
314
+ }
315
+
316
+ checkCompatibleOnce[dir] = !!fs.existsSync(path.join(dir, 'blocklet.yml'));
317
+
318
+ return checkCompatibleOnce[dir];
319
+ };
320
+
321
+ /**
322
+ * Get bundle directory for blocklet
323
+ * @param {string} installDir - Installation directory
324
+ * @param {object} meta - Blocklet meta
325
+ * @returns {string} Bundle directory path
326
+ */
327
+ const getBundleDir = (installDir, meta) => {
328
+ const oldDir = path.join(installDir, meta.bundleName || meta.name, meta.version);
329
+ if (compatibleWithOldBlocklets(oldDir)) {
330
+ return oldDir;
331
+ }
332
+
333
+ return path.join(installDir, meta.bundleName || meta.name, getVersionScope(meta));
334
+ };
335
+
336
+ /**
337
+ * Check if blocklet needs to be downloaded
338
+ * @param {object} blocklet - New blocklet
339
+ * @param {object} oldBlocklet - Existing blocklet
340
+ * @returns {boolean}
341
+ */
342
+ const needBlockletDownload = (blocklet, oldBlocklet) => {
343
+ if ([BlockletSource.upload, BlockletSource.local, BlockletSource.custom].includes(blocklet.source)) {
344
+ return false;
345
+ }
346
+
347
+ if (!get(oldBlocklet, 'meta.dist.integrity')) {
348
+ return true;
349
+ }
350
+
351
+ return get(oldBlocklet, 'meta.dist.integrity') !== get(blocklet, 'meta.dist.integrity');
352
+ };
353
+
354
+ module.exports = {
355
+ getVersionScope,
356
+ expandTarball,
357
+ verifyIntegrity,
358
+ getAppDirs,
359
+ pruneBlockletBundle,
360
+ getTypeFromInstallParams,
361
+ getDiffFiles,
362
+ compatibleWithOldBlocklets,
363
+ getBundleDir,
364
+ needBlockletDownload,
365
+ };
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Logo Module
3
+ *
4
+ * Functions for blocklet logo generation and management
5
+ */
6
+
7
+ const fs = require('fs-extra');
8
+ const path = require('node:path');
9
+
10
+ const { isEthereumDid } = require('@arcblock/did');
11
+ const { toSvg: createDidLogo } = require('@arcblock/did-motif');
12
+ const { createBlockiesSvg } = require('@blocklet/meta/lib/blockies');
13
+ const { BlockletSource } = require('@blocklet/constant');
14
+
15
+ const { getBundleDir } = require('./install-utils');
16
+
17
+ /**
18
+ * Update blocklet fallback logo with DID-based SVG
19
+ * @param {object} blocklet - Blocklet object with env.dataDir and meta.did
20
+ */
21
+ const updateBlockletFallbackLogo = async (blocklet) => {
22
+ if (isEthereumDid(blocklet.meta.did)) {
23
+ await fs.writeFile(path.join(blocklet.env.dataDir, 'logo.svg'), createBlockiesSvg(blocklet.meta.did));
24
+ } else {
25
+ await fs.writeFile(path.join(blocklet.env.dataDir, 'logo.svg'), createDidLogo(blocklet.meta.did));
26
+ }
27
+ };
28
+
29
+ /**
30
+ * Ensure app logo exists by copying from component if needed
31
+ * @param {object} blocklet - Blocklet object
32
+ * @param {string} blockletsDir - Blocklets directory path
33
+ */
34
+ const ensureAppLogo = async (blocklet, blockletsDir) => {
35
+ if (!blocklet) {
36
+ return;
37
+ }
38
+
39
+ if (
40
+ blocklet.source === BlockletSource.custom &&
41
+ (blocklet.children || [])[0]?.meta?.logo &&
42
+ blocklet.children[0].env.appDir
43
+ ) {
44
+ const fileName = blocklet.children[0].meta.logo;
45
+ const src = path.join(blocklet.children[0].env.appDir, fileName);
46
+ const dist = path.join(getBundleDir(blockletsDir, blocklet.meta), fileName);
47
+
48
+ if (fs.existsSync(src)) {
49
+ await fs.copy(src, dist);
50
+ }
51
+ }
52
+ };
53
+
54
+ module.exports = {
55
+ updateBlockletFallbackLogo,
56
+ ensureAppLogo,
57
+ };
@@ -0,0 +1,269 @@
1
+ /**
2
+ * Meta Utils Module
3
+ *
4
+ * Functions for managing blocklet metadata, themes, and status utilities
5
+ * Extracted from blocklet.js for better modularity
6
+ */
7
+
8
+ const cloneDeep = require('lodash/cloneDeep');
9
+ const mergeWith = require('lodash/mergeWith');
10
+ const semver = require('semver');
11
+
12
+ const { BLOCKLET_THEME_LIGHT, BLOCKLET_THEME_DARK } = require('@blocklet/theme');
13
+ const { getComponentConfig } = require('@blocklet/resolver');
14
+ const { BlockletStatus, BlockletGroup, BlockletSource } = require('@blocklet/constant');
15
+ const {
16
+ forEachChildSync,
17
+ findWebInterface,
18
+ forEachComponentV2Sync,
19
+ isInProgress,
20
+ isRunning,
21
+ } = require('@blocklet/meta/lib/util');
22
+
23
+ /**
24
+ * Format blocklet theme configuration
25
+ * @param {object} rawTheme - Raw theme configuration
26
+ * @returns {object} Formatted theme configuration
27
+ */
28
+ const formatBlockletTheme = (rawTheme) => {
29
+ let themeConfig = {};
30
+
31
+ if (rawTheme) {
32
+ if (Array.isArray(rawTheme.concepts) && rawTheme.currentConceptId) {
33
+ const concept = rawTheme.concepts.find((x) => x.id === rawTheme.currentConceptId);
34
+ themeConfig = {
35
+ ...concept.themeConfig,
36
+ prefer: concept.prefer,
37
+ name: concept.name,
38
+ };
39
+ } else {
40
+ // 兼容旧数据
41
+ themeConfig = {
42
+ light: rawTheme.light || {},
43
+ dark: rawTheme.dark || {},
44
+ common: rawTheme.common || {},
45
+ prefer: rawTheme.prefer || 'system',
46
+ name: rawTheme.name || 'Default',
47
+ };
48
+ }
49
+ }
50
+
51
+ const result = mergeWith(
52
+ // 至少提供 palette 色板值(客户端会使用)
53
+ cloneDeep({
54
+ light: { palette: BLOCKLET_THEME_LIGHT.palette },
55
+ dark: { palette: BLOCKLET_THEME_DARK.palette },
56
+ prefer: 'system',
57
+ }),
58
+ themeConfig,
59
+ // 数组值直接替换
60
+ (_, srcValue) => {
61
+ if (Array.isArray(srcValue)) {
62
+ return srcValue;
63
+ }
64
+ return undefined;
65
+ }
66
+ );
67
+
68
+ // 保留原始数据,用于 settings 保存
69
+ Object.defineProperty(result, 'raw', {
70
+ value: rawTheme,
71
+ enumerable: false,
72
+ writable: false,
73
+ });
74
+
75
+ return result;
76
+ };
77
+
78
+ /**
79
+ * merge services
80
+ * from meta.children[].mountPoints[].services, meta.children[].services
81
+ * to childrenMeta[].interfaces[].services
82
+ *
83
+ * @param {array<child>|object{children:array}} source e.g. [<config>] or { children: [<config>] }
84
+ * @param {array<meta|{meta}>} childrenMeta e.g. [<meta>] or [{ meta: <meta> }]
85
+ */
86
+ const mergeMeta = (source, childrenMeta = []) => {
87
+ // configMap
88
+ const configMap = {};
89
+ (Array.isArray(source) ? source : getComponentConfig(source) || []).forEach((x) => {
90
+ configMap[x.name] = x;
91
+ });
92
+
93
+ // merge service from config to child meta
94
+ childrenMeta.forEach((child) => {
95
+ const childMeta = child.meta || child;
96
+ const config = configMap[childMeta.name];
97
+ if (!config) {
98
+ return;
99
+ }
100
+
101
+ (config.mountPoints || []).forEach((mountPoint) => {
102
+ if (!mountPoint.services) {
103
+ return;
104
+ }
105
+
106
+ const childInterface = childMeta.interfaces.find((y) => y.name === mountPoint.child.interfaceName);
107
+ if (childInterface) {
108
+ // merge
109
+ const services = childInterface.services || [];
110
+ mountPoint.services.forEach((x) => {
111
+ const index = services.findIndex((y) => y.name === x.name);
112
+ if (index >= 0) {
113
+ services.splice(index, 1, x);
114
+ } else {
115
+ services.push(x);
116
+ }
117
+ });
118
+ childInterface.services = services;
119
+ }
120
+ });
121
+
122
+ if (config.services) {
123
+ const childInterface = findWebInterface(childMeta);
124
+ if (childInterface) {
125
+ // merge
126
+ const services = childInterface.services || [];
127
+ config.services.forEach((x) => {
128
+ const index = services.findIndex((y) => y.name === x.name);
129
+ if (index >= 0) {
130
+ services.splice(index, 1, x);
131
+ } else {
132
+ services.push(x);
133
+ }
134
+ });
135
+ childInterface.services = services;
136
+ }
137
+ }
138
+ });
139
+ };
140
+
141
+ /**
142
+ * Get list of components that need updates based on version comparison
143
+ * @param {object} oldBlocklet - Old blocklet state
144
+ * @param {object} newBlocklet - New blocklet state
145
+ * @returns {Array} Array of {id, meta} for components needing update
146
+ */
147
+ const getUpdateMetaList = (oldBlocklet = {}, newBlocklet = {}) => {
148
+ const oldMap = {};
149
+ forEachChildSync(oldBlocklet, (b, { id }) => {
150
+ if (b.bundleSource) {
151
+ oldMap[id] = b.meta.version;
152
+ }
153
+ });
154
+
155
+ const res = [];
156
+
157
+ forEachChildSync(newBlocklet, (b, { id }) => {
158
+ if ((b.bundleSource && semver.gt(b.meta.version, oldMap[id])) || process.env.TEST_UPDATE_ALL_BLOCKLET === 'true') {
159
+ res.push({ id, meta: b.meta });
160
+ }
161
+ });
162
+
163
+ return res;
164
+ };
165
+
166
+ /**
167
+ * Get fixed bundle source from component
168
+ * @param {object} component - Component object
169
+ * @returns {object|null} Bundle source object or null
170
+ */
171
+ const getFixedBundleSource = (component) => {
172
+ if (!component) {
173
+ return null;
174
+ }
175
+
176
+ if (component.bundleSource) {
177
+ return component.bundleSource;
178
+ }
179
+
180
+ const { source, deployedFrom, meta: { bundleName } = {} } = component;
181
+
182
+ if (!deployedFrom) {
183
+ return null;
184
+ }
185
+
186
+ if (source === BlockletSource.registry && bundleName) {
187
+ return {
188
+ store: deployedFrom,
189
+ name: bundleName,
190
+ version: 'latest',
191
+ };
192
+ }
193
+
194
+ if (source === BlockletSource.url) {
195
+ return {
196
+ url: deployedFrom,
197
+ };
198
+ }
199
+
200
+ return null;
201
+ };
202
+
203
+ /**
204
+ * Get computed status of a blocklet based on its components
205
+ * @param {object} blocklet - Blocklet object
206
+ * @returns {string} Computed status
207
+ */
208
+ const getBlockletStatus = (blocklet) => {
209
+ const fallbackStatus = BlockletStatus.stopped;
210
+
211
+ if (!blocklet) {
212
+ return fallbackStatus;
213
+ }
214
+
215
+ if (!blocklet.children?.length) {
216
+ if (blocklet.meta?.group === BlockletGroup.gateway) {
217
+ return blocklet.status;
218
+ }
219
+
220
+ if (blocklet.status === BlockletStatus.added) {
221
+ return BlockletStatus.added;
222
+ }
223
+
224
+ // for backward compatibility
225
+ if (!blocklet.structVersion) {
226
+ return blocklet.status;
227
+ }
228
+
229
+ return fallbackStatus;
230
+ }
231
+
232
+ let inProgressStatus;
233
+ let runningStatus;
234
+ let status;
235
+
236
+ forEachComponentV2Sync(blocklet, (component) => {
237
+ if (component.meta?.group === BlockletGroup.gateway) {
238
+ return;
239
+ }
240
+
241
+ if (isInProgress(component.status)) {
242
+ if (!inProgressStatus) {
243
+ inProgressStatus = component.status;
244
+ }
245
+ return;
246
+ }
247
+
248
+ if (isRunning(component.status) || isRunning(component.greenStatus)) {
249
+ runningStatus = BlockletStatus.running;
250
+ return;
251
+ }
252
+
253
+ if (status === BlockletStatus.stopped) {
254
+ return;
255
+ }
256
+
257
+ status = component.status;
258
+ });
259
+
260
+ return inProgressStatus || runningStatus || status;
261
+ };
262
+
263
+ module.exports = {
264
+ formatBlockletTheme,
265
+ mergeMeta,
266
+ getUpdateMetaList,
267
+ getFixedBundleSource,
268
+ getBlockletStatus,
269
+ };