lasal-mcp 0.1.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.
Files changed (42) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +198 -0
  3. package/dist/core/envelope.js +8 -0
  4. package/dist/core/errors.js +12 -0
  5. package/dist/core/http.js +19 -0
  6. package/dist/core/process.js +30 -0
  7. package/dist/core/response.js +37 -0
  8. package/dist/core/scratch.js +6 -0
  9. package/dist/core/staticServer.js +88 -0
  10. package/dist/server.js +230 -0
  11. package/dist/state.js +57 -0
  12. package/dist/tools/applyProjectChanges.js +371 -0
  13. package/dist/tools/deployAll.js +320 -0
  14. package/dist/tools/hmiBrowser.js +224 -0
  15. package/dist/tools/hmiRuntime.js +273 -0
  16. package/dist/tools/inspectProject.js +162 -0
  17. package/dist/tools/inspectVisuProject.js +474 -0
  18. package/dist/tools/larsRuntime.js +536 -0
  19. package/dist/tools/lasalApps.js +111 -0
  20. package/dist/tools/plcControl.js +530 -0
  21. package/dist/tools/plcDiagnostics.js +172 -0
  22. package/dist/tools/readClassSource.js +147 -0
  23. package/dist/tools/selectProject.js +47 -0
  24. package/dist/tools/setTargetIp.js +114 -0
  25. package/dist/tools/status.js +146 -0
  26. package/dist/tools/visuControl.js +447 -0
  27. package/dist/tools/visuDashboard.js +571 -0
  28. package/dist/utils/batchScript.js +257 -0
  29. package/dist/utils/config.js +34 -0
  30. package/dist/utils/editTransaction.js +39 -0
  31. package/dist/utils/engine.js +163 -0
  32. package/dist/utils/lars.js +471 -0
  33. package/dist/utils/lasalXml.js +758 -0
  34. package/dist/utils/preflight.js +194 -0
  35. package/dist/utils/projectScanner.js +212 -0
  36. package/dist/utils/resolvePaths.js +46 -0
  37. package/dist/utils/respond.js +14 -0
  38. package/dist/utils/scriptRunner.js +161 -0
  39. package/dist/utils/visuDashboardIO.js +198 -0
  40. package/dist/utils/visuPropertyEncoding.js +174 -0
  41. package/dist/utils/visuScript.js +262 -0
  42. package/package.json +64 -0
