@aryacode/dsh-engineering 0.8.3 → 0.8.4

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 CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.8.4] - 2026-10-27
4
+
5
+ ### Added
6
+
7
+ - **`ftview_export` now brings the alarm setup and the tag database.** The vendor export server covers
8
+ displays, global objects and project settings; alarms and tags live beside the application in the
9
+ Studio project store and were missing from a "full" export, so tag bindings had nothing to check
10
+ against. The export now copies `Alarms/Alarms.xml` and `Tags/<application>.csv` into the export in the
11
+ same layout Studio uses, and reports a warning for each when the stored snapshot is older than the
12
+ application database (on the commissioning application both were dated 2026-09-29 against a database
13
+ modified 2026-10-27).
14
+
15
+ ### Documentation
16
+
17
+ - FTView skill, getting-started and the handbook now describe the alarms/tags copy and its staleness
18
+ warning instead of "the vendor export carries no tag CSV".
3
19
  ## [0.8.3] - 2026-10-27
4
20
 
5
21
  ### Documentation
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aryacode/dsh-engineering",
3
- "version": "0.8.3",
3
+ "version": "0.8.4",
4
4
  "description": "AryaCode engineering bundle: one install surface for the Studio 5000 L5X, FactoryTalk View ME, MotionWorks IEC and FactoryTalk Optix surfaces, one row per product, with the ladder and display previews.",
5
5
  "keywords": [
6
6
  "dsh",
@@ -77,7 +77,10 @@ Start from the application, not from a folder someone emailed:
77
77
 
78
78
  1. `ftview_apps` lists the ME applications installed on this host.
79
79
  2. `ftview_export` writes one to a NEW folder through the vendor export server. No Studio window is
80
- needed and the application is not modified.
80
+ needed and the application is not modified. It also copies the alarm setup (`Alarms/Alarms.xml`) and
81
+ the tag CSV (`Tags/<application>.csv`) from the Studio project store, and reports a `warning` for
82
+ either when the stored file is older than the application database — say so rather than treating
83
+ those tags or alarms as current.
81
84
  3. `ftview_open` that folder, then author against it:
82
85
  - `ftview_draft_display_create` for a new display;
83
86
  - `ftview_draft_object_add` for one object (`text`, `panel`, `momentaryButton`, `numericDisplay`,
@@ -64,10 +64,13 @@ export function vendorTools(fs) {
64
64
  if (exported.error !== undefined) {
65
65
  throw new EngineeringError('VENDOR_EXPORT_FAILED', exported.error, 'Check that the application is not open in Studio, then retry with a new output_root.');
66
66
  }
67
- const files = exported.value ?? [];
67
+ const files = exported.value?.files ?? [];
68
+ const warnings = exported.value?.warnings ?? [];
68
69
  const displays = files.filter(file => /\/Displays\/.+\.xml$/iu.test(file) && !/batchimport/iu.test(file));
69
70
  const manifests = files.filter(file => /batchimport/iu.test(file));
70
- return success(`Exported ${application}: ${files.length} file(s), ${displays.length} display(s)${manifests.length === 0 ? '' : `, ${manifests.length} import manifest(s)`}.`, { application, output_root: destination, files, display_count: displays.length, manifests }, [{ tool: 'ftview_open', args: { root: destination }, why: 'Open the fresh export and author against it.' }], VENDOR_COVERAGE);
71
+ const alarms = files.filter(file => /^Alarms\//iu.test(file));
72
+ const tags = files.filter(file => /^Tags\/.+\.csv$/iu.test(file));
73
+ return success(`Exported ${application}: ${files.length} file(s), ${displays.length} display(s)${alarms.length === 0 ? ', no alarms' : ', alarms'}${tags.length === 0 ? ', no tags CSV' : ', tags CSV'}${manifests.length === 0 ? '' : `, ${manifests.length} import manifest(s)`}${warnings.length === 0 ? '' : `; ${warnings.length} warning(s)`}.`, { application, output_root: destination, files, display_count: displays.length, alarms, tags, manifests, warnings }, [{ tool: 'ftview_open', args: { root: destination }, why: 'Open the fresh export and author against it.' }], VENDOR_COVERAGE);
71
74
  },
72
75
  }),
73
76
  ];
