@aryacode/dsh-engineering 0.0.0-stage → 0.3.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 +115 -0
- package/LICENSE +21 -0
- package/README.md +228 -2
- package/cordis.patch.yml +122 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +77 -3
- package/vendor/ftview/README.md +55 -0
- package/vendor/ftview/SKILL.md +74 -0
- package/vendor/ftview/lib/checks.d.ts +27 -0
- package/vendor/ftview/lib/checks.js +39 -0
- package/vendor/ftview/lib/core.d.ts +208 -0
- package/vendor/ftview/lib/core.js +160 -0
- package/vendor/ftview/lib/edit/drafts.d.ts +49 -0
- package/vendor/ftview/lib/edit/drafts.js +84 -0
- package/vendor/ftview/lib/edit/operations.d.ts +62 -0
- package/vendor/ftview/lib/edit/operations.js +126 -0
- package/vendor/ftview/lib/edit/prepare.d.ts +38 -0
- package/vendor/ftview/lib/edit/prepare.js +133 -0
- package/vendor/ftview/lib/edit/tools.d.ts +25 -0
- package/vendor/ftview/lib/edit/tools.js +198 -0
- package/vendor/ftview/lib/edit.d.ts +30 -0
- package/vendor/ftview/lib/edit.js +38 -0
- package/vendor/ftview/lib/index.d.ts +31 -0
- package/vendor/ftview/lib/index.js +36 -0
- package/vendor/ftview/lib/model.d.ts +80 -0
- package/vendor/ftview/lib/model.js +267 -0
- package/vendor/ftview/lib/session.d.ts +48 -0
- package/vendor/ftview/lib/session.js +98 -0
- package/vendor/ftview/lib/skill.d.ts +29 -0
- package/vendor/ftview/lib/skill.js +43 -0
- package/vendor/ftview/lib/studio/discover.d.ts +36 -0
- package/vendor/ftview/lib/studio/discover.js +87 -0
- package/vendor/ftview/lib/studio/preflight.d.ts +51 -0
- package/vendor/ftview/lib/studio/preflight.js +64 -0
- package/vendor/ftview/lib/studio/tools.d.ts +15 -0
- package/vendor/ftview/lib/studio/tools.js +83 -0
- package/vendor/ftview/lib/studio.d.ts +30 -0
- package/vendor/ftview/lib/studio.js +37 -0
- package/vendor/ftview/lib/tools.d.ts +14 -0
- package/vendor/ftview/lib/tools.js +202 -0
- package/vendor/l5x/README.md +62 -0
- package/vendor/l5x/SKILL.md +81 -0
- package/vendor/l5x/lib/approvals.d.ts +50 -0
- package/vendor/l5x/lib/approvals.js +93 -0
- package/vendor/l5x/lib/client.js +412 -0
- package/vendor/l5x/lib/client.js.map +1 -0
- package/vendor/l5x/lib/core/errors.d.ts +38 -0
- package/vendor/l5x/lib/core/errors.js +48 -0
- package/vendor/l5x/lib/core/params.d.ts +49 -0
- package/vendor/l5x/lib/core/params.js +39 -0
- package/vendor/l5x/lib/core/result.d.ts +46 -0
- package/vendor/l5x/lib/core/result.js +21 -0
- package/vendor/l5x/lib/core/tool.d.ts +102 -0
- package/vendor/l5x/lib/core/tool.js +19 -0
- package/vendor/l5x/lib/drafts.d.ts +66 -0
- package/vendor/l5x/lib/drafts.js +98 -0
- package/vendor/l5x/lib/editing.d.ts +30 -0
- package/vendor/l5x/lib/editing.js +42 -0
- package/vendor/l5x/lib/engine/acd-edit.d.ts +13 -0
- package/vendor/l5x/lib/engine/acd-edit.js +115 -0
- package/vendor/l5x/lib/engine/analysis.d.ts +22 -0
- package/vendor/l5x/lib/engine/analysis.js +258 -0
- package/vendor/l5x/lib/engine/capabilities.d.ts +9 -0
- package/vendor/l5x/lib/engine/capabilities.js +40 -0
- package/vendor/l5x/lib/engine/change-set.d.ts +122 -0
- package/vendor/l5x/lib/engine/change-set.js +446 -0
- package/vendor/l5x/lib/engine/configuration.d.ts +21 -0
- package/vendor/l5x/lib/engine/configuration.js +42 -0
- package/vendor/l5x/lib/engine/details.d.ts +47 -0
- package/vendor/l5x/lib/engine/details.js +173 -0
- package/vendor/l5x/lib/engine/direct-polling.d.ts +91 -0
- package/vendor/l5x/lib/engine/direct-polling.js +193 -0
- package/vendor/l5x/lib/engine/download-review.d.ts +15 -0
- package/vendor/l5x/lib/engine/download-review.js +63 -0
- package/vendor/l5x/lib/engine/logic-syntax.d.ts +60 -0
- package/vendor/l5x/lib/engine/logic-syntax.js +237 -0
- package/vendor/l5x/lib/engine/observations.d.ts +41 -0
- package/vendor/l5x/lib/engine/observations.js +67 -0
- package/vendor/l5x/lib/engine/project-analysis.d.ts +131 -0
- package/vendor/l5x/lib/engine/project-analysis.js +314 -0
- package/vendor/l5x/lib/engine/review-evidence.d.ts +8 -0
- package/vendor/l5x/lib/engine/review-evidence.js +49 -0
- package/vendor/l5x/lib/engine/review.d.ts +18 -0
- package/vendor/l5x/lib/engine/review.js +123 -0
- package/vendor/l5x/lib/engine/studio-sdk.d.ts +101 -0
- package/vendor/l5x/lib/engine/studio-sdk.js +571 -0
- package/vendor/l5x/lib/engine/tag-resolution.d.ts +20 -0
- package/vendor/l5x/lib/engine/tag-resolution.js +169 -0
- package/vendor/l5x/lib/engine/usage.d.ts +16 -0
- package/vendor/l5x/lib/engine/usage.js +92 -0
- package/vendor/l5x/lib/engine/xml-regions.d.ts +23 -0
- package/vendor/l5x/lib/engine/xml-regions.js +77 -0
- package/vendor/l5x/lib/fs.d.ts +17 -0
- package/vendor/l5x/lib/fs.js +36 -0
- package/vendor/l5x/lib/index.d.ts +36 -0
- package/vendor/l5x/lib/index.js +42 -0
- package/vendor/l5x/lib/live.d.ts +48 -0
- package/vendor/l5x/lib/live.js +48 -0
- package/vendor/l5x/lib/model/aois.d.ts +39 -0
- package/vendor/l5x/lib/model/aois.js +112 -0
- package/vendor/l5x/lib/model/errors.d.ts +15 -0
- package/vendor/l5x/lib/model/errors.js +18 -0
- package/vendor/l5x/lib/model/index.d.ts +20 -0
- package/vendor/l5x/lib/model/index.js +19 -0
- package/vendor/l5x/lib/model/module.d.ts +37 -0
- package/vendor/l5x/lib/model/module.js +54 -0
- package/vendor/l5x/lib/model/modules.d.ts +24 -0
- package/vendor/l5x/lib/model/modules.js +55 -0
- package/vendor/l5x/lib/model/parse.d.ts +32 -0
- package/vendor/l5x/lib/model/parse.js +116 -0
- package/vendor/l5x/lib/model/project.d.ts +15 -0
- package/vendor/l5x/lib/model/project.js +31 -0
- package/vendor/l5x/lib/model/routines.d.ts +71 -0
- package/vendor/l5x/lib/model/routines.js +183 -0
- package/vendor/l5x/lib/model/tags.d.ts +51 -0
- package/vendor/l5x/lib/model/tags.js +119 -0
- package/vendor/l5x/lib/model/types.d.ts +304 -0
- package/vendor/l5x/lib/model/types.js +12 -0
- package/vendor/l5x/lib/model/udts.d.ts +34 -0
- package/vendor/l5x/lib/model/udts.js +81 -0
- package/vendor/l5x/lib/model/xml.d.ts +139 -0
- package/vendor/l5x/lib/model/xml.js +291 -0
- package/vendor/l5x/lib/model/xref.d.ts +28 -0
- package/vendor/l5x/lib/model/xref.js +154 -0
- package/vendor/l5x/lib/session.d.ts +119 -0
- package/vendor/l5x/lib/session.js +236 -0
- package/vendor/l5x/lib/skill.d.ts +30 -0
- package/vendor/l5x/lib/skill.js +44 -0
- package/vendor/l5x/lib/studio-state.d.ts +77 -0
- package/vendor/l5x/lib/studio-state.js +79 -0
- package/vendor/l5x/lib/studio.d.ts +29 -0
- package/vendor/l5x/lib/studio.js +40 -0
- package/vendor/l5x/lib/tools/checks.d.ts +19 -0
- package/vendor/l5x/lib/tools/checks.js +140 -0
- package/vendor/l5x/lib/tools/common.d.ts +41 -0
- package/vendor/l5x/lib/tools/common.js +84 -0
- package/vendor/l5x/lib/tools/controller.d.ts +21 -0
- package/vendor/l5x/lib/tools/controller.js +170 -0
- package/vendor/l5x/lib/tools/drafts.d.ts +23 -0
- package/vendor/l5x/lib/tools/drafts.js +270 -0
- package/vendor/l5x/lib/tools/live.d.ts +21 -0
- package/vendor/l5x/lib/tools/live.js +175 -0
- package/vendor/l5x/lib/tools/read.d.ts +19 -0
- package/vendor/l5x/lib/tools/read.js +245 -0
- package/vendor/l5x/lib/tools/references.d.ts +20 -0
- package/vendor/l5x/lib/tools/references.js +269 -0
- package/vendor/l5x/lib/tools/studio.d.ts +21 -0
- package/vendor/l5x/lib/tools/studio.js +193 -0
- package/vendor/l5x/poll-worker.mjs +77 -0
- package/vendor/l5x/studio-bridge/NuGet.Config +1 -0
- package/vendor/l5x/studio-bridge/Program.cs +304 -0
- package/vendor/l5x/studio-bridge/StudioBridge.csproj +11 -0
- package/vendor/mwi/README.md +60 -0
- package/vendor/mwi/SKILL.md +87 -0
- package/vendor/mwi/bridge/README.md +8 -0
- package/vendor/mwi/bridge/mw_bridge.ps1 +2763 -0
- package/vendor/mwi/bridge/sdk-interop-importer.cs +30 -0
- package/vendor/mwi/bridge/sdk-variable-reader.cs +22 -0
- package/vendor/mwi/bridge/start_bridge.cmd +21 -0
- package/vendor/mwi/bridge/watch_dialogs.cmd +13 -0
- package/vendor/mwi/bridge/watch_dialogs.ps1 +153 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__init__.py +14 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/__init__.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/block_interfaces.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/cfb.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/datatypes.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/declarations.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/errors.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/graphical_listing.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/grid.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/knowledge.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/mwt_bind.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/navigation.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/program_checks.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/project.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/staging.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/structure.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/task_bindings.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/tasks.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/tree.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/validation.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/variable_edit.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/variables.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/workflow.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/block_interfaces.py +275 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/build.py +339 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/capture.py +448 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/cfb.py +765 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/com.py +268 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/datatypes.py +204 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/declarations.py +399 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/descriptions.py +30 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/eip.py +292 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/errors.py +36 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/graphical_listing.py +208 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/grid.py +227 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/ide.py +317 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/iec.py +317 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/installed_help.py +151 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/knowledge.py +218 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/libraries.py +250 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/manuals.py +520 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/mwt_bind.py +133 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/navigation.py +44 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/pou_writer.py +1547 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/program_checks.py +360 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/project.py +704 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/reference_data/catalog.json +672 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/reference_data/patterns.json +86 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/restore.py +208 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/server.py +1913 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/snapshot.py +138 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/st_comments.py +25 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/staging.py +179 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/stlint.py +676 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/structure.py +98 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/task_bindings.py +68 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/tasks.py +162 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/transaction.py +157 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/tree.py +563 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/tree_writer.py +790 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/uia.py +465 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/validation.py +41 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/variable_edit.py +183 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/variables.py +472 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/workflow.py +130 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/writer.py +666 -0
- package/vendor/mwi/code/mw_code.py +1258 -0
- package/vendor/mwi/legacy/edit-session.js +50 -0
- package/vendor/mwi/legacy/fb-insertion.js +74 -0
- package/vendor/mwi/legacy/native-code.js +83 -0
- package/vendor/mwi/legacy/native-groups.js +75 -0
- package/vendor/mwi/legacy/native-structure.js +143 -0
- package/vendor/mwi/legacy/native-variables.js +77 -0
- package/vendor/mwi/legacy/pou-conversion.js +47 -0
- package/vendor/mwi/legacy/pou-package.js +66 -0
- package/vendor/mwi/lib/core/bridge.d.ts +71 -0
- package/vendor/mwi/lib/core/bridge.js +53 -0
- package/vendor/mwi/lib/core/code-engine.d.ts +47 -0
- package/vendor/mwi/lib/core/code-engine.js +109 -0
- package/vendor/mwi/lib/core/errors.d.ts +37 -0
- package/vendor/mwi/lib/core/errors.js +47 -0
- package/vendor/mwi/lib/core/params.d.ts +52 -0
- package/vendor/mwi/lib/core/params.js +46 -0
- package/vendor/mwi/lib/core/ps-bridge.d.ts +62 -0
- package/vendor/mwi/lib/core/ps-bridge.js +127 -0
- package/vendor/mwi/lib/core/result.d.ts +45 -0
- package/vendor/mwi/lib/core/result.js +20 -0
- package/vendor/mwi/lib/core/tool.d.ts +80 -0
- package/vendor/mwi/lib/core/tool.js +55 -0
- package/vendor/mwi/lib/edit/compare.d.ts +33 -0
- package/vendor/mwi/lib/edit/compare.js +70 -0
- package/vendor/mwi/lib/edit.d.ts +20 -0
- package/vendor/mwi/lib/edit.js +32 -0
- package/vendor/mwi/lib/index.d.ts +21 -0
- package/vendor/mwi/lib/index.js +33 -0
- package/vendor/mwi/lib/read/service.d.ts +117 -0
- package/vendor/mwi/lib/read/service.js +229 -0
- package/vendor/mwi/lib/read.d.ts +21 -0
- package/vendor/mwi/lib/read.js +33 -0
- package/vendor/mwi/lib/session/project.d.ts +34 -0
- package/vendor/mwi/lib/session/project.js +54 -0
- package/vendor/mwi/lib/session/service.d.ts +167 -0
- package/vendor/mwi/lib/session/service.js +566 -0
- package/vendor/mwi/lib/session/stage.d.ts +84 -0
- package/vendor/mwi/lib/session/stage.js +187 -0
- package/vendor/mwi/lib/session/workspace.d.ts +52 -0
- package/vendor/mwi/lib/session/workspace.js +90 -0
- package/vendor/mwi/lib/skill.d.ts +36 -0
- package/vendor/mwi/lib/skill.js +56 -0
- package/vendor/mwi/lib/tools/edit.d.ts +20 -0
- package/vendor/mwi/lib/tools/edit.js +286 -0
- package/vendor/mwi/lib/tools/read.d.ts +19 -0
- package/vendor/mwi/lib/tools/read.js +161 -0
- package/vendor/mwi/lib/tools/session.d.ts +20 -0
- package/vendor/mwi/lib/tools/session.js +166 -0
- package/vendor/mwi/lib/tools/verify.d.ts +19 -0
- package/vendor/mwi/lib/tools/verify.js +85 -0
- package/vendor/mwi/lib/verify.d.ts +17 -0
- package/vendor/mwi/lib/verify.js +29 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text-level edits over an ME export.
|
|
3
|
+
*
|
|
4
|
+
* Every transform changes only the targeted bytes: an attribute on one tag, a
|
|
5
|
+
* display's name attribute, or one tag CSV row. Nothing is re-serialized, so
|
|
6
|
+
* unknown elements, comments and formatting survive.
|
|
7
|
+
*
|
|
8
|
+
* @module dsh-engineering-ftview/edit/operations
|
|
9
|
+
*/
|
|
10
|
+
/** Set one attribute on a display or one of its named objects. */
|
|
11
|
+
export interface DisplaySetOperation {
|
|
12
|
+
readonly kind: 'display_set';
|
|
13
|
+
readonly file: string;
|
|
14
|
+
readonly object?: string;
|
|
15
|
+
readonly attribute: string;
|
|
16
|
+
readonly value: string;
|
|
17
|
+
}
|
|
18
|
+
/** Add a display by copying an existing display file under a new name. */
|
|
19
|
+
export interface DisplayAddOperation {
|
|
20
|
+
readonly kind: 'display_add';
|
|
21
|
+
readonly copyFrom: string;
|
|
22
|
+
readonly name: string;
|
|
23
|
+
}
|
|
24
|
+
/** Insert or update one HMI tag row. */
|
|
25
|
+
export interface TagUpsertOperation {
|
|
26
|
+
readonly kind: 'tag_upsert';
|
|
27
|
+
readonly name: string;
|
|
28
|
+
readonly description?: string;
|
|
29
|
+
readonly type?: string;
|
|
30
|
+
}
|
|
31
|
+
/** One staged edit. */
|
|
32
|
+
export type EditOperation = DisplaySetOperation | DisplayAddOperation | TagUpsertOperation;
|
|
33
|
+
/** Escape one XML attribute value. */
|
|
34
|
+
export declare function escapeAttribute(value: string): string;
|
|
35
|
+
/** Escape one CSV cell when it needs quoting. */
|
|
36
|
+
export declare function encodeCell(value: string): string;
|
|
37
|
+
/**
|
|
38
|
+
* Set one attribute on a display root or object, preserving the rest byte for byte.
|
|
39
|
+
*
|
|
40
|
+
* @param xml - Display XML.
|
|
41
|
+
* @param object - Object name, or undefined for the display root.
|
|
42
|
+
* @param attribute - Attribute to set; a plain XML name.
|
|
43
|
+
* @param value - New value.
|
|
44
|
+
* @returns The modified XML.
|
|
45
|
+
*/
|
|
46
|
+
export declare function setAttribute(xml: string, object: string | undefined, attribute: string, value: string): string;
|
|
47
|
+
/**
|
|
48
|
+
* Rename a display root.
|
|
49
|
+
*
|
|
50
|
+
* @param xml - Display XML.
|
|
51
|
+
* @param name - New display name.
|
|
52
|
+
* @returns The modified XML.
|
|
53
|
+
*/
|
|
54
|
+
export declare function renameDisplay(xml: string, name: string): string;
|
|
55
|
+
/**
|
|
56
|
+
* Insert or update one tag row in the CSV.
|
|
57
|
+
*
|
|
58
|
+
* @param csv - Whole tag CSV.
|
|
59
|
+
* @param tag - Tag name and the columns to set.
|
|
60
|
+
* @returns The modified CSV.
|
|
61
|
+
*/
|
|
62
|
+
export declare function upsertTag(csv: string, tag: TagUpsertOperation): string;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text-level edits over an ME export.
|
|
3
|
+
*
|
|
4
|
+
* Every transform changes only the targeted bytes: an attribute on one tag, a
|
|
5
|
+
* display's name attribute, or one tag CSV row. Nothing is re-serialized, so
|
|
6
|
+
* unknown elements, comments and formatting survive.
|
|
7
|
+
*
|
|
8
|
+
* @module dsh-engineering-ftview/edit/operations
|
|
9
|
+
*/
|
|
10
|
+
import { EngineeringError } from "../core.js";
|
|
11
|
+
import { splitCsv } from "../model.js";
|
|
12
|
+
/** Escape one XML attribute value. */
|
|
13
|
+
export function escapeAttribute(value) {
|
|
14
|
+
return value.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"');
|
|
15
|
+
}
|
|
16
|
+
/** Escape one CSV cell when it needs quoting. */
|
|
17
|
+
export function encodeCell(value) {
|
|
18
|
+
return /[";\r\n]/u.test(value) ? `"${value.replaceAll('"', '""')}"` : value;
|
|
19
|
+
}
|
|
20
|
+
/** Read one attribute from an attribute string. */
|
|
21
|
+
function attributeValue(attributes, name) {
|
|
22
|
+
const match = new RegExp(`\\b${name}\\s*=\\s*("([^"]*)"|'([^']*)')`, 'u').exec(attributes);
|
|
23
|
+
return match?.[2] ?? match?.[3];
|
|
24
|
+
}
|
|
25
|
+
/** Find the root tag or the tag whose name attribute equals `object`. */
|
|
26
|
+
function findTag(xml, object) {
|
|
27
|
+
if (object === undefined) {
|
|
28
|
+
const root = /<([A-Za-z_][\w.:-]*)((?:[^<>"']|"[^"]*"|'[^']*')*?)(\/?)>/u.exec(xml);
|
|
29
|
+
if (root === null)
|
|
30
|
+
throw new EngineeringError('INVALID_DISPLAY', 'The display has no root element.', 'Open the display with ftview_get first; only display XML can be edited.');
|
|
31
|
+
return { start: root.index, end: root.index + root[0].length, attributes: root[2] ?? '', selfClosing: root[3] === '/' };
|
|
32
|
+
}
|
|
33
|
+
const pattern = /<([A-Za-z_][\w.:-]*)((?:[^<>"']|"[^"]*"|'[^']*')*?)(\/?)>/gu;
|
|
34
|
+
let match;
|
|
35
|
+
while ((match = pattern.exec(xml)) !== null) {
|
|
36
|
+
if (attributeValue(match[2] ?? '', 'name') === object) {
|
|
37
|
+
return { start: match.index, end: match.index + match[0].length, attributes: match[2] ?? '', selfClosing: match[3] === '/' };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
throw new EngineeringError('COMPONENT_NOT_FOUND', `No object in this display is named ${JSON.stringify(object)}.`, 'Call ftview_get with kind "display" to see the object names, then set the attribute on one of them.');
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Set one attribute on a display root or object, preserving the rest byte for byte.
|
|
44
|
+
*
|
|
45
|
+
* @param xml - Display XML.
|
|
46
|
+
* @param object - Object name, or undefined for the display root.
|
|
47
|
+
* @param attribute - Attribute to set; a plain XML name.
|
|
48
|
+
* @param value - New value.
|
|
49
|
+
* @returns The modified XML.
|
|
50
|
+
*/
|
|
51
|
+
export function setAttribute(xml, object, attribute, value) {
|
|
52
|
+
if (!/^[A-Za-z_][\w.:-]*$/u.test(attribute)) {
|
|
53
|
+
throw new EngineeringError('INVALID_ATTRIBUTE', `${JSON.stringify(attribute)} is not an XML attribute name.`, 'Pass a plain attribute name such as "visible" or "caption".');
|
|
54
|
+
}
|
|
55
|
+
const span = findTag(xml, object);
|
|
56
|
+
const existing = new RegExp(`\\b${attribute}\\s*=\\s*("[^"]*"|'[^']*')`, 'u');
|
|
57
|
+
let attributes;
|
|
58
|
+
if (existing.test(span.attributes)) {
|
|
59
|
+
attributes = span.attributes.replace(existing, `${attribute}="${escapeAttribute(value)}"`);
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
attributes = `${span.attributes} ${attribute}="${escapeAttribute(value)}"`;
|
|
63
|
+
}
|
|
64
|
+
const tagName = /^<([A-Za-z_][\w.:-]*)/u.exec(xml.slice(span.start, span.end))?.[1] ?? '';
|
|
65
|
+
const rebuilt = `<${tagName}${attributes}${span.selfClosing ? '/' : ''}>`;
|
|
66
|
+
return xml.slice(0, span.start) + rebuilt + xml.slice(span.end);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Rename a display root.
|
|
70
|
+
*
|
|
71
|
+
* @param xml - Display XML.
|
|
72
|
+
* @param name - New display name.
|
|
73
|
+
* @returns The modified XML.
|
|
74
|
+
*/
|
|
75
|
+
export function renameDisplay(xml, name) {
|
|
76
|
+
return setAttribute(xml, undefined, 'name', name);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Insert or update one tag row in the CSV.
|
|
80
|
+
*
|
|
81
|
+
* @param csv - Whole tag CSV.
|
|
82
|
+
* @param tag - Tag name and the columns to set.
|
|
83
|
+
* @returns The modified CSV.
|
|
84
|
+
*/
|
|
85
|
+
export function upsertTag(csv, tag) {
|
|
86
|
+
const eol = csv.includes('\r\n') ? '\r\n' : '\n';
|
|
87
|
+
const lines = csv.split(/\r?\n/);
|
|
88
|
+
const versionIndex = lines.findIndex(line => line.includes('###002'));
|
|
89
|
+
if (versionIndex < 0) {
|
|
90
|
+
throw new EngineeringError('TAG_CSV_MISSING', 'The tag CSV has no `###002` version line.', 'Pass an export whose tag file is the documented CSV export.');
|
|
91
|
+
}
|
|
92
|
+
const headerIndex = lines.findIndex((line, index) => index > versionIndex && line.trim() !== '');
|
|
93
|
+
if (headerIndex < 0)
|
|
94
|
+
throw new EngineeringError('TAG_CSV_MISSING', 'The tag CSV has no header row.', 'Re-export the HMI tag database as a CSV.');
|
|
95
|
+
const header = splitCsv(lines[headerIndex]).map(cell => cell.trim());
|
|
96
|
+
const nameIndex = header.indexOf('Tag Name');
|
|
97
|
+
if (nameIndex < 0)
|
|
98
|
+
throw new EngineeringError('TAG_CSV_MISSING', 'The tag CSV header has no `Tag Name` column.', 'Re-export the HMI tag database as a CSV.');
|
|
99
|
+
const descriptionIndex = header.indexOf('Tag Description');
|
|
100
|
+
const typeIndex = header.indexOf('Tag Type');
|
|
101
|
+
if (tag.description !== undefined && descriptionIndex < 0)
|
|
102
|
+
throw new EngineeringError('TAG_CSV_UNSUPPORTED', 'This tag CSV has no `Tag Description` column.', 'Set only the type, or re-export with descriptions enabled.');
|
|
103
|
+
if (tag.type !== undefined && typeIndex < 0)
|
|
104
|
+
throw new EngineeringError('TAG_CSV_UNSUPPORTED', 'This tag CSV has no `Tag Type` column.', 'Set only the description, or re-export with tag types.');
|
|
105
|
+
const rowIndex = lines.findIndex((line, index) => index > headerIndex && line.trim() !== '' && (splitCsv(line)[nameIndex] ?? '').trim() === tag.name);
|
|
106
|
+
if (rowIndex >= 0) {
|
|
107
|
+
const cells = splitCsv(lines[rowIndex]);
|
|
108
|
+
if (tag.description !== undefined)
|
|
109
|
+
cells[descriptionIndex] = tag.description;
|
|
110
|
+
if (tag.type !== undefined)
|
|
111
|
+
cells[typeIndex] = tag.type;
|
|
112
|
+
lines[rowIndex] = cells.map(encodeCell).join(';');
|
|
113
|
+
return lines.join(eol);
|
|
114
|
+
}
|
|
115
|
+
const cells = new Array(header.length).fill('');
|
|
116
|
+
cells[nameIndex] = tag.name;
|
|
117
|
+
if (tag.description !== undefined)
|
|
118
|
+
cells[descriptionIndex] = tag.description;
|
|
119
|
+
if (tag.type !== undefined)
|
|
120
|
+
cells[typeIndex] = tag.type;
|
|
121
|
+
if (lines.length > 0 && lines[lines.length - 1] === '')
|
|
122
|
+
lines.splice(lines.length - 1, 0, cells.map(encodeCell).join(';'));
|
|
123
|
+
else
|
|
124
|
+
lines.push(cells.map(encodeCell).join(';'));
|
|
125
|
+
return lines.join(eol);
|
|
126
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prepare staged edits against one opened export.
|
|
3
|
+
*
|
|
4
|
+
* Preparation reads the source files, applies every operation to the text, and
|
|
5
|
+
* re-parses the result. It writes nothing: apply is the only writer, and it
|
|
6
|
+
* writes to a new folder.
|
|
7
|
+
*
|
|
8
|
+
* @module dsh-engineering-ftview/edit/prepare
|
|
9
|
+
*/
|
|
10
|
+
import { type Coverage, type FsService } from '../core.ts';
|
|
11
|
+
import { type Finding } from '../checks.ts';
|
|
12
|
+
import { type Application } from '../model.ts';
|
|
13
|
+
import { type EditOperation } from './operations.ts';
|
|
14
|
+
/** One prepared edit set. */
|
|
15
|
+
export interface PreparedEdit {
|
|
16
|
+
/** Files whose text changed, keyed by export-relative path. */
|
|
17
|
+
readonly texts: ReadonlyMap<string, string>;
|
|
18
|
+
/** Files the edit adds. */
|
|
19
|
+
readonly added: readonly string[];
|
|
20
|
+
/** Every file written by apply, including unchanged copies. */
|
|
21
|
+
readonly changed: readonly string[];
|
|
22
|
+
/** Findings after the edit. */
|
|
23
|
+
readonly findings: readonly Finding[];
|
|
24
|
+
/** Stable hash over the source hashes and the operations. */
|
|
25
|
+
readonly hash: string;
|
|
26
|
+
}
|
|
27
|
+
/** Coverage for anything derived from an export. */
|
|
28
|
+
export declare const EDIT_COVERAGE: Coverage;
|
|
29
|
+
/**
|
|
30
|
+
* Apply every operation to text and re-parse the result.
|
|
31
|
+
*
|
|
32
|
+
* @param fs - Filesystem provider.
|
|
33
|
+
* @param app - The opened export.
|
|
34
|
+
* @param operations - Staged operations.
|
|
35
|
+
* @param signal - Cancellation.
|
|
36
|
+
* @returns Prepared texts, findings and a hash.
|
|
37
|
+
*/
|
|
38
|
+
export declare function prepareEdit(fs: FsService, app: Application, operations: readonly EditOperation[], signal: AbortSignal | undefined): Promise<PreparedEdit>;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prepare staged edits against one opened export.
|
|
3
|
+
*
|
|
4
|
+
* Preparation reads the source files, applies every operation to the text, and
|
|
5
|
+
* re-parses the result. It writes nothing: apply is the only writer, and it
|
|
6
|
+
* writes to a new folder.
|
|
7
|
+
*
|
|
8
|
+
* @module dsh-engineering-ftview/edit/prepare
|
|
9
|
+
*/
|
|
10
|
+
import { createHash } from 'node:crypto';
|
|
11
|
+
import { EngineeringError } from "../core.js";
|
|
12
|
+
import { findingsOf } from "../checks.js";
|
|
13
|
+
import { parseDisplay, parseTagCsv } from "../model.js";
|
|
14
|
+
import { renameDisplay, setAttribute, upsertTag } from "./operations.js";
|
|
15
|
+
/** Coverage for anything derived from an export. */
|
|
16
|
+
export const EDIT_COVERAGE = {
|
|
17
|
+
complete: false,
|
|
18
|
+
unverified: [
|
|
19
|
+
'vendor XSD schema validation',
|
|
20
|
+
'Studio import and runtime behavior',
|
|
21
|
+
'native binary files are not copied by apply',
|
|
22
|
+
'unchanged text exports are copied verbatim',
|
|
23
|
+
],
|
|
24
|
+
};
|
|
25
|
+
/** Decode UTF-8 file bytes. */
|
|
26
|
+
function decode(bytes) {
|
|
27
|
+
return new TextDecoder('utf-8').decode(bytes);
|
|
28
|
+
}
|
|
29
|
+
/** Read one source file as text. */
|
|
30
|
+
async function readText(fs, root, file, signal) {
|
|
31
|
+
return decode(await fs.readBytes(`${root}/${file}`, signal, 64 * 1024 * 1024));
|
|
32
|
+
}
|
|
33
|
+
/** The tag CSV the edit targets, refusing an ambiguous export. */
|
|
34
|
+
function tagFileOf(app) {
|
|
35
|
+
if (app.tagFiles.length === 0) {
|
|
36
|
+
throw new EngineeringError('TAG_CSV_MISSING', 'This export has no tag CSV, so tags cannot be edited.', 'Export the HMI tag database as a CSV into the same folder, then reopen the export.');
|
|
37
|
+
}
|
|
38
|
+
if (app.tagFiles.length > 1) {
|
|
39
|
+
throw new EngineeringError('AMBIGUOUS_TAG_CSV', `This export has ${app.tagFiles.length} tag CSV files, so the edit target is ambiguous.`, 'Keep one tag CSV per export folder, then reopen and retry.', app.tagFiles);
|
|
40
|
+
}
|
|
41
|
+
return app.tagFiles[0];
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Apply every operation to text and re-parse the result.
|
|
45
|
+
*
|
|
46
|
+
* @param fs - Filesystem provider.
|
|
47
|
+
* @param app - The opened export.
|
|
48
|
+
* @param operations - Staged operations.
|
|
49
|
+
* @param signal - Cancellation.
|
|
50
|
+
* @returns Prepared texts, findings and a hash.
|
|
51
|
+
*/
|
|
52
|
+
export async function prepareEdit(fs, app, operations, signal) {
|
|
53
|
+
const texts = new Map();
|
|
54
|
+
const added = [];
|
|
55
|
+
const displayByFile = new Map(app.displays.map(display => [display.file, display]));
|
|
56
|
+
const displayNames = new Set(app.displays.map(display => display.name));
|
|
57
|
+
for (const operation of operations) {
|
|
58
|
+
if (operation.kind === 'display_set') {
|
|
59
|
+
if (!displayByFile.has(operation.file)) {
|
|
60
|
+
throw new EngineeringError('COMPONENT_NOT_FOUND', `The draft targets ${operation.file}, which this export no longer contains.`, 'Reopen the export and stage the edit again.');
|
|
61
|
+
}
|
|
62
|
+
const current = texts.get(operation.file) ?? await readText(fs, app.root, operation.file, signal);
|
|
63
|
+
texts.set(operation.file, setAttribute(current, operation.object, operation.attribute, operation.value));
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (operation.kind === 'display_add') {
|
|
67
|
+
const source = app.displays.find(display => display.name === operation.copyFrom);
|
|
68
|
+
if (source === undefined) {
|
|
69
|
+
throw new EngineeringError('COMPONENT_NOT_FOUND', `No display is named ${JSON.stringify(operation.copyFrom)} to copy.`, 'Call ftview_list with kind "display" to see the display names.', [...displayNames].slice(0, 5));
|
|
70
|
+
}
|
|
71
|
+
if (displayNames.has(operation.name) || added.includes(operation.name)) {
|
|
72
|
+
throw new EngineeringError('COMPONENT_EXISTS', `A display named ${JSON.stringify(operation.name)} already exists in this export or draft.`, 'Pass a new display name, for example "LineOverview2".');
|
|
73
|
+
}
|
|
74
|
+
const text = renameDisplay(await readText(fs, app.root, source.file, signal), operation.name);
|
|
75
|
+
const directory = source.file.includes('/') ? source.file.slice(0, source.file.lastIndexOf('/') + 1) : '';
|
|
76
|
+
const target = `${directory}${operation.name}.xml`;
|
|
77
|
+
if (app.files.some(file => file.file.toLowerCase() === target.toLowerCase())) {
|
|
78
|
+
throw new EngineeringError('COMPONENT_EXISTS', `The export already has a file ${target}.`, 'Pass a different new display name.');
|
|
79
|
+
}
|
|
80
|
+
texts.set(target, text);
|
|
81
|
+
added.push(target);
|
|
82
|
+
displayNames.add(operation.name);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const tagFile = tagFileOf(app);
|
|
86
|
+
const current = texts.get(tagFile) ?? await readText(fs, app.root, tagFile, signal);
|
|
87
|
+
texts.set(tagFile, upsertTag(current, operation));
|
|
88
|
+
}
|
|
89
|
+
const displays = [];
|
|
90
|
+
for (const display of app.displays) {
|
|
91
|
+
const text = texts.get(display.file);
|
|
92
|
+
if (text === undefined) {
|
|
93
|
+
displays.push(display);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const parsed = parseDisplay(display.file, text);
|
|
97
|
+
if (parsed.parseError !== undefined) {
|
|
98
|
+
throw new EngineeringError('INVALID_DISPLAY', `The edited ${display.file} no longer parses: ${parsed.parseError}`, 'Undo the last operation and check the display file, then stage it again.');
|
|
99
|
+
}
|
|
100
|
+
displays.push(parsed);
|
|
101
|
+
}
|
|
102
|
+
for (const file of added) {
|
|
103
|
+
const text = texts.get(file);
|
|
104
|
+
const parsed = parseDisplay(file, text);
|
|
105
|
+
if (parsed.parseError !== undefined) {
|
|
106
|
+
throw new EngineeringError('INVALID_DISPLAY', `The added ${file} does not parse: ${parsed.parseError}`, 'Copy a different display or pass another name.');
|
|
107
|
+
}
|
|
108
|
+
displays.push(parsed);
|
|
109
|
+
}
|
|
110
|
+
let tags = app.tags;
|
|
111
|
+
let tagErrors = app.tagErrors;
|
|
112
|
+
for (const [file, text] of texts) {
|
|
113
|
+
if (!app.tagFiles.includes(file))
|
|
114
|
+
continue;
|
|
115
|
+
const parsed = parseTagCsv(text);
|
|
116
|
+
tags = parsed.rows;
|
|
117
|
+
tagErrors = parsed.errors;
|
|
118
|
+
}
|
|
119
|
+
const changedFiles = new Set([...texts.keys()]);
|
|
120
|
+
const diagnostics = app.diagnostics.filter(diagnostic => {
|
|
121
|
+
const file = diagnostic.split(':')[0] ?? '';
|
|
122
|
+
return !changedFiles.has(file);
|
|
123
|
+
});
|
|
124
|
+
const findings = findingsOf({ displays, tags, tagErrors, diagnostics });
|
|
125
|
+
const hashInput = app.files.map(file => `${file.file}:${file.sha256}`).join('\n') + '\n' + JSON.stringify(operations);
|
|
126
|
+
return {
|
|
127
|
+
texts,
|
|
128
|
+
added,
|
|
129
|
+
changed: [...app.files.map(file => file.file), ...added],
|
|
130
|
+
findings,
|
|
131
|
+
hash: createHash('sha256').update(hashInput).digest('hex'),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The FTView editing tools: stage, validate, apply.
|
|
3
|
+
*
|
|
4
|
+
* One operation per call, accumulated per chat and export, validated against a
|
|
5
|
+
* re-parsed copy, then written to a new export folder. The source is never
|
|
6
|
+
* modified.
|
|
7
|
+
*
|
|
8
|
+
* @module dsh-engineering-ftview/edit/tools
|
|
9
|
+
*/
|
|
10
|
+
import { type FsService, type ToolDefinition } from '../core.ts';
|
|
11
|
+
import type { ViewSession } from '../session.ts';
|
|
12
|
+
import type { EditDrafts } from './drafts.ts';
|
|
13
|
+
import type { EditOperation } from './operations.ts';
|
|
14
|
+
/**
|
|
15
|
+
* The editing tools.
|
|
16
|
+
*
|
|
17
|
+
* @param session - View handles from the read entry.
|
|
18
|
+
* @param drafts - Draft store for the calling chat.
|
|
19
|
+
* @param fs - Filesystem provider used to read sources and write the result.
|
|
20
|
+
* @param maxOperations - Inclusive operations per draft.
|
|
21
|
+
* @returns Tool definitions in registration order.
|
|
22
|
+
*/
|
|
23
|
+
export declare function editTools(session: ViewSession, drafts: EditDrafts, fs: FsService, maxOperations: number): readonly ToolDefinition[];
|
|
24
|
+
/** A helper kept for callers that want the operation count type. */
|
|
25
|
+
export type { EditOperation };
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The FTView editing tools: stage, validate, apply.
|
|
3
|
+
*
|
|
4
|
+
* One operation per call, accumulated per chat and export, validated against a
|
|
5
|
+
* re-parsed copy, then written to a new export folder. The source is never
|
|
6
|
+
* modified.
|
|
7
|
+
*
|
|
8
|
+
* @module dsh-engineering-ftview/edit/tools
|
|
9
|
+
*/
|
|
10
|
+
import { createHash } from 'node:crypto';
|
|
11
|
+
import { EngineeringError, object, optionalString, requiredString, string, success, tool } from "../core.js";
|
|
12
|
+
import { EDIT_COVERAGE, prepareEdit } from "./prepare.js";
|
|
13
|
+
/** The view handle parameter, described once. */
|
|
14
|
+
const VIEW_ID = string('View handle returned by ftview_open. Omit when exactly one export is open in this chat.');
|
|
15
|
+
/** The draft handle parameter, described once. */
|
|
16
|
+
const DRAFT_ID = string('Draft handle returned by an ftview_draft_* tool. Omit when exactly one draft exists in this chat.');
|
|
17
|
+
/** Coverage for apply, which writes text exports only. */
|
|
18
|
+
const APPLY_COVERAGE = {
|
|
19
|
+
...EDIT_COVERAGE,
|
|
20
|
+
unverified: [...EDIT_COVERAGE.unverified, 'files that are not display XML or tag CSV are not copied'],
|
|
21
|
+
};
|
|
22
|
+
/** The next step after staging an operation. */
|
|
23
|
+
function nextValidate(draftId) {
|
|
24
|
+
return [{ tool: 'ftview_validate', args: { draft_id: draftId }, why: 'Prepare the changed export and read the findings before writing anything.' }];
|
|
25
|
+
}
|
|
26
|
+
/** Count duplicate named objects in one display. */
|
|
27
|
+
function objectCount(display, name) {
|
|
28
|
+
return display.objects.filter(object => object.name === name).length;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The editing tools.
|
|
32
|
+
*
|
|
33
|
+
* @param session - View handles from the read entry.
|
|
34
|
+
* @param drafts - Draft store for the calling chat.
|
|
35
|
+
* @param fs - Filesystem provider used to read sources and write the result.
|
|
36
|
+
* @param maxOperations - Inclusive operations per draft.
|
|
37
|
+
* @returns Tool definitions in registration order.
|
|
38
|
+
*/
|
|
39
|
+
export function editTools(session, drafts, fs, maxOperations) {
|
|
40
|
+
/** Resolve the view and append one operation. */
|
|
41
|
+
const stage = (exec, viewId, operation) => {
|
|
42
|
+
const view = session.require(viewId, exec);
|
|
43
|
+
return { view, draft: drafts.append(exec, view.id, operation, maxOperations) };
|
|
44
|
+
};
|
|
45
|
+
const staged = (draft, operation) => success(`Draft ${draft.id}: ${draft.operations.length} ${draft.operations.length === 1 ? 'operation' : 'operations'}.`, { draft_id: draft.id, operation_count: draft.operations.length, last_operation: operation }, nextValidate(draft.id), EDIT_COVERAGE);
|
|
46
|
+
return [
|
|
47
|
+
tool({
|
|
48
|
+
name: 'ftview_draft_display_set',
|
|
49
|
+
description: 'Stage one attribute change on a display or on one of its named objects. Nothing is written until ftview_apply.',
|
|
50
|
+
parameters: object({
|
|
51
|
+
view_id: VIEW_ID,
|
|
52
|
+
display: string('Display name as listed by ftview_list. Example: "Main".'),
|
|
53
|
+
object: string('Object name inside the display. Omit to change the display root. Example: "StartPB".'),
|
|
54
|
+
attribute: string('Attribute to set. Example: "visible" or "caption".'),
|
|
55
|
+
value: string('New attribute value. Example: "0".'),
|
|
56
|
+
}, ['display', 'attribute', 'value']),
|
|
57
|
+
async run(args, exec) {
|
|
58
|
+
const view = session.require(optionalString(args, 'view_id'), exec);
|
|
59
|
+
const displayName = requiredString(args, 'display');
|
|
60
|
+
const display = view.app.displays.find(candidate => candidate.name === displayName);
|
|
61
|
+
if (display === undefined) {
|
|
62
|
+
throw new EngineeringError('COMPONENT_NOT_FOUND', `No display is named ${JSON.stringify(displayName)} in this export.`, 'Call ftview_list with kind "display" to see the display names.', view.app.displays.slice(0, 5).map(candidate => candidate.name));
|
|
63
|
+
}
|
|
64
|
+
const objectName = optionalString(args, 'object');
|
|
65
|
+
if (objectName !== undefined) {
|
|
66
|
+
const count = objectCount(display, objectName);
|
|
67
|
+
if (count === 0) {
|
|
68
|
+
throw new EngineeringError('COMPONENT_NOT_FOUND', `No object in ${JSON.stringify(display.name)} is named ${JSON.stringify(objectName)}.`, 'Call ftview_get with kind "display" to see the object names.', display.objects.flatMap(candidate => candidate.name === undefined ? [] : [candidate.name]).slice(0, 5));
|
|
69
|
+
}
|
|
70
|
+
if (count > 1) {
|
|
71
|
+
throw new EngineeringError('AMBIGUOUS_OBJECT', `${display.name} has ${count} objects named ${JSON.stringify(objectName)}, so the edit target is ambiguous.`, 'This export cannot be edited by name alone; use Studio for that display.');
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const { draft } = stage(exec, optionalString(args, 'view_id'), {
|
|
75
|
+
kind: 'display_set',
|
|
76
|
+
file: display.file,
|
|
77
|
+
...(objectName === undefined ? {} : { object: objectName }),
|
|
78
|
+
attribute: requiredString(args, 'attribute'),
|
|
79
|
+
value: requiredString(args, 'value'),
|
|
80
|
+
});
|
|
81
|
+
return staged(draft, draft.operations[draft.operations.length - 1]);
|
|
82
|
+
},
|
|
83
|
+
}),
|
|
84
|
+
tool({
|
|
85
|
+
name: 'ftview_draft_display_add',
|
|
86
|
+
description: 'Stage a new display by copying an existing display file under a new name. Nothing is written until ftview_apply.',
|
|
87
|
+
parameters: object({
|
|
88
|
+
view_id: VIEW_ID,
|
|
89
|
+
copy_from: string('Display name to copy. Example: "Main".'),
|
|
90
|
+
name: string('New display name, unique in the export. Example: "MainCopy".'),
|
|
91
|
+
}, ['copy_from', 'name']),
|
|
92
|
+
async run(args, exec) {
|
|
93
|
+
const view = session.require(optionalString(args, 'view_id'), exec);
|
|
94
|
+
const copyFrom = requiredString(args, 'copy_from');
|
|
95
|
+
const name = requiredString(args, 'name');
|
|
96
|
+
if (!view.app.displays.some(display => display.name === copyFrom)) {
|
|
97
|
+
throw new EngineeringError('COMPONENT_NOT_FOUND', `No display is named ${JSON.stringify(copyFrom)} to copy.`, 'Call ftview_list with kind "display" to see the display names.', view.app.displays.slice(0, 5).map(display => display.name));
|
|
98
|
+
}
|
|
99
|
+
if (view.app.displays.some(display => display.name === name)) {
|
|
100
|
+
throw new EngineeringError('COMPONENT_EXISTS', `A display named ${JSON.stringify(name)} already exists in this export.`, 'Pass a new display name, for example "LineOverview2".');
|
|
101
|
+
}
|
|
102
|
+
const { draft } = stage(exec, optionalString(args, 'view_id'), {
|
|
103
|
+
kind: 'display_add',
|
|
104
|
+
copyFrom,
|
|
105
|
+
name,
|
|
106
|
+
});
|
|
107
|
+
return staged(draft, draft.operations[draft.operations.length - 1]);
|
|
108
|
+
},
|
|
109
|
+
}),
|
|
110
|
+
tool({
|
|
111
|
+
name: 'ftview_draft_tag_upsert',
|
|
112
|
+
description: 'Stage one HMI tag update: change an existing row\'s description or type, or append a new tag row. Nothing is written until ftview_apply.',
|
|
113
|
+
parameters: object({
|
|
114
|
+
view_id: VIEW_ID,
|
|
115
|
+
name: string('Tag name. Example: "StartPB".'),
|
|
116
|
+
description: string('New tag description. Example: "Start pushbutton".'),
|
|
117
|
+
type: string('New tag type. Example: "A" for analog or "D" for digital.'),
|
|
118
|
+
}, ['name']),
|
|
119
|
+
async run(args, exec) {
|
|
120
|
+
const description = optionalString(args, 'description');
|
|
121
|
+
const type = optionalString(args, 'type');
|
|
122
|
+
if (description === undefined && type === undefined) {
|
|
123
|
+
throw new EngineeringError('INVALID_ARGUMENT', 'A tag upsert needs a description, a type, or both.', 'Pass description, type, or both; for example "Start pushbutton".');
|
|
124
|
+
}
|
|
125
|
+
const { draft } = stage(exec, optionalString(args, 'view_id'), {
|
|
126
|
+
kind: 'tag_upsert',
|
|
127
|
+
name: requiredString(args, 'name'),
|
|
128
|
+
...(description === undefined ? {} : { description }),
|
|
129
|
+
...(type === undefined ? {} : { type }),
|
|
130
|
+
});
|
|
131
|
+
return staged(draft, draft.operations[draft.operations.length - 1]);
|
|
132
|
+
},
|
|
133
|
+
}),
|
|
134
|
+
tool({
|
|
135
|
+
name: 'ftview_validate',
|
|
136
|
+
description: 'Prepare the staged edits: apply them to a re-parsed copy, report the changed files and the new structural findings. Writes nothing.',
|
|
137
|
+
parameters: object({ draft_id: DRAFT_ID }, []),
|
|
138
|
+
async run(args, exec) {
|
|
139
|
+
const draft = drafts.get(exec, optionalString(args, 'draft_id'));
|
|
140
|
+
const view = session.require(draft.viewId, exec);
|
|
141
|
+
const prepared = await prepareEdit(fs, view.app, draft.operations, exec.signal);
|
|
142
|
+
const byRule = {};
|
|
143
|
+
for (const finding of prepared.findings)
|
|
144
|
+
byRule[finding.rule] = (byRule[finding.rule] ?? 0) + 1;
|
|
145
|
+
return success(`${draft.operations.length} operation(s); ${prepared.texts.size} file(s) change; ${prepared.findings.length} finding(s).`, {
|
|
146
|
+
draft_id: draft.id,
|
|
147
|
+
draft_sha256: prepared.hash,
|
|
148
|
+
changed_files: [...prepared.texts.keys()],
|
|
149
|
+
added: prepared.added,
|
|
150
|
+
findings: prepared.findings,
|
|
151
|
+
by_rule: byRule,
|
|
152
|
+
}, [{ tool: 'ftview_apply', args: { draft_id: draft.id }, why: 'Write the prepared export to a new folder.' }], EDIT_COVERAGE);
|
|
153
|
+
},
|
|
154
|
+
}),
|
|
155
|
+
tool({
|
|
156
|
+
name: 'ftview_apply',
|
|
157
|
+
description: 'Write the prepared edits to a NEW export folder: changed files get the edits, other text exports are copied verbatim. The source folder is never modified.',
|
|
158
|
+
parameters: object({
|
|
159
|
+
draft_id: DRAFT_ID,
|
|
160
|
+
output_root: string('New folder for the edited export. It must not exist. Example: "C:/Projects/Line/ME Export Edited".'),
|
|
161
|
+
}, ['output_root']),
|
|
162
|
+
async run(args, exec) {
|
|
163
|
+
const draft = drafts.get(exec, optionalString(args, 'draft_id'));
|
|
164
|
+
const view = session.require(draft.viewId, exec);
|
|
165
|
+
const prepared = await prepareEdit(fs, view.app, draft.operations, exec.signal);
|
|
166
|
+
const outputRoot = await fs.resolve(requiredString(args, 'output_root'), {
|
|
167
|
+
...(exec.cwd === undefined ? {} : { cwd: exec.cwd }),
|
|
168
|
+
...(exec.signal === undefined ? {} : { signal: exec.signal }),
|
|
169
|
+
});
|
|
170
|
+
const existing = await fs.stat(outputRoot, exec.signal);
|
|
171
|
+
if (existing !== undefined) {
|
|
172
|
+
throw new EngineeringError('OUTPUT_EXISTS', `A file or folder already exists at ${requiredString(args, 'output_root')}.`, 'Pass a new output_root; apply never merges into an existing folder.');
|
|
173
|
+
}
|
|
174
|
+
if (fs.writeText === undefined) {
|
|
175
|
+
throw new EngineeringError('OUTPUT_UNAVAILABLE', 'The filesystem provider in this profile cannot write files.', 'Use a profile whose filesystem provider allows writes.');
|
|
176
|
+
}
|
|
177
|
+
const hashes = new Map(view.app.files.map(file => [file.file, file.sha256]));
|
|
178
|
+
let written = 0;
|
|
179
|
+
for (const file of view.app.files) {
|
|
180
|
+
const bytes = await fs.readBytes(`${view.app.root}/${file.file}`, exec.signal, 64 * 1024 * 1024);
|
|
181
|
+
if (createHash('sha256').update(bytes).digest('hex') !== hashes.get(file.file)) {
|
|
182
|
+
throw new EngineeringError('STALE_EXPORT', `The export changed on disk since it was opened: ${file.file}.`, 'Reopen the export with ftview_open, stage the edits again, then apply.');
|
|
183
|
+
}
|
|
184
|
+
const text = prepared.texts.get(file.file) ?? new TextDecoder('utf-8').decode(bytes);
|
|
185
|
+
await fs.writeText(`${outputRoot}/${file.file}`, text, { kind: 'createIfAbsent' }, exec.signal);
|
|
186
|
+
written += 1;
|
|
187
|
+
}
|
|
188
|
+
for (const file of prepared.added) {
|
|
189
|
+
const text = prepared.texts.get(file);
|
|
190
|
+
await fs.writeText(`${outputRoot}/${file}`, text, { kind: 'createIfAbsent' }, exec.signal);
|
|
191
|
+
written += 1;
|
|
192
|
+
}
|
|
193
|
+
drafts.remove(exec, draft.id);
|
|
194
|
+
return success(`Wrote ${written} file(s) to ${outputRoot}.`, { output_root: outputRoot, draft_id: draft.id, files_written: written, changed_files: [...prepared.texts.keys()] }, [{ tool: 'ftview_open', args: { root: outputRoot }, why: 'Reopen the written export and check it.' }], APPLY_COVERAGE);
|
|
195
|
+
},
|
|
196
|
+
}),
|
|
197
|
+
];
|
|
198
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The L5X-style editing entry for FactoryTalk View ME: drafts over the read
|
|
3
|
+
* entry's opened views.
|
|
4
|
+
*
|
|
5
|
+
* Separate so a read-only profile does not pay for five edit schemas. It
|
|
6
|
+
* consumes the `ftviewViews` service the read entry provides, so an export is
|
|
7
|
+
* scanned once and both entries see the same handle.
|
|
8
|
+
*
|
|
9
|
+
* @module dsh-engineering-ftview/edit
|
|
10
|
+
*/
|
|
11
|
+
import { type PluginContext } from './core.ts';
|
|
12
|
+
/** Plugin name in a composition. */
|
|
13
|
+
export declare const name = "ftview-edit";
|
|
14
|
+
/** The read entry's view service must already be mounted. */
|
|
15
|
+
export declare const inject: readonly ["tools", "fs", "ftviewViews"];
|
|
16
|
+
/** Configuration for the editing entry. */
|
|
17
|
+
export interface Config {
|
|
18
|
+
/** Most operations one draft may accumulate. Defaults to 100. */
|
|
19
|
+
maxDraftOperations?: number;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Register the editing tools.
|
|
23
|
+
*
|
|
24
|
+
* @param ctx - Plugin context carrying the read entry's view service.
|
|
25
|
+
* @param config - Deployment limits.
|
|
26
|
+
*/
|
|
27
|
+
export declare function apply(ctx: PluginContext, config?: Config): void;
|
|
28
|
+
export { EditDrafts } from './edit/drafts.ts';
|
|
29
|
+
export { editTools } from './edit/tools.ts';
|
|
30
|
+
export { prepareEdit } from './edit/prepare.ts';
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The L5X-style editing entry for FactoryTalk View ME: drafts over the read
|
|
3
|
+
* entry's opened views.
|
|
4
|
+
*
|
|
5
|
+
* Separate so a read-only profile does not pay for five edit schemas. It
|
|
6
|
+
* consumes the `ftviewViews` service the read entry provides, so an export is
|
|
7
|
+
* scanned once and both entries see the same handle.
|
|
8
|
+
*
|
|
9
|
+
* @module dsh-engineering-ftview/edit
|
|
10
|
+
*/
|
|
11
|
+
import { stringFs } from "./core.js";
|
|
12
|
+
import { EditDrafts } from "./edit/drafts.js";
|
|
13
|
+
import { editTools } from "./edit/tools.js";
|
|
14
|
+
/** Plugin name in a composition. */
|
|
15
|
+
export const name = 'ftview-edit';
|
|
16
|
+
/** The read entry's view service must already be mounted. */
|
|
17
|
+
export const inject = ['tools', 'fs', 'ftviewViews'];
|
|
18
|
+
/**
|
|
19
|
+
* Register the editing tools.
|
|
20
|
+
*
|
|
21
|
+
* @param ctx - Plugin context carrying the read entry's view service.
|
|
22
|
+
* @param config - Deployment limits.
|
|
23
|
+
*/
|
|
24
|
+
export function apply(ctx, config = {}) {
|
|
25
|
+
const session = ctx.get?.('ftviewViews');
|
|
26
|
+
if (session === undefined) {
|
|
27
|
+
throw new Error('ftview-edit: the ftviewViews service is unavailable; mount the FTView read entry before this one');
|
|
28
|
+
}
|
|
29
|
+
const maxDraftOperations = config.maxDraftOperations ?? 100;
|
|
30
|
+
if (!Number.isInteger(maxDraftOperations) || maxDraftOperations <= 0) {
|
|
31
|
+
throw new Error('ftview-edit: maxDraftOperations must be a positive integer');
|
|
32
|
+
}
|
|
33
|
+
for (const definition of editTools(session, new EditDrafts(), stringFs(ctx.fs), maxDraftOperations))
|
|
34
|
+
ctx.tools.register(definition);
|
|
35
|
+
}
|
|
36
|
+
export { EditDrafts } from "./edit/drafts.js";
|
|
37
|
+
export { editTools } from "./edit/tools.js";
|
|
38
|
+
export { prepareEdit } from "./edit/prepare.js";
|