@aryacode/dsh-engineering 0.8.2 → 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 +30 -0
- package/README.md +7 -0
- package/package.json +1 -1
- package/vendor/ftview/SKILL.md +4 -1
- package/vendor/ftview/lib/vendor-tools.js +5 -2
- package/vendor/ftview/lib/vendor.d.ts +43 -3
- package/vendor/ftview/lib/vendor.js +95 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
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".
|
|
19
|
+
## [0.8.3] - 2026-10-27
|
|
20
|
+
|
|
21
|
+
### Documentation
|
|
22
|
+
|
|
23
|
+
- **A workflow handbook, generated from one source.** `docs/handbook/Engineering-Handbook.pdf` (7 pages),
|
|
24
|
+
an editable `.docx` and a printable `.html` cover the four products: what each row needs, the shared
|
|
25
|
+
read → act → verify → hand-off loop, what the agent does, what stays human, and a "what proves what"
|
|
26
|
+
table.
|
|
27
|
+
- **Six workflow diagrams** in `docs/diagrams/` (SVG masters and PNG): the shared loop and one per
|
|
28
|
+
product, with agent steps, human steps and proof steps visually distinguished. They are embedded in
|
|
29
|
+
`docs/getting-started.md` and in the handbook.
|
|
30
|
+
- The diagrams and handbook are **generated**, not hand-drawn: `scripts/gen-diagrams.py` owns the layout
|
|
31
|
+
and `scripts/gen-handbook.py` the prose, so a changed workflow is one edit and one regeneration.
|
|
32
|
+
- README: the handbook in the documentation table and the generators in the Development commands.
|
|
3
33
|
## [0.8.2] - 2026-10-27
|
|
4
34
|
|
|
5
35
|
### Documentation
|
package/README.md
CHANGED
|
@@ -238,6 +238,7 @@ even when the implementation behind a row is replaced.
|
|
|
238
238
|
| Document | Owns |
|
|
239
239
|
|---|---|
|
|
240
240
|
| [Getting started](docs/getting-started.md) | The first session: what each row needs, the shared loop, and a paste-ready prompt per product |
|
|
241
|
+
| [Workflow handbook (PDF)](docs/handbook/Engineering-Handbook.pdf) | The whole surface as a printable handbook with the four workflow diagrams; [editable DOCX](docs/handbook/Engineering-Handbook.docx) and [HTML](docs/handbook/Engineering-Handbook.html) beside it |
|
|
241
242
|
| [Tool reference](docs/reference/tools.md) | Every tool, parameter and description (generated) |
|
|
242
243
|
| [Examples](docs/examples.md) | End-to-end sessions per product |
|
|
243
244
|
| [Architecture](docs/architecture.md) | Composition, row model, configuration, client faces, packaging |
|
|
@@ -256,8 +257,14 @@ pnpm build:client # regenerate lib/client.js from the committed face bundles
|
|
|
256
257
|
pnpm test # patch, docs, tool-reference, manifest and client-face checks
|
|
257
258
|
pnpm docs:tools # regenerate the tool reference from the registered definitions
|
|
258
259
|
pnpm docs:check # fail if the committed reference has drifted
|
|
260
|
+
python scripts/gen-diagrams.py # regenerate docs/diagrams/*.svg and *.png
|
|
261
|
+
python scripts/gen-handbook.py # rebuild the PDF, DOCX and HTML handbook from those figures
|
|
259
262
|
```
|
|
260
263
|
|
|
264
|
+
The diagrams and the handbook are generated, not hand-drawn: layout lives in
|
|
265
|
+
`scripts/gen-diagrams.py` and the prose in `scripts/gen-handbook.py`, so a changed workflow is one edit
|
|
266
|
+
and one regeneration. Both need Pillow; the handbook also needs python-docx and reportlab.
|
|
267
|
+
|
|
261
268
|
Tests run through `tsx` so they can read the TypeScript tool factories. A registered tool that no
|
|
262
269
|
skill governs fails the suite — the policy is a test, not a convention.
|
|
263
270
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aryacode/dsh-engineering",
|
|
3
|
-
"version": "0.8.
|
|
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",
|
package/vendor/ftview/SKILL.md
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
* @
|
|
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
|
|
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
|
|
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
|
-
* @
|
|
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.
|