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
package/lib/commands/scene.js
CHANGED
|
@@ -5,6 +5,12 @@ const { spawnSync } = require('child_process');
|
|
|
5
5
|
const fs = require('fs-extra');
|
|
6
6
|
const chalk = require('chalk');
|
|
7
7
|
const yaml = require('js-yaml');
|
|
8
|
+
const { listSceneWorkspaces, getSceneWorkspace } = require('../app/scene-workspace-store');
|
|
9
|
+
const { buildSceneWorkspaceApplyPlan } = require('../app/install-plan-service');
|
|
10
|
+
const { executeInstallPlan } = require('../app/install-apply-runner');
|
|
11
|
+
const { runAppRuntimeInstallCommand, runAppRuntimeActivateCommand, runAppRuntimeUninstallCommand } = require('./app');
|
|
12
|
+
const { loadDeviceOverride } = require('../device/device-override-store');
|
|
13
|
+
const { getCurrentDeviceProfile } = require('../device/current-device');
|
|
8
14
|
|
|
9
15
|
const semver = require('semver');
|
|
10
16
|
const SceneLoader = require('../scene-runtime/scene-loader');
|
|
@@ -50,6 +56,19 @@ const SCENE_ROUTE_POLICY_DIFF_KEYS = [
|
|
|
50
56
|
'mode_bias.commit.critical',
|
|
51
57
|
'max_alternatives'
|
|
52
58
|
];
|
|
59
|
+
|
|
60
|
+
async function runSceneWorkspaceCommand(handler, options = {}, context = 'scene workspace command') {
|
|
61
|
+
try {
|
|
62
|
+
await handler(options);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
if (options && options.json) {
|
|
65
|
+
console.log(JSON.stringify({ success: false, error: error.message }, null, 2));
|
|
66
|
+
} else {
|
|
67
|
+
console.error(chalk.red(`${context} failed:`), error.message);
|
|
68
|
+
}
|
|
69
|
+
process.exitCode = 1;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
53
72
|
const SCENE_PACKAGE_API_VERSION = 'sce.scene.package/v0.1';
|
|
54
73
|
const SCENE_PACKAGE_KINDS = new Set([
|
|
55
74
|
'scene-template',
|
|
@@ -202,6 +221,40 @@ function registerSceneCommands(program) {
|
|
|
202
221
|
.command('scene')
|
|
203
222
|
.description('Execute scene contracts with runtime guardrails');
|
|
204
223
|
|
|
224
|
+
const workspaceCmd = sceneCmd
|
|
225
|
+
.command('workspace')
|
|
226
|
+
.description('Inspect file-backed scene workspace intent definitions');
|
|
227
|
+
|
|
228
|
+
workspaceCmd
|
|
229
|
+
.command('list')
|
|
230
|
+
.description('List scene workspace definitions from .sce/app/scene-profiles')
|
|
231
|
+
.option('--limit <n>', 'Maximum rows', '100')
|
|
232
|
+
.option('--status <status>', 'Filter by workspace status')
|
|
233
|
+
.option('--query <text>', 'Free-text query against id/name/description/tags/collections')
|
|
234
|
+
.option('--json', 'Print machine-readable JSON output')
|
|
235
|
+
.action(async (options) => {
|
|
236
|
+
await runSceneWorkspaceCommand(runSceneWorkspaceListCommand, options, 'scene workspace list');
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
workspaceCmd
|
|
240
|
+
.command('show')
|
|
241
|
+
.description('Show one scene workspace definition')
|
|
242
|
+
.requiredOption('--workspace <id>', 'Workspace id or file basename')
|
|
243
|
+
.option('--json', 'Print machine-readable JSON output')
|
|
244
|
+
.action(async (options) => {
|
|
245
|
+
await runSceneWorkspaceCommand(runSceneWorkspaceShowCommand, options, 'scene workspace show');
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
workspaceCmd
|
|
249
|
+
.command('apply')
|
|
250
|
+
.description('Build a plan-first apply diff for one scene workspace')
|
|
251
|
+
.requiredOption('--workspace <id>', 'Workspace id or file basename')
|
|
252
|
+
.option('--execute', 'Reserved for future explicit execution; currently returns a blocked plan')
|
|
253
|
+
.option('--json', 'Print machine-readable JSON output')
|
|
254
|
+
.action(async (options) => {
|
|
255
|
+
await runSceneWorkspaceCommand(runSceneWorkspaceApplyCommand, options, 'scene workspace apply');
|
|
256
|
+
});
|
|
257
|
+
|
|
205
258
|
sceneCmd
|
|
206
259
|
.command('validate')
|
|
207
260
|
.description('Validate scene manifest from spec or file path')
|
|
@@ -6883,6 +6936,178 @@ function printScaffoldSummary(options, summary) {
|
|
|
6883
6936
|
console.log(` Dry Run: ${summary.dry_run ? 'yes' : 'no'}`);
|
|
6884
6937
|
}
|
|
6885
6938
|
|
|
6939
|
+
async function runSceneWorkspaceListCommand(rawOptions = {}, dependencies = {}) {
|
|
6940
|
+
const options = rawOptions && typeof rawOptions === 'object' ? rawOptions : {};
|
|
6941
|
+
const projectRoot = dependencies.projectRoot || process.cwd();
|
|
6942
|
+
const fileSystem = dependencies.fileSystem || fs;
|
|
6943
|
+
const limitCandidate = Number.parseInt(`${options.limit || 100}`, 10);
|
|
6944
|
+
const limit = Number.isFinite(limitCandidate) && limitCandidate > 0 ? Math.min(limitCandidate, 1000) : 100;
|
|
6945
|
+
const workspaces = await listSceneWorkspaces(projectRoot, {
|
|
6946
|
+
fileSystem,
|
|
6947
|
+
query: options.query,
|
|
6948
|
+
status: options.status
|
|
6949
|
+
});
|
|
6950
|
+
const items = workspaces.slice(0, limit).map((item) => ({
|
|
6951
|
+
workspace_id: item.workspace_id,
|
|
6952
|
+
name: item.name,
|
|
6953
|
+
description: item.description,
|
|
6954
|
+
status: item.status,
|
|
6955
|
+
item_count: item.item_count,
|
|
6956
|
+
collection_ref_count: item.collection_refs.length,
|
|
6957
|
+
tags: item.tags,
|
|
6958
|
+
source_file: item.source_file
|
|
6959
|
+
}));
|
|
6960
|
+
const payload = {
|
|
6961
|
+
mode: 'scene-workspace-list',
|
|
6962
|
+
generated_at: new Date().toISOString(),
|
|
6963
|
+
query: {
|
|
6964
|
+
limit,
|
|
6965
|
+
status: typeof options.status === 'string' ? options.status.trim() || null : null,
|
|
6966
|
+
query: typeof options.query === 'string' ? options.query.trim() || null : null
|
|
6967
|
+
},
|
|
6968
|
+
summary: {
|
|
6969
|
+
total: items.length
|
|
6970
|
+
},
|
|
6971
|
+
items,
|
|
6972
|
+
view_model: {
|
|
6973
|
+
type: 'table',
|
|
6974
|
+
columns: ['workspace_id', 'name', 'status', 'item_count', 'collection_ref_count', 'source_file']
|
|
6975
|
+
}
|
|
6976
|
+
};
|
|
6977
|
+
if (options.json) {
|
|
6978
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
6979
|
+
} else {
|
|
6980
|
+
console.log(chalk.blue(`Scene Workspace List (${items.length})`));
|
|
6981
|
+
items.forEach((item) => {
|
|
6982
|
+
console.log(` - ${item.workspace_id} | ${item.name} | ${item.status} | items=${item.item_count}`);
|
|
6983
|
+
});
|
|
6984
|
+
}
|
|
6985
|
+
return payload;
|
|
6986
|
+
}
|
|
6987
|
+
|
|
6988
|
+
async function runSceneWorkspaceShowCommand(rawOptions = {}, dependencies = {}) {
|
|
6989
|
+
const options = rawOptions && typeof rawOptions === 'object' ? rawOptions : {};
|
|
6990
|
+
const workspaceRef = typeof options.workspace === 'string' ? options.workspace.trim() : '';
|
|
6991
|
+
if (!workspaceRef) {
|
|
6992
|
+
throw new Error('--workspace is required');
|
|
6993
|
+
}
|
|
6994
|
+
const projectRoot = dependencies.projectRoot || process.cwd();
|
|
6995
|
+
const fileSystem = dependencies.fileSystem || fs;
|
|
6996
|
+
const workspace = await getSceneWorkspace(projectRoot, workspaceRef, { fileSystem });
|
|
6997
|
+
if (!workspace) {
|
|
6998
|
+
throw new Error(`scene workspace not found: ${workspaceRef}`);
|
|
6999
|
+
}
|
|
7000
|
+
const payload = {
|
|
7001
|
+
mode: 'scene-workspace-show',
|
|
7002
|
+
generated_at: new Date().toISOString(),
|
|
7003
|
+
query: {
|
|
7004
|
+
workspace: workspaceRef
|
|
7005
|
+
},
|
|
7006
|
+
summary: {
|
|
7007
|
+
workspace_id: workspace.workspace_id,
|
|
7008
|
+
name: workspace.name,
|
|
7009
|
+
status: workspace.status,
|
|
7010
|
+
item_count: workspace.item_count,
|
|
7011
|
+
collection_ref_count: workspace.collection_refs.length
|
|
7012
|
+
},
|
|
7013
|
+
workspace
|
|
7014
|
+
};
|
|
7015
|
+
if (options.json) {
|
|
7016
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
7017
|
+
} else {
|
|
7018
|
+
console.log(chalk.blue('Scene Workspace Show'));
|
|
7019
|
+
console.log(` Workspace: ${payload.summary.workspace_id}`);
|
|
7020
|
+
console.log(` Name: ${payload.summary.name}`);
|
|
7021
|
+
console.log(` Status: ${payload.summary.status}`);
|
|
7022
|
+
console.log(` Items: ${payload.summary.item_count}`);
|
|
7023
|
+
console.log(` Collection Refs: ${payload.summary.collection_ref_count}`);
|
|
7024
|
+
}
|
|
7025
|
+
return payload;
|
|
7026
|
+
}
|
|
7027
|
+
|
|
7028
|
+
async function runSceneWorkspaceApplyCommand(rawOptions = {}, dependencies = {}) {
|
|
7029
|
+
const options = rawOptions && typeof rawOptions === 'object' ? rawOptions : {};
|
|
7030
|
+
const workspaceRef = typeof options.workspace === 'string' ? options.workspace.trim() : '';
|
|
7031
|
+
if (!workspaceRef) {
|
|
7032
|
+
throw new Error('--workspace is required');
|
|
7033
|
+
}
|
|
7034
|
+
const projectRoot = dependencies.projectRoot || process.cwd();
|
|
7035
|
+
const fileSystem = dependencies.fileSystem || fs;
|
|
7036
|
+
const stateStore = dependencies.stateStore || require('../state/sce-state-store').getSceStateStore(projectRoot, {
|
|
7037
|
+
fileSystem,
|
|
7038
|
+
env: dependencies.env || process.env
|
|
7039
|
+
});
|
|
7040
|
+
const currentDevice = await getCurrentDeviceProfile(projectRoot, {
|
|
7041
|
+
fileSystem,
|
|
7042
|
+
persistIfMissing: false
|
|
7043
|
+
});
|
|
7044
|
+
const deviceOverride = await loadDeviceOverride(projectRoot, { fileSystem });
|
|
7045
|
+
const plan = await buildSceneWorkspaceApplyPlan(projectRoot, {
|
|
7046
|
+
fileSystem,
|
|
7047
|
+
store: stateStore,
|
|
7048
|
+
workspaceRef,
|
|
7049
|
+
currentDevice,
|
|
7050
|
+
deviceOverride
|
|
7051
|
+
});
|
|
7052
|
+
const execution = options.execute
|
|
7053
|
+
? await executeInstallPlan(plan, {
|
|
7054
|
+
store: stateStore,
|
|
7055
|
+
executeInstall: runAppRuntimeInstallCommand,
|
|
7056
|
+
executeActivate: runAppRuntimeActivateCommand,
|
|
7057
|
+
executeUninstall: runAppRuntimeUninstallCommand,
|
|
7058
|
+
dependencies: {
|
|
7059
|
+
projectPath: projectRoot,
|
|
7060
|
+
projectRoot,
|
|
7061
|
+
fileSystem,
|
|
7062
|
+
env: dependencies.env || process.env,
|
|
7063
|
+
stateStore
|
|
7064
|
+
},
|
|
7065
|
+
commandOptions: options
|
|
7066
|
+
})
|
|
7067
|
+
: {
|
|
7068
|
+
execute_supported: true,
|
|
7069
|
+
executed: false,
|
|
7070
|
+
blocked_reason: null,
|
|
7071
|
+
results: []
|
|
7072
|
+
};
|
|
7073
|
+
const payload = {
|
|
7074
|
+
mode: 'scene-workspace-apply',
|
|
7075
|
+
generated_at: new Date().toISOString(),
|
|
7076
|
+
execute_supported: execution.execute_supported,
|
|
7077
|
+
executed: execution.executed,
|
|
7078
|
+
execution_blocked_reason: execution.blocked_reason,
|
|
7079
|
+
execution: {
|
|
7080
|
+
results: execution.results,
|
|
7081
|
+
preflight_failures: execution.preflight_failures || []
|
|
7082
|
+
},
|
|
7083
|
+
current_device: currentDevice,
|
|
7084
|
+
device_override: deviceOverride,
|
|
7085
|
+
summary: {
|
|
7086
|
+
source_type: plan.source.type,
|
|
7087
|
+
source_id: plan.source.id,
|
|
7088
|
+
desired_app_count: plan.desired_apps.length,
|
|
7089
|
+
install_count: plan.counts.install,
|
|
7090
|
+
activate_count: plan.counts.activate,
|
|
7091
|
+
uninstall_count: plan.counts.uninstall,
|
|
7092
|
+
keep_count: plan.counts.keep,
|
|
7093
|
+
skip_count: plan.counts.skip
|
|
7094
|
+
},
|
|
7095
|
+
plan
|
|
7096
|
+
};
|
|
7097
|
+
if (options.json) {
|
|
7098
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
7099
|
+
} else {
|
|
7100
|
+
console.log(chalk.blue('Scene Workspace Apply'));
|
|
7101
|
+
console.log(` Workspace: ${payload.summary.source_id}`);
|
|
7102
|
+
console.log(` Desired Apps: ${payload.summary.desired_app_count}`);
|
|
7103
|
+
console.log(` Install: ${payload.summary.install_count}`);
|
|
7104
|
+
console.log(` Uninstall: ${payload.summary.uninstall_count}`);
|
|
7105
|
+
console.log(` Keep: ${payload.summary.keep_count}`);
|
|
7106
|
+
console.log(` Skip: ${payload.summary.skip_count}`);
|
|
7107
|
+
}
|
|
7108
|
+
return payload;
|
|
7109
|
+
}
|
|
7110
|
+
|
|
6886
7111
|
async function runSceneValidateCommand(rawOptions = {}, dependencies = {}) {
|
|
6887
7112
|
const projectRoot = dependencies.projectRoot || process.cwd();
|
|
6888
7113
|
const sceneLoader = dependencies.sceneLoader || new SceneLoader({ projectPath: projectRoot });
|
|
@@ -16552,6 +16777,9 @@ module.exports = {
|
|
|
16552
16777
|
validateSceneContributeOptions,
|
|
16553
16778
|
runSceneContributeCommand,
|
|
16554
16779
|
printSceneContributeSummary,
|
|
16780
|
+
runSceneWorkspaceListCommand,
|
|
16781
|
+
runSceneWorkspaceShowCommand,
|
|
16782
|
+
runSceneWorkspaceApplyCommand,
|
|
16555
16783
|
normalizeOntologyOptions,
|
|
16556
16784
|
validateOntologyOptions,
|
|
16557
16785
|
normalizeOntologyImpactOptions,
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
const os = require('os');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const fs = require('fs-extra');
|
|
5
|
+
const { MachineIdentifier } = require('../lock/machine-identifier');
|
|
6
|
+
|
|
7
|
+
function normalizeString(value) {
|
|
8
|
+
if (typeof value !== 'string') {
|
|
9
|
+
return '';
|
|
10
|
+
}
|
|
11
|
+
return value.trim();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function normalizeStringArray(value, fallback = []) {
|
|
15
|
+
if (!Array.isArray(value)) {
|
|
16
|
+
return [...fallback];
|
|
17
|
+
}
|
|
18
|
+
const seen = new Set();
|
|
19
|
+
const items = [];
|
|
20
|
+
for (const entry of value) {
|
|
21
|
+
const normalized = normalizeString(entry);
|
|
22
|
+
if (!normalized || seen.has(normalized)) {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
seen.add(normalized);
|
|
26
|
+
items.push(normalized);
|
|
27
|
+
}
|
|
28
|
+
return items;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function safeHostname() {
|
|
32
|
+
try {
|
|
33
|
+
return os.hostname() || 'unknown-host';
|
|
34
|
+
} catch (_error) {
|
|
35
|
+
return 'unknown-host';
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function safeUsername() {
|
|
40
|
+
try {
|
|
41
|
+
return os.userInfo().username || process.env.USER || process.env.USERNAME || 'unknown-user';
|
|
42
|
+
} catch (_error) {
|
|
43
|
+
return process.env.USER || process.env.USERNAME || 'unknown-user';
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function buildEphemeralDeviceId({ hostname, user, platform, arch }) {
|
|
48
|
+
const hash = crypto
|
|
49
|
+
.createHash('sha1')
|
|
50
|
+
.update(`${hostname}::${user}::${platform}::${arch}`)
|
|
51
|
+
.digest('hex')
|
|
52
|
+
.slice(0, 12);
|
|
53
|
+
return `${hostname}-ephemeral-${hash}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function buildDefaultCapabilityTags({ platform, arch }) {
|
|
57
|
+
const tags = ['desktop'];
|
|
58
|
+
if (platform) {
|
|
59
|
+
tags.push(platform);
|
|
60
|
+
}
|
|
61
|
+
if (arch) {
|
|
62
|
+
tags.push(arch);
|
|
63
|
+
}
|
|
64
|
+
if (platform && arch) {
|
|
65
|
+
tags.push(`${platform}-${arch}`);
|
|
66
|
+
}
|
|
67
|
+
return normalizeStringArray(tags);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function readJsonIfExists(filePath, fileSystem) {
|
|
71
|
+
if (!await fileSystem.pathExists(filePath)) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
return fileSystem.readJson(filePath);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function getCurrentDeviceProfile(projectPath = process.cwd(), options = {}) {
|
|
78
|
+
const fileSystem = options.fileSystem || fs;
|
|
79
|
+
const configDir = options.configDir || path.join(projectPath, '.sce', 'config');
|
|
80
|
+
const machineIdFile = options.machineIdFile || path.join(configDir, 'machine-id.json');
|
|
81
|
+
const profilePath = options.profilePath || path.join(projectPath, '.sce', 'state', 'device', 'device-current.json');
|
|
82
|
+
const persistIfMissing = options.persistIfMissing === true;
|
|
83
|
+
const fallbackPlatform = normalizeString(options.platform) || os.platform();
|
|
84
|
+
const fallbackArch = normalizeString(options.arch) || os.arch();
|
|
85
|
+
const fallbackHostname = normalizeString(options.hostname) || safeHostname();
|
|
86
|
+
const fallbackUser = normalizeString(options.user) || safeUsername();
|
|
87
|
+
|
|
88
|
+
let machineInfo = null;
|
|
89
|
+
let identitySource = 'ephemeral-device-fingerprint';
|
|
90
|
+
|
|
91
|
+
if (persistIfMissing) {
|
|
92
|
+
const machineIdentifier = options.machineIdentifier || new MachineIdentifier(configDir);
|
|
93
|
+
machineInfo = await machineIdentifier.getMachineInfo();
|
|
94
|
+
identitySource = 'machine-identifier';
|
|
95
|
+
} else {
|
|
96
|
+
const persistedMachineId = await readJsonIfExists(machineIdFile, fileSystem);
|
|
97
|
+
if (persistedMachineId && typeof persistedMachineId === 'object') {
|
|
98
|
+
machineInfo = {
|
|
99
|
+
id: normalizeString(persistedMachineId.id),
|
|
100
|
+
hostname: normalizeString(persistedMachineId.hostname) || fallbackHostname,
|
|
101
|
+
createdAt: normalizeString(persistedMachineId.createdAt) || null,
|
|
102
|
+
platform: fallbackPlatform,
|
|
103
|
+
user: fallbackUser
|
|
104
|
+
};
|
|
105
|
+
if (machineInfo.id) {
|
|
106
|
+
identitySource = 'machine-identifier';
|
|
107
|
+
} else {
|
|
108
|
+
machineInfo = null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (!machineInfo) {
|
|
114
|
+
machineInfo = {
|
|
115
|
+
id: buildEphemeralDeviceId({
|
|
116
|
+
hostname: fallbackHostname,
|
|
117
|
+
user: fallbackUser,
|
|
118
|
+
platform: fallbackPlatform,
|
|
119
|
+
arch: fallbackArch
|
|
120
|
+
}),
|
|
121
|
+
hostname: fallbackHostname,
|
|
122
|
+
createdAt: null,
|
|
123
|
+
platform: fallbackPlatform,
|
|
124
|
+
user: fallbackUser
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const profile = await readJsonIfExists(profilePath, fileSystem);
|
|
129
|
+
const normalizedProfile = profile && typeof profile === 'object' ? profile : {};
|
|
130
|
+
const platform = normalizeString(normalizedProfile.platform) || normalizeString(machineInfo.platform) || fallbackPlatform;
|
|
131
|
+
const arch = normalizeString(normalizedProfile.arch) || fallbackArch;
|
|
132
|
+
const capabilityTags = normalizeStringArray(
|
|
133
|
+
normalizedProfile.capability_tags || normalizedProfile.capabilityTags,
|
|
134
|
+
buildDefaultCapabilityTags({ platform, arch })
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
device_id: normalizeString(normalizedProfile.device_id || normalizedProfile.deviceId || normalizedProfile.id) || machineInfo.id,
|
|
139
|
+
hostname: normalizeString(normalizedProfile.hostname) || normalizeString(machineInfo.hostname) || fallbackHostname,
|
|
140
|
+
label: normalizeString(normalizedProfile.label || normalizedProfile.name) || normalizeString(machineInfo.hostname) || fallbackHostname,
|
|
141
|
+
platform,
|
|
142
|
+
arch,
|
|
143
|
+
user: normalizeString(normalizedProfile.user) || normalizeString(machineInfo.user) || fallbackUser,
|
|
144
|
+
created_at: normalizeString(normalizedProfile.created_at || normalizedProfile.createdAt) || normalizeString(machineInfo.createdAt) || null,
|
|
145
|
+
capability_tags: capabilityTags,
|
|
146
|
+
metadata: normalizedProfile.metadata && typeof normalizedProfile.metadata === 'object' ? normalizedProfile.metadata : {},
|
|
147
|
+
profile_path: profile ? profilePath : null,
|
|
148
|
+
identity_source: profile
|
|
149
|
+
? `${identitySource}+device-profile`
|
|
150
|
+
: identitySource,
|
|
151
|
+
persistent_id_available: identitySource === 'machine-identifier'
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
module.exports = {
|
|
156
|
+
getCurrentDeviceProfile,
|
|
157
|
+
buildDefaultCapabilityTags
|
|
158
|
+
};
|
|
@@ -0,0 +1,157 @@
|
|
|
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 normalizeOverrideItem(item = {}) {
|
|
36
|
+
return {
|
|
37
|
+
app_id: normalizeString(item.app_id || item.appId) || null,
|
|
38
|
+
app_key: normalizeString(item.app_key || item.appKey) || null,
|
|
39
|
+
required: normalizeBoolean(item.required, false),
|
|
40
|
+
allow_local_remove: normalizeBoolean(item.allow_local_remove ?? item.allowLocalRemove, true),
|
|
41
|
+
priority: Number.isFinite(Number(item.priority)) ? Number(item.priority) : null,
|
|
42
|
+
default_entry: normalizeString(item.default_entry || item.defaultEntry) || null,
|
|
43
|
+
capability_tags: normalizeStringArray(item.capability_tags || item.capabilityTags),
|
|
44
|
+
metadata: item.metadata && typeof item.metadata === 'object' ? item.metadata : {}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function overrideItemKey(item = {}) {
|
|
49
|
+
return normalizeString(item.app_id) || normalizeString(item.app_key);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function normalizeDeviceOverride(raw = {}, filePath = null) {
|
|
53
|
+
const addedApps = Array.isArray(raw.added_apps || raw.addedApps)
|
|
54
|
+
? (raw.added_apps || raw.addedApps)
|
|
55
|
+
.filter((item) => item && typeof item === 'object')
|
|
56
|
+
.map((item) => normalizeOverrideItem(item))
|
|
57
|
+
.filter((item) => item.app_id || item.app_key)
|
|
58
|
+
: [];
|
|
59
|
+
|
|
60
|
+
const dedupedAddedApps = [];
|
|
61
|
+
const seen = new Set();
|
|
62
|
+
for (const item of addedApps) {
|
|
63
|
+
const key = overrideItemKey(item);
|
|
64
|
+
if (!key || seen.has(key)) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
seen.add(key);
|
|
68
|
+
dedupedAddedApps.push(item);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
removed_apps: normalizeStringArray(raw.removed_apps || raw.removedApps),
|
|
73
|
+
added_apps: dedupedAddedApps,
|
|
74
|
+
metadata: raw.metadata && typeof raw.metadata === 'object' ? raw.metadata : {},
|
|
75
|
+
source_file: filePath
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function hasOwn(object, key) {
|
|
80
|
+
return Boolean(object) && Object.prototype.hasOwnProperty.call(object, key);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function mergeDeviceOverride(existing = {}, patch = {}, filePath = null) {
|
|
84
|
+
const normalizedExisting = normalizeDeviceOverride(existing, filePath);
|
|
85
|
+
const nextRaw = {
|
|
86
|
+
removed_apps: normalizedExisting.removed_apps,
|
|
87
|
+
added_apps: normalizedExisting.added_apps,
|
|
88
|
+
metadata: normalizedExisting.metadata
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
if (hasOwn(patch, 'removed_apps') || hasOwn(patch, 'removedApps')) {
|
|
92
|
+
nextRaw.removed_apps = patch.removed_apps || patch.removedApps;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (hasOwn(patch, 'added_apps') || hasOwn(patch, 'addedApps')) {
|
|
96
|
+
nextRaw.added_apps = patch.added_apps || patch.addedApps;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (hasOwn(patch, 'metadata')) {
|
|
100
|
+
const patchMetadata = patch.metadata && typeof patch.metadata === 'object' ? patch.metadata : {};
|
|
101
|
+
nextRaw.metadata = {
|
|
102
|
+
...normalizedExisting.metadata,
|
|
103
|
+
...patchMetadata
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return normalizeDeviceOverride(nextRaw, filePath);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function loadDeviceOverride(projectPath = process.cwd(), options = {}) {
|
|
111
|
+
const fileSystem = options.fileSystem || fs;
|
|
112
|
+
const filePath = options.filePath || path.join(projectPath, '.sce', 'state', 'device', 'device-override.json');
|
|
113
|
+
if (!await fileSystem.pathExists(filePath)) {
|
|
114
|
+
return normalizeDeviceOverride({}, null);
|
|
115
|
+
}
|
|
116
|
+
const raw = await fileSystem.readJson(filePath);
|
|
117
|
+
return normalizeDeviceOverride(raw, filePath);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function saveDeviceOverride(projectPath = process.cwd(), override = {}, options = {}) {
|
|
121
|
+
const fileSystem = options.fileSystem || fs;
|
|
122
|
+
const filePath = options.filePath || path.join(projectPath, '.sce', 'state', 'device', 'device-override.json');
|
|
123
|
+
const normalized = normalizeDeviceOverride(override, filePath);
|
|
124
|
+
await fileSystem.ensureDir(path.dirname(filePath));
|
|
125
|
+
await fileSystem.writeJson(filePath, {
|
|
126
|
+
removed_apps: normalized.removed_apps,
|
|
127
|
+
added_apps: normalized.added_apps,
|
|
128
|
+
metadata: normalized.metadata
|
|
129
|
+
}, { spaces: 2 });
|
|
130
|
+
return normalizeDeviceOverride({
|
|
131
|
+
removed_apps: normalized.removed_apps,
|
|
132
|
+
added_apps: normalized.added_apps,
|
|
133
|
+
metadata: normalized.metadata
|
|
134
|
+
}, filePath);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function upsertDeviceOverride(projectPath = process.cwd(), patch = {}, options = {}) {
|
|
138
|
+
const fileSystem = options.fileSystem || fs;
|
|
139
|
+
const filePath = options.filePath || path.join(projectPath, '.sce', 'state', 'device', 'device-override.json');
|
|
140
|
+
const existing = await loadDeviceOverride(projectPath, {
|
|
141
|
+
fileSystem,
|
|
142
|
+
filePath
|
|
143
|
+
});
|
|
144
|
+
const merged = mergeDeviceOverride(existing, patch, filePath);
|
|
145
|
+
return saveDeviceOverride(projectPath, merged, {
|
|
146
|
+
fileSystem,
|
|
147
|
+
filePath
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
module.exports = {
|
|
152
|
+
loadDeviceOverride,
|
|
153
|
+
normalizeDeviceOverride,
|
|
154
|
+
mergeDeviceOverride,
|
|
155
|
+
saveDeviceOverride,
|
|
156
|
+
upsertDeviceOverride
|
|
157
|
+
};
|