@@ -0,0 +1,198 @@
1
+ import { copyFileSync, existsSync, writeFileSync, readFileSync, readdirSync } from "fs";
2
+ import { join, basename, dirname } from "path";
3
+ import { randomUUID } from "crypto";
4
+ // Generate UUID v4
5
+ export function newDesignTimeId() {
6
+ return randomUUID();
7
+ }
8
+ // Generate stripped instance ID
9
+ export function newInstanceId(prefix) {
10
+ const uuid = newDesignTimeId().replace(/-/g, "");
11
+ return `${prefix}${uuid}`;
12
+ }
13
+ // Read Real Version from .lvp
14
+ export function getFileEntryVersion(lvpPath, key) {
15
+ try {
16
+ if (existsSync(lvpPath)) {
17
+ const data = JSON.parse(readFileSync(lvpPath, "utf-8"));
18
+ if (data && data.fileEntryVersions && data.fileEntryVersions[key]) {
19
+ return data.fileEntryVersions[key];
20
+ }
21
+ }
22
+ }
23
+ catch { }
24
+ return "0.0.96"; // standard fallback
25
+ }
26
+ // Backup file alongside original
27
+ const backedUpFiles = new Set();
28
+ export function backupFile(filePath) {
29
+ if (backedUpFiles.has(filePath))
30
+ return null;
31
+ if (!existsSync(filePath))
32
+ return null;
33
+ const dir = dirname(filePath);
34
+ const base = basename(filePath);
35
+ const ts = Date.now();
36
+ const backupPath = join(dir, `${base}.bak-${ts}`);
37
+ copyFileSync(filePath, backupPath);
38
+ backedUpFiles.add(filePath);
39
+ return backupPath;
40
+ }
41
+ // Resolve Text Reference: List:AliasName -> ID
42
+ export function resolveTextRef(projectDir, listName, aliasName) {
43
+ const locDir = join(projectDir, "Localization");
44
+ if (!existsSync(locDir))
45
+ return null;
46
+ try {
47
+ const langs = readdirSync(locDir);
48
+ for (const lang of langs) {
49
+ const listFile = join(locDir, lang, `${listName}.json`);
50
+ if (existsSync(listFile)) {
51
+ const content = readFileSync(listFile, "utf-8");
52
+ const data = JSON.parse(content);
53
+ if (data && Array.isArray(data.texts)) {
54
+ const item = data.texts.find((t) => t.aliasName === aliasName);
55
+ if (item && item.id)
56
+ return item.id;
57
+ }
58
+ }
59
+ }
60
+ }
61
+ catch { }
62
+ return null;
63
+ }
64
+ // Resolve Schemes/StateSchemes: Group:Scheme -> designTimeId
65
+ export function resolveSchemeRef(projectDir, schemeType, schemeName) {
66
+ const dir = join(projectDir, "Schemes", schemeType);
67
+ if (!existsSync(dir))
68
+ return null;
69
+ try {
70
+ for (const f of readdirSync(dir)) {
71
+ if (!f.endsWith(".json"))
72
+ continue;
73
+ const content = readFileSync(join(dir, f), "utf-8");
74
+ const data = JSON.parse(content);
75
+ if (data && Array.isArray(data.schemes)) {
76
+ const s = data.schemes.find((x) => x.name === schemeName);
77
+ if (s && s.designTimeId)
78
+ return s.designTimeId;
79
+ }
80
+ }
81
+ }
82
+ catch { }
83
+ return null;
84
+ }
85
+ // Resolve Color Reference: Palette:ColorName -> ID
86
+ export function resolveColorRef(projectDir, colorVal) {
87
+ if (!colorVal.includes(":"))
88
+ return null;
89
+ const [palette, name] = colorVal.split(":");
90
+ const path = join(projectDir, "Resources", `${palette}.colors`);
91
+ if (existsSync(path)) {
92
+ try {
93
+ const content = readFileSync(path, "utf-8");
94
+ const data = JSON.parse(content);
95
+ if (data && Array.isArray(data.colors)) {
96
+ const c = data.colors.find((x) => x.name === name);
97
+ if (c && c.id)
98
+ return c.id;
99
+ }
100
+ }
101
+ catch { }
102
+ }
103
+ return null;
104
+ }
105
+ // Resolve Font Reference: FontStyleName -> designTimeId
106
+ export function resolveFontRef(projectDir, fontName) {
107
+ const path = join(projectDir, "FontStyles", "FontStyles.json");
108
+ if (existsSync(path)) {
109
+ try {
110
+ const content = readFileSync(path, "utf-8");
111
+ const data = JSON.parse(content);
112
+ if (data && Array.isArray(data.fontStyles)) {
113
+ const f = data.fontStyles.find((x) => x.name === fontName);
114
+ if (f && f.designTimeId)
115
+ return f.designTimeId;
116
+ }
117
+ }
118
+ catch { }
119
+ }
120
+ return null;
121
+ }
122
+ // Resolve StyleClass Reference: StyleClassName -> designTimeId
123
+ export function resolveStyleClassRef(projectDir, className) {
124
+ const path = join(projectDir, "DefaultStyles", "DefaultStyles.json");
125
+ if (!existsSync(path))
126
+ return null;
127
+ try {
128
+ const content = readFileSync(path, "utf-8");
129
+ const data = JSON.parse(content);
130
+ if (data && Array.isArray(data.componentStyles)) {
131
+ const findInNode = (node) => {
132
+ if (node.name === className && node.designTimeId)
133
+ return node.designTimeId;
134
+ if (Array.isArray(node.children)) {
135
+ for (const child of node.children) {
136
+ const res = findInNode(child);
137
+ if (res)
138
+ return res;
139
+ }
140
+ }
141
+ return null;
142
+ };
143
+ for (const style of data.componentStyles) {
144
+ const res = findInNode(style);
145
+ if (res)
146
+ return res;
147
+ }
148
+ }
149
+ }
150
+ catch { }
151
+ return null;
152
+ }
153
+ // Resolve Template Reference: templateName -> designTimeId
154
+ export function resolveTemplateRef(projectDir, templateName) {
155
+ const path = join(projectDir, "ControlTemplate", `${templateName}.json`);
156
+ if (existsSync(path)) {
157
+ try {
158
+ const content = readFileSync(path, "utf-8");
159
+ const data = JSON.parse(content);
160
+ if (data && data.designTimeId)
161
+ return data.designTimeId;
162
+ }
163
+ catch { }
164
+ }
165
+ return null;
166
+ }
167
+ // Resolve Media Reference: mediaPath -> ID
168
+ export function resolveMediaRef(projectDir, mediaPath) {
169
+ const filename = basename(mediaPath.replace(/\\/g, "/"));
170
+ const resDir = join(projectDir, "Resources");
171
+ if (!existsSync(resDir))
172
+ return null;
173
+ try {
174
+ for (const sub of readdirSync(resDir)) {
175
+ const subPath = join(resDir, sub);
176
+ if (existsSync(subPath)) {
177
+ for (const file of readdirSync(subPath)) {
178
+ if (file.endsWith("Collection.json")) {
179
+ const content = readFileSync(join(subPath, file), "utf-8");
180
+ const data = JSON.parse(content);
181
+ if (data && Array.isArray(data.mediaItems)) {
182
+ const item = data.mediaItems.find((x) => x.name === filename);
183
+ if (item && item.id)
184
+ return item.id;
185
+ }
186
+ }
187
+ }
188
+ }
189
+ }
190
+ }
191
+ catch { }
192
+ return null;
193
+ }
194
+ // Tab indented JSON output
195
+ export function writeTabIndentedJson(filePath, data) {
196
+ const content = JSON.stringify(data, null, "\t");
197
+ writeFileSync(filePath, content, "utf-8");
198
+ }
@@ -0,0 +1,174 @@
1
+ import { existsSync, readFileSync } from "fs";
2
+ import { join } from "path";
3
+ import { newDesignTimeId, resolveTextRef, resolveSchemeRef, resolveColorRef, resolveFontRef, resolveStyleClassRef, resolveTemplateRef, resolveMediaRef, } from "./visuDashboardIO.js";
4
+ // Clean JSON helper for manifests (strips comments & trailing commas)
5
+ export function cleanJson(content) {
6
+ content = content.replace(/\/\*[\s\S]*?\*\//g, "");
7
+ content = content.replace(/^[ \t]*\/\/.*$/gm, "");
8
+ content = content.replace(/,(\s*[\]}])/g, "$1");
9
+ return content;
10
+ }
11
+ // Load control manifest, merging inherited properties
12
+ const manifestCache = {};
13
+ export function loadControlManifest(projectDir, controlId) {
14
+ if (manifestCache[controlId])
15
+ return manifestCache[controlId];
16
+ const userPath = join(projectDir, "Runtime", "DesignerRuntime", "res", "components", "user", controlId, `${controlId}.json`);
17
+ const sigmatekPath = join(projectDir, "Runtime", "DesignerRuntime", "res", "components", "sigmatek", controlId, `${controlId}.json`);
18
+ let manifest = null;
19
+ if (existsSync(userPath)) {
20
+ manifest = JSON.parse(cleanJson(readFileSync(userPath, "utf-8")));
21
+ }
22
+ else if (existsSync(sigmatekPath)) {
23
+ manifest = JSON.parse(cleanJson(readFileSync(sigmatekPath, "utf-8")));
24
+ }
25
+ if (!manifest)
26
+ return null;
27
+ // Resolve base Component properties recursively
28
+ if (manifest.baseComponent && manifest.baseComponent !== controlId) {
29
+ const parent = loadControlManifest(projectDir, manifest.baseComponent);
30
+ if (parent && parent.properties) {
31
+ const mergedProps = [...(parent.properties || [])];
32
+ for (const prop of manifest.properties || []) {
33
+ const idx = mergedProps.findIndex((p) => p.name === prop.name);
34
+ if (idx >= 0)
35
+ mergedProps[idx] = prop;
36
+ else
37
+ mergedProps.push(prop);
38
+ }
39
+ manifest.properties = mergedProps;
40
+ }
41
+ }
42
+ manifestCache[controlId] = manifest;
43
+ return manifest;
44
+ }
45
+ // Format UUID function block ID
46
+ export function resolveFunctionBlockRef(projectDir, fbName) {
47
+ const path = join(projectDir, "Functionblocks", `${fbName}.json`);
48
+ if (existsSync(path)) {
49
+ try {
50
+ const data = JSON.parse(cleanJson(readFileSync(path, "utf-8")));
51
+ if (data && data.id) {
52
+ let idStr = data.id;
53
+ if (idStr.startsWith("_")) {
54
+ idStr = idStr.substring(1);
55
+ }
56
+ if (idStr.length === 32) {
57
+ return `${idStr.substring(0, 8)}-${idStr.substring(8, 12)}-${idStr.substring(12, 16)}-${idStr.substring(16, 20)}-${idStr.substring(20)}`;
58
+ }
59
+ return idStr;
60
+ }
61
+ }
62
+ catch { }
63
+ }
64
+ return null;
65
+ }
66
+ // Encode property based on manifest entry and sourceType
67
+ export function encodeProperty(projectDir, propName, manifestProp, sourceType, value) {
68
+ const propType = manifestProp ? manifestProp.propertyType || "none" : "none";
69
+ let typeId = 0;
70
+ let propTypeId = 2;
71
+ let refId;
72
+ switch (sourceType) {
73
+ case "constString":
74
+ case "constNumber":
75
+ case "constBool":
76
+ if (propType === "css") {
77
+ if (propName.startsWith("--theme-sig-element-")) {
78
+ typeId = 17;
79
+ }
80
+ else {
81
+ typeId = 0;
82
+ }
83
+ propTypeId = 2;
84
+ }
85
+ else if (propType === "rotation") {
86
+ typeId = 17;
87
+ propTypeId = 12;
88
+ }
89
+ else if (propType === "variable") {
90
+ typeId = 0;
91
+ propTypeId = 1;
92
+ }
93
+ else {
94
+ typeId = 0;
95
+ propTypeId = 2;
96
+ }
97
+ break;
98
+ case "datapoint":
99
+ typeId = 4;
100
+ propTypeId = 1;
101
+ refId = newDesignTimeId(); // freestanding UUID
102
+ if (typeof value === "string" && !value.includes(":")) {
103
+ value = `0:${value}`;
104
+ }
105
+ break;
106
+ case "text":
107
+ typeId = 5;
108
+ propTypeId = 1;
109
+ if (typeof value === "string" && value.includes(":")) {
110
+ const [listName, aliasName] = value.split(":");
111
+ refId = resolveTextRef(projectDir, listName, aliasName) || undefined;
112
+ }
113
+ break;
114
+ case "colorScheme":
115
+ // If it contains a colon, check if it resolves as a palette color first, then as colorScheme
116
+ if (typeof value === "string" && value.includes(":")) {
117
+ const paletteColorId = resolveColorRef(projectDir, value);
118
+ if (paletteColorId) {
119
+ typeId = 9;
120
+ propTypeId = 2;
121
+ refId = paletteColorId;
122
+ break;
123
+ }
124
+ }
125
+ typeId = 18;
126
+ propTypeId = 2;
127
+ refId = resolveSchemeRef(projectDir, "ColorSchemes", String(value)) || undefined;
128
+ break;
129
+ case "stateScheme":
130
+ typeId = 21;
131
+ propTypeId = 1;
132
+ refId = resolveSchemeRef(projectDir, "StateSchemes", String(value)) || undefined;
133
+ break;
134
+ case "functionBlock":
135
+ typeId = 1;
136
+ propTypeId = 3;
137
+ refId = resolveFunctionBlockRef(projectDir, String(value)) || undefined;
138
+ break;
139
+ case "compositeControl":
140
+ typeId = 23;
141
+ propTypeId = 5;
142
+ refId = resolveTemplateRef(projectDir, String(value)) || undefined;
143
+ break;
144
+ case "imageOrMedia":
145
+ typeId = 15;
146
+ propTypeId = 2;
147
+ refId = resolveMediaRef(projectDir, String(value)) || undefined;
148
+ break;
149
+ case "fontStyle":
150
+ typeId = 26;
151
+ propTypeId = 6;
152
+ refId = resolveFontRef(projectDir, String(value)) || undefined;
153
+ break;
154
+ case "styleClass":
155
+ typeId = 34;
156
+ propTypeId = 14;
157
+ refId = resolveStyleClassRef(projectDir, String(value)) || undefined;
158
+ break;
159
+ default:
160
+ // Fallback
161
+ typeId = 0;
162
+ propTypeId = 2;
163
+ }
164
+ const result = {
165
+ name: propName,
166
+ value,
167
+ typeId,
168
+ propTypeId,
169
+ };
170
+ if (refId) {
171
+ result.refId = refId;
172
+ }
173
+ return result;
174
+ }
@@ -0,0 +1,262 @@
1
+ import { writeFileSync } from "fs";
2
+ import { join } from "path";
3
+ import { randomUUID } from "crypto";
4
+ import { VISUDESIGNER_EXE, SCRATCH, killVisuDesigner } from "./engine.js";
5
+ import { runEngineScript } from "./scriptRunner.js";
6
+ import { ensureScratch } from "../core/scratch.js";
7
+ function emitStr(s) {
8
+ return `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r")}"`;
9
+ }
10
+ function emitPyScalar(v) {
11
+ if (v === null || v === undefined)
12
+ return "None";
13
+ if (typeof v === "boolean")
14
+ return v ? "True" : "False";
15
+ if (typeof v === "number")
16
+ return String(v);
17
+ return emitStr(String(v));
18
+ }
19
+ function emitStrList(arr) {
20
+ return `[${arr.map(emitStr).join(", ")}]`;
21
+ }
22
+ function emitOptStrList(arr) {
23
+ return arr && arr.length > 0 ? emitStrList(arr) : "None";
24
+ }
25
+ function emitTextElementsForEntry(entry) {
26
+ const { id, ...langs } = entry;
27
+ const langKeys = Object.keys(langs).filter((k) => langs[k] !== undefined);
28
+ if (langKeys.length === 0) {
29
+ return [`lvd.TextElement(${emitStr(id)})`];
30
+ }
31
+ return langKeys.map((lang) => `lvd.TextElement(${emitStr(id)}, ${emitStr(lang)}, ${emitStr(langs[lang])})`);
32
+ }
33
+ function emitTextList(def) {
34
+ let elements = "None";
35
+ if (def.texts && def.texts.length > 0) {
36
+ const elems = def.texts.flatMap(emitTextElementsForEntry);
37
+ elements = `[${elems.join(", ")}]`;
38
+ }
39
+ const rev = def.revision ? emitStr(def.revision) : "None";
40
+ return `lvd.TextList(${emitStr(def.name)}, ${elements}, ${rev})`;
41
+ }
42
+ function emitTextListForRemove(def) {
43
+ const elems = def.ids.map((id) => `lvd.TextElement(${emitStr(id)})`).join(", ");
44
+ return `lvd.TextList(${emitStr(def.name)}, [${elems}], None)`;
45
+ }
46
+ function emitPropertySet(def) {
47
+ return `lvd.PropertySet(${emitStr(def.element)}, ${emitStr(def.property)}, ${emitPyScalar(def.value)})`;
48
+ }
49
+ function emitSchemeEntry(e) {
50
+ const prop = e.property ? emitStr(e.property) : "None";
51
+ const val = emitPyScalar(e.value ?? null);
52
+ const newPos = e.new_position !== undefined ? String(e.new_position) : "None";
53
+ return `lvd.SchemeEntry(${e.position}, ${prop}, ${val}, ${newPos})`;
54
+ }
55
+ function emitSchemeFull(def) {
56
+ const inp = def.input ? emitStr(def.input) : "None";
57
+ let entries = "None";
58
+ if (def.entries && def.entries.length > 0) {
59
+ entries = `[${def.entries.map(emitSchemeEntry).join(", ")}]`;
60
+ }
61
+ return `lvd.Scheme(${emitStr(def.scheme_type)}, ${emitStr(def.name)}, ${inp}, ${entries})`;
62
+ }
63
+ function emitSchemeForRemove(def) {
64
+ return `lvd.Scheme(${emitStr(def.scheme_type)}, ${emitStr(def.name)}, None, None)`;
65
+ }
66
+ // Positions or entries to remove
67
+ function emitSchemeForRemoveEntries(def) {
68
+ const positions = def.positions ?? def.entries?.map((e) => e.position) ?? [];
69
+ const elems = positions.map((p) => `lvd.SchemeEntry(${p})`).join(", ");
70
+ return `lvd.Scheme(${emitStr(def.scheme_type)}, ${emitStr(def.name)}, None, [${elems}])`;
71
+ }
72
+ function emitSchemeForMoves(def) {
73
+ const moves = def.moves ?? [];
74
+ const elems = moves.map((m) => `lvd.SchemeEntry(${m.from}, None, None, ${m.to})`).join(", ");
75
+ return `lvd.Scheme(${emitStr(def.scheme_type)}, ${emitStr(def.name)}, None, [${elems}])`;
76
+ }
77
+ function emitSchemeForInput(def) {
78
+ const inp = def.input ? emitStr(def.input) : "None";
79
+ return `lvd.Scheme(${emitStr(def.scheme_type)}, ${emitStr(def.name)}, ${inp}, None)`;
80
+ }
81
+ function emitMediaItem(def) {
82
+ return `lvd.MediaItem(${emitStr(def.media_type)}, ${emitStr(def.path ?? def.name ?? "")})`;
83
+ }
84
+ export function buildVisuScript(lvpPath, ops, logPath, stepsPath, saveAtEnd = true) {
85
+ const lines = [];
86
+ const expectedSteps = ["LoadProject"];
87
+ lines.push(' f_step = open(r"' + stepsPath + '", "a", encoding="utf-8")', ' f_step.write("STEP LoadProject OK\\n")', " f_step.close()", "");
88
+ for (let i = 0; i < ops.length; i++) {
89
+ const op = ops[i];
90
+ if (!op)
91
+ continue;
92
+ const label = `${i}_${op.type}`;
93
+ expectedSteps.push(label);
94
+ const opLines = [];
95
+ switch (op.type) {
96
+ case "update_all_stations":
97
+ opLines.push("lvd.UpdateAllStations(prj)");
98
+ break;
99
+ case "update_station":
100
+ opLines.push(`lvd.UpdateStation(prj, ${op.station_nr})`);
101
+ break;
102
+ case "publish":
103
+ opLines.push(`lvd.PublishProject(prj, ${op.debug ? "True" : "False"})`);
104
+ break;
105
+ case "add_text_lists":
106
+ opLines.push(`lvd.AddTextLists(prj, [${op.text_lists.map(emitTextList).join(", ")}])`);
107
+ break;
108
+ case "remove_text_lists":
109
+ opLines.push(`lvd.RemoveTextLists(prj, ${emitStrList(op.names)})`);
110
+ break;
111
+ case "add_texts":
112
+ opLines.push(`lvd.AddTexts(prj, [${op.text_lists.map(emitTextList).join(", ")}])`);
113
+ break;
114
+ case "remove_texts":
115
+ opLines.push(`lvd.RemoveTexts(prj, [${op.text_lists.map(emitTextListForRemove).join(", ")}])`);
116
+ break;
117
+ case "change_texts":
118
+ opLines.push(`lvd.ChangeTexts(prj, [${op.text_lists.map(emitTextList).join(", ")}])`);
119
+ break;
120
+ case "change_component_texts":
121
+ opLines.push(`lvd.ChangeComponentTexts(prj, [${op.text_lists.map(emitTextList).join(", ")}])`);
122
+ break;
123
+ case "set_text_list_revisions":
124
+ opLines.push(`lvd.SetTextListRevisions(prj, [${op.text_lists.map(emitTextList).join(", ")}])`);
125
+ break;
126
+ case "set_component_text_list_revisions":
127
+ opLines.push(`lvd.SetComponentTextListRevisions(prj, [${op.text_lists.map(emitTextList).join(", ")}])`);
128
+ break;
129
+ case "csv_export_text_lists":
130
+ opLines.push(`lvd.CsvExportTextLists(prj, ${emitStr(op.csv_path)}, ${emitOptStrList(op.text_lists)}, ${emitOptStrList(op.languages)})`);
131
+ break;
132
+ case "csv_import_text_lists":
133
+ opLines.push(`lvd.CsvImportTextLists(prj, ${emitStrList(op.file_paths)}, ${emitOptStrList(op.text_lists)}, ${emitOptStrList(op.languages)})`);
134
+ break;
135
+ case "csv_export_component_text_lists":
136
+ opLines.push(`lvd.CsvExportComponentTextLists(prj, ${emitStr(op.csv_path)}, ${emitOptStrList(op.text_lists)}, ${emitOptStrList(op.languages)})`);
137
+ break;
138
+ case "csv_import_component_text_lists":
139
+ opLines.push(`lvd.CsvImportComponentTextLists(prj, ${emitStrList(op.file_paths)}, ${emitOptStrList(op.text_lists)}, ${emitOptStrList(op.languages)})`);
140
+ break;
141
+ case "set_datapoint_properties":
142
+ opLines.push(`lvd.SetDatapointProperties(prj, [${op.properties.map(emitPropertySet).join(", ")}])`);
143
+ break;
144
+ case "set_datatype_properties":
145
+ opLines.push(`lvd.SetDataTypeProperties(prj, [${op.properties.map(emitPropertySet).join(", ")}])`);
146
+ break;
147
+ case "add_schemes":
148
+ opLines.push(`lvd.AddSchemes(prj, [${op.schemes.map(emitSchemeFull).join(", ")}])`);
149
+ break;
150
+ case "remove_schemes":
151
+ opLines.push(`lvd.RemoveSchemes(prj, [${op.schemes.map(emitSchemeForRemove).join(", ")}])`);
152
+ break;
153
+ case "add_scheme_entries":
154
+ opLines.push(`lvd.AddSchemeEntries(prj, [${op.schemes.map(emitSchemeFull).join(", ")}])`);
155
+ break;
156
+ case "remove_scheme_entries":
157
+ opLines.push(`lvd.RemoveSchemeEntries(prj, [${op.schemes.map(emitSchemeForRemoveEntries).join(", ")}])`);
158
+ break;
159
+ case "move_scheme_entries":
160
+ opLines.push(`lvd.MoveSchemeEntries(prj, [${op.schemes.map(emitSchemeForMoves).join(", ")}])`);
161
+ break;
162
+ case "set_scheme_inputs":
163
+ opLines.push(`lvd.SetSchemeInputs(prj, [${op.schemes.map(emitSchemeForInput).join(", ")}])`);
164
+ break;
165
+ case "set_scheme_properties":
166
+ opLines.push(`lvd.SetSchemeProperties(prj, ${emitStr(op.scheme_type)}, [${op.properties.map(emitPropertySet).join(", ")}])`);
167
+ break;
168
+ case "set_scheme_entry_properties":
169
+ opLines.push(`lvd.SetSchemeEntryProperties(prj, [${op.schemes.map(emitSchemeFull).join(", ")}])`);
170
+ break;
171
+ case "add_media_items": {
172
+ const overwrite = op.overwrite ? "True" : "False";
173
+ opLines.push(`lvd.AddMediaItems(prj, [${op.items.map(emitMediaItem).join(", ")}], ${overwrite})`);
174
+ break;
175
+ }
176
+ case "remove_media_items":
177
+ opLines.push(`lvd.RemoveMediaItems(prj, [${op.items.map(emitMediaItem).join(", ")}])`);
178
+ break;
179
+ case "add_code_modules":
180
+ opLines.push(`lvd.AddCodeModules(prj, ${emitStrList(op.paths)})`);
181
+ break;
182
+ case "remove_code_modules":
183
+ opLines.push(`lvd.RemoveCodeModules(prj, ${emitStrList(op.names)})`);
184
+ break;
185
+ case "update_property_values":
186
+ opLines.push("lvd.UpdatePropertyValues(prj)");
187
+ break;
188
+ case "download": {
189
+ const flags = op.flags ?? 0;
190
+ const addRuntime = op.add_runtime ? "True" : "False";
191
+ opLines.push(`lvd.DownloadProject(prj, ${emitStr(op.connection)}, ${flags}, ${addRuntime})`);
192
+ break;
193
+ }
194
+ }
195
+ for (const opLine of opLines) {
196
+ lines.push(` ${opLine}`);
197
+ }
198
+ lines.push(` f_step = open(r"${stepsPath}", "a", encoding="utf-8")`, ` f_step.write("STEP ${label} OK\\n")`, ` f_step.close()`, "");
199
+ }
200
+ if (saveAtEnd) {
201
+ expectedSteps.push("SaveProject");
202
+ lines.push(" lvd.SaveProject(prj)", ` f_step = open(r"${stepsPath}", "a", encoding="utf-8")`, ` f_step.write("STEP SaveProject OK\\n")`, ` f_step.close()`, "");
203
+ }
204
+ expectedSteps.push("CloseProject");
205
+ lines.push(" lvd.CloseProject(prj)", ` f_step = open(r"${stepsPath}", "a", encoding="utf-8")`, ` f_step.write("STEP CloseProject OK\\n")`, ` f_step.close()`, "");
206
+ const finalScript = [
207
+ "import sys",
208
+ "import traceback",
209
+ `log_file = open(${emitStr(logPath)}, "w", encoding="utf-8")`,
210
+ "sys.stdout = log_file",
211
+ "sys.stderr = log_file",
212
+ "",
213
+ "import sigmatek.lasal.lvd as lvd",
214
+ "lvd.SetExceptionOnError(True)",
215
+ "",
216
+ "try:",
217
+ ` prj = lvd.LoadProject(${emitStr(lvpPath)})`,
218
+ " if prj is None: raise RuntimeError('Failed to load project')",
219
+ "",
220
+ lines.join("\n"),
221
+ "except Exception as e:",
222
+ " print('(ERROR) Exception occurred during script execution:')",
223
+ " traceback.print_exc()",
224
+ " sys.exit(1)",
225
+ "finally:",
226
+ " log_file.close()",
227
+ ].join("\n") + "\n";
228
+ return {
229
+ script: finalScript,
230
+ expectedSteps,
231
+ };
232
+ }
233
+ export async function runVisuOps(lvpPath, ops, saveAtEnd = true, timeoutMs = 180_000) {
234
+ ensureScratch();
235
+ const id = randomUUID();
236
+ const scriptPath = join(SCRATCH, `visu_${id}.py`);
237
+ const logPath = join(SCRATCH, `visu_${id}.log`);
238
+ const stepsPath = join(SCRATCH, `visu_${id}.steps`);
239
+ const { script, expectedSteps } = buildVisuScript(lvpPath, ops, logPath, stepsPath, saveAtEnd);
240
+ writeFileSync(scriptPath, script, "utf-8");
241
+ killVisuDesigner();
242
+ const result = await runEngineScript(scriptPath, {
243
+ exe: VISUDESIGNER_EXE,
244
+ argsFor: (p) => ["--script", p],
245
+ timeoutMs,
246
+ logEncoding: "utf-8",
247
+ killOnFailure: killVisuDesigner,
248
+ expectedSteps,
249
+ stepsPath,
250
+ }, logPath);
251
+ return {
252
+ ok: result.ok,
253
+ exitCode: result.exitCode,
254
+ logPath: result.logPath,
255
+ errors: result.errors,
256
+ warnings: result.warnings,
257
+ durationMs: result.durationMs,
258
+ steps: result.steps,
259
+ timedOut: result.timedOut,
260
+ hints: result.hints,
261
+ };
262
+ }