@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.
Files changed (281) hide show
  1. package/CHANGELOG.md +115 -0
  2. package/LICENSE +21 -0
  3. package/README.md +228 -2
  4. package/cordis.patch.yml +122 -0
  5. package/locale/en.json +6 -0
  6. package/locale/zh.json +6 -0
  7. package/package.json +77 -3
  8. package/vendor/ftview/README.md +55 -0
  9. package/vendor/ftview/SKILL.md +74 -0
  10. package/vendor/ftview/lib/checks.d.ts +27 -0
  11. package/vendor/ftview/lib/checks.js +39 -0
  12. package/vendor/ftview/lib/core.d.ts +208 -0
  13. package/vendor/ftview/lib/core.js +160 -0
  14. package/vendor/ftview/lib/edit/drafts.d.ts +49 -0
  15. package/vendor/ftview/lib/edit/drafts.js +84 -0
  16. package/vendor/ftview/lib/edit/operations.d.ts +62 -0
  17. package/vendor/ftview/lib/edit/operations.js +126 -0
  18. package/vendor/ftview/lib/edit/prepare.d.ts +38 -0
  19. package/vendor/ftview/lib/edit/prepare.js +133 -0
  20. package/vendor/ftview/lib/edit/tools.d.ts +25 -0
  21. package/vendor/ftview/lib/edit/tools.js +198 -0
  22. package/vendor/ftview/lib/edit.d.ts +30 -0
  23. package/vendor/ftview/lib/edit.js +38 -0
  24. package/vendor/ftview/lib/index.d.ts +31 -0
  25. package/vendor/ftview/lib/index.js +36 -0
  26. package/vendor/ftview/lib/model.d.ts +80 -0
  27. package/vendor/ftview/lib/model.js +267 -0
  28. package/vendor/ftview/lib/session.d.ts +48 -0
  29. package/vendor/ftview/lib/session.js +98 -0
  30. package/vendor/ftview/lib/skill.d.ts +29 -0
  31. package/vendor/ftview/lib/skill.js +43 -0
  32. package/vendor/ftview/lib/studio/discover.d.ts +36 -0
  33. package/vendor/ftview/lib/studio/discover.js +87 -0
  34. package/vendor/ftview/lib/studio/preflight.d.ts +51 -0
  35. package/vendor/ftview/lib/studio/preflight.js +64 -0
  36. package/vendor/ftview/lib/studio/tools.d.ts +15 -0
  37. package/vendor/ftview/lib/studio/tools.js +83 -0
  38. package/vendor/ftview/lib/studio.d.ts +30 -0
  39. package/vendor/ftview/lib/studio.js +37 -0
  40. package/vendor/ftview/lib/tools.d.ts +14 -0
  41. package/vendor/ftview/lib/tools.js +202 -0
  42. package/vendor/l5x/README.md +62 -0
  43. package/vendor/l5x/SKILL.md +81 -0
  44. package/vendor/l5x/lib/approvals.d.ts +50 -0
  45. package/vendor/l5x/lib/approvals.js +93 -0
  46. package/vendor/l5x/lib/client.js +412 -0
  47. package/vendor/l5x/lib/client.js.map +1 -0
  48. package/vendor/l5x/lib/core/errors.d.ts +38 -0
  49. package/vendor/l5x/lib/core/errors.js +48 -0
  50. package/vendor/l5x/lib/core/params.d.ts +49 -0
  51. package/vendor/l5x/lib/core/params.js +39 -0
  52. package/vendor/l5x/lib/core/result.d.ts +46 -0
  53. package/vendor/l5x/lib/core/result.js +21 -0
  54. package/vendor/l5x/lib/core/tool.d.ts +102 -0
  55. package/vendor/l5x/lib/core/tool.js +19 -0
  56. package/vendor/l5x/lib/drafts.d.ts +66 -0
  57. package/vendor/l5x/lib/drafts.js +98 -0
  58. package/vendor/l5x/lib/editing.d.ts +30 -0
  59. package/vendor/l5x/lib/editing.js +42 -0
  60. package/vendor/l5x/lib/engine/acd-edit.d.ts +13 -0
  61. package/vendor/l5x/lib/engine/acd-edit.js +115 -0
  62. package/vendor/l5x/lib/engine/analysis.d.ts +22 -0
  63. package/vendor/l5x/lib/engine/analysis.js +258 -0
  64. package/vendor/l5x/lib/engine/capabilities.d.ts +9 -0
  65. package/vendor/l5x/lib/engine/capabilities.js +40 -0
  66. package/vendor/l5x/lib/engine/change-set.d.ts +122 -0
  67. package/vendor/l5x/lib/engine/change-set.js +446 -0
  68. package/vendor/l5x/lib/engine/configuration.d.ts +21 -0
  69. package/vendor/l5x/lib/engine/configuration.js +42 -0
  70. package/vendor/l5x/lib/engine/details.d.ts +47 -0
  71. package/vendor/l5x/lib/engine/details.js +173 -0
  72. package/vendor/l5x/lib/engine/direct-polling.d.ts +91 -0
  73. package/vendor/l5x/lib/engine/direct-polling.js +193 -0
  74. package/vendor/l5x/lib/engine/download-review.d.ts +15 -0
  75. package/vendor/l5x/lib/engine/download-review.js +63 -0
  76. package/vendor/l5x/lib/engine/logic-syntax.d.ts +60 -0
  77. package/vendor/l5x/lib/engine/logic-syntax.js +237 -0
  78. package/vendor/l5x/lib/engine/observations.d.ts +41 -0
  79. package/vendor/l5x/lib/engine/observations.js +67 -0
  80. package/vendor/l5x/lib/engine/project-analysis.d.ts +131 -0
  81. package/vendor/l5x/lib/engine/project-analysis.js +314 -0
  82. package/vendor/l5x/lib/engine/review-evidence.d.ts +8 -0
  83. package/vendor/l5x/lib/engine/review-evidence.js +49 -0
  84. package/vendor/l5x/lib/engine/review.d.ts +18 -0
  85. package/vendor/l5x/lib/engine/review.js +123 -0
  86. package/vendor/l5x/lib/engine/studio-sdk.d.ts +101 -0
  87. package/vendor/l5x/lib/engine/studio-sdk.js +571 -0
  88. package/vendor/l5x/lib/engine/tag-resolution.d.ts +20 -0
  89. package/vendor/l5x/lib/engine/tag-resolution.js +169 -0
  90. package/vendor/l5x/lib/engine/usage.d.ts +16 -0
  91. package/vendor/l5x/lib/engine/usage.js +92 -0
  92. package/vendor/l5x/lib/engine/xml-regions.d.ts +23 -0
  93. package/vendor/l5x/lib/engine/xml-regions.js +77 -0
  94. package/vendor/l5x/lib/fs.d.ts +17 -0
  95. package/vendor/l5x/lib/fs.js +36 -0
  96. package/vendor/l5x/lib/index.d.ts +36 -0
  97. package/vendor/l5x/lib/index.js +42 -0
  98. package/vendor/l5x/lib/live.d.ts +48 -0
  99. package/vendor/l5x/lib/live.js +48 -0
  100. package/vendor/l5x/lib/model/aois.d.ts +39 -0
  101. package/vendor/l5x/lib/model/aois.js +112 -0
  102. package/vendor/l5x/lib/model/errors.d.ts +15 -0
  103. package/vendor/l5x/lib/model/errors.js +18 -0
  104. package/vendor/l5x/lib/model/index.d.ts +20 -0
  105. package/vendor/l5x/lib/model/index.js +19 -0
  106. package/vendor/l5x/lib/model/module.d.ts +37 -0
  107. package/vendor/l5x/lib/model/module.js +54 -0
  108. package/vendor/l5x/lib/model/modules.d.ts +24 -0
  109. package/vendor/l5x/lib/model/modules.js +55 -0
  110. package/vendor/l5x/lib/model/parse.d.ts +32 -0
  111. package/vendor/l5x/lib/model/parse.js +116 -0
  112. package/vendor/l5x/lib/model/project.d.ts +15 -0
  113. package/vendor/l5x/lib/model/project.js +31 -0
  114. package/vendor/l5x/lib/model/routines.d.ts +71 -0
  115. package/vendor/l5x/lib/model/routines.js +183 -0
  116. package/vendor/l5x/lib/model/tags.d.ts +51 -0
  117. package/vendor/l5x/lib/model/tags.js +119 -0
  118. package/vendor/l5x/lib/model/types.d.ts +304 -0
  119. package/vendor/l5x/lib/model/types.js +12 -0
  120. package/vendor/l5x/lib/model/udts.d.ts +34 -0
  121. package/vendor/l5x/lib/model/udts.js +81 -0
  122. package/vendor/l5x/lib/model/xml.d.ts +139 -0
  123. package/vendor/l5x/lib/model/xml.js +291 -0
  124. package/vendor/l5x/lib/model/xref.d.ts +28 -0
  125. package/vendor/l5x/lib/model/xref.js +154 -0
  126. package/vendor/l5x/lib/session.d.ts +119 -0
  127. package/vendor/l5x/lib/session.js +236 -0
  128. package/vendor/l5x/lib/skill.d.ts +30 -0
  129. package/vendor/l5x/lib/skill.js +44 -0
  130. package/vendor/l5x/lib/studio-state.d.ts +77 -0
  131. package/vendor/l5x/lib/studio-state.js +79 -0
  132. package/vendor/l5x/lib/studio.d.ts +29 -0
  133. package/vendor/l5x/lib/studio.js +40 -0
  134. package/vendor/l5x/lib/tools/checks.d.ts +19 -0
  135. package/vendor/l5x/lib/tools/checks.js +140 -0
  136. package/vendor/l5x/lib/tools/common.d.ts +41 -0
  137. package/vendor/l5x/lib/tools/common.js +84 -0
  138. package/vendor/l5x/lib/tools/controller.d.ts +21 -0
  139. package/vendor/l5x/lib/tools/controller.js +170 -0
  140. package/vendor/l5x/lib/tools/drafts.d.ts +23 -0
  141. package/vendor/l5x/lib/tools/drafts.js +270 -0
  142. package/vendor/l5x/lib/tools/live.d.ts +21 -0
  143. package/vendor/l5x/lib/tools/live.js +175 -0
  144. package/vendor/l5x/lib/tools/read.d.ts +19 -0
  145. package/vendor/l5x/lib/tools/read.js +245 -0
  146. package/vendor/l5x/lib/tools/references.d.ts +20 -0
  147. package/vendor/l5x/lib/tools/references.js +269 -0
  148. package/vendor/l5x/lib/tools/studio.d.ts +21 -0
  149. package/vendor/l5x/lib/tools/studio.js +193 -0
  150. package/vendor/l5x/poll-worker.mjs +77 -0
  151. package/vendor/l5x/studio-bridge/NuGet.Config +1 -0
  152. package/vendor/l5x/studio-bridge/Program.cs +304 -0
  153. package/vendor/l5x/studio-bridge/StudioBridge.csproj +11 -0
  154. package/vendor/mwi/README.md +60 -0
  155. package/vendor/mwi/SKILL.md +87 -0
  156. package/vendor/mwi/bridge/README.md +8 -0
  157. package/vendor/mwi/bridge/mw_bridge.ps1 +2763 -0
  158. package/vendor/mwi/bridge/sdk-interop-importer.cs +30 -0
  159. package/vendor/mwi/bridge/sdk-variable-reader.cs +22 -0
  160. package/vendor/mwi/bridge/start_bridge.cmd +21 -0
  161. package/vendor/mwi/bridge/watch_dialogs.cmd +13 -0
  162. package/vendor/mwi/bridge/watch_dialogs.ps1 +153 -0
  163. package/vendor/mwi/code/engine/motionworks_iec_mcp/__init__.py +14 -0
  164. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/__init__.cpython-312.pyc +0 -0
  165. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/block_interfaces.cpython-312.pyc +0 -0
  166. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/cfb.cpython-312.pyc +0 -0
  167. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/datatypes.cpython-312.pyc +0 -0
  168. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/declarations.cpython-312.pyc +0 -0
  169. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/errors.cpython-312.pyc +0 -0
  170. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/graphical_listing.cpython-312.pyc +0 -0
  171. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/grid.cpython-312.pyc +0 -0
  172. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/knowledge.cpython-312.pyc +0 -0
  173. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/mwt_bind.cpython-312.pyc +0 -0
  174. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/navigation.cpython-312.pyc +0 -0
  175. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/program_checks.cpython-312.pyc +0 -0
  176. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/project.cpython-312.pyc +0 -0
  177. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/staging.cpython-312.pyc +0 -0
  178. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/structure.cpython-312.pyc +0 -0
  179. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/task_bindings.cpython-312.pyc +0 -0
  180. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/tasks.cpython-312.pyc +0 -0
  181. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/tree.cpython-312.pyc +0 -0
  182. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/validation.cpython-312.pyc +0 -0
  183. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/variable_edit.cpython-312.pyc +0 -0
  184. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/variables.cpython-312.pyc +0 -0
  185. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/workflow.cpython-312.pyc +0 -0
  186. package/vendor/mwi/code/engine/motionworks_iec_mcp/block_interfaces.py +275 -0
  187. package/vendor/mwi/code/engine/motionworks_iec_mcp/build.py +339 -0
  188. package/vendor/mwi/code/engine/motionworks_iec_mcp/capture.py +448 -0
  189. package/vendor/mwi/code/engine/motionworks_iec_mcp/cfb.py +765 -0
  190. package/vendor/mwi/code/engine/motionworks_iec_mcp/com.py +268 -0
  191. package/vendor/mwi/code/engine/motionworks_iec_mcp/datatypes.py +204 -0
  192. package/vendor/mwi/code/engine/motionworks_iec_mcp/declarations.py +399 -0
  193. package/vendor/mwi/code/engine/motionworks_iec_mcp/descriptions.py +30 -0
  194. package/vendor/mwi/code/engine/motionworks_iec_mcp/eip.py +292 -0
  195. package/vendor/mwi/code/engine/motionworks_iec_mcp/errors.py +36 -0
  196. package/vendor/mwi/code/engine/motionworks_iec_mcp/graphical_listing.py +208 -0
  197. package/vendor/mwi/code/engine/motionworks_iec_mcp/grid.py +227 -0
  198. package/vendor/mwi/code/engine/motionworks_iec_mcp/ide.py +317 -0
  199. package/vendor/mwi/code/engine/motionworks_iec_mcp/iec.py +317 -0
  200. package/vendor/mwi/code/engine/motionworks_iec_mcp/installed_help.py +151 -0
  201. package/vendor/mwi/code/engine/motionworks_iec_mcp/knowledge.py +218 -0
  202. package/vendor/mwi/code/engine/motionworks_iec_mcp/libraries.py +250 -0
  203. package/vendor/mwi/code/engine/motionworks_iec_mcp/manuals.py +520 -0
  204. package/vendor/mwi/code/engine/motionworks_iec_mcp/mwt_bind.py +133 -0
  205. package/vendor/mwi/code/engine/motionworks_iec_mcp/navigation.py +44 -0
  206. package/vendor/mwi/code/engine/motionworks_iec_mcp/pou_writer.py +1547 -0
  207. package/vendor/mwi/code/engine/motionworks_iec_mcp/program_checks.py +360 -0
  208. package/vendor/mwi/code/engine/motionworks_iec_mcp/project.py +704 -0
  209. package/vendor/mwi/code/engine/motionworks_iec_mcp/reference_data/catalog.json +672 -0
  210. package/vendor/mwi/code/engine/motionworks_iec_mcp/reference_data/patterns.json +86 -0
  211. package/vendor/mwi/code/engine/motionworks_iec_mcp/restore.py +208 -0
  212. package/vendor/mwi/code/engine/motionworks_iec_mcp/server.py +1913 -0
  213. package/vendor/mwi/code/engine/motionworks_iec_mcp/snapshot.py +138 -0
  214. package/vendor/mwi/code/engine/motionworks_iec_mcp/st_comments.py +25 -0
  215. package/vendor/mwi/code/engine/motionworks_iec_mcp/staging.py +179 -0
  216. package/vendor/mwi/code/engine/motionworks_iec_mcp/stlint.py +676 -0
  217. package/vendor/mwi/code/engine/motionworks_iec_mcp/structure.py +98 -0
  218. package/vendor/mwi/code/engine/motionworks_iec_mcp/task_bindings.py +68 -0
  219. package/vendor/mwi/code/engine/motionworks_iec_mcp/tasks.py +162 -0
  220. package/vendor/mwi/code/engine/motionworks_iec_mcp/transaction.py +157 -0
  221. package/vendor/mwi/code/engine/motionworks_iec_mcp/tree.py +563 -0
  222. package/vendor/mwi/code/engine/motionworks_iec_mcp/tree_writer.py +790 -0
  223. package/vendor/mwi/code/engine/motionworks_iec_mcp/uia.py +465 -0
  224. package/vendor/mwi/code/engine/motionworks_iec_mcp/validation.py +41 -0
  225. package/vendor/mwi/code/engine/motionworks_iec_mcp/variable_edit.py +183 -0
  226. package/vendor/mwi/code/engine/motionworks_iec_mcp/variables.py +472 -0
  227. package/vendor/mwi/code/engine/motionworks_iec_mcp/workflow.py +130 -0
  228. package/vendor/mwi/code/engine/motionworks_iec_mcp/writer.py +666 -0
  229. package/vendor/mwi/code/mw_code.py +1258 -0
  230. package/vendor/mwi/legacy/edit-session.js +50 -0
  231. package/vendor/mwi/legacy/fb-insertion.js +74 -0
  232. package/vendor/mwi/legacy/native-code.js +83 -0
  233. package/vendor/mwi/legacy/native-groups.js +75 -0
  234. package/vendor/mwi/legacy/native-structure.js +143 -0
  235. package/vendor/mwi/legacy/native-variables.js +77 -0
  236. package/vendor/mwi/legacy/pou-conversion.js +47 -0
  237. package/vendor/mwi/legacy/pou-package.js +66 -0
  238. package/vendor/mwi/lib/core/bridge.d.ts +71 -0
  239. package/vendor/mwi/lib/core/bridge.js +53 -0
  240. package/vendor/mwi/lib/core/code-engine.d.ts +47 -0
  241. package/vendor/mwi/lib/core/code-engine.js +109 -0
  242. package/vendor/mwi/lib/core/errors.d.ts +37 -0
  243. package/vendor/mwi/lib/core/errors.js +47 -0
  244. package/vendor/mwi/lib/core/params.d.ts +52 -0
  245. package/vendor/mwi/lib/core/params.js +46 -0
  246. package/vendor/mwi/lib/core/ps-bridge.d.ts +62 -0
  247. package/vendor/mwi/lib/core/ps-bridge.js +127 -0
  248. package/vendor/mwi/lib/core/result.d.ts +45 -0
  249. package/vendor/mwi/lib/core/result.js +20 -0
  250. package/vendor/mwi/lib/core/tool.d.ts +80 -0
  251. package/vendor/mwi/lib/core/tool.js +55 -0
  252. package/vendor/mwi/lib/edit/compare.d.ts +33 -0
  253. package/vendor/mwi/lib/edit/compare.js +70 -0
  254. package/vendor/mwi/lib/edit.d.ts +20 -0
  255. package/vendor/mwi/lib/edit.js +32 -0
  256. package/vendor/mwi/lib/index.d.ts +21 -0
  257. package/vendor/mwi/lib/index.js +33 -0
  258. package/vendor/mwi/lib/read/service.d.ts +117 -0
  259. package/vendor/mwi/lib/read/service.js +229 -0
  260. package/vendor/mwi/lib/read.d.ts +21 -0
  261. package/vendor/mwi/lib/read.js +33 -0
  262. package/vendor/mwi/lib/session/project.d.ts +34 -0
  263. package/vendor/mwi/lib/session/project.js +54 -0
  264. package/vendor/mwi/lib/session/service.d.ts +167 -0
  265. package/vendor/mwi/lib/session/service.js +566 -0
  266. package/vendor/mwi/lib/session/stage.d.ts +84 -0
  267. package/vendor/mwi/lib/session/stage.js +187 -0
  268. package/vendor/mwi/lib/session/workspace.d.ts +52 -0
  269. package/vendor/mwi/lib/session/workspace.js +90 -0
  270. package/vendor/mwi/lib/skill.d.ts +36 -0
  271. package/vendor/mwi/lib/skill.js +56 -0
  272. package/vendor/mwi/lib/tools/edit.d.ts +20 -0
  273. package/vendor/mwi/lib/tools/edit.js +286 -0
  274. package/vendor/mwi/lib/tools/read.d.ts +19 -0
  275. package/vendor/mwi/lib/tools/read.js +161 -0
  276. package/vendor/mwi/lib/tools/session.d.ts +20 -0
  277. package/vendor/mwi/lib/tools/session.js +166 -0
  278. package/vendor/mwi/lib/tools/verify.d.ts +19 -0
  279. package/vendor/mwi/lib/tools/verify.js +85 -0
  280. package/vendor/mwi/lib/verify.d.ts +17 -0
  281. package/vendor/mwi/lib/verify.js +29 -0
