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.
Files changed (33) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +6 -1
  3. package/README.zh.md +6 -1
  4. package/bin/scene-capability-engine.js +2 -0
  5. package/docs/command-reference.md +29 -1
  6. package/docs/magicball-app-collection-phase-1.md +133 -0
  7. package/docs/magicball-cli-invocation-examples.md +40 -0
  8. package/docs/magicball-integration-doc-index.md +14 -6
  9. package/docs/magicball-integration-issue-tracker.md +42 -3
  10. package/docs/magicball-sce-adaptation-guide.md +36 -9
  11. package/docs/releases/README.md +2 -0
  12. package/docs/releases/v3.6.54.md +19 -0
  13. package/docs/releases/v3.6.55.md +18 -0
  14. package/docs/zh/releases/README.md +2 -0
  15. package/docs/zh/releases/v3.6.54.md +19 -0
  16. package/docs/zh/releases/v3.6.55.md +18 -0
  17. package/lib/app/collection-store.js +127 -0
  18. package/lib/app/install-apply-runner.js +192 -0
  19. package/lib/app/install-plan-service.js +410 -0
  20. package/lib/app/scene-workspace-store.js +132 -0
  21. package/lib/commands/app.js +281 -0
  22. package/lib/commands/device.js +194 -0
  23. package/lib/commands/scene.js +228 -0
  24. package/lib/device/current-device.js +158 -0
  25. package/lib/device/device-override-store.js +157 -0
  26. package/lib/problem/project-problem-projection.js +239 -0
  27. package/lib/workspace/collab-governance-audit.js +107 -0
  28. package/lib/workspace/collab-governance-gate.js +24 -4
  29. package/lib/workspace/takeover-baseline.js +76 -0
  30. package/package.json +1 -1
  31. package/template/.sce/README.md +1 -1
  32. package/template/.sce/config/problem-closure-policy.json +5 -0
  33. package/template/.sce/knowledge/problem/project-shared-problems.json +16 -0
@@ -3,6 +3,11 @@ const chalk = require('chalk');
3
3
  const fs = require('fs-extra');
4
4
  const { ensureWriteAuthorization } = require('../security/write-authorization');
5
5
  const { getSceStateStore } = require('../state/sce-state-store');
6
+ const { listAppCollections, getAppCollection } = require('../app/collection-store');
7
+ const { buildCollectionApplyPlan } = require('../app/install-plan-service');
8
+ const { executeInstallPlan } = require('../app/install-apply-runner');
9
+ const { getCurrentDeviceProfile } = require('../device/current-device');
10
+ const { loadDeviceOverride } = require('../device/device-override-store');
6
11
  const { loadAppRegistryConfig, saveAppRegistryConfig } = require('../app/registry-config');
7
12
  const { syncBundleRegistry, syncServiceCatalog } = require('../app/registry-sync-service');
8
13
 
@@ -49,6 +54,9 @@ function createStore(dependencies = {}) {
49
54
  }
50
55
 
