@codesys/mcp-toolkit 1.2.0 → 1.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -16,7 +16,9 @@
16
16
  "Bash(npm pack:*)",
17
17
  "Bash(npm pack:*)",
18
18
  "Bash(npm publish:*)",
19
- "Bash(npm publish:*)"
19
+ "Bash(npm publish:*)",
20
+ "Bash(ls:*)",
21
+ "Bash(ls:*)"
20
22
  ],
21
23
  "deny": []
22
24
  }
package/CHANGELOG.md CHANGED
@@ -5,6 +5,26 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.2.4] - 2025-05-02
9
+ ### Fixed
10
+ - Fully disabled session management to prevent operations appearing to succeed but not actually running
11
+ - Made it clearer that the persistent session feature is not yet implemented
12
+
13
+ ## [1.2.3] - 2025-05-02
14
+ ### Fixed
15
+ - Improved Windows path handling in Python scripts
16
+ - Added better error detection and reporting for file operations
17
+ - Enhanced debugging for file creation operations
18
+
19
+ ## [1.2.2] - 2025-05-02
20
+ ### Fixed
21
+ - Fixed issue where session management appeared to work but didn't create files
22
+ - Temporarily disabled incomplete session manager implementation
23
+
24
+ ## [1.2.1] - 2025-05-02
25
+ ### Fixed
26
+ - Fixed Python script templates not being included in the npm package
27
+
8
28
  ## [1.2.0] - 2025-05-02
9
29
  ### Added
10
30
  - Session management functionality with `start_session`, `stop_session`, and `session_status` commands
Binary file
@@ -56,12 +56,15 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
56
56
  };
57
57
  Object.defineProperty(exports, "__esModule", { value: true });
58
58
  exports.executeCodesysScript = executeCodesysScript;
59
+ exports.checkSessionStatus = checkSessionStatus;
60
+ exports.startSession = startSession;
61
+ exports.stopSession = stopSession;
59
62
  const child_process_1 = require("child_process");
60
63
  const promises_1 = require("fs/promises");
61
64
  const path = __importStar(require("path"));
62
65
  const os = __importStar(require("os"));
63
66
  const fs = __importStar(require("fs")); // Import fs for existsSync check
64
- // Optional import for session manager
67
+ // Session manager import
65
68
  const sessionManager_1 = require("./sessionManager");
66
69
  // Define expected success/error markers from the Python scripts
67
70
  const SCRIPT_SUCCESS_MARKER = 'SCRIPT_SUCCESS';