@@ -0,0 +1,571 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { spawn } from 'node:child_process';
3
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
4
+ import { copyFile, mkdir, readFile, readdir, stat, writeFile } from 'node:fs/promises';
5
+ import { tmpdir } from 'node:os';
6
+ import { dirname, extname, join, resolve } from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+ import { EngineeringError } from "../session.js";
9
+ import { documentRoot, firstChild, optionalAttribute, parseXml } from "../model/xml.js";
10
+ import { decodeExport } from "../fs.js";
11
+ const moduleDir = dirname(fileURLToPath(import.meta.url));
12
+ const bridgeRoot = [resolve(moduleDir, '../../studio-bridge'), resolve(moduleDir, '../studio-bridge')].find(existsSync) ?? resolve(moduleDir, '../../studio-bridge');
13
+ const bridgeProject = join(bridgeRoot, 'StudioBridge.csproj');
14
+ const bridgeSource = join(bridgeRoot, 'Program.cs');
15
+ const bridgeKey = existsSync(bridgeProject) && existsSync(bridgeSource)
16
+ ? createHash('sha256').update(readFileSync(bridgeProject)).update(readFileSync(bridgeSource)).digest('hex').slice(0, 16)
17
+ : 'missing';
18
+ const bridgeBuildRoot = join(tmpdir(), `arya-l5x-studio-bridge-${bridgeKey}-${process.pid}`);
19
+ const bridgeDll = join(bridgeBuildRoot, 'bin', 'Release', 'net8.0', 'StudioBridge.dll');
20
+ const defaultSdkRoot = 'C:\\Users\\Public\\Documents\\Studio 5000\\Logix Designer SDK';
21
+ const sdkRoot = () => process.env.LOGIX_DESIGNER_SDK_ROOT || defaultSdkRoot;
22
+ const bridgeBuilt = () => existsSync(bridgeProject) && existsSync(bridgeSource) && existsSync(bridgeDll);
23
+ /** The SDK selects the engine by the project's revision, so these are project floors. */
24
+ const MIN_OPEN_REVISION = 31;
25
+ const MIN_BUILD_REVISION = 37;
26
+ /** The SDK's documented refusal for a project below the build floor. */
27
+ const BUILD_REVISION_OBSERVED = 'Operation not supported on Logix Designer version 36.0';
28
+ /** A FactoryTalk Directory cache failure is transient; one retry refreshes it. */
29
+ const FTSP_RETRYABLE = /Security cannot proceed|Directory Server|cache has expired/iu;
30
+ export function parseSdkServiceState(output) {
31
+ return /\bSTATE\s*:\s*\d+\s+([A-Z_]+)/iu.exec(output)?.[1]?.toUpperCase() ?? 'UNKNOWN';
32
+ }
33
+ export function hasLiveCommunicationsService(states) {
34
+ return states.slice(0, 2).includes('RUNNING');
35
+ }
36
+ export function sdkDiagnosticLines(output) {
37
+ // ImportLog can contain the only specific reason for an import failure.
38
+ // Remove progress chatter, but preserve import evidence for the engineer.
39
+ return output.split(/\r?\n/u).filter(line => !line.includes('[PROGRESS]'));
40
+ }
41
+ async function windowsServiceState(name) {
42
+ if (process.platform !== 'win32')
43
+ return 'UNSUPPORTED_PLATFORM';
44
+ try {
45
+ const result = await run('sc.exe', ['query', name], 5_000);
46
+ return result.code === 0 ? parseSdkServiceState(result.output) : 'NOT_INSTALLED';
47
+ }
48
+ catch {
49
+ return 'UNAVAILABLE';
50
+ }
51
+ }
52
+ function installedStudioVersions() {
53
+ const base = 'C:\\Program Files (x86)\\Rockwell Software\\Studio 5000\\Logix Designer\\ENU';
54
+ if (!existsSync(base))
55
+ return [];
56
+ return readdirSync(base).map(name => /^v(\d+)$/iu.exec(name)).filter((match) => match !== null).map(match => Number(match[1])).sort((a, b) => a - b);
57
+ }
58
+ async function run(program, args, timeoutMs, signal) {
59
+ return new Promise((done, fail) => {
60
+ const child = spawn(program, args, { windowsHide: true, signal, shell: false });
61
+ let output = '';
62
+ const collect = (chunk) => { output = (output + chunk.toString()).slice(-1_000_000); };
63
+ child.stdout.on('data', collect);
64
+ child.stderr.on('data', collect);
65
+ const timeout = setTimeout(() => child.kill(), timeoutMs);
66
+ child.on('error', error => { clearTimeout(timeout); fail(error); });
67
+ child.on('close', code => { clearTimeout(timeout); done({ code, output }); });
68
+ });
69
+ }
70
+ /** Run the bridge, retrying once when FactoryTalk Directory cached credentials expired. */
71
+ async function runBridge(args, timeoutMs, signal) {
72
+ const first = await run('dotnet', args, timeoutMs, signal);
73
+ if (first.code === 0 || !FTSP_RETRYABLE.test(first.output))
74
+ return first;
75
+ return run('dotnet', args, timeoutMs, signal);
76
+ }
77
+ export async function studioSdkStatus() {
78
+ const root = sdkRoot();
79
+ const installedVersions = installedStudioVersions();
80
+ const release = installedVersions.at(-1) ?? null;
81
+ const [service, activation, factorytalk, factorytalk02, rslinx] = await Promise.all([
82
+ windowsServiceState('LdSdkService'), windowsServiceState('FactoryTalk Activation Service'),
83
+ windowsServiceState('RSLinxNG'), windowsServiceState('RSLinxNG02'), windowsServiceState('RSLinx'),
84
+ ]);
85
+ const serviceFields = { sdk_service_status: service, activation_service_status: activation,
86
+ communication_services: { factorytalk_linx: factorytalk, factorytalk_linx_02: factorytalk02, rslinx_classic: rslinx },
87
+ live_communications_available: hasLiveCommunicationsService([factorytalk, factorytalk02, rslinx]) };
88
+ if (service !== 'RUNNING')
89
+ return { available: false, reason: `Logix Designer SDK Service (LdSdkService) is ${service}; start it in Windows Services before using SDK tools`, ...serviceFields, sdk_root: root, bridge_built: bridgeBuilt(), open_supported_project_revisions: '31+', build_supported_project_revisions: '37+', sdk_release: release, installed_studio_versions: installedVersions };
90
+ if (activation !== 'RUNNING')
91
+ return { available: false, reason: `FactoryTalk Activation Service is ${activation}; start it before using SDK tools`, ...serviceFields, sdk_root: root, bridge_built: bridgeBuilt(), open_supported_project_revisions: '31+', build_supported_project_revisions: '37+', sdk_release: release, installed_studio_versions: installedVersions };
92
+ if (!existsSync(bridgeProject) || !existsSync(bridgeSource))
93
+ return { available: false, reason: 'Studio bridge source was not packaged', ...serviceFields, sdk_root: root, bridge_built: false, open_supported_project_revisions: '31+', build_supported_project_revisions: '37+', sdk_release: release, installed_studio_versions: installedVersions };
94
+ const packageDir = join(root, 'dotnet');
95
+ const files = existsSync(packageDir) ? await readdir(packageDir) : [];
96
+ const clientPackage = files.find(file => /^RockwellAutomation\.LogixDesigner\.CSClient\.2\.1\.974\.nupkg$/iu.test(file));
97
+ if (clientPackage === undefined)
98
+ return { available: false, reason: 'Compatible SDK client package not installed', ...serviceFields, sdk_root: root, bridge_built: bridgeBuilt(), open_supported_project_revisions: '31+', build_supported_project_revisions: '37+', sdk_release: release, installed_studio_versions: installedVersions };
99
+ if (installedVersions.length === 0)
100
+ return { available: false, reason: 'No Studio 5000 Logix Designer version is installed', ...serviceFields, sdk_root: root, client_package: clientPackage, bridge_built: bridgeBuilt(), open_supported_project_revisions: '31+', build_supported_project_revisions: '37+', sdk_release: release, installed_studio_versions: installedVersions };
101
+ try {
102
+ const dotnet = await run('dotnet', ['--list-sdks'], 10_000);
103
+ const hasNet8 = /(?:^|\n)8\.\d+|(?:^|\n)(?:9|10)\.\d+/u.test(dotnet.output);
104
+ if (dotnet.code !== 0 || !hasNet8)
105
+ return { available: false, reason: '.NET SDK 8 or later is unavailable', ...serviceFields, sdk_root: root, client_package: clientPackage, bridge_built: bridgeBuilt(), open_supported_project_revisions: '31+', build_supported_project_revisions: '37+', sdk_release: release, installed_studio_versions: installedVersions };
106
+ }
107
+ catch {
108
+ return { available: false, reason: 'dotnet executable is unavailable', ...serviceFields, sdk_root: root, client_package: clientPackage, bridge_built: bridgeBuilt(), open_supported_project_revisions: '31+', build_supported_project_revisions: '37+', sdk_release: release, installed_studio_versions: installedVersions };
109
+ }
110
+ return { available: true, reason: 'SDK and activation services, client package, and .NET SDK detected', ...serviceFields, sdk_root: root, client_package: clientPackage, bridge_built: bridgeBuilt(), open_supported_project_revisions: '31+', build_supported_project_revisions: '37+', sdk_release: release, installed_studio_versions: installedVersions };
111
+ }
112
+ function requireLiveDependencies(availability) {
113
+ if (!availability.available)
114
+ throw new EngineeringError('SDK_UNAVAILABLE', availability.reason);
115
+ if (!availability.live_communications_available)
116
+ throw new EngineeringError('COMMUNICATIONS_UNAVAILABLE', 'FactoryTalk Linx is not running; the Logix Designer SDK does not support RSLinx Classic communications');
117
+ }
118
+ /** Export one existing ACD as a new full L5X; this never connects to a controller. */
119
+ export async function exportAcdToL5x(acdPath, outputPath, signal) {
120
+ const availability = await studioSdkStatus();
121
+ if (!availability.available)
122
+ throw new EngineeringError('SDK_UNAVAILABLE', availability.reason);
123
+ if (extname(acdPath).toLowerCase() !== '.acd')
124
+ throw new EngineeringError('INVALID_ACD', 'Source must be an ACD file');
125
+ if (extname(outputPath).toLowerCase() !== '.l5x')
126
+ throw new EngineeringError('INVALID_OUTPUT', 'Output must end in .l5x');
127
+ const source = resolve(acdPath);
128
+ const output = resolve(outputPath);
129
+ const sourceInfo = await stat(source).catch(() => { throw new EngineeringError('FILE_NOT_FOUND', 'ACD file not found'); });
130
+ if (!sourceInfo.isFile())
131
+ throw new EngineeringError('INVALID_ACD', 'ACD path must name a file');
132
+ if (existsSync(output))
133
+ throw new EngineeringError('OUTPUT_EXISTS', 'L5X output must be a new file');
134
+ if (!existsSync(dirname(output)))
135
+ throw new EngineeringError('OUTPUT_DIRECTORY', 'Output directory does not exist');
136
+ const sourceHash = createHash('sha256').update(await readFile(source)).digest('hex');
137
+ await ensureBridge(availability, signal);
138
+ const result = await runBridge([bridgeDll, 'export-l5x', source, output], 240_000, signal);
139
+ if (result.code !== 0 || !result.output.includes('EXPORTED_L5X_SUCCEEDED'))
140
+ throw new EngineeringError('L5X_EXPORT_FAILED', result.output);
141
+ if (createHash('sha256').update(await readFile(source)).digest('hex') !== sourceHash)
142
+ throw new EngineeringError('ACD_CHANGED', 'Source ACD changed during export; reopen and export an unchanged source copy');
143
+ const bytes = await readFile(output);
144
+ if (bytes.length === 0 || !bytes.toString('utf8', 0, Math.min(bytes.length, 2048)).includes('<RSLogix5000Content')) {
145
+ throw new EngineeringError('L5X_EXPORT_INVALID', 'SDK export did not produce a recognizable L5X file');
146
+ }
147
+ return { source_acd: source, source_sha256: sourceHash, output_path: output, output_bytes: bytes.length,
148
+ output_sha256: createHash('sha256').update(bytes).digest('hex'), exported_at: new Date().toISOString(),
149
+ controller_operation: 'none', studio_review: 'required' };
150
+ }
151
+ function checkProject(project, availability) {
152
+ if (!availability.available)
153
+ throw new EngineeringError('SDK_UNAVAILABLE', availability.reason);
154
+ if (project.exportKind !== 'controller')
155
+ throw new EngineeringError('UNSUPPORTED_EXPORT', 'Offline Studio build requires a full controller L5X export');
156
+ const revision = Number.parseInt(project.softwareRevision, 10);
157
+ if (!Number.isFinite(revision) || revision < MIN_OPEN_REVISION)
158
+ throw new EngineeringError('SDK_REVISION_UNSUPPORTED', `Installed SDK opens project revision ${MIN_OPEN_REVISION} and later`);
159
+ if (!availability.installed_studio_versions.includes(revision))
160
+ throw new EngineeringError('STUDIO_VERSION_UNAVAILABLE', `Studio 5000 v${revision} is not installed; the SDK opens a project with the matching Logix Designer release. Installed versions: ${availability.installed_studio_versions.join(', ') || 'none'}`);
161
+ }
162
+ async function ensureBridge(availability, signal) {
163
+ if (bridgeBuilt())
164
+ return;
165
+ const packageDir = join(availability.sdk_root, 'dotnet');
166
+ const escaped = packageDir.replaceAll('&', '&amp;').replaceAll('"', '&quot;').replaceAll('<', '&lt;');
167
+ await mkdir(bridgeBuildRoot, { recursive: true });
168
+ await copyFile(bridgeProject, join(bridgeBuildRoot, 'StudioBridge.csproj'));
169
+ await copyFile(bridgeSource, join(bridgeBuildRoot, 'Program.cs'));
170
+ await writeFile(join(bridgeBuildRoot, 'NuGet.Config'), `<configuration><packageSources><clear/><add key="LocalRockwellSdk" value="${escaped}"/><add key="nuget.org" value="https://api.nuget.org/v3/index.json"/></packageSources></configuration>`, 'utf8');
171
+ const buildProject = join(bridgeBuildRoot, 'StudioBridge.csproj');
172
+ const restored = await run('dotnet', ['restore', buildProject, '--configfile', join(bridgeBuildRoot, 'NuGet.Config'), '--ignore-failed-sources'], 120_000, signal);
173
+ if (restored.code !== 0)
174
+ throw new EngineeringError('SDK_BRIDGE_BUILD_FAILED', restored.output);
175
+ const built = await run('dotnet', ['build', buildProject, '-c', 'Release', '--no-restore'], 120_000, signal);
176
+ if (built.code !== 0 || !existsSync(bridgeDll))
177
+ throw new EngineeringError('SDK_BRIDGE_BUILD_FAILED', built.output);
178
+ }
179
+ export async function verifyOfflineL5x(project, outputPath, signal, options = {}) {
180
+ const availability = await studioSdkStatus();
181
+ checkProject(project, availability);
182
+ const revision = Number.parseInt(project.softwareRevision, 10);
183
+ if (revision < MIN_BUILD_REVISION && options.probeBuild !== true)
184
+ throw new EngineeringError('SDK_BUILD_UNSUPPORTED', `Project revision ${revision} cannot be built: the SDK selects the Logix Designer engine by project revision, and Build is supported only for project revision ${MIN_BUILD_REVISION} or later (observed: "${BUILD_REVISION_OBSERVED}"). Upgrade or convert the project, use Studio 5000 Import + Verify, or pass probe=true to attempt the build and report the SDK's own error.`);
185
+ if (extname(outputPath).toLowerCase() !== '.acd')
186
+ throw new EngineeringError('INVALID_OUTPUT', 'Output path must end in .acd');
187
+ const source = resolve(project.path);
188
+ const output = resolve(outputPath);
189
+ if (source.toLowerCase() === output.toLowerCase())
190
+ throw new EngineeringError('SOURCE_OVERWRITE', 'Output must differ from source');
191
+ if (existsSync(output))
192
+ throw new EngineeringError('OUTPUT_EXISTS', 'Output must be a new file');
193
+ if (!existsSync(dirname(output)))
194
+ throw new EngineeringError('OUTPUT_DIRECTORY', 'Output directory does not exist');
195
+ const sourceHash = createHash('sha256').update(await readFile(source)).digest('hex');
196
+ if (sourceHash !== project.sha256)
197
+ throw new EngineeringError('STALE_EXPORT', 'Source L5X changed since open_project');
198
+ await ensureBridge(availability, signal);
199
+ const result = await runBridge([bridgeDll, 'verify', source, output], 180_000, signal);
200
+ const outputInfo = existsSync(output) ? await stat(output) : undefined;
201
+ const summary = /<Summary Warnings="(\d+)" Errors="(\d+)"\s*\/>/u.exec(result.output);
202
+ const lines = sdkDiagnosticLines(result.output);
203
+ const log = lines.slice(-60).join('\n').slice(-16_000);
204
+ const passed = result.code === 0 && result.output.includes('OFFLINE_BUILD_SUCCEEDED') && (outputInfo?.size ?? 0) > 0;
205
+ const diagnostics = lines.filter(line => /\b(?:error|failed|exception|warning)\b/iu.test(line) && !/Warnings="0" Errors="0"/u.test(line)).slice(-30);
206
+ const sdkError = /SDK_OPERATION_FAILED:\s*([^\r\n]+)/u.exec(result.output)?.[1]?.trim()
207
+ ?? /(?:ERROR|error)[^\r\n]*(?:Exception|XMLSrv_)[^\r\n]*/u.exec(result.output)?.[0]?.trim() ?? null;
208
+ const failureStage = passed ? null
209
+ : /XMLSrv_E_IMPORT|Import was cancelled|IMPORT_ABORTED/iu.test(result.output) ? 'import'
210
+ : result.output.includes('Build started') ? 'build'
211
+ : result.output.includes('Save As started') ? 'import_or_save' : 'open_source';
212
+ return {
213
+ project_id: project.id, source_path: source, source_sha256: sourceHash,
214
+ output_path: passed ? output : null, partial_output_path: passed ? null : outputInfo === undefined ? null : output,
215
+ output_sha256: passed ? createHash('sha256').update(await readFile(output)).digest('hex') : null,
216
+ sdk_available: true, offline_build: passed ? 'passed' : 'failed', download_ready: passed,
217
+ build_gate: { axis: 'project_revision', minimum: MIN_BUILD_REVISION },
218
+ output_bytes: outputInfo?.size ?? 0, import_warnings: summary === null ? null : Number(summary[1]), import_errors: summary === null ? null : Number(summary[2]),
219
+ failure_stage: failureStage, sdk_error: sdkError, diagnostics, log,
220
+ next_step: passed ? 'Review the engineering diff and controller target before any approved download.'
221
+ : failureStage === 'import' ? 'The SDK could not import this export; the failure is not the build revision. Read sdk_error and diagnostics, then compare an unedited export from this project before changing the draft.'
222
+ : 'Read diagnostics, correct the L5X, reopen the changed file, and retry with a new ACD path. If the cause is unclear, build the unedited source as a baseline.',
223
+ controller_operation: 'none', studio_review: 'required',
224
+ };
225
+ }
226
+ /** Work only on a fresh ACD copy; this bridge command has no controller route. */
227
+ export async function editOfflineAcd(acdPath, draft, program, routine, outputPath, exportPath, signal) {
228
+ const availability = await studioSdkStatus();
229
+ checkProject(draft, availability);
230
+ if (Number.parseInt(draft.softwareRevision, 10) < MIN_BUILD_REVISION)
231
+ throw new EngineeringError('SDK_BUILD_UNSUPPORTED', `Offline ACD edit/build requires project revision ${MIN_BUILD_REVISION} or later because the SDK selects the engine by project revision (observed: "${BUILD_REVISION_OBSERVED}"). Use Studio 5000 for a revision ${draft.softwareRevision} project.`);
232
+ const source = resolve(acdPath);
233
+ const output = resolve(outputPath);
234
+ const exported = resolve(exportPath);
235
+ if (extname(source).toLowerCase() !== '.acd' || extname(output).toLowerCase() !== '.acd' || extname(exported).toLowerCase() !== '.l5x')
236
+ throw new EngineeringError('INVALID_OUTPUT', 'Source/output must be ACD and result export must be L5X');
237
+ for (const path of [output, exported, `${output}.routine.l5x`]) {
238
+ if (existsSync(path))
239
+ throw new EngineeringError('OUTPUT_EXISTS', `Output must be new: ${path}`);
240
+ if (!existsSync(dirname(path)))
241
+ throw new EngineeringError('OUTPUT_DIRECTORY', 'Output directory does not exist');
242
+ }
243
+ const sourceHash = createHash('sha256').update(await readFile(source)).digest('hex');
244
+ if (createHash('sha256').update(await readFile(draft.path)).digest('hex') !== draft.sha256)
245
+ throw new EngineeringError('STALE_EXPORT', 'Draft changed since open_project');
246
+ await ensureBridge(availability, signal);
247
+ const result = await run('dotnet', [bridgeDll, 'edit-routine', source, resolve(draft.path), output, program, routine, exported, sourceHash, draft.sha256], 240_000, signal);
248
+ const summary = /<Summary Warnings="(\d+)" Errors="(\d+)"\s*\/>/u.exec(result.output);
249
+ const importErrors = summary === null ? null : Number(summary[2]);
250
+ const passed = result.code === 0 && result.output.includes('OFFLINE_ACD_EDIT_SUCCEEDED') && existsSync(output) && existsSync(exported) && (importErrors ?? 0) === 0;
251
+ return { offline_build: passed ? 'passed' : 'failed', source_acd_sha256: sourceHash,
252
+ output_path: passed ? output : null, output_sha256: passed ? createHash('sha256').update(await readFile(output)).digest('hex') : null,
253
+ result_export_path: passed ? exported : null, log: sdkDiagnosticLines(result.output).slice(-80).join('\n').slice(-24_000),
254
+ partial_output_path: !passed && existsSync(output) ? output : null, import_errors: importErrors,
255
+ failure_stage: passed ? null : result.output.includes('Build succeeded') ? 'save_or_export' : result.output.includes('Build started') ? 'build' : result.output.includes('OFFLINE_COMPONENT_IMPORT_STARTED') ? 'component_import' : 'open_or_prepare_component' };
256
+ }
257
+ export async function readControllerMode(project, commPath, signal, snapshotPath) {
258
+ const availability = await studioSdkStatus();
259
+ checkProject(project, availability);
260
+ requireLiveDependencies(availability);
261
+ if (commPath.length > 256 || /[\r\n\0]/u.test(commPath))
262
+ throw new EngineeringError('INVALID_COMM_PATH', 'Provide one FactoryTalk Linx communications path');
263
+ let mode;
264
+ let snapshot;
265
+ if (snapshotPath !== undefined) {
266
+ snapshot = await controllerActionPreflight(snapshotPath, commPath);
267
+ mode = await readControllerModeForAction(snapshot, commPath, signal);
268
+ }
269
+ else {
270
+ await ensureBridge(availability, signal);
271
+ const result = await run('dotnet', [bridgeDll, 'read-mode', resolve(project.path), commPath], 90_000, signal);
272
+ const observed = /^READ_CONTROLLER_MODE=(\w+)$/mu.exec(result.output)?.[1];
273
+ if (result.code !== 0 || observed === undefined)
274
+ throw new EngineeringError('CONTROLLER_READ_FAILED', result.output);
275
+ mode = observed;
276
+ }
277
+ return { project_id: project.id, source_project_sha256: project.sha256, ...(snapshot === undefined ? {} : { snapshot_path: snapshot.path, snapshot_sha256: snapshot.sha256 }), controller_mode: mode, observed_at: new Date().toISOString(), communications_path: commPath, project_controller_match: 'unverified', operation: 'read_only' };
278
+ }
279
+ /** Read the exported Controller attribute, never an instruction string or a guessed route. */
280
+ export function savedL5xCommPath(xml) {
281
+ if (/<!DOCTYPE\b|<!ENTITY\b/iu.test(xml))
282
+ throw new EngineeringError('UNSUPPORTED_XML', 'DTD and entity declarations are not accepted');
283
+ const root = documentRoot(parseXml(xml));
284
+ if (root.name !== 'RSLogix5000Content')
285
+ throw new EngineeringError('INVALID_PROJECT', 'Expected an L5X export');
286
+ const controller = firstChild(root, 'Controller');
287
+ if (controller === undefined)
288
+ throw new EngineeringError('INVALID_PROJECT', 'L5X export has no Controller element');
289
+ return optionalAttribute(controller, 'CommPath', '').trim();
290
+ }
291
+ /** Inspect the path recorded in one project, without browsing drivers or going online. */
292
+ export async function readProjectCommPath(inputPath, signal) {
293
+ if (!['.acd', '.l5x'].includes(extname(inputPath).toLowerCase()))
294
+ throw new EngineeringError('INVALID_PROJECT', 'Path must name an ACD or L5X project');
295
+ const availability = extname(inputPath).toLowerCase() === '.acd' ? await studioSdkStatus() : undefined;
296
+ if (availability !== undefined && !availability.available)
297
+ throw new EngineeringError('SDK_UNAVAILABLE', availability.reason);
298
+ const path = resolve(inputPath);
299
+ const info = await stat(path).catch(() => { throw new EngineeringError('FILE_NOT_FOUND', 'Project file not found'); });
300
+ if (!info.isFile())
301
+ throw new EngineeringError('INVALID_PROJECT', 'Project path must name a file');
302
+ if (extname(path).toLowerCase() === '.l5x') {
303
+ const bytes = await readFile(path, signal === undefined ? undefined : { signal });
304
+ const commPath = savedL5xCommPath(decodeExport(bytes));
305
+ return { project_path: path, communications_path: commPath, source: 'exported_controller_attribute',
306
+ verified_against_controller: false, operation: 'offline_read_only',
307
+ note: commPath === '' ? 'No communications path is saved; obtain one from Studio 5000 Who Active / FactoryTalk Linx.'
308
+ : 'Confirm this exported path in Studio 5000 Who Active / FactoryTalk Linx before a controller operation.' };
309
+ }
310
+ if (availability === undefined)
311
+ throw new EngineeringError('SDK_UNAVAILABLE', 'ACD route inspection requires the SDK');
312
+ await ensureBridge(availability, signal);
313
+ const result = await run('dotnet', [bridgeDll, 'get-comm-path', path, '--read-only'], 90_000, signal);
314
+ const commPath = /^PROJECT_COMM_PATH=(.*)$/mu.exec(result.output)?.[1]?.trim();
315
+ if (result.code !== 0 || commPath === undefined)
316
+ throw new EngineeringError('COMM_PATH_READ_FAILED', result.output);
317
+ return { project_path: path, communications_path: commPath, source: 'project_saved_path',
318
+ verified_against_controller: false, operation: 'offline_read_only',
319
+ note: commPath === '' ? 'No communications path is saved; obtain one from Studio 5000 Who Active / FactoryTalk Linx.'
320
+ : 'Confirm this saved path in Studio 5000 Who Active / FactoryTalk Linx before a controller operation.' };
321
+ }
322
+ const controllerRouteCache = new Map();
323
+ export async function rememberControllerRoute(ip, projectPath, route) {
324
+ validateControllerRoute(ip, route);
325
+ const path = resolve(projectPath);
326
+ const bytes = await readFile(path);
327
+ controllerRouteCache.set(path.toLowerCase(), { sha256: createHash('sha256').update(bytes).digest('hex'), ip, path: route });
328
+ }
329
+ export async function inheritControllerRoute(sourcePath, targetPath) {
330
+ const source = resolve(sourcePath);
331
+ const saved = controllerRouteCache.get(source.toLowerCase());
332
+ if (saved === undefined)
333
+ return;
334
+ const sourceHash = createHash('sha256').update(await readFile(source)).digest('hex');
335
+ if (sourceHash !== saved.sha256)
336
+ return;
337
+ await rememberControllerRoute(saved.ip, targetPath, saved.path);
338
+ }
339
+ export function validateControllerIp(ip) {
340
+ const parts = ip.split('.');
341
+ if (parts.length !== 4 || parts.some(part => !/^(?:0|[1-9]\d{0,2})$/u.test(part) || Number(part) > 255))
342
+ throw new EngineeringError('INVALID_IP_ADDRESS', 'Provide the target controller IPv4 address explicitly, for example 192.168.1.3');
343
+ return ip;
344
+ }
345
+ /** A saved Linx route is usable only when its final IP hop is the user's exact target. */
346
+ export function validateControllerRoute(ip, route) {
347
+ validateControllerIp(ip);
348
+ if (!route || route.length > 256 || /[\r\n\0]/u.test(route))
349
+ throw new EngineeringError('ROUTE_UNAVAILABLE', 'The selected project has no usable FactoryTalk Linx route');
350
+ const addresses = route.split('\\').filter(segment => /^(?:\d{1,3}\.){3}\d{1,3}$/u.test(segment));
351
+ if (addresses.at(-1) !== ip)
352
+ throw new EngineeringError('CONTROLLER_IP_MISMATCH', `Saved FactoryTalk Linx route does not target the supplied controller IP ${ip}`);
353
+ return route;
354
+ }
355
+ /** Resolve a route from the named project and cache it only while that exact file hash is unchanged. */
356
+ export async function resolveControllerRoute(ip, projectPath, signal) {
357
+ validateControllerIp(ip);
358
+ requireLiveDependencies(await studioSdkStatus());
359
+ if (!['.acd', '.l5x'].includes(extname(projectPath).toLowerCase()))
360
+ throw new EngineeringError('INVALID_ROUTE_SOURCE', 'route project must be an ACD or full L5X export');
361
+ const path = resolve(projectPath);
362
+ const bytes = await readFile(path).catch(() => { throw new EngineeringError('FILE_NOT_FOUND', 'Route source project not found'); });
363
+ const sha256 = createHash('sha256').update(bytes).digest('hex');
364
+ const cached = controllerRouteCache.get(path.toLowerCase());
365
+ if (cached?.sha256 === sha256 && cached.ip === ip)
366
+ return validateControllerRoute(ip, cached.path);
367
+ const result = await readProjectCommPath(path, signal);
368
+ const route = validateControllerRoute(ip, result.communications_path);
369
+ controllerRouteCache.set(path.toLowerCase(), { sha256, ip, path: route });
370
+ return route;
371
+ }
372
+ /** Validate SDK sample wire data, including identity, per-tag type, quality and timestamps.
373
+ * @param text - JSON emitted by the owned bridge.
374
+ * @param tags - exact requested watch list.
375
+ * @returns a validated sweep.
376
+ */
377
+ export function decodeLogicSample(text, tags) {
378
+ const object = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
379
+ const value = JSON.parse(text);
380
+ if (!object(value) || typeof value.elapsed_ms !== 'number' || !Number.isFinite(value.elapsed_ms) || value.elapsed_ms < 0
381
+ || typeof value.sweep_ms !== 'number' || !Number.isFinite(value.sweep_ms) || value.sweep_ms < 0 || !Array.isArray(value.values) || value.values.length !== tags.length)
382
+ throw new EngineeringError('CONTROLLER_READ_FAILED', 'SDK returned invalid sample timing or tag count');
383
+ const values = value.values.map((entry, index) => {
384
+ const tag = tags[index];
385
+ if (!object(entry) || entry.tag !== tag.name || entry.data_type !== tag.data_type || !['good', 'error'].includes(String(entry.quality))
386
+ || typeof entry.observed_at !== 'string' || !Number.isFinite(Date.parse(entry.observed_at)))
387
+ throw new EngineeringError('CONTROLLER_READ_FAILED', 'SDK returned incorrect tag identity, quality or timestamp');
388
+ const scalar = entry.value;
389
+ if (entry.quality === 'good' && (tag.data_type === 'BOOL' ? typeof scalar !== 'boolean'
390
+ : typeof scalar !== 'number' || !Number.isFinite(scalar) || (tag.data_type !== 'REAL' && !Number.isInteger(scalar))))
391
+ throw new EngineeringError('CONTROLLER_READ_FAILED', 'SDK returned a value inconsistent with the requested type');
392
+ if (entry.quality === 'error' && scalar !== null)
393
+ throw new EngineeringError('CONTROLLER_READ_FAILED', 'Failed read must contain no value');
394
+ return { tag: tag.name, value: typeof scalar === 'boolean' || typeof scalar === 'number' ? scalar : null,
395
+ quality: String(entry.quality), observed_at: entry.observed_at, ...(typeof entry.error === 'string' ? { error: entry.error } : {}) };
396
+ });
397
+ return { elapsed_ms: value.elapsed_ms, sweep_ms: value.sweep_ms, values };
398
+ }
399
+ export function validateLiveSampling(tags, sampleCount, intervalMs) {
400
+ if (!Array.isArray(tags) || tags.length < 1 || tags.length > 16)
401
+ throw new EngineeringError('INVALID_WATCH', 'Provide 1 to 16 controller-scoped scalar tags');
402
+ if (!Number.isInteger(sampleCount) || sampleCount < 1 || sampleCount > 20)
403
+ throw new EngineeringError('INVALID_SAMPLE_COUNT', 'sample_count must be 1 through 20');
404
+ if (!Number.isInteger(intervalMs) || intervalMs < 250 || intervalMs > 5000 || sampleCount * intervalMs > 30_000)
405
+ throw new EngineeringError('INVALID_INTERVAL', 'interval_ms must be 250 through 5000, with a maximum 30 second sampling window');
406
+ const names = new Set();
407
+ for (const tag of tags) {
408
+ if (typeof tag?.name !== 'string' || !/^[A-Za-z_][A-Za-z0-9_]*$/u.test(tag.name))
409
+ throw new EngineeringError('INVALID_TAG', 'Watch tags must be controller-scoped scalar names');
410
+ if (!['BOOL', 'INT', 'DINT', 'REAL'].includes(tag.data_type))
411
+ throw new EngineeringError('INVALID_DATA_TYPE', 'Watch data_type must be BOOL, INT, DINT, or REAL');
412
+ const key = tag.name.toLowerCase();
413
+ if (names.has(key))
414
+ throw new EngineeringError('DUPLICATE_TAG', `Duplicate watch tag: ${tag.name}`);
415
+ names.add(key);
416
+ }
417
+ }
418
+ /** Read repeated sweeps through one open SDK project. Actual sweep timing is returned; the requested interval is not guaranteed. */
419
+ export async function sampleLiveTags(snapshotPath, commPath, tags, sampleCount, intervalMs, signal) {
420
+ validateLiveSampling(tags, sampleCount, intervalMs);
421
+ const availability = await studioSdkStatus();
422
+ requireLiveDependencies(availability);
423
+ checkCommPath(commPath);
424
+ if (extname(snapshotPath).toLowerCase() !== '.acd')
425
+ throw new EngineeringError('INVALID_SNAPSHOT', 'Live reads require an ACD uploaded from the target controller');
426
+ const snapshot = resolve(snapshotPath);
427
+ const bytes = await readFile(snapshot).catch(() => { throw new EngineeringError('FILE_NOT_FOUND', 'Snapshot ACD not found'); });
428
+ await ensureBridge(availability, signal);
429
+ const specs = tags.map(tag => ({ Name: tag.name, DataType: tag.data_type }));
430
+ const result = await run('dotnet', [bridgeDll, 'sample-tags', snapshot, commPath, JSON.stringify(specs), String(sampleCount), String(intervalMs)], 100_000, signal);
431
+ const lines = result.output.split(/\r?\n/u).filter(line => line.startsWith('SAMPLE_JSON='));
432
+ if (result.code !== 0 || !result.output.includes('SAMPLING_SUCCEEDED') || lines.length !== sampleCount)
433
+ throw new EngineeringError('CONTROLLER_READ_FAILED', result.output);
434
+ let samples;
435
+ try {
436
+ samples = lines.map(line => decodeLogicSample(line.slice('SAMPLE_JSON='.length), tags));
437
+ }
438
+ catch {
439
+ throw new EngineeringError('CONTROLLER_READ_FAILED', 'SDK returned malformed sample data');
440
+ }
441
+ if (samples.some(sample => !Number.isFinite(sample.elapsed_ms) || !Number.isFinite(sample.sweep_ms) || !Array.isArray(sample.values) || sample.values.length !== tags.length))
442
+ throw new EngineeringError('CONTROLLER_READ_FAILED', 'SDK returned incomplete sample data');
443
+ const sweepTimes = samples.map(sample => sample.sweep_ms);
444
+ const observedIntervals = samples.slice(1).map((sample, index) => sample.elapsed_ms - samples[index].elapsed_ms);
445
+ return { snapshot_path: snapshot, snapshot_sha256: createHash('sha256').update(bytes).digest('hex'), communications_path: commPath,
446
+ requested_interval_ms: intervalMs, sample_count: sampleCount, tags, samples,
447
+ timing: { average_sweep_ms: Math.round(sweepTimes.reduce((sum, value) => sum + value, 0) / sweepTimes.length),
448
+ max_sweep_ms: Math.max(...sweepTimes),
449
+ average_observed_interval_ms: observedIntervals.length ? Math.round(observedIntervals.reduce((sum, value) => sum + value, 0) / observedIntervals.length) : null },
450
+ operation: 'read_only',
451
+ note: 'Readings are sequential SDK calls. Reported sweep and elapsed times show the achieved rate; short transitions can be missed.' };
452
+ }
453
+ export async function readLiveTag(snapshotPath, commPath, tag, dataType, signal) {
454
+ const availability = await studioSdkStatus();
455
+ requireLiveDependencies(availability);
456
+ if (commPath.length > 256 || /[\r\n\0]/u.test(commPath))
457
+ throw new EngineeringError('INVALID_COMM_PATH', 'Provide one FactoryTalk Linx communications path');
458
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/u.test(tag))
459
+ throw new EngineeringError('INVALID_TAG', 'Only one controller-scoped scalar tag name is accepted');
460
+ const normalizedType = dataType.toUpperCase();
461
+ if (!['BOOL', 'INT', 'DINT', 'REAL'].includes(normalizedType))
462
+ throw new EngineeringError('INVALID_DATA_TYPE', 'data_type must be BOOL, INT, DINT, or REAL');
463
+ if (extname(snapshotPath).toLowerCase() !== '.acd')
464
+ throw new EngineeringError('INVALID_SNAPSHOT', 'Live reads require an ACD uploaded from the target controller');
465
+ const snapshot = resolve(snapshotPath);
466
+ if (!existsSync(snapshot))
467
+ throw new EngineeringError('FILE_NOT_FOUND', 'Snapshot ACD not found');
468
+ const bytes = await readFile(snapshot);
469
+ await ensureBridge(availability, signal);
470
+ const result = await run('dotnet', [bridgeDll, 'read-tag', snapshot, commPath, tag, normalizedType], 90_000, signal);
471
+ const value = /^READ_TAG=(.+)$/mu.exec(result.output)?.[1];
472
+ if (result.code !== 0 || value === undefined)
473
+ throw new EngineeringError('CONTROLLER_READ_FAILED', result.output);
474
+ if (normalizedType === 'BOOL' && value !== 'true' && value !== 'false')
475
+ throw new EngineeringError('CONTROLLER_READ_FAILED', 'Invalid BOOL value returned by SDK');
476
+ const parsed = normalizedType === 'BOOL' ? value === 'true' : Number(value);
477
+ if (normalizedType !== 'BOOL' && !Number.isFinite(parsed))
478
+ throw new EngineeringError('CONTROLLER_READ_FAILED', `Invalid ${normalizedType} value returned by SDK`);
479
+ return { snapshot_path: snapshot, snapshot_sha256: createHash('sha256').update(bytes).digest('hex'), tag, data_type: normalizedType, value: parsed, observed_at: new Date().toISOString(), communications_path: commPath, operation: 'read_only' };
480
+ }
481
+ export async function readLiveDint(snapshotPath, commPath, tag, signal) {
482
+ return readLiveTag(snapshotPath, commPath, tag, 'DINT', signal);
483
+ }
484
+ export async function captureControllerSnapshot(commPath, outputPath, signal) {
485
+ const availability = await studioSdkStatus();
486
+ requireLiveDependencies(availability);
487
+ if (commPath.length > 256 || /[\r\n\0]/u.test(commPath))
488
+ throw new EngineeringError('INVALID_COMM_PATH', 'Provide one FactoryTalk Linx communications path');
489
+ if (extname(outputPath).toLowerCase() !== '.acd')
490
+ throw new EngineeringError('INVALID_OUTPUT', 'Snapshot path must end in .acd');
491
+ const output = resolve(outputPath);
492
+ if (existsSync(output))
493
+ throw new EngineeringError('OUTPUT_EXISTS', 'Snapshot must be a new file');
494
+ if (!existsSync(dirname(output)))
495
+ throw new EngineeringError('OUTPUT_DIRECTORY', 'Snapshot directory does not exist');
496
+ await ensureBridge(availability, signal);
497
+ const result = await run('dotnet', [bridgeDll, 'upload-new', output, commPath], 900_000, signal);
498
+ if (result.code !== 0 || !result.output.includes('UPLOAD_TO_NEW_ACD_SUCCEEDED'))
499
+ throw new EngineeringError('CONTROLLER_UPLOAD_FAILED', result.output);
500
+ const bytes = await readFile(output);
501
+ return { output_path: output, bytes: bytes.length, sha256: createHash('sha256').update(bytes).digest('hex'), captured_at: new Date().toISOString(), communications_path: commPath, controller_operation: 'read_only_upload_to_new_local_file' };
502
+ }
503
+ export async function controllerUploadPreflight(commPath, outputPath) {
504
+ const availability = await studioSdkStatus();
505
+ requireLiveDependencies(availability);
506
+ checkCommPath(commPath);
507
+ if (extname(outputPath).toLowerCase() !== '.acd')
508
+ throw new EngineeringError('INVALID_OUTPUT', 'Snapshot path must end in .acd');
509
+ const output = resolve(outputPath);
510
+ if (existsSync(output))
511
+ throw new EngineeringError('OUTPUT_EXISTS', 'Snapshot must be a new file');
512
+ if (!existsSync(dirname(output)))
513
+ throw new EngineeringError('OUTPUT_DIRECTORY', 'Snapshot directory does not exist');
514
+ return output;
515
+ }
516
+ function checkCommPath(commPath) {
517
+ if (!commPath || commPath.length > 256 || /[\r\n\0]/u.test(commPath))
518
+ throw new EngineeringError('INVALID_COMM_PATH', 'Provide one FactoryTalk Linx communications path');
519
+ }
520
+ export async function controllerActionPreflight(acdPath, commPath) {
521
+ const availability = await studioSdkStatus();
522
+ requireLiveDependencies(availability);
523
+ checkCommPath(commPath);
524
+ if (extname(acdPath).toLowerCase() !== '.acd')
525
+ throw new EngineeringError('INVALID_ACD', 'Controller actions require an existing ACD file');
526
+ const path = resolve(acdPath);
527
+ const info = await stat(path).catch(() => { throw new EngineeringError('FILE_NOT_FOUND', 'ACD file not found'); });
528
+ if (!info.isFile())
529
+ throw new EngineeringError('INVALID_ACD', 'ACD path must name a file');
530
+ const sha256 = createHash('sha256').update(await readFile(path)).digest('hex');
531
+ return { path, sha256 };
532
+ }
533
+ async function runControllerAction(command, file, commPath, signal, mode, expectedMode) {
534
+ const availability = await studioSdkStatus();
535
+ requireLiveDependencies(availability);
536
+ checkCommPath(commPath);
537
+ const current = createHash('sha256').update(await readFile(file.path)).digest('hex');
538
+ if (current !== file.sha256)
539
+ throw new EngineeringError('ACD_CHANGED', 'ACD changed after approval; request a new approval');
540
+ await ensureBridge(availability, signal);
541
+ const result = await run('dotnet', [bridgeDll, command, file.path, commPath, file.sha256, ...(mode ? [mode] : []), ...(expectedMode ? [expectedMode] : [])], command === 'download' ? 240_000 : 90_000, signal);
542
+ if (result.code !== 0)
543
+ throw new EngineeringError('CONTROLLER_OPERATION_FAILED', result.output);
544
+ return result.output;
545
+ }
546
+ export async function goOnline(acdPath, commPath, signal) {
547
+ const file = await controllerActionPreflight(acdPath, commPath);
548
+ await runControllerAction('go-online', file, commPath, signal);
549
+ return { acd_path: file.path, acd_sha256: file.sha256, communications_path: commPath, operation: 'read_only_go_online', observed_at: new Date().toISOString() };
550
+ }
551
+ export async function readControllerModeForAction(file, commPath, signal) {
552
+ const availability = await studioSdkStatus();
553
+ requireLiveDependencies(availability);
554
+ checkCommPath(commPath);
555
+ await ensureBridge(availability, signal);
556
+ const result = await run('dotnet', [bridgeDll, 'read-mode', file.path, commPath], 90_000, signal);
557
+ const mode = /^READ_CONTROLLER_MODE=(\w+)$/mu.exec(result.output)?.[1];
558
+ if (result.code !== 0 || mode === undefined)
559
+ throw new EngineeringError('CONTROLLER_READ_FAILED', result.output);
560
+ return mode;
561
+ }
562
+ export async function changeControllerMode(file, commPath, mode, expectedMode, signal) {
563
+ if (!['Program', 'Run', 'Test'].includes(mode))
564
+ throw new EngineeringError('INVALID_MODE', 'target_mode must be Program, Run, or Test');
565
+ const log = await runControllerAction('change-mode', file, commPath, signal, mode, expectedMode);
566
+ return { acd_path: file.path, acd_sha256: file.sha256, communications_path: commPath, mode_before: expectedMode, requested_mode: mode, operation: 'approved_mode_change', observed_mode: /^CONTROLLER_MODE_AFTER=(\w+)$/mu.exec(log)?.[1] ?? 'unverified', completed_at: new Date().toISOString() };
567
+ }
568
+ export async function downloadProject(file, commPath, signal) {
569
+ const log = await runControllerAction('download', file, commPath, signal);
570
+ return { acd_path: file.path, acd_sha256: file.sha256, communications_path: commPath, operation: 'approved_download', controller_mode_before: /^CONTROLLER_MODE_BEFORE=(\w+)$/mu.exec(log)?.[1] ?? 'unverified', completed_at: new Date().toISOString(), studio_validation: 'human_required' };
571
+ }
@@ -0,0 +1,20 @@
1
+ /** Resolve Logix addresses against exported declarations, retaining array and alias evidence. */
2
+ import type { L5xDocument } from '../model/types.ts';
3
+ /** Resolution never certifies a dynamic subscript or a declaration absent from the export. */
4
+ export interface TagResolution {
5
+ readonly symbol: string;
6
+ readonly scope: string;
7
+ readonly resolved: boolean;
8
+ readonly canonical?: string;
9
+ readonly data_type?: string;
10
+ readonly aliases: readonly string[];
11
+ readonly indices_verified: boolean;
12
+ readonly reason?: string;
13
+ }
14
+ /** Resolve an address without discarding indices or assuming an absent member exists.
15
+ * @param document - Export containing tag, AOI and UDT declarations.
16
+ * @param scope - Program name, controller, or AOI::name.
17
+ * @param symbol - Address, optionally qualified as Program:name.Tag.
18
+ * @returns Resolution with canonical declaration spelling, final type, alias chain and unresolved reason.
19
+ */
20
+ export declare function resolveTag(document: L5xDocument, scope: string, symbol: string): TagResolution;