scene-capability-engine 3.6.53 → 3.6.55
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/README.md +6 -1
- package/README.zh.md +6 -1
- package/bin/scene-capability-engine.js +2 -0
- package/docs/command-reference.md +29 -1
- package/docs/magicball-app-collection-phase-1.md +133 -0
- package/docs/magicball-cli-invocation-examples.md +40 -0
- package/docs/magicball-integration-doc-index.md +14 -6
- package/docs/magicball-integration-issue-tracker.md +42 -3
- package/docs/magicball-sce-adaptation-guide.md +36 -9
- package/docs/releases/README.md +2 -0
- package/docs/releases/v3.6.54.md +19 -0
- package/docs/releases/v3.6.55.md +18 -0
- package/docs/zh/releases/README.md +2 -0
- package/docs/zh/releases/v3.6.54.md +19 -0
- package/docs/zh/releases/v3.6.55.md +18 -0
- package/lib/app/collection-store.js +127 -0
- package/lib/app/install-apply-runner.js +192 -0
- package/lib/app/install-plan-service.js +410 -0
- package/lib/app/scene-workspace-store.js +132 -0
- package/lib/commands/app.js +281 -0
- package/lib/commands/device.js +194 -0
- package/lib/commands/scene.js +228 -0
- package/lib/device/current-device.js +158 -0
- package/lib/device/device-override-store.js +157 -0
- package/lib/problem/project-problem-projection.js +239 -0
- package/lib/workspace/collab-governance-audit.js +107 -0
- package/lib/workspace/collab-governance-gate.js +24 -4
- package/lib/workspace/takeover-baseline.js +76 -0
- package/package.json +1 -1
- package/template/.sce/README.md +1 -1
- package/template/.sce/config/problem-closure-policy.json +5 -0
- package/template/.sce/knowledge/problem/project-shared-problems.json +16 -0
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
const { getAppCollection } = require('./collection-store');
|
|
2
|
+
const { getSceneWorkspace } = require('./scene-workspace-store');
|
|
3
|
+
|
|
4
|
+
function normalizeString(value) {
|
|
5
|
+
if (typeof value !== 'string') {
|
|
6
|
+
return '';
|
|
7
|
+
}
|
|
8
|
+
return value.trim();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function normalizeStringArray(value) {
|
|
12
|
+
if (!Array.isArray(value)) {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
const seen = new Set();
|
|
16
|
+
const items = [];
|
|
17
|
+
for (const entry of value) {
|
|
18
|
+
const normalized = normalizeString(entry);
|
|
19
|
+
if (!normalized || seen.has(normalized)) {
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
seen.add(normalized);
|
|
23
|
+
items.push(normalized);
|
|
24
|
+
}
|
|
25
|
+
return items;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getBundleRuntimeState(bundle = {}) {
|
|
29
|
+
const metadata = bundle.metadata && typeof bundle.metadata === 'object' ? bundle.metadata : {};
|
|
30
|
+
const installation = metadata.runtime_installation && typeof metadata.runtime_installation === 'object'
|
|
31
|
+
? metadata.runtime_installation
|
|
32
|
+
: {};
|
|
33
|
+
const runtimeActivation = metadata.runtime_activation && typeof metadata.runtime_activation === 'object'
|
|
34
|
+
? metadata.runtime_activation
|
|
35
|
+
: {};
|
|
36
|
+
const installStatus = normalizeString(installation.status) || 'not-installed';
|
|
37
|
+
const installedReleaseId = installStatus === 'installed'
|
|
38
|
+
? (normalizeString(installation.release_id) || null)
|
|
39
|
+
: null;
|
|
40
|
+
const activeReleaseId = normalizeString(
|
|
41
|
+
bundle.runtime_release_id || runtimeActivation.active_release_id
|
|
42
|
+
) || null;
|
|
43
|
+
return {
|
|
44
|
+
install_status: installStatus,
|
|
45
|
+
installed_release_id: installedReleaseId,
|
|
46
|
+
active_release_id: activeReleaseId
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function desiredKeyFromItem(item = {}) {
|
|
51
|
+
return normalizeString(item.app_id) || normalizeString(item.app_key);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function mergeDesiredItem(target = {}, incoming = {}, sourceRef = '') {
|
|
55
|
+
const sources = normalizeStringArray([...(target.sources || []), sourceRef]);
|
|
56
|
+
const capabilityTags = normalizeStringArray([
|
|
57
|
+
...(target.capability_tags || []),
|
|
58
|
+
...(incoming.capability_tags || [])
|
|
59
|
+
]);
|
|
60
|
+
const numericPriority = [target.priority, incoming.priority]
|
|
61
|
+
.filter((value) => Number.isFinite(Number(value)))
|
|
62
|
+
.map((value) => Number(value));
|
|
63
|
+
return {
|
|
64
|
+
app_id: normalizeString(target.app_id) || normalizeString(incoming.app_id) || null,
|
|
65
|
+
app_key: normalizeString(target.app_key) || normalizeString(incoming.app_key) || null,
|
|
66
|
+
required: Boolean(target.required || incoming.required),
|
|
67
|
+
allow_local_remove: target.allow_local_remove === false || incoming.allow_local_remove === false ? false : true,
|
|
68
|
+
priority: numericPriority.length > 0 ? Math.min(...numericPriority) : null,
|
|
69
|
+
default_entry: normalizeString(target.default_entry) || normalizeString(incoming.default_entry) || null,
|
|
70
|
+
capability_tags: capabilityTags,
|
|
71
|
+
metadata: {
|
|
72
|
+
...(target.metadata && typeof target.metadata === 'object' ? target.metadata : {}),
|
|
73
|
+
...(incoming.metadata && typeof incoming.metadata === 'object' ? incoming.metadata : {})
|
|
74
|
+
},
|
|
75
|
+
sources
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function createBundleIndex(bundles = []) {
|
|
80
|
+
const byId = new Map();
|
|
81
|
+
const byKey = new Map();
|
|
82
|
+
for (const bundle of bundles) {
|
|
83
|
+
const appId = normalizeString(bundle.app_id);
|
|
84
|
+
const appKey = normalizeString(bundle.app_key);
|
|
85
|
+
if (appId) {
|
|
86
|
+
byId.set(appId, bundle);
|
|
87
|
+
}
|
|
88
|
+
if (appKey) {
|
|
89
|
+
byKey.set(appKey, bundle);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return { byId, byKey };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function matchesDeviceCapabilities(item = {}, currentDevice = {}) {
|
|
96
|
+
const requiredTags = normalizeStringArray(item.capability_tags || []);
|
|
97
|
+
if (requiredTags.length === 0) {
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
const deviceTags = new Set(normalizeStringArray(currentDevice.capability_tags || []));
|
|
101
|
+
return requiredTags.some((tag) => deviceTags.has(tag));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function buildDesiredActionEntry(item = {}, bundle = null, currentDevice = {}) {
|
|
105
|
+
if (!matchesDeviceCapabilities(item, currentDevice)) {
|
|
106
|
+
return {
|
|
107
|
+
app_id: item.app_id || null,
|
|
108
|
+
app_key: item.app_key || null,
|
|
109
|
+
app_name: bundle && bundle.app_name ? bundle.app_name : null,
|
|
110
|
+
decision: 'skip',
|
|
111
|
+
reason: 'device-capability-mismatch',
|
|
112
|
+
install_status: bundle ? getBundleRuntimeState(bundle).install_status : 'unknown',
|
|
113
|
+
installed_release_id: bundle ? getBundleRuntimeState(bundle).installed_release_id : null,
|
|
114
|
+
active_release_id: bundle ? getBundleRuntimeState(bundle).active_release_id : null,
|
|
115
|
+
required: item.required === true,
|
|
116
|
+
sources: item.sources || []
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
const runtimeState = getBundleRuntimeState(bundle || {});
|
|
120
|
+
if (!bundle) {
|
|
121
|
+
return {
|
|
122
|
+
app_id: item.app_id || null,
|
|
123
|
+
app_key: item.app_key || null,
|
|
124
|
+
app_name: null,
|
|
125
|
+
decision: 'skip',
|
|
126
|
+
reason: 'app-bundle-not-found',
|
|
127
|
+
install_status: 'unknown',
|
|
128
|
+
installed_release_id: null,
|
|
129
|
+
active_release_id: null,
|
|
130
|
+
required: item.required === true,
|
|
131
|
+
sources: item.sources || []
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
if (runtimeState.install_status !== 'installed') {
|
|
135
|
+
return {
|
|
136
|
+
app_id: bundle.app_id || null,
|
|
137
|
+
app_key: bundle.app_key || null,
|
|
138
|
+
app_name: bundle.app_name || null,
|
|
139
|
+
decision: 'install',
|
|
140
|
+
reason: 'desired-app-not-installed',
|
|
141
|
+
install_status: runtimeState.install_status,
|
|
142
|
+
installed_release_id: runtimeState.installed_release_id,
|
|
143
|
+
active_release_id: runtimeState.active_release_id,
|
|
144
|
+
required: item.required === true,
|
|
145
|
+
sources: item.sources || []
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
if (runtimeState.installed_release_id && runtimeState.active_release_id !== runtimeState.installed_release_id) {
|
|
149
|
+
return {
|
|
150
|
+
app_id: bundle.app_id || null,
|
|
151
|
+
app_key: bundle.app_key || null,
|
|
152
|
+
app_name: bundle.app_name || null,
|
|
153
|
+
decision: 'activate',
|
|
154
|
+
reason: 'installed-release-not-active',
|
|
155
|
+
install_status: runtimeState.install_status,
|
|
156
|
+
installed_release_id: runtimeState.installed_release_id,
|
|
157
|
+
active_release_id: runtimeState.active_release_id,
|
|
158
|
+
required: item.required === true,
|
|
159
|
+
sources: item.sources || []
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
return {
|
|
163
|
+
app_id: bundle.app_id || null,
|
|
164
|
+
app_key: bundle.app_key || null,
|
|
165
|
+
app_name: bundle.app_name || null,
|
|
166
|
+
decision: 'keep',
|
|
167
|
+
reason: 'desired-app-already-installed',
|
|
168
|
+
install_status: runtimeState.install_status,
|
|
169
|
+
installed_release_id: runtimeState.installed_release_id,
|
|
170
|
+
active_release_id: runtimeState.active_release_id,
|
|
171
|
+
required: item.required === true,
|
|
172
|
+
sources: item.sources || []
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function buildRemovalAction(bundle = {}) {
|
|
177
|
+
const runtimeState = getBundleRuntimeState(bundle);
|
|
178
|
+
if (runtimeState.install_status !== 'installed') {
|
|
179
|
+
return {
|
|
180
|
+
app_id: bundle.app_id || null,
|
|
181
|
+
app_key: bundle.app_key || null,
|
|
182
|
+
app_name: bundle.app_name || null,
|
|
183
|
+
decision: 'keep',
|
|
184
|
+
reason: 'not-desired-and-not-installed',
|
|
185
|
+
install_status: runtimeState.install_status,
|
|
186
|
+
installed_release_id: runtimeState.installed_release_id,
|
|
187
|
+
active_release_id: runtimeState.active_release_id,
|
|
188
|
+
required: false,
|
|
189
|
+
sources: []
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
if (runtimeState.active_release_id && runtimeState.active_release_id === runtimeState.installed_release_id) {
|
|
193
|
+
return {
|
|
194
|
+
app_id: bundle.app_id || null,
|
|
195
|
+
app_key: bundle.app_key || null,
|
|
196
|
+
app_name: bundle.app_name || null,
|
|
197
|
+
decision: 'skip',
|
|
198
|
+
reason: 'active-release-protected',
|
|
199
|
+
install_status: runtimeState.install_status,
|
|
200
|
+
installed_release_id: runtimeState.installed_release_id,
|
|
201
|
+
active_release_id: runtimeState.active_release_id,
|
|
202
|
+
required: false,
|
|
203
|
+
sources: []
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
return {
|
|
207
|
+
app_id: bundle.app_id || null,
|
|
208
|
+
app_key: bundle.app_key || null,
|
|
209
|
+
app_name: bundle.app_name || null,
|
|
210
|
+
decision: 'uninstall',
|
|
211
|
+
reason: 'not-desired-on-current-device',
|
|
212
|
+
install_status: runtimeState.install_status,
|
|
213
|
+
installed_release_id: runtimeState.installed_release_id,
|
|
214
|
+
active_release_id: runtimeState.active_release_id,
|
|
215
|
+
required: false,
|
|
216
|
+
sources: []
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function expandCollectionDefinition(projectPath, collectionRef, fileSystem, desiredMap, unresolvedCollections, sourceRef) {
|
|
221
|
+
const collection = await getAppCollection(projectPath, collectionRef, { fileSystem });
|
|
222
|
+
if (!collection) {
|
|
223
|
+
unresolvedCollections.push(collectionRef);
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
for (const item of collection.items) {
|
|
227
|
+
const key = desiredKeyFromItem(item);
|
|
228
|
+
if (!key) {
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
desiredMap.set(key, mergeDesiredItem(desiredMap.get(key), item, sourceRef || `collection:${collection.collection_id}`));
|
|
232
|
+
}
|
|
233
|
+
return collection;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function applyDeviceOverride(desiredMap = new Map(), deviceOverride = {}) {
|
|
237
|
+
const removedRefs = normalizeStringArray(deviceOverride.removed_apps || []);
|
|
238
|
+
for (const removedRef of removedRefs) {
|
|
239
|
+
desiredMap.delete(removedRef);
|
|
240
|
+
for (const [key, item] of desiredMap.entries()) {
|
|
241
|
+
if (normalizeString(item.app_id) === removedRef || normalizeString(item.app_key) === removedRef) {
|
|
242
|
+
desiredMap.delete(key);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const addedApps = Array.isArray(deviceOverride.added_apps) ? deviceOverride.added_apps : [];
|
|
248
|
+
for (const item of addedApps) {
|
|
249
|
+
const key = desiredKeyFromItem(item);
|
|
250
|
+
if (!key) {
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
desiredMap.set(key, mergeDesiredItem(desiredMap.get(key), item, 'device-override:add'));
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function summarizeActions(actions = []) {
|
|
258
|
+
const counts = {
|
|
259
|
+
install: 0,
|
|
260
|
+
uninstall: 0,
|
|
261
|
+
activate: 0,
|
|
262
|
+
keep: 0,
|
|
263
|
+
skip: 0
|
|
264
|
+
};
|
|
265
|
+
for (const item of actions) {
|
|
266
|
+
const key = normalizeString(item.decision);
|
|
267
|
+
if (Object.prototype.hasOwnProperty.call(counts, key)) {
|
|
268
|
+
counts[key] += 1;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return counts;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async function buildCollectionApplyPlan(projectPath = process.cwd(), options = {}) {
|
|
275
|
+
const fileSystem = options.fileSystem;
|
|
276
|
+
const store = options.store;
|
|
277
|
+
const currentDevice = options.currentDevice || {};
|
|
278
|
+
const deviceOverride = options.deviceOverride || {};
|
|
279
|
+
const collectionRef = normalizeString(options.collectionRef);
|
|
280
|
+
const collection = await getAppCollection(projectPath, collectionRef, { fileSystem });
|
|
281
|
+
if (!collection) {
|
|
282
|
+
throw new Error(`app collection not found: ${collectionRef}`);
|
|
283
|
+
}
|
|
284
|
+
const desiredMap = new Map();
|
|
285
|
+
for (const item of collection.items) {
|
|
286
|
+
const key = desiredKeyFromItem(item);
|
|
287
|
+
if (!key) {
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
desiredMap.set(key, mergeDesiredItem(desiredMap.get(key), item, `collection:${collection.collection_id}`));
|
|
291
|
+
}
|
|
292
|
+
applyDeviceOverride(desiredMap, deviceOverride);
|
|
293
|
+
|
|
294
|
+
const bundles = await store.listAppBundles({ limit: 1000 });
|
|
295
|
+
const bundleIndex = createBundleIndex(Array.isArray(bundles) ? bundles : []);
|
|
296
|
+
const actions = [];
|
|
297
|
+
const desiredBundleIds = new Set();
|
|
298
|
+
|
|
299
|
+
for (const desired of desiredMap.values()) {
|
|
300
|
+
const bundle = bundleIndex.byId.get(normalizeString(desired.app_id)) || bundleIndex.byKey.get(normalizeString(desired.app_key)) || null;
|
|
301
|
+
if (bundle && bundle.app_id) {
|
|
302
|
+
desiredBundleIds.add(bundle.app_id);
|
|
303
|
+
}
|
|
304
|
+
actions.push(buildDesiredActionEntry(desired, bundle, currentDevice));
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
for (const bundle of Array.isArray(bundles) ? bundles : []) {
|
|
308
|
+
if (desiredBundleIds.has(bundle.app_id)) {
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
const removal = buildRemovalAction(bundle);
|
|
312
|
+
if (removal.decision === 'keep' && removal.reason === 'not-desired-and-not-installed') {
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
actions.push(removal);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const counts = summarizeActions(actions);
|
|
319
|
+
return {
|
|
320
|
+
source: {
|
|
321
|
+
type: 'app-collection',
|
|
322
|
+
id: collection.collection_id,
|
|
323
|
+
name: collection.name
|
|
324
|
+
},
|
|
325
|
+
current_device: currentDevice,
|
|
326
|
+
device_override: deviceOverride,
|
|
327
|
+
desired_apps: [...desiredMap.values()],
|
|
328
|
+
unresolved_collections: [],
|
|
329
|
+
unresolved_apps: actions.filter((item) => item.reason === 'app-bundle-not-found').map((item) => item.app_id || item.app_key),
|
|
330
|
+
actions,
|
|
331
|
+
counts
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
async function buildSceneWorkspaceApplyPlan(projectPath = process.cwd(), options = {}) {
|
|
336
|
+
const fileSystem = options.fileSystem;
|
|
337
|
+
const store = options.store;
|
|
338
|
+
const currentDevice = options.currentDevice || {};
|
|
339
|
+
const deviceOverride = options.deviceOverride || {};
|
|
340
|
+
const workspaceRef = normalizeString(options.workspaceRef);
|
|
341
|
+
const workspace = await getSceneWorkspace(projectPath, workspaceRef, { fileSystem });
|
|
342
|
+
if (!workspace) {
|
|
343
|
+
throw new Error(`scene workspace not found: ${workspaceRef}`);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const desiredMap = new Map();
|
|
347
|
+
const unresolvedCollections = [];
|
|
348
|
+
|
|
349
|
+
for (const collectionRef of workspace.collection_refs) {
|
|
350
|
+
await expandCollectionDefinition(projectPath, collectionRef, fileSystem, desiredMap, unresolvedCollections, `workspace:${workspace.workspace_id}:collection:${collectionRef}`);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
for (const item of workspace.items) {
|
|
354
|
+
if (item.collection_id && !item.app_id && !item.app_key) {
|
|
355
|
+
await expandCollectionDefinition(projectPath, item.collection_id, fileSystem, desiredMap, unresolvedCollections, `workspace:${workspace.workspace_id}:collection:${item.collection_id}`);
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
const key = desiredKeyFromItem(item);
|
|
359
|
+
if (!key) {
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
desiredMap.set(key, mergeDesiredItem(desiredMap.get(key), item, `workspace:${workspace.workspace_id}`));
|
|
363
|
+
}
|
|
364
|
+
applyDeviceOverride(desiredMap, deviceOverride);
|
|
365
|
+
|
|
366
|
+
const bundles = await store.listAppBundles({ limit: 1000 });
|
|
367
|
+
const bundleIndex = createBundleIndex(Array.isArray(bundles) ? bundles : []);
|
|
368
|
+
const actions = [];
|
|
369
|
+
const desiredBundleIds = new Set();
|
|
370
|
+
|
|
371
|
+
for (const desired of desiredMap.values()) {
|
|
372
|
+
const bundle = bundleIndex.byId.get(normalizeString(desired.app_id)) || bundleIndex.byKey.get(normalizeString(desired.app_key)) || null;
|
|
373
|
+
if (bundle && bundle.app_id) {
|
|
374
|
+
desiredBundleIds.add(bundle.app_id);
|
|
375
|
+
}
|
|
376
|
+
actions.push(buildDesiredActionEntry(desired, bundle, currentDevice));
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
for (const bundle of Array.isArray(bundles) ? bundles : []) {
|
|
380
|
+
if (desiredBundleIds.has(bundle.app_id)) {
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
const removal = buildRemovalAction(bundle);
|
|
384
|
+
if (removal.decision === 'keep' && removal.reason === 'not-desired-and-not-installed') {
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
actions.push(removal);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const counts = summarizeActions(actions);
|
|
391
|
+
return {
|
|
392
|
+
source: {
|
|
393
|
+
type: 'scene-workspace',
|
|
394
|
+
id: workspace.workspace_id,
|
|
395
|
+
name: workspace.name
|
|
396
|
+
},
|
|
397
|
+
current_device: currentDevice,
|
|
398
|
+
device_override: deviceOverride,
|
|
399
|
+
desired_apps: [...desiredMap.values()],
|
|
400
|
+
unresolved_collections: normalizeStringArray(unresolvedCollections),
|
|
401
|
+
unresolved_apps: actions.filter((item) => item.reason === 'app-bundle-not-found').map((item) => item.app_id || item.app_key),
|
|
402
|
+
actions,
|
|
403
|
+
counts
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
module.exports = {
|
|
408
|
+
buildCollectionApplyPlan,
|
|
409
|
+
buildSceneWorkspaceApplyPlan
|
|
410
|
+
};
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs-extra');
|
|
3
|
+
|
|
4
|
+
function normalizeString(value) {
|
|
5
|
+
if (typeof value !== 'string') {
|
|
6
|
+
return '';
|
|
7
|
+
}
|
|
8
|
+
return value.trim();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function normalizeStringArray(value) {
|
|
12
|
+
if (!Array.isArray(value)) {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
const seen = new Set();
|
|
16
|
+
const items = [];
|
|
17
|
+
for (const entry of value) {
|
|
18
|
+
const normalized = normalizeString(entry);
|
|
19
|
+
if (!normalized || seen.has(normalized)) {
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
seen.add(normalized);
|
|
23
|
+
items.push(normalized);
|
|
24
|
+
}
|
|
25
|
+
return items;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function normalizeBoolean(value, fallback = false) {
|
|
29
|
+
if (typeof value === 'boolean') {
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
return fallback;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function normalizeWorkspaceItem(item = {}) {
|
|
36
|
+
return {
|
|
37
|
+
app_id: normalizeString(item.app_id || item.appId) || null,
|
|
38
|
+
app_key: normalizeString(item.app_key || item.appKey) || null,
|
|
39
|
+
collection_id: normalizeString(item.collection_id || item.collectionId) || null,
|
|
40
|
+
required: normalizeBoolean(item.required, false),
|
|
41
|
+
allow_local_remove: normalizeBoolean(item.allow_local_remove ?? item.allowLocalRemove, true),
|
|
42
|
+
priority: Number.isFinite(Number(item.priority)) ? Number(item.priority) : null,
|
|
43
|
+
default_entry: normalizeString(item.default_entry || item.defaultEntry) || null,
|
|
44
|
+
capability_tags: normalizeStringArray(item.capability_tags || item.capabilityTags),
|
|
45
|
+
metadata: item.metadata && typeof item.metadata === 'object' ? item.metadata : {}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function normalizeSceneWorkspace(raw = {}, filePath = '') {
|
|
50
|
+
const fileName = normalizeString(path.basename(filePath, path.extname(filePath)));
|
|
51
|
+
const workspaceId = normalizeString(raw.workspace_id || raw.workspaceId || raw.scene_profile_id || raw.sceneProfileId || raw.id) || fileName;
|
|
52
|
+
if (!workspaceId) {
|
|
53
|
+
throw new Error(`invalid scene workspace file: missing workspace_id (${filePath})`);
|
|
54
|
+
}
|
|
55
|
+
const items = Array.isArray(raw.items)
|
|
56
|
+
? raw.items
|
|
57
|
+
.filter((item) => item && typeof item === 'object')
|
|
58
|
+
.map((item) => normalizeWorkspaceItem(item))
|
|
59
|
+
.filter((item) => item.app_id || item.app_key || item.collection_id)
|
|
60
|
+
: [];
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
workspace_id: workspaceId,
|
|
64
|
+
name: normalizeString(raw.name) || workspaceId,
|
|
65
|
+
description: normalizeString(raw.description) || null,
|
|
66
|
+
status: normalizeString(raw.status) || 'active',
|
|
67
|
+
tags: normalizeStringArray(raw.tags),
|
|
68
|
+
collection_refs: normalizeStringArray(raw.collection_refs || raw.collectionRefs || raw.collections),
|
|
69
|
+
default_entry: normalizeString(raw.default_entry || raw.defaultEntry) || null,
|
|
70
|
+
layout_hint: normalizeString(raw.layout_hint || raw.layoutHint) || null,
|
|
71
|
+
item_count: items.length,
|
|
72
|
+
items,
|
|
73
|
+
metadata: raw.metadata && typeof raw.metadata === 'object' ? raw.metadata : {},
|
|
74
|
+
source_file: filePath
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function listSceneWorkspaces(projectPath = process.cwd(), options = {}) {
|
|
79
|
+
const fileSystem = options.fileSystem || fs;
|
|
80
|
+
const workspaceDir = path.join(projectPath, '.sce', 'app', 'scene-profiles');
|
|
81
|
+
if (!await fileSystem.pathExists(workspaceDir)) {
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const entries = await fileSystem.readdir(workspaceDir);
|
|
86
|
+
const query = normalizeString(options.query).toLowerCase();
|
|
87
|
+
const status = normalizeString(options.status);
|
|
88
|
+
const normalized = [];
|
|
89
|
+
|
|
90
|
+
for (const entry of entries) {
|
|
91
|
+
if (!entry.toLowerCase().endsWith('.json')) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const absolutePath = path.join(workspaceDir, entry);
|
|
95
|
+
const raw = await fileSystem.readJson(absolutePath);
|
|
96
|
+
const workspace = normalizeSceneWorkspace(raw, absolutePath);
|
|
97
|
+
if (status && workspace.status !== status) {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (query) {
|
|
101
|
+
const haystack = [
|
|
102
|
+
workspace.workspace_id,
|
|
103
|
+
workspace.name,
|
|
104
|
+
workspace.description,
|
|
105
|
+
...workspace.tags,
|
|
106
|
+
...workspace.collection_refs
|
|
107
|
+
].join(' ').toLowerCase();
|
|
108
|
+
if (!haystack.includes(query)) {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
normalized.push(workspace);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
normalized.sort((left, right) => left.workspace_id.localeCompare(right.workspace_id));
|
|
116
|
+
return normalized;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function getSceneWorkspace(projectPath = process.cwd(), workspaceRef = '', options = {}) {
|
|
120
|
+
const normalizedRef = normalizeString(workspaceRef);
|
|
121
|
+
if (!normalizedRef) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
const workspaces = await listSceneWorkspaces(projectPath, options);
|
|
125
|
+
return workspaces.find((item) => item.workspace_id === normalizedRef || path.basename(item.source_file, '.json') === normalizedRef) || null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
module.exports = {
|
|
129
|
+
normalizeSceneWorkspace,
|
|
130
|
+
listSceneWorkspaces,
|
|
131
|
+
getSceneWorkspace
|
|
132
|
+
};
|