@@ -37,13 +37,53 @@ export interface VendorResult<T> {
37
37
  */
38
38
  export declare function listApplications(): Promise<VendorResult<readonly MeApplication[]>>;
39
39
  /**
40
- * Export one application's displays and globals into a new folder.
40
+ * Export one application's displays, globals, alarms and tags into a new folder.
41
+ *
42
+ * The export server covers displays, global objects and project settings. The
43
+ * alarm setup and the tag database live beside the application in the Studio
44
+ * project store, and `ExportAll` does not copy them, so this function does:
45
+ * without them a "full" export has no tags to bind and no alarms to review.
46
+ *
47
+ * Those two files are snapshots Studio writes when it exports them from their
48
+ * editors, not live views of the database, so each copy is checked against the
49
+ * application database's timestamp and a stale one is reported rather than
50
+ * silently handed over as current.
41
51
  *
42
52
  * @param application - Application name as listed.
43
53
  * @param destination - Folder to create and fill.
44
- * @returns Files written, relative to the destination.
54
+ * @param documentsRoot - Public Documents folder, injectable for tests.
55
+ * @returns Files written and any freshness warnings.
56
+ */
57
+ export declare function exportApplication(application: string, destination: string, documentsRoot?: string): Promise<VendorResult<ExportResult>>;
58
+ /** One export run's files and freshness warnings. */
59
+ export interface ExportResult {
60
+ /** Files written, relative to the destination. */
61
+ readonly files: readonly string[];
62
+ /** Components that are missing or older than the application database. */
63
+ readonly warnings: readonly string[];
64
+ }
65
+ /** What the project store contributed to an export. */
66
+ export interface ProjectExtras {
67
+ /** Export-relative paths copied from the store. */
68
+ readonly copied: readonly string[];
69
+ /** Why a component is missing or possibly stale. */
70
+ readonly warnings: readonly string[];
71
+ }
72
+ /** The Studio project store that holds one application's alarms and tags. */
73
+ export declare function projectFolder(application: string, documentsRoot?: string): string | undefined;
74
+ /**
75
+ * Copy the alarm setup and the tag CSV from the project store into an export.
76
+ *
77
+ * The file names and layout match what Studio writes when it exports the whole
78
+ * application (`Alarms/Alarms.xml`, `Tags/<application>.csv`), so a reader
79
+ * cannot tell this export from a hand-made one.
80
+ *
81
+ * @param application - Application name.
82
+ * @param destination - Export folder that already exists.
83
+ * @param documentsRoot - Public Documents folder, injectable for tests.
84
+ * @returns Paths copied and any staleness warnings.
45
85
  */
46
- export declare function exportApplication(application: string, destination: string): Promise<VendorResult<readonly string[]>>;
86
+ export declare function copyProjectExtras(application: string, destination: string, documentsRoot?: string): ProjectExtras;
47
87
  /**
48
88
  * Back one application up to a zip.
49
89
  *
@@ -11,7 +11,7 @@
11
11
  * @module dsh-engineering-ftview/vendor
12
12
  */
13
13
  import { spawn } from 'node:child_process';
14
- import { existsSync, readdirSync, statSync } from 'node:fs';
14
+ import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from 'node:fs';
15
15
  import { join } from 'node:path';
16
16
  /** CLSID of the ME export server (`MEExport.exe`). */
17
17
  export const EXPORT_CLSID = '9d882577-aa92-466b-a749-6d6b5093b623';
@@ -88,13 +88,24 @@ export async function listApplications() {
88
88
  }
89
89
  }
90
90
  /**
91
- * Export one application's displays and globals into a new folder.
91
+ * Export one application's displays, globals, alarms and tags into a new folder.
92
+ *
93
+ * The export server covers displays, global objects and project settings. The
94
+ * alarm setup and the tag database live beside the application in the Studio
95
+ * project store, and `ExportAll` does not copy them, so this function does:
96
+ * without them a "full" export has no tags to bind and no alarms to review.
97
+ *
98
+ * Those two files are snapshots Studio writes when it exports them from their
99
+ * editors, not live views of the database, so each copy is checked against the
100
+ * application database's timestamp and a stale one is reported rather than
101
+ * silently handed over as current.
92
102
  *
93
103
  * @param application - Application name as listed.
94
104
  * @param destination - Folder to create and fill.
95
- * @returns Files written, relative to the destination.
105
+ * @param documentsRoot - Public Documents folder, injectable for tests.
106
+ * @returns Files written and any freshness warnings.
96
107
  */