51
56
  function printPayload(payload, options = {}, title = 'App Bundle') {
57
+ if (options.silent === true) {
58
+ return;
59
+ }
52
60
  if (options.json) {
53
61
  console.log(JSON.stringify(payload, null, 2));
54
62
  return;
@@ -175,6 +183,36 @@ function buildRuntimeSummary(graph = {}) {
175
183
  };
176
184
  }
177
185
 
186
+ function buildAppInstallStateItem(bundle = {}, currentDevice = null) {
187
+ const state = getRuntimeProjectionState({ bundle });
188
+ const installation = state.installation && typeof state.installation === 'object'
189
+ ? state.installation
190
+ : {};
191
+ const installationMachineId = normalizeString(installation.machine_id) || null;
192
+ let machineScope = 'unspecified';
193
+
194
+ if (installationMachineId && currentDevice && currentDevice.device_id) {
195
+ machineScope = installationMachineId === currentDevice.device_id ? 'current-device' : 'other-device';
196
+ }
197
+
198
+ return {
199
+ app_id: bundle.app_id || null,
200
+ app_key: bundle.app_key || null,
201
+ app_name: bundle.app_name || null,
202
+ environment: bundle.environment || null,
203
+ bundle_status: bundle.status || null,
204
+ install_status: state.installStatus,
205
+ installed_release_id: state.installedReleaseId,
206
+ active_release_id: state.activeReleaseId,
207
+ install_root: installation.install_root || null,
208
+ machine_id: installationMachineId,
209
+ device_hostname: normalizeString(installation.hostname) || null,
210
+ machine_scope: machineScope,
211
+ release_count: state.releases.length,
212
+ status: state.installStatus
213
+ };
214
+ }
215
+
178
216
  function buildEngineeringSummary(graph = {}) {
179
217
  const bundle = graph.bundle || {};
180
218
  const engineeringProject = graph.engineering_project || {};
@@ -381,6 +419,141 @@ async function runAppBundleRegisterCommand(options = {}, dependencies = {}) {
381
419
  return payload;
382
420
  }
383
421
 
422
+ async function runAppCollectionListCommand(options = {}, dependencies = {}) {
423
+ const projectPath = dependencies.projectPath || process.cwd();
424
+ const fileSystem = dependencies.fileSystem || fs;
425
+ const collections = await listAppCollections(projectPath, {
426
+ fileSystem,
427
+ query: options.query,
428
+ status: options.status
429
+ });
430
+ const limit = normalizePositiveInteger(options.limit, 100, 1000);
431
+ const items = collections.slice(0, limit).map((item) => ({
432
+ collection_id: item.collection_id,
433
+ name: item.name,
434
+ description: item.description,
435
+ status: item.status,
436
+ item_count: item.item_count,
437
+ tags: item.tags,
438
+ source_file: item.source_file
439
+ }));
440
+
441
+ const payload = {
442
+ mode: 'app-collection-list',
443
+ generated_at: new Date().toISOString(),
444
+ query: {
445
+ limit,
446
+ status: normalizeString(options.status) || null,
447
+ query: normalizeString(options.query) || null
448
+ },
449
+ summary: {
450
+ total: items.length
451
+ },
452
+ items,
453
+ view_model: {
454
+ type: 'table',
455
+ columns: ['collection_id', 'name', 'status', 'item_count', 'source_file']
456
+ }
457
+ };
458
+ printPayload(payload, options, 'App Collection List');
459
+ return payload;
460
+ }
461
+
462
+ async function runAppCollectionShowCommand(options = {}, dependencies = {}) {
463
+ const collectionRef = normalizeString(options.collection);
464
+ if (!collectionRef) {
465
+ throw new Error('--collection is required');
466
+ }
467
+ const projectPath = dependencies.projectPath || process.cwd();
468
+ const fileSystem = dependencies.fileSystem || fs;
469
+ const collection = await getAppCollection(projectPath, collectionRef, {
470
+ fileSystem
471
+ });
472
+ if (!collection) {
473
+ throw new Error(`app collection not found: ${collectionRef}`);
474
+ }
475
+
476
+ const payload = {
477
+ mode: 'app-collection-show',
478
+ generated_at: new Date().toISOString(),
479
+ query: {
480
+ collection: collectionRef
481
+ },
482
+ summary: {
483
+ collection_id: collection.collection_id,
484
+ name: collection.name,
485
+ status: collection.status,
486
+ item_count: collection.item_count
487
+ },
488
+ collection
489
+ };
490
+ printPayload(payload, options, 'App Collection Show');
491
+ return payload;
492
+ }
493
+
494
+ async function runAppCollectionApplyCommand(options = {}, dependencies = {}) {
495
+ const collectionRef = normalizeString(options.collection);
496
+ if (!collectionRef) {
497
+ throw new Error('--collection is required');
498
+ }
499
+ const projectPath = dependencies.projectPath || process.cwd();
500
+ const fileSystem = dependencies.fileSystem || fs;
501
+ const store = createStore(dependencies);
502
+ const currentDevice = await getCurrentDeviceProfile(projectPath, {
503
+ fileSystem,
504
+ persistIfMissing: false
505
+ });
506
+ const deviceOverride = await loadDeviceOverride(projectPath, { fileSystem });
507
+ const plan = await buildCollectionApplyPlan(projectPath, {
508
+ fileSystem,
509
+ store,
510
+ collectionRef,
511
+ currentDevice,
512
+ deviceOverride
513
+ });
514
+ const execution = options.execute
515
+ ? await executeInstallPlan(plan, {
516
+ store,
517
+ executeInstall: runAppRuntimeInstallCommand,
518
+ executeActivate: runAppRuntimeActivateCommand,
519
+ executeUninstall: runAppRuntimeUninstallCommand,
520
+ dependencies,
521
+ commandOptions: options
522
+ })
523
+ : {
524
+ execute_supported: true,
525
+ executed: false,
526
+ blocked_reason: null,
527
+ results: []
528
+ };
529
+ const payload = {
530
+ mode: 'app-collection-apply',
531
+ generated_at: new Date().toISOString(),
532
+ execute_supported: execution.execute_supported,
533
+ executed: execution.executed,
534
+ execution_blocked_reason: execution.blocked_reason,
535
+ execution: {
536
+ results: execution.results,
537
+ preflight_failures: execution.preflight_failures || []
538
+ },
539
+ current_device: currentDevice,
540
+ device_override: deviceOverride,
541
+ summary: {
542
+ source_type: plan.source.type,
543
+ source_id: plan.source.id,
544
+ desired_app_count: plan.desired_apps.length,
545
+ install_count: plan.counts.install,
546
+ activate_count: plan.counts.activate,
547
+ uninstall_count: plan.counts.uninstall,
548
+ keep_count: plan.counts.keep,
549
+ skip_count: plan.counts.skip
550
+ },
551
+ plan
552
+ };
553
+ printPayload(payload, options, 'App Collection Apply');
554
+ return payload;
555
+ }
556
+
384
557
  async function runAppEngineeringShowCommand(options = {}, dependencies = {}) {
385
558
  const appRef = normalizeString(options.app);
386
559
  if (!appRef) {
@@ -687,6 +860,10 @@ async function runAppRuntimeInstallCommand(options = {}, dependencies = {}) {
687
860
  await ensureAuthorized('app:runtime:install', options, dependencies);
688
861
  const projectPath = dependencies.projectPath || process.cwd();
689
862
  const fileSystem = dependencies.fileSystem || fs;
863
+ const currentDevice = await getCurrentDeviceProfile(projectPath, {
864
+ fileSystem,
865
+ persistIfMissing: true
866
+ });
690
867
  const { store, graph } = await requireAppGraph(appRef, dependencies);
691
868
  const nextPayload = graphToRegisterPayload(graph);
692
869
  const serviceCatalog = nextPayload.metadata && nextPayload.metadata.service_catalog && typeof nextPayload.metadata.service_catalog === 'object'
@@ -705,6 +882,8 @@ async function runAppRuntimeInstallCommand(options = {}, dependencies = {}) {
705
882
  status: 'installed',
706
883
  install_root: installRoot,
707
884
  release_id: releaseId,
885
+ machine_id: currentDevice.device_id,
886
+ hostname: currentDevice.hostname,
708
887
  installed_at: new Date().toISOString(),
709
888
  source: 'sce app runtime install'
710
889
  };
@@ -773,7 +952,12 @@ async function runAppRuntimeUninstallCommand(options = {}, dependencies = {}) {
773
952
  throw new Error('--app is required');
774
953
  }
775
954
  await ensureAuthorized('app:runtime:uninstall', options, dependencies);
955
+ const projectPath = dependencies.projectPath || process.cwd();
776
956
  const fileSystem = dependencies.fileSystem || fs;
957
+ const currentDevice = await getCurrentDeviceProfile(projectPath, {
958
+ fileSystem,
959
+ persistIfMissing: true
960
+ });
777
961
  const { store, graph } = await requireAppGraph(appRef, dependencies);
778
962
  const nextPayload = graphToRegisterPayload(graph);
779
963
  const state = getRuntimeProjectionState(graph);
@@ -800,6 +984,8 @@ async function runAppRuntimeUninstallCommand(options = {}, dependencies = {}) {
800
984
  status: 'not-installed',
801
985
  release_id: null,
802
986
  install_root: null,
987
+ machine_id: currentDevice.device_id,
988
+ hostname: currentDevice.hostname,
803
989
  uninstalled_at: new Date().toISOString(),
804
990
  source: 'sce app runtime uninstall',
805
991
  previous_release_id: targetReleaseId
@@ -819,6 +1005,53 @@ async function runAppRuntimeUninstallCommand(options = {}, dependencies = {}) {
819
1005
  return payload;
820
1006
  }
821
1007
 
1008
+ async function runAppInstallStateListCommand(options = {}, dependencies = {}) {
1009
+ const store = createStore(dependencies);
1010
+ const currentDevice = await getCurrentDeviceProfile(dependencies.projectPath || process.cwd(), {
1011
+ fileSystem: dependencies.fileSystem || fs,
1012
+ persistIfMissing: false
1013
+ });
1014
+ const bundles = await store.listAppBundles({
1015
+ limit: normalizePositiveInteger(options.limit, 100, 1000),
1016
+ status: options.status,
1017
+ environment: options.environment,
1018
+ workspaceId: options.workspaceId,
1019
+ query: options.query
1020
+ });
1021
+ const installStatusFilter = normalizeString(options.installStatus);
1022
+ const items = (Array.isArray(bundles) ? bundles : [])
1023
+ .map((bundle) => buildAppInstallStateItem(bundle, currentDevice))
1024
+ .filter((item) => !installStatusFilter || item.install_status === installStatusFilter);
1025
+
1026
+ const payload = {
1027
+ mode: 'app-install-state-list',
1028
+ generated_at: new Date().toISOString(),
1029
+ query: {
1030
+ limit: normalizePositiveInteger(options.limit, 100, 1000),
1031
+ status: normalizeString(options.status) || null,
1032
+ environment: normalizeString(options.environment) || null,
1033
+ workspace_id: normalizeString(options.workspaceId) || null,
1034
+ query: normalizeString(options.query) || null,
1035
+ install_status: installStatusFilter || null
1036
+ },
1037
+ current_device: currentDevice,
1038
+ summary: {
1039
+ total: items.length,
1040
+ installed_count: items.filter((item) => item.install_status === 'installed').length,
1041
+ not_installed_count: items.filter((item) => item.install_status !== 'installed').length,
1042
+ active_count: items.filter((item) => Boolean(item.active_release_id)).length,
1043
+ current_device_id: currentDevice.device_id
1044
+ },
1045
+ items,
1046
+ view_model: {
1047
+ type: 'table',
1048
+ columns: ['app_id', 'app_key', 'app_name', 'install_status', 'installed_release_id', 'active_release_id', 'machine_scope', 'environment']
1049
+ }
1050
+ };
1051
+ printPayload(payload, options, 'App Install State');
1052
+ return payload;
1053
+ }
1054
+
822
1055
  function safeRun(handler, options = {}, context = 'app command') {
823
1056
  Promise.resolve(handler(options))
824
1057
  .catch((error) => {
@@ -868,6 +1101,34 @@ function registerAppCommands(program) {
868
1101
  .option('--json', 'Print machine-readable JSON output')
869
1102
  .action((options) => safeRun(runAppBundleRegisterCommand, options, 'app bundle register'));
870
1103
 
1104
+ const collection = app
1105
+ .command('collection')
1106
+ .description('Inspect file-backed app collection intent definitions');
1107
+
1108
+ collection
1109
+ .command('list')
1110
+ .description('List app collection definitions from .sce/app/collections')
1111
+ .option('--limit <n>', 'Maximum rows', '100')
1112
+ .option('--status <status>', 'Filter by collection status')
1113
+ .option('--query <text>', 'Free-text query against id/name/description/tags')
1114
+ .option('--json', 'Print machine-readable JSON output')
1115
+ .action((options) => safeRun(runAppCollectionListCommand, options, 'app collection list'));
1116
+
1117
+ collection
1118
+ .command('show')
1119
+ .description('Show one app collection definition')
1120
+ .requiredOption('--collection <id>', 'Collection id or file basename')
1121
+ .option('--json', 'Print machine-readable JSON output')
1122
+ .action((options) => safeRun(runAppCollectionShowCommand, options, 'app collection show'));
1123
+
1124
+ collection
1125
+ .command('apply')
1126
+ .description('Build a plan-first apply diff for one app collection')
1127
+ .requiredOption('--collection <id>', 'Collection id or file basename')
1128
+ .option('--execute', 'Reserved for future explicit execution; currently returns a blocked plan')
1129
+ .option('--json', 'Print machine-readable JSON output')
1130
+ .action((options) => safeRun(runAppCollectionApplyCommand, options, 'app collection apply'));
1131
+
871
1132
  const registry = app
872
1133
  .command('registry')
873
1134
  .description('Manage remote app bundle and service catalog registry configuration');
@@ -965,6 +1226,22 @@ function registerAppCommands(program) {
965
1226
  .option('--json', 'Print machine-readable JSON output')
966
1227
  .action((options) => safeRun(runAppRuntimeUninstallCommand, options, 'app runtime uninstall'));
967
1228
 
1229
+ const installState = app
1230
+ .command('install-state')
1231
+ .description('Inspect current device install state across app bundles');
1232
+
1233
+ installState
1234
+ .command('list')
1235
+ .description('List current device install state across app bundles')
1236
+ .option('--limit <n>', 'Maximum rows', '100')
1237
+ .option('--status <status>', 'Filter by app bundle status')
1238
+ .option('--environment <env>', 'Filter by environment')
1239
+ .option('--workspace-id <id>', 'Filter by workspace id')
1240
+ .option('--query <text>', 'Free-text query against id/key/name')
1241
+ .option('--install-status <status>', 'Filter by install status')
1242
+ .option('--json', 'Print machine-readable JSON output')
1243
+ .action((options) => safeRun(runAppInstallStateListCommand, options, 'app install-state list'));
1244
+
968
1245
  const engineering = app
969
1246
  .command('engineering')
970
1247
  .description('Manage engineering project projection for one app bundle');
@@ -1020,6 +1297,9 @@ module.exports = {
1020
1297
  runAppBundleListCommand,
1021
1298
  runAppBundleShowCommand,
1022
1299
  runAppBundleRegisterCommand,
1300
+ runAppCollectionListCommand,
1301
+ runAppCollectionShowCommand,
1302
+ runAppCollectionApplyCommand,
1023
1303
  runAppRegistryStatusCommand,
1024
1304
  runAppRegistryConfigureCommand,
1025
1305
  runAppRegistrySyncBundlesCommand,
@@ -1030,6 +1310,7 @@ module.exports = {
1030
1310
  runAppRuntimeInstallCommand,
1031
1311
  runAppRuntimeActivateCommand,
1032
1312
  runAppRuntimeUninstallCommand,
1313
+ runAppInstallStateListCommand,
1033
1314
  runAppEngineeringShowCommand,
1034
1315
  runAppEngineeringAttachCommand,
1035
1316
  runAppEngineeringHydrateCommand,
@@ -0,0 +1,194 @@
1
+ const chalk = require('chalk');
2
+ const fs = require('fs-extra');
3
+ const path = require('path');
4
+ const { ensureWriteAuthorization } = require('../security/write-authorization');
5
+ const { getCurrentDeviceProfile } = require('../device/current-device');
6
+ const { loadDeviceOverride, upsertDeviceOverride } = require('../device/device-override-store');
7
+
8
+ function normalizeString(value) {
9
+ if (typeof value !== 'string') {
10
+ return '';
11
+ }
12
+ return value.trim();
13
+ }
14
+
15
+ function printDevicePayload(payload, options = {}) {
16
+ if (options.json) {
17
+ console.log(JSON.stringify(payload, null, 2));
18
+ return;
19
+ }
20
+
21
+ console.log(chalk.blue('Current Device'));
22
+ console.log(` ID: ${payload.summary.device_id}`);
23
+ console.log(` Label: ${payload.summary.label}`);
24
+ console.log(` Hostname: ${payload.summary.hostname}`);
25
+ console.log(` Platform: ${payload.summary.platform}`);
26
+ console.log(` Arch: ${payload.summary.arch}`);
27
+ console.log(` User: ${payload.summary.user}`);
28
+ console.log(` Capability Tags: ${payload.summary.capability_tag_count}`);
29
+ console.log(` Identity Source: ${payload.summary.identity_source}`);
30
+ }
31
+
32
+ function printDeviceOverridePayload(payload, options = {}) {
33
+ if (options.json) {
34
+ console.log(JSON.stringify(payload, null, 2));
35
+ return;
36
+ }
37
+
38
+ console.log(chalk.blue('Device Override'));
39
+ console.log(` Source File: ${payload.summary.source_file || '(not created)'}`);
40
+ console.log(` Removed Apps: ${payload.summary.removed_app_count}`);
41
+ console.log(` Added Apps: ${payload.summary.added_app_count}`);
42
+ }
43
+
44
+ async function ensureAuthorized(action, options = {}, dependencies = {}) {
45
+ const projectPath = dependencies.projectPath || process.cwd();
46
+ const fileSystem = dependencies.fileSystem || fs;
47
+ const env = dependencies.env || process.env;
48
+ await ensureWriteAuthorization(action, {
49
+ authLease: options.authLease,
50
+ authPassword: options.authPassword,
51
+ actor: options.actor
52
+ }, {
53
+ projectPath,
54
+ fileSystem,
55
+ env
56
+ });
57
+ }
58
+
59
+ async function runDeviceCurrentCommand(options = {}, dependencies = {}) {
60
+ const projectPath = dependencies.projectPath || process.cwd();
61
+ const fileSystem = dependencies.fileSystem || fs;
62
+ const device = await getCurrentDeviceProfile(projectPath, {
63
+ fileSystem,
64
+ persistIfMissing: false
65
+ });
66
+
67
+ const payload = {
68
+ mode: 'device-current',
69
+ generated_at: new Date().toISOString(),
70
+ summary: {
71
+ device_id: device.device_id,
72
+ label: device.label,
73
+ hostname: device.hostname,
74
+ platform: device.platform,
75
+ arch: device.arch,
76
+ user: device.user,
77
+ capability_tag_count: Array.isArray(device.capability_tags) ? device.capability_tags.length : 0,
78
+ identity_source: device.identity_source
79
+ },
80
+ device
81
+ };
82
+ printDevicePayload(payload, options);
83
+ return payload;
84
+ }
85
+
86
+ async function runDeviceOverrideShowCommand(options = {}, dependencies = {}) {
87
+ const projectPath = dependencies.projectPath || process.cwd();
88
+ const fileSystem = dependencies.fileSystem || fs;
89
+ const override = await loadDeviceOverride(projectPath, { fileSystem });
90
+ const payload = {
91
+ mode: 'device-override-show',
92
+ generated_at: new Date().toISOString(),
93
+ summary: {
94
+ source_file: override.source_file,
95
+ removed_app_count: Array.isArray(override.removed_apps) ? override.removed_apps.length : 0,
96
+ added_app_count: Array.isArray(override.added_apps) ? override.added_apps.length : 0
97
+ },
98
+ override
99
+ };
100
+ printDeviceOverridePayload(payload, options);
101
+ return payload;
102
+ }
103
+
104
+ async function runDeviceOverrideUpsertCommand(options = {}, dependencies = {}) {
105
+ const inputFile = normalizeString(options.input);
106
+ if (!inputFile) {
107
+ throw new Error('--input is required');
108
+ }
109
+
110
+ await ensureAuthorized('device:override:upsert', options, dependencies);
111
+ const projectPath = dependencies.projectPath || process.cwd();
112
+ const fileSystem = dependencies.fileSystem || fs;
113
+ const resolvedInput = path.isAbsolute(inputFile)
114
+ ? inputFile
115
+ : path.join(projectPath, inputFile);
116
+ const patch = await fileSystem.readJson(resolvedInput);
117
+ const override = await upsertDeviceOverride(projectPath, patch, { fileSystem });
118
+ const payload = {
119
+ mode: 'device-override-upsert',
120
+ success: true,
121
+ generated_at: new Date().toISOString(),
122
+ input_file: resolvedInput,
123
+ summary: {
124
+ source_file: override.source_file,
125
+ removed_app_count: Array.isArray(override.removed_apps) ? override.removed_apps.length : 0,
126
+ added_app_count: Array.isArray(override.added_apps) ? override.added_apps.length : 0
127
+ },
128
+ override
129
+ };
130
+ printDeviceOverridePayload(payload, options);
131
+ return payload;
132
+ }
133
+
134
+ function safeRun(handler, options = {}, context = 'device command') {
135
+ Promise.resolve(handler(options))
136
+ .catch((error) => {
137
+ if (options.json) {
138
+ console.log(JSON.stringify({ success: false, error: error.message }, null, 2));
139
+ } else {
140
+ console.error(chalk.red(`${context} failed:`), error.message);
141
+ }
142
+ process.exitCode = 1;
143
+ });
144
+ }
145
+
146
+ function registerDeviceCommands(program) {
147
+ const device = program
148
+ .command('device')
149
+ .description('Inspect current device identity, capability tags, and local override state');
150
+
151
+ device
152
+ .command('current')
153
+ .description('Show current device identity and capability tags')
154
+ .option('--json', 'Print machine-readable JSON output')
155
+ .action(async (options) => {
156
+ try {
157
+ await runDeviceCurrentCommand(options);
158
+ } catch (error) {
159
+ if (options.json) {
160
+ console.log(JSON.stringify({ success: false, error: error.message }, null, 2));
161
+ } else {
162
+ console.error(chalk.red('device current failed:'), error.message);
163
+ }
164
+ process.exitCode = 1;
165
+ }
166
+ });
167
+
168
+ const override = device
169
+ .command('override')
170
+ .description('Inspect and update local device override policy');
171
+
172
+ override
173
+ .command('show')
174
+ .description('Show current local device override policy')
175
+ .option('--json', 'Print machine-readable JSON output')
176
+ .action((options) => safeRun(runDeviceOverrideShowCommand, options, 'device override show'));
177
+
178
+ override
179
+ .command('upsert')
180
+ .description('Merge local device override fields from JSON input')
181
+ .requiredOption('--input <path>', 'JSON file with override fields to upsert')
182
+ .option('--auth-lease <lease-id>', 'Write authorization lease id')
183
+ .option('--auth-password <password>', 'Inline auth password if policy allows')
184
+ .option('--actor <actor>', 'Audit actor override')
185
+ .option('--json', 'Print machine-readable JSON output')
186
+ .action((options) => safeRun(runDeviceOverrideUpsertCommand, options, 'device override upsert'));
187
+ }
188
+
189
+ module.exports = {
190
+ runDeviceCurrentCommand,
191
+ runDeviceOverrideShowCommand,
192
+ runDeviceOverrideUpsertCommand,
193
+ registerDeviceCommands
194
+ };