@aryacode/dsh-engineering 0.7.0 → 0.8.0
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 +25 -0
- package/README.md +3 -2
- package/package.json +1 -1
- package/vendor/ftview/SKILL.md +34 -5
- package/vendor/ftview/lib/display-tree.d.ts +99 -0
- package/vendor/ftview/lib/display-tree.js +306 -0
- package/vendor/ftview/lib/edit/operations.d.ts +85 -1
- package/vendor/ftview/lib/edit/operations.js +201 -0
- package/vendor/ftview/lib/edit/prepare.d.ts +3 -0
- package/vendor/ftview/lib/edit/prepare.js +35 -1
- package/vendor/ftview/lib/edit/tools.js +249 -5
- package/vendor/ftview/lib/handoff.d.ts +58 -0
- package/vendor/ftview/lib/handoff.js +95 -0
- package/vendor/ftview/lib/index.d.ts +1 -0
- package/vendor/ftview/lib/index.js +3 -1
- package/vendor/ftview/lib/schema.d.ts +68 -0
- package/vendor/ftview/lib/schema.js +156 -0
- package/vendor/ftview/lib/vendor-tools.d.ts +17 -0
- package/vendor/ftview/lib/vendor-tools.js +74 -0
- package/vendor/ftview/lib/vendor.d.ts +56 -0
- package/vendor/ftview/lib/vendor.js +143 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drive the installed FactoryTalk View ME engine.
|
|
3
|
+
*
|
|
4
|
+
* Studio ships two scriptable COM servers that matter here: `MEExportSvr`
|
|
5
|
+
* (list applications, export displays) and `AppMgrServer` (backup and restore).
|
|
6
|
+
* The export server runs out of process and works from a 64-bit host; the
|
|
7
|
+
* application manager is a 32-bit in-process server, so it is reached through
|
|
8
|
+
* the 32-bit PowerShell. Nothing here edits a project: export reads, backup
|
|
9
|
+
* copies.
|
|
10
|
+
*
|
|
11
|
+
* @module dsh-engineering-ftview/vendor
|
|
12
|
+
*/
|
|
13
|
+
import { spawn } from 'node:child_process';
|
|
14
|
+
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
/** CLSID of the ME export server (`MEExport.exe`). */
|
|
17
|
+
export const EXPORT_CLSID = '9d882577-aa92-466b-a749-6d6b5093b623';
|
|
18
|
+
/** CLSID of the application manager (`ApplicationManagerServer.dll`). */
|
|
19
|
+
export const APP_MANAGER_CLSID = '4fead81a-cd72-42da-b659-f1a41499f0f9';
|
|
20
|
+
/** Where the 32-bit PowerShell lives on this host. */
|
|
21
|
+
function powershell32() {
|
|
22
|
+
const path = join(process.env.WINDIR ?? 'C:/Windows', 'SysWOW64/WindowsPowerShell/v1.0/powershell.exe');
|
|
23
|
+
return existsSync(path) ? path : 'powershell.exe';
|
|
24
|
+
}
|
|
25
|
+
/** Run a PowerShell script with a JSON payload on stdin. */
|
|
26
|
+
function runPowerShell(executable, script, payload) {
|
|
27
|
+
const encoded = Buffer.from(script, 'utf16le').toString('base64');
|
|
28
|
+
return new Promise(resolvePromise => {
|
|
29
|
+
const child = spawn(executable, ['-NoProfile', '-NonInteractive', '-EncodedCommand', encoded], { windowsHide: true });
|
|
30
|
+
let stdout = '';
|
|
31
|
+
let stderr = '';
|
|
32
|
+
child.stdout?.setEncoding('utf8').on('data', chunk => { stdout += chunk; });
|
|
33
|
+
child.stderr?.setEncoding('utf8').on('data', chunk => { stderr += chunk; });
|
|
34
|
+
child.on('error', error => resolvePromise({ code: -1, stdout, stderr: stderr || error.message }));
|
|
35
|
+
child.on('close', code => resolvePromise({ code: code ?? -1, stdout, stderr }));
|
|
36
|
+
child.stdin?.end(JSON.stringify(payload), 'utf8');
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
/** Script that lists applications through the export server. */
|
|
40
|
+
const LIST_SCRIPT = `
|
|
41
|
+
$ErrorActionPreference = 'Stop'
|
|
42
|
+
$p = [Console]::In.ReadToEnd() | ConvertFrom-Json
|
|
43
|
+
$t = [Type]::GetTypeFromCLSID([guid]$p.clsid)
|
|
44
|
+
$o = [Activator]::CreateInstance($t)
|
|
45
|
+
$names = @($o.GetMEAppNames())
|
|
46
|
+
$apps = @()
|
|
47
|
+
foreach ($name in $names) {
|
|
48
|
+
$platform = $null; $version = $null
|
|
49
|
+
try { $info = @($o.GetAppInfo([string]$name)); if ($info.Count -gt 0) { $platform = [string]$info[0] }; if ($info.Count -gt 1) { $version = [string]$info[1] } } catch { }
|
|
50
|
+
$apps += [pscustomobject]@{ name = [string]$name; platform = $platform; version = $version }
|
|
51
|
+
}
|
|
52
|
+
[pscustomobject]@{ apps = $apps } | ConvertTo-Json -Depth 4 -Compress
|
|
53
|
+
`;
|
|
54
|
+
/** Script that exports one application's displays. */
|
|
55
|
+
const EXPORT_SCRIPT = `
|
|
56
|
+
$ErrorActionPreference = 'Stop'
|
|
57
|
+
$p = [Console]::In.ReadToEnd() | ConvertFrom-Json
|
|
58
|
+
$t = [Type]::GetTypeFromCLSID([guid]$p.clsid)
|
|
59
|
+
$o = [Activator]::CreateInstance($t)
|
|
60
|
+
$o.LoadMEApp([string]$p.app)
|
|
61
|
+
try { $o.ExportAll([string]$p.dest) } finally { $o.UnloadMEApp() }
|
|
62
|
+
[pscustomobject]@{ ok = $true } | ConvertTo-Json -Compress
|
|
63
|
+
`;
|
|
64
|
+
/** Script that backs one application up to a zip through the application manager. */
|
|
65
|
+
const BACKUP_SCRIPT = `
|
|
66
|
+
$ErrorActionPreference = 'Stop'
|
|
67
|
+
$p = [Console]::In.ReadToEnd() | ConvertFrom-Json
|
|
68
|
+
$t = [Type]::GetTypeFromCLSID([guid]$p.clsid)
|
|
69
|
+
$o = [Activator]::CreateInstance($t)
|
|
70
|
+
$o.BackupLocal([string]$p.app, [string]$p.zip, '', 'ME')
|
|
71
|
+
[pscustomobject]@{ ok = $true } | ConvertTo-Json -Compress
|
|
72
|
+
`;
|
|
73
|
+
/**
|
|
74
|
+
* List every ME application the engine knows.
|
|
75
|
+
*
|
|
76
|
+
* @returns Applications, or the reason the engine could not be reached.
|
|
77
|
+
*/
|
|
78
|
+
export async function listApplications() {
|
|
79
|
+
const result = await runPowerShell('powershell.exe', LIST_SCRIPT, { clsid: EXPORT_CLSID });
|
|
80
|
+
if (result.code !== 0)
|
|
81
|
+
return { error: `The ME export server is unavailable: ${result.stderr.trim() || `exit ${result.code}`}` };
|
|
82
|
+
try {
|
|
83
|
+
const parsed = JSON.parse(result.stdout);
|
|
84
|
+
return { value: parsed.apps ?? [] };
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return { error: 'The ME export server returned an unreadable answer.' };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Export one application's displays and globals into a new folder.
|
|
92
|
+
*
|
|
93
|
+
* @param application - Application name as listed.
|
|
94
|
+
* @param destination - Folder to create and fill.
|
|
95
|
+
* @returns Files written, relative to the destination.
|
|
96
|
+
*/
|
|
97
|
+
export async function exportApplication(application, destination) {
|
|
98
|
+
if (existsSync(destination))
|
|
99
|
+
return { error: `A file or folder already exists at ${destination}. Export into a new folder.` };
|
|
100
|
+
const result = await runPowerShell('powershell.exe', EXPORT_SCRIPT, { clsid: EXPORT_CLSID, app: application, dest: destination });
|
|
101
|
+
if (result.code !== 0)
|
|
102
|
+
return { error: `Export failed: ${result.stderr.trim() || `exit ${result.code}`}` };
|
|
103
|
+
if (!existsSync(destination))
|
|
104
|
+
return { error: 'The engine reported success but wrote no folder.' };
|
|
105
|
+
const files = [];
|
|
106
|
+
const walk = (directory, prefix) => {
|
|
107
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
108
|
+
const relative = prefix === '' ? entry.name : `${prefix}/${entry.name}`;
|
|
109
|
+
if (entry.isDirectory())
|
|
110
|
+
walk(join(directory, entry.name), relative);
|
|
111
|
+
else
|
|
112
|
+
files.push(relative);
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
walk(destination, '');
|
|
116
|
+
return { value: files.sort() };
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Back one application up to a zip.
|
|
120
|
+
*
|
|
121
|
+
* @param application - Application name.
|
|
122
|
+
* @param zipPath - Destination zip; must not exist.
|
|
123
|
+
* @returns The zip path on success.
|
|
124
|
+
*/
|
|
125
|
+
export async function backupApplication(application, zipPath) {
|
|
126
|
+
if (existsSync(zipPath))
|
|
127
|
+
return { error: `A file already exists at ${zipPath}.` };
|
|
128
|
+
const result = await runPowerShell(powershell32(), BACKUP_SCRIPT, { clsid: APP_MANAGER_CLSID, app: application, zip: zipPath });
|
|
129
|
+
if (result.code !== 0)
|
|
130
|
+
return { error: `Backup failed: ${result.stderr.trim() || `exit ${result.code}`}` };
|
|
131
|
+
if (!existsSync(zipPath))
|
|
132
|
+
return { error: 'The application manager reported success but wrote no zip.' };
|
|
133
|
+
return { value: zipPath };
|
|
134
|
+
}
|
|
135
|
+
/** Size of one file, for reports. */
|
|
136
|
+
export function fileSize(path) {
|
|
137
|
+
try {
|
|
138
|
+
return statSync(path).size;
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return 0;
|
|
142
|
+
}
|
|
143
|
+
}
|