97
- export async function exportApplication(application, destination) {
108
+ export async function exportApplication(application, destination, documentsRoot) {
98
109
  if (existsSync(destination))
99
110
  return { error: `A file or folder already exists at ${destination}. Export into a new folder.` };
100
111
  const result = await runPowerShell('powershell.exe', EXPORT_SCRIPT, { clsid: EXPORT_CLSID, app: application, dest: destination });
@@ -102,6 +113,7 @@ export async function exportApplication(application, destination) {
102
113
  return { error: `Export failed: ${result.stderr.trim() || `exit ${result.code}`}` };
103
114
  if (!existsSync(destination))
104
115
  return { error: 'The engine reported success but wrote no folder.' };
116
+ const extras = copyProjectExtras(application, destination, documentsRoot);
105
117
  const files = [];
106
118
  const walk = (directory, prefix) => {
107
119
  for (const entry of readdirSync(directory, { withFileTypes: true })) {
@@ -113,7 +125,85 @@ export async function exportApplication(application, destination) {
113
125
  }
114
126
  };
115
127
  walk(destination, '');
116
- return { value: files.sort() };
128
+ return { value: { files: files.sort(), warnings: extras.warnings } };
129
+ }
130
+ /** The Studio project store that holds one application's alarms and tags. */
131
+ export function projectFolder(application, documentsRoot = join(process.env.PUBLIC ?? 'C:/Users/Public', 'Documents')) {
132
+ const folder = join(documentsRoot, 'RSView Enterprise/ME/HMI projects', application);
133
+ return existsSync(folder) ? folder : undefined;
134
+ }
135
+ /** A readable date for one file's timestamp. */
136
+ function modifiedOn(path) {
137
+ try {
138
+ return statSync(path).mtime.toISOString().slice(0, 10);
139
+ }
140
+ catch {
141
+ return 'unknown';
142
+ }
143
+ }
144
+ /** A warning when one copied snapshot predates the application database. */
145
+ function freshness(file, relative, database) {
146
+ if (database === undefined)
147
+ return undefined;
148
+ try {
149
+ if (statSync(file).mtimeMs + 60_000 >= statSync(database).mtimeMs)
150
+ return undefined;
151
+ return `${relative} was written ${modifiedOn(file)}, older than the application database (${modifiedOn(database)}); it may not reflect recent changes. Studio refreshes it on its next full export.`;
152
+ }
153
+ catch {
154
+ return undefined;
155
+ }
156
+ }
157
+ /**
158
+ * Copy the alarm setup and the tag CSV from the project store into an export.
159
+ *
160
+ * The file names and layout match what Studio writes when it exports the whole
161
+ * application (`Alarms/Alarms.xml`, `Tags/<application>.csv`), so a reader
162
+ * cannot tell this export from a hand-made one.
163
+ *
164
+ * @param application - Application name.
165
+ * @param destination - Export folder that already exists.
166
+ * @param documentsRoot - Public Documents folder, injectable for tests.
167
+ * @returns Paths copied and any staleness warnings.
168
+ */
169
+ export function copyProjectExtras(application, destination, documentsRoot) {
170
+ const source = projectFolder(application, documentsRoot);
171
+ if (source === undefined) {
172
+ return { copied: [], warnings: [`No Studio project folder was found for ${application}, so alarms and tags were not exported.`] };
173
+ }
174
+ const database = join(source, `${application}.med`);
175
+ const copied = [];
176
+ const warnings = [];
177
+ const alarms = join(source, 'Alarms.xml');
178
+ if (existsSync(alarms)) {
179
+ const target = join(destination, 'Alarms');
180
+ mkdirSync(target, { recursive: true });
181
+ copyFileSync(alarms, join(target, 'Alarms.xml'));
182
+ copied.push('Alarms/Alarms.xml');
183
+ const warning = freshness(alarms, 'Alarms/Alarms.xml', existsSync(database) ? database : undefined);
184
+ if (warning !== undefined)
185
+ warnings.push(warning);
186
+ }
187
+ else {
188
+ warnings.push('No Alarms.xml in the application store, so alarms were not exported. Export alarm setup from Studio once.');
189
+ }
190
+ // Studio names the tag file `<application>-Tags.CSV`; match the suffix
191
+ // case-insensitively because older applications used `.csv`.
192
+ const entries = readdirSync(source, { withFileTypes: true });
193
+ const tags = entries.find(entry => entry.isFile() && /-tags\.csv$/iu.test(entry.name));
194
+ if (tags !== undefined) {
195
+ const target = join(destination, 'Tags');
196
+ mkdirSync(target, { recursive: true });
197
+ copyFileSync(join(source, tags.name), join(target, `${application}.csv`));
198
+ copied.push(`Tags/${application}.csv`);
199
+ const warning = freshness(join(source, tags.name), `Tags/${application}.csv`, existsSync(database) ? database : undefined);
200
+ if (warning !== undefined)
201
+ warnings.push(warning);
202
+ }
203
+ else {
204
+ warnings.push('No tag CSV in the application store, so tags were not exported. Export the tag database from Studio once.');
205
+ }
206
+ return { copied, warnings };
117
207
  }
118
208
  /**
119
209
  * Back one application up to a zip.