@@ -88,12 +91,16 @@ function executeCodesysScript(scriptContent, codesysExePath, codesysProfileName,
88
91
  if (!fs.existsSync(codesysExePath))
89
92
  throw new Error(`CODESYS executable not found at provided path: ${codesysExePath}`);
90
93
  // --- End Pre-checks ---
94
+ // Enable or disable session management
95
+ const SESSION_FEATURE_DISABLED = false;
91
96
  // --- Check if we should use the persistent session ---
92
- if (sessionManager) {
97
+ if (!SESSION_FEATURE_DISABLED && sessionManager) {
93
98
  const sessionState = sessionManager.getState();
99
+ // Check if the session is active
94
100
  if (sessionState === sessionManager_1.SessionState.RUNNING || sessionState === sessionManager_1.SessionState.BUSY) {
95
101
  process.stderr.write(`INTEROP: Using persistent session for script execution\n`);
96
102
  try {
103
+ // Execute the script in the session
97
104
  const result = yield sessionManager.executeScript(scriptContent);
98
105
  return {
99
106
  success: result.success,
@@ -106,10 +113,40 @@ function executeCodesysScript(scriptContent, codesysExePath, codesysProfileName,
106
113
  // Continue with the non-persistent approach as fallback
107
114
  }
108
115
  }
116
+ else if (sessionState === sessionManager_1.SessionState.STOPPED || sessionState === sessionManager_1.SessionState.ERROR) {
117
+ // Try to start the session if it's stopped or in error state
118
+ process.stderr.write(`INTEROP: Session is in ${sessionState} state. Attempting to start it.\n`);
119
+ try {
120
+ yield sessionManager.startSession();
121
+ const newState = sessionManager.getState();
122
+ if (newState === sessionManager_1.SessionState.RUNNING) {
123
+ process.stderr.write(`INTEROP: Session started successfully. Using it for script execution.\n`);
124
+ // Now that the session is running, execute the script
125
+ const result = yield sessionManager.executeScript(scriptContent);
126
+ return {
127
+ success: result.success,
128
+ output: result.output
129
+ };
130
+ }
131
+ else {
132
+ process.stderr.write(`INTEROP: Failed to start session (state: ${newState}). Using one-time execution.\n`);
133
+ }
134
+ }
135
+ catch (error) {
136
+ process.stderr.write(`INTEROP: Error starting session: ${error.message}\n`);
137
+ process.stderr.write(`INTEROP: Using one-time execution as fallback.\n`);
138
+ }
139
+ }
109
140
  else {
110
141
  process.stderr.write(`INTEROP: Session not in RUNNING/BUSY state (current: ${sessionState}). Using one-time execution.\n`);
111
142
  }
112
143
  }
144
+ else if (SESSION_FEATURE_DISABLED) {
145
+ process.stderr.write(`INTEROP: Session feature is disabled. Using one-time execution mode.\n`);
146
+ }
147
+ else {
148
+ process.stderr.write(`INTEROP: No session manager provided. Using one-time execution mode.\n`);
149
+ }
113
150
  // --- Standard one-time script execution ---
114
151
  const tempDir = os.tmpdir();
115
152
  const tempFileName = `codesys_script_${Date.now()}_${Math.random().toString(36).substring(2, 9)}.py`;
@@ -295,3 +332,107 @@ function executeCodesysScript(scriptContent, codesysExePath, codesysProfileName,
295
332
  return { success, output: finalOutput };
296
333
  });
297
334
  }
335
+ /**
336
+ * Checks if a session is currently active
337
+ *
338
+ * @param codesysExePath The full path to the CODESYS.exe executable
339
+ * @param codesysProfileName The name of the CODESYS profile to use for scripting
340
+ * @param sessionManager The session manager instance
341
+ * @returns Promise resolving to an object with session status
342
+ */
343
+ function checkSessionStatus(codesysExePath, codesysProfileName, sessionManager) {
344
+ return __awaiter(this, void 0, void 0, function* () {
345
+ if (!sessionManager) {
346
+ return { active: false, info: null };
347
+ }
348
+ try {
349
+ // Check if session is active
350
+ const isActive = yield sessionManager.isSessionActive();
351
+ if (!isActive) {
352
+ return { active: false, info: null };
353
+ }
354
+ // Get session information
355
+ const sessionInfo = yield sessionManager.getSessionInfo();
356
+ return {
357
+ active: true,
358
+ info: sessionInfo
359
+ };
360
+ }
361
+ catch (error) {
362
+ process.stderr.write(`INTEROP: Error checking session status: ${error.message}\n`);
363
+ return { active: false, info: null };
364
+ }
365
+ });
366
+ }
367
+ /**
368
+ * Starts a new persistent CODESYS session
369
+ *
370
+ * @param codesysExePath The full path to the CODESYS.exe executable
371
+ * @param codesysProfileName The name of the CODESYS profile to use for scripting
372
+ * @param sessionManager The session manager instance
373
+ * @returns Promise resolving when the session has started
374
+ */
375
+ function startSession(codesysExePath, codesysProfileName, sessionManager) {
376
+ return __awaiter(this, void 0, void 0, function* () {
377
+ if (!sessionManager) {
378
+ return { success: false, message: "No session manager provided" };
379
+ }
380
+ try {
381
+ // Check if session is already active
382
+ const isActive = yield sessionManager.isSessionActive();
383
+ if (isActive) {
384
+ return { success: true, message: "Session is already active" };
385
+ }
386
+ // Start a new session
387
+ yield sessionManager.startSession();
388
+ // Check if session started successfully
389
+ const statusAfterStart = yield sessionManager.isSessionActive();
390
+ if (statusAfterStart) {
391
+ return { success: true, message: "Session started successfully" };
392
+ }
393
+ else {
394
+ return { success: false, message: "Failed to start session" };
395
+ }
396
+ }
397
+ catch (error) {
398
+ process.stderr.write(`INTEROP: Error starting session: ${error.message}\n`);
399
+ return { success: false, message: `Error starting session: ${error.message}` };
400
+ }
401
+ });
402
+ }
403
+ /**
404
+ * Stops an active CODESYS session
405
+ *
406
+ * @param codesysExePath The full path to the CODESYS.exe executable
407
+ * @param codesysProfileName The name of the CODESYS profile to use for scripting
408
+ * @param sessionManager The session manager instance
409
+ * @returns Promise resolving when the session has stopped
410
+ */
411
+ function stopSession(codesysExePath, codesysProfileName, sessionManager) {
412
+ return __awaiter(this, void 0, void 0, function* () {
413
+ if (!sessionManager) {
414
+ return { success: false, message: "No session manager provided" };
415
+ }
416
+ try {
417
+ // Check if session is active
418
+ const isActive = yield sessionManager.isSessionActive();
419
+ if (!isActive) {
420
+ return { success: true, message: "No active session to stop" };
421
+ }
422
+ // Stop the session
423
+ yield sessionManager.stopSession();
424
+ // Verify session stopped
425
+ const statusAfterStop = yield sessionManager.isSessionActive();
426
+ if (!statusAfterStop) {
427
+ return { success: true, message: "Session stopped successfully" };
428
+ }
429
+ else {
430
+ return { success: false, message: "Failed to stop session" };
431
+ }
432
+ }
433
+ catch (error) {
434
+ process.stderr.write(`INTEROP: Error stopping session: ${error.message}\n`);
435
+ return { success: false, message: `Error stopping session: ${error.message}` };
436
+ }
437
+ });
438
+ }
@@ -121,8 +121,20 @@ function interpolateScript(template, params) {
121
121
  let result = template;
122
122
  // Replace each parameter in the template
123
123
  for (const [key, value] of Object.entries(params)) {
124
- // Convert value to string and escape backslashes for Python
125
- const escapedValue = String(value).replace(/\\/g, '\\\\');
124
+ // Handle Windows paths specially
125
+ let escapedValue;
126
+ // Check if this looks like a Windows path
127
+ if (typeof value === 'string' && (value.includes(':\\') || value.includes('\\'))) {
128
+ // For Windows paths, use a different approach with raw strings
129
+ console.error(`SCRIPT_LOADER: Detected Windows path: ${value}`);
130
+ // Use Python raw string format to avoid backslash interpretation issues
131
+ escapedValue = `r'${value.replace(/'/g, "\\'")}'`;
132
+ console.error(`SCRIPT_LOADER: Converted to Python raw string: ${escapedValue}`);
133
+ }
134
+ else {
135
+ // For non-path values, just escape backslashes normally
136
+ escapedValue = String(value).replace(/\\/g, '\\\\');
137
+ }
126
138
  // Replace {PARAM_NAME} with the escaped value
127
139
  const paramPattern = new RegExp(`\\{${key}\\}`, 'g');
128
140
  result = result.replace(paramPattern, escapedValue);
@@ -0,0 +1,21 @@
1
+ import sys, scriptengine as script_engine, os, traceback
2
+ project_open = False; project_name = "No project open"; project_path = "N/A"; scripting_ok = False
3
+ try:
4
+ scripting_ok = True; primary_project = script_engine.projects.primary
5
+ if primary_project:
6
+ project_open = True
7
+ try:
8
+ project_path = os.path.normcase(os.path.abspath(primary_project.path))
9
+ try:
10
+ project_name = primary_project.get_name() # Might fail
11
+ if not project_name: project_name = "Unnamed (path: %s)" % os.path.basename(project_path)
12
+ except: project_name = "Unnamed (path: %s)" % os.path.basename(project_path)
13
+ except Exception as e_path: project_path = "N/A (Error: %s)" % e_path; project_name = "Unnamed (Path Error)"
14
+ print("Project Open: %s" % project_open); print("Project Name: %s" % project_name)
15
+ print("Project Path: %s" % project_path); print("Scripting OK: %s" % scripting_ok)
16
+ print("SCRIPT_SUCCESS: Status check complete."); sys.exit(0)
17
+ except Exception as e:
18
+ error_message = "Error during status check: %s" % e
19
+ print(error_message); print("Scripting OK: False")
20
+ # traceback.print_exc() # Optional traceback
21
+ print("SCRIPT_ERROR: %s" % error_message); sys.exit(1)
@@ -0,0 +1,65 @@
1
+ import sys, scriptengine as script_engine, os, traceback
2
+
3
+ # Import ensure_project_open function from the template
4
+ # This will be prepended by the script loader
5
+ def ensure_project_open(target_project_path):
6
+ # Function implementation will be included from ensure_project_open.py
7
+ pass
8
+
9
+ try:
10
+ print("DEBUG: compile_project script: Project='%s'" % PROJECT_FILE_PATH)
11
+ primary_project = ensure_project_open(PROJECT_FILE_PATH)
12
+ project_name = os.path.basename(PROJECT_FILE_PATH)
13
+ target_app = None
14
+ app_name = "N/A"
15
+
16
+ # Try getting active application first
17
+ try:
18
+ target_app = primary_project.active_application
19
+ if target_app:
20
+ app_name = getattr(target_app, 'get_name', lambda: "Unnamed App (Active)")()
21
+ print("DEBUG: Found active application: %s" % app_name)
22
+ except Exception as active_err:
23
+ print("WARN: Could not get active application: %s. Searching..." % active_err)
24
+
25
+ # If no active app, search for the first one
26
+ if not target_app:
27
+ print("DEBUG: Searching for first compilable application...")
28
+ apps = []
29
+ try:
30
+ # Search recursively through all project objects
31
+ all_children = primary_project.get_children(True)
32
+ for child in all_children:
33
+ # Check using the marker property and if build method exists
34
+ if hasattr(child, 'is_application') and child.is_application and hasattr(child, 'build'):
35
+ app_name_found = getattr(child, 'get_name', lambda: "Unnamed App")()
36
+ print("DEBUG: Found potential application object: %s" % app_name_found)
37
+ apps.append(child)
38
+ break # Take the first one found
39
+ except Exception as find_err: print("WARN: Error finding application object: %s" % find_err)
40
+
41
+ if not apps: raise RuntimeError("No compilable application found in project '%s'" % project_name)
42
+ target_app = apps[0]
43
+ app_name = getattr(target_app, 'get_name', lambda: "Unnamed App (First Found)")()
44
+ print("WARN: Compiling first found application: %s" % app_name)
45
+
46
+ print("DEBUG: Calling build() on app '%s'..." % app_name)
47
+ if not hasattr(target_app, 'build'):
48
+ raise TypeError("Selected object '%s' is not an application or doesn't support build()." % app_name)
49
+
50
+ # Execute the build
51
+ target_app.build();
52
+ print("DEBUG: Build command executed for application '%s'." % app_name)
53
+
54
+ # Check messages is harder without direct access to message store from script.
55
+ # Rely on CODESYS UI or log output for now.
56
+ print("Compile Initiated For Application: %s" % app_name)
57
+ print("In Project: %s" % project_name)
58
+ print("SCRIPT_SUCCESS: Application compilation initiated.")
59
+ sys.exit(0)
60
+ except Exception as e:
61
+ detailed_error = traceback.format_exc()
62
+ error_message = "Error initiating compilation for project %s: %s\\n%s" % (PROJECT_FILE_PATH, e, detailed_error)
63
+ print(error_message)
64
+ print("SCRIPT_ERROR: %s" % error_message)
65
+ sys.exit(1)
@@ -0,0 +1,80 @@
1
+ import sys, scriptengine as script_engine, os, traceback
2
+
3
+ # Import ensure_project_open function from the template
4
+ # This will be prepended by the script loader
5
+ def ensure_project_open(target_project_path):
6
+ # Function implementation will be included from ensure_project_open.py
7
+ pass
8
+
9
+ # Import find_object_by_path function from the template
10
+ # This will be prepended by the script loader
11
+ def find_object_by_path_robust(start_node, full_path, target_type_name="object"):
12
+ # Function implementation will be included from find_object_by_path.py
13
+ pass
14
+
15
+ PARENT_POU_FULL_PATH = "{PARENT_POU_FULL_PATH}" # e.g., "Application/MyFB"
16
+ METHOD_NAME = "{METHOD_NAME}"
17
+ RETURN_TYPE = "{RETURN_TYPE}" # Can be empty string for no return type
18
+ # Optional: Language
19
+ # LANG_GUID_STR = "{LANG_GUID_STR}" # Example if needed
20
+
21
+ try:
22
+ print("DEBUG: create_method script: ParentPOU='%s', Name='%s', ReturnType='%s', Project='%s'" % (PARENT_POU_FULL_PATH, METHOD_NAME, RETURN_TYPE, PROJECT_FILE_PATH))
23
+ primary_project = ensure_project_open(PROJECT_FILE_PATH)
24
+ if not PARENT_POU_FULL_PATH: raise ValueError("Parent POU full path empty.")
25
+ if not METHOD_NAME: raise ValueError("Method name empty.")
26
+ # RETURN_TYPE can be empty
27
+
28
+ # Find the parent POU object
29
+ parent_pou_object = find_object_by_path_robust(primary_project, PARENT_POU_FULL_PATH, "parent POU")
30
+ if not parent_pou_object: raise ValueError("Parent POU object not found: %s" % PARENT_POU_FULL_PATH)
31
+
32
+ parent_pou_name = getattr(parent_pou_object, 'get_name', lambda: PARENT_POU_FULL_PATH)()
33
+ print("DEBUG: Found Parent POU object: %s" % parent_pou_name)
34
+
35
+ # Check if parent object supports creating methods (should implement ScriptIecLanguageMemberContainer)
36
+ if not hasattr(parent_pou_object, 'create_method'):
37
+ raise TypeError("Parent object '%s' of type %s does not support create_method." % (parent_pou_name, type(parent_pou_object).__name__))
38
+
39
+ # Default language to None (usually ST)
40
+ lang_guid = None
41
+ # Use None if RETURN_TYPE is empty string, otherwise use the string
42
+ actual_return_type = RETURN_TYPE if RETURN_TYPE else None
43
+ print("DEBUG: Calling create_method: Name='%s', ReturnType=%s, Lang=%s" % (METHOD_NAME, actual_return_type, lang_guid))
44
+
45
+ # Call the create_method method ON THE PARENT POU
46
+ new_method_object = parent_pou_object.create_method(
47
+ name=METHOD_NAME,
48
+ return_type=actual_return_type,
49
+ language=lang_guid # Pass None to use default
50
+ )
51
+
52
+ if new_method_object:
53
+ new_meth_name = getattr(new_method_object, 'get_name', lambda: METHOD_NAME)()
54
+ print("DEBUG: Method object created: %s" % new_meth_name)
55
+
56
+ # --- SAVE THE PROJECT TO PERSIST THE NEW METHOD OBJECT ---
57
+ try:
58
+ print("DEBUG: Saving Project (after method creation)...")
59
+ primary_project.save()
60
+ print("DEBUG: Project saved successfully after method creation.")
61
+ except Exception as save_err:
62
+ print("ERROR: Failed to save Project after creating method: %s" % save_err)
63
+ detailed_error = traceback.format_exc()
64
+ error_message = "Error saving Project after creating method '%s': %s\\n%s" % (METHOD_NAME, save_err, detailed_error)
65
+ print(error_message); print("SCRIPT_ERROR: %s" % error_message); sys.exit(1)
66
+ # --- END SAVING ---
67
+
68
+ print("Method Created: %s" % new_meth_name)
69
+ print("Parent POU: %s" % PARENT_POU_FULL_PATH)
70
+ print("Return Type: %s" % (RETURN_TYPE if RETURN_TYPE else "(None)"))
71
+ print("SCRIPT_SUCCESS: Method created successfully.")
72
+ sys.exit(0)
73
+ else:
74
+ error_message = "Failed to create method '%s' under '%s'. create_method returned None." % (METHOD_NAME, parent_pou_name)
75
+ print(error_message); print("SCRIPT_ERROR: %s" % error_message); sys.exit(1)
76
+
77
+ except Exception as e:
78
+ detailed_error = traceback.format_exc()
79
+ error_message = "Error creating method '%s' under POU '%s' in project '%s': %s\\n%s" % (METHOD_NAME, PARENT_POU_FULL_PATH, PROJECT_FILE_PATH, e, detailed_error)
80
+ print(error_message); print("SCRIPT_ERROR: %s" % error_message); sys.exit(1)
@@ -0,0 +1,96 @@
1
+ import sys, scriptengine as script_engine, os, traceback
2
+
3
+ # Import ensure_project_open function from the template
4
+ # This will be prepended by the script loader
5
+ def ensure_project_open(target_project_path):
6
+ # Function implementation will be included from ensure_project_open.py
7
+ pass
8
+
9
+ # Import find_object_by_path function from the template
10
+ # This will be prepended by the script loader
11
+ def find_object_by_path_robust(start_node, full_path, target_type_name="object"):
12
+ # Function implementation will be included from find_object_by_path.py
13
+ pass
14
+
15
+ POU_NAME = "{POU_NAME}"
16
+ POU_TYPE_STR = "{POU_TYPE_STR}"
17
+ IMPL_LANGUAGE_STR = "{IMPL_LANGUAGE_STR}"
18
+ PARENT_PATH_REL = "{PARENT_PATH}"
19
+
20
+ pou_type_map = {
21
+ "Program": script_engine.PouType.Program,
22
+ "FunctionBlock": script_engine.PouType.FunctionBlock,
23
+ "Function": script_engine.PouType.Function
24
+ }
25
+ # Map common language names to ImplementationLanguages attributes if needed (optional, None usually works)
26
+ # lang_map = { "ST": script_engine.ImplementationLanguage.st, ... }
27
+
28
+ try:
29
+ print("DEBUG: create_pou script: Name='%s', Type='%s', Lang='%s', ParentPath='%s', Project='%s'" % (POU_NAME, POU_TYPE_STR, IMPL_LANGUAGE_STR, PARENT_PATH_REL, PROJECT_FILE_PATH))
30
+ primary_project = ensure_project_open(PROJECT_FILE_PATH)
31
+ if not POU_NAME: raise ValueError("POU name empty.")
32
+ if not PARENT_PATH_REL: raise ValueError("Parent path empty.")
33
+
34
+ # Resolve POU Type Enum
35
+ pou_type_enum = pou_type_map.get(POU_TYPE_STR)
36
+ if not pou_type_enum: raise ValueError("Invalid POU type string: %s. Use Program, FunctionBlock, or Function." % POU_TYPE_STR)
37
+
38
+ # Find parent object using the robust function
39
+ parent_object = find_object_by_path_robust(primary_project, PARENT_PATH_REL, "parent container")
40
+ if not parent_object: raise ValueError("Parent object not found for path: %s" % PARENT_PATH_REL)
41
+
42
+ parent_name = getattr(parent_object, 'get_name', lambda: str(parent_object))()
43
+ print("DEBUG: Using parent object: %s (Type: %s)" % (parent_name, type(parent_object).__name__))
44
+
45
+ # Check if parent object supports creating POUs (should implement ScriptIecLanguageObjectContainer)
46
+ if not hasattr(parent_object, 'create_pou'):
47
+ raise TypeError("Parent object '%s' of type %s does not support create_pou." % (parent_name, type(parent_object).__name__))
48
+
49
+ # Set language GUID to None (let CODESYS default based on parent/settings)
50
+ lang_guid = None
51
+ print("DEBUG: Setting language to None (will use default).")
52
+ # Example if mapping language string: lang_guid = lang_map.get(IMPL_LANGUAGE_STR, None)
53
+
54
+ print("DEBUG: Calling parent_object.create_pou: Name='%s', Type=%s, Lang=%s" % (POU_NAME, pou_type_enum, lang_guid))
55
+
56
+ # Call create_pou using keyword arguments
57
+ new_pou = parent_object.create_pou(
58
+ name=POU_NAME,
59
+ type=pou_type_enum,
60
+ language=lang_guid # Pass None
61
+ )
62
+
63
+ print("DEBUG: parent_object.create_pou returned: %s" % new_pou)
64
+ if new_pou:
65
+ new_pou_name = getattr(new_pou, 'get_name', lambda: POU_NAME)()
66
+ print("DEBUG: POU object created: %s" % new_pou_name)
67
+
68
+ # --- SAVE THE PROJECT TO PERSIST THE NEW POU ---
69
+ try:
70
+ print("DEBUG: Saving Project...")
71
+ primary_project.save() # Save the overall project file
72
+ print("DEBUG: Project saved successfully after POU creation.")
73
+ except Exception as save_err:
74
+ print("ERROR: Failed to save Project after POU creation: %s" % save_err)
75
+ detailed_error = traceback.format_exc()
76
+ error_message = "Error saving Project after creating POU '%s': %s\\n%s" % (new_pou_name, save_err, detailed_error)
77
+ print(error_message); print("SCRIPT_ERROR: %s" % error_message); sys.exit(1)
78
+ # --- END SAVING ---
79
+
80
+ print("POU Created: %s" % new_pou_name)
81
+ print("Type: %s" % POU_TYPE_STR)
82
+ print("Language: %s (Defaulted)" % IMPL_LANGUAGE_STR)
83
+ print("Parent Path: %s" % PARENT_PATH_REL)
84
+ print("SCRIPT_SUCCESS: POU created successfully.")
85
+ sys.exit(0)
86
+ else:
87
+ error_message = "Failed to create POU '%s'. create_pou returned None." % POU_NAME
88
+ print(error_message)
89
+ print("SCRIPT_ERROR: %s" % error_message)
90
+ sys.exit(1)
91
+ except Exception as e:
92
+ detailed_error = traceback.format_exc()
93
+ error_message = "Error creating POU '%s' in project '%s': %s\\n%s" % (POU_NAME, PROJECT_FILE_PATH, e, detailed_error)
94
+ print(error_message)
95
+ print("SCRIPT_ERROR: Error creating POU '%s': %s" % (POU_NAME, e))
96
+ sys.exit(1)
@@ -0,0 +1,100 @@
1
+ import sys, scriptengine as script_engine, os, shutil, time, traceback
2
+ # Placeholders - These might be Python raw strings (r'path') from the Node.js side
3
+ # So we need to handle them specially
4
+ TEMPLATE_PROJECT_PATH_RAW = '{TEMPLATE_PROJECT_PATH}' # Path to Standard.project
5
+ PROJECT_FILE_PATH_RAW = '{PROJECT_FILE_PATH}' # Path for the new project (Target Path)
6
+
7
+ # Process the paths - if they start with r' and end with ', extract the content
8
+ def extract_path(path_str):
9
+ if path_str.startswith("r'") and path_str.endswith("'"):
10
+ # This is a raw string from Node.js - extract the content
11
+ return path_str[2:-1]
12
+ return path_str
13
+
14
+ TEMPLATE_PROJECT_PATH = extract_path(TEMPLATE_PROJECT_PATH_RAW)
15
+ PROJECT_FILE_PATH = extract_path(PROJECT_FILE_PATH_RAW)
16
+
17
+ print("DEBUG: Processed TEMPLATE_PROJECT_PATH: %s" % TEMPLATE_PROJECT_PATH)
18
+ print("DEBUG: Processed PROJECT_FILE_PATH: %s" % PROJECT_FILE_PATH)
19
+ try:
20
+ print("DEBUG: Python script create_project (copy from template):")
21
+ print("DEBUG: Template Source = %s" % TEMPLATE_PROJECT_PATH)
22
+ print("DEBUG: Target Path = %s" % PROJECT_FILE_PATH)
23
+ if not PROJECT_FILE_PATH: raise ValueError("Target project file path empty.")
24
+ if not TEMPLATE_PROJECT_PATH: raise ValueError("Template project file path empty.")
25
+ if not os.path.exists(TEMPLATE_PROJECT_PATH): raise IOError("Template project file not found: %s" % TEMPLATE_PROJECT_PATH)
26
+
27
+ # 1. Copy the template project file to the new location
28
+ target_dir = os.path.dirname(PROJECT_FILE_PATH)
29
+ print("DEBUG: Target directory path: %s" % target_dir)
30
+
31
+ # More robust directory creation
32
+ if not os.path.exists(target_dir):
33
+ try:
34
+ print("DEBUG: Creating target directory: %s" % target_dir)
35
+ os.makedirs(target_dir, exist_ok=True)
36
+ print("DEBUG: Directory creation result - exists after creation: %s" % os.path.exists(target_dir))
37
+ except Exception as dir_err:
38
+ print("ERROR: Failed to create directory %s: %s" % (target_dir, dir_err))
39
+ raise
40
+
41
+ # Check if target file already exists
42
+ if os.path.exists(PROJECT_FILE_PATH): print("WARN: Target project file already exists, overwriting: %s" % PROJECT_FILE_PATH)
43
+
44
+ print("DEBUG: Copying '%s' to '%s'..." % (TEMPLATE_PROJECT_PATH, PROJECT_FILE_PATH))
45
+ try:
46
+ # Complete path diagnosis
47
+ print("DEBUG: Template path absolute: %s" % os.path.abspath(TEMPLATE_PROJECT_PATH))
48
+ print("DEBUG: Target path absolute: %s" % os.path.abspath(PROJECT_FILE_PATH))
49
+ print("DEBUG: Template exists: %s" % os.path.exists(TEMPLATE_PROJECT_PATH))
50
+ print("DEBUG: Target dir exists: %s" % os.path.exists(target_dir))
51
+ print("DEBUG: Target dir is writable: %s" % os.access(target_dir, os.W_OK))
52
+
53
+ # Perform the copy
54
+ shutil.copy2(TEMPLATE_PROJECT_PATH, PROJECT_FILE_PATH) # copy2 preserves metadata
55
+
56
+ # Verify copy succeeded
57
+ if os.path.exists(PROJECT_FILE_PATH):
58
+ print("DEBUG: File copy VERIFIED - file exists at destination")
59
+ print("DEBUG: File size at destination: %s bytes" % os.path.getsize(PROJECT_FILE_PATH))
60
+ else:
61
+ print("ERROR: Copy seemed to succeed but file does not exist at destination!")
62
+ raise IOError("File not found at destination after copy: %s" % PROJECT_FILE_PATH)
63
+ except Exception as copy_err:
64
+ print("ERROR: Failed to copy file: %s" % copy_err)
65
+ raise
66
+
67
+ print("DEBUG: File copy complete.")
68
+
69
+ # 2. Open the newly copied project file
70
+ print("DEBUG: Opening the copied project: %s" % PROJECT_FILE_PATH)
71
+ # Set flags for silent opening
72
+ update_mode = script_engine.VersionUpdateFlags.NoUpdates | script_engine.VersionUpdateFlags.SilentMode
73
+ # try:
74
+ # update_mode = script_engine.VersionUpdateFlags.NoUpdates | script_engine.VersionUpdateFlags.SilentMode
75
+ # except AttributeError:
76
+ # print("WARN: VersionUpdateFlags not found, using integer flags for open (1 | 2 = 3).")
77
+ # update_mode = 3
78
+
79
+ project = script_engine.projects.open(PROJECT_FILE_PATH, update_flags=update_mode)
80
+ print("DEBUG: script_engine.projects.open returned: %s" % project)
81
+ if project:
82
+ print("DEBUG: Pausing briefly after open...")
83
+ time.sleep(1.0) # Allow CODESYS to potentially initialize things
84
+ try:
85
+ print("DEBUG: Explicitly saving project after opening copy...")
86
+ project.save();
87
+ print("DEBUG: Project save after opening copy succeeded.")
88
+ except Exception as save_err:
89
+ print("WARN: Explicit save after opening copy failed: %s" % save_err)
90
+ # Decide if this is critical - maybe not, but good to know.
91
+ print("Project Created from Template Copy at: %s" % PROJECT_FILE_PATH)
92
+ print("SCRIPT_SUCCESS: Project copied from template and opened successfully.")
93
+ sys.exit(0)
94
+ else:
95
+ error_message = "Failed to open project copy %s after copying template %s. projects.open returned None." % (PROJECT_FILE_PATH, TEMPLATE_PROJECT_PATH)
96
+ print(error_message); print("SCRIPT_ERROR: %s" % error_message); sys.exit(1)
97
+ except Exception as e:
98
+ detailed_error = traceback.format_exc()
99
+ error_message = "Error creating project '%s' from template '%s': %s\\n%s" % (PROJECT_FILE_PATH, TEMPLATE_PROJECT_PATH, e, detailed_error)
100
+ print(error_message); print("SCRIPT_ERROR: Error copying/opening template: %s" % e); sys.exit(1)