Initial commit: Wonderware / System Platform tools and reference

Five tools under one repo, all docs organized per DOCS-GUIDE.md:

- aalogcli: .NET 4.8 / x86 CliFx CLI for reading System Platform binary
  logs (*.aaLGX) for LLM debugging, built on aaOpenSource/aaLog. Commands:
  last, tail, range, unread, fields. Stable JSON envelope under --llm-json.
  Build template under lib/build/ for rebuilding aaLogReader.dll.

- aot: ArchestrA Object Toolkit 2014 v4.0 reference material. Dev guide
  (Markdown converted from CHM), API reference for the ArchestrA.Toolkit
  namespace, and the Monitor / Watchdog VS sample solutions.

- graccesscli: .NET 4.8 / x86 CliFx CLI that automates Galaxy
  configuration via the ArchestrA GRAccess COM interop. Includes session
  daemon, IPC protocol, and llm-json envelope contract.

- grdb: SQL/DDL exploration of the Galaxy Repository database. DDL
  captures, reusable queries, hierarchy / contained-name <-> tag-name
  translation notes.

- histdb: LLM-oriented reference for AVEVA Historian retrieval. INSQL
  linked-server, extension tables, every wwXxx time-domain extension,
  every retrieval mode, alarm/event SQL recipes, REST API. Distilled
  from the 243-page Historian Retrieval Guide.

Root contains:
- CLAUDE.md: thin index pointing into each tool's README.
- DOCS-GUIDE.md: doctrine for organizing docs for LLM consumption.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-05-03 18:22:20 -04:00
commit 32f26272ae
411 changed files with 69973 additions and 0 deletions
+6
View File
@@ -0,0 +1,6 @@
bin/
obj/
publish/
*.user
*.suo
.vs/
+154
View File
@@ -0,0 +1,154 @@
# AGENTS.md
Guidance for coding agents working in the `graccesscli` folder.
## Project Snapshot
This folder contains `graccesscli`, a .NET Framework 4.8 x86 CLI for automating AVEVA/Wonderware System Platform Galaxy configuration through the ArchestrA GRAccess COM/.NET interop library.
The important platform constraint is that GRAccess is a 32-bit COM stack. Do not try to directly load the native GRAccess DLL from a .NET 10 x64 process. The current CLI stays on `net48`, `x86`, and `[STAThread]` for COM compatibility.
For the detailed local investigation behind this constraint, read [`GRAccess-DLL-and-DotNet10-Notes.md`](GRAccess-DLL-and-DotNet10-Notes.md). Active mutation-path defect tracking lives in [`requirements-mutation-typelib-fix.md`](requirements-mutation-typelib-fix.md).
## Key Documentation
All paths below are relative to this `graccesscli/` folder.
- [`GRAccess-DLL-and-DotNet10-Notes.md`](GRAccess-DLL-and-DotNet10-Notes.md) - local GRAccess DLL, COM registration, architecture, and .NET 10 compatibility findings.
- [`requirements-mutation-typelib-fix.md`](requirements-mutation-typelib-fix.md) - mutation-path COM lifecycle fixes (typelib regression resolved, follow-on defect tracked here).
- [`CLAUDE.md`](CLAUDE.md) - existing detailed agent guide for this CLI.
- [`graccess_documentation.md`](graccess_documentation.md) - full GRAccess Toolkit API reference.
- [`graccess_operations.md`](graccess_operations.md) - GRAccess operations grouped by functional area with page references.
- [`docs/usage.md`](docs/usage.md) - CLI commands, options, session mode, and IPC protocol. Update this whenever user-facing commands change.
- [`docs/llm-integration.md`](docs/llm-integration.md) - LLM-facing operating contract, safety rules, and recommended CLI improvements for System Platform IDE automation.
- [`docs/adding-features.md`](docs/adding-features.md) - checklist for adding commands, dispatcher handlers, capabilities metadata, LLM output, validation, tests, and documentation.
- [`docs/zb-galaxy.md`](docs/zb-galaxy.md) - read-only documentation captured from the live `ZB` galaxy with `graccesscli`.
- [`docs/zb-testmachine.md`](docs/zb-testmachine.md) - deep read-only documentation of the `ZB` `$TestMachine` template family and instances.
- [`docs/template-parsing.md`](docs/template-parsing.md) - read-only workflow for inspecting existing templates such as `TestMachine`.
- [`docs/attribute-parsing.md`](docs/attribute-parsing.md) - detailed workflow for parsing all template attributes and setting families.
- [`docs/script-parsing.md`](docs/script-parsing.md) - workflow for parsing script libraries and object-level scripts.
- [`docs/template-editing.md`](docs/template-editing.md) - end-to-end workflow for safely editing existing templates.
- [`docs/template-instance-editing.md`](docs/template-instance-editing.md) - workflow for creating and editing template instances, areas, engine assignments, and I/O settings.
- [`docs/attribute-editing.md`](docs/attribute-editing.md) - detailed workflow for editing template attributes, UDAs, extensions, and setting families.
- [`docs/script-editing.md`](docs/script-editing.md) - workflow for editing script libraries and script-bearing template content.
- [`usage.md`](usage.md) - compatibility copy of the CLI usage documentation; keep it aligned with [`docs/usage.md`](docs/usage.md) if edited.
- [`docs/README.md`](docs/README.md) - index for CLI documentation stored under `docs/`.
- [`docs/clifx_reference.md`](docs/clifx_reference.md) - local CliFx framework reference.
## Repository Layout
```text
graccesscli/
ZB.MOM.WW.GRAccess.Cli.slnx
lib/ArchestrA.GRAccess.dll
src/ZB.MOM.WW.GRAccess.Cli/
Program.cs
Commands/
GRAccess/
Infrastructure/
Protocol/
Session/
tests/ZB.MOM.WW.GRAccess.Cli.Tests/
```
## Build And Test
Run commands from this `graccesscli` folder unless noted otherwise.
```powershell
dotnet build src/ZB.MOM.WW.GRAccess.Cli/ZB.MOM.WW.GRAccess.Cli.csproj -p:Platform=x86
dotnet test tests/ZB.MOM.WW.GRAccess.Cli.Tests/ZB.MOM.WW.GRAccess.Cli.Tests.csproj -p:Platform=x86
dotnet test tests/ZB.MOM.WW.GRAccess.Cli.Tests --filter "FullyQualifiedName~TestName"
```
Run the CLI:
```powershell
dotnet run --project src/ZB.MOM.WW.GRAccess.Cli/ZB.MOM.WW.GRAccess.Cli.csproj -- <args>
```
## Implementation Rules
- Keep the CLI and tests targeting `net48` and `x86`.
- Keep the entry point `[STAThread]`; GRAccess COM calls must run on an STA thread.
- Route long-lived GRAccess work through the session daemon infrastructure when appropriate.
- All GRAccess COM access in daemon mode must go through `Session/StaComThread.cs`.
- Keep the `GRAccessApp` object alive for the lifetime of any GRAccess objects derived from it.
- Check `CommandResult.Successful` after GRAccess calls. Multi-object calls return `CommandResults`.
- For object changes, follow `CheckOut()` -> modify -> `Save()` -> `CheckIn(comment)`. Skipping `CheckIn` leaves the object locked in the Galaxy repo.
- Treat GRAccess collections as 1-based unless the API docs prove otherwise.
- Use `using ArchestrA.GRAccess;` for GRAccess API types.
- Use CliFx command patterns already present in the repo: command classes implement `ICommand` and use `[Command]`, `[CommandParameter]`, and `[CommandOption]`.
- C# language version is 9.0. `init` is supported through `IsExternalInit.cs`; do not use the `required` keyword.
## Session Mode
Session mode keeps a GRAccess connection open in a daemon to avoid expensive reconnects.
```powershell
graccess session start --galaxy MyGalaxy --node MyNode
graccess <command> --galaxy MyGalaxy
graccess session status --galaxy MyGalaxy
graccess session stop --galaxy MyGalaxy
```
The daemon uses:
- Session file: `%LOCALAPPDATA%\ZB.MOM.WW.GRAccess.Cli\sessions\{galaxy}.json`
- Log file: `%LOCALAPPDATA%\ZB.MOM.WW.GRAccess.Cli\logs\daemon-{galaxy}.log`
- Named pipe: `graccess-session-{galaxy}`
- Mutex: `Global\graccess-session-{galaxy}`
- IPC: newline-delimited JSON
See [`docs/usage.md`](docs/usage.md) and [`CLAUDE.md`](CLAUDE.md) before changing session behavior.
## GRAccess References
The primary API entry point is `GRAccessAppClass`.
Typical flow:
```csharp
GRAccessApp grAccess = new GRAccessAppClass();
IGalaxies galaxies = grAccess.QueryGalaxies(Environment.MachineName);
IGalaxy galaxy = galaxies["GalaxyName"];
galaxy.Login("", "");
// work with galaxy
galaxy.Logout();
```
Object model summary:
```text
GRAccessApp
IGalaxies -> IGalaxy
IGalaxySecurity / roles / users
IgObjects -> ITemplate / IInstance
IAttributes -> IAttribute
IToolsets
IScriptLibraries
ICommandResults
```
For exact method signatures, enum names, and page references, use [`graccess_documentation.md`](graccess_documentation.md) and [`graccess_operations.md`](graccess_operations.md).
## Current CLI Query Surface
Galaxy-bound commands route through an active session when one exists, and otherwise fall back to one-shot mode:
```powershell
graccess object list --galaxy ZB --node . --type instance --pattern '%'
graccess template list --galaxy ZB --node . --pattern '%'
graccess instance list --galaxy ZB --node . --pattern '%'
graccess object attributes --galaxy ZB --node . --name DEV --type instance --configurable
```
When a session is active, routed commands can omit `--node` and are dispatched by `SessionDaemon.ExecuteCommandAsync` through `GRAccessCommandDispatcher` on the daemon STA thread. GRAccess `namedLike` patterns use `%` as the wildcard. Attribute metadata should be read defensively because some COM-backed properties can throw when storage is unavailable; unavailable JSON fields are represented as `null`.
The expanded CLI surface is concentrated in `Commands/GRAccessSurfaceCommands.cs` and routes through `GRAccess/GRAccessCommandDispatcher.cs`. Mutating commands must pass `--confirm` and matching `--confirm-target`; keep that guard in place for delete, deploy, restore, migrate, import, GRLoad, and object configuration changes.
LLM-facing automation is documented in [`docs/llm-integration.md`](docs/llm-integration.md). Preserve legacy `--json` shapes; use `--llm-json` for stable envelopes. Keep `capabilities`, `validate`, `batch`, `object snapshot`, attribute value commands, object script commands, and area/engine/I/O wrappers aligned with `CommandCapabilityRegistry`.
For deep read-only parsing, prefer `object snapshot`, `object lineage`, and `object children` with `--llm-json`. These commands should use direct GRAccess typed reads first and may use temporary GRAccess-exported package parsing for lineage, contained objects, attribute values, and script bodies when direct COM properties are unavailable. Normal CLI behavior must not query the Galaxy Repository SQL database; SQL access is development verification/debugging only.
When adding new CLI features, follow [`docs/adding-features.md`](docs/adding-features.md) and update user-facing docs in the same change.
Binary file not shown.
+169
View File
@@ -0,0 +1,169 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
This is a .NET Framework 4.8 console application (CLI) for automating Aveva System Platform Galaxy configuration via the **ArchestrA GRAccess** COM/.NET library. The GRAccess API exposes a hierarchical object model rooted at `GRAccessAppClass` for programmatically managing Galaxies, templates, instances, attributes, users, roles, and security.
## Key References
- **GRAccess documentation**: `graccess_documentation.md` (full API reference, type definitions, code examples)
- **GRAccess DLL**: `lib/ArchestrA.GRAccess.dll` — COM interop assembly, referenced by the CLI csproj
- **GRAccess operations**: `graccess_operations.md` - all library operations organized by functional area with page references
- **CLI usage**: `docs/usage.md` - all CLI commands, options, session mode, IPC protocol. **Must be updated whenever commands are added or changed.**
- **LLM integration**: `docs/llm-integration.md` - implemented LLM-facing operating contract, stable envelope, safety rules, validation, batch plans, and IDE intent wrappers.
- **Adding features**: `docs/adding-features.md` - checklist for adding commands, dispatcher handlers, capabilities metadata, LLM output, validation, tests, and documentation.
- **ZB galaxy documentation**: `docs/zb-galaxy.md` - read-only documentation captured from the live `ZB` galaxy with `graccess_cli`.
- **ZB TestMachine documentation**: `docs/zb-testmachine.md` - deep read-only documentation of the `ZB` `$TestMachine` template family and instances.
- **Template parsing**: `docs/template-parsing.md` - read-only workflow for inspecting existing templates such as `TestMachine`.
- **Attribute parsing**: `docs/attribute-parsing.md` - detailed workflow for parsing all template attributes and setting families.
- **Script parsing**: `docs/script-parsing.md` - workflow for parsing script libraries and object-level scripts.
- **Template editing**: `docs/template-editing.md` - end-to-end workflow for safely editing existing templates.
- **Template instance editing**: `docs/template-instance-editing.md` - workflow for creating and editing template instances, areas, engine assignments, and I/O settings.
- **Attribute editing**: `docs/attribute-editing.md` - detailed workflow for editing template attributes, UDAs, extensions, and setting families.
- **Script editing**: `docs/script-editing.md` - workflow for editing script libraries and script-bearing template content.
- **CliFx reference**: `docs/clifx_reference.md` - local copy of CliFx framework docs (commands, parameters, options, DI, testing)
- The DLL's default GAC path is `C:\Windows\assembly\GAC\Archestra.GRAccess\1.7.0.0_23106a86e706d0ae\Archestra.GRAccess.dll`
## Solution Layout
```
ZB.MOM.WW.GRAccess.Cli.slnx
├── lib/ArchestrA.GRAccess.dll
├── docs/usage.md
├── docs/llm-integration.md
├── docs/adding-features.md
├── docs/zb-galaxy.md
├── docs/zb-testmachine.md
├── docs/template-parsing.md
├── docs/attribute-parsing.md
├── docs/script-parsing.md
├── docs/template-editing.md
├── docs/template-instance-editing.md
├── docs/attribute-editing.md
├── docs/script-editing.md
├── docs/clifx_reference.md
├── graccess_documentation.md
├── graccess_operations.md
├── src/ZB.MOM.WW.GRAccess.Cli/ # CLI project (net48, x86, CliFx)
│ ├── Program.cs # Dual entry: CLI mode or --daemon mode
│ ├── IsExternalInit.cs # Polyfill for init accessors on net48
│ ├── Commands/Session/ # session start|stop|status commands
│ ├── Session/ # Daemon infrastructure
│ │ ├── StaComThread.cs # STA thread + Win32 message pump
│ │ ├── SessionDaemon.cs # Named pipe server, idle timeout
│ │ ├── SessionClient.cs # Named pipe client
│ │ └── SessionInfo.cs # Session state file (PID, pipe name)
│ ├── GRAccess/
│ │ ├── GRAccessConnection.cs # Wraps connect/login/logout
│ │ └── PackageSnapshot.cs # Read-only exported package parser for deep snapshots
│ ├── Protocol/ # IPC message DTOs
│ │ ├── PipeRequest.cs
│ │ ├── PipeResponse.cs
│ │ └── PipeProtocol.cs
│ └── Infrastructure/
│ └── CommandRouter.cs # Routes via session or one-shot
└── tests/ZB.MOM.WW.GRAccess.Cli.Tests/ # xunit + Shouldly
```
## Build & Test Commands
```bash
# Build
dotnet build src/ZB.MOM.WW.GRAccess.Cli/ZB.MOM.WW.GRAccess.Cli.csproj -p:Platform=x86
# Run CLI
dotnet run --project src/ZB.MOM.WW.GRAccess.Cli/ZB.MOM.WW.GRAccess.Cli.csproj -- <args>
# Run all tests
dotnet test tests/ZB.MOM.WW.GRAccess.Cli.Tests/ZB.MOM.WW.GRAccess.Cli.Tests.csproj -p:Platform=x86
# Run a single test
dotnet test tests/ZB.MOM.WW.GRAccess.Cli.Tests/ --filter "FullyQualifiedName~TestName"
```
Target framework is **.NET Framework 4.8** (SDK-style csproj). Must target **x86** (both CLI and test projects) because GRAccess is a 32-bit COM component. Uses **CliFx** for CLI parsing — commands are classes implementing `ICommand` with `[Command]`, `[CommandParameter]`, and `[CommandOption]` attributes. C# 9.0 `init` accessors require the `IsExternalInit.cs` polyfill; `required` keyword is not available.
## Session Architecture
The CLI supports two execution modes to avoid expensive repeated GRAccess connections:
### One-shot mode (default)
Opens connection, runs command, closes. Simple but slow.
### Session mode
```bash
graccess session start --galaxy MyGalaxy --node MyNode # spawns daemon
graccess session status --galaxy MyGalaxy # check status
graccess <command> --galaxy MyGalaxy # routes via daemon
graccess session stop --galaxy MyGalaxy # tears down
```
**How it works:** Same binary, two codepaths. `--daemon` hidden flag (bypasses CliFx) launches the daemon which:
1. Acquires a `Mutex` (`Global\graccess-session-{galaxy}`) for single-instance
2. Starts a `StaComThread` (STA thread + Win32 message pump for COM interop)
3. Connects to the galaxy on the STA thread
4. Writes session info to `%LOCALAPPDATA%\ZB.MOM.WW.GRAccess.Cli\sessions\{galaxy}.json`
5. Listens on named pipe `graccess-session-{galaxy}` for JSON requests
6. Auto-exits after idle timeout (default 30 min)
**IPC protocol:** Newline-delimited JSON over named pipes. Request types: `execute`, `shutdown`, `status`.
**`CommandRouter`** detects active sessions via `SessionClient.TryConnect()` and routes commands through the pipe. Falls back to one-shot if no session is running.
**Key constraint:** All GRAccess COM calls must execute on the `StaComThread`. The daemon marshals work via `RunAsync<T>(Func<T>)` which posts to a `ConcurrentQueue` and wakes the message pump with `PostThreadMessage(WM_APP)`.
For LLM/deep parsing, prefer `object snapshot`, `object lineage`, and `object children` with `--llm-json`. These commands use typed GRAccess reads first and can fall back to temporary GRAccess-exported package parsing for lineage, contained objects, scalar attribute values, and script bodies. Normal CLI behavior must not query the Galaxy Repository SQL database; SQL is only for development verification/debugging.
## GRAccess API Patterns
All GRAccess code must follow these patterns:
1. **Entry point must use `[STAThread]`** — GRAccess is a COM STA component
2. **`GRAccessApp` must stay in scope** for the lifetime of any GRAccess objects — do not let it get garbage collected
3. **Error handling**: Check `CommandResult.Successful` after every API call. Multi-object operations return `CommandResults` (plural). These work like `GetLastError` in C++.
4. **Workflow for modifying objects**: `CheckOut()` → make changes → `Save()``CheckIn(comment)`
5. **Collections are 1-based**, not 0-based
6. **Namespace**: `using ArchestrA.GRAccess;` — classes are `GRAccessAppClass`, `MxValueClass`, etc.
### Typical connection flow
```csharp
GRAccessApp grAccess = new GRAccessAppClass();
IGalaxies galaxies = grAccess.QueryGalaxies(Environment.MachineName);
IGalaxy galaxy = galaxies["GalaxyName"];
galaxy.Login("", "");
// ... work with galaxy ...
galaxy.Logout();
```
## GRAccess Object Model Hierarchy
```
GRAccessApp (root)
├── IGalaxies → IGalaxy
│ ├── IGalaxySecurity → ISecurityGroups → ISecurityGroup
│ ├── IGalaxyRoles → IGalaxyRole → IGalaxyUsers → IGalaxyUser
│ └── IgObjects (QueryObjects/QueryObjectsByName)
│ ├── ITemplate → CreateInstance() → IInstance
│ └── IInstance (CheckOut/Save/CheckIn/Deploy/Undeploy)
│ └── IAttributes → IAttribute (Get/SetValue via MxValue)
├── IToolsets → IToolset
├── IScriptLibraries → IScriptLibrary
└── ICommandResults → ICommandResult
```
## Important Type Definitions
- **`EgObjectIsTemplateOrInstance`**: `gObjectIsTemplate` or `gObjectIsInstance` — used in query methods
- **`MxDataType`**: `MxString`, `MxInteger`, `MxFloat`, `MxBoolean`, etc.
- **`MxAttributeCategory`**: `MxCategoryWriteable_USC_Lockable`, etc.
- **`EAuthenticationMode`**: `galaxyAuthenticationMode`, `osAuthenticationMode`
- **`MxValue`/`MxValueClass`**: Universal value container (like Variant), handles type conversions automatically
## Platform Requirements
- Must run on a machine with Aveva System Platform (Application Server) installed, or have the GRAccess DLL registered
- For remote machine access, the OSConfigUtility must have been run
- On 64-bit OS, the import path must include `(x86)`: `C:\Program Files (x86)\ArchestrA\Framework\Bin\`
@@ -0,0 +1,149 @@
# AVEVA GRAccess DLL and .NET 10 Notes
Date: 2026-04-26
## Installed GRAccess files
Primary AVEVA/System Platform GRAccess files found on this machine:
| Path | Purpose | Runtime / architecture |
| --- | --- | --- |
| `C:\Program Files (x86)\Common Files\ArchestrA\Framework\Bin\GRAccess.dll` | Main GRAccess COM in-process server | Native PE32 x86, file version `5200.0051.1991.1` |
| `C:\Program Files (x86)\Common Files\ArchestrA\ArchestrA.GRAccess.dll` | .NET COM interop assembly | IL-only, CLR v2 metadata, assembly version `2.0.0.0`, file version `2000.0002.1957.1` |
| `C:\Windows\assembly\GAC\ArchestrA.GRAccess\2.0.0.0__23106a86e706d0ae\ArchestrA.GRAccess.dll` | GAC copy of the interop assembly | Same assembly version `2.0.0.0` |
| `C:\Program Files (x86)\ArchestrA\Framework\Bin\GRAccessApp.exe` | GRAccess COM out-of-process server | Native PE32 x86, file version `5200.0051.1991.1` |
| `C:\Program Files (x86)\Common Files\ArchestrA\GRAccess20.tlb` | Current GRAccess type library | COM metadata |
Older type libraries are also installed in:
`C:\Program Files (x86)\Common Files\ArchestrA`
Files present:
- `GRAccess10.tlb`
- `GRAccess11.tlb`
- `GRAccess111.tlb`
- `GRAccess12.tlb`
- `GRAccess13.tlb`
- `GRAccess14.tlb`
- `GRAccess15.tlb`
- `GRAccess16.tlb`
- `GRAccess17.tlb`
- `GRAccess18.tlb`
- `GRAccess19.tlb`
- `GRAccess20.tlb`
AutoBuild-related GRAccess files:
| Path | Purpose | Runtime / architecture |
| --- | --- | --- |
| `C:\Program Files (x86)\Wonderware\OI-Server\AutoBuild\Bin\ArchestrA.GRAccess.dll` | AutoBuild interop assembly | IL-only, CLR v2 metadata, assembly version `1.8.0.0` |
| `C:\Program Files (x86)\Wonderware\OI-Server\AutoBuild\Bin\GRAccessUtility.dll` | AutoBuild helper | Managed IL-only, references .NET Framework 4-era assemblies |
| `C:\Program Files (x86)\Wonderware\OI-Server\AutoBuild\Bin\GRAccessProcess.exe` | AutoBuild process helper | Managed IL-only, `32BITREQUIRED` |
## COM registration
`GRAccess` / `GRAccess.1`
- CLSID: `{40F5AD46-FE1A-4D0E-B92D-C643E8D2BDF3}`
- Registered under the 32-bit COM registry view:
`HKLM\SOFTWARE\WOW6432Node\Classes\CLSID\{40F5AD46-FE1A-4D0E-B92D-C643E8D2BDF3}`
- In-process server:
`C:\Program Files (x86)\Common Files\ArchestrA\Framework\Bin\GRAccess.dll`
- Threading model: `Apartment`
`GRAccessApp` / `GRAccessApp.1`
- CLSID: `{949654D1-B9C0-468f-B43D-33431004297A}`
- AppID: `{604B52A4-FD85-4F02-9C53-C61B23D2039C}`
- Local server:
`C:\Program Files (x86)\ArchestrA\Framework\Bin\GRAccessApp.exe`
- TypeLib: `{054F998C-9E20-425C-A69D-FCEA0F44442E}`
The `GRAccessApp` type library registration points to:
`C:\Program Files (x86)\ArchestrA\Framework\Bin\GRAccessApp.exe`
## ExportObjects `TYPE_E_LIBNOTREGISTERED` finding
`IgObjects.ExportObjects(...)` can fail with:
```text
Library not registered. (Exception from HRESULT: 0x8002801D (TYPE_E_LIBNOTREGISTERED))
```
Two separate issues were verified locally:
1. .NET publisher policy redirects `ArchestrA.GRAccess` to the GAC `2.0.0.0` interop assembly unless the CLI opts out. The CLI carries and builds against the local `1.7.0.0` interop assembly, which matches the working command surface. `src/ZB.MOM.WW.GRAccess.Cli/App.config` disables publisher policy for `ArchestrA.GRAccess` so the private assembly is used.
2. Late-bound reflection calls against `IgObjects.ExportObjects` can trigger type-library lookup failures even when the typed interface call works. Export/package fallback code should call the strongly typed `IgObjects.ExportObjects(...)`, `IgObjects.ExportObjectsAsProtected(...)`, and `IgObjects.Add(...)` methods instead of using `InvokeMember`.
After both fixes, this command successfully exports `$TestMachine` from `ZB` to an `.aaPKG`:
```powershell
graccess objects export --galaxy ZB --node . --type template --name '$TestMachine' --output "$env:TEMP\TestMachine.aaPKG" --export-type exportAsPDF --confirm --confirm-target "$env:TEMP\TestMachine.aaPKG" --llm-json
```
## .NET 10 / x64 compatibility notes
The installed AVEVA GRAccess stack is not a .NET 10, full-managed, x64-compatible DLL stack.
Key findings:
- `GRAccess.dll` is native PE32 x86. It cannot be loaded into a .NET 10 x64 process.
- `ArchestrA.GRAccess.dll` is an interop assembly, not the implementation. It contains mostly `[ComImport]` interfaces/classes such as `IGRAccess`, `GRAccessClass`, and `GRAccessAppClass`.
- The interop assembly uses old CLR metadata and references `mscorlib, Version=1.0.5000.0`.
- The in-process COM class `GRAccess` is registered only in the 32-bit COM view. Creating it from 64-bit PowerShell/.NET fails with `REGDB_E_CLASSNOTREG` / `0x80040154`.
- The out-of-process COM class `GRAccessApp` can be created from a 64-bit process because COM can launch the 32-bit local server EXE.
- `GRAccessApp.exe.config` uses:
```xml
<startup useLegacyV2RuntimeActivationPolicy="true">
<supportedRuntime version="v4.0"/>
</startup>
```
This is .NET Framework-era hosting/configuration, not .NET 10.
## Native dependency imports
`GRAccess.dll` imports these native DLLs:
```text
ADVAPI32.dll
ATL100.DLL
dbghelp.dll
KERNEL32.dll
MSVCP100.dll
MSVCR100.dll
ole32.dll
OLEAUT32.dll
Secur32.dll
SHLWAPI.dll
USER32.dll
WWPackageManager.DLL
```
`GRAccessApp.exe` imports:
```text
ADVAPI32.dll
ATL100.DLL
dbghelp.dll
KERNEL32.dll
MSVCP100.dll
MSVCR100.dll
ole32.dll
OLEAUT32.dll
SHLWAPI.dll
USER32.dll
```
## Practical options for a new .NET 10 project
For a .NET 10 x64 project, there are three realistic approaches:
1. Use `GRAccessApp` as an out-of-process COM bridge if its exposed API covers the required operations.
2. Build a 32-bit sidecar process that loads the native GRAccess COM stack, then expose a modern IPC API to .NET 10 x64.
3. Reimplement the needed GRAccess behavior in managed .NET 10 x64 using the type libraries and observed behavior as reference material.
Do not plan on directly referencing `GRAccess.dll` from .NET 10 x64. It is a 32-bit native COM server.
+62
View File
@@ -0,0 +1,62 @@
# graccesscli
A `.NET Framework 4.8 / x86` CliFx-based CLI for automating AVEVA / Wonderware System Platform Galaxy configuration through the **ArchestrA GRAccess** COM interop library (`ZB.MOM.WW.GRAccess.Cli`).
## Hard constraints
GRAccess is a 32-bit COM stack. Skipping any of these will fail at load or corrupt Galaxy state:
- Target framework / arch: `net48`, `x86`, `[STAThread]`. No exceptions.
- **Cannot** be loaded from a .NET 10 / x64 process — see [`GRAccess-DLL-and-DotNet10-Notes.md`](GRAccess-DLL-and-DotNet10-Notes.md).
- AVEVA System Platform must be installed locally (or `ArchestrA.GRAccess.dll` registered in the GAC at `C:\Windows\assembly\GAC\Archestra.GRAccess\1.7.0.0_23106a86e706d0ae\`). The bundled copy in `lib/` is for build-time reference only.
- In daemon mode, every COM call must be marshalled through [`Session/StaComThread.cs`](src/ZB.MOM.WW.GRAccess.Cli/Session/StaComThread.cs); calling GRAccess from any other thread will deadlock or corrupt state.
- Keep the root `GRAccessApp` alive for the lifetime of any derived `IGalaxy` / `IgObject` handle.
- Mutation flow is fixed: `CheckOut → modify → Save → CheckIn(comment)`. Skipping `CheckIn` leaves the object locked in the Galaxy repo.
- Build requires Visual Studio / MSBuild with the `x86` platform target.
## Layout
```text
graccesscli/
ZB.MOM.WW.GRAccess.Cli.slnx # solution
lib/ArchestrA.GRAccess.dll # bundled COM interop assembly (reference only)
src/ZB.MOM.WW.GRAccess.Cli/ # CLI project (net48, x86, CliFx)
tests/ZB.MOM.WW.GRAccess.Cli.Tests # test project
docs/ # CLI workflows, LLM contract, parsing/editing guides
AGENTS.md # coding-agent rules for this tool
CLAUDE.md # detailed agent guide
graccess_documentation.md # full GRAccess API reference
graccess_operations.md # GRAccess operations grouped by functional area
usage.md # compatibility copy of docs/usage.md
GRAccess-DLL-and-DotNet10-Notes.md # platform / 32-bit COM / .NET 10 incident notes
requirements-mutation-typelib-fix.md # active mutation-path defect tracker
```
## Resource index
| Task | Go to |
| --- | --- |
| Coding-agent rules for working in this folder | [`AGENTS.md`](AGENTS.md) |
| General agent guide (project overview, references) | [`CLAUDE.md`](CLAUDE.md) |
| Full GRAccess API surface (types, methods) | [`graccess_documentation.md`](graccess_documentation.md) |
| GRAccess operations grouped by functional area, with page refs | [`graccess_operations.md`](graccess_operations.md) |
| CLI commands, options, session mode, IPC protocol | [`docs/usage.md`](docs/usage.md) |
| LLM-facing operating contract (envelopes, safety, batch) | [`docs/llm-integration.md`](docs/llm-integration.md) |
| Add a new CLI command end-to-end | [`docs/adding-features.md`](docs/adding-features.md) |
| Inspect / read existing templates (read-only) | [`docs/template-parsing.md`](docs/template-parsing.md) |
| Parse template attributes and setting families | [`docs/attribute-parsing.md`](docs/attribute-parsing.md) |
| Parse script libraries and object-level scripts | [`docs/script-parsing.md`](docs/script-parsing.md) |
| Edit existing templates safely | [`docs/template-editing.md`](docs/template-editing.md) |
| Create / edit template instances, areas, engine assignments, I/O | [`docs/template-instance-editing.md`](docs/template-instance-editing.md) |
| Edit template attributes, UDAs, extensions, setting families | [`docs/attribute-editing.md`](docs/attribute-editing.md) |
| Edit script libraries and script-bearing template content | [`docs/script-editing.md`](docs/script-editing.md) |
| Live `ZB` galaxy reference (read-only capture) | [`docs/zb-galaxy.md`](docs/zb-galaxy.md) |
| Live `ZB` `$TestMachine` template family reference | [`docs/zb-testmachine.md`](docs/zb-testmachine.md) |
| CliFx framework reference (commands, options, DI, testing) | [`docs/clifx_reference.md`](docs/clifx_reference.md) |
| GRAccess DLL / 32-bit COM / .NET 10 platform notes | [`GRAccess-DLL-and-DotNet10-Notes.md`](GRAccess-DLL-and-DotNet10-Notes.md) |
| Active mutation-path COM lifecycle defects | [`requirements-mutation-typelib-fix.md`](requirements-mutation-typelib-fix.md) |
| Index for `docs/` only | [`docs/README.md`](docs/README.md) |
## Maintenance
Documentation rules live in [`../DOCS-GUIDE.md`](../DOCS-GUIDE.md); the root task → tool index lives in [`../CLAUDE.md`](../CLAUDE.md). When adding, renaming, or removing any doc in this folder, update the resource index above in the same change. When the user-facing CLI surface changes, update [`docs/usage.md`](docs/usage.md) (and keep [`usage.md`](usage.md) aligned).
+8
View File
@@ -0,0 +1,8 @@
<Solution>
<Folder Name="/src/">
<Project Path="src/ZB.MOM.WW.GRAccess.Cli/ZB.MOM.WW.GRAccess.Cli.csproj" />
</Folder>
<Folder Name="/tests/">
<Project Path="tests/ZB.MOM.WW.GRAccess.Cli.Tests/ZB.MOM.WW.GRAccess.Cli.Tests.csproj" />
</Folder>
</Solution>
+17
View File
@@ -0,0 +1,17 @@
# GRAccess CLI Documentation
This folder stores user-facing and implementation reference documentation for `graccess_cli`.
- `usage.md` - CLI commands, options, session mode, IPC protocol, and expanded command surface. Update this whenever user-facing commands change.
- `llm-integration.md` - implemented LLM-facing operating contract, stable envelope, safety rules, validation, batch plans, and IDE intent wrappers.
- `adding-features.md` - contributor checklist for adding commands, dispatcher handlers, LLM output, validation, tests, and documentation.
- `zb-galaxy.md` - read-only documentation captured from the live `ZB` galaxy with `graccess_cli`.
- `zb-testmachine.md` - deep read-only documentation of the `ZB` `$TestMachine` template family and instances.
- `template-parsing.md` - read-only workflow for inspecting existing templates such as `TestMachine`.
- `attribute-parsing.md` - detailed workflow for parsing all template attributes and setting families.
- `script-parsing.md` - workflow for parsing script libraries and object-level scripts.
- `template-editing.md` - end-to-end workflow for safely editing existing templates.
- `template-instance-editing.md` - workflow for creating and editing template instances, areas, engine assignments, and I/O settings.
- `attribute-editing.md` - detailed workflow for editing template attributes, UDAs, extensions, and setting families.
- `script-editing.md` - workflow for editing script libraries and script-bearing template content.
- `clifx_reference.md` - local CliFx framework reference.
+192
View File
@@ -0,0 +1,192 @@
# Adding GRAccess CLI Features
Use this checklist when adding a new `graccess_cli` command, option, output field, dispatcher operation, or LLM-facing workflow. The goal is to keep human CLI behavior, script compatibility, session routing, and LLM automation aligned.
## First Read
Before editing code, read the relevant source material:
- `graccess_operations.md` for the operation family and page references.
- `graccess_documentation.md` for exact GRAccess signatures, enum names, return types, and object model constraints.
- `docs/usage.md` for the current public CLI contract.
- `docs/llm-integration.md` for machine-facing output, validation, batch, and confirmation rules.
- The domain docs that match the feature: parsing, editing, scripts, attributes, or template instances.
Do not infer COM behavior from names alone. GRAccess methods often return `CommandResult` or `CommandResults` and may expose properties that throw when backing storage is unavailable.
## Decide The Command Shape
Choose the smallest command that maps clearly to the GRAccess operation.
- Use established CliFx patterns: command classes implement `ICommand` and use `[Command]`, `[CommandParameter]`, and `[CommandOption]`.
- Keep galaxy-bound commands routable through `CommandRouter`.
- Keep pre-login galaxy operations one-shot when they cannot use a galaxy-specific session.
- Prefer explicit subcommands over overloaded string modes.
- Preserve existing command names and existing `--json` shapes.
- Add `--llm-json` for machine-stable output when the command is useful to automation.
- Add `--dry-run` to mutating routed commands for validation without invoking mutating COM calls.
For mutating operations, choose the safety rule before implementation:
- All mutations require `--confirm`.
- Destructive, deployment, import/export, restore, migrate, GRLoad, and object configuration changes require `--confirm-target <exact target>`.
- Batch plans must include per-step `confirm: true` and exact `confirm-target`; do not add global mutation confirmation.
## Register Capabilities
Every public command intended for automation must be represented in `Infrastructure/CommandCapabilityRegistry.cs`.
Add or update:
- Command name.
- Required and optional args.
- Whether it mutates state.
- Whether it supports session routing.
- Whether it requires `--confirm`.
- Whether it requires `--confirm-target`, and which target rule applies.
- Output modes, including `text`, `json`, and `llm-json` where supported.
- Schema name for the request or response family.
The registry is the source for `graccess capabilities`, `graccess validate`, batch validation, and tests. Do not scrape docs or help output to discover commands.
## Add The CliFx Wrapper
Put thin command wrappers in the existing command area that matches the feature:
- `Commands/GRAccessSurfaceCommands.cs` for most expanded GRAccess operation surface commands.
- `Commands/Objects/*` for established object list/query commands.
- `Commands/Galaxy/*` for galaxy commands.
- `Commands/LlmCommands.cs` for capabilities, validate, and batch behavior.
Wrapper responsibilities should stay narrow:
- Parse CLI args.
- Build a structured `Dictionary<string, object>` request.
- Pass the request to `CommandRouter` for galaxy-bound work.
- Print text, legacy JSON, or LLM JSON exactly once.
Do not put GRAccess COM logic in command wrappers unless the operation is explicitly one-shot and cannot be session-routed.
## Route Through The Dispatcher
Galaxy-bound behavior belongs in `GRAccess/GRAccessCommandDispatcher.cs`.
When adding a handler:
- Dispatch by command family and subcommand.
- Keep daemon and one-shot behavior identical by using the same dispatcher path.
- Keep all daemon COM access on `StaComThread`.
- Check GRAccess `CommandResult.Successful` or every result in `CommandResults`.
- For object mutations, follow `CheckOut()` -> modify -> `Save()` -> `CheckIn(comment)`.
- Use defensive reads for optional COM-backed properties.
- Represent unreadable fields as structured unavailable data instead of throwing when a partial result is useful.
For object-reference assignment, prefer resolving named GRAccess objects first and only fall back to string assignment when the API allows it.
## Output Rules
Legacy behavior is part of the contract.
- Do not change existing `--json` shapes for existing commands.
- `--llm-json` must use the stable envelope:
```json
{
"success": true,
"command": "object get",
"galaxy": "ZB",
"target": "TestMachine",
"data": {},
"commandResult": null,
"warnings": [],
"unavailable": []
}
```
- Failure envelopes must include `success: false`, `error`, `exitCode`, and any useful GRAccess command result details.
- Normal log lines must not pollute `--llm-json` stdout.
- Include unavailable fields in `unavailable` when GRAccess lacks direct support or the local COM layer throws.
- Include `CommandResult` / `CommandResults` details for mutations.
## Validation And Dry Run
Validation should catch request-shape and safety issues before COM mutation.
- Add required args and confirmation rules to `CommandCapabilityRegistry`.
- Ensure `graccess validate --request plan.json --llm-json` catches missing args, unknown commands, and mismatched confirmation targets.
- Ensure `graccess batch --mode validate --llm-json` validates every step.
- Ensure `--dry-run` validates args, routing, and confirmation without calling mutating GRAccess methods.
If a true GRAccess dry-run is not available, document that dry-run is input and routing validation only.
## Tests
Add focused tests for the changed behavior.
Minimum expected coverage:
- Capability registry entry for the new command.
- Command parsing for the public CLI shape.
- Dispatcher request mapping from CliFx wrapper to command/subcommand/args.
- Confirmation guard behavior for normal, dry-run, and batch paths when the command mutates.
- Legacy `--json` compatibility if an existing command changed.
- `--llm-json` success and failure envelope shape when LLM output is supported.
- Structured IPC round-trip if the command uses arrays, condition lists, booleans, enums, or object batches.
- Fake or mocked handler tests for session route and one-shot route equivalence when practical.
Live validation against `ZB` should be read-only unless the test creates an explicitly named disposable object and removes it afterward. Never run wildcard bulk mutation live.
## Documentation Updates
Update docs in the same change as the feature.
Required updates:
- `docs/usage.md` for every user-facing command, option, output mode, and example.
- `docs/llm-integration.md` for every LLM-facing command, envelope field, batch behavior, validation behavior, or safety rule.
- `docs/README.md` if a new documentation page is added.
- `AGENTS.md` and `CLAUDE.md` when agent-critical workflows, constraints, or references change.
Domain-specific updates:
- Parsing commands: update `template-parsing.md`, `attribute-parsing.md`, or `script-parsing.md`.
- Editing commands: update `template-editing.md`, `template-instance-editing.md`, `attribute-editing.md`, or `script-editing.md`.
- IDE intent wrappers: update `template-instance-editing.md` and `llm-integration.md`.
- Safety changes: update all docs that include mutation examples.
Documentation should include:
- Command syntax.
- Required flags.
- Confirmation requirements.
- Session behavior.
- `--llm-json` example when useful.
- Any known GRAccess limitation or unavailable field behavior.
## Implementation Checklist
Use this order for most features:
1. Find the GRAccess API operation and confirm signatures.
2. Design the CLI command and safety rule.
3. Add or update capability registry metadata.
4. Add the CliFx wrapper.
5. Add the dispatcher handler.
6. Preserve legacy output and add `--llm-json` if machine-facing.
7. Add validation, dry-run, and batch support where applicable.
8. Add tests.
9. Update docs.
10. Run build and tests.
11. Run read-only smoke validation against `ZB` when the feature can be safely verified.
## Common Pitfalls
- Loading GRAccess from x64 or non-STA code.
- Letting `GRAccessApp` go out of scope while derived objects are still in use.
- Changing legacy `--json` output while adding `--llm-json`.
- Returning success for unsupported COM behavior instead of structured unavailable details.
- Adding a command wrapper but forgetting `CommandCapabilityRegistry`.
- Adding a dispatcher handler that only works in one-shot mode.
- Running mutating live validation against real objects or wildcard selections.
- Assuming GRAccess collections are zero-based.
- Forgetting to update `docs/usage.md`.
+270
View File
@@ -0,0 +1,270 @@
# Editing Template Attributes
This guide describes how to edit template attributes for an existing GRAccess template such as `TestMachine`.
Read `attribute-parsing.md` first so you know the exact attribute names and current settings before changing anything.
Run commands from `graccess_cli`. Examples assume an active session:
```powershell
graccess session start --galaxy ZB --node .
```
Without a session, add `--node .` to each command.
For LLM-driven edits, use `--llm-json` and validate first:
```powershell
graccess object snapshot --galaxy ZB --name TestMachine --type template --llm-json
graccess object attribute value set --galaxy ZB --name TestMachine --type template --attribute Description --value Updated --data-type string --confirm --confirm-target TestMachine --dry-run --llm-json
```
## Edit Flow
Wrap attribute edits in the standard GRAccess object edit flow:
```powershell
graccess object checkout --galaxy ZB --name TestMachine --type template --confirm --confirm-target TestMachine
# Attribute mutation commands.
graccess object save --galaxy ZB --name TestMachine --type template --confirm --confirm-target TestMachine
graccess object checkin --galaxy ZB --name TestMachine --type template --comment 'Edit template attributes' --confirm --confirm-target TestMachine
```
If any mutation fails, do not continue with more changes until the failure is understood. Use `object undo-checkout` when the edit should be discarded.
## Confirm The Target Attribute
Before editing a specific attribute:
```powershell
graccess object attribute get --galaxy ZB --name TestMachine --type template --attribute Description --json
```
For setting-family edits, find candidate names first:
```powershell
graccess object attributes --galaxy ZB --name TestMachine --type template --json
graccess object attributes --galaxy ZB --name TestMachine --type template --configurable --json
graccess object extended-attributes --galaxy ZB --name TestMachine --type template --json
```
## Set Attribute Values
Use `object attribute set`:
```powershell
graccess object attribute set --galaxy ZB --name TestMachine --type template --attribute Description --value 'Updated description' --data-type string --confirm --confirm-target TestMachine
```
Supported `--data-type` values:
| Data type | Conversion |
|---|---|
| `string` | `MxValue.PutString(...)` |
| `bool`, `boolean`, `mxboolean` | `MxValue.PutBoolean(...)` |
| `int`, `integer`, `mxinteger` | `MxValue.PutInteger(...)` |
| `float`, `mxfloat` | `MxValue.PutFloat(...)` |
| `double`, `mxdouble` | `MxValue.PutDouble(...)` |
Examples:
```powershell
graccess object attribute set --galaxy ZB --name TestMachine --type template --attribute EnableFeature --value true --data-type bool --confirm --confirm-target TestMachine
graccess object attribute set --galaxy ZB --name TestMachine --type template --attribute ScanPeriod --value 1000 --data-type int --confirm --confirm-target TestMachine
graccess object attribute set --galaxy ZB --name TestMachine --type template --attribute Gain --value 1.25 --data-type double --confirm --confirm-target TestMachine
```
The current CLI does not support array values, time values, references, or complex `MxValue` payloads. Use export/import or extend `CreateMxValue(...)` when those are needed.
## Lock Or Unlock Attributes
Use `object attribute lock`:
```powershell
graccess object attribute lock --galaxy ZB --name TestMachine --type template --attribute Description --locked MxPropertyUnlocked --confirm --confirm-target TestMachine
```
The `--locked` value must match a local `MxPropertyLockedEnum` member. The enum parser is case-insensitive and also accepts values with or without a `galaxy_` prefix when such a prefix exists.
## Change Attribute Security
Use `object attribute security`:
```powershell
graccess object attribute security --galaxy ZB --name TestMachine --type template --attribute Description --security MxSecurityOperate --confirm --confirm-target TestMachine
```
The `--security` value must match a local `MxSecurityClassification` member.
## Change Attribute Buffer Flag
Use `object attribute buffer`:
```powershell
graccess object attribute buffer --galaxy ZB --name TestMachine --type template --attribute Description --has-buffer --confirm --confirm-target TestMachine
```
To clear the flag, omit `--has-buffer`:
```powershell
graccess object attribute buffer --galaxy ZB --name TestMachine --type template --attribute Description --confirm --confirm-target TestMachine
```
## Add A UDA
Use `object uda add`:
```powershell
graccess object uda add --galaxy ZB --name TestMachine --type template --uda CustomCode --data-type MxString --category MxCategoryWriteable_USC --security MxSecurityUndefined --confirm --confirm-target TestMachine
```
Array UDA:
```powershell
graccess object uda add --galaxy ZB --name TestMachine --type template --uda CustomArray --data-type MxString --category MxCategoryWriteable_USC --security MxSecurityUndefined --is-array --array-count 10 --confirm --confirm-target TestMachine
```
## Rename, Update, Or Delete A UDA
Rename:
```powershell
graccess object uda rename --galaxy ZB --name TestMachine --type template --uda CustomCode --new-name CustomCode2 --confirm --confirm-target TestMachine
```
Update metadata:
```powershell
graccess object uda update --galaxy ZB --name TestMachine --type template --uda CustomCode2 --data-type MxString --category MxCategoryWriteable_USC --security MxSecurityUndefined --confirm --confirm-target TestMachine
```
Delete:
```powershell
graccess object uda delete --galaxy ZB --name TestMachine --type template --uda CustomCode2 --confirm --confirm-target TestMachine
```
## Edit Extensions
Use extension commands only when you know the supported extension type and primitive name for the template.
```powershell
graccess object extension add --galaxy ZB --name TestMachine --type template --extension-type ScriptExtension --primitive OnScan --object-extension --confirm --confirm-target TestMachine
```
```powershell
graccess object extension rename --galaxy ZB --name TestMachine --type template --extension-type ScriptExtension --primitive OnScan --new-name OnScan2 --object-extension --confirm --confirm-target TestMachine
```
```powershell
graccess object extension delete --galaxy ZB --name TestMachine --type template --extension-type ScriptExtension --primitive OnScan2 --object-extension --confirm --confirm-target TestMachine
```
## Edit History Settings
History settings are usually stored as attributes or extension attributes. First identify candidate names:
```powershell
$attrs = graccess object attributes --galaxy ZB --name TestMachine --type template --json | ConvertFrom-Json
$extended = graccess object extended-attributes --galaxy ZB --name TestMachine --type template --json | ConvertFrom-Json
$history = (@($attrs) + @($extended)) | Where-Object {
$_.Name -match '(?i)hist|history|historian|storage|trend'
} | Sort-Object Name -Unique
$history | Select-Object Name, DataType, Category, Locked
```
Then edit a known setting with `object attribute set`, lock, security, or buffer commands. Example:
```powershell
graccess object attribute set --galaxy ZB --name TestMachine --type template --attribute HistoryEnabled --value true --data-type bool --confirm --confirm-target TestMachine
```
If the setting value is not a simple string/bool/int/float/double, use export/import or extend CLI value serialization first.
## Edit I/O Settings
I/O settings may be attributes, object-reference assignments, or extension payloads.
Find likely attributes:
```powershell
$io = (@($attrs) + @($extended)) | Where-Object {
$_.Name -match '(?i)\bio\b|input|output|source|destination|scan|topic|device|address|reference'
} | Sort-Object Name -Unique
$io | Select-Object Name, DataType, Category, RuntimeSetHandler, ConfigSetHandler
```
Edit simple attribute-backed settings:
```powershell
graccess object attribute set --galaxy ZB --name TestMachine --type template --attribute ScanPeriod --value 1000 --data-type int --confirm --confirm-target TestMachine
```
Edit object-level assignment properties with `object set` when applicable:
```powershell
graccess object set --galaxy ZB --name TestMachine --type template --property host --value AppEngine_001 --confirm --confirm-target TestMachine
```
If the COM setter requires an object reference instead of a string, extend the CLI to resolve the named object and assign that object.
## Edit Alarm Settings
Alarm settings are commonly attribute-backed.
Find likely alarm attributes:
```powershell
$alarm = (@($attrs) + @($extended)) | Where-Object {
$_.Name -match '(?i)alarm|alert|limit|priority|severity|deadband|deviation|rate|roc|hihi|lolo|(^|[._])hi($|[._])|(^|[._])lo($|[._])|ack'
} | Sort-Object Name -Unique
$alarm | Select-Object Name, DataType, Category, SecurityClassification, Locked
```
Edit simple value-backed settings:
```powershell
graccess object attribute set --galaxy ZB --name TestMachine --type template --attribute AlarmPriority --value 500 --data-type int --confirm --confirm-target TestMachine
```
Security and lock changes use the same attribute commands:
```powershell
graccess object attribute security --galaxy ZB --name TestMachine --type template --attribute AlarmPriority --security MxSecurityOperate --confirm --confirm-target TestMachine
graccess object attribute lock --galaxy ZB --name TestMachine --type template --attribute AlarmPriority --locked MxPropertyLocked --confirm --confirm-target TestMachine
```
## Verify Attribute Edits
After checkin, re-read the edited metadata:
```powershell
graccess object attribute get --galaxy ZB --name TestMachine --type template --attribute Description --json
graccess object attributes --galaxy ZB --name TestMachine --type template --configurable --json
```
The current CLI does not read back attribute values, so value verification requires one of:
| Need | Path |
|---|---|
| Confirm metadata changed | `object attribute get` or `object attributes` |
| Confirm simple value changed | Add value readback support |
| Confirm full vendor configuration | Export object and compare package content with vendor tooling |
## Recommended CLI Extension For Full Attribute Editing
To fully support history, I/O, alarm, and script-related settings as first-class CLI operations, add:
```text
graccess object attribute value get --galaxy ZB --name TestMachine --type template --attribute AttrName --json
graccess object attribute value set --galaxy ZB --name TestMachine --type template --attribute AttrName --value-json file.json --confirm --confirm-target TestMachine
```
The implementation should serialize `IAttribute.Value` defensively and support complex `MxValue` shapes beyond the current scalar `string`, `bool`, `int`, `float`, and `double` conversions.
+249
View File
@@ -0,0 +1,249 @@
# Parsing Template Attributes
This guide describes how to parse attributes for an existing GRAccess template such as `TestMachine`, including the settings families normally needed for template analysis: object properties, attribute metadata, history settings, I/O settings, alarm settings, and extension/UDA settings.
Run commands from `graccess_cli`. Examples assume an active session:
```powershell
graccess session start --galaxy ZB --node .
```
Without a session, add `--node .` to each command.
For LLM workflows, start with the stable snapshot envelope:
```powershell
graccess object snapshot --galaxy ZB --name TestMachine --type template --llm-json
```
Then use `object attribute value get --llm-json` for individual scalar value readback. The value command tries direct `IAttribute.Value` first and then the read-only package fallback.
## Attribute Sources
Use all three read paths when building a complete parse:
| Source | Command | Purpose |
|---|---|---|
| Object identity | `object get` | Template tagname, contained name, hierarchy, checkout status |
| All attributes | `object attributes` | Every attribute visible through `IgObject.Attributes` |
| Configurable attributes | `object attributes --configurable` | Attributes visible through `IgObject.ConfigurableAttributes` |
| Extended attributes | `object extended-attributes` | Inherited or hierarchy-expanded attributes from `GetExtendedAttributes` |
| One attribute | `object attribute get` | Metadata for a known attribute |
| One attribute value | `object attribute value get` | Scalar value readback through direct GRAccess or package fallback where supported |
Base commands:
```powershell
graccess object get --galaxy ZB --name TestMachine --type template --json
graccess object attributes --galaxy ZB --name TestMachine --type template --json
graccess object attributes --galaxy ZB --name TestMachine --type template --configurable --json
graccess object extended-attributes --galaxy ZB --name TestMachine --type template --json
```
## Current CLI Coverage
The current CLI exposes the stable `IAttribute` metadata listed below.
| JSON field | GRAccess API member | Notes |
|---|---|---|
| `Name` | `IAttribute.Name` | Attribute name |
| `DataType` | `IAttribute.DataType` | `MxDataType` value |
| `Category` | `IAttribute.AttributeCategory` | Attribute category |
| `SecurityClassification` | `IAttribute.SecurityClassification` | Security classification |
| `Locked` | `IAttribute.Locked` | Lock state |
| `UpperBoundDim1` | `IAttribute.UpperBoundDim1` | First array dimension upper bound |
| `HasBuffer` | `IAttribute.HasBuffer` | Attribute buffer flag |
| `RuntimeSetHandler` | `IAttribute.RtSethandler` | Runtime set handler flag |
| `ConfigSetHandler` | `IAttribute.CfgSethandler` | Configuration set handler flag |
The GRAccess API also exposes `IAttribute.Value`. The CLI serializes scalar values when the local COM object exposes a supported accessor, and falls back to a temporary exported package when direct readback is unavailable. Because many history, I/O, alarm, and script-related settings are stored as attribute values, strict value-level parsing should use these paths:
| Need | Best current path |
|---|---|
| Inventory every setting name and metadata | `object attributes`, `--configurable`, and `object extended-attributes` |
| Read a single setting metadata record | `object attribute get` |
| Read one scalar value | `object attribute value get --llm-json` |
| Preserve full vendor configuration, including values and scripts | `object snapshot --llm-json` package fallback, `objects export`, or `galaxy export-all` |
| Arrays or complex values | Treat as unavailable unless the snapshot/package parser emits a safe scalar representation |
Normal CLI parsing must not query the Galaxy SQL database for attribute values. SQL can be used only in development verification scripts to compare GRAccess/package output against repository facts.
## Parse All Attributes
Capture all attribute lists before classifying them:
```powershell
$name = 'TestMachine'
$out = '.\template-snapshots'
New-Item -ItemType Directory -Force -Path $out | Out-Null
graccess object attributes --galaxy ZB --name $name --type template --json `
| Set-Content "$out\$name.attributes.json"
graccess object attributes --galaxy ZB --name $name --type template --configurable --json `
| Set-Content "$out\$name.configurable-attributes.json"
graccess object extended-attributes --galaxy ZB --name $name --type template --json `
| Set-Content "$out\$name.extended-attributes.json"
```
Load the snapshot in PowerShell:
```powershell
$attrs = Get-Content '.\template-snapshots\TestMachine.attributes.json' -Raw | ConvertFrom-Json
$configurable = Get-Content '.\template-snapshots\TestMachine.configurable-attributes.json' -Raw | ConvertFrom-Json
$extended = Get-Content '.\template-snapshots\TestMachine.extended-attributes.json' -Raw | ConvertFrom-Json
```
Merge and de-duplicate by attribute name:
```powershell
$all = @($attrs) + @($configurable) + @($extended)
$byName = $all | Sort-Object Name -Unique
$byName | Select-Object Name, DataType, Category, Locked, HasBuffer
```
## Parse Object Properties
Object-level properties are not the same as attributes. The current `object get` output gives identity and checkout information:
```powershell
graccess object get --galaxy ZB --name TestMachine --type template --json
```
`graccess_operations.md` lists additional GRAccess object properties that exist on `ITemplate` and `IInstance`, including `DerivedFrom`, `BasedOn`, `Category`, `CategoryGUID`, `Container`, `Area`, `Host`, `Toolset`, `ConfigVersion`, `CheckedOutBy`, `EditStatus`, `ValidationStatus`, `Errors`, and `Warnings`.
The current CLI does not emit every one of those properties in `object get`. If a parse requires all object properties, extend `GRAccessCommandDispatcher.ObjectDetails(...)` to read them defensively with the same `Try(...)` pattern used for current COM-backed fields.
## Parse History Settings
History settings are commonly represented as attributes or extended attributes. To parse them:
1. Capture all attributes and extended attributes.
2. Find setting names related to history or historian configuration.
3. Preserve metadata for each matching attribute.
4. Use object export or a future value-emitting CLI command when actual setting values are required.
PowerShell classification:
```powershell
$history = $byName | Where-Object {
$_.Name -match '(?i)hist|history|historian|storage|trend'
}
$history | Sort-Object Name | Select-Object Name, DataType, Category, Locked, HasBuffer
```
This is intentionally name-driven because exact attribute names vary by template type, System Platform version, primitive, and extension package.
## Parse I/O Settings
I/O settings can appear as object-level assignment properties, attributes, extension attributes, or exported package content. Parse them in this order:
1. `object get` for identity and hierarchy.
2. `object attributes` and `object extended-attributes` for setting names and metadata.
3. Query relationships such as host, containment, and area when relevant.
4. Use export for full value-level source/destination details.
PowerShell classification:
```powershell
$io = $byName | Where-Object {
$_.Name -match '(?i)\bio\b|input|output|source|destination|scan|topic|device|address|reference'
}
$io | Sort-Object Name | Select-Object Name, DataType, Category, Locked, RuntimeSetHandler, ConfigSetHandler
```
Relationship queries that often help with I/O context:
```powershell
graccess object query-condition --galaxy ZB --type all --condition containedBy --value TestMachine --json
graccess object query-condition --galaxy ZB --type all --condition assignedTo --value TestMachine --json
graccess object query-condition --galaxy ZB --type all --condition hostEngineIs --value TestMachine --json
```
## Parse Alarm Settings
Alarm settings are usually represented as attributes and extension attributes. Capture all attribute sources, then classify by alarm-related names:
```powershell
$alarm = $byName | Where-Object {
$_.Name -match '(?i)alarm|alert|limit|priority|severity|deadband|deviation|rate|roc|hihi|lolo|(^|[._])hi($|[._])|(^|[._])lo($|[._])|ack'
}
$alarm | Sort-Object Name | Select-Object Name, DataType, Category, SecurityClassification, Locked
```
For value-level alarm settings, use `objects export` until the CLI emits `IAttribute.Value`.
## Parse UDAs And Extensions
User-defined attributes are included in the attribute collections. Extension primitives normally show up through extended attributes, object export, or extension-specific attributes.
Recommended read-only capture:
```powershell
graccess object attributes --galaxy ZB --name TestMachine --type template --json
graccess object extended-attributes --galaxy ZB --name TestMachine --type template --json
```
The CLI currently has mutation commands for UDA and extension management, but it does not have a dedicated read-only `object uda list` or `object extension list` command. For complete extension parsing, prefer export or add dedicated list commands that call the GRAccess object/extension APIs if available in the local COM assembly.
## Full Attribute Snapshot Script
This script creates a structured local parse folder and classifies likely setting families:
```powershell
$galaxy = 'ZB'
$name = 'TestMachine'
$type = 'template'
$out = ".\template-snapshots\$name"
New-Item -ItemType Directory -Force -Path $out | Out-Null
graccess object get --galaxy $galaxy --name $name --type $type --json `
| Set-Content "$out\object.json"
graccess object attributes --galaxy $galaxy --name $name --type $type --json `
| Set-Content "$out\attributes.json"
graccess object attributes --galaxy $galaxy --name $name --type $type --configurable --json `
| Set-Content "$out\configurable-attributes.json"
graccess object extended-attributes --galaxy $galaxy --name $name --type $type --json `
| Set-Content "$out\extended-attributes.json"
$all = @(
Get-Content "$out\attributes.json" -Raw | ConvertFrom-Json
) + @(
Get-Content "$out\configurable-attributes.json" -Raw | ConvertFrom-Json
) + @(
Get-Content "$out\extended-attributes.json" -Raw | ConvertFrom-Json
)
$byName = $all | Sort-Object Name -Unique
$byName | ConvertTo-Json -Depth 5 | Set-Content "$out\all-attributes.unique.json"
$groups = [ordered]@{
history = @($byName | Where-Object { $_.Name -match '(?i)hist|history|historian|storage|trend' })
io = @($byName | Where-Object { $_.Name -match '(?i)\bio\b|input|output|source|destination|scan|topic|device|address|reference' })
alarm = @($byName | Where-Object { $_.Name -match '(?i)alarm|alert|limit|priority|severity|deadband|deviation|rate|roc|hihi|lolo|(^|[._])hi($|[._])|(^|[._])lo($|[._])|ack' })
script = @($byName | Where-Object { $_.Name -match '(?i)script|execute|trigger|expression|declaration|startup|shutdown' })
}
$groups | ConvertTo-Json -Depth 8 | Set-Content "$out\attribute-groups.json"
```
## CLI Gap For Complete Attribute Values
To make `graccess_cli` parse all attribute settings without relying on export files, add a read-only command or extend `object attributes` to include safe value serialization:
| Proposed field | Source |
|---|---|
| `Value` | `IAttribute.Value` converted from `IMxValue` |
| `IsArray` | Derived from bounds and value shape |
| `Dimensions` | `UpperBoundDim1` and any additional dimensions exposed by local API |
| `CommandResult` | `IAttribute.CommandResult` after value reads where relevant |
| `UnavailableReason` | Exception message when a COM-backed field cannot be read |
Keep value reads defensive. Some attributes may throw because storage is unavailable, the value is runtime-only, the datatype is not serializable, or the current login cannot read the field.
+245
View File
@@ -0,0 +1,245 @@
# CliFx Reference (v2.3.6)
Local reference for the CliFx CLI framework. Source: https://github.com/Tyrrrz/CliFx
## Setup
```csharp
using CliFx;
public static class Program
{
public static async Task<int> Main() =>
await new CliApplicationBuilder()
.AddCommandsFromThisAssembly()
.Build()
.RunAsync();
}
```
**Important:** `Main()` must return the integer exit code from `RunAsync()`.
`RunAsync()` resolves args from `Environment.GetCommandLineArgs()` and env vars from `Environment.GetEnvironmentVariables()`. You can also provide them manually via overloads.
## Defining Commands
Commands implement `ICommand` and are decorated with `[Command]`:
```csharp
using CliFx;
using CliFx.Attributes;
[Command(Description = "Calculates the logarithm of a value.")]
public class LogCommand : ICommand
{
[CommandParameter(0, Description = "Value whose logarithm is to be found.")]
public required double Value { get; init; }
[CommandOption("base", 'b', Description = "Logarithm base.")]
public double Base { get; init; } = 10;
public ValueTask ExecuteAsync(IConsole console)
{
var result = Math.Log(Value, Base);
console.Output.WriteLine(result);
return default;
}
}
```
Use `IConsole` (not `System.Console`) for all console I/O — this enables testability.
## Parameters vs Options
| Aspect | `[CommandParameter]` | `[CommandOption]` |
|---|---|---|
| Binding | By position order | By name (`--foo`) or short name (`-f`) |
| Required by default | Yes | No |
| Make optional | Omit `required` (last param only) | Omit `required` |
| Make required | Use `required` keyword | Use `required` keyword |
| Non-scalar (collections) | Only last parameter | Any option |
| Env var fallback | No | Yes (`EnvironmentVariable = "ENV_FOO"`) |
```csharp
// Required option
[CommandOption("foo")]
public required string Foo { get; init; }
// Optional parameter (must be last)
[CommandParameter(0)]
public string? OptionalParam { get; init; }
// Option with env var fallback
[CommandOption("foo", EnvironmentVariable = "ENV_FOO")]
public required string FooWithFallback { get; init; }
// Non-scalar option
[CommandOption("items")]
public required IReadOnlyList<string> Items { get; init; }
```
## Argument Syntax (POSIX-style)
```
myapp [...directives] [command] [...parameters] [...options]
```
- `--foo bar` → option "foo" = "bar"
- `-f bar` → option 'f' = "bar"
- `--switch` → option "switch" (no value / boolean)
- `-abc` → options 'a', 'b', 'c' (no value)
- `-i file1.txt file2.txt` → option 'i' = ["file1.txt", "file2.txt"]
- `-i file1.txt -i file2.txt` → same as above
## Value Conversion
Supports out of the box:
- **Primitives**: `int`, `bool`, `double`, `ulong`, `char`, etc.
- **Date/time**: `DateTime`, `DateTimeOffset`, `TimeSpan`
- **Enums**: by name or numeric value
- **String-initializable**: types with `ctor(string)` or static `Parse(string)` — e.g. `FileInfo`, `Guid`, `BigInteger`
- **Nullable** versions of all above
- **Collections**: `T[]`, `IReadOnlyList<T>`, `List<T>`, `HashSet<T>`, etc.
### Custom Converter
```csharp
public class VectorConverter : BindingConverter<Vector2>
{
public override Vector2 Convert(string? rawValue)
{
if (string.IsNullOrWhiteSpace(rawValue))
return default;
var components = rawValue.Split('x', 'X', ';');
var x = int.Parse(components[0], CultureInfo.InvariantCulture);
var y = int.Parse(components[1], CultureInfo.InvariantCulture);
return new Vector2(x, y);
}
}
[CommandParameter(0, Converter = typeof(VectorConverter))]
public required Vector2 Point { get; init; }
```
## Multiple / Nested Commands
Give each command a unique name. Common name segments create hierarchy:
```csharp
[Command] // Default (unnamed) command
public class DefaultCommand : ICommand { ... }
[Command("cmd1")] // Child of default
public class FirstCommand : ICommand { ... }
[Command("cmd1 sub")] // Child of cmd1
public class SubCommand : ICommand { ... }
```
Usage: `myapp cmd1 sub arg1 --opt value`
Default command is optional. If absent, running without a command shows root help text.
## Error Reporting
Use `CommandException` to report errors with specific exit codes:
```csharp
throw new CommandException("Division by zero is not supported.", 133);
```
Exit codes should be 1255 (8-bit unsigned) to avoid overflow on Unix.
## Graceful Cancellation
```csharp
public async ValueTask ExecuteAsync(IConsole console)
{
var cancellation = console.RegisterCancellationHandler();
await DoSomethingAsync(cancellation);
}
```
First interrupt signal triggers the token. Second interrupt force-kills.
## Dependency Injection
Use `UseTypeActivator(...)` with `Microsoft.Extensions.DependencyInjection`:
```csharp
await new CliApplicationBuilder()
.AddCommandsFromThisAssembly()
.UseTypeActivator(commandTypes =>
{
var services = new ServiceCollection();
services.AddSingleton<MyService>();
foreach (var commandType in commandTypes)
services.AddTransient(commandType);
return services.BuildServiceProvider();
})
.Build()
.RunAsync();
```
## Testing
Use `FakeInMemoryConsole` to test commands in isolation:
### Command-level test
```csharp
using var console = new FakeInMemoryConsole();
var command = new ConcatCommand { Left = "foo", Right = "bar" };
await command.ExecuteAsync(console);
var stdOut = console.ReadOutputString();
Assert.Equal("foo bar", stdOut);
```
### Application-level test
```csharp
using var console = new FakeInMemoryConsole();
var app = new CliApplicationBuilder()
.AddCommand<ConcatCommand>()
.UseConsole(console)
.Build();
var args = new[] { "--left", "foo", "--right", "bar" };
var envVars = new Dictionary<string, string>();
await app.RunAsync(args, envVars);
var stdOut = console.ReadOutputString();
Assert.Equal("foo bar", stdOut);
```
## Debug & Preview Directives
```bash
# Suspend until debugger attaches
myapp [debug] cmd -o
# Print parsed args without executing
myapp [preview] cmd arg1 -o foo
```
Disable in production:
```csharp
new CliApplicationBuilder()
.AllowDebugMode(false)
.AllowPreviewMode(false)
.Build();
```
## .NET Framework 4.8 Notes
- CliFx targets .NET Standard 2.0+, fully compatible with net48
- `required` keyword requires C# 11+ — on net48 (LangVersion 9.0), use properties with default values and validate in `ExecuteAsync` instead
- `init` setters require C# 9+ (supported on net48 with LangVersion 9.0)
- `async Task<int> Main` requires `System.Threading.Tasks` using directive on net48
+172
View File
@@ -0,0 +1,172 @@
# LLM Integration Guide
This document is the implemented machine-facing contract for using `graccess_cli` as an LLM automation layer for AVEVA/Wonderware System Platform IDE work through GRAccess.
The CLI still preserves legacy human output and existing `--json` shapes. LLMs should use `--llm-json` for a stable envelope.
## Stable Envelope
Galaxy-bound routed commands and LLM helper commands accept `--llm-json`.
```json
{
"success": true,
"command": "object snapshot",
"galaxy": "ZB",
"target": "TestMachine",
"data": {},
"commandResult": null,
"warnings": [],
"unavailable": [],
"error": null,
"exitCode": 0
}
```
Failures use the same envelope with `success=false`, `error`, and `exitCode`.
## Discovery
Use capabilities before generating plans:
```powershell
graccess capabilities --json
graccess capabilities --llm-json
```
The capability registry is code-backed, not scraped from prose. It returns command name, dispatcher command/subcommand, argument metadata, mutation status, session routing support, confirmation target rule, and output schema name.
## Read Workflow
Prefer session mode for repeated reads:
```powershell
graccess session start --galaxy ZB --node .
graccess object snapshot --galaxy ZB --name TestMachine --type template --llm-json
```
`object snapshot` includes object details, all attributes, configurable attributes, extended attributes, relationships, lineage, children, contained objects, package-backed attribute values/script bodies where available, and `Unavailable` entries for COM-backed sections that cannot be read safely.
Useful read commands:
```powershell
graccess object get --galaxy ZB --name TestMachine --type template --llm-json
graccess object lineage --galaxy ZB --name '$TestMachine' --type template --llm-json
graccess object children --galaxy ZB --name '$TestMachine' --type template --llm-json
graccess object attribute value get --galaxy ZB --name TestMachine --type template --attribute Description --llm-json
graccess object scripts list --galaxy ZB --name TestMachine --type template --llm-json
graccess object scripts get --galaxy ZB --name TestMachine --type template --script UpdateTestChangingInt --llm-json
```
The CLI tries direct GRAccess reads first. If direct GRAccess does not expose inheritance, attribute values, or script bodies, read commands may export the target object to a temporary `.aaPKG`, parse textual/package entries, recurse into nested package archives, parse binary UTF-16 script extension records, and delete the temp files. SQL Server repository reads are not part of normal CLI behavior and should only be used for development verification/debugging.
Script body access is adapter-dependent. For `ScriptExtension` objects, a bare script name maps to `.ExecuteText`; `object scripts set` writes the matching script body attribute through GRAccess. Mutating script and attribute commands prefer `ConfigurableAttributes[...]` before `Attributes[...]`, because script extension settings such as `TriggerPeriod`, `TriggerType`, `Expression`, and `ExecuteText` are configuration attributes in common GRAccess builds. If neither direct GRAccess nor the package fallback exposes body text, script read commands return structured unavailable details instead of pretending success.
Use the script settings wrapper for IDE-style script configuration:
```powershell
graccess object scripts settings set --galaxy ZB --name '$TestMachine' --type template --script UpdateTestChangingInt --trigger-period-ms 500 --lock-trigger-period --confirm --confirm-target '$TestMachine' --llm-json
```
Use the create wrapper for new object-level script extensions:
```powershell
graccess object scripts create --galaxy ZB --name '$MyTemplate' --type template --script OnScan --file OnScan.txt --trigger-type Periodic --trigger-period-ms 1000 --confirm --confirm-target '$MyTemplate' --llm-json
```
## Inheritance And Embedded Objects
For template families, model inheritance and containment explicitly. Do not rely on a single field being populated by every GRAccess version.
```powershell
graccess object snapshot --galaxy ZB --name '$TestMachine' --type template --llm-json
graccess object lineage --galaxy ZB --name '$TestMachine' --type template --llm-json
graccess object children --galaxy ZB --name '$TestMachine' --type template --llm-json
graccess object query-condition --galaxy ZB --type all --condition derivedOrInstantiatedFrom --value '$gMachine' --llm-json
graccess object query-condition --galaxy ZB --type all --condition basedOn --value '$gMachine' --llm-json
graccess object query-condition --galaxy ZB --type all --condition containedBy --value '$TestMachine' --llm-json
```
Read `data.Lineage`, `data.Children`, `data.ContainedObjects`, `data.Object.DerivedFrom`, `data.Object.BasedOn`, `ContainedName`, and `HierarchicalName` when available. The documented `ZB` `$TestMachine` family is an example: `$TestMachine` extends `$gMachine`, while `$TestMachine.DelmiaReceiver` and `$TestMachine.MESReceiver` are contained embedded templates.
Create derived templates and instances with contained objects when the family requires embedded children:
```powershell
graccess template derive --galaxy ZB --name '$gMachine' --type template --new-name '$MyMachine' --confirm --confirm-target '$gMachine' --llm-json
graccess template instantiate --galaxy ZB --name '$TestMachine' --type template --new-name TestMachine_021 --create-contained --confirm --confirm-target '$TestMachine' --llm-json
```
Edit contained child templates or instances by targeting their own object names, such as `$TestMachine.DelmiaReceiver` or `DelmiaReceiver_001`, and use normal checkout/save/checkin safety.
## Validation And Batch
Per-command dry-run validates command shape and confirmation without calling mutating GRAccess methods:
```powershell
graccess object attribute value set --galaxy ZB --name TestMachine --type template --attribute Description --value Updated --data-type string --confirm --confirm-target TestMachine --dry-run --llm-json
```
Plan validation:
```powershell
graccess validate --request plan.json --llm-json
```
Batch validation or execution:
```powershell
graccess batch --file plan.json --mode validate --llm-json
graccess batch --file plan.json --mode execute --llm-json
```
Plan shape:
```json
{
"Galaxy": "ZB",
"Node": ".",
"Commands": [
{
"Command": "object checkout",
"Args": {
"name": "TestMachine",
"type": "template",
"confirm": true,
"confirm-target": "TestMachine"
}
}
]
}
```
Every mutating step must include its own `confirm=true` and exact `confirm-target`. There is no global mutation confirmation. Batch execute stops on first failure.
## IDE Intent Wrappers
Use intent-level commands instead of generic property edits when possible:
```powershell
graccess area list --galaxy ZB --llm-json
graccess area create --galaxy ZB --template '$Area' --name Area_Test --confirm --confirm-target '$Area' --llm-json
graccess engine list --galaxy ZB --llm-json
graccess engine create --galaxy ZB --template '$AppEngine' --name AppEngine_Test --confirm --confirm-target '$AppEngine' --llm-json
graccess instance assign-area --galaxy ZB --name TestMachine_001 --area Area_Test --confirm --confirm-target TestMachine_001 --llm-json
graccess instance assign-engine --galaxy ZB --name TestMachine_001 --engine AppEngine_Test --confirm --confirm-target TestMachine_001 --llm-json
graccess instance assign-container --galaxy ZB --name TestMachine_001 --container ParentObject --confirm --confirm-target TestMachine_001 --llm-json
graccess io assign --galaxy ZB --name TestMachine_001 --attribute DeviceAddress --value D100 --confirm --confirm-target TestMachine_001 --llm-json
```
`object set --property area|host|container|toolset|security-group` attempts to resolve the named GRAccess object first, then falls back to raw string assignment for version-specific setters.
## Safety Rules
1. Start with `capabilities` and read-only snapshots.
2. Do not mutate without an exact object or file target.
3. Never use wildcard bulk mutation in production.
4. Use `--dry-run --llm-json` before mutation plans.
5. Snapshot before editing and re-read after editing.
6. Stop after the first failed mutation or `CommandResult.Successful=false`.
7. Treat missing or unavailable fields as unknown, not false.
8. Prefer test objects or derived templates before production targets.
9. Use session mode for repeated galaxy-bound commands.
10. Use deployment commands only when the user explicitly names targets.
+206
View File
@@ -0,0 +1,206 @@
# Editing Scripts And Script Libraries
This guide describes how to edit scripts related to a template such as `TestMachine`.
Read `script-parsing.md` first so you know whether the target script is a script library, an object-level script-like attribute, an extension primitive, or content that only appears in an exported object package.
Run commands from `graccess_cli`. Examples assume an active session:
```powershell
graccess session start --galaxy ZB --node .
```
Without a session, add `--node .` to each command.
For LLM-driven script work, read script metadata and validate guarded edits with the LLM envelope:
```powershell
graccess object scripts list --galaxy ZB --name TestMachine --type template --llm-json
graccess object scripts get --galaxy ZB --name TestMachine --type template --script UpdateTestChangingInt --llm-json
graccess object scripts set --galaxy ZB --name TestMachine --type template --script UpdateTestChangingInt --file .\UpdateTestChangingInt.txt --confirm --confirm-target TestMachine --dry-run --llm-json
```
## What The CLI Can Edit Today
| Area | Current command support |
|---|---|
| Script library inventory | `script-library list` |
| Script library export | `script-library export` |
| Script library import/add | `script-library import`, `script-library add`, `galaxy import-script-library` |
| Object script metadata | `object scripts list`, `object scripts get` |
| Object script body read/write | `object scripts get`, `object scripts set` for script body attributes such as `ExecuteText` |
| Script-like attributes | `object attribute set`, lock, security, buffer |
| Extension primitives | `object extension add/delete/rename` |
| Full object package script payloads | Use `objects export` and `galaxy import-objects` |
## Export Before Editing
Export script libraries before changing them:
```powershell
$out = '.\template-snapshots\script-libraries-before'
New-Item -ItemType Directory -Force -Path $out | Out-Null
$libs = graccess script-library list --galaxy ZB --json | ConvertFrom-Json
foreach ($lib in $libs) {
$name = $lib.Name
$path = Join-Path $out "$name.aaslib"
graccess script-library export --galaxy ZB --name $name --output $path --confirm --confirm-target $path
}
```
Export the template package when object-level scripts may be involved:
```powershell
$pkg = '.\template-snapshots\TestMachine-before\TestMachine.aaPKG'
graccess objects export --galaxy ZB --type template --name TestMachine --output $pkg --confirm --confirm-target $pkg
```
## Edit Script Libraries
Script library content is edited outside the CLI in the `.aaslib` file or source toolchain that creates it. Import the updated library after review:
```powershell
graccess script-library import --galaxy ZB --path '.\scripts\CommonScripts.aaslib' --confirm --confirm-target '.\scripts\CommonScripts.aaslib'
```
The galaxy-level import command is also available:
```powershell
graccess galaxy import-script-library --galaxy ZB --path '.\scripts\CommonScripts.aaslib' --confirm --confirm-target '.\scripts\CommonScripts.aaslib'
```
After import, list libraries again:
```powershell
graccess script-library list --galaxy ZB --json
```
## Edit Script-Like Attributes
Some templates store expressions, declarations, triggers, or script fragments in attributes. Find candidates:
```powershell
$attrs = graccess object attributes --galaxy ZB --name TestMachine --type template --json | ConvertFrom-Json
$extended = graccess object extended-attributes --galaxy ZB --name TestMachine --type template --json | ConvertFrom-Json
$scripts = (@($attrs) + @($extended)) | Where-Object {
$_.Name -match '(?i)script|execute|trigger|expression|declaration|startup|shutdown|scan'
} | Sort-Object Name -Unique
$scripts | Select-Object Name, DataType, Category, Locked
```
If the script-like setting is a scalar string attribute, edit it through the normal template edit flow. Script extension fields should be written through `ConfigurableAttributes`; the CLI does this automatically for `object scripts set`, `object scripts settings set`, and mutating `object attribute` commands. `object scripts set` is the convenience wrapper for script body attributes. A bare script name maps to `.ExecuteText`; explicit fields such as `.StartupText`, `.OnScanText`, or `.Expression` are preserved.
```powershell
graccess object checkout --galaxy ZB --name TestMachine --type template --confirm --confirm-target TestMachine
graccess object scripts set --galaxy ZB --name TestMachine --type template --script UpdateTestChangingInt --file .\UpdateTestChangingInt.txt --confirm --confirm-target TestMachine
graccess object save --galaxy ZB --name TestMachine --type template --confirm --confirm-target TestMachine
graccess object checkin --galaxy ZB --name TestMachine --type template --comment 'Update script text' --confirm --confirm-target TestMachine
```
Update periodic script settings and lock the interval for deployment inheritance:
```powershell
graccess object checkout --galaxy ZB --name '$TestMachine' --type template --confirm --confirm-target '$TestMachine'
graccess object scripts settings set --galaxy ZB --name '$TestMachine' --type template --script UpdateTestChangingInt --trigger-period-ms 500 --lock-trigger-period --confirm --confirm-target '$TestMachine'
graccess object save --galaxy ZB --name '$TestMachine' --type template --confirm --confirm-target '$TestMachine'
graccess object checkin --galaxy ZB --name '$TestMachine' --type template --comment 'Set UpdateTestChangingInt interval to 500ms' --confirm --confirm-target '$TestMachine'
```
Verify with package-backed readback:
```powershell
graccess object scripts get --galaxy ZB --name TestMachine --type template --script UpdateTestChangingInt --llm-json
```
## Edit Extension Primitives
When a script is represented by an extension primitive, use the extension commands for the primitive lifecycle:
```powershell
graccess object checkout --galaxy ZB --name TestMachine --type template --confirm --confirm-target TestMachine
graccess object extension add --galaxy ZB --name TestMachine --type template --extension-type ScriptExtension --primitive OnScan --object-extension --confirm --confirm-target TestMachine
graccess object save --galaxy ZB --name TestMachine --type template --confirm --confirm-target TestMachine
graccess object checkin --galaxy ZB --name TestMachine --type template --comment 'Add script extension primitive' --confirm --confirm-target TestMachine
```
Rename:
```powershell
graccess object extension rename --galaxy ZB --name TestMachine --type template --extension-type ScriptExtension --primitive OnScan --new-name OnScan2 --object-extension --confirm --confirm-target TestMachine
```
Delete:
```powershell
graccess object extension delete --galaxy ZB --name TestMachine --type template --extension-type ScriptExtension --primitive OnScan2 --object-extension --confirm --confirm-target TestMachine
```
These commands manage primitive structure. The IDE-oriented wrapper below creates the same `ScriptExtension` primitive and can initialize the body and common settings:
```powershell
graccess object checkout --galaxy ZB --name '$MyTemplate' --type template --confirm --confirm-target '$MyTemplate'
graccess object scripts create --galaxy ZB --name '$MyTemplate' --type template --script OnScan --file .\OnScan.txt --trigger-type Periodic --trigger-period-ms 1000 --confirm --confirm-target '$MyTemplate'
graccess object save --galaxy ZB --name '$MyTemplate' --type template --confirm --confirm-target '$MyTemplate'
graccess object checkin --galaxy ZB --name '$MyTemplate' --type template --comment 'Add OnScan script' --confirm --confirm-target '$MyTemplate'
```
After adding a `ScriptExtension` primitive, set its body with `object scripts set --script <primitiveName>` or `object scripts set --script <primitiveName>.ExecuteText`.
## Full-Fidelity Object Script Edits
When script bodies are only available inside the object package, use export/import:
1. Export the template package.
2. Edit with the supported vendor tooling or package workflow.
3. Import the updated package with explicit confirmation.
4. Re-parse and validate the template.
Export:
```powershell
$pkg = '.\template-work\TestMachine.aaPKG'
graccess objects export --galaxy ZB --type template --name TestMachine --output $pkg --confirm --confirm-target $pkg
```
Import:
```powershell
graccess galaxy import-objects --galaxy ZB --file '.\template-work\TestMachine.aaPKG' --overwrite --confirm --confirm-target '.\template-work\TestMachine.aaPKG'
```
For conflict-aware import:
```powershell
graccess galaxy import-objects-ex --galaxy ZB --file '.\template-work\TestMachine.aaPKG' --version-conflict '<E_RESOLVE_VERSION_CONFLICT_ACTION>' --name-conflict '<E_RESOLVE_NAME_CONFLICT_ACTION>' --confirm --confirm-target '.\template-work\TestMachine.aaPKG'
```
The exact enum values for import conflict handling must match the local GRAccess interop assembly. See `graccess_operations.md` and `graccess_documentation.md`.
## Validate Script Edits
After import or checkin:
```powershell
graccess object get --galaxy ZB --name TestMachine --type template --json
graccess object attributes --galaxy ZB --name TestMachine --type template --json
graccess object extended-attributes --galaxy ZB --name TestMachine --type template --json
```
For runtime validation, instantiate a test object and deploy only that explicitly named test instance:
```powershell
graccess template instantiate --galaxy ZB --name TestMachine --type template --new-name TestMachine_ScriptTest_001 --create-contained --confirm --confirm-target TestMachine
graccess instance deploy --galaxy ZB --name TestMachine_ScriptTest_001 --type instance --confirm --confirm-target TestMachine_ScriptTest_001
```
## Supported Object Script Command Pattern
```text
graccess object scripts list --galaxy ZB --name TestMachine --type template --json
graccess object scripts get --galaxy ZB --name TestMachine --type template --script UpdateTestChangingInt --llm-json
graccess object scripts set --galaxy ZB --name TestMachine --type template --script UpdateTestChangingInt --file .\UpdateTestChangingInt.txt --confirm --confirm-target TestMachine --llm-json
```
The local GRAccess examples sometimes show `Template.Scripts[index].ScriptString`. This repository's installed interop exposes the same practical content through script extension attributes such as `UpdateTestChangingInt.ExecuteText`; the CLI writes those attributes directly via `IAttribute.SetValue`.
+130
View File
@@ -0,0 +1,130 @@
# Parsing Scripts And Script Libraries
This guide describes how to capture scripts related to a template such as `TestMachine`.
GRAccess exposes script libraries directly through `IScriptLibraries` and `IScriptLibrary`. Object-level scripts are less direct: depending on object type and System Platform version, they may appear as attributes, extension attributes, or only inside exported object packages.
Run commands from `graccess_cli`. Examples assume an active session:
```powershell
graccess session start --galaxy ZB --node .
```
Without a session, add `--node .` to each command.
For LLM workflows, include object script metadata in the snapshot first:
```powershell
graccess object snapshot --galaxy ZB --name TestMachine --type template --llm-json
graccess object scripts list --galaxy ZB --name TestMachine --type template --llm-json
graccess object scripts get --galaxy ZB --name TestMachine --type template --script UpdateTestChangingInt.ExecuteText --llm-json
graccess object scripts get --galaxy ZB --name TestMachine --type template --script UpdateTestChangingInt --llm-json
```
Direct object script body readback is version-specific. The CLI tries direct GRAccess metadata first, then exports the target object to a temporary `.aaPKG`, recurses through nested package archives, and parses binary UTF-16 package records for `ScriptExtension` content. For script extension names, a bare script such as `UpdateTestChangingInt` resolves to `UpdateTestChangingInt.ExecuteText`. Explicit fields such as `UpdateTestChangingInt.StartupText` are preserved.
The exported package fallback can read script text fields such as `ExecuteText`, `DeclarationsText`, `StartupText`, `ShutdownText`, `OnScanText`, `OffScanText`, and `Expression` when present. When body access is unavailable from both direct GRAccess and package data, `object scripts get` reports that in structured output.
## Script Library Inventory
List script libraries:
```powershell
graccess script-library list --galaxy ZB --json
```
The current CLI returns library names. The underlying GRAccess API for `IScriptLibrary` exposes `Name` and `Export(path)`.
## Export Script Libraries
Export is the authoritative way to preserve script library content:
```powershell
$out = '.\template-snapshots\script-libraries'
New-Item -ItemType Directory -Force -Path $out | Out-Null
graccess script-library export --galaxy ZB --name CommonScripts --output "$out\CommonScripts.aaslib" --confirm --confirm-target "$out\CommonScripts.aaslib"
```
`script-library export` writes a local file, so the CLI requires confirmation and a matching `--confirm-target` equal to the output path.
To export every listed script library, parse the JSON list and call export for each name:
```powershell
$galaxy = 'ZB'
$out = '.\template-snapshots\script-libraries'
New-Item -ItemType Directory -Force -Path $out | Out-Null
$libs = graccess script-library list --galaxy $galaxy --json | ConvertFrom-Json
foreach ($lib in $libs) {
$name = $lib.Name
$path = Join-Path $out "$name.aaslib"
graccess script-library export --galaxy $galaxy --name $name --output $path --confirm --confirm-target $path
}
```
## Object-Level Script Discovery
For a template such as `TestMachine`, first inspect attributes and extended attributes for script-like names:
```powershell
graccess object attributes --galaxy ZB --name TestMachine --type template --json
graccess object extended-attributes --galaxy ZB --name TestMachine --type template --json
```
PowerShell classification:
```powershell
$attrs = Get-Content '.\template-snapshots\TestMachine.attributes.json' -Raw | ConvertFrom-Json
$extended = Get-Content '.\template-snapshots\TestMachine.extended-attributes.json' -Raw | ConvertFrom-Json
$all = (@($attrs) + @($extended)) | Sort-Object Name -Unique
$scripts = $all | Where-Object {
$_.Name -match '(?i)script|execute|trigger|expression|declaration|startup|shutdown|scan'
}
$scripts | Sort-Object Name | Select-Object Name, DataType, Category, Locked
```
This discovers script-related setting names and metadata. For bodies, prefer `object scripts get --llm-json` or `object snapshot --llm-json`; both can include package-backed script text when GRAccess export is available. Normal CLI script parsing must not query the Galaxy SQL database; SQL is development verification/debug-only.
## Export Template Objects For Script Bodies
Use object export when you need the full object configuration, including script bodies, declarations, triggers, and extension payloads that are not exposed as simple attribute metadata:
```powershell
$out = '.\template-snapshots\TestMachine'
New-Item -ItemType Directory -Force -Path $out | Out-Null
$pkg = Join-Path $out 'TestMachine.aaPKG'
graccess objects export --galaxy ZB --type template --name TestMachine --output $pkg --confirm --confirm-target $pkg
```
The exported package should be treated as the complete vendor-owned representation. Use it as the source of truth when CLI JSON and GRAccess attribute metadata do not expose script content.
For a galaxy-wide reference export:
```powershell
$pkg = '.\template-snapshots\ZB.export.aaPKG'
graccess galaxy export-all --galaxy ZB --output $pkg --json
```
`galaxy export-all` is read-only from the galaxy perspective but writes an output file.
## Suggested Parse Bundle
A practical template parse bundle should include:
| File | Command |
|---|---|
| `object.json` | `object get` |
| `attributes.json` | `object attributes` |
| `configurable-attributes.json` | `object attributes --configurable` |
| `extended-attributes.json` | `object extended-attributes` |
| `attribute-groups.json` | Local classifier from `attribute-parsing.md` |
| `template.aaPKG` | `objects export` |
| `script-libraries/*.aaslib` | `script-library export` |
## Notes On GRAccess Script Collections
Some GRAccess examples refer to `Template.Scripts[index].ScriptString`. The local installed GRAccess interop and docs used by this CLI expose `ScriptExtension` content through script-like attributes and exported package records instead. Use `object scripts list/get` as the supported discovery and readback contract for this repository.
+370
View File
@@ -0,0 +1,370 @@
# Editing Existing Templates
This guide describes how to edit an existing GRAccess template such as `TestMachine` with `graccess_cli`.
Use this document as the top-level workflow. For deeper coverage, also read:
- `attribute-editing.md` - attribute values, locks, security, buffers, UDAs, extensions, and settings families such as history, I/O, and alarms.
- `script-editing.md` - script-library import/export and the current options for object-level script edits.
- `template-instance-editing.md` - create and edit instances from templates, including areas, engines, and I/O assignment.
- `template-parsing.md` - read-only inspection workflow to run before and after edits.
Run commands from `graccess_cli`. Examples assume the CLI is available as `graccess`. During local development, replace `graccess` with:
```powershell
dotnet run --project src/ZB.MOM.WW.GRAccess.Cli/ZB.MOM.WW.GRAccess.Cli.csproj --
```
## Safety Model
Every mutating command requires `--confirm`. Template/object mutation commands also require `--confirm-target` matching the exact object name being edited. File import/export commands use the file path as the confirmation target.
For template edits, use this pattern:
```powershell
--confirm --confirm-target TestMachine
```
For LLM-driven edits, validate the final command or batch plan before mutation:
```powershell
graccess object attribute value set --galaxy ZB --name TestMachine --type template --attribute Description --value Updated --data-type string --confirm --confirm-target TestMachine --dry-run --llm-json
graccess batch --file plan.json --mode validate --llm-json
```
Do not edit production templates directly unless the target, backup, and rollback path are explicit. For risky work, derive or export a test copy first.
## Session Setup
Start a session for repeated edits:
```powershell
graccess session start --galaxy ZB --node .
```
With a session active, omit `--node` on logged-in commands:
```powershell
graccess object get --galaxy ZB --name TestMachine --type template --json
```
Use `object snapshot --llm-json` for before/after verification when an LLM is planning or executing the edit:
```powershell
graccess object snapshot --galaxy ZB --name TestMachine --type template --llm-json
```
Without a session, include `--node .` on each command.
## Pre-Edit Snapshot
Take a read-only snapshot before changing anything:
```powershell
$galaxy = 'ZB'
$name = 'TestMachine'
$out = ".\template-snapshots\$name-before"
New-Item -ItemType Directory -Force -Path $out | Out-Null
graccess object get --galaxy $galaxy --name $name --type template --json `
| Set-Content "$out\object.json"
graccess object attributes --galaxy $galaxy --name $name --type template --json `
| Set-Content "$out\attributes.json"
graccess object attributes --galaxy $galaxy --name $name --type template --configurable --json `
| Set-Content "$out\configurable-attributes.json"
graccess object extended-attributes --galaxy $galaxy --name $name --type template --json `
| Set-Content "$out\extended-attributes.json"
```
For a fuller snapshot, follow `template-parsing.md`, `attribute-parsing.md`, and `script-parsing.md`.
## Optional Backup Export
For a rollback artifact, export the template before editing:
```powershell
$pkg = '.\template-snapshots\TestMachine-before\TestMachine.aaPKG'
graccess objects export --galaxy ZB --type template --name TestMachine --output $pkg --confirm --confirm-target $pkg
```
`objects export` writes a file and therefore requires `--confirm-target` equal to the output path.
## Standard Edit Flow
GRAccess object changes should follow:
```text
CheckOut -> mutate -> Save -> CheckIn(comment)
```
With `graccess_cli`:
```powershell
graccess object checkout --galaxy ZB --name TestMachine --type template --confirm --confirm-target TestMachine
# Run one or more mutation commands here.
graccess object save --galaxy ZB --name TestMachine --type template --confirm --confirm-target TestMachine
graccess object checkin --galaxy ZB --name TestMachine --type template --comment 'Edited TestMachine' --confirm --confirm-target TestMachine
```
If you need to discard changes:
```powershell
graccess object undo-checkout --galaxy ZB --name TestMachine --type template --confirm --confirm-target TestMachine
```
`object unload` releases the object from the GRAccess cache:
```powershell
graccess object unload --galaxy ZB --name TestMachine --type template --confirm --confirm-target TestMachine
```
## Edit Object Properties
Use `object set` for supported object-level properties:
| Property | Command value | Notes |
|---|---|---|
| Tagname | `tagname` | Renames the object tagname |
| Contained name | `contained-name` | Changes contained name |
| Area | `area` | Object reference or accepted GRAccess value |
| Host | `host` | Object reference or accepted GRAccess value |
| Container | `container` | Object reference or accepted GRAccess value |
| Toolset | `toolset` | Toolset reference or accepted GRAccess value |
| Security group | `security-group` | Security group reference or accepted GRAccess value |
Example:
```powershell
graccess object checkout --galaxy ZB --name TestMachine --type template --confirm --confirm-target TestMachine
graccess object set --galaxy ZB --name TestMachine --type template --property contained-name --value TestMachineEdited --confirm --confirm-target TestMachine
graccess object save --galaxy ZB --name TestMachine --type template --confirm --confirm-target TestMachine
graccess object checkin --galaxy ZB --name TestMachine --type template --comment 'Update contained name' --confirm --confirm-target TestMachine
```
When renaming `tagname`, the command target is the current name. After a successful rename, use the new name for later commands:
```powershell
graccess object checkout --galaxy ZB --name TestMachine --type template --confirm --confirm-target TestMachine
graccess object set --galaxy ZB --name TestMachine --type template --property tagname --value TestMachineV2 --confirm --confirm-target TestMachine
graccess object save --galaxy ZB --name TestMachineV2 --type template --confirm --confirm-target TestMachineV2
graccess object checkin --galaxy ZB --name TestMachineV2 --type template --comment 'Rename template' --confirm --confirm-target TestMachineV2
```
Validate tagname renames in a test galaxy first. If the local GRAccess cache cannot find the object by the new name before `Save`, the CLI needs a combined rename/save/checkin command that keeps the same COM object reference for the full operation.
Object-reference properties such as area, host, container, toolset, and security group depend on what the local GRAccess COM property setter accepts. If a string value fails, extend the CLI to resolve the named target object first and assign the COM object instead of the raw string.
## Edit Attributes And Settings
Use `attribute-editing.md` for the full attribute workflow.
Common commands:
```powershell
graccess object attribute set --galaxy ZB --name TestMachine --type template --attribute Description --value 'Updated description' --data-type string --confirm --confirm-target TestMachine
graccess object attribute lock --galaxy ZB --name TestMachine --type template --attribute Description --locked MxPropertyUnlocked --confirm --confirm-target TestMachine
graccess object attribute security --galaxy ZB --name TestMachine --type template --attribute Description --security MxSecurityOperate --confirm --confirm-target TestMachine
graccess object attribute buffer --galaxy ZB --name TestMachine --type template --attribute Description --has-buffer --confirm --confirm-target TestMachine
```
The current CLI supports attribute value types `string`, `bool`, `int`, `float`, and `double`.
History, I/O, alarm, and script settings are usually represented as attributes, extended attributes, object package content, or extension-specific payloads. For value-level edits beyond the current `object attribute set` support, use export/import or add CLI support for safe `IAttribute.Value` parsing and serialization.
## Edit UDAs
UDAs are object mutations and should be wrapped in checkout/save/checkin.
Add a UDA:
```powershell
graccess object checkout --galaxy ZB --name TestMachine --type template --confirm --confirm-target TestMachine
graccess object uda add --galaxy ZB --name TestMachine --type template --uda CustomCode --data-type MxString --category MxCategoryWriteable_USC --security MxSecurityUndefined --confirm --confirm-target TestMachine
graccess object save --galaxy ZB --name TestMachine --type template --confirm --confirm-target TestMachine
graccess object checkin --galaxy ZB --name TestMachine --type template --comment 'Add CustomCode UDA' --confirm --confirm-target TestMachine
```
Rename a UDA:
```powershell
graccess object uda rename --galaxy ZB --name TestMachine --type template --uda CustomCode --new-name CustomCode2 --confirm --confirm-target TestMachine
```
Update UDA metadata:
```powershell
graccess object uda update --galaxy ZB --name TestMachine --type template --uda CustomCode2 --data-type MxString --category MxCategoryWriteable_USC --security MxSecurityUndefined --confirm --confirm-target TestMachine
```
Delete a UDA:
```powershell
graccess object uda delete --galaxy ZB --name TestMachine --type template --uda CustomCode2 --confirm --confirm-target TestMachine
```
## Edit Extensions
Extension primitives are object mutations and should be wrapped in checkout/save/checkin.
Add an extension primitive:
```powershell
graccess object extension add --galaxy ZB --name TestMachine --type template --extension-type ScriptExtension --primitive OnScan --object-extension --confirm --confirm-target TestMachine
```
Rename an extension primitive:
```powershell
graccess object extension rename --galaxy ZB --name TestMachine --type template --extension-type ScriptExtension --primitive OnScan --new-name OnScan2 --object-extension --confirm --confirm-target TestMachine
```
Delete an extension primitive:
```powershell
graccess object extension delete --galaxy ZB --name TestMachine --type template --extension-type ScriptExtension --primitive OnScan2 --object-extension --confirm --confirm-target TestMachine
```
Exact extension type and primitive names must match what the local System Platform installation and template support.
## Derive A Template
Derive a test template before editing a production template directly. This is also how you extend a base System Platform template, such as deriving `$TestMachine` from `$gMachine`.
```powershell
graccess template derive --galaxy ZB --name TestMachine --type template --new-name TestMachine_EditTest --create-contained --confirm --confirm-target TestMachine
```
Then edit `TestMachine_EditTest`:
```powershell
graccess object checkout --galaxy ZB --name TestMachine_EditTest --type template --confirm --confirm-target TestMachine_EditTest
```
To extend `$gMachine` with a new machine template:
```powershell
graccess template derive --galaxy ZB --name '$gMachine' --type template --new-name '$MyMachine' --confirm --confirm-target '$gMachine' --llm-json
graccess object snapshot --galaxy ZB --name '$MyMachine' --type template --llm-json
```
Use `--create-contained` only when the source template has contained templates/objects that should be copied into the derived template. For a family like `$TestMachine`, contained templates such as `$TestMachine.DelmiaReceiver` and `$TestMachine.MESReceiver` are part of the template design and should be verified after derivation:
```powershell
graccess template derive --galaxy ZB --name '$TestMachine' --type template --new-name '$MyMachine' --create-contained --confirm --confirm-target '$TestMachine' --llm-json
graccess template list --galaxy ZB --pattern '%MyMachine%' --llm-json
```
If the derived family does not include expected embedded templates, do not hand-edit production objects to compensate. Add or validate CLI support for contained template creation against a disposable test family first.
## Edit Embedded Template Objects
Contained templates are edited by targeting the contained template tagname. For the `TestMachine` family:
```powershell
graccess object snapshot --galaxy ZB --name '$TestMachine.DelmiaReceiver' --type template --llm-json
graccess object checkout --galaxy ZB --name '$TestMachine.DelmiaReceiver' --type template --confirm --confirm-target '$TestMachine.DelmiaReceiver'
graccess object attribute value set --galaxy ZB --name '$TestMachine.DelmiaReceiver' --type template --attribute DownloadPath --value 'C:\Recipes' --data-type string --confirm --confirm-target '$TestMachine.DelmiaReceiver' --llm-json
graccess object save --galaxy ZB --name '$TestMachine.DelmiaReceiver' --type template --confirm --confirm-target '$TestMachine.DelmiaReceiver'
graccess object checkin --galaxy ZB --name '$TestMachine.DelmiaReceiver' --type template --comment 'Update Delmia receiver download path' --confirm --confirm-target '$TestMachine.DelmiaReceiver'
```
Contained template edits flow to future instances and may affect derived/instantiated objects depending on lock and override state. Before editing an embedded template, snapshot representative existing child instances such as `DelmiaReceiver_001` and compare after checkin.
## Instantiate For Validation
Create a test instance from the edited template:
```powershell
graccess template instantiate --galaxy ZB --name TestMachine_EditTest --type template --new-name TestMachine_EditTest_001 --create-contained --confirm --confirm-target TestMachine_EditTest
```
Use instance deployment commands only against explicitly named test instances:
```powershell
graccess instance deploy --galaxy ZB --name TestMachine_EditTest_001 --type instance --confirm --confirm-target TestMachine_EditTest_001
```
## Edit Scripts
Use `script-editing.md` for script-library import/export and object-level script guidance.
The current CLI can import script libraries:
```powershell
graccess script-library import --galaxy ZB --path '.\scripts\CommonScripts.aaslib' --confirm --confirm-target '.\scripts\CommonScripts.aaslib'
```
The current CLI does not expose direct JSON editing for object-level script bodies. Use object export/import for full-fidelity script body edits, or add a dedicated command that exposes script primitives and script-valued attributes.
## Post-Edit Verification
After checkin, take a second snapshot:
```powershell
$galaxy = 'ZB'
$name = 'TestMachine'
$out = ".\template-snapshots\$name-after"
New-Item -ItemType Directory -Force -Path $out | Out-Null
graccess object get --galaxy $galaxy --name $name --type template --json `
| Set-Content "$out\object.json"
graccess object attributes --galaxy $galaxy --name $name --type template --json `
| Set-Content "$out\attributes.json"
graccess object attributes --galaxy $galaxy --name $name --type template --configurable --json `
| Set-Content "$out\configurable-attributes.json"
graccess object extended-attributes --galaxy $galaxy --name $name --type template --json `
| Set-Content "$out\extended-attributes.json"
```
Compare before and after snapshots:
```powershell
Compare-Object `
(Get-Content '.\template-snapshots\TestMachine-before\attributes.json') `
(Get-Content '.\template-snapshots\TestMachine-after\attributes.json')
```
Also verify command summaries after each mutation. GRAccess returns `CommandResult` or `CommandResults`; failed calls should be treated as failed edits even if the CLI process exits after printing output.
## Rollback Options
Rollback depends on what changed:
| Change type | Rollback path |
|---|---|
| Unchecked-in edit | `object undo-checkout` |
| Attribute value/metadata edit | Restore previous value with another edit cycle |
| UDA or extension edit | Reverse the add/delete/rename/update operation |
| Derived test template | Delete the derived template after validation |
| Full object/package edit | Re-import the pre-edit export package with explicit confirmation |
Importing packages is production-impacting and requires exact confirmation:
```powershell
graccess galaxy import-objects --galaxy ZB --file '.\template-snapshots\TestMachine-before\TestMachine.aaPKG' --overwrite --confirm --confirm-target '.\template-snapshots\TestMachine-before\TestMachine.aaPKG'
```
## Current CLI Gaps For Full-Fidelity Editing
The current CLI can edit object properties, attribute values, locks, security classifications, buffers, UDAs, extensions, derived templates, instances, and script libraries. Full-fidelity template editing still has gaps:
| Area | Current status | Recommended next step |
|---|---|---|
| Attribute value readback | Values are not emitted by `object attributes` | Add defensive `IAttribute.Value` serialization |
| History/I/O/alarm value editing | Possible only when the setting is a known attribute and supported value type | Add typed setting commands or broader `MxValue` support |
| Object-level script bodies | Not exposed as JSON | Add `object scripts get/set` or use export/import |
| Object-reference property assignment | Raw string assignment may fail for some COM properties | Resolve named GRAccess objects before assignment |
| Validation details | `object get` does not emit all errors/warnings/status fields | Extend object detail output defensively |
Prefer implementing those gaps as read-only parse support first, then mutation support with confirmation guards.
@@ -0,0 +1,441 @@
# Creating And Editing Template Instances
This guide describes how to create and edit instances from templates with `graccess_cli`, including area creation, area assignment, engine assignment, I/O-related configuration, deployment, and rollback.
Use this guide with:
- `template-parsing.md` - inspect templates before instantiation.
- `template-editing.md` - edit source templates.
- `attribute-parsing.md` and `attribute-editing.md` - inspect and edit instance settings.
- `script-parsing.md` and `script-editing.md` - inspect and edit script-related content.
Run commands from `graccess_cli`. Examples assume the CLI is available as `graccess`. During local development, replace `graccess` with:
```powershell
dotnet run --project src/ZB.MOM.WW.GRAccess.Cli/ZB.MOM.WW.GRAccess.Cli.csproj --
```
## Safety Model
Mutating commands require `--confirm`. Commands that edit an object require `--confirm-target` matching the exact current object name.
Important confirmation targets:
| Command | `--confirm-target` must match |
|---|---|
| `template instantiate` | Source template name |
| `object checkout/save/checkin/undo-checkout` | Target object or instance name |
| `object set` | Target object or instance name |
| `object attribute set/lock/security/buffer` | Target object or instance name |
| `object attribute value set` | Target object or instance name |
| `instance assign-area/assign-engine/assign-container` | Target instance name |
| `io assign` | Target instance name |
| `instance deploy/undeploy/upload/delete` | Target instance name |
| `objects export` | Output file path |
| `galaxy import-objects` | Input file path |
For production work, create or use a test area and test instance first.
## Session Setup
Start a session for repeated work:
```powershell
graccess session start --galaxy ZB --node .
```
With a session active, omit `--node` on logged-in commands. Without a session, add `--node .` to each command.
For LLM-driven instance work, snapshot before and after edits:
```powershell
graccess object snapshot --galaxy ZB --name TestMachine_001 --type instance --llm-json
```
Prefer intent wrappers for IDE concepts:
```powershell
graccess area list --galaxy ZB --llm-json
graccess engine list --galaxy ZB --llm-json
graccess instance assign-area --galaxy ZB --name TestMachine_001 --area Area_Test --confirm --confirm-target TestMachine_001 --llm-json
graccess instance assign-engine --galaxy ZB --name TestMachine_001 --engine AppEngine_Test --confirm --confirm-target TestMachine_001 --llm-json
graccess io assign --galaxy ZB --name TestMachine_001 --attribute DeviceAddress --value D100 --confirm --confirm-target TestMachine_001 --llm-json
```
## Discover Source Templates
Find the exact template name before instantiation:
```powershell
graccess template list --galaxy ZB --pattern '%TestMachine%' --json
```
If the template starts with `$`, quote it in PowerShell:
```powershell
graccess template list --galaxy ZB --pattern '%$Area%' --json
graccess template list --galaxy ZB --pattern '%$AppEngine%' --json
```
Template names differ by System Platform version and galaxy standards. Always confirm the actual template names in the target galaxy.
## Create An Instance From A Template
Use `template instantiate`:
```powershell
graccess template instantiate --galaxy ZB --name TestMachine --type template --new-name TestMachine_001 --create-contained --confirm --confirm-target TestMachine
```
Notes:
| Option | Meaning |
|---|---|
| `--name` | Source template name |
| `--new-name` | New instance tagname |
| `--create-contained` | Create contained objects from the template when supported |
| `--confirm-target` | Must match the source template name |
Verify the new instance:
```powershell
graccess object get --galaxy ZB --name TestMachine_001 --type instance --json
graccess object attributes --galaxy ZB --name TestMachine_001 --type instance --json
```
## Create Areas
Areas are galaxy objects. The CLI has an `area create` wrapper and also supports the underlying `template instantiate` workflow. Prefer the wrapper when it maps cleanly to the target galaxy; fall back to `template instantiate` when you need explicit control over the source template.
Find candidate area templates:
```powershell
graccess template list --galaxy ZB --pattern '%Area%' --json
```
Create an area instance, using the exact area template name returned by the galaxy. If the template is `$Area`, quote it:
```powershell
graccess area create --galaxy ZB --template '$Area' --name Area_Test --confirm --confirm-target '$Area' --llm-json
```
Equivalent lower-level form:
```powershell
graccess template instantiate --galaxy ZB --name '$Area' --type template --new-name Area_Test --create-contained --confirm --confirm-target '$Area'
```
Verify the area:
```powershell
graccess object get --galaxy ZB --name Area_Test --type instance --json
```
Edit an area like any other instance:
```powershell
graccess object checkout --galaxy ZB --name Area_Test --type instance --confirm --confirm-target Area_Test
graccess object attribute set --galaxy ZB --name Area_Test --type instance --attribute Description --value 'Test area' --data-type string --confirm --confirm-target Area_Test
graccess object save --galaxy ZB --name Area_Test --type instance --confirm --confirm-target Area_Test
graccess object checkin --galaxy ZB --name Area_Test --type instance --comment 'Configure test area' --confirm --confirm-target Area_Test
```
If the area template or area attributes differ in the target galaxy, use the parsing docs to identify the supported attributes before editing.
## Assign An Instance To An Area
Use `object set --property area` on the instance:
```powershell
graccess object checkout --galaxy ZB --name TestMachine_001 --type instance --confirm --confirm-target TestMachine_001
graccess object set --galaxy ZB --name TestMachine_001 --type instance --property area --value Area_Test --confirm --confirm-target TestMachine_001
graccess object save --galaxy ZB --name TestMachine_001 --type instance --confirm --confirm-target TestMachine_001
graccess object checkin --galaxy ZB --name TestMachine_001 --type instance --comment 'Assign area' --confirm --confirm-target TestMachine_001
```
Validate area relationships:
```powershell
graccess object query-condition --galaxy ZB --type instance --condition belongsToArea --value Area_Test --json
```
The current CLI resolves named GRAccess objects before falling back to string assignment for object-reference properties such as `area`, `host`, and `container`.
## Create Or Find Engines
Engine creation is also template-driven. The CLI has an `engine create` wrapper and also supports the underlying `template instantiate` workflow.
Find engine templates:
```powershell
graccess template list --galaxy ZB --pattern '%Engine%' --json
```
Create an engine instance only after confirming the correct platform/engine template for the target galaxy:
```powershell
graccess engine create --galaxy ZB --template '$AppEngine' --name AppEngine_Test --confirm --confirm-target '$AppEngine' --llm-json
```
Equivalent lower-level form:
```powershell
graccess template instantiate --galaxy ZB --name '$AppEngine' --type template --new-name AppEngine_Test --create-contained --confirm --confirm-target '$AppEngine'
```
If the galaxy already has a target engine, inspect it instead:
```powershell
graccess object get --galaxy ZB --name AppEngine_Test --type instance --json
graccess object attributes --galaxy ZB --name AppEngine_Test --type instance --json
```
Engine templates and required container/platform relationships vary by System Platform version and galaxy standards. Use export/import or vendor tooling if engine creation requires fields the CLI does not expose yet.
## Assign An Instance To An Engine
Use `object set --property host`:
```powershell
graccess object checkout --galaxy ZB --name TestMachine_001 --type instance --confirm --confirm-target TestMachine_001
graccess object set --galaxy ZB --name TestMachine_001 --type instance --property host --value AppEngine_Test --confirm --confirm-target TestMachine_001
graccess object save --galaxy ZB --name TestMachine_001 --type instance --confirm --confirm-target TestMachine_001
graccess object checkin --galaxy ZB --name TestMachine_001 --type instance --comment 'Assign host engine' --confirm --confirm-target TestMachine_001
```
Validate engine assignment:
```powershell
graccess object query-condition --galaxy ZB --type instance --condition hostEngineIs --value AppEngine_Test --json
```
The current CLI resolves named GRAccess objects before falling back to string assignment for object-reference properties.
## Assign Container Or Parent Object
Use `object set --property container` when the instance should be contained by another object:
```powershell
graccess object checkout --galaxy ZB --name TestMachine_001 --type instance --confirm --confirm-target TestMachine_001
graccess object set --galaxy ZB --name TestMachine_001 --type instance --property container --value Area_Test --confirm --confirm-target TestMachine_001
graccess object save --galaxy ZB --name TestMachine_001 --type instance --confirm --confirm-target TestMachine_001
graccess object checkin --galaxy ZB --name TestMachine_001 --type instance --comment 'Assign container' --confirm --confirm-target TestMachine_001
```
Validate containment:
```powershell
graccess object query-condition --galaxy ZB --type instance --condition containedBy --value Area_Test --json
```
Container, area, and host are separate concepts. Use the property that matches the object model requirement for the template being instantiated.
## Create And Edit Embedded Objects
Embedded objects are contained child templates or instances. In the `TestMachine` family, `$TestMachine` contains `$TestMachine.DelmiaReceiver` and `$TestMachine.MESReceiver`; each `TestMachine_NNN` instance contains `DelmiaReceiver_NNN` and `MESReceiver_NNN`.
Create embedded objects by instantiating the parent with `--create-contained`:
```powershell
graccess template instantiate --galaxy ZB --name '$TestMachine' --type template --new-name TestMachine_021 --create-contained --confirm --confirm-target '$TestMachine' --llm-json
```
Verify parent and child objects:
```powershell
graccess object snapshot --galaxy ZB --name TestMachine_021 --type instance --llm-json
graccess instance list --galaxy ZB --pattern '%021%' --llm-json
graccess object query-condition --galaxy ZB --type instance --condition hierarchicalNameLike --value '%TestMachine_021%' --llm-json
```
If relationship queries do not return the embedded children, fall back to instance list patterns and child naming conventions:
```powershell
graccess instance list --galaxy ZB --pattern '%DelmiaReceiver_021%' --llm-json
graccess instance list --galaxy ZB --pattern '%MESReceiver_021%' --llm-json
```
Edit embedded child instances by targeting the child instance tagname:
```powershell
graccess object snapshot --galaxy ZB --name DelmiaReceiver_021 --type instance --llm-json
graccess object checkout --galaxy ZB --name DelmiaReceiver_021 --type instance --confirm --confirm-target DelmiaReceiver_021
graccess object attribute value set --galaxy ZB --name DelmiaReceiver_021 --type instance --attribute DownloadPath --value 'C:\Recipes\021' --data-type string --confirm --confirm-target DelmiaReceiver_021 --llm-json
graccess object save --galaxy ZB --name DelmiaReceiver_021 --type instance --confirm --confirm-target DelmiaReceiver_021
graccess object checkin --galaxy ZB --name DelmiaReceiver_021 --type instance --comment 'Configure embedded Delmia receiver' --confirm --confirm-target DelmiaReceiver_021
```
Assign or move containment with `instance assign-container` or `object set --property container` only when the target object model supports moving that object:
```powershell
graccess instance assign-container --galaxy ZB --name DelmiaReceiver_021 --container TestMachine_021 --confirm --confirm-target DelmiaReceiver_021 --llm-json
```
Do not assume embedded child tag names are globally derivable from the parent number in every galaxy. Always verify `ContainedName` and `HierarchicalName` with `object snapshot`.
## Assign I/O
I/O assignment can mean different things depending on the template:
| I/O concern | Usual configuration path |
|---|---|
| Host engine | `object set --property host` |
| Area | `object set --property area` |
| Parent/container | `object set --property container` |
| Device/topic/address/reference settings | Attribute values or extension payloads |
| Complex I/O object structure | Instantiate I/O templates, set relationships, or import package |
First parse likely I/O settings:
```powershell
$attrs = graccess object attributes --galaxy ZB --name TestMachine_001 --type instance --json | ConvertFrom-Json
$extended = graccess object extended-attributes --galaxy ZB --name TestMachine_001 --type instance --json | ConvertFrom-Json
$io = (@($attrs) + @($extended)) | Where-Object {
$_.Name -match '(?i)\bio\b|input|output|source|destination|scan|topic|device|address|reference'
} | Sort-Object Name -Unique
$io | Select-Object Name, DataType, Category, RuntimeSetHandler, ConfigSetHandler
```
Edit simple I/O attributes:
```powershell
graccess object checkout --galaxy ZB --name TestMachine_001 --type instance --confirm --confirm-target TestMachine_001
graccess object attribute set --galaxy ZB --name TestMachine_001 --type instance --attribute DeviceAddress --value 'D100' --data-type string --confirm --confirm-target TestMachine_001
graccess object attribute set --galaxy ZB --name TestMachine_001 --type instance --attribute ScanPeriod --value 1000 --data-type int --confirm --confirm-target TestMachine_001
graccess object save --galaxy ZB --name TestMachine_001 --type instance --confirm --confirm-target TestMachine_001
graccess object checkin --galaxy ZB --name TestMachine_001 --type instance --comment 'Assign I/O settings' --confirm --confirm-target TestMachine_001
```
The exact attribute names are template-specific. Use `attribute-parsing.md` to identify real names before editing.
The current CLI supports scalar attribute values only: `string`, `bool`, `int`, `float`, and `double`. Complex I/O settings such as object references, arrays, structured addresses, or extension-specific payloads may require object export/import or additional CLI value serialization.
## Edit Instance Attributes
For normal instance configuration:
```powershell
graccess object checkout --galaxy ZB --name TestMachine_001 --type instance --confirm --confirm-target TestMachine_001
graccess object attribute set --galaxy ZB --name TestMachine_001 --type instance --attribute Description --value 'Machine 001' --data-type string --confirm --confirm-target TestMachine_001
graccess object attribute security --galaxy ZB --name TestMachine_001 --type instance --attribute Description --security MxSecurityOperate --confirm --confirm-target TestMachine_001
graccess object save --galaxy ZB --name TestMachine_001 --type instance --confirm --confirm-target TestMachine_001
graccess object checkin --galaxy ZB --name TestMachine_001 --type instance --comment 'Configure instance' --confirm --confirm-target TestMachine_001
```
For a deeper attribute workflow, use `attribute-editing.md`.
## Deploy, Undeploy, And Upload
Deploy an explicitly named test instance:
```powershell
graccess instance deploy --galaxy ZB --name TestMachine_001 --type instance --confirm --confirm-target TestMachine_001
```
Undeploy:
```powershell
graccess instance undeploy --galaxy ZB --name TestMachine_001 --type instance --confirm --confirm-target TestMachine_001
```
Upload runtime changes:
```powershell
graccess instance upload --galaxy ZB --name TestMachine_001 --type instance --confirm --confirm-target TestMachine_001
```
Bulk deployment commands are available, but use them only with explicit target selection:
```powershell
graccess objects deploy --galaxy ZB --type instance --name TestMachine_001 --name TestMachine_002 --confirm --confirm-target TestMachine_001,TestMachine_002
```
For bulk commands, the confirmation target must match the CLI's target list. Prefer single-instance deployment until the target list behavior is verified in a test galaxy.
## Export Instance Configuration
Export before risky edits:
```powershell
$pkg = '.\instance-snapshots\TestMachine_001-before.aaPKG'
graccess objects export --galaxy ZB --type instance --name TestMachine_001 --output $pkg --confirm --confirm-target $pkg
```
For full-fidelity edits not supported by scalar CLI commands, edit through the supported package workflow and import:
```powershell
graccess galaxy import-objects --galaxy ZB --file '.\instance-snapshots\TestMachine_001-edited.aaPKG' --overwrite --confirm --confirm-target '.\instance-snapshots\TestMachine_001-edited.aaPKG'
```
## Delete Test Instances
Delete only explicitly named test instances:
```powershell
graccess instance delete --galaxy ZB --name TestMachine_001 --type instance --confirm --confirm-target TestMachine_001
```
If the object is deployed, undeploy it first unless the selected force option explicitly allows deletion. The command exposes `--force-option`; the default is `undeployIfDeployed`.
```powershell
graccess instance delete --galaxy ZB --name TestMachine_001 --type instance --force-option undeployIfDeployed --confirm --confirm-target TestMachine_001
```
## End-To-End Example
This creates an area, creates a template instance, assigns area and engine, sets simple I/O attributes, and deploys the instance.
```powershell
$galaxy = 'ZB'
$template = 'TestMachine'
$areaTemplate = '$Area'
$area = 'Area_Test'
$engine = 'AppEngine_Test'
$instance = 'TestMachine_001'
graccess session start --galaxy $galaxy --node .
graccess template instantiate --galaxy $galaxy --name $areaTemplate --type template --new-name $area --create-contained --confirm --confirm-target $areaTemplate
graccess template instantiate --galaxy $galaxy --name $template --type template --new-name $instance --create-contained --confirm --confirm-target $template
graccess object checkout --galaxy $galaxy --name $instance --type instance --confirm --confirm-target $instance
graccess object set --galaxy $galaxy --name $instance --type instance --property area --value $area --confirm --confirm-target $instance
graccess object set --galaxy $galaxy --name $instance --type instance --property host --value $engine --confirm --confirm-target $instance
graccess object attribute set --galaxy $galaxy --name $instance --type instance --attribute DeviceAddress --value 'D100' --data-type string --confirm --confirm-target $instance
graccess object attribute set --galaxy $galaxy --name $instance --type instance --attribute ScanPeriod --value 1000 --data-type int --confirm --confirm-target $instance
graccess object save --galaxy $galaxy --name $instance --type instance --confirm --confirm-target $instance
graccess object checkin --galaxy $galaxy --name $instance --type instance --comment 'Create and configure test instance' --confirm --confirm-target $instance
graccess instance deploy --galaxy $galaxy --name $instance --type instance --confirm --confirm-target $instance
```
Before running this exact sequence, verify `$Area`, `AppEngine_Test`, `DeviceAddress`, and `ScanPeriod` are valid in the target galaxy. These names are examples, not universal System Platform requirements.
## Post-Edit Verification
Verify object records and relationships:
```powershell
graccess object get --galaxy ZB --name TestMachine_001 --type instance --json
graccess object attributes --galaxy ZB --name TestMachine_001 --type instance --configurable --json
graccess object query-condition --galaxy ZB --type instance --condition belongsToArea --value Area_Test --json
graccess object query-condition --galaxy ZB --type instance --condition hostEngineIs --value AppEngine_Test --json
```
If deployed, verify deployment status through available object details or System Platform tooling. The current `object get` output does not emit every `IInstance` status property; extending object details to include `DeploymentStatus`, `DeployedVersion`, validation errors, and warnings is recommended.
## Current CLI Gaps For Instance Work
| Area | Current status | Recommended next step |
|---|---|---|
| Dedicated area commands | Areas are created and edited as normal instances | Add `area create/edit/list` wrappers if desired |
| Dedicated engine commands | Engines are created and edited as normal instances | Add `engine create/edit/list` wrappers if desired |
| Object-reference assignment | `object set` currently writes the supplied value directly | Resolve named objects and assign COM object references when required |
| Bulk property edits | Bulk deploy/undeploy/upload/delete/export exist; bulk `area`/`host` assignment does not | Add guarded `objects set` after single-object assignment is reliable |
| Complex I/O values | Scalar `MxValue` writes only | Add array/reference/structured value serialization |
| Deployment status details | `object get` does not emit all instance status fields | Extend object detail output defensively |
+275
View File
@@ -0,0 +1,275 @@
# Parsing Existing Templates
This guide describes a read-only workflow for inspecting an existing GRAccess template, using `TestMachine` as the example target. The same steps work for any template name.
Use this document as the top-level workflow. For deeper coverage, also read:
- `attribute-parsing.md` - all attributes, configurable attributes, extended attributes, and setting families such as history, I/O, alarms, UDAs, and extensions.
- `script-parsing.md` - script libraries, object-level script discovery, and export-based script capture.
Examples assume the CLI is available as `graccess`. During local development, replace `graccess` with:
```powershell
dotnet run --project src/ZB.MOM.WW.GRAccess.Cli/ZB.MOM.WW.GRAccess.Cli.csproj --
```
Run commands from `graccess_cli`.
## Session Setup
For repeated template parsing, start a session first so commands reuse one GRAccess login:
```powershell
graccess session start --galaxy ZB --node .
```
After a session is active, logged-in commands can omit `--node`:
```powershell
graccess template list --galaxy ZB --pattern '%TestMachine%' --json
```
Without a session, include `--node .` on each command:
```powershell
graccess template list --galaxy ZB --node . --pattern '%TestMachine%' --json
```
For LLM-driven parsing, prefer the bundled machine snapshot before running narrower commands:
```powershell
graccess object snapshot --galaxy ZB --name TestMachine --type template --llm-json
```
Use the narrower `object attributes`, `object extended-attributes`, and script commands when you need to drill into a specific section or compare legacy `--json` output.
## Find The Template
Use `template list` to confirm the exact template tagname. GRAccess patterns use `%` as the wildcard.
```powershell
graccess template list --galaxy ZB --pattern '%TestMachine%' --json
```
If the template name starts with `$`, quote it with single quotes in PowerShell:
```powershell
graccess template list --galaxy ZB --pattern '%$TestMachine%' --json
```
## Read Template Identity
Use `object get` with `--type template` to fetch the core object record:
```powershell
graccess object get --galaxy ZB --name TestMachine --type template --json
```
Expected fields include:
| Field | Meaning |
|---|---|
| `Kind` | `template` for template objects |
| `Tagname` | Template tagname |
| `ContainedName` | Name inside its container |
| `HierarchicalName` | Galaxy hierarchy path when available |
| `CheckoutStatus` | Current checkout state |
For LLM-oriented parsing, prefer `object snapshot --llm-json` because it returns the same identity section plus attributes, relationships, script metadata, and unavailable-field details in one stable envelope.
## Parse Inheritance And Containment
Template inheritance and containment are separate relationships:
| Relationship | Meaning | Typical command |
| --- | --- | --- |
| Derived from | Template extends another template, such as `$TestMachine` extending `$gMachine` | `object snapshot`, `object lineage`, `object query-condition --condition derivedOrInstantiatedFrom` |
| Based on | Descendant or instance family relationship to a base template | `object query-condition --condition basedOn` |
| Contained by | Embedded template or object inside a parent template/instance | `object children`, `object query-condition --condition containedBy`; `template instantiate --create-contained` for creation |
Start with the snapshot:
```powershell
graccess object snapshot --galaxy ZB --name '$TestMachine' --type template --llm-json
graccess object lineage --galaxy ZB --name '$TestMachine' --type template --llm-json
graccess object children --galaxy ZB --name '$TestMachine' --type template --llm-json
```
Check `data.Lineage`, `data.Children`, `data.ContainedObjects`, and these fields in the returned `data.Object` object:
| Field | Meaning |
| --- | --- |
| `DerivedFrom` | Immediate parent template when the local GRAccess property is exposed |
| `BasedOn` | Base template lineage when exposed |
| `ContainedName` | Embedded name inside the parent template or instance |
| `HierarchicalName` | Full contained path when exposed |
Then run relationship queries:
```powershell
graccess object query-condition --galaxy ZB --type all --condition derivedOrInstantiatedFrom --value '$gMachine' --llm-json
graccess object query-condition --galaxy ZB --type all --condition basedOn --value '$gMachine' --llm-json
graccess object query-condition --galaxy ZB --type all --condition containedBy --value '$TestMachine' --llm-json
graccess object query-condition --galaxy ZB --type all --condition hierarchicalNameLike --value '%TestMachine%' --llm-json
```
GRAccess relationship queries can be version-sensitive. The CLI now tries typed template/instance properties first, then uses a read-only exported package fallback for snapshot, lineage, children, attribute values, and script bodies when export is available. If both direct GRAccess and package fallback are unavailable, document the unavailable entry and avoid guessing. Do not use Galaxy SQL as part of normal parsing; SQL is only a development verification/debugging oracle.
Contained embedded templates should be parsed as their own templates:
```powershell
graccess object snapshot --galaxy ZB --name '$TestMachine.DelmiaReceiver' --type template --llm-json
graccess object snapshot --galaxy ZB --name '$TestMachine.MESReceiver' --type template --llm-json
```
Contained instances should be parsed as their own instances:
```powershell
graccess object snapshot --galaxy ZB --name DelmiaReceiver_001 --type instance --llm-json
graccess object snapshot --galaxy ZB --name MESReceiver_001 --type instance --llm-json
```
When available, compare `Tagname`, `ContainedName`, and `HierarchicalName` to connect child instance tags back to parent paths such as `TestMachine_001.DelmiaReceiver`.
## Dump Attributes
Use `object attributes` to enumerate the template attributes:
```powershell
graccess object attributes --galaxy ZB --name TestMachine --type template --json
```
Use `--configurable` when you only need configurable attributes:
```powershell
graccess object attributes --galaxy ZB --name TestMachine --type template --configurable --json
```
Attribute JSON includes the stable metadata currently exposed by the CLI:
| Field | Meaning |
|---|---|
| `Name` | Attribute name |
| `DataType` | GRAccess data type |
| `Category` | Attribute category |
| `SecurityClassification` | Security classification |
| `Locked` | Lock state |
| `UpperBoundDim1` | Array upper bound when available |
| `HasBuffer` | Attribute buffer flag when available |
| `RuntimeSetHandler` | Runtime set handler flag when available |
| `ConfigSetHandler` | Config set handler flag when available |
Some COM-backed metadata is not available on every attribute. In JSON output, unavailable values are returned as `null`.
## Read One Attribute
Use `object attribute get` when you already know the attribute name and need its metadata without dumping the full collection:
```powershell
graccess object attribute get --galaxy ZB --name TestMachine --type template --attribute Description --json
```
This command is read-only and does not require `--confirm`.
## Dump Extended Attributes
Use `object extended-attributes` to call GRAccess `GetExtendedAttributes` for the template:
```powershell
graccess object extended-attributes --galaxy ZB --name TestMachine --type template --json
```
You can scope the request with an attribute name and hierarchy level:
```powershell
graccess object extended-attributes --galaxy ZB --name TestMachine --type template --attribute Description --level 0 --json
```
The output uses the same attribute metadata shape as `object attributes`.
For parsing all attribute settings, including properties, history settings, I/O settings, alarm settings, UDAs, and extensions, use the expanded workflow in `attribute-parsing.md`.
## Capture Scripts
Template scripts may appear as script-related attributes, extension attributes, exported object package content, or script libraries. Use `script-parsing.md` when a parse needs script bodies, declarations, triggers, or script-library files.
Minimum script-related capture for `TestMachine`:
```powershell
graccess script-library list --galaxy ZB --json
graccess object attributes --galaxy ZB --name TestMachine --type template --json
graccess object extended-attributes --galaxy ZB --name TestMachine --type template --json
```
For complete script content, export the template object:
```powershell
$pkg = '.\template-snapshots\TestMachine\TestMachine.aaPKG'
graccess objects export --galaxy ZB --type template --name TestMachine --output $pkg --confirm --confirm-target $pkg
```
## Find Related Objects
Use `object query-condition` to find objects related to the template.
First-level derived templates or instantiated instances:
```powershell
graccess object query-condition --galaxy ZB --type all --condition derivedOrInstantiatedFrom --value TestMachine --json
```
All descendants based on the same base template:
```powershell
graccess object query-condition --galaxy ZB --type all --condition basedOn --value TestMachine --json
```
Objects contained by the template:
```powershell
graccess object query-condition --galaxy ZB --type all --condition containedBy --value TestMachine --json
```
Useful `EConditionType` values for parsing template relationships are documented in `graccess_documentation.md` under `EConditionType`. The most common values for template parsing are `namedLike`, `NameEquals`, `derivedOrInstantiatedFrom`, `basedOn`, `containedBy`, and `hierarchicalNameLike`.
## Save A Local Parse Snapshot
PowerShell can capture JSON output into files for comparison or downstream processing:
```powershell
$name = 'TestMachine'
$out = '.\template-snapshots'
New-Item -ItemType Directory -Force -Path $out | Out-Null
graccess object get --galaxy ZB --name $name --type template --json `
| Set-Content "$out\$name.object.json"
graccess object attributes --galaxy ZB --name $name --type template --json `
| Set-Content "$out\$name.attributes.json"
graccess object attributes --galaxy ZB --name $name --type template --configurable --json `
| Set-Content "$out\$name.configurable-attributes.json"
graccess object extended-attributes --galaxy ZB --name $name --type template --json `
| Set-Content "$out\$name.extended-attributes.json"
graccess script-library list --galaxy ZB --json `
| Set-Content "$out\script-libraries.json"
graccess object query-condition --galaxy ZB --type all --condition derivedOrInstantiatedFrom --value $name --json `
| Set-Content "$out\$name.children.json"
graccess object query-condition --galaxy ZB --type all --condition basedOn --value $name --json `
| Set-Content "$out\$name.descendants.json"
```
To inspect the parsed JSON in PowerShell:
```powershell
$attrs = Get-Content '.\template-snapshots\TestMachine.attributes.json' -Raw | ConvertFrom-Json
$attrs | Sort-Object Name | Select-Object Name, DataType, Category, Locked
```
## Safety Notes
The commands in this guide are read-only. Do not use `object set`, `object attribute set`, `object uda`, `object extension`, `template derive`, `template instantiate`, or delete/deploy commands while parsing unless you intentionally want to mutate the galaxy and pass the required `--confirm` and `--confirm-target` flags.
+435
View File
@@ -0,0 +1,435 @@
# GRAccess CLI Usage
Command-line interface for Aveva System Platform Galaxy management via the GRAccess library.
## Global Options
All commands that interact with a galaxy require:
| Option | Short | Description | Required |
|---|---|---|---|
| `--galaxy` | `-g` | Galaxy name | Yes |
| `--node` | `-n` | GR node name. Blank defaults to local node; `.` is normalized to the local machine name. | One-shot mode only |
In session mode, `--node` is not needed because the daemon already holds the connection.
Machine-facing commands also support:
| Option | Description |
|---|---|
| `--json` | Legacy command-specific JSON. Existing shapes are preserved. |
| `--llm-json` | Stable success/error envelope for LLM and tool callers. |
| `--dry-run` | For mutating routed commands, validate arguments and confirmation without invoking mutating GRAccess calls. |
`--llm-json` envelope shape:
```json
{
"success": true,
"command": "object get",
"galaxy": "ZB",
"target": "TestMachine",
"data": {},
"commandResult": null,
"warnings": [],
"unavailable": [],
"error": null,
"exitCode": 0
}
```
## LLM And Tooling Commands
### `capabilities`
Return the code-backed command registry.
```powershell
graccess capabilities --json
graccess capabilities --llm-json
```
The registry includes command names, arguments, mutation status, session routing support, confirmation target rules, and output schema names.
### `validate`
Validate a single-command or batch plan file without connecting to GRAccess.
```powershell
graccess validate --request plan.json --llm-json
```
### `batch`
Validate or execute a command plan. Execution stops on first failure. Every mutating step must include its own `confirm=true` and exact `confirm-target`.
```powershell
graccess batch --file plan.json --mode validate --llm-json
graccess batch --file plan.json --mode execute --llm-json
```
Plan example:
```json
{
"Galaxy": "ZB",
"Node": ".",
"Commands": [
{
"Command": "object attribute value set",
"Args": {
"name": "TestMachine",
"type": "template",
"attribute": "Description",
"value": "Updated",
"data-type": "string",
"confirm": true,
"confirm-target": "TestMachine"
}
}
]
}
```
## Galaxy Commands
### `galaxy list`
List available galaxies on a GR node.
```
graccess galaxy list [--node <node>] [--json] [--llm-json]
```
| Option | Short | Required | Description |
|---|---|---|---|
| `--node` | `-n` | No | GR node name to query. Blank defaults to local node; `.` is normalized to the local machine name. |
| `--json` | | No | Output as JSON array |
| `--llm-json` | | No | Output stable LLM envelope |
This command always runs in one-shot mode (no galaxy login needed). It does not use or require an active session.
Example output:
```
MyGalaxy1
MyGalaxy2
TestGalaxy
```
With `--json`:
```json
[
"MyGalaxy1",
"MyGalaxy2",
"TestGalaxy"
]
```
---
## Object Query Commands
These commands route through an active session when one exists for the target galaxy. Without an active session, they connect to a galaxy, run a read-only GRAccess query, and disconnect. Name patterns use the GRAccess `namedLike` condition; use `%` as the wildcard.
### `object list`
List templates, instances, or both.
```
graccess object list --galaxy <name> --node <node> [--type all|template|instance] [--pattern <pattern>] [--json]
```
| Option | Short | Default | Description |
|---|---|---|---|
| `--galaxy` | `-g` | (required) | Galaxy name |
| `--node` | `-n` | local node | GR node name. `.` is normalized to the local machine name. |
| `--type` | `-t` | `all` | Object type: `all`, `template`, or `instance` |
| `--pattern` | `-p` | `%` | GRAccess name pattern. Use `%` as wildcard. |
| `--json` | | `false` | Output as JSON |
Example:
```powershell
graccess object list --galaxy ZB --node . --type instance --pattern 'TestMachine_%'
```
### `template list`
List templates.
```
graccess template list --galaxy <name> --node <node> [--pattern <pattern>] [--json]
```
### `instance list`
List instances.
```
graccess instance list --galaxy <name> --node <node> [--pattern <pattern>] [--json]
```
Example:
```powershell
graccess instance list --galaxy ZB --node . --pattern '%'
```
### `object attributes`
List attributes for a template or instance.
```
graccess object attributes --galaxy <name> --node <node> --name <tagname> [--type all|template|instance] [--configurable] [--json]
```
| Option | Short | Default | Description |
|---|---|---|---|
| `--galaxy` | `-g` | (required) | Galaxy name |
| `--node` | `-n` | local node | GR node name. `.` is normalized to the local machine name. |
| `--name` | | (required) | Template or instance tagname |
| `--type` | `-t` | `all` | Object type: `all`, `template`, or `instance` |
| `--configurable` | | `false` | Query `ConfigurableAttributes` instead of `Attributes` |
| `--json` | | `false` | Output as JSON |
Some optional COM-backed attribute properties are not available for every object. In JSON output those values are emitted as `null`; text output includes the stable name, data type, and category columns.
Example:
```powershell
graccess object attributes --galaxy ZB --node . --name DEV --type instance --configurable
```
---
## LLM Snapshot And IDE Commands
### `object snapshot`
Return a bundled object snapshot for planning and verification.
```powershell
graccess object snapshot --galaxy ZB --name TestMachine --type template --llm-json
```
Snapshot data includes object identity/status, all attributes, configurable attributes, extended attributes, relationships, lineage, children, contained objects, package-backed attribute values and script bodies where available, and unavailable field details.
Use snapshots, direct lineage commands, and relationship queries together to parse inheritance and containment:
```powershell
graccess object snapshot --galaxy ZB --name '$TestMachine' --type template --llm-json
graccess object lineage --galaxy ZB --name '$TestMachine' --type template --llm-json
graccess object children --galaxy ZB --name '$TestMachine' --type template --llm-json
graccess object query-condition --galaxy ZB --type all --condition derivedOrInstantiatedFrom --value '$gMachine' --llm-json
graccess object query-condition --galaxy ZB --type all --condition basedOn --value '$gMachine' --llm-json
graccess object query-condition --galaxy ZB --type all --condition containedBy --value '$TestMachine' --llm-json
graccess object query-condition --galaxy ZB --type all --condition hierarchicalNameLike --value '%TestMachine%' --llm-json
```
`object lineage` walks typed `ITemplate` / `IInstance` relationship properties first and falls back to the read-only package snapshot when GRAccess export is available. `object children` combines package-backed containment with direct GRAccess scans for objects whose `DerivedFrom`, `BasedOn`, `Container`, or `HierarchicalName` point at the target.
When direct GRAccess does not expose a field, the CLI records a structured unavailable entry instead of guessing. Normal CLI usage does not query the Galaxy SQL database; SQL is allowed only for development verification/debugging outside the supported command path.
### Attribute values
```powershell
graccess object attribute value get --galaxy ZB --name TestMachine --type template --attribute Description --llm-json
graccess object attribute value set --galaxy ZB --name TestMachine --type template --attribute Description --value Updated --data-type string --confirm --confirm-target TestMachine --llm-json
```
Scalar `string`, `bool`, `int`, `float`, and `double` writes are supported first. Value reads try direct `IAttribute.Value` first, then use the read-only exported package fallback for scalar package values. Array and complex readback returns structured unavailable details unless parsed safely.
### Object scripts
```powershell
graccess object scripts list --galaxy ZB --name TestMachine --type template --llm-json
graccess object scripts get --galaxy ZB --name TestMachine --type template --script UpdateTestChangingInt --llm-json
graccess object scripts get --galaxy ZB --name TestMachine --type template --script UpdateTestChangingInt.ExecuteText --llm-json
graccess object scripts set --galaxy ZB --name TestMachine --type template --script UpdateTestChangingInt --file UpdateTestChangingInt.txt --confirm --confirm-target TestMachine --llm-json
graccess object scripts settings set --galaxy ZB --name '$TestMachine' --type template --script UpdateTestChangingInt --trigger-period-ms 500 --lock-trigger-period --confirm --confirm-target '$TestMachine' --llm-json
graccess object scripts create --galaxy ZB --name '$MyTemplate' --type template --script OnScan --file OnScan.txt --trigger-type Periodic --trigger-period-ms 1000 --confirm --confirm-target '$MyTemplate' --llm-json
```
Direct object script body access depends on the local GRAccess object model. Reads inspect the exported package fallback for script extension bodies and script text fields such as `ExecuteText`, `DeclarationsText`, `StartupText`, `ShutdownText`, `OnScanText`, `OffScanText`, and `Expression`.
For writes, the CLI follows the GRAccess pattern used by `ScriptExtension` objects: script body and setting mutations prefer `IgObject.ConfigurableAttributes[...]`, then fall back to `Attributes[...]` only if the configurable collection does not expose the requested field. `object scripts set` writes the matching script body attribute; a bare script name maps to `.ExecuteText`. `object scripts settings set` writes common script settings such as `TriggerPeriod`, `TriggerType`, and `Expression`; `--lock-trigger-period` applies `MxLockedInMe` so derived instances receive the interval on deploy. `object scripts create` calls `AddExtensionPrimitive("ScriptExtension", <script>, true)` and can initialize the body/settings in the same checkout flow.
### Area, engine, assignment, and I/O wrappers
```powershell
graccess area list --galaxy ZB --llm-json
graccess area create --galaxy ZB --template '$Area' --name Area_Test --confirm --confirm-target '$Area' --llm-json
graccess engine list --galaxy ZB --llm-json
graccess engine create --galaxy ZB --template '$AppEngine' --name AppEngine_Test --confirm --confirm-target '$AppEngine' --llm-json
graccess instance assign-area --galaxy ZB --name TestMachine_001 --area Area_Test --confirm --confirm-target TestMachine_001 --llm-json
graccess instance assign-engine --galaxy ZB --name TestMachine_001 --engine AppEngine_Test --confirm --confirm-target TestMachine_001 --llm-json
graccess instance assign-container --galaxy ZB --name TestMachine_001 --container ParentObject --confirm --confirm-target TestMachine_001 --llm-json
graccess io assign --galaxy ZB --name TestMachine_001 --attribute DeviceAddress --value D100 --confirm --confirm-target TestMachine_001 --llm-json
```
`object set --property area|host|container|toolset|security-group` resolves named GRAccess objects before falling back to string assignment.
`instance assign-engine` sets the host when the object model accepts a direct engine host. If the instance is already assigned to an area hosted by that engine, the command treats that state as successful because some GRAccess object families use the area as the assignable host.
### Extending templates and embedded objects
Use `template derive` to extend an existing template:
```powershell
graccess template derive --galaxy ZB --name '$gMachine' --type template --new-name '$MyMachine' --confirm --confirm-target '$gMachine' --llm-json
```
Use `--create-contained` when deriving or instantiating a template family whose embedded objects should come along:
```powershell
graccess template derive --galaxy ZB --name '$TestMachine' --type template --new-name '$MyMachine' --create-contained --confirm --confirm-target '$TestMachine' --llm-json
graccess template instantiate --galaxy ZB --name '$TestMachine' --type template --new-name TestMachine_021 --create-contained --confirm --confirm-target '$TestMachine' --llm-json
```
`template delete --force-option` defaults to `dontForceTemplateDelete`, which fails when instances exist instead of cascading. `instance delete --force-option` defaults to `undeployIfDeployed` so cleanup can remove deployed test instances after confirmation. `object uda add` and `object uda update` default `--category` to `MxCategoryWriteable_USC`; GRAccess builds that reject non-lockable UDA categories are normalized to `MxCategoryWriteable_USC_Lockable` at the COM call. Pass another valid `MxAttributeCategory` explicitly when a configure-only or calculated category is required. `object extension add/delete` pass typed extension calls to GRAccess; the legacy `AnalogLimitAlarm` extension type is accepted as an alias for the local `AnalogExtension` primitive type.
Single-object UDA, extension, attribute, script, and I/O mutations are atomic: the CLI checks out the object, applies the mutation, saves, and checks it back in. If the mutation or save fails after checkout, the CLI attempts `UndoCheckOut` before returning the original error. The explicit `object checkout/save/checkin` and `objects checkout/checkin` commands remain available for manual lifecycle operations.
Contained templates and child instances are edited as normal objects by targeting their own tagname:
```powershell
graccess object snapshot --galaxy ZB --name '$TestMachine.DelmiaReceiver' --type template --llm-json
graccess object snapshot --galaxy ZB --name DelmiaReceiver_001 --type instance --llm-json
graccess object checkout --galaxy ZB --name DelmiaReceiver_001 --type instance --confirm --confirm-target DelmiaReceiver_001
graccess object attribute value set --galaxy ZB --name DelmiaReceiver_001 --type instance --attribute DownloadPath --value 'C:\Recipes\001' --data-type string --confirm --confirm-target DelmiaReceiver_001 --llm-json
graccess object save --galaxy ZB --name DelmiaReceiver_001 --type instance --confirm --confirm-target DelmiaReceiver_001
graccess object checkin --galaxy ZB --name DelmiaReceiver_001 --type instance --comment 'Configure embedded receiver' --confirm --confirm-target DelmiaReceiver_001
```
Use `instance assign-container` or `object set --property container` only after confirming the object model allows moving/reparenting that child.
---
## Session Management
Session mode keeps a GRAccess connection open in a background daemon process, avoiding expensive reconnection on each CLI invocation.
### `session start`
Start a background session for a galaxy.
```
graccess session start --galaxy <name> --node <node> [--idle-timeout <minutes>]
```
| Option | Default | Description |
|---|---|---|
| `--galaxy`, `-g` | (required) | Galaxy name |
| `--node`, `-n` | (required) | GR node name. `.` is normalized to the local machine name. |
| `--idle-timeout` | 30 | Minutes of inactivity before auto-shutdown |
Behavior:
- Spawns a background daemon process
- Waits up to 90 seconds for the daemon to connect and become ready
- If a session is already running for this galaxy, reports the existing PID
- Daemon logs to `%LOCALAPPDATA%\ZB.MOM.WW.GRAccess.Cli\logs\daemon-{galaxy}.log`
### `session stop`
Stop a running session.
```
graccess session stop --galaxy <name>
```
Sends a shutdown signal to the daemon via named pipe. The daemon disconnects from the galaxy and exits.
### `session status`
Check if a session is running.
```
graccess session status --galaxy <name>
```
Output includes: galaxy name, node, PID, start time, and pipe name. Automatically cleans up stale session files from crashed daemons.
## Execution Modes
### Session mode (fast)
When a session is active, commands route through the daemon automatically:
```bash
graccess session start --galaxy MyGalaxy --node MyNode
graccess <command> --galaxy MyGalaxy [options] # routed via named pipe
graccess <command> --galaxy MyGalaxy [options] # reuses same connection
graccess session stop --galaxy MyGalaxy
```
### One-shot mode (no session)
Without an active session, commands open a direct connection, execute, and disconnect:
```bash
graccess <command> --galaxy MyGalaxy --node MyNode [options]
```
This is slower but requires no setup. The `--node` option is required in one-shot mode.
### Routing logic
The `CommandRouter` checks for an active session first:
1. Look up session info file at `%LOCALAPPDATA%\ZB.MOM.WW.GRAccess.Cli\sessions\{galaxy}.json`
2. Verify the daemon process is alive (PID check)
3. If alive, send command as JSON via named pipe `graccess-session-{galaxy}`
4. If no session, fall back to one-shot mode (requires `--node`)
Currently routed through the session daemon:
- `object list`
- `template list`
- `instance list`
- `object attributes`
- All other logged-in galaxy commands added under `galaxy`, `object`, `template`, `instance`, `objects`, `toolset`, `script-library`, `security`, and `settings`.
Pre-login galaxy administration commands remain one-shot because they operate before a galaxy session exists:
- `galaxy list`
- `galaxy create`
- `galaxy create-from-template`
- `galaxy delete`
## Expanded Command Surface
The CLI exposes the GRAccess operations listed in `graccess_operations.md` through these command families:
| Family | Commands |
|---|---|
| Galaxy | `galaxy info`, `galaxy sync`, `galaxy cdi-version`, `galaxy defaults get/set`, `galaxy backup/restore/migrate`, `galaxy import-objects`, `galaxy import-objects-ex`, `galaxy import-script-library`, `galaxy export-all`, `galaxy grload`, `galaxy create`, `galaxy create-from-template`, `galaxy delete` |
| Objects | `object get`, `object snapshot`, `object lineage`, `object children`, `object query-name`, `object query-condition`, `object query-multi`, `object extended-attributes`, `object help-url`, `object scripts list/get/set`, `object checkout/checkin/undo-checkout/save/unload/set` |
| Templates and instances | `template derive`, `template instantiate`, `template delete`, `instance delete/deploy/undeploy/upload`, `instance assign-area/assign-engine/assign-container` |
| Attributes, UDAs, extensions | `object attribute get/set/value get/value set/lock/security/buffer`, `object uda add/delete/rename/update`, `object extension add/delete/rename` |
| Bulk objects | `objects checkout/checkin/undo-checkout/deploy/undeploy/upload/delete/export/export-protected` |
| IDE wrappers and tooling | `capabilities`, `validate`, `batch`, `area list/create`, `engine list/create`, `io assign` |
| Toolsets/scripts/security/settings | `toolset list/tree/add/delete/rename/move`, `script-library list/add/import/export`, `security info/roles/users/groups/permissions`, `settings locale get`, `settings time-master get` |
Mutating commands require `--confirm`. Mutating commands also validate `--confirm-target` against the exact object, galaxy, bulk target list, or file target used by the command.
## IPC Protocol
Communication between CLI and daemon uses newline-delimited JSON over named pipes.
### Request format
```json
{"type":"execute","command":"<cmd>","subcommand":"<subcmd>","args":{"key":"value"}}
```
Request types: `execute`, `shutdown`, `status`
### Response format
```json
{"success":true,"output":"...","exitCode":0}
{"success":false,"error":"...","exitCode":1}
```
## Daemon Details
- **Pipe name**: `graccess-session-{galaxyname}` (lowercase)
- **Mutex**: `Global\graccess-session-{galaxyname}` (prevents duplicate daemons per galaxy)
- **Session file**: `%LOCALAPPDATA%\ZB.MOM.WW.GRAccess.Cli\sessions\{galaxy}.json`
- **Log file**: `%LOCALAPPDATA%\ZB.MOM.WW.GRAccess.Cli\logs\daemon-{galaxy}.log` (daily rolling, 7 day retention)
- **Idle timeout**: Checked every 30 seconds; daemon self-exits when exceeded
- **COM threading**: All GRAccess calls run on a dedicated STA thread with a Win32 message pump (`StaComThread`)
+156
View File
@@ -0,0 +1,156 @@
# ZB Galaxy Documentation
Captured read-only with `graccess_cli` on 2026-04-28 through a session daemon:
```powershell
graccess session start --galaxy ZB --node .
graccess galaxy info --galaxy ZB --llm-json
graccess template list --galaxy ZB --pattern '%' --llm-json
graccess instance list --galaxy ZB --pattern '%' --llm-json
graccess security info --galaxy ZB --llm-json
graccess settings time-master get --galaxy ZB --llm-json
```
Session observed during capture:
- Galaxy: `ZB`
- Node: `DESKTOP-6JL3KKO`
- Pipe: `graccess-session-zb`
## Galaxy Identity
| Field | Value |
| --- | --- |
| Name | `ZB` |
| System Platform version string | `20.0.00000` |
| Version number | `43` |
| Upgrade required | `false` |
| CDI version | `5800.0474.7005.1` |
## Inventory Summary
| Object class | Count | Notes |
| --- | ---: | --- |
| Templates | 45 | Includes base System Platform templates, master templates, and project templates. |
| Instances | 67 | Includes 60 objects in the `TestMachine` family plus 7 standalone/development objects. |
| Toolsets | 0 | `toolset list` returned an empty array through the CLI. |
| Script libraries | 0 | `script-library list` returned an empty array through the CLI. |
| Security roles | 0 | `security roles` returned an empty array. |
| Security users | 0 | `security users` returned an empty array. |
| Security groups | 0 | `security groups` returned an empty array. |
`area list` and `engine list` returned empty arrays with their default patterns, but the raw instance inventory includes objects that look like development platform/engine/area objects: `DevPlatform`, `DevAppEngine`, `TestArea`, and `TestArea2`. Treat the typed wrapper results as the current CLI view, not proof that no platform or area-like objects exist in the galaxy.
## Templates
The galaxy has 45 templates:
| Template | Contained name | Checkout |
| --- | --- | --- |
| `$AnalogDevice` | | `notCheckedOut` |
| `$AppEngine` | | `notCheckedOut` |
| `$Area` | | `notCheckedOut` |
| `$DDESuiteLinkClient` | | `notCheckedOut` |
| `$DelmiaReceiver` | | `notCheckedOut` |
| `$DiscreteDevice` | | `notCheckedOut` |
| `$gAppEngine` | | `notCheckedOut` |
| `$gArea` | | `notCheckedOut` |
| `$gDDESuiteLinkClient` | | `notCheckedOut` |
| `$gMachine` | | `notCheckedOut` |
| `$gUserDefined` | | `notCheckedOut` |
| `$gViewEngine` | | `notCheckedOut` |
| `$gWinPlatform` | | `notCheckedOut` |
| `$InTouchOMI_ViewApp` | | `notCheckedOut` |
| `$InTouchProxy` | | `notCheckedOut` |
| `$InTouchViewApp` | | `notCheckedOut` |
| `$Master_AnalogDevice` | | `notCheckedOut` |
| `$Master_AppEngine` | | `notCheckedOut` |
| `$Master_Area` | | `notCheckedOut` |
| `$Master_DDESuiteLinkClient` | | `notCheckedOut` |
| `$Master_DiscreteDevice` | | `notCheckedOut` |
| `$Master_InTouchProxy` | | `notCheckedOut` |
| `$Master_OPCClient` | | `notCheckedOut` |
| `$Master_RedundantDIObject` | | `notCheckedOut` |
| `$Master_Sequencer` | | `notCheckedOut` |
| `$Master_SQLData` | | `notCheckedOut` |
| `$Master_UserDefined` | | `notCheckedOut` |
| `$Master_ViewEngine` | | `notCheckedOut` |
| `$Master_WinPlatform` | | `notCheckedOut` |
| `$MESReceiver` | | `notCheckedOut` |
| `$OIGW` | | `notCheckedOut` |
| `$OPCClient` | | `notCheckedOut` |
| `$RedundantDIObject` | | `notCheckedOut` |
| `$Sequencer` | | `notCheckedOut` |
| `$Sim` | | `notCheckedOut` |
| `$SQLData` | | `notCheckedOut` |
| `$TestMachine` | | `checkedOutToMe` |
| `$TestMachine.DelmiaReceiver` | `DelmiaReceiver` | `notCheckedOut` |
| `$TestMachine.MESReceiver` | `MESReceiver` | `notCheckedOut` |
| `$TestObject` | | `notCheckedOut` |
| `$TestObject.TestChildObject` | `TestChildObject` | `notCheckedOut` |
| `$UserDefined` | | `notCheckedOut` |
| `$ViewApp` | | `notCheckedOut` |
| `$ViewEngine` | | `notCheckedOut` |
| `$WinPlatform` | | `notCheckedOut` |
## Instances
The instance inventory has 67 objects. Grouped by contained name:
| Contained name | Count | Meaning |
| --- | ---: | --- |
| empty | 24 | 20 `TestMachine_*` parent instances plus `DEV`, `DevAppEngine`, `DevPlatform`, and `DevTestObject`. |
| `DelmiaReceiver` | 20 | One contained receiver under each `TestMachine_001` through `TestMachine_020`. |
| `MESReceiver` | 20 | One contained MES receiver under each `TestMachine_001` through `TestMachine_020`. |
| `TestArea` | 1 | Contained under `DEV`. |
| `TestArea2` | 1 | Contained under `DEV`. |
| `TestChildObject` | 1 | Contained under `DevTestObject`. |
The `TestMachine` instance family is the dominant application model in this galaxy:
| Parent | Delmia child | MES child |
| --- | --- | --- |
| `TestMachine_001` | `TestMachine_001.DelmiaReceiver` / `DelmiaReceiver_001` | `TestMachine_001.MESReceiver` / `MESReceiver_001` |
| `TestMachine_002` | `TestMachine_002.DelmiaReceiver` / `DelmiaReceiver_002` | `TestMachine_002.MESReceiver` / `MESReceiver_002` |
| `TestMachine_003` | `TestMachine_003.DelmiaReceiver` / `DelmiaReceiver_003` | `TestMachine_003.MESReceiver` / `MESReceiver_003` |
| `TestMachine_004` | `TestMachine_004.DelmiaReceiver` / `DelmiaReceiver_004` | `TestMachine_004.MESReceiver` / `MESReceiver_004` |
| `TestMachine_005` | `TestMachine_005.DelmiaReceiver` / `DelmiaReceiver_005` | `TestMachine_005.MESReceiver` / `MESReceiver_005` |
| `TestMachine_006` | `TestMachine_006.DelmiaReceiver` / `DelmiaReceiver_006` | `TestMachine_006.MESReceiver` / `MESReceiver_006` |
| `TestMachine_007` | `TestMachine_007.DelmiaReceiver` / `DelmiaReceiver_007` | `TestMachine_007.MESReceiver` / `MESReceiver_007` |
| `TestMachine_008` | `TestMachine_008.DelmiaReceiver` / `DelmiaReceiver_008` | `TestMachine_008.MESReceiver` / `MESReceiver_008` |
| `TestMachine_009` | `TestMachine_009.DelmiaReceiver` / `DelmiaReceiver_009` | `TestMachine_009.MESReceiver` / `MESReceiver_009` |
| `TestMachine_010` | `TestMachine_010.DelmiaReceiver` / `DelmiaReceiver_010` | `TestMachine_010.MESReceiver` / `MESReceiver_010` |
| `TestMachine_011` | `TestMachine_011.DelmiaReceiver` / `DelmiaReceiver_011` | `TestMachine_011.MESReceiver` / `MESReceiver_011` |
| `TestMachine_012` | `TestMachine_012.DelmiaReceiver` / `DelmiaReceiver_012` | `TestMachine_012.MESReceiver` / `MESReceiver_012` |
| `TestMachine_013` | `TestMachine_013.DelmiaReceiver` / `DelmiaReceiver_013` | `TestMachine_013.MESReceiver` / `MESReceiver_013` |
| `TestMachine_014` | `TestMachine_014.DelmiaReceiver` / `DelmiaReceiver_014` | `TestMachine_014.MESReceiver` / `MESReceiver_014` |
| `TestMachine_015` | `TestMachine_015.DelmiaReceiver` / `DelmiaReceiver_015` | `TestMachine_015.MESReceiver` / `MESReceiver_015` |
| `TestMachine_016` | `TestMachine_016.DelmiaReceiver` / `DelmiaReceiver_016` | `TestMachine_016.MESReceiver` / `MESReceiver_016` |
| `TestMachine_017` | `TestMachine_017.DelmiaReceiver` / `DelmiaReceiver_017` | `TestMachine_017.MESReceiver` / `MESReceiver_017` |
| `TestMachine_018` | `TestMachine_018.DelmiaReceiver` / `DelmiaReceiver_018` | `TestMachine_018.MESReceiver` / `MESReceiver_018` |
| `TestMachine_019` | `TestMachine_019.DelmiaReceiver` / `DelmiaReceiver_019` | `TestMachine_019.MESReceiver` / `MESReceiver_019` |
| `TestMachine_020` | `TestMachine_020.DelmiaReceiver` / `DelmiaReceiver_020` | `TestMachine_020.MESReceiver` / `MESReceiver_020` |
All listed instances were `notCheckedOut` during capture.
## Security
`security info` returned:
| Field | Value |
| --- | --- |
| Authentication mode | `eNone` |
| Login time | `1000` |
| Role update interval | `0` |
The role, user, and group collections returned empty arrays through the CLI.
## Settings And Read Limitations
| Read | Result |
| --- | --- |
| `settings time-master get` | Success, `Instance` was empty. |
| `settings locale get` | Failed with `COMException`: `Exception from HRESULT: 0xC0000005`. |
| `galaxy cdi-version` | Success, `5800.0474.7005.1`. |
Treat failed or empty settings reads as observed CLI/GRAccess results, not necessarily as authoritative absence in the IDE. The local GRAccess COM layer can fail specific property reads while other object queries continue to work.
+320
View File
@@ -0,0 +1,320 @@
# ZB TestMachine Template And Instance Documentation
Captured read-only from galaxy `ZB` on 2026-04-28 with `graccess_cli --llm-json`.
Core commands used:
```powershell
graccess template list --galaxy ZB --pattern '%TestMachine%' --llm-json
graccess instance list --galaxy ZB --pattern '%TestMachine%' --llm-json
graccess object snapshot --galaxy ZB --name '$TestMachine' --type template --llm-json
graccess object lineage --galaxy ZB --name '$TestMachine' --type template --llm-json
graccess object children --galaxy ZB --name '$TestMachine' --type template --llm-json
graccess object snapshot --galaxy ZB --name '$TestMachine.DelmiaReceiver' --type template --llm-json
graccess object snapshot --galaxy ZB --name '$TestMachine.MESReceiver' --type template --llm-json
graccess object snapshot --galaxy ZB --name TestMachine_001 --type instance --llm-json
graccess object snapshot --galaxy ZB --name DelmiaReceiver_001 --type instance --llm-json
graccess object snapshot --galaxy ZB --name MESReceiver_001 --type instance --llm-json
```
## Object Family
`$TestMachine` is a parent template derived from the System Platform `$gMachine` base template. Older captures had blank generic `DerivedFrom` / `BasedOn` fields; newer CLI builds should prefer `object lineage` and `object children` because they use typed GRAccess relationship reads and package fallback where available. The intended IDE inheritance for this object is:
```text
$gMachine
-> $TestMachine
-> $TestMachine.DelmiaReceiver
-> $TestMachine.MESReceiver
```
`$TestMachine.DelmiaReceiver` and `$TestMachine.MESReceiver` are contained embedded templates under `$TestMachine`, not standalone peer templates in the runtime hierarchy. They become contained child objects when a `TestMachine_*` parent instance is instantiated with contained-object creation enabled.
| Object | Relationship | Notes |
| --- | --- | --- |
| `$gMachine` | Base template | System Platform machine template extended by `$TestMachine`. |
| `$TestMachine` | Derived template | Adds machine identity, test alarm, historized value, array, protected-value, and script families. |
| `$TestMachine.DelmiaReceiver` | Contained template | Embedded receiver template with recipe/download attributes and `ProcessRecipe` / `Reset` script families. |
| `$TestMachine.MESReceiver` | Contained template | Embedded receiver template with MES move-in/move-out attributes. |
The family contains two child templates:
| Template | Contained name | Checkout status |
| --- | --- | --- |
| `$TestMachine` | | `checkedOutToMe` |
| `$TestMachine.DelmiaReceiver` | `DelmiaReceiver` | `notCheckedOut` |
| `$TestMachine.MESReceiver` | `MESReceiver` | `notCheckedOut` |
There are 20 parent instances, each with one `DelmiaReceiver` child and one `MESReceiver` child.
| Parent instance range | Child ranges |
| --- | --- |
| `TestMachine_001` through `TestMachine_020` | `DelmiaReceiver_001` through `DelmiaReceiver_020`; `MESReceiver_001` through `MESReceiver_020` |
The child instances use contained hierarchical names, for example:
- `DelmiaReceiver_001` has hierarchical name `TestMachine_001.DelmiaReceiver`.
- `MESReceiver_001` has hierarchical name `TestMachine_001.MESReceiver`.
For future CLI captures, parse inheritance with lineage, children, snapshot fields, and relationship queries:
```powershell
graccess object snapshot --galaxy ZB --name '$TestMachine' --type template --llm-json
graccess object lineage --galaxy ZB --name '$TestMachine' --type template --llm-json
graccess object children --galaxy ZB --name '$TestMachine' --type template --llm-json
graccess object query-condition --galaxy ZB --type all --condition basedOn --value '$gMachine' --llm-json
graccess object query-condition --galaxy ZB --type all --condition derivedOrInstantiatedFrom --value '$gMachine' --llm-json
```
During the original capture, the two relationship queries returned empty arrays through the generic query path, so `$TestMachine -> $gMachine` was verified separately as IDE/SQL verification evidence. Normal CLI usage must not depend on SQL; SQL is only a development verification/debugging oracle.
## Snapshot Summary
| Object | Kind | Attributes | Configurable attributes | Script-like entries | Unavailable fields |
| --- | --- | ---: | ---: | ---: | ---: |
| `$TestMachine` | template | 282 | 282 | 22 | 0 |
| `$TestMachine.DelmiaReceiver` | template | 178 | 178 | 36 | 1 |
| `$TestMachine.MESReceiver` | template | 110 | 110 | 25 | 1 |
| `TestMachine_001` | instance | 282 | 282 | 22 | 0 |
| `DelmiaReceiver_001` | instance | 179 | 179 | 36 | 1 |
| `MESReceiver_001` | instance | 110 | 110 | 25 | 1 |
Earlier snapshots reported package/export fallback failures with:
```text
Library not registered. (Exception from HRESULT: 0x8002801D (TYPE_E_LIBNOTREGISTERED))
```
That was traced to .NET publisher-policy binding to the GAC `ArchestrA.GRAccess` 2.0 interop assembly plus late-bound reflection on `IgObjects.ExportObjects`. Current CLI builds disable that publisher policy and use typed `IgObjects` export calls. `object snapshot --llm-json` now reaches package fallback without a `TYPE_E_LIBNOTREGISTERED` unavailable entry.
Script metadata is readable. Direct script body readback is still not exposed through the generic local GRAccess value path, but the CLI now parses nested exported package archives and binary UTF-16 `ScriptExtension` records. `object scripts get --script UpdateTestChangingInt` and `--script UpdateTestChangingInt.ExecuteText` return the package-backed body:
```text
Me.TestChangingInt = System.Random().Next(1,1000);
```
## `$TestMachine` Attribute Structure
The base template has 282 attributes. Category distribution:
| Category | Count |
| --- | ---: |
| `MxCategoryCalculated` | 65 |
| `MxCategoryWriteable_USC_Lockable` | 46 |
| `MxCategoryPackageOnly` | 29 |
| `MxCategoryCalculatedRetentive` | 27 |
| `MxCategoryWriteable_C_Lockable` | 25 |
| `MxCategory_SystemInternal` | 22 |
| `MxCategoryPackageOnly_Lockable` | 22 |
| `MxCategoryWriteable_US` | 15 |
| `MxCategory_Constant` | 13 |
| `MxCategory_SystemWriteable` | 7 |
Data type distribution:
| Data type | Count |
| --- | ---: |
| `MxString` | 87 |
| `MxBoolean` | 63 |
| `MxInteger` | 37 |
| `MxQualifiedEnum` | 22 |
| `MxBigString` | 18 |
| `MxTime` | 17 |
| `MxQualifiedStruct` | 9 |
| `MxReferenceType` | 9 |
| `MxDataTypeEnum` | 7 |
| `MxFloat` | 5 |
| `MxElapsedTime` | 4 |
| `MxInternationalizedString` | 2 |
| `MxDouble` | 1 |
| `MxStatusType` | 1 |
Security classification distribution:
| Security classification | Count |
| --- | ---: |
| `MxSecurityUndefined` | 192 |
| `MxSecurityOperate` | 28 |
| `MxSecurityFreeAccess` | 24 |
| `MxSecurityViewOnly` | 18 |
| `MxSecurityTune` | 12 |
| `MxSecurityConfigure` | 6 |
| `MxSecuritySecuredWrite` | 1 |
| `MxSecurityVerifiedWrite` | 1 |
## `$TestMachine` Top-Level User Attributes
| Attribute | Data type | Category | Security | Lock |
| --- | --- | --- | --- | --- |
| `MachineCode` | `MxString` | `MxCategoryWriteable_USC_Lockable` | `MxSecurityOperate` | `MxUnLocked` |
| `MachineDescription` | `MxString` | `MxCategoryWriteable_USC_Lockable` | `MxSecurityOperate` | `MxUnLocked` |
| `MachineID` | `MxString` | `MxCategoryWriteable_USC_Lockable` | `MxSecurityOperate` | `MxUnLocked` |
| `TestAlarm001` | `MxBoolean` | `MxCategoryWriteable_USC_Lockable` | `MxSecurityOperate` | `MxUnLocked` |
| `TestAlarm002` | `MxBoolean` | `MxCategoryWriteable_USC_Lockable` | `MxSecurityOperate` | `MxUnLocked` |
| `TestAlarm003` | `MxBoolean` | `MxCategoryWriteable_USC_Lockable` | `MxSecurityOperate` | `MxUnLocked` |
| `ProtectedValue` | `MxBoolean` | `MxCategoryWriteable_USC_Lockable` | `MxSecuritySecuredWrite` | `MxUnLocked` |
| `ProtectedValue1` | `MxBoolean` | `MxCategoryWriteable_USC_Lockable` | `MxSecurityVerifiedWrite` | `MxUnLocked` |
| `TestHistoryValue` | `MxInteger` | `MxCategoryWriteable_USC_Lockable` | `MxSecurityOperate` | `MxUnLocked` |
| `TestStringArray` | `MxString` | `MxCategoryWriteable_USC_Lockable` | `MxSecurityOperate` | `MxUnLocked` |
| `TestIntArray` | `MxInteger` | `MxCategoryWriteable_USC_Lockable` | `MxSecurityOperate` | `MxUnLocked` |
| `TestDateTimeArray` | `MxTime` | `MxCategoryWriteable_USC_Lockable` | `MxSecurityOperate` | `MxUnLocked` |
| `TestBoolArray` | `MxBoolean` | `MxCategoryWriteable_USC_Lockable` | `MxSecurityOperate` | `MxUnLocked` |
| `TestChangingInt` | `MxInteger` | `MxCategoryWriteable_USC_Lockable` | `MxSecurityOperate` | `MxUnLocked` |
The CLI also read normal framework top-level attributes such as `Tagname`, `ShortDesc`, `ScanStateCmd`, `SecurityGroup`, `Area`, `Container`, `Host`, `ConfigVersion`, `ExecutionRelatedObject`, `ExecutionRelativeOrder`, `ContainedName`, and `HierarchicalName`.
Scalar value readback for the user attributes returned "not exposed by this GRAccess attribute" through the current generic value path. Use snapshots for metadata and use export/version-specific adapters when exact IDE-entered values are required.
## `$TestMachine` Attribute Families
Dot-child groups found in the base template:
| Root | Child attribute count | Purpose |
| --- | ---: | --- |
| `MachineCode` | 1 | Description metadata. |
| `MachineDescription` | 1 | Description metadata. |
| `MachineID` | 1 | Description metadata. |
| `TestAlarm001` | 45 | Alarm extension/settings family. |
| `TestAlarm002` | 45 | Alarm extension/settings family. |
| `TestAlarm003` | 45 | Alarm extension/settings family. |
| `TestHistoryValue` | 19 | Historization/settings family. |
| `UpdateTestChangingInt` | 47 | Script object/settings family. |
Alarm families for `TestAlarm001`, `TestAlarm002`, and `TestAlarm003` include:
- Identity and extension metadata: `_ExternalName`, `_InternalName`, `HasStatistics`, `_RefAttrID`, `_ExtensionAttributeDatatypes`, `_ExtensionAttributeCategories`.
- Aggregate status: `AlarmMostUrgentSeverity`, `AlarmMostUrgentMode`, `AlarmMostUrgentAcked`, `AlarmCntsBySeverity`, `AlarmMostUrgentInAlarm`, `AlarmMostUrgentShelved`.
- Runtime status: `InAlarm`, `TimeAlarmOn`, `TimeAlarmOff`, `TimeAlarmAcked`, `Acked`.
- Operator/configuration settings: `Priority`, `Category`, `AckMsg`, `DescAttrName`, `ActiveAlarmState`, `AlarmMode`, `AlarmModeCmd`, `AlarmInhibit`.
- Shelving settings: `AlarmShelveCmd`, `AlarmShelved`, `AlarmShelveStartTime`, `AlarmShelveStopTime`, `AlarmShelveReason`, `AlarmShelveUser`, `AlarmShelveNode`.
- Condition/source settings: `Alarm.TimeDeadband`, `Condition`, `ConditionCached`, `AlarmSourceAttr`.
The `TestHistoryValue` history family includes:
- Identity metadata: `_ExternalName`, `_InternalName`, `_key`, `_refHistAttrKey`, `_ExtensionAttributeDatatypes`, `_ExtensionAttributeCategories`.
- Storage behavior: `ValueDeadBand`, `ForceStoragePeriod`, `InterpolationType`, `RolloverValue`, `SampleCount`, `EnableSwingingDoor`, `RateDeadBand`.
- Display/range metadata: `TrendHi`, `TrendLo`, `EngUnits`, `Hist.DescAttrName`.
The `UpdateTestChangingInt` script family includes:
- Script text fields: `ExecuteText`, `DeclarationsText`, `StartupText`, `ShutdownText`, `OnScanText`, `OffScanText`, `Expression`.
- Package-backed `ExecuteText` body: `Me.TestChangingInt = System.Random().Next(1,1000);`.
- Trigger/execution settings: `TriggerType`, `TriggerPeriod`, `TriggerOnQualityChange`, `DataChangeDeadband`, `RunsAsync`, `ExecuteTimeout.Limit`, `AsyncShutdownCmd`.
- Runtime statistics/status: `StatsReset`, `ErrorCnt`, `ExecutionCnt`, `ExecutionTime`, `ExecutionTimeAvg`, `ExecutionTimeStamp`, `Disabled`, `State`, `_LastExpression`.
- Dependency/reference metadata: `AliasReferences`, `Aliases`, `_ExternalReferences`, `_ExternalReferenceFlags`, `_AliasReferenceFlags`, `_Guid`, `_LibraryDependencies`.
- Error metadata: `ExecutionError.Condition`, `ExecutionError.Alarmed`, `ExecutionError.Desc`, `_ErrorMessage`, `_ErrorLine`, `_ErrorColumn`, `_ErrorReport`.
- Ordering/group metadata: `ScriptExecutionGroup`, `ScriptOrder`, `_ScriptExecutionGroupEnum`.
## `$TestMachine.DelmiaReceiver`
This contained template has 178 attributes and 36 script-like metadata entries.
Main user attributes:
| Attribute | Data type | Category | Security |
| --- | --- | --- | --- |
| `DownloadPath` | `MxString` | `MxCategoryWriteable_USC_Lockable` | `MxSecurityOperate` |
| `JobStepNumber` | `MxString` | `MxCategoryWriteable_USC_Lockable` | `MxSecurityOperate` |
| `PartNumber` | `MxString` | `MxCategoryWriteable_USC_Lockable` | `MxSecurityOperate` |
| `ReadyFlag` | `MxBoolean` | `MxCategoryWriteable_USC_Lockable` | `MxSecurityOperate` |
| `RecipeDownloadFlag` | `MxBoolean` | `MxCategoryWriteable_USC_Lockable` | `MxSecurityOperate` |
| `RecipeProcessedFlag` | `MxBoolean` | `MxCategoryWriteable_USC_Lockable` | `MxSecurityOperate` |
| `RecipeProcessResult` | `MxBoolean` | `MxCategoryWriteable_USC_Lockable` | `MxSecurityOperate` |
| `RecipeProcessResultText` | `MxString` | `MxCategoryWriteable_USC_Lockable` | `MxSecurityOperate` |
| `Username` | `MxString` | `MxCategoryWriteable_USC_Lockable` | `MxSecurityOperate` |
| `WorkOrderNumber` | `MxString` | `MxCategoryWriteable_USC_Lockable` | `MxSecurityOperate` |
Script families:
| Root | Child count | Notable fields |
| --- | ---: | --- |
| `ProcessRecipe` | 47 | `ExecuteText`, `Expression`, startup/shutdown/on-scan/off-scan text, trigger settings, execution stats, timeout, async shutdown. |
| `Reset` | 47 | Same script/settings pattern as `ProcessRecipe`. |
## `$TestMachine.MESReceiver`
This contained template has 110 attributes and 25 script-like metadata entries.
Main user attributes:
| Attribute | Data type |
| --- | --- |
| `MoveInBatchID` | `MxInteger` |
| `MoveInCompleteFlag` | `MxInteger` |
| `MoveInErrorText` | `MxString` |
| `MoveInFlag` | `MxBoolean` |
| `MoveInJobSequenceNumber` | `MxString` |
| `MoveInMesContainerNumber` | `MxString` |
| `MoveInNumberWorkOrders` | `MxInteger` |
| `MoveInOperatorName` | `MxString` |
| `MoveInPartNumbers` | `MxString` |
| `MoveInReadyFlag` | `MxBoolean` |
| `MoveInSuccessFlag` | `MxBoolean` |
| `MoveInWorkOrderNumbers` | `MxString` |
| `MoveOutBatchID` | `MxInteger` |
| `MoveOutFlag` | `MxBoolean` |
| `MoveOutCompleteFlag` | `MxBoolean` |
| `MoveOutErrorText` | `MxString` |
| `MoveOutMesContainerNum` | `MxString` |
| `MoveOutNumberWorkOrders` | `MxInteger` |
| `MoveOutOperatorName` | `MxString` |
| `MoveOutPartNumbers` | `MxString` |
| `MoveOutWorkOrderNumbers` | `MxString` |
| `MoveOutReadyFlag` | `MxBoolean` |
| `MoveOutSuccessfulFlag` | `MxBoolean` |
All listed MES receiver user attributes were `MxCategoryWriteable_USC_Lockable`, `MxSecurityOperate`, and `MxUnLocked` in the snapshot.
## Instance Topology
All parent instances observed:
```text
TestMachine_001
TestMachine_002
TestMachine_003
TestMachine_004
TestMachine_005
TestMachine_006
TestMachine_007
TestMachine_008
TestMachine_009
TestMachine_010
TestMachine_011
TestMachine_012
TestMachine_013
TestMachine_014
TestMachine_015
TestMachine_016
TestMachine_017
TestMachine_018
TestMachine_019
TestMachine_020
```
For each parent, the galaxy contains:
- `DelmiaReceiver_NNN` with hierarchical name `TestMachine_NNN.DelmiaReceiver`.
- `MESReceiver_NNN` with hierarchical name `TestMachine_NNN.MESReceiver`.
Representative instance snapshots:
| Instance | Hierarchical name | Attributes | Scripts | Checkout |
| --- | --- | ---: | ---: | --- |
| `TestMachine_001` | `TestMachine_001` | 282 | 22 | `notCheckedOut` |
| `DelmiaReceiver_001` | `TestMachine_001.DelmiaReceiver` | 179 | 36 | `notCheckedOut` |
| `MESReceiver_001` | `TestMachine_001.MESReceiver` | 110 | 25 | `notCheckedOut` |
The parent instance shape matches `$TestMachine` by count and script metadata. `DelmiaReceiver_001` has one more attribute than `$TestMachine.DelmiaReceiver` in the captured snapshot; the extra field should be treated as instance/runtime metadata until a version-specific diff tool reports the exact delta.
## Operational Notes For LLM Automation
- Prefer `object snapshot --llm-json` for template and instance discovery.
- Use `instance list --pattern '%TestMachine%' --llm-json` for current topology before editing.
- Treat `$TestMachine` as an extension of `$gMachine`; verify with `object lineage` first, then snapshot relationship fields, then package fallback/unavailable details.
- Use `template instantiate --create-contained` when creating `TestMachine_*` instances so `DelmiaReceiver` and `MESReceiver` child objects are created with the parent.
- Edit embedded child objects by targeting the child instance tagname, such as `DelmiaReceiver_001`, or its hierarchical name when a command supports hierarchical lookup.
- Do not mutate `$TestMachine` until the existing checkout state is understood; the template was `checkedOutToMe` during capture.
- Direct scalar value readback for many template attributes is not exposed by the current generic attribute value path.
- Direct script body readback is not exposed by the current generic value path; use `object scripts get --llm-json` package fallback for reads and `object scripts set` for script body attribute writes.
- Extended attributes failed because the local COM type library was not registered.
File diff suppressed because one or more lines are too long
+309
View File
@@ -0,0 +1,309 @@
# GRAccess Operations Reference
Operations supported by the ArchestrA GRAccess library, organized by functional area.
Page references link to sections in `graccess_documentation.md`.
---
## Galaxy Management (IGRAccess — Pages 101104)
| Operation | Method | Description |
|---|---|---|
| Query galaxies | `QueryGalaxies(nodeName)` | List available galaxies on a GR node |
| Create galaxy | `CreateGalaxy(name, nodeName, hasSecurity, authMode, description)` | Create a new galaxy |
| Create from template | `CreateGalaxyFromTemplate(...)` | Create a galaxy from a template |
| Delete galaxy | `DeleteGalaxy(galaxyName, nodeName)` | Delete a galaxy |
## Galaxy Connection & Session (IGalaxy — Pages 3555)
| Operation | Method | Description |
|---|---|---|
| Login | `Login(userName, password)` | Log in with forced client sync |
| Login (extended) | `LoginEx(userName, password, forceSynchronization)` | Log in with optional deferred sync |
| Synchronize | `SynchronizeClient()` | Sync client with server |
| Logout | `Logout()` | Log out from the galaxy |
| Get version | `VersionString` / `VersionNumber` | Retrieve galaxy version |
| Check upgrade needed | `UpgradeRequired` | Check if galaxy needs upgrade |
| Migrate galaxy | `MigrateGalaxy(galaxyName, grNodeName)` | Upgrade a galaxy |
## Galaxy Backup & Restore (IGalaxy — Pages 3940, 4748)
| Operation | Method | Description |
|---|---|---|
| Backup | `Backup(processId, backupFile, nodeName, galaxyName)` | Back up a galaxy to file |
| Restore | `Restore(processId, backupFile, nodeName, galaxyName, restoreOlder)` | Restore a galaxy from backup |
## Object Queries (IGalaxy — Pages 3738, 4445, 5152)
| Operation | Method | Description |
|---|---|---|
| Query by name | `QueryObjectsByName(templateOrInstance, tagnames[])` | Find objects by tagname array |
| Query by condition | `QueryObjects(templateOrInstance, conditionType, value, matchCondition)` | Find objects by single condition |
| Query multi-condition | `QueryObjectsMultiCondition(templateOrInstance, conditions)` | Find objects by multiple conditions |
| Create conditions | `CreateConditionsObject()` | Create an IConditions for multi-condition queries |
### Condition (ICondition — Page 29, IConditions — Pages 3032)
| Property/Method | Description |
|---|---|
| `Kind` | Condition type (EConditionType) |
| `Value` | Value to search for |
| `Negation` | Negate the expression |
| `Add(condition)` | Add a condition to the collection |
| `Remove(index)` | Remove a condition |
| `Join(condition)` | Join two conditions |
## Template Operations (ITemplate — Pages 182207)
| Operation | Method | Description |
|---|---|---|
| Create instance | `CreateInstance(name, createContainedObjects)` | Create a single instance from template |
| Create instances | `CreateInstances(tagnames[], createContainedObjects)` | Create multiple instances from template |
| Create derived template | `CreateTemplate(name, createContainedObjects)` | Derive a new template |
| Delete template | `DeleteTemplate(forceOption)` | Delete the template |
| Check out | `CheckOut()` | Check out for editing |
| Check in | `CheckIn(comment)` | Check in with comment |
| Undo check out | `UndoCheckOut()` | Discard checkout changes |
| Save | `Save()` | Save after configuration |
| Unload | `Unload()` | Release object cache |
| Add UDA | `AddUDA(name, dataType, category, security, isArray, arrayCount)` | Add a user-defined attribute |
| Delete UDA | `DeleteUDA(name)` | Delete a UDA |
| Rename UDA | `RenameUDA(oldName, newName)` | Rename a UDA |
| Update UDA | `UpdateUDA(name, dataType, category, security, isArray, arrayCount)` | Modify a UDA |
| Add extension primitive | `AddExtensionPrimitive(type, name, isObjectExtension)` | Add an extension primitive |
| Delete extension primitive | `DeleteExtensionPrimitive(type, name)` | Delete an extension primitive |
| Rename extension primitive | `RenameExtensionPrimitive(oldName, newName)` | Rename an extension primitive |
| Get extended attributes | `GetExtendedAttributes(attrName, upToLevel, categories[])` | Get attributes across hierarchy |
| Get help URL | `GetObjectHelpURL()` | Get object help URL |
### Template Properties (ITemplate — Pages 182207)
| Property | Description |
|---|---|
| `Tagname` | Object tagname (read/write) |
| `ContainedName` | Contained name (read/write) |
| `HierarchicalName` | Full hierarchical name (read-only) |
| `DerivedFrom` | Parent template name |
| `BasedOn` | Root ancestor template name |
| `Category` | Object category (ECATEGORY) |
| `CategoryGUID` | Category GUID |
| `Container` | Container object (read/write) |
| `Area` | Area object (read/write) |
| `Host` | Host assignment (read/write) |
| `Toolset` | Toolset membership (read/write) |
| `Attributes` | All attributes collection |
| `ConfigurableAttributes` | Editable attributes collection |
| `ConfigVersion` | Configuration version number |
| `CheckoutStatus` | Checkout status (ECheckoutStatus) |
| `CheckedOutBy` | User who checked out |
| `EditStatus` | Edit status (EEditStatus) |
| `ValidationStatus` | Validation status (EPACKAGESTATUS) |
| `Errors` | Validation error list |
| `Warnings` | Validation warning list |
| `CommandResult` | Last operation result |
| `CommandResults` | Last batch operation results |
## Instance Operations (IInstance — Pages 104133)
| Operation | Method | Description |
|---|---|---|
| Delete instance | `DeleteInstance()` | Delete this instance |
| Deploy | `Deploy()` | Deploy the instance |
| Deploy (extended) | `DeployEx(...)` | Deploy with extended options |
| Undeploy | `Undeploy()` | Undeploy the instance |
| Undeploy (extended) | `UndeployEx(...)` | Undeploy with extended options |
| Upload | `Upload()` | Upload runtime config changes |
| Upload (extended) | `UploadEx(...)` | Upload with extended options |
| Check out | `CheckOut()` | Check out for editing |
| Check in | `CheckIn(comment)` | Check in with comment |
| Undo check out | `UndoCheckOut()` | Discard checkout changes |
| Save | `Save()` | Save after configuration |
| Unload | `Unload()` | Release object cache |
| Add UDA | `AddUDA(name, dataType, category, security, isArray, arrayCount)` | Add a user-defined attribute |
| Delete UDA | `DeleteUDA(name)` | Delete a UDA |
| Rename UDA | `RenameUDA(oldName, newName)` | Rename a UDA |
| Update UDA | `UpdateUDA(name, dataType, category, security, isArray, arrayCount)` | Modify a UDA |
| Add extension primitive | `AddExtensionPrimitive(type, name, isObjectExtension)` | Add an extension primitive |
| Delete extension primitive | `DeleteExtensionPrimitive(type, name)` | Delete an extension primitive |
| Rename extension primitive | `RenameExtensionPrimitive(oldName, newName)` | Rename an extension primitive |
| Get extended attributes | `GetExtendedAttributes(attrName, upToLevel, categories[])` | Get attributes across hierarchy |
| Get help URL | `GetObjectHelpURL()` | Get object help URL |
### Instance Properties (IInstance — Pages 104133)
All properties from ITemplate above, plus:
| Property | Description |
|---|---|
| `DeployedVersion` | Config version at last deploy |
| `DeploymentStatus` | Deploy status (EDeploymentStatus) |
## Bulk Object Operations (IgObjects — Pages 87101)
| Operation | Method | Description |
|---|---|---|
| Check out (bulk) | `CheckOut()` | Check out all objects in collection |
| Check in (bulk) | `CheckIn(comment)` | Check in all objects |
| Undo check out (bulk) | `UndoCheckOut()` | Undo checkout for all objects |
| Deploy (bulk) | `Deploy()` | Deploy all instances in collection |
| Deploy (bulk extended) | `DeployEx(...)` | Deploy with extended options |
| Undeploy (bulk) | `Undeploy()` | Undeploy all instances |
| Undeploy (bulk extended) | `UndeployEx(...)` | Undeploy with extended options |
| Upload (bulk) | `Upload()` | Upload runtime changes for all |
| Upload (bulk extended) | `UploadEx(...)` | Upload with extended options |
| Delete all | `DeleteAllObjects()` | Delete all objects in collection |
| Export objects | `ExportObjects(exportType, outputFile)` | Export objects from galaxy |
| Export as protected | `ExportObjectsAsProtected(...)` | Export as protected |
| Add to collection | `Add(gObject)` | Add an object to the list |
| Add from collection | `AddFromCollection(gObjects)` | Merge another collection |
| Set area (bulk) | `Area` (set) | Set area for all objects |
| Set host (bulk) | `Host` (set) | Set host for all objects |
| Set container (bulk) | `Container` (set) | Set container for all objects |
| Set security group (bulk) | `SecurityGroup` (set) | Set security group for all objects |
## Attribute Operations (IAttribute — Pages 1320, IAttributes — Pages 2124)
### IAttribute
| Operation | Method/Property | Description |
|---|---|---|
| Get value | `Value` | Get the attribute value (IMxValue) |
| Set value | `SetValue(mxValue)` | Set the attribute value |
| Get name | `Name` | Attribute name |
| Get data type | `DataType` | Attribute data type (MxDataType) |
| Get category | `AttributeCategory` | Attribute category |
| Get security | `SecurityClassification` | Security classification |
| Set security | `SetSecurityClassification(classification)` | Change security classification |
| Get locked state | `Locked` | Whether attribute is locked |
| Set locked state | `SetLocked(locked)` | Lock/unlock the attribute |
| Get has buffer | `HasBuffer` | Whether attribute has a buffer |
| Set has buffer | `SetHasBuffer(hasBuffer)` | Set buffer flag |
| Get array bound | `UpperBoundDim1` | Upper bound of first dimension |
| Get set handlers | `RtSethandler` / `CfgSethandler` | Runtime/config set handlers |
### IAttributes Collection
| Property | Description |
|---|---|
| `Item[nameOrIndex]` | Get attribute by name or 1-based index |
| `Count` | Number of attributes |
| `ShortDescription` | Short description of the object |
| `ExecutionOrder` | Execution order name |
| `SecurityGroup` | Security group name |
| `ExecutionRelatedObject` | Execution related object name |
## Import / Export (IGalaxy — Pages 4344, 5255)
| Operation | Method | Description |
|---|---|---|
| Import objects | `ImportObjects(inputFile, overwritesAllowed)` | Import objects from file |
| Import objects (extended) | `ImportObjectsEx(inputFile, versionConflict, nameConflict, appendName)` | Import with conflict resolution |
| Import script library | `ImportScriptLib(path)` | Import a script library |
| Export all | `ExportAll(exportType, outputFile)` | Export all galaxy objects |
| GR Load | `GRLoad(csvFilePath, grLoadMode)` | Load galaxy from CSV file |
## Toolset Management (IToolset — Pages 207209, IToolsets — Pages 210213)
| Operation | Method | Description |
|---|---|---|
| Query toolsets | `galaxy.QueryToolsets()` | Get all toolsets |
| Query toolsets (extended) | `galaxy.QueryToolsetsEx(folderType)` | Get toolsets by folder type |
| Add toolset | `toolsets.Add(name)` | Add a new toolset |
| Add child toolset | `toolsets.AddToolSet(name, parentToolset)` | Add toolset under parent |
| Delete toolset | `toolsets.DeleteToolSet(name)` | Delete a toolset |
| Rename toolset | `toolset.Rename(newName)` | Rename a toolset |
| Move toolset | `toolset.MoveToToolset(newParent)` | Move to different parent |
| Get children | `toolset.GetChildToolsets()` | Get child toolsets |
## Script Library Management (IScriptLibrary — Pages 175177, IScriptLibraries — Pages 173175)
| Operation | Method | Description |
|---|---|---|
| Query script libraries | `galaxy.QueryScriptLibraries()` | Get all script libraries |
| Add script library | `scriptLibraries.Add(name)` | Add a new script library |
| Export script library | `scriptLibrary.Export(path)` | Export a script library to file |
| Get name | `scriptLibrary.Name` | Script library name |
## Security (IGalaxySecurity — Pages 5962)
| Operation | Method/Property | Description |
|---|---|---|
| Get security settings | `galaxy.GetSecuritySettings()` | Get IGalaxySecurity object |
| Get read-only security | `galaxy.GetReadOnlySecurity()` | Get security in read-only mode |
| Authentication mode | `security.AuthenticationMode` | Galaxy auth mode |
| Available roles | `security.RolesAvailable` | Collection of configured roles |
| Available users | `security.UsersAvailable` | Collection of configured users |
| Available security groups | `security.SecurityGroupsAvailable` | Collection of security groups |
| Login time | `security.LoginTime` | Configured login timeout |
| Role update interval | `security.RoleUpdateInterval` | Role update interval |
### Galaxy Roles (IGalaxyRole — Pages 5557, IGalaxyRoles — Pages 5759)
| Property | Description |
|---|---|
| `RoleName` | Role name |
| `AccessLevel` | Access level |
| `Permissions` | General permissions (IPermissions) |
| `OperationalPermissions` | Operational permissions (IPermissions) |
### Galaxy Users (IGalaxyUser — Pages 6264, IGalaxyUsers — Pages 6465)
| Property | Description |
|---|---|
| `UserName` | `domain\username` |
| `FullName` | `first last` |
| `AssociatedRoles` | Roles collection |
### Permissions (IPermission — Pages 167171, IPermissions — Pages 171172)
| Property | Description |
|---|---|
| `PermissionName` | Permission name |
| `PermissionParentName` | Parent permission name |
| `IsConfigured` | Whether permission is active |
| `HasChildren` | Whether it has child permissions |
| `ChildPermissions` | Child permissions collection |
| `IsSecurityGroup` | Whether it's a security group |
| `SecurityGroup` | Associated security group |
### Security Groups (ISecurityGroup — Pages 177178, ISecurityGroups — Pages 178179)
| Property | Description |
|---|---|
| `GroupName` | Security group name |
| `gObjects` | Objects in this security group |
## Settings (ISettings — Pages 180182)
| Operation | Method | Description |
|---|---|---|
| Get settings | `galaxy.GetLocaleSettings()` / `galaxy.GetTimeMasterSettings()` | Retrieve settings |
| Get instance | `settings.Instance` | Get the instance for configuring |
| Save & close | `settings.Close()` | Save and check in settings |
| Cancel | `settings.Cancel()` | Cancel without saving |
## User Defaults (IGalaxy — Pages 4547)
| Operation | Method | Description |
|---|---|---|
| Get user defaults | `GetUserDefaults(userDefault)` | Get logged-in user's defaults |
| Set user defaults | `SetUserDefaults(userDefault, value)` | Set user defaults |
## Error Checking (ICommandResult — Pages 2526, ICommandResults — Pages 2628)
| Property | Description |
|---|---|
| `Successful` | Whether the operation succeeded |
| `Text` | Description of the result code |
| `ID` | Result reason code (EGRCommandResult) |
| `CustomMessage` | Custom message |
| `CompletelySuccessful` | (ICommandResults) All results successful |
| `Count` | (ICommandResults) Number of results |
| `Item[index]` | (ICommandResults) Get result by index |
## Utility (IGalaxy — Pages 43, 5051)
| Operation | Method | Description |
|---|---|---|
| Create empty collection | `CreategObjectCollection()` | Create an empty IgObjects collection |
| Get CDI version | `CdiVersionString` | CDI version string |
Binary file not shown.
@@ -0,0 +1,327 @@
# Requirements — fix mutation-path COM lifecycle
Authored 2026-04-29; updated 2026-04-30 after retest with the typelib
fix in place. The original `TYPE_E_LIBNOTREGISTERED` error has been
**resolved**`objects checkout` now succeeds. A second mutation-path
defect has been uncovered in the same retest. This doc covers both;
the typelib regression is preserved as historical context (✅ shipped)
and the new defect is the active blocker.
## ✅ Already fixed (2026-04-30)
| Command | Mode | Status |
|---|---|---|
| `objects checkout` | session + one-shot | ✅ `CheckOut: True` |
| `objects checkin` | session + one-shot | ✅ `CheckIn: True` |
| `object uda add` (first call) | session + one-shot | ✅ `Add UDA: OK` (but see active defect below) |
The publisher-policy + typed-call fix used for `object snapshot` is now
applied to the mutation surface. Thank you.
## 🔴 Active defect — `object uda add` doesn't `Save` + `CheckIn`
### Symptom
After a successful `object uda add`, the template ends up in a
phantom checked-out state that persists across CLI processes and
across helper-process kills:
```text
$cli session start --node localhost --galaxy ZB
$cli template derive --galaxy ZB --name '$UserDefined' --new-name '$X' --confirm --confirm-target '$UserDefined'
# CreateTemplate: OK
$cli objects checkout --galaxy ZB --type template --name '$X' --confirm --confirm-target '$X'
# CheckOut: True
$cli object uda add --galaxy ZB --type template --name '$X' --uda A --data-type MxFloat --category MxCategoryWriteable_USC --security MxSecurityOperate --confirm --confirm-target '$X'
# Add UDA: OK ← lies; nothing persists
$cli object uda add --galaxy ZB --type template --name '$X' --uda B --data-type MxFloat --category MxCategoryWriteable_USC --security MxSecurityTune --confirm --confirm-target '$X'
# Error: Add UDA failed: ID=cmdObjectIsCheckedOutToSomeoneElse (302)
```
### Two consequences
1. **The "Add UDA: OK" return is a lie.** Subsequent
`object snapshot --llm-json` on the same template shows
`"AttributeValues": []` and `"CheckoutStatus": "notCheckedOut"`. The
first AddUDA's mutation never persisted in the export-package
fallback the snapshot reads from.
2. **The phantom checkout survives process death.** Killing all
`GRAccessApp.exe` helpers (12+ accumulated over an hour of testing)
does not release the lock. `object snapshot` still says
`notCheckedOut` while AddUDA still says `cmdObjectIsCheckedOutToSomeoneElse`.
The two views are inconsistent — likely the GR repo's checkout
table holds a row that the AddUDA path checks but the snapshot path
doesn't.
### Root cause hypothesis
`GRAccessCommandDispatcher.cs:507-515` (the `case "add":` arm of
`ExecuteUda`) calls `obj.AddUDA(...)` directly:
```csharp
case "add":
obj.AddUDA(
Arg(args, "uda"),
EnumValue<MxDataType>(Arg(args, "data-type")),
UdaCategory(Arg(args, "category")),
EnumValue<MxSecurityClassification>(Arg(args, "security")),
BoolArg(args, "is-array"),
IntArg(args, "array-count", 0));
return CommandSummary(obj, "Add UDA");
```
There is no `obj.Save()` and no implicit checkin. The GRAccess
mutation lifecycle is:
```
CheckOut → AddUDA / DeleteUDA / etc. → Save → CheckIn
```
Skipping `Save` leaves the change in the helper process's COM-memory
only — when that process dies, the change vanishes. Skipping
`CheckIn` keeps the GR repo's checkout row alive past the process
death, which is what blocks every subsequent mutation.
The same defect almost certainly affects:
- `object uda delete`, `object uda rename`, `object uda update`
- `object extension add`, `object extension delete`, `object extension rename`
- `object attribute set`, `object attribute security`, `object attribute lock`, `object attribute buffer`
- `object scripts set`
- Anything else under `ExecuteUda` / `ExecuteAttribute` / `ExecuteExtension` / `ExecuteScript` that mutates without an explicit Save+CheckIn.
### Recommended fix
Two options, in order of preference:
#### Option A — wrap each mutation in atomic CheckOut + Save + CheckIn
The dispatcher takes responsibility for the lifecycle:
```csharp
case "add":
obj.CheckOut();
try
{
obj.AddUDA(
Arg(args, "uda"),
EnumValue<MxDataType>(Arg(args, "data-type")),
UdaCategory(Arg(args, "category")),
EnumValue<MxSecurityClassification>(Arg(args, "security")),
BoolArg(args, "is-array"),
IntArg(args, "array-count", 0));
obj.Save();
obj.CheckIn(string.Empty);
}
catch
{
try { obj.UndoCheckOut(); } catch { /* best effort */ }
throw;
}
return CommandSummary(obj, "Add UDA");
```
This makes each `object uda add` self-contained — no `objects checkout`
required from the caller. Mirrors the GUI's Edit→Save→Check-in flow.
**Downside:** breaks the "do many edits, then one CheckIn" batch
pattern that the existing `objects checkout` + `objects checkin`
commands suggest is supported.
#### Option B — keep the batch model, add Save only
The dispatcher keeps the explicit-CheckOut-and-CheckIn workflow but
calls `Save()` after each mutation so the change is persisted even
within an open checkout:
```csharp
case "add":
obj.AddUDA(...);
obj.Save();
return CommandSummary(obj, "Add UDA");
```
Caller is still expected to do `objects checkout` once, run all the
mutations, then `objects checkin`. The phantom-checkout symptom goes
away on process exit because the explicit `objects checkin` always
runs the `CheckIn`. **Downside:** the "first AddUDA OK then second
fails" pattern means `obj.Save()` mid-checkout might itself release
the checkout — needs verification against the live GRAccess.
I recommend **Option A** for `object uda add` and the other
single-shot mutation commands, and keeping the explicit
`objects checkout` / `objects checkin` for callers that genuinely want
the batch model. That way `graccess object uda add ...` "just works"
in a script without a checkout dance.
### Acceptance criteria (re-stated)
The script in the original "Acceptance criteria" section below must
run end-to-end without phantom-checkout failures. Repeat-runs (delete
the sandbox, re-derive, re-add UDAs) must produce identical results.
A `object snapshot --llm-json` after the AddUDA must include the new
UDA in `AttributeValues` (or wherever the dispatcher surfaces them in
its snapshot).
## ✅ Default-value fixes (2026-04-30)
The two bad-default complaints from the 2026-04-29 doc are still
worth tracking — please verify these in the same fix:
### `--category` default for `object uda add`
Currently `MxCategoryWriteable_C`, which is **not** in the
`MxAttributeCategory` enum. Valid members:
```
MxCategoryWriteable_U
MxCategoryWriteable_S
MxCategoryWriteable_US
MxCategoryWriteable_UC
MxCategoryWriteable_USC ← recommended new default
MxCategoryWriteable_UC_Lockable
MxCategoryWriteable_USC_Lockable
MxCategoryWriteable_C_Lockable
```
### `--force-option` default for `template delete`
Currently `galaxy_DeleteIfNoInstances`, which is not in the
`EForceDeleteTemplateOption` enum. Valid members:
```
dontForceTemplateDelete ← recommended new default
cascadeDeleteDontUndeploy
```
## 🚿 Helper process leak (lower priority)
Each CLI invocation spawns a `GRAccessApp.exe` helper that survives
the CLI process. After 12+ invocations during a single rig-setup
session there were 12 stale helpers consuming memory. They don't
appear to cause bugs (the phantom checkout persists even after they
are killed), but they do leak.
The CLI should `Marshal.ReleaseComObject` / `Marshal.FinalReleaseComObject`
the GRAccess COM root on exit, which should let the helper terminate.
## Acceptance script
Repeated from the 2026-04-29 doc — this script must run end-to-end on
this dev box's live `ZB` galaxy after both the typelib + lifecycle
fixes are in:
```powershell
$cli = "C:\Users\dohertj2\Desktop\graccess\graccess_cli\src\ZB.MOM.WW.GRAccess.Cli\bin\x86\Debug\net48\ZB.MOM.WW.GRAccess.Cli.exe"
# 1. Derive sandbox template from $UserDefined
& $cli template derive --node localhost --galaxy ZB `
--name '$UserDefined' --new-name '$OtOpcUaParityTest' `
--confirm --confirm-target '$UserDefined'
# 2. Add four UDAs covering the parity matrix's write classifications
# (Option A: each call is self-contained — no separate checkout/checkin)
& $cli object uda add --node localhost --galaxy ZB `
--type template --name '$OtOpcUaParityTest' `
--uda OperateValue --data-type MxFloat `
--category MxCategoryWriteable_USC --security MxSecurityOperate `
--confirm --confirm-target '$OtOpcUaParityTest'
& $cli object uda add --node localhost --galaxy ZB `
--type template --name '$OtOpcUaParityTest' `
--uda TuneValue --data-type MxFloat `
--category MxCategoryWriteable_USC --security MxSecurityTune `
--confirm --confirm-target '$OtOpcUaParityTest'
& $cli object uda add --node localhost --galaxy ZB `
--type template --name '$OtOpcUaParityTest' `
--uda ConfigValue --data-type MxFloat `
--category MxCategoryWriteable_C_Lockable --security MxSecurityConfigure `
--confirm --confirm-target '$OtOpcUaParityTest'
& $cli object uda add --node localhost --galaxy ZB `
--type template --name '$OtOpcUaParityTest' `
--uda Counter --data-type MxInteger `
--category MxCategoryWriteable_USC --security MxSecurityFreeAccess `
--confirm --confirm-target '$OtOpcUaParityTest'
# 3. Add an analog-limit alarm extension on Counter and a history extension
# on OperateValue
& $cli object extension add --node localhost --galaxy ZB `
--type template --name '$OtOpcUaParityTest' `
--extension-type AnalogLimitAlarm --primitive Counter `
--object-extension `
--confirm --confirm-target '$OtOpcUaParityTest'
& $cli object extension add --node localhost --galaxy ZB `
--type template --name '$OtOpcUaParityTest' `
--extension-type HistoryExtension --primitive OperateValue `
--object-extension `
--confirm --confirm-target '$OtOpcUaParityTest'
# 4. Verify state — must show all 4 UDAs and both extensions
& $cli object snapshot --node localhost --galaxy ZB `
--name '$OtOpcUaParityTest' --llm-json
# 5. Instantiate, assign, deploy
& $cli template instantiate --node localhost --galaxy ZB `
--name '$OtOpcUaParityTest' --new-name 'OtOpcUaParityTest_001' `
--confirm --confirm-target '$OtOpcUaParityTest'
& $cli instance assign-area --node localhost --galaxy ZB `
--name 'OtOpcUaParityTest_001' --area 'DEV' `
--confirm --confirm-target 'OtOpcUaParityTest_001'
& $cli instance assign-engine --node localhost --galaxy ZB `
--name 'OtOpcUaParityTest_001' --engine 'DevAppEngine' `
--confirm --confirm-target 'OtOpcUaParityTest_001'
& $cli instance deploy --node localhost --galaxy ZB `
--name 'OtOpcUaParityTest_001' `
--confirm --confirm-target 'OtOpcUaParityTest_001'
# 6. Cleanup (verify the delete path too)
& $cli instance delete --node localhost --galaxy ZB `
--name 'OtOpcUaParityTest_001' `
--confirm --confirm-target 'OtOpcUaParityTest_001'
& $cli template delete --node localhost --galaxy ZB `
--name '$OtOpcUaParityTest' `
--force-option dontForceTemplateDelete `
--confirm --confirm-target '$OtOpcUaParityTest'
```
Pass criteria:
1. Every command exits with status 0.
2. Step 4's snapshot shows `OperateValue`, `TuneValue`, `ConfigValue`,
`Counter` in `AttributeValues`, and the two extensions in
`Extensions` (or wherever the dispatcher surfaces them).
3. Step 5's deploy succeeds and the instance is reachable from MxAccess
(verifiable via the lmxopcua parity tests, or a manual
`aaWindowViewer` browse).
4. Repeat the script — second run after cleanup must produce the same
outputs (idempotency).
## Verification environment
- Windows 10, dev box `DESKTOP-6JL3KKO`
- ArchestrA System Platform installed with live `ZB` galaxy
(single deployed `$WinPlatform` named `DevPlatform`, engine
`DevAppEngine`, area `DEV`)
- `OtOpcUaGalaxyHost` Windows service running. Stop it before deploys
if MxAccess sessions hold the platform; otherwise leave it running.
- graccess-cli x86 build at the path above.
## Consumer of this fix
The lmxopcua repo's parity rig
(`docs/v2/Galaxy.ParityRig.md` in the lmxopcua repo, branch
`v2-mxgw-integration`) wants to provision the sandbox from a script so
the seven scenario classes in `Driver.Galaxy.ParityTests` exercise
real attributes instead of skipping. The 17-test parity matrix is the
gate for retiring the legacy Galaxy.Host backend. Fixing the mutation
lifecycle here unblocks that workflow on every parity rig (dev box +
customer rigs) without requiring GUI clicks.
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8" />
</startup>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="ArchestrA.GRAccess" publicKeyToken="23106a86e706d0ae" culture="neutral" />
<publisherPolicy apply="no" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>
@@ -0,0 +1,819 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using ArchestrA.GRAccess;
using CliFx;
using CliFx.Attributes;
using CliFx.Infrastructure;
using ZB.MOM.WW.GRAccess.Cli.GRAccess;
using ZB.MOM.WW.GRAccess.Cli.Infrastructure;
namespace ZB.MOM.WW.GRAccess.Cli.Commands
{
public abstract class RoutedCommandBase : ICommand
{
[CommandOption("galaxy", 'g', Description = "Galaxy name", IsRequired = true)]
public string GalaxyName { get; init; }
[CommandOption("node", 'n', Description = "GR node name. Required for one-shot mode; ignored when a session is active.")]
public string NodeName { get; init; } = "";
[CommandOption("json", Description = "Output as JSON")]
public bool Json { get; init; }
[CommandOption("llm-json", Description = "Output stable machine JSON envelope")]
public bool LlmJson { get; init; }
public abstract string Command { get; }
public abstract string Subcommand { get; }
public virtual Dictionary<string, object> Args() => new Dictionary<string, object>
{
["json"] = Json,
["llm-json"] = LlmJson
};
public async ValueTask ExecuteAsync(IConsole console)
{
var args = Args();
await CommandRouter.ExecuteAsync(
console,
GalaxyName,
NodeName,
Command,
Subcommand,
args,
galaxy => GRAccessCommandDispatcher.Execute(galaxy, Command, Subcommand, args))
.ConfigureAwait(false);
}
}
public abstract class ConfirmedRoutedCommandBase : RoutedCommandBase
{
[CommandOption("confirm", Description = "Required for mutating commands")]
public bool Confirm { get; init; }
[CommandOption("confirm-target", Description = "Required exact target for mutating/destructive commands")]
public string ConfirmTarget { get; init; } = "";
[CommandOption("dry-run", Description = "Validate a mutating command without invoking mutating GRAccess calls")]
public bool DryRun { get; init; }
public override Dictionary<string, object> Args()
{
var args = base.Args();
args["confirm"] = Confirm;
args["confirm-target"] = ConfirmTarget;
args["dry-run"] = DryRun;
return args;
}
}
public abstract class ObjectCommandBase : ConfirmedRoutedCommandBase
{
[CommandOption("name", Description = "Object tagname", IsRequired = true)]
public string ObjectName { get; init; }
[CommandOption("type", 't', Description = "Object type: all, template, instance")]
public string Type { get; init; } = "all";
public override Dictionary<string, object> Args()
{
var args = base.Args();
args["name"] = ObjectName;
args["type"] = Type;
return args;
}
}
public abstract class BulkObjectsCommandBase : ConfirmedRoutedCommandBase
{
[CommandOption("name", Description = "Object tagname. May be repeated.")]
public IReadOnlyList<string> Names { get; init; } = Array.Empty<string>();
[CommandOption("pattern", 'p', Description = "GRAccess name pattern. May be repeated. Use % as wildcard.")]
public IReadOnlyList<string> Patterns { get; init; } = Array.Empty<string>();
[CommandOption("type", 't', Description = "Object type: all, template, instance")]
public string Type { get; init; } = "instance";
public override Dictionary<string, object> Args()
{
var args = base.Args();
args["name"] = Names;
args["pattern"] = Patterns;
args["type"] = Type;
return args;
}
}
[Command("galaxy info", Description = "Show galaxy version and status")]
public sealed class GalaxyInfoCommand : RoutedCommandBase { public override string Command => "galaxy"; public override string Subcommand => "info"; }
[Command("galaxy sync", Description = "Synchronize the local client with the galaxy")]
public sealed class GalaxySyncCommand : RoutedCommandBase { public override string Command => "galaxy"; public override string Subcommand => "sync"; }
[Command("galaxy cdi-version", Description = "Show galaxy CDI version")]
public sealed class GalaxyCdiVersionCommand : RoutedCommandBase { public override string Command => "galaxy"; public override string Subcommand => "cdi-version"; }
[Command("galaxy defaults get", Description = "Get a user default")]
public sealed class GalaxyDefaultsGetCommand : RoutedCommandBase
{
public override string Command => "galaxy";
public override string Subcommand => "defaults-get";
[CommandOption("default", Description = "EUserDefault enum name", IsRequired = true)]
public string Default { get; init; }
public override Dictionary<string, object> Args() { var args = base.Args(); args["default"] = Default; return args; }
}
[Command("galaxy defaults set", Description = "Set a user default")]
public sealed class GalaxyDefaultsSetCommand : ConfirmedRoutedCommandBase
{
public override string Command => "galaxy";
public override string Subcommand => "defaults-set";
[CommandOption("default", Description = "EUserDefault enum name", IsRequired = true)]
public string Default { get; init; }
[CommandOption("value", Description = "Default value", IsRequired = true)]
public string Value { get; init; }
public override Dictionary<string, object> Args() { var args = base.Args(); args["default"] = Default; args["value"] = Value; return args; }
}
[Command("galaxy backup", Description = "Back up a galaxy")]
public sealed class GalaxyBackupCommand : ConfirmedRoutedCommandBase
{
public override string Command => "galaxy";
public override string Subcommand => "backup";
[CommandOption("file", Description = "Backup file path", IsRequired = true)]
public string File { get; init; }
public override Dictionary<string, object> Args() { var args = base.Args(); args["file"] = File; args["node"] = NodeName; return args; }
}
[Command("galaxy restore", Description = "Restore a galaxy backup")]
public sealed class GalaxyRestoreCommand : ConfirmedRoutedCommandBase
{
public override string Command => "galaxy";
public override string Subcommand => "restore";
[CommandOption("file", Description = "Backup file path", IsRequired = true)]
public string File { get; init; }
[CommandOption("restore-older", Description = "Allow restoring an older version")]
public bool RestoreOlder { get; init; }
public override Dictionary<string, object> Args() { var args = base.Args(); args["file"] = File; args["node"] = NodeName; args["restore-older"] = RestoreOlder; return args; }
}
[Command("galaxy migrate", Description = "Migrate or upgrade a galaxy")]
public sealed class GalaxyMigrateCommand : ConfirmedRoutedCommandBase { public override string Command => "galaxy"; public override string Subcommand => "migrate"; public override Dictionary<string, object> Args() { var args = base.Args(); args["node"] = NodeName; return args; } }
[Command("galaxy import-objects", Description = "Import objects from an aaPKG file")]
public sealed class GalaxyImportObjectsCommand : ConfirmedRoutedCommandBase
{
public override string Command => "galaxy";
public override string Subcommand => "import-objects";
[CommandOption("file", Description = "Input file path", IsRequired = true)]
public string File { get; init; }
[CommandOption("overwrite", Description = "Allow overwrites")]
public bool Overwrite { get; init; }
public override Dictionary<string, object> Args() { var args = base.Args(); args["file"] = File; args["overwrite"] = Overwrite; return args; }
}
[Command("galaxy import-objects-ex", Description = "Import objects with conflict resolution")]
public sealed class GalaxyImportObjectsExCommand : ConfirmedRoutedCommandBase
{
public override string Command => "galaxy";
public override string Subcommand => "import-objects-ex";
[CommandOption("file", Description = "Input file path", IsRequired = true)]
public string File { get; init; }
[CommandOption("version-conflict", Description = "E_RESOLVE_VERSION_CONFLICT_ACTION enum value", IsRequired = true)]
public string VersionConflict { get; init; }
[CommandOption("name-conflict", Description = "E_RESOLVE_NAME_CONFLICT_ACTION enum value", IsRequired = true)]
public string NameConflict { get; init; }
[CommandOption("append-name", Description = "Append suffix for name conflicts")]
public string AppendName { get; init; } = "";
public override Dictionary<string, object> Args() { var args = base.Args(); args["file"] = File; args["version-conflict"] = VersionConflict; args["name-conflict"] = NameConflict; args["append-name"] = AppendName; return args; }
}
[Command("galaxy import-script-library", Description = "Import a script library")]
public sealed class GalaxyImportScriptLibraryCommand : ConfirmedRoutedCommandBase
{
public override string Command => "galaxy";
public override string Subcommand => "import-script-library";
[CommandOption("path", Description = "Script library path", IsRequired = true)]
public string Path { get; init; }
public override Dictionary<string, object> Args() { var args = base.Args(); args["path"] = Path; return args; }
}
[Command("galaxy export-all", Description = "Export all objects from a galaxy")]
public sealed class GalaxyExportAllCommand : RoutedCommandBase
{
public override string Command => "galaxy";
public override string Subcommand => "export-all";
[CommandOption("output", Description = "Output file path", IsRequired = true)]
public string Output { get; init; }
[CommandOption("export-type", Description = "EExportType enum value")]
public string ExportType { get; init; } = "exportGalaxyDump";
public override Dictionary<string, object> Args() { var args = base.Args(); args["output"] = Output; args["export-type"] = ExportType; return args; }
}
[Command("galaxy grload", Description = "Run GRLoad from a CSV file")]
public sealed class GalaxyGrloadCommand : ConfirmedRoutedCommandBase
{
public override string Command => "galaxy";
public override string Subcommand => "grload";
[CommandOption("file", Description = "CSV file path", IsRequired = true)]
public string File { get; init; }
[CommandOption("mode", Description = "GRLoadMode enum value")]
public string Mode { get; init; } = "Create";
public override Dictionary<string, object> Args() { var args = base.Args(); args["file"] = File; args["mode"] = Mode; return args; }
}
public abstract class PreLoginGalaxyCommandBase : ICommand
{
[CommandOption("node", 'n', Description = "GR node name. Blank = local node; . = local machine.")]
public string NodeName { get; init; } = "";
[CommandOption("confirm", Description = "Required for mutating commands")]
public bool Confirm { get; init; }
[CommandOption("confirm-target", Description = "Required exact target for destructive commands")]
public string ConfirmTarget { get; init; } = "";
protected IGRAccess CreateApp() => new GRAccessAppClass();
protected string Node() => GRAccessDiagnostics.NormalizeNodeName(NodeName);
public abstract ValueTask ExecuteAsync(IConsole console);
protected void RequireConfirm(string target)
{
if (!Confirm) throw new CliFx.Exceptions.CommandException("This command requires --confirm.", 1);
if (!string.Equals(ConfirmTarget, target, StringComparison.OrdinalIgnoreCase))
throw new CliFx.Exceptions.CommandException($"This command requires --confirm-target {target}.", 1);
}
}
[Command("galaxy create", Description = "Create a galaxy")]
public sealed class GalaxyCreateCommand : PreLoginGalaxyCommandBase
{
[CommandOption("galaxy", 'g', Description = "Galaxy name", IsRequired = true)]
public string GalaxyName { get; init; }
[CommandOption("security", Description = "Enable security")]
public bool Security { get; init; }
[CommandOption("auth-mode", Description = "EAuthenticationMode enum value")]
public string AuthMode { get; init; } = "aaNoAuthentication";
[CommandOption("os-user", Description = "OS user name/description")]
public string OsUser { get; init; } = "";
public override ValueTask ExecuteAsync(IConsole console)
{
RequireConfirm(GalaxyName);
var app = CreateApp();
app.CreateGalaxy(GalaxyName, Node(), Security, (EAuthenticationMode)Enum.Parse(typeof(EAuthenticationMode), AuthMode), OsUser);
console.Output.WriteLine(GRAccessDiagnostics.FormatCommandResult("CreateGalaxy", app.CommandResult));
return default;
}
}
[Command("galaxy create-from-template", Description = "Create a galaxy from a template")]
public sealed class GalaxyCreateFromTemplateCommand : PreLoginGalaxyCommandBase
{
[CommandOption("galaxy", 'g', Description = "Galaxy name", IsRequired = true)]
public string GalaxyName { get; init; }
[CommandOption("template", Description = "Create galaxy template name", IsRequired = true)]
public string Template { get; init; }
public override ValueTask ExecuteAsync(IConsole console)
{
RequireConfirm(GalaxyName);
var app = CreateApp();
app.CreateGalaxyFromTemplate(Template, GalaxyName, Node());
console.Output.WriteLine(GRAccessDiagnostics.FormatCommandResult("CreateGalaxyFromTemplate", app.CommandResult));
return default;
}
}
[Command("galaxy delete", Description = "Delete a galaxy")]
public sealed class GalaxyDeleteCommand : PreLoginGalaxyCommandBase
{
[CommandOption("galaxy", 'g', Description = "Galaxy name", IsRequired = true)]
public string GalaxyName { get; init; }
public override ValueTask ExecuteAsync(IConsole console)
{
RequireConfirm(GalaxyName);
var app = CreateApp();
app.DeleteGalaxy(GalaxyName, Node());
console.Output.WriteLine(GRAccessDiagnostics.FormatCommandResult("DeleteGalaxy", app.CommandResult));
return default;
}
}
[Command("object get", Description = "Get object details")]
public sealed class ObjectGetCommand : ObjectCommandBase { public override string Command => "object"; public override string Subcommand => "get"; }
[Command("object snapshot", Description = "Get a machine-oriented snapshot of an object, attributes, relationships, and script metadata")]
public sealed class ObjectSnapshotCommand : ObjectCommandBase { public override string Command => "object"; public override string Subcommand => "snapshot"; }
[Command("object lineage", Description = "Get object inheritance and containment lineage")]
public sealed class ObjectLineageCommand : ObjectCommandBase { public override string Command => "object"; public override string Subcommand => "lineage"; }
[Command("object children", Description = "Get contained or derived child objects")]
public sealed class ObjectChildrenCommand : ObjectCommandBase { public override string Command => "object"; public override string Subcommand => "children"; }
[Command("object query-name", Description = "Find objects by exact names")]
public sealed class ObjectQueryNameCommand : RoutedCommandBase
{
public override string Command => "object";
public override string Subcommand => "query-name";
[CommandOption("name", Description = "Object tagname. May be repeated.", IsRequired = true)]
public IReadOnlyList<string> Names { get; init; }
[CommandOption("type", 't', Description = "Object type: all, template, instance")]
public string Type { get; init; } = "all";
public override Dictionary<string, object> Args() { var args = base.Args(); args["name"] = Names; args["type"] = Type; return args; }
}
[Command("object query-condition", Description = "Find objects by one GRAccess condition")]
public sealed class ObjectQueryConditionCommand : RoutedCommandBase
{
public override string Command => "object";
public override string Subcommand => "query-condition";
[CommandOption("condition", Description = "EConditionType enum value")]
public string Condition { get; init; } = "namedLike";
[CommandOption("value", Description = "Condition value")]
public string Value { get; init; } = "%";
[CommandOption("type", 't', Description = "Object type: all, template, instance")]
public string Type { get; init; } = "all";
public override Dictionary<string, object> Args() { var args = base.Args(); args["condition"] = Condition; args["value"] = Value; args["type"] = Type; return args; }
}
[Command("object query-multi", Description = "Find objects matching any supplied pattern")]
public sealed class ObjectQueryMultiCommand : RoutedCommandBase
{
public override string Command => "object";
public override string Subcommand => "query-multi";
[CommandOption("pattern", 'p', Description = "Name pattern. May be repeated.")]
public IReadOnlyList<string> Patterns { get; init; } = Array.Empty<string>();
[CommandOption("type", 't', Description = "Object type: all, template, instance")]
public string Type { get; init; } = "all";
public override Dictionary<string, object> Args() { var args = base.Args(); args["pattern"] = Patterns; args["type"] = Type; return args; }
}
[Command("object extended-attributes", Description = "Get extended attributes for an object")]
public sealed class ObjectExtendedAttributesCommand : ObjectCommandBase
{
public override string Command => "object";
public override string Subcommand => "extended-attributes";
[CommandOption("attribute", Description = "Attribute name")]
public string Attribute { get; init; } = "";
[CommandOption("level", Description = "Hierarchy level")]
public int Level { get; init; }
public override Dictionary<string, object> Args() { var args = base.Args(); args["attribute"] = Attribute; args["level"] = Level; return args; }
}
[Command("object help-url", Description = "Get object help URL")]
public sealed class ObjectHelpUrlCommand : ObjectCommandBase { public override string Command => "object"; public override string Subcommand => "help-url"; }
[Command("object scripts list", Description = "List script-like metadata exposed on an object")]
public sealed class ObjectScriptsListCommand : ObjectCommandBase { public override string Command => "object"; public override string Subcommand => "scripts-list"; }
[Command("object scripts get", Description = "Get script metadata and body availability for an object script")]
public class ObjectScriptsGetCommand : ObjectCommandBase
{
public override string Command => "object";
public override string Subcommand => "scripts-get";
[CommandOption("script", Description = "Script name", IsRequired = true)]
public string Script { get; init; }
public override Dictionary<string, object> Args() { var args = base.Args(); args["script"] = Script; return args; }
}
[Command("object scripts set", Description = "Set an object script body when supported by the local adapter")]
public sealed class ObjectScriptsSetCommand : ObjectScriptsGetCommand
{
public override string Subcommand => "scripts-set";
[CommandOption("file", Description = "Script source file", IsRequired = true)]
public string File { get; init; }
public override Dictionary<string, object> Args() { var args = base.Args(); args["file"] = File; return args; }
}
public abstract class ObjectScriptSettingsCommandBase : ObjectScriptsGetCommand
{
[CommandOption("trigger-period-ms", Description = "Periodic script trigger interval in milliseconds")]
public string TriggerPeriodMs { get; init; } = "";
[CommandOption("trigger-type", Description = "Script trigger type value")]
public string TriggerType { get; init; } = "";
[CommandOption("expression", Description = "Script expression value")]
public string Expression { get; init; } = "";
[CommandOption("lock-trigger-period", Description = "Lock TriggerPeriod in this object after setting it")]
public bool LockTriggerPeriod { get; init; }
public override Dictionary<string, object> Args()
{
var args = base.Args();
args["trigger-period-ms"] = TriggerPeriodMs;
args["trigger-type"] = TriggerType;
args["expression"] = Expression;
args["lock-trigger-period"] = LockTriggerPeriod;
return args;
}
}
[Command("object scripts create", Description = "Create a ScriptExtension primitive and optionally initialize its body/settings")]
public sealed class ObjectScriptsCreateCommand : ObjectScriptSettingsCommandBase
{
public override string Subcommand => "scripts-create";
[CommandOption("file", Description = "Optional script source file")]
public string File { get; init; } = "";
public override Dictionary<string, object> Args() { var args = base.Args(); args["file"] = File; return args; }
}
[Command("object scripts settings set", Description = "Set ScriptExtension settings through ConfigurableAttributes")]
public sealed class ObjectScriptsSettingsSetCommand : ObjectScriptSettingsCommandBase
{
public override string Subcommand => "scripts-settings-set";
}
[Command("object checkout", Description = "Check out an object")]
public sealed class ObjectCheckoutCommand : ObjectCommandBase { public override string Command => "object"; public override string Subcommand => "checkout"; }
[Command("object checkin", Description = "Check in an object")]
public sealed class ObjectCheckinCommand : ObjectCommandBase
{
public override string Command => "object";
public override string Subcommand => "checkin";
[CommandOption("comment", Description = "Check-in comment")]
public string Comment { get; init; } = "";
public override Dictionary<string, object> Args() { var args = base.Args(); args["comment"] = Comment; return args; }
}
[Command("object undo-checkout", Description = "Undo object checkout")]
public sealed class ObjectUndoCheckoutCommand : ObjectCommandBase { public override string Command => "object"; public override string Subcommand => "undo-checkout"; }
[Command("object save", Description = "Save an object")]
public sealed class ObjectSaveCommand : ObjectCommandBase { public override string Command => "object"; public override string Subcommand => "save"; }
[Command("object unload", Description = "Unload an object from cache")]
public sealed class ObjectUnloadCommand : ObjectCommandBase { public override string Command => "object"; public override string Subcommand => "unload"; }
[Command("object set", Description = "Set an object property")]
public sealed class ObjectSetCommand : ObjectCommandBase
{
public override string Command => "object";
public override string Subcommand => "set";
[CommandOption("property", Description = "tagname, contained-name, area, host, container, toolset, security-group", IsRequired = true)]
public string Property { get; init; }
[CommandOption("value", Description = "New value", IsRequired = true)]
public string Value { get; init; }
public override Dictionary<string, object> Args() { var args = base.Args(); args["property"] = Property; args["value"] = Value; return args; }
}
[Command("template derive", Description = "Create a derived template")]
public sealed class TemplateDeriveCommand : ObjectCommandBase
{
public override string Command => "template";
public override string Subcommand => "derive";
[CommandOption("new-name", Description = "New template name", IsRequired = true)] public string NewName { get; init; }
[CommandOption("create-contained", Description = "Create contained objects")] public bool CreateContained { get; init; }
public override Dictionary<string, object> Args() { var args = base.Args(); args["new-name"] = NewName; args["create-contained"] = CreateContained; return args; }
}
[Command("template instantiate", Description = "Create an instance from a template")]
public sealed class TemplateInstantiateCommand : ObjectCommandBase
{
public override string Command => "template";
public override string Subcommand => "instantiate";
[CommandOption("new-name", Description = "New instance name", IsRequired = true)] public string NewName { get; init; }
[CommandOption("create-contained", Description = "Create contained objects")] public bool CreateContained { get; init; }
public override Dictionary<string, object> Args() { var args = base.Args(); args["new-name"] = NewName; args["create-contained"] = CreateContained; return args; }
}
[Command("template delete", Description = "Delete a template")]
public sealed class TemplateDeleteCommand : ObjectCommandBase
{
public override string Command => "template";
public override string Subcommand => "delete";
[CommandOption("force-option", Description = "EForceDeleteTemplateOption enum value")] public string ForceOption { get; init; } = "dontForceTemplateDelete";
public override Dictionary<string, object> Args() { var args = base.Args(); args["force-option"] = ForceOption; return args; }
}
public abstract class InstanceLifecycleCommandBase : ObjectCommandBase
{
[CommandOption("force-option", Description = "Delete force option")]
public string ForceOption { get; init; } = "undeployIfDeployed";
public override Dictionary<string, object> Args() { var args = base.Args(); args["force-option"] = ForceOption; return args; }
}
[Command("instance delete", Description = "Delete an instance")]
public sealed class InstanceDeleteCommand : InstanceLifecycleCommandBase { public override string Command => "instance"; public override string Subcommand => "delete"; }
[Command("instance deploy", Description = "Deploy an instance")]
public sealed class InstanceDeployCommand : ObjectCommandBase { public override string Command => "instance"; public override string Subcommand => "deploy"; }
[Command("instance undeploy", Description = "Undeploy an instance")]
public sealed class InstanceUndeployCommand : ObjectCommandBase { public override string Command => "instance"; public override string Subcommand => "undeploy"; }
[Command("instance upload", Description = "Upload runtime changes for an instance")]
public sealed class InstanceUploadCommand : ObjectCommandBase { public override string Command => "instance"; public override string Subcommand => "upload"; }
[Command("instance assign-area", Description = "Assign an instance to an area")]
public sealed class InstanceAssignAreaCommand : ObjectCommandBase
{
public override string Command => "instance";
public override string Subcommand => "assign-area";
[CommandOption("area", Description = "Area instance name", IsRequired = true)] public string Area { get; init; }
public override Dictionary<string, object> Args() { var args = base.Args(); args["area"] = Area; return args; }
}
[Command("instance assign-engine", Description = "Assign an instance to an engine")]
public sealed class InstanceAssignEngineCommand : ObjectCommandBase
{
public override string Command => "instance";
public override string Subcommand => "assign-engine";
[CommandOption("engine", Description = "Engine instance name", IsRequired = true)] public string Engine { get; init; }
public override Dictionary<string, object> Args() { var args = base.Args(); args["engine"] = Engine; return args; }
}
[Command("instance assign-container", Description = "Assign an instance to a container")]
public sealed class InstanceAssignContainerCommand : ObjectCommandBase
{
public override string Command => "instance";
public override string Subcommand => "assign-container";
[CommandOption("container", Description = "Container instance name", IsRequired = true)] public string Container { get; init; }
public override Dictionary<string, object> Args() { var args = base.Args(); args["container"] = Container; return args; }
}
[Command("io assign", Description = "Assign an I/O-related instance attribute")]
public sealed class IoAssignCommand : ConfirmedRoutedCommandBase
{
public override string Command => "io";
public override string Subcommand => "assign";
[CommandOption("name", Description = "Instance tagname", IsRequired = true)]
public string ObjectName { get; init; }
[CommandOption("attribute", Description = "Attribute name", IsRequired = true)]
public string Attribute { get; init; }
[CommandOption("value", Description = "Assigned value", IsRequired = true)]
public string Value { get; init; }
[CommandOption("data-type", Description = "string, bool, int, float, double")]
public string DataType { get; init; } = "string";
public override Dictionary<string, object> Args()
{
var args = base.Args();
args["name"] = ObjectName;
args["attribute"] = Attribute;
args["value"] = Value;
args["data-type"] = DataType;
return args;
}
}
public abstract class AreaEngineCommandBase : ConfirmedRoutedCommandBase
{
[CommandOption("template", Description = "Template to instantiate", IsRequired = true)]
public string Template { get; init; }
[CommandOption("name", Description = "New instance name", IsRequired = true)]
public string Name { get; init; }
[CommandOption("create-contained", Description = "Create contained objects")]
public bool CreateContained { get; init; }
public override Dictionary<string, object> Args()
{
var args = base.Args();
args["template"] = Template;
args["name"] = Name;
args["create-contained"] = CreateContained;
return args;
}
}
[Command("area list", Description = "List area-like instances")]
public sealed class AreaListCommand : RoutedCommandBase
{
public override string Command => "area";
public override string Subcommand => "list";
[CommandOption("pattern", 'p', Description = "Name pattern. Use % as wildcard.")] public string Pattern { get; init; } = "%Area%";
public override Dictionary<string, object> Args() { var args = base.Args(); args["pattern"] = Pattern; return args; }
}
[Command("area create", Description = "Create an area instance from a template")]
public sealed class AreaCreateCommand : AreaEngineCommandBase { public override string Command => "area"; public override string Subcommand => "create"; }
[Command("engine list", Description = "List engine-like instances")]
public sealed class EngineListCommand : RoutedCommandBase
{
public override string Command => "engine";
public override string Subcommand => "list";
[CommandOption("pattern", 'p', Description = "Name pattern. Use % as wildcard.")] public string Pattern { get; init; } = "%Engine%";
public override Dictionary<string, object> Args() { var args = base.Args(); args["pattern"] = Pattern; return args; }
}
[Command("engine create", Description = "Create an engine instance from a template")]
public sealed class EngineCreateCommand : AreaEngineCommandBase { public override string Command => "engine"; public override string Subcommand => "create"; }
public abstract class AttributeCommandBase : ObjectCommandBase
{
[CommandOption("attribute", Description = "Attribute name", IsRequired = true)]
public string Attribute { get; init; }
public override Dictionary<string, object> Args() { var args = base.Args(); args["attribute"] = Attribute; return args; }
}
[Command("object attribute get", Description = "Get one attribute")]
public sealed class ObjectAttributeGetCommand : AttributeCommandBase { public override string Command => "object"; public override string Subcommand => "attribute-get"; }
[Command("object attribute set", Description = "Set one attribute value")]
public sealed class ObjectAttributeSetCommand : AttributeCommandBase
{
public override string Command => "object";
public override string Subcommand => "attribute-set";
[CommandOption("value", Description = "New value", IsRequired = true)] public string Value { get; init; }
[CommandOption("data-type", Description = "string, bool, int, float, double")] public string DataType { get; init; } = "string";
public override Dictionary<string, object> Args() { var args = base.Args(); args["value"] = Value; args["data-type"] = DataType; return args; }
}
[Command("object attribute value get", Description = "Read back one attribute value with scalar type metadata")]
public sealed class ObjectAttributeValueGetCommand : AttributeCommandBase { public override string Command => "object"; public override string Subcommand => "attribute-value-get"; }
[Command("object attribute value set", Description = "Set one scalar attribute value")]
public sealed class ObjectAttributeValueSetCommand : AttributeCommandBase
{
public override string Command => "object";
public override string Subcommand => "attribute-value-set";
[CommandOption("value", Description = "New value", IsRequired = true)] public string Value { get; init; }
[CommandOption("data-type", Description = "string, bool, int, float, double")] public string DataType { get; init; } = "string";
public override Dictionary<string, object> Args() { var args = base.Args(); args["value"] = Value; args["data-type"] = DataType; return args; }
}
[Command("object attribute lock", Description = "Set attribute locked state")]
public sealed class ObjectAttributeLockCommand : AttributeCommandBase
{
public override string Command => "object"; public override string Subcommand => "attribute-lock";
[CommandOption("locked", Description = "MxPropertyLockedEnum value", IsRequired = true)] public string Locked { get; init; }
public override Dictionary<string, object> Args() { var args = base.Args(); args["locked"] = Locked; return args; }
}
[Command("object attribute security", Description = "Set attribute security classification")]
public sealed class ObjectAttributeSecurityCommand : AttributeCommandBase
{
public override string Command => "object"; public override string Subcommand => "attribute-security";
[CommandOption("security", Description = "MxSecurityClassification value", IsRequired = true)] public string Security { get; init; }
public override Dictionary<string, object> Args() { var args = base.Args(); args["security"] = Security; return args; }
}
[Command("object attribute buffer", Description = "Set attribute buffer flag")]
public sealed class ObjectAttributeBufferCommand : AttributeCommandBase
{
public override string Command => "object"; public override string Subcommand => "attribute-buffer";
[CommandOption("has-buffer", Description = "Whether the attribute has a buffer")] public bool HasBuffer { get; init; }
public override Dictionary<string, object> Args() { var args = base.Args(); args["has-buffer"] = HasBuffer; return args; }
}
public abstract class UdaCommandBase : ObjectCommandBase
{
[CommandOption("uda", Description = "UDA name", IsRequired = true)] public string Uda { get; init; }
[CommandOption("data-type", Description = "MxDataType value")] public string DataType { get; init; } = "MxString";
[CommandOption("category", Description = "MxAttributeCategory value")] public string Category { get; init; } = "MxCategoryWriteable_USC";
[CommandOption("security", Description = "MxSecurityClassification value")] public string Security { get; init; } = "MxSecurityUndefined";
[CommandOption("is-array", Description = "Create/update as array")] public bool IsArray { get; init; }
[CommandOption("array-count", Description = "Array element count")] public int ArrayCount { get; init; }
public override Dictionary<string, object> Args() { var args = base.Args(); args["uda"] = Uda; args["data-type"] = DataType; args["category"] = Category; args["security"] = Security; args["is-array"] = IsArray; args["array-count"] = ArrayCount; return args; }
}
[Command("object uda add", Description = "Add a UDA")]
public sealed class ObjectUdaAddCommand : UdaCommandBase { public override string Command => "object"; public override string Subcommand => "uda-add"; }
[Command("object uda delete", Description = "Delete a UDA")]
public sealed class ObjectUdaDeleteCommand : UdaCommandBase { public override string Command => "object"; public override string Subcommand => "uda-delete"; }
[Command("object uda rename", Description = "Rename a UDA")]
public sealed class ObjectUdaRenameCommand : UdaCommandBase { public override string Command => "object"; public override string Subcommand => "uda-rename"; [CommandOption("new-name", Description = "New UDA name", IsRequired = true)] public string NewName { get; init; } public override Dictionary<string, object> Args() { var args = base.Args(); args["new-name"] = NewName; return args; } }
[Command("object uda update", Description = "Update a UDA")]
public sealed class ObjectUdaUpdateCommand : UdaCommandBase { public override string Command => "object"; public override string Subcommand => "uda-update"; }
public abstract class ExtensionCommandBase : ObjectCommandBase
{
[CommandOption("extension-type", Description = "Extension type", IsRequired = true)] public string ExtensionType { get; init; }
[CommandOption("primitive", Description = "Primitive name", IsRequired = true)] public string Primitive { get; init; }
[CommandOption("object-extension", Description = "Whether this is an object extension")] public bool ObjectExtension { get; init; }
public override Dictionary<string, object> Args() { var args = base.Args(); args["extension-type"] = ExtensionType; args["primitive"] = Primitive; args["object-extension"] = ObjectExtension; return args; }
}
[Command("object extension add", Description = "Add an extension primitive")]
public sealed class ObjectExtensionAddCommand : ExtensionCommandBase { public override string Command => "object"; public override string Subcommand => "extension-add"; }
[Command("object extension delete", Description = "Delete an extension primitive")]
public sealed class ObjectExtensionDeleteCommand : ExtensionCommandBase { public override string Command => "object"; public override string Subcommand => "extension-delete"; }
[Command("object extension rename", Description = "Rename an extension primitive")]
public sealed class ObjectExtensionRenameCommand : ExtensionCommandBase { public override string Command => "object"; public override string Subcommand => "extension-rename"; [CommandOption("new-name", Description = "New primitive name", IsRequired = true)] public string NewName { get; init; } public override Dictionary<string, object> Args() { var args = base.Args(); args["new-name"] = NewName; return args; } }
[Command("objects checkout", Description = "Check out multiple objects")]
public sealed class ObjectsCheckoutCommand : BulkObjectsCommandBase { public override string Command => "objects"; public override string Subcommand => "checkout"; }
[Command("objects checkin", Description = "Check in multiple objects")]
public sealed class ObjectsCheckinCommand : BulkObjectsCommandBase { public override string Command => "objects"; public override string Subcommand => "checkin"; [CommandOption("comment", Description = "Check-in comment")] public string Comment { get; init; } = ""; public override Dictionary<string, object> Args() { var args = base.Args(); args["comment"] = Comment; return args; } }
[Command("objects undo-checkout", Description = "Undo checkout for multiple objects")]
public sealed class ObjectsUndoCheckoutCommand : BulkObjectsCommandBase { public override string Command => "objects"; public override string Subcommand => "undo-checkout"; }
[Command("objects deploy", Description = "Deploy multiple instances")]
public sealed class ObjectsDeployCommand : BulkObjectsCommandBase { public override string Command => "objects"; public override string Subcommand => "deploy"; }
[Command("objects undeploy", Description = "Undeploy multiple instances")]
public sealed class ObjectsUndeployCommand : BulkObjectsCommandBase { public override string Command => "objects"; public override string Subcommand => "undeploy"; }
[Command("objects upload", Description = "Upload multiple instances")]
public sealed class ObjectsUploadCommand : BulkObjectsCommandBase { public override string Command => "objects"; public override string Subcommand => "upload"; }
[Command("objects delete", Description = "Delete multiple objects")]
public sealed class ObjectsDeleteCommand : BulkObjectsCommandBase { public override string Command => "objects"; public override string Subcommand => "delete"; }
[Command("objects export", Description = "Export multiple objects")]
public sealed class ObjectsExportCommand : BulkObjectsCommandBase { public override string Command => "objects"; public override string Subcommand => "export"; [CommandOption("output", Description = "Output file path", IsRequired = true)] public string Output { get; init; } [CommandOption("export-type", Description = "EExportType value")] public string ExportType { get; init; } = "exportGalaxyDump"; public override Dictionary<string, object> Args() { var args = base.Args(); args["output"] = Output; args["export-type"] = ExportType; return args; } }
[Command("objects export-protected", Description = "Export multiple objects as protected")]
public sealed class ObjectsExportProtectedCommand : BulkObjectsCommandBase { public override string Command => "objects"; public override string Subcommand => "export-protected"; [CommandOption("output", Description = "Output file path", IsRequired = true)] public string Output { get; init; } public override Dictionary<string, object> Args() { var args = base.Args(); args["output"] = Output; return args; } }
[Command("toolset list", Description = "List toolsets")]
public sealed class ToolsetListCommand : RoutedCommandBase { public override string Command => "toolset"; public override string Subcommand => "list"; }
[Command("toolset tree", Description = "Show toolset tree")]
public sealed class ToolsetTreeCommand : RoutedCommandBase { public override string Command => "toolset"; public override string Subcommand => "tree"; }
[Command("toolset add", Description = "Add a toolset")]
public class ToolsetAddCommand : ConfirmedRoutedCommandBase { public override string Command => "toolset"; public override string Subcommand => "add"; [CommandOption("name", Description = "Toolset name", IsRequired = true)] public string Name { get; init; } public override Dictionary<string, object> Args() { var args = base.Args(); args["name"] = Name; return args; } }
[Command("toolset delete", Description = "Delete a toolset")]
public sealed class ToolsetDeleteCommand : ToolsetAddCommand { public override string Subcommand => "delete"; }
[Command("toolset rename", Description = "Rename a toolset")]
public sealed class ToolsetRenameCommand : ToolsetAddCommand { public override string Subcommand => "rename"; [CommandOption("new-name", Description = "New toolset name", IsRequired = true)] public string NewName { get; init; } public override Dictionary<string, object> Args() { var args = base.Args(); args["new-name"] = NewName; return args; } }
[Command("toolset move", Description = "Move a toolset")]
public sealed class ToolsetMoveCommand : ToolsetAddCommand { public override string Subcommand => "move"; [CommandOption("parent", Description = "Parent toolset name", IsRequired = true)] public string Parent { get; init; } public override Dictionary<string, object> Args() { var args = base.Args(); args["parent"] = Parent; return args; } }
[Command("script-library list", Description = "List script libraries")]
public sealed class ScriptLibraryListCommand : RoutedCommandBase { public override string Command => "script-library"; public override string Subcommand => "list"; }
[Command("script-library add", Description = "Add/import a script library")]
public class ScriptLibraryAddCommand : ConfirmedRoutedCommandBase { public override string Command => "script-library"; public override string Subcommand => "add"; [CommandOption("path", Description = "Script library path", IsRequired = true)] public string Path { get; init; } public override Dictionary<string, object> Args() { var args = base.Args(); args["path"] = Path; return args; } }
[Command("script-library import", Description = "Import a script library")]
public sealed class ScriptLibraryImportCommand : ScriptLibraryAddCommand { public override string Subcommand => "import"; }
[Command("script-library export", Description = "Export a script library")]
public sealed class ScriptLibraryExportCommand : ConfirmedRoutedCommandBase { public override string Command => "script-library"; public override string Subcommand => "export"; [CommandOption("name", Description = "Script library name", IsRequired = true)] public string Name { get; init; } [CommandOption("output", Description = "Output path", IsRequired = true)] public string Output { get; init; } public override Dictionary<string, object> Args() { var args = base.Args(); args["name"] = Name; args["output"] = Output; return args; } }
[Command("security info", Description = "Show security settings")]
public sealed class SecurityInfoCommand : RoutedCommandBase { public override string Command => "security"; public override string Subcommand => "info"; }
[Command("security roles", Description = "List security roles")]
public sealed class SecurityRolesCommand : RoutedCommandBase { public override string Command => "security"; public override string Subcommand => "roles"; }
[Command("security users", Description = "List security users")]
public sealed class SecurityUsersCommand : RoutedCommandBase { public override string Command => "security"; public override string Subcommand => "users"; }
[Command("security groups", Description = "List security groups")]
public sealed class SecurityGroupsCommand : RoutedCommandBase { public override string Command => "security"; public override string Subcommand => "groups"; }
[Command("security permissions", Description = "List permissions for a role")]
public sealed class SecurityPermissionsCommand : RoutedCommandBase { public override string Command => "security"; public override string Subcommand => "permissions"; [CommandOption("role", Description = "Role name", IsRequired = true)] public string Role { get; init; } public override Dictionary<string, object> Args() { var args = base.Args(); args["role"] = Role; return args; } }
[Command("settings locale get", Description = "Get locale settings")]
public sealed class SettingsLocaleGetCommand : RoutedCommandBase { public override string Command => "settings"; public override string Subcommand => "locale-get"; }
[Command("settings time-master get", Description = "Get time master settings")]
public sealed class SettingsTimeMasterGetCommand : RoutedCommandBase { public override string Command => "settings"; public override string Subcommand => "time-master-get"; }
}
@@ -0,0 +1,80 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using CliFx;
using CliFx.Attributes;
using CliFx.Infrastructure;
using ArchestrA.GRAccess;
using Newtonsoft.Json;
using ZB.MOM.WW.GRAccess.Cli.GRAccess;
using ZB.MOM.WW.GRAccess.Cli.Protocol;
using ZB.MOM.WW.GRAccess.Cli.Session;
namespace ZB.MOM.WW.GRAccess.Cli.Commands.Galaxy
{
[Command("galaxy list", Description = "List available galaxies on a GR node")]
public class GalaxyListCommand : ICommand
{
[CommandOption("node", 'n', Description = "GR node name (blank = local node)")]
public string NodeName { get; init; } = "";
[CommandOption("json", Description = "Output as JSON")]
public bool Json { get; init; }
[CommandOption("llm-json", Description = "Output stable machine JSON envelope")]
public bool LlmJson { get; init; }
public async ValueTask ExecuteAsync(IConsole console)
{
// This command always runs one-shot — no galaxy login needed,
// so no session routing (session is galaxy-specific).
var galaxyNames = ListGalaxies(NodeName);
if (LlmJson)
{
await console.Output.WriteLineAsync(LlmResponse.Ok("galaxy list", string.Empty, NodeName, galaxyNames)).ConfigureAwait(false);
return;
}
if (Json)
{
var json = JsonConvert.SerializeObject(galaxyNames, Formatting.Indented);
await console.Output.WriteLineAsync(json).ConfigureAwait(false);
}
else
{
foreach (var name in galaxyNames)
await console.Output.WriteLineAsync(name).ConfigureAwait(false);
}
}
internal static List<string> ListGalaxies(string nodeName)
{
nodeName = GRAccessDiagnostics.NormalizeNodeName(nodeName);
var grAccess = new GRAccessAppClass();
var galaxies = grAccess.QueryGalaxies(nodeName);
var result = grAccess.CommandResult;
if (result != null && !result.Successful)
{
throw new InvalidOperationException(
GRAccessDiagnostics.FormatCommandResult("QueryGalaxies", result));
}
var names = new List<string>();
if (galaxies != null)
{
for (int i = 1; i <= galaxies.count; i++)
{
var galaxy = galaxies[i];
if (galaxy != null)
names.Add(galaxy.Name);
}
}
return names;
}
}
}
@@ -0,0 +1,274 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using CliFx;
using CliFx.Attributes;
using CliFx.Exceptions;
using CliFx.Infrastructure;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using ZB.MOM.WW.GRAccess.Cli.GRAccess;
using ZB.MOM.WW.GRAccess.Cli.Infrastructure;
using ZB.MOM.WW.GRAccess.Cli.Protocol;
using ZB.MOM.WW.GRAccess.Cli.Session;
namespace ZB.MOM.WW.GRAccess.Cli.Commands
{
[Command("capabilities", Description = "List machine-readable command capabilities")]
public sealed class CapabilitiesCommand : ICommand
{
[CommandOption("json", Description = "Output as JSON")]
public bool Json { get; init; }
[CommandOption("llm-json", Description = "Output stable machine JSON envelope")]
public bool LlmJson { get; init; }
public async ValueTask ExecuteAsync(IConsole console)
{
var data = new
{
Version = 1,
Commands = CommandCapabilityRegistry.Commands
};
if (LlmJson)
{
await console.Output.WriteLineAsync(LlmResponse.Ok("capabilities", string.Empty, string.Empty, data)).ConfigureAwait(false);
return;
}
if (Json)
{
await console.Output.WriteLineAsync(JsonConvert.SerializeObject(data, Formatting.Indented)).ConfigureAwait(false);
return;
}
foreach (var command in CommandCapabilityRegistry.Commands)
await console.Output.WriteLineAsync($"{command.Name}\tmutates={command.Mutates}\tsession={command.RoutesThroughSession}").ConfigureAwait(false);
}
}
[Command("validate", Description = "Validate a machine command or batch plan JSON file")]
public sealed class ValidateCommand : ICommand
{
[CommandOption("request", Description = "Plan JSON file path", IsRequired = true)]
public string Request { get; init; }
[CommandOption("llm-json", Description = "Output stable machine JSON envelope")]
public bool LlmJson { get; init; }
public async ValueTask ExecuteAsync(IConsole console)
{
var plan = BatchPlan.Load(Request);
var result = BatchValidator.Validate(plan, requireMutationConfirmation: true);
var output = new { result.Valid, Steps = result.Steps };
if (LlmJson)
await console.Output.WriteLineAsync(LlmResponse.Ok("validate", plan.Galaxy, Request, output)).ConfigureAwait(false);
else
await console.Output.WriteLineAsync(JsonConvert.SerializeObject(output, Formatting.Indented)).ConfigureAwait(false);
}
}
[Command("batch", Description = "Validate or execute a machine command plan")]
public sealed class BatchCommand : ICommand
{
[CommandOption("file", Description = "Plan JSON file path", IsRequired = true)]
public string File { get; init; }
[CommandOption("mode", Description = "validate or execute")]
public string Mode { get; init; } = "validate";
[CommandOption("llm-json", Description = "Output stable machine JSON envelope")]
public bool LlmJson { get; init; }
public async ValueTask ExecuteAsync(IConsole console)
{
var plan = BatchPlan.Load(File);
var validation = BatchValidator.Validate(plan, requireMutationConfirmation: true);
if (!string.Equals(Mode, "execute", StringComparison.OrdinalIgnoreCase))
{
await WriteBatchOutput(console, plan, new { validation.Valid, Steps = validation.Steps }, validation.Valid).ConfigureAwait(false);
return;
}
if (!validation.Valid)
{
await WriteBatchOutput(console, plan, new { validation.Valid, Steps = validation.Steps }, false).ConfigureAwait(false);
throw new CommandException("Batch validation failed.", 1);
}
var outputs = new List<object>();
for (var i = 0; i < plan.Commands.Count; i++)
{
var step = plan.Commands[i];
try
{
var output = await ExecuteStepAsync(plan, step).ConfigureAwait(false);
outputs.Add(new { Index = i, step.Command, Success = true, Output = ParseOutput(output) });
}
catch (Exception ex)
{
outputs.Add(new { Index = i, step.Command, Success = false, Error = ex.Message });
await WriteBatchOutput(console, plan, new { Valid = true, Executed = outputs }, false).ConfigureAwait(false);
throw new CommandException(ex.Message, 1);
}
}
await WriteBatchOutput(console, plan, new { Valid = true, Executed = outputs }, true).ConfigureAwait(false);
}
private async Task WriteBatchOutput(IConsole console, BatchPlan plan, object data, bool success)
{
if (LlmJson)
{
var response = new LlmResponse
{
Success = success,
Command = "batch",
Galaxy = plan.Galaxy ?? string.Empty,
Target = File ?? string.Empty,
Data = data,
ExitCode = success ? 0 : 1
};
await console.Output.WriteLineAsync(LlmResponse.Serialize(response)).ConfigureAwait(false);
return;
}
await console.Output.WriteLineAsync(JsonConvert.SerializeObject(data, Formatting.Indented)).ConfigureAwait(false);
}
private static async Task<string> ExecuteStepAsync(BatchPlan plan, BatchStep step)
{
var args = step.MaterializedArgs(plan);
args["llm-json"] = true;
args["json"] = false;
var split = CommandCapabilityRegistry.SplitCommandName(step.Command);
var galaxy = ReadString(args, "galaxy", plan.Galaxy);
var node = ReadString(args, "node", plan.Node);
if (SessionClient.TryConnect(galaxy, out var client))
{
using (client)
{
var response = await client.SendCommandAsync(PipeRequest.Execute(split.Command, split.Subcommand, args)).ConfigureAwait(false);
if (!response.Success)
throw new InvalidOperationException(response.Error);
return response.Output;
}
}
if (string.IsNullOrWhiteSpace(node))
throw new InvalidOperationException("No active session found. Provide a top-level or step node for one-shot batch execution.");
using (var connection = new GRAccessConnection(galaxy, node))
{
connection.Connect();
return GRAccessCommandDispatcher.Execute(connection.Galaxy, split.Command, split.Subcommand, args);
}
}
private static object ParseOutput(string output)
{
if (string.IsNullOrWhiteSpace(output))
return null;
try { return JToken.Parse(output); }
catch { return output; }
}
private static string ReadString(IDictionary<string, object> args, string key, string fallback)
{
return args.TryGetValue(key, out var value) && value != null
? Convert.ToString(value) ?? string.Empty
: fallback ?? string.Empty;
}
}
public sealed class BatchPlan
{
public string Galaxy { get; set; } = string.Empty;
public string Node { get; set; } = string.Empty;
public List<BatchStep> Commands { get; set; } = new List<BatchStep>();
public static BatchPlan Load(string path)
{
var root = JObject.Parse(System.IO.File.ReadAllText(path));
if (root["commands"] == null && root["command"] != null)
root = new JObject { ["commands"] = new JArray(root) };
var plan = root.ToObject<BatchPlan>() ?? new BatchPlan();
if (plan.Commands == null)
plan.Commands = new List<BatchStep>();
return plan;
}
}
public sealed class BatchStep
{
public string Command { get; set; } = string.Empty;
public Dictionary<string, object> Args { get; set; } = new Dictionary<string, object>();
public Dictionary<string, object> MaterializedArgs(BatchPlan plan)
{
var args = Args == null
? new Dictionary<string, object>()
: new Dictionary<string, object>(Args, StringComparer.OrdinalIgnoreCase);
if (!args.ContainsKey("galaxy") && !string.IsNullOrWhiteSpace(plan.Galaxy))
args["galaxy"] = plan.Galaxy;
if (!args.ContainsKey("node") && !string.IsNullOrWhiteSpace(plan.Node))
args["node"] = plan.Node;
return args;
}
}
public sealed class BatchValidationSummary
{
public bool Valid => Steps.All(s => s.Valid);
public List<BatchStepValidation> Steps { get; } = new List<BatchStepValidation>();
}
public sealed class BatchStepValidation
{
public int Index { get; set; }
public string Command { get; set; } = string.Empty;
public bool Valid { get; set; }
public IReadOnlyList<string> Errors { get; set; } = Array.Empty<string>();
public bool Mutates { get; set; }
public string ConfirmationTargetRule { get; set; } = string.Empty;
}
public static class BatchValidator
{
public static BatchValidationSummary Validate(BatchPlan plan, bool requireMutationConfirmation)
{
var summary = new BatchValidationSummary();
for (var i = 0; i < plan.Commands.Count; i++)
{
var step = plan.Commands[i];
var args = step.MaterializedArgs(plan);
var result = CommandCapabilityRegistry.Validate(step.Command, args, requireMutationConfirmation);
summary.Steps.Add(new BatchStepValidation
{
Index = i,
Command = step.Command,
Valid = result.Valid,
Errors = result.Errors,
Mutates = result.Capability?.Mutates ?? false,
ConfirmationTargetRule = result.Capability?.ConfirmTarget ?? string.Empty
});
}
if (plan.Commands.Count == 0)
summary.Steps.Add(new BatchStepValidation { Index = -1, Command = string.Empty, Valid = false, Errors = new[] { "Plan contains no commands." }, Mutates = false, ConfirmationTargetRule = string.Empty });
return summary;
}
}
}
@@ -0,0 +1,49 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using CliFx;
using CliFx.Attributes;
using CliFx.Infrastructure;
using ZB.MOM.WW.GRAccess.Cli.GRAccess;
using ZB.MOM.WW.GRAccess.Cli.Infrastructure;
namespace ZB.MOM.WW.GRAccess.Cli.Commands.Objects
{
[Command("instance list", Description = "List instances in a galaxy")]
public class InstanceListCommand : ICommand
{
[CommandOption("galaxy", 'g', Description = "Galaxy name", IsRequired = true)]
public string GalaxyName { get; init; }
[CommandOption("node", 'n', Description = "GR node name. Blank = local node; . = local machine")]
public string NodeName { get; init; } = "";
[CommandOption("pattern", 'p', Description = "Instance name pattern for GRAccess namedLike query. Use % as wildcard.")]
public string Pattern { get; init; } = "%";
[CommandOption("json", Description = "Output as JSON")]
public bool Json { get; init; }
[CommandOption("llm-json", Description = "Output stable machine JSON envelope")]
public bool LlmJson { get; init; }
public async ValueTask ExecuteAsync(IConsole console)
{
var args = new Dictionary<string, object>
{
["pattern"] = Pattern,
["json"] = Json.ToString(),
["llm-json"] = LlmJson
};
await CommandRouter.ExecuteAsync(
console,
GalaxyName,
NodeName,
"instance",
"list",
args,
galaxy => GRAccessCommandDispatcher.Execute(galaxy, "instance", "list", args))
.ConfigureAwait(false);
}
}
}
@@ -0,0 +1,57 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using CliFx;
using CliFx.Attributes;
using CliFx.Infrastructure;
using ZB.MOM.WW.GRAccess.Cli.GRAccess;
using ZB.MOM.WW.GRAccess.Cli.Infrastructure;
namespace ZB.MOM.WW.GRAccess.Cli.Commands.Objects
{
[Command("object attributes", Description = "List attributes for a template or instance")]
public class ObjectAttributesCommand : ICommand
{
[CommandOption("galaxy", 'g', Description = "Galaxy name", IsRequired = true)]
public string GalaxyName { get; init; }
[CommandOption("node", 'n', Description = "GR node name. Blank = local node; . = local machine")]
public string NodeName { get; init; } = "";
[CommandOption("name", Description = "Template or instance tagname", IsRequired = true)]
public string ObjectName { get; init; }
[CommandOption("type", 't', Description = "Object type: all, template, instance")]
public string Type { get; init; } = "all";
[CommandOption("configurable", Description = "List ConfigurableAttributes instead of all Attributes")]
public bool ConfigurableOnly { get; init; }
[CommandOption("json", Description = "Output as JSON")]
public bool Json { get; init; }
[CommandOption("llm-json", Description = "Output stable machine JSON envelope")]
public bool LlmJson { get; init; }
public async ValueTask ExecuteAsync(IConsole console)
{
var args = new Dictionary<string, object>
{
["name"] = ObjectName,
["type"] = Type,
["configurable"] = ConfigurableOnly.ToString(),
["json"] = Json.ToString(),
["llm-json"] = LlmJson
};
await CommandRouter.ExecuteAsync(
console,
GalaxyName,
NodeName,
"object",
"attributes",
args,
galaxy => GRAccessCommandDispatcher.Execute(galaxy, "object", "attributes", args))
.ConfigureAwait(false);
}
}
}
@@ -0,0 +1,59 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using CliFx;
using CliFx.Attributes;
using CliFx.Infrastructure;
using ZB.MOM.WW.GRAccess.Cli.GRAccess;
using ZB.MOM.WW.GRAccess.Cli.Infrastructure;
namespace ZB.MOM.WW.GRAccess.Cli.Commands.Objects
{
[Command("object list", Description = "List templates and/or instances in a galaxy")]
public class ObjectListCommand : ICommand
{
[CommandOption("galaxy", 'g', Description = "Galaxy name", IsRequired = true)]
public string GalaxyName { get; init; }
[CommandOption("node", 'n', Description = "GR node name. Blank = local node; . = local machine")]
public string NodeName { get; init; } = "";
[CommandOption("type", 't', Description = "Object type: all, template, instance")]
public string Type { get; init; } = "all";
[CommandOption("pattern", 'p', Description = "Name pattern for GRAccess namedLike query. Use % as wildcard.")]
public string Pattern { get; init; } = "%";
[CommandOption("json", Description = "Output as JSON")]
public bool Json { get; init; }
[CommandOption("llm-json", Description = "Output stable machine JSON envelope")]
public bool LlmJson { get; init; }
public async ValueTask ExecuteAsync(IConsole console)
{
var args = new Dictionary<string, object>
{
["type"] = Type,
["pattern"] = Pattern,
["json"] = Json.ToString(),
["llm-json"] = LlmJson
};
await CommandRouter.ExecuteAsync(
console,
GalaxyName,
NodeName,
"object",
"list",
args,
galaxy => GRAccessCommandDispatcher.Execute(galaxy, "object", "list", args))
.ConfigureAwait(false);
}
internal static GRAccessObjectKind ParseKind(string value)
{
return GRAccessQueryCommandHandler.ParseKind(value);
}
}
}
@@ -0,0 +1,49 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using CliFx;
using CliFx.Attributes;
using CliFx.Infrastructure;
using ZB.MOM.WW.GRAccess.Cli.GRAccess;
using ZB.MOM.WW.GRAccess.Cli.Infrastructure;
namespace ZB.MOM.WW.GRAccess.Cli.Commands.Objects
{
[Command("template list", Description = "List templates in a galaxy")]
public class TemplateListCommand : ICommand
{
[CommandOption("galaxy", 'g', Description = "Galaxy name", IsRequired = true)]
public string GalaxyName { get; init; }
[CommandOption("node", 'n', Description = "GR node name. Blank = local node; . = local machine")]
public string NodeName { get; init; } = "";
[CommandOption("pattern", 'p', Description = "Template name pattern for GRAccess namedLike query. Use % as wildcard.")]
public string Pattern { get; init; } = "%";
[CommandOption("json", Description = "Output as JSON")]
public bool Json { get; init; }
[CommandOption("llm-json", Description = "Output stable machine JSON envelope")]
public bool LlmJson { get; init; }
public async ValueTask ExecuteAsync(IConsole console)
{
var args = new Dictionary<string, object>
{
["pattern"] = Pattern,
["json"] = Json.ToString(),
["llm-json"] = LlmJson
};
await CommandRouter.ExecuteAsync(
console,
GalaxyName,
NodeName,
"template",
"list",
args,
galaxy => GRAccessCommandDispatcher.Execute(galaxy, "template", "list", args))
.ConfigureAwait(false);
}
}
}
@@ -0,0 +1,92 @@
using System;
using System.Diagnostics;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using CliFx;
using CliFx.Attributes;
using CliFx.Infrastructure;
using ZB.MOM.WW.GRAccess.Cli.GRAccess;
using ZB.MOM.WW.GRAccess.Cli.Protocol;
using ZB.MOM.WW.GRAccess.Cli.Session;
namespace ZB.MOM.WW.GRAccess.Cli.Commands.Session
{
[Command("session start", Description = "Start a background GRAccess session")]
public class SessionStartCommand : ICommand
{
[CommandOption("galaxy", 'g', Description = "Galaxy name", IsRequired = true)]
public string GalaxyName { get; init; }
[CommandOption("node", 'n', Description = "GR node name", IsRequired = true)]
public string NodeName { get; init; }
[CommandOption("idle-timeout", Description = "Idle timeout in minutes")]
public int IdleTimeoutMinutes { get; init; } = 30;
public async ValueTask ExecuteAsync(IConsole console)
{
var nodeName = GRAccessDiagnostics.NormalizeNodeName(NodeName);
// Check if session already running
var existing = SessionInfo.Load(GalaxyName);
if (existing != null && existing.IsAlive())
{
await console.Output.WriteLineAsync(
$"Session already running for galaxy '{GalaxyName}' (PID {existing.ProcessId})")
.ConfigureAwait(false);
return;
}
// Clean up stale session file
existing?.Delete();
// Launch self as background daemon
var exePath = Assembly.GetExecutingAssembly().Location;
var startInfo = new ProcessStartInfo
{
FileName = exePath,
Arguments = $"--daemon --galaxy \"{GalaxyName}\" --node \"{nodeName}\" --idle-timeout {IdleTimeoutMinutes}",
UseShellExecute = false,
CreateNoWindow = true,
WindowStyle = ProcessWindowStyle.Hidden
};
var process = Process.Start(startInfo);
if (process == null)
throw new InvalidOperationException("Failed to start session daemon process.");
// Wait for daemon to become ready
var deadline = DateTime.UtcNow.AddSeconds(90);
bool ready = false;
while (DateTime.UtcNow < deadline)
{
Thread.Sleep(500);
if (SessionClient.TryConnect(GalaxyName, out var client))
{
client.Dispose();
ready = true;
break;
}
// Check if daemon process exited early (connection failure, etc.)
if (process.HasExited)
{
throw new InvalidOperationException(
$"Daemon process exited with code {process.ExitCode}. Check logs for details.");
}
}
if (ready)
{
await console.Output.WriteLineAsync(
$"Session started for galaxy '{GalaxyName}' (PID {process.Id})")
.ConfigureAwait(false);
}
else
{
throw new TimeoutException(
"Session daemon did not become ready within 90 seconds. Check logs for details.");
}
}
}
}
@@ -0,0 +1,40 @@
using System.Threading.Tasks;
using CliFx;
using CliFx.Attributes;
using CliFx.Infrastructure;
using ZB.MOM.WW.GRAccess.Cli.Session;
namespace ZB.MOM.WW.GRAccess.Cli.Commands.Session
{
[Command("session status", Description = "Show GRAccess session status")]
public class SessionStatusCommand : ICommand
{
[CommandOption("galaxy", 'g', Description = "Galaxy name", IsRequired = true)]
public string GalaxyName { get; init; }
public async ValueTask ExecuteAsync(IConsole console)
{
var info = SessionInfo.Load(GalaxyName);
if (info == null)
{
await console.Output.WriteLineAsync("No session found.").ConfigureAwait(false);
return;
}
if (!info.IsAlive())
{
await console.Output.WriteLineAsync(
"Session file exists but daemon is not running (stale). Cleaning up.")
.ConfigureAwait(false);
info.Delete();
return;
}
await console.Output.WriteLineAsync($"Galaxy: {info.GalaxyName}").ConfigureAwait(false);
await console.Output.WriteLineAsync($"Node: {info.NodeName}").ConfigureAwait(false);
await console.Output.WriteLineAsync($"PID: {info.ProcessId}").ConfigureAwait(false);
await console.Output.WriteLineAsync($"Started: {info.StartedAtUtc:u}").ConfigureAwait(false);
await console.Output.WriteLineAsync($"Pipe: {info.PipeName}").ConfigureAwait(false);
}
}
}
@@ -0,0 +1,34 @@
using System.Threading.Tasks;
using CliFx;
using CliFx.Attributes;
using CliFx.Infrastructure;
using ZB.MOM.WW.GRAccess.Cli.Session;
namespace ZB.MOM.WW.GRAccess.Cli.Commands.Session
{
[Command("session stop", Description = "Stop the background GRAccess session")]
public class SessionStopCommand : ICommand
{
[CommandOption("galaxy", 'g', Description = "Galaxy name", IsRequired = true)]
public string GalaxyName { get; init; }
public async ValueTask ExecuteAsync(IConsole console)
{
if (!SessionClient.TryConnect(GalaxyName, out var client))
{
await console.Output.WriteLineAsync("No active session found.").ConfigureAwait(false);
return;
}
using (client)
{
var response = await client.SendShutdownAsync().ConfigureAwait(false);
if (response.Success)
await console.Output.WriteLineAsync("Session stopped.").ConfigureAwait(false);
else
await console.Error.WriteLineAsync($"Error stopping session: {response.Error}")
.ConfigureAwait(false);
}
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,102 @@
using System;
using System.Runtime.InteropServices;
using ArchestrA.GRAccess;
using Serilog;
namespace ZB.MOM.WW.GRAccess.Cli.GRAccess
{
public sealed class GRAccessConnection : IDisposable
{
private static readonly ILogger Log = Serilog.Log.ForContext<GRAccessConnection>();
private GRAccessApp _grAccessApp;
public string GalaxyName { get; }
public string NodeName { get; }
public IGalaxy Galaxy { get; private set; }
public bool IsConnected { get; private set; }
public GRAccessConnection(string galaxyName, string nodeName)
{
GalaxyName = galaxyName ?? throw new ArgumentNullException(nameof(galaxyName));
NodeName = GRAccessDiagnostics.NormalizeNodeName(
nodeName ?? throw new ArgumentNullException(nameof(nodeName)));
}
/// <summary>
/// Must be called on an STA thread.
/// </summary>
public void Connect()
{
Log.Information("Connecting to galaxy {Galaxy} on {Node}", GalaxyName, NodeName);
_grAccessApp = new GRAccessAppClass();
var galaxies = _grAccessApp.QueryGalaxies(NodeName);
GRAccessDiagnostics.ThrowIfFailed(_grAccessApp.CommandResult, "QueryGalaxies");
if (galaxies == null || galaxies.count == 0)
throw new InvalidOperationException($"No galaxies found on node '{NodeName}'.");
Galaxy = galaxies[GalaxyName];
if (Galaxy == null)
throw new InvalidOperationException($"Galaxy '{GalaxyName}' not found on node '{NodeName}'.");
Galaxy.Login("", "");
GRAccessDiagnostics.ThrowIfFailed(Galaxy.CommandResult, "Login");
IsConnected = true;
Log.Information("Connected to galaxy {Galaxy}", GalaxyName);
}
/// <summary>
/// Must be called on an STA thread.
/// </summary>
public void Disconnect()
{
if (!IsConnected && Galaxy == null && _grAccessApp == null)
return;
try
{
if (IsConnected)
{
Galaxy?.Logout();
Log.Information("Disconnected from galaxy {Galaxy}", GalaxyName);
}
}
catch (Exception ex)
{
Log.Warning(ex, "Error during galaxy logout");
}
finally
{
ReleaseComObject(Galaxy);
ReleaseComObject(_grAccessApp);
Galaxy = null;
_grAccessApp = null;
IsConnected = false;
}
}
public void Dispose()
{
Disconnect();
}
private static void ReleaseComObject(object value)
{
if (value == null || !Marshal.IsComObject(value))
return;
try
{
Marshal.FinalReleaseComObject(value);
}
catch (Exception ex)
{
Log.Warning(ex, "Error releasing GRAccess COM object");
}
}
}
}
@@ -0,0 +1,34 @@
using System;
using ArchestrA.GRAccess;
namespace ZB.MOM.WW.GRAccess.Cli.GRAccess
{
internal static class GRAccessDiagnostics
{
public static string NormalizeNodeName(string nodeName)
{
if (string.IsNullOrWhiteSpace(nodeName))
return string.Empty;
var trimmed = nodeName.Trim();
return trimmed == "." ? Environment.MachineName : trimmed;
}
public static string FormatCommandResult(string operation, ICommandResult result)
{
if (result == null)
return $"{operation} failed: no GRAccess command result was returned.";
if (result.Successful)
return $"{operation}: OK";
return $"{operation} failed: ID={result.ID} ({(int)result.ID}); Text='{result.Text}'; CustomMessage='{result.CustomMessage}'";
}
public static void ThrowIfFailed(ICommandResult result, string operation)
{
if (result != null && !result.Successful)
throw new InvalidOperationException(FormatCommandResult(operation, result));
}
}
}
@@ -0,0 +1,123 @@
using System;
using System.Collections.Generic;
using System.Linq;
using ArchestrA.GRAccess;
using Newtonsoft.Json;
namespace ZB.MOM.WW.GRAccess.Cli.GRAccess
{
internal static class GRAccessQueryCommandHandler
{
public static string Execute(
IGalaxy galaxy,
string command,
string subcommand,
IDictionary<string, string> args)
{
command = (command ?? string.Empty).Trim().ToLowerInvariant();
subcommand = (subcommand ?? string.Empty).Trim().ToLowerInvariant();
args = args ?? new Dictionary<string, string>();
if (command == "object" && subcommand == "list")
return ExecuteObjectList(galaxy, args);
if (command == "template" && subcommand == "list")
return ExecuteTypedList(galaxy, args, GRAccessObjectKind.Template);
if (command == "instance" && subcommand == "list")
return ExecuteTypedList(galaxy, args, GRAccessObjectKind.Instance);
if (command == "object" && subcommand == "attributes")
return ExecuteObjectAttributes(galaxy, args);
throw new NotSupportedException($"Command '{command} {subcommand}' is not implemented.");
}
public static string ExecuteObjectList(IGalaxy galaxy, IDictionary<string, string> args)
{
var kind = ParseKind(GetArg(args, "type", "all"));
var pattern = GetArg(args, "pattern", "%");
var json = GetBoolArg(args, "json");
var objects = GRAccessQueryService.QueryObjects(galaxy, kind, pattern)
.OrderBy(o => o.Kind)
.ThenBy(o => o.Tagname)
.ToList();
if (json)
return JsonConvert.SerializeObject(objects, Formatting.Indented);
return string.Join(
Environment.NewLine,
objects.Select(o => $"{o.Kind}\t{o.Tagname}\t{o.HierarchicalName}"));
}
public static string ExecuteTypedList(
IGalaxy galaxy,
IDictionary<string, string> args,
GRAccessObjectKind kind)
{
var pattern = GetArg(args, "pattern", "%");
var json = GetBoolArg(args, "json");
var objects = GRAccessQueryService.QueryObjects(galaxy, kind, pattern)
.OrderBy(o => o.Tagname)
.ToList();
if (json)
return JsonConvert.SerializeObject(objects, Formatting.Indented);
return string.Join(Environment.NewLine, objects.Select(o => o.Tagname));
}
public static string ExecuteObjectAttributes(IGalaxy galaxy, IDictionary<string, string> args)
{
var kind = ParseKind(GetArg(args, "type", "all"));
var objectName = GetArg(args, "name", string.Empty);
var configurableOnly = GetBoolArg(args, "configurable");
var json = GetBoolArg(args, "json");
var attributes = GRAccessQueryService.QueryAttributes(
galaxy,
kind,
objectName,
configurableOnly)
.ToList();
if (json)
return JsonConvert.SerializeObject(attributes, Formatting.Indented);
return string.Join(
Environment.NewLine,
attributes.Select(a => $"{a.Name}\t{a.DataType}\t{a.Category}"));
}
public static GRAccessObjectKind ParseKind(string value)
{
switch ((value ?? string.Empty).Trim().ToLowerInvariant())
{
case "":
case "all":
return GRAccessObjectKind.All;
case "template":
case "templates":
return GRAccessObjectKind.Template;
case "instance":
case "instances":
return GRAccessObjectKind.Instance;
default:
throw new ArgumentException("Object type must be one of: all, template, instance.");
}
}
private static string GetArg(IDictionary<string, string> args, string key, string defaultValue)
{
return args != null && args.TryGetValue(key, out var value) ? value : defaultValue;
}
private static bool GetBoolArg(IDictionary<string, string> args, string key)
{
return args != null
&& args.TryGetValue(key, out var value)
&& bool.TryParse(value, out var parsed)
&& parsed;
}
}
}
@@ -0,0 +1,220 @@
using System;
using System.Collections.Generic;
using ArchestrA.GRAccess;
namespace ZB.MOM.WW.GRAccess.Cli.GRAccess
{
internal static class GRAccessQueryService
{
public static IReadOnlyList<GRAccessObjectInfo> QueryObjects(
IGalaxy galaxy,
GRAccessObjectKind kind,
string pattern)
{
if (galaxy == null) throw new ArgumentNullException(nameof(galaxy));
if (string.IsNullOrWhiteSpace(pattern)) pattern = "%";
var results = new List<GRAccessObjectInfo>();
if (kind == GRAccessObjectKind.All || kind == GRAccessObjectKind.Template)
results.AddRange(QueryObjects(galaxy, EgObjectIsTemplateOrInstance.gObjectIsTemplate, "template", pattern));
if (kind == GRAccessObjectKind.All || kind == GRAccessObjectKind.Instance)
results.AddRange(QueryObjects(galaxy, EgObjectIsTemplateOrInstance.gObjectIsInstance, "instance", pattern));
return results;
}
public static IReadOnlyList<GRAccessAttributeInfo> QueryAttributes(
IGalaxy galaxy,
GRAccessObjectKind kind,
string objectName,
bool configurableOnly)
{
if (galaxy == null) throw new ArgumentNullException(nameof(galaxy));
if (string.IsNullOrWhiteSpace(objectName))
throw new ArgumentException("Object name is required.", nameof(objectName));
var obj = FindSingleObject(galaxy, kind, objectName);
var attrs = configurableOnly ? obj.ConfigurableAttributes : obj.Attributes;
var results = new List<GRAccessAttributeInfo>();
if (attrs == null)
return results;
for (var i = 1; i <= attrs.count; i++)
{
var attr = attrs[i];
if (attr == null)
continue;
results.Add(new GRAccessAttributeInfo
{
Name = TryRead(() => attr.Name),
DataType = TryRead(() => attr.DataType.ToString()),
Category = TryRead(() => attr.AttributeCategory.ToString()),
SecurityClassification = TryRead(() => attr.SecurityClassification.ToString()),
Locked = TryRead(() => attr.Locked.ToString()),
UpperBoundDim1 = TryReadNullableShort(() => attr.UpperBoundDim1),
HasBuffer = TryReadNullableBool(() => attr.HasBuffer),
RuntimeSetHandler = TryReadNullableBool(() => attr.RtSethandler),
ConfigSetHandler = TryReadNullableBool(() => attr.CfgSethandler)
});
}
return results;
}
private static IEnumerable<GRAccessObjectInfo> QueryObjects(
IGalaxy galaxy,
EgObjectIsTemplateOrInstance templateOrInstance,
string kind,
string pattern)
{
var objects = galaxy.QueryObjects(
templateOrInstance,
EConditionType.namedLike,
pattern,
EMatch.MatchCondition);
GRAccessDiagnostics.ThrowIfFailed(galaxy.CommandResult, "QueryObjects");
if (objects == null)
yield break;
for (var i = 1; i <= objects.count; i++)
{
var obj = objects[i];
if (obj == null)
continue;
yield return new GRAccessObjectInfo
{
Kind = kind,
Tagname = obj.Tagname,
ContainedName = obj.ContainedName,
HierarchicalName = obj.HierarchicalName,
CheckoutStatus = TryRead(() => obj.CheckoutStatus.ToString())
};
}
}
private static IgObject FindSingleObject(IGalaxy galaxy, GRAccessObjectKind kind, string objectName)
{
if (kind == GRAccessObjectKind.All)
{
var template = FindSingleObjectOrNull(galaxy, EgObjectIsTemplateOrInstance.gObjectIsTemplate, objectName);
if (template != null)
return template;
var instance = FindSingleObjectOrNull(galaxy, EgObjectIsTemplateOrInstance.gObjectIsInstance, objectName);
if (instance != null)
return instance;
}
else
{
var templateOrInstance = kind == GRAccessObjectKind.Template
? EgObjectIsTemplateOrInstance.gObjectIsTemplate
: EgObjectIsTemplateOrInstance.gObjectIsInstance;
var obj = FindSingleObjectOrNull(galaxy, templateOrInstance, objectName);
if (obj != null)
return obj;
}
throw new InvalidOperationException($"Object '{objectName}' was not found.");
}
private static IgObject FindSingleObjectOrNull(
IGalaxy galaxy,
EgObjectIsTemplateOrInstance templateOrInstance,
string objectName)
{
var names = new[] { objectName };
var objects = galaxy.QueryObjectsByName(templateOrInstance, ref names);
GRAccessDiagnostics.ThrowIfFailed(galaxy.CommandResult, "QueryObjectsByName");
if (objects == null || objects.count == 0)
return null;
return objects[1];
}
private static string TryRead(Func<string> read)
{
try
{
return read() ?? string.Empty;
}
catch
{
return string.Empty;
}
}
private static bool? TryReadNullableBool(Func<bool> read)
{
try
{
return read();
}
catch
{
return null;
}
}
private static short? TryReadNullableShort(Func<short> read)
{
try
{
return read();
}
catch
{
return null;
}
}
}
internal enum GRAccessObjectKind
{
All,
Template,
Instance
}
public sealed class GRAccessObjectInfo
{
public string Kind { get; set; } = string.Empty;
public string Tagname { get; set; } = string.Empty;
public string ContainedName { get; set; } = string.Empty;
public string HierarchicalName { get; set; } = string.Empty;
public string CheckoutStatus { get; set; } = string.Empty;
public string DerivedFrom { get; set; } = string.Empty;
public string BasedOn { get; set; } = string.Empty;
public string Area { get; set; } = string.Empty;
public string Host { get; set; } = string.Empty;
public string Container { get; set; } = string.Empty;
public string Toolset { get; set; } = string.Empty;
public string SecurityGroup { get; set; } = string.Empty;
public string ConfigVersion { get; set; } = string.Empty;
public string DeploymentStatus { get; set; } = string.Empty;
public string DeployedVersion { get; set; } = string.Empty;
public string ValidationErrors { get; set; } = string.Empty;
public string ValidationWarnings { get; set; } = string.Empty;
}
internal sealed class GRAccessAttributeInfo
{
public string Name { get; set; } = string.Empty;
public string DataType { get; set; } = string.Empty;
public string Category { get; set; } = string.Empty;
public string SecurityClassification { get; set; } = string.Empty;
public string Locked { get; set; } = string.Empty;
public short? UpperBoundDim1 { get; set; }
public bool? HasBuffer { get; set; }
public bool? RuntimeSetHandler { get; set; }
public bool? ConfigSetHandler { get; set; }
}
}
@@ -0,0 +1,98 @@
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
namespace ZB.MOM.WW.GRAccess.Cli.GRAccess
{
public sealed class LlmUnavailableField
{
[JsonProperty("field")]
public string Field { get; set; } = string.Empty;
[JsonProperty("reason")]
public string Reason { get; set; } = string.Empty;
}
public sealed class LlmError
{
[JsonProperty("message")]
public string Message { get; set; } = string.Empty;
[JsonProperty("type")]
public string Type { get; set; } = string.Empty;
}
public sealed class LlmResponse
{
[JsonProperty("success")]
public bool Success { get; set; }
[JsonProperty("command")]
public string Command { get; set; } = string.Empty;
[JsonProperty("galaxy")]
public string Galaxy { get; set; } = string.Empty;
[JsonProperty("target")]
public string Target { get; set; } = string.Empty;
[JsonProperty("data")]
public object Data { get; set; }
[JsonProperty("commandResult")]
public object CommandResult { get; set; }
[JsonProperty("warnings")]
public List<string> Warnings { get; set; } = new List<string>();
[JsonProperty("unavailable")]
public List<LlmUnavailableField> Unavailable { get; set; } = new List<LlmUnavailableField>();
[JsonProperty("error")]
public LlmError Error { get; set; }
[JsonProperty("exitCode")]
public int ExitCode { get; set; }
public static string Ok(string command, string galaxy, string target, object data, object commandResult = null, IEnumerable<string> warnings = null, IEnumerable<LlmUnavailableField> unavailable = null)
{
return Serialize(new LlmResponse
{
Success = true,
Command = command,
Galaxy = galaxy ?? string.Empty,
Target = target ?? string.Empty,
Data = data,
CommandResult = commandResult,
Warnings = warnings == null ? new List<string>() : new List<string>(warnings),
Unavailable = unavailable == null ? new List<LlmUnavailableField>() : new List<LlmUnavailableField>(unavailable),
ExitCode = 0
});
}
public static string Fail(string command, string galaxy, string target, Exception exception, int exitCode = 1)
{
return Serialize(new LlmResponse
{
Success = false,
Command = command ?? string.Empty,
Galaxy = galaxy ?? string.Empty,
Target = target ?? string.Empty,
Error = new LlmError
{
Message = exception?.Message ?? "Unknown error",
Type = exception?.GetType().Name ?? "Error"
},
ExitCode = exitCode
});
}
public static string Serialize(object value)
{
return JsonConvert.SerializeObject(value, Formatting.Indented, new JsonSerializerSettings
{
NullValueHandling = NullValueHandling.Include
});
}
}
}
@@ -0,0 +1,485 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using Newtonsoft.Json.Linq;
namespace ZB.MOM.WW.GRAccess.Cli.GRAccess
{
public sealed class PackageAttributeValue
{
public string Name { get; set; } = string.Empty;
public string DataType { get; set; } = string.Empty;
public object Value { get; set; }
public string Source { get; set; } = "package";
}
public sealed class PackageScriptBody
{
public string Name { get; set; } = string.Empty;
public string Body { get; set; } = string.Empty;
public string Source { get; set; } = "package";
}
public sealed class PackageSnapshot
{
public List<string> Lineage { get; } = new List<string>();
public List<GRAccessObjectInfo> Children { get; } = new List<GRAccessObjectInfo>();
public List<GRAccessObjectInfo> ContainedObjects { get; } = new List<GRAccessObjectInfo>();
public List<PackageAttributeValue> AttributeValues { get; } = new List<PackageAttributeValue>();
public List<PackageScriptBody> ScriptBodies { get; } = new List<PackageScriptBody>();
public List<LlmUnavailableField> Unavailable { get; } = new List<LlmUnavailableField>();
public bool PackageFallbackUsed { get; set; }
public string Source { get; set; } = "direct-graccess";
}
public static class PackageSnapshotParser
{
private static readonly Regex LineageRegex = new Regex(@"Lineage\s*[:=]\s*(?<value>.+)$", RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.Compiled);
private static readonly Regex DerivedRegex = new Regex(@"DerivedFrom\s*[:=]\s*(?<value>[^\r\n<]+)", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex BasedOnRegex = new Regex(@"BasedOn\s*[:=]\s*(?<value>[^\r\n<]+)", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex XmlValueRegex = new Regex(@"<(?<tag>DerivedFrom|BasedOn)>\s*(?<value>[^<]+)\s*</\k<tag>>", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex AttributeRegex = new Regex(@"AttributeValue\s+Name=""(?<name>[^""]+)""(?:\s+DataType=""(?<type>[^""]*)"")?\s+Value=""(?<value>[^""]*)""", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex AttributeKeyRegex = new Regex(@"AttributeValues\.(?<name>[^=\r\n]+)\s*=\s*(?<value>[^\r\n]*)", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex ScriptRegex = new Regex(@"ScriptBody\s+Name=""(?<name>[^""]+)""\s*>\s*(?<body>.*?)\s*</ScriptBody>", RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.Compiled);
private static readonly Regex ScriptKeyRegex = new Regex(@"ScriptBodies\.(?<name>[^=\r\n]+)\s*=\s*(?<value>[\s\S]*?)(?=\r?\n[A-Za-z0-9_.-]+\s*=|\z)", RegexOptions.IgnoreCase | RegexOptions.Compiled);
public static PackageSnapshot Parse(string path)
{
var snapshot = new PackageSnapshot { PackageFallbackUsed = true, Source = "export-package" };
var entries = ReadTextEntries(path).ToList();
if (!entries.Any() && File.Exists(path))
{
try
{
entries.Add(File.ReadAllText(path));
}
catch
{
snapshot.Unavailable.Add(new LlmUnavailableField { Field = "package", Reason = "Package file could not be read as text." });
}
}
foreach (var text in entries)
ParseText(text, snapshot);
if (File.Exists(path) && !LooksLikeZip(path))
{
try
{
var text = File.ReadAllText(path);
ParseText(text, snapshot);
ParseLineageLines(text, snapshot);
}
catch
{
// Directory and archive package paths are handled above.
}
}
Deduplicate(snapshot);
return snapshot;
}
private static IEnumerable<string> ReadTextEntries(string path)
{
if (Directory.Exists(path))
{
foreach (var file in Directory.EnumerateFiles(path, "*.*", SearchOption.AllDirectories))
foreach (var text in ReadPackageFile(file, 0))
yield return text;
yield break;
}
if (!File.Exists(path))
yield break;
foreach (var text in ReadPackageFile(path, 0))
yield return text;
}
private static IEnumerable<string> ReadPackageFile(string path, int depth)
{
byte[] bytes;
try
{
bytes = File.ReadAllBytes(path);
}
catch
{
yield break;
}
foreach (var text in ReadPackageBytes(bytes, path, depth))
yield return text;
}
private static IEnumerable<string> ReadPackageBytes(byte[] bytes, string name, int depth)
{
if (bytes == null || bytes.Length == 0 || depth > 6)
yield break;
if (LooksLikeZip(bytes))
{
using (var stream = new MemoryStream(bytes))
using (var archive = new ZipArchive(stream, ZipArchiveMode.Read))
{
foreach (var entry in archive.Entries)
{
if (entry.Length == 0)
continue;
byte[] entryBytes;
using (var entryStream = entry.Open())
using (var copy = new MemoryStream())
{
entryStream.CopyTo(copy);
entryBytes = copy.ToArray();
}
foreach (var text in ReadPackageBytes(entryBytes, $"{name}!{entry.FullName}", depth + 1))
yield return text;
}
}
yield break;
}
if (LooksTextual(name))
{
var utf8 = ReadText(bytes, Encoding.UTF8);
if (!string.IsNullOrWhiteSpace(utf8))
yield return utf8;
}
var utf16Strings = ExtractUtf16Strings(bytes);
if (!string.IsNullOrWhiteSpace(utf16Strings))
yield return utf16Strings;
}
private static IEnumerable<string> ReadTextFile(string path)
{
string text;
try
{
text = File.ReadAllText(path);
}
catch
{
yield break;
}
if (!string.IsNullOrWhiteSpace(text))
yield return text;
}
private static bool LooksLikeZip(string path)
{
try
{
using (var stream = File.OpenRead(path))
{
if (stream.Length < 4)
return false;
return stream.ReadByte() == 0x50 && stream.ReadByte() == 0x4b;
}
}
catch
{
return false;
}
}
private static bool LooksLikeZip(byte[] bytes)
{
return bytes.Length >= 4 && bytes[0] == 0x50 && bytes[1] == 0x4b;
}
private static bool LooksTextual(string path)
{
var ext = Path.GetExtension(path).ToLowerInvariant();
return ext == ".xml" || ext == ".json" || ext == ".txt" || ext == ".csv" || ext == ".ini" || ext == ".config" || ext == ".pkg" || ext == ".aapkg";
}
private static string ReadText(byte[] bytes, Encoding fallback)
{
try
{
using (var stream = new MemoryStream(bytes))
using (var reader = new StreamReader(stream, fallback, true))
return reader.ReadToEnd();
}
catch
{
return string.Empty;
}
}
private static string ExtractUtf16Strings(byte[] bytes)
{
var lines = new List<string>();
var builder = new StringBuilder();
for (var i = 0; i < bytes.Length - 1;)
{
var start = i;
builder.Clear();
while (i < bytes.Length - 1)
{
var lo = bytes[i];
var hi = bytes[i + 1];
if (hi == 0 && IsPackageTextByte(lo))
{
builder.Append((char)lo);
i += 2;
}
else
{
break;
}
}
if (builder.Length >= 4)
lines.Add(builder.ToString());
if (i == start)
i++;
}
return string.Join("\n", lines);
}
private static bool IsPackageTextByte(byte value)
{
return (value >= 32 && value <= 126) || value == 9 || value == 10 || value == 13;
}
private static void ParseText(string text, PackageSnapshot snapshot)
{
TryParseJson(text, snapshot);
ParseLineage(text, snapshot);
ParseAttributes(text, snapshot);
ParseScripts(text, snapshot);
}
private static void TryParseJson(string text, PackageSnapshot snapshot)
{
try
{
var token = JToken.Parse(text);
foreach (var item in token.SelectTokens("$..Lineage[*]").Values<string>())
AddLineage(snapshot, item);
foreach (var item in token.SelectTokens("$..Children[*]"))
AddObject(snapshot.Children, item);
foreach (var item in token.SelectTokens("$..ContainedObjects[*]"))
AddObject(snapshot.ContainedObjects, item);
foreach (var item in token.SelectTokens("$..AttributeValues[*]"))
AddAttribute(snapshot, item);
foreach (var item in token.SelectTokens("$..ScriptBodies[*]"))
AddScript(snapshot, item);
}
catch
{
// Non-JSON package entries are parsed by the text matchers.
}
}
private static void ParseLineage(string text, PackageSnapshot snapshot)
{
ParseLineageLines(text, snapshot);
foreach (Match match in LineageRegex.Matches(text))
foreach (var item in match.Groups["value"].Value.Split(new[] { "->", "|", "," }, StringSplitOptions.RemoveEmptyEntries))
AddLineage(snapshot, item);
foreach (Match match in DerivedRegex.Matches(text))
AddLineage(snapshot, match.Groups["value"].Value);
foreach (Match match in BasedOnRegex.Matches(text))
AddLineage(snapshot, match.Groups["value"].Value);
foreach (Match match in XmlValueRegex.Matches(text))
AddLineage(snapshot, match.Groups["value"].Value);
}
private static void ParseLineageLines(string text, PackageSnapshot snapshot)
{
foreach (var line in text.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None))
{
var trimmed = line.Trim();
if (!trimmed.StartsWith("Lineage:", StringComparison.OrdinalIgnoreCase) && !trimmed.StartsWith("Lineage=", StringComparison.OrdinalIgnoreCase))
continue;
var separator = trimmed.IndexOfAny(new[] { ':', '=' });
if (separator < 0)
continue;
var value = trimmed.Substring(separator + 1);
foreach (var item in value.Split(new[] { "->", "|", "," }, StringSplitOptions.RemoveEmptyEntries))
AddLineage(snapshot, item);
}
}
private static void ParseAttributes(string text, PackageSnapshot snapshot)
{
foreach (Match match in AttributeRegex.Matches(text))
{
snapshot.AttributeValues.Add(new PackageAttributeValue
{
Name = match.Groups["name"].Value.Trim(),
DataType = match.Groups["type"].Value.Trim(),
Value = match.Groups["value"].Value
});
}
foreach (Match match in AttributeKeyRegex.Matches(text))
{
snapshot.AttributeValues.Add(new PackageAttributeValue
{
Name = match.Groups["name"].Value.Trim(),
Value = match.Groups["value"].Value.Trim()
});
}
}
private static void ParseScripts(string text, PackageSnapshot snapshot)
{
foreach (Match match in ScriptRegex.Matches(text))
snapshot.ScriptBodies.Add(new PackageScriptBody { Name = match.Groups["name"].Value.Trim(), Body = match.Groups["body"].Value });
foreach (Match match in ScriptKeyRegex.Matches(text))
snapshot.ScriptBodies.Add(new PackageScriptBody { Name = match.Groups["name"].Value.Trim(), Body = match.Groups["value"].Value.Trim() });
ParseBinaryScriptRecords(text, snapshot);
}
private static void ParseBinaryScriptRecords(string text, PackageSnapshot snapshot)
{
var lines = text
.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None)
.Select(line => line.Trim())
.Where(line => !string.IsNullOrWhiteSpace(line))
.ToList();
for (var i = 0; i < lines.Count; i++)
{
var marker = lines[i];
if (!marker.EndsWith("_ScriptExtension", StringComparison.OrdinalIgnoreCase))
continue;
var baseName = marker.Substring(0, marker.Length - "_ScriptExtension".Length);
if (string.IsNullOrWhiteSpace(baseName))
continue;
for (var j = i + 1; j < Math.Min(lines.Count, i + 12); j++)
{
var body = lines[j];
if (!LooksLikeScriptBody(body))
continue;
AddScriptBody(snapshot, $"{baseName}.ExecuteText", body, "export-package:binary");
break;
}
}
}
private static bool LooksLikeScriptBody(string value)
{
if (string.IsNullOrWhiteSpace(value))
return false;
var trimmed = value.Trim();
if (trimmed.StartsWith("{", StringComparison.Ordinal) && trimmed.EndsWith("}", StringComparison.Ordinal))
return false;
if (trimmed.IndexOf("_ScriptExtension", StringComparison.OrdinalIgnoreCase) >= 0)
return false;
return trimmed.IndexOf("Me.", StringComparison.OrdinalIgnoreCase) >= 0
|| trimmed.IndexOf("System.", StringComparison.OrdinalIgnoreCase) >= 0
|| trimmed.IndexOf("ScriptExchange", StringComparison.OrdinalIgnoreCase) >= 0
|| trimmed.IndexOf(";", StringComparison.OrdinalIgnoreCase) >= 0
|| trimmed.IndexOf(":=", StringComparison.OrdinalIgnoreCase) >= 0;
}
private static void AddScriptBody(PackageSnapshot snapshot, string name, string body, string source)
{
snapshot.ScriptBodies.Add(new PackageScriptBody { Name = name, Body = body, Source = source });
snapshot.AttributeValues.Add(new PackageAttributeValue
{
Name = name,
DataType = "MxBigString",
Value = body,
Source = source
});
}
private static void AddObject(List<GRAccessObjectInfo> target, JToken token)
{
target.Add(new GRAccessObjectInfo
{
Kind = Value(token, "Kind"),
Tagname = Value(token, "Tagname", "Name"),
ContainedName = Value(token, "ContainedName"),
HierarchicalName = Value(token, "HierarchicalName"),
CheckoutStatus = Value(token, "CheckoutStatus"),
DerivedFrom = Value(token, "DerivedFrom"),
BasedOn = Value(token, "BasedOn"),
Container = Value(token, "Container")
});
}
private static void AddAttribute(PackageSnapshot snapshot, JToken token)
{
snapshot.AttributeValues.Add(new PackageAttributeValue
{
Name = Value(token, "Name", "Attribute"),
DataType = Value(token, "DataType"),
Value = token["Value"]?.ToObject<object>()
});
}
private static void AddScript(PackageSnapshot snapshot, JToken token)
{
snapshot.ScriptBodies.Add(new PackageScriptBody
{
Name = Value(token, "Name", "Script"),
Body = Value(token, "Body", "Text")
});
}
private static string Value(JToken token, params string[] names)
{
foreach (var name in names)
{
var value = token[name] ?? token[name.ToLowerInvariant()];
if (value != null)
return value.Type == JTokenType.Null ? string.Empty : value.ToString();
}
return string.Empty;
}
private static void AddLineage(PackageSnapshot snapshot, string value)
{
value = (value ?? string.Empty).Trim();
if (!string.IsNullOrWhiteSpace(value))
snapshot.Lineage.Add(value);
}
private static void Deduplicate(PackageSnapshot snapshot)
{
Replace(snapshot.Lineage, snapshot.Lineage.Where(s => !string.IsNullOrWhiteSpace(s)).Distinct(StringComparer.OrdinalIgnoreCase));
Replace(snapshot.Children, snapshot.Children.GroupBy(o => $"{o.Kind}:{o.Tagname}", StringComparer.OrdinalIgnoreCase).Select(g => g.First()));
Replace(snapshot.ContainedObjects, snapshot.ContainedObjects.GroupBy(o => $"{o.Kind}:{o.Tagname}", StringComparer.OrdinalIgnoreCase).Select(g => g.First()));
Replace(snapshot.AttributeValues, snapshot.AttributeValues.Where(a => !string.IsNullOrWhiteSpace(a.Name)).GroupBy(a => a.Name, StringComparer.OrdinalIgnoreCase).Select(g => g.First()));
Replace(snapshot.ScriptBodies, snapshot.ScriptBodies.Where(s => !string.IsNullOrWhiteSpace(s.Name)).GroupBy(s => s.Name, StringComparer.OrdinalIgnoreCase).Select(g => g.First()));
}
private static void Replace<T>(List<T> list, IEnumerable<T> values)
{
var materialized = values.ToList();
list.Clear();
list.AddRange(materialized);
}
}
}
@@ -0,0 +1,311 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace ZB.MOM.WW.GRAccess.Cli.Infrastructure
{
public sealed class CommandArgumentCapability
{
public string Name { get; set; } = string.Empty;
public string Type { get; set; } = "string";
public bool Required { get; set; }
public IReadOnlyList<string> Values { get; set; } = Array.Empty<string>();
}
public sealed class CommandCapability
{
public string Name { get; set; } = string.Empty;
public string Command { get; set; } = string.Empty;
public string Subcommand { get; set; } = string.Empty;
public bool Mutates { get; set; }
public bool RequiresConfirm { get; set; }
public string ConfirmTarget { get; set; } = string.Empty;
public bool RoutesThroughSession { get; set; } = true;
public bool SupportsJson { get; set; } = true;
public bool SupportsLlmJson { get; set; } = true;
public string OutputSchema { get; set; } = "Text";
public IReadOnlyList<CommandArgumentCapability> Args { get; set; } = Array.Empty<CommandArgumentCapability>();
}
public sealed class CommandValidationResult
{
public bool Valid => Errors.Count == 0;
public List<string> Errors { get; } = new List<string>();
public CommandCapability Capability { get; set; }
}
public static class CommandCapabilityRegistry
{
private static readonly IReadOnlyList<CommandCapability> _commands = Build();
public static IReadOnlyList<CommandCapability> Commands => _commands;
public static CommandCapability Find(string name)
{
return _commands.FirstOrDefault(c => string.Equals(c.Name, NormalizeName(name), StringComparison.OrdinalIgnoreCase));
}
public static CommandCapability Find(string command, string subcommand)
{
command = NormalizeToken(command);
subcommand = NormalizeToken(subcommand);
return _commands.FirstOrDefault(c =>
string.Equals(c.Command, command, StringComparison.OrdinalIgnoreCase)
&& string.Equals(c.Subcommand, subcommand, StringComparison.OrdinalIgnoreCase));
}
public static CommandValidationResult Validate(string commandName, IDictionary<string, object> args, bool requireMutationConfirmation)
{
var result = new CommandValidationResult();
var capability = Find(commandName);
if (capability == null)
{
result.Errors.Add($"Unknown command '{commandName}'.");
return result;
}
result.Capability = capability;
args = args ?? new Dictionary<string, object>();
foreach (var arg in capability.Args.Where(a => a.Required))
{
if (!args.TryGetValue(arg.Name, out var value) || value == null || string.IsNullOrWhiteSpace(Convert.ToString(value)))
result.Errors.Add($"Missing required argument '{arg.Name}'.");
}
if (requireMutationConfirmation && capability.RequiresConfirm)
{
if (!ReadBool(args, "confirm"))
result.Errors.Add("Missing required confirm=true.");
var expected = ExpectedConfirmTarget(capability, args);
var actual = ReadString(args, "confirm-target");
if (!string.IsNullOrWhiteSpace(expected) && !string.Equals(expected, actual, StringComparison.OrdinalIgnoreCase))
result.Errors.Add($"confirm-target must be '{expected}'.");
}
return result;
}
public static string ExpectedConfirmTarget(CommandCapability capability, IDictionary<string, object> args)
{
if (capability == null || args == null)
return string.Empty;
switch (capability.ConfirmTarget)
{
case "name": return ReadString(args, "name");
case "galaxy": return ReadString(args, "galaxy");
case "file": return ReadString(args, "file");
case "path": return ReadString(args, "path");
case "output": return ReadString(args, "output");
case "default": return ReadString(args, "default");
case "bulk":
var names = ReadList(args, "name").ToList();
if (names.Any()) return string.Join(",", names);
return string.Join(",", ReadList(args, "pattern"));
case "template":
return ReadString(args, "template");
default:
return capability.ConfirmTarget ?? string.Empty;
}
}
public static (string Command, string Subcommand) SplitCommandName(string commandName)
{
var normalized = NormalizeName(commandName);
var capability = Find(normalized);
if (capability != null)
return (capability.Command, capability.Subcommand);
var parts = normalized.Split(new[] { ' ' }, 2, StringSplitOptions.RemoveEmptyEntries);
return parts.Length == 1 ? (parts[0], string.Empty) : (parts[0], parts[1].Replace(' ', '-'));
}
private static IReadOnlyList<CommandCapability> Build()
{
var list = new List<CommandCapability>();
void Add(string name, bool mutates = false, string confirmTarget = "", bool session = true, string schema = "Object", params CommandArgumentCapability[] args)
{
var split = SplitStatic(name);
list.Add(new CommandCapability
{
Name = name,
Command = split.Command,
Subcommand = split.Subcommand,
Mutates = mutates,
RequiresConfirm = mutates,
ConfirmTarget = confirmTarget,
RoutesThroughSession = session,
OutputSchema = schema,
Args = args
});
}
var galaxy = Arg("galaxy", required: true);
var node = Arg("node");
var name = Arg("name", required: true);
var type = Arg("type", "enum", false, "all", "template", "instance");
var json = Arg("json", "bool");
var llm = Arg("llm-json", "bool");
var confirm = Arg("confirm", "bool");
var confirmTarget = Arg("confirm-target");
Add("capabilities", session: false, schema: "CapabilityList", args: new[] { json, llm });
Add("validate", session: false, schema: "ValidationResult", args: new[] { Arg("request", required: true), llm });
Add("batch", session: true, schema: "BatchResult", args: new[] { Arg("file", required: true), Arg("mode"), llm });
Add("galaxy list", session: false, schema: "StringArray", args: new[] { node, json, llm });
Add("galaxy info", args: new[] { galaxy, node, json, llm });
Add("galaxy sync", args: new[] { galaxy, node, json, llm });
Add("galaxy cdi-version", args: new[] { galaxy, node, json, llm });
Add("galaxy defaults get", args: new[] { galaxy, node, Arg("default", required: true), json, llm });
Add("galaxy defaults set", true, "default", args: new[] { galaxy, node, Arg("default", required: true), Arg("value", required: true), confirm, confirmTarget, llm });
Add("galaxy backup", true, "file", args: new[] { galaxy, node, Arg("file", required: true), confirm, confirmTarget, llm });
Add("galaxy restore", true, "file", args: new[] { galaxy, node, Arg("file", required: true), Arg("restore-older", "bool"), confirm, confirmTarget, llm });
Add("galaxy migrate", true, "galaxy", args: new[] { galaxy, node, confirm, confirmTarget, llm });
Add("galaxy import-objects", true, "file", args: new[] { galaxy, node, Arg("file", required: true), Arg("overwrite", "bool"), confirm, confirmTarget, llm });
Add("galaxy import-objects-ex", true, "file", args: new[] { galaxy, node, Arg("file", required: true), Arg("version-conflict", required: true), Arg("name-conflict", required: true), Arg("append-name"), confirm, confirmTarget, llm });
Add("galaxy import-script-library", true, "path", args: new[] { galaxy, node, Arg("path", required: true), confirm, confirmTarget, llm });
Add("galaxy export-all", args: new[] { galaxy, node, Arg("output", required: true), Arg("export-type"), json, llm });
Add("galaxy grload", true, "file", args: new[] { galaxy, node, Arg("file", required: true), Arg("mode"), confirm, confirmTarget, llm });
Add("galaxy create", true, "galaxy", session: false, args: new[] { Arg("galaxy", required: true), node, confirm, confirmTarget });
Add("galaxy create-from-template", true, "galaxy", session: false, args: new[] { Arg("galaxy", required: true), node, Arg("template", required: true), confirm, confirmTarget });
Add("galaxy delete", true, "galaxy", session: false, args: new[] { Arg("galaxy", required: true), node, confirm, confirmTarget });
Add("object list", args: new[] { galaxy, node, type, Arg("pattern"), json, llm });
Add("template list", args: new[] { galaxy, node, Arg("pattern"), json, llm });
Add("instance list", args: new[] { galaxy, node, Arg("pattern"), json, llm });
Add("object get", args: new[] { galaxy, node, name, type, json, llm });
Add("object snapshot", schema: "ObjectSnapshot", args: new[] { galaxy, node, name, type, llm });
Add("object lineage", schema: "ObjectLineage", args: new[] { galaxy, node, name, type, json, llm });
Add("object children", schema: "ObjectChildren", args: new[] { galaxy, node, name, type, json, llm });
Add("object query-name", args: new[] { galaxy, node, Arg("name", "array", true), type, json, llm });
Add("object query-condition", args: new[] { galaxy, node, Arg("condition"), Arg("value"), type, json, llm });
Add("object query-multi", args: new[] { galaxy, node, Arg("pattern", "array"), type, json, llm });
Add("object attributes", args: new[] { galaxy, node, name, type, Arg("configurable", "bool"), json, llm });
Add("object extended-attributes", args: new[] { galaxy, node, name, type, Arg("attribute"), Arg("level", "int"), json, llm });
Add("object help-url", args: new[] { galaxy, node, name, type, json, llm });
Add("object checkout", true, "name", args: new[] { galaxy, node, name, type, confirm, confirmTarget, llm, Arg("dry-run", "bool") });
Add("object checkin", true, "name", args: new[] { galaxy, node, name, type, Arg("comment"), confirm, confirmTarget, llm, Arg("dry-run", "bool") });
Add("object undo-checkout", true, "name", args: new[] { galaxy, node, name, type, confirm, confirmTarget, llm, Arg("dry-run", "bool") });
Add("object save", true, "name", args: new[] { galaxy, node, name, type, confirm, confirmTarget, llm, Arg("dry-run", "bool") });
Add("object unload", true, "name", args: new[] { galaxy, node, name, type, confirm, confirmTarget, llm, Arg("dry-run", "bool") });
Add("object set", true, "name", args: new[] { galaxy, node, name, type, Arg("property", required: true), Arg("value", required: true), confirm, confirmTarget, llm, Arg("dry-run", "bool") });
Add("object attribute get", args: new[] { galaxy, node, name, type, Arg("attribute", required: true), json, llm });
Add("object attribute set", true, "name", args: new[] { galaxy, node, name, type, Arg("attribute", required: true), Arg("value", required: true), Arg("data-type"), confirm, confirmTarget, llm, Arg("dry-run", "bool") });
Add("object attribute value get", args: new[] { galaxy, node, name, type, Arg("attribute", required: true), json, llm });
Add("object attribute value set", true, "name", args: new[] { galaxy, node, name, type, Arg("attribute", required: true), Arg("value", required: true), Arg("data-type"), confirm, confirmTarget, llm, Arg("dry-run", "bool") });
Add("object attribute lock", true, "name", args: new[] { galaxy, node, name, type, Arg("attribute", required: true), Arg("locked", required: true), confirm, confirmTarget, llm, Arg("dry-run", "bool") });
Add("object attribute security", true, "name", args: new[] { galaxy, node, name, type, Arg("attribute", required: true), Arg("security", required: true), confirm, confirmTarget, llm, Arg("dry-run", "bool") });
Add("object attribute buffer", true, "name", args: new[] { galaxy, node, name, type, Arg("attribute", required: true), Arg("has-buffer", "bool"), confirm, confirmTarget, llm, Arg("dry-run", "bool") });
Add("template derive", true, "name", args: new[] { galaxy, node, name, type, Arg("new-name", required: true), Arg("create-contained", "bool"), confirm, confirmTarget, llm, Arg("dry-run", "bool") });
Add("template instantiate", true, "name", args: new[] { galaxy, node, name, type, Arg("new-name", required: true), Arg("create-contained", "bool"), confirm, confirmTarget, llm, Arg("dry-run", "bool") });
Add("template delete", true, "name", args: new[] { galaxy, node, name, type, Arg("force-option"), confirm, confirmTarget, llm, Arg("dry-run", "bool") });
Add("instance delete", true, "name", args: new[] { galaxy, node, name, type, Arg("force-option"), confirm, confirmTarget, llm, Arg("dry-run", "bool") });
Add("instance deploy", true, "name", args: new[] { galaxy, node, name, type, confirm, confirmTarget, llm, Arg("dry-run", "bool") });
Add("instance undeploy", true, "name", args: new[] { galaxy, node, name, type, confirm, confirmTarget, llm, Arg("dry-run", "bool") });
Add("instance upload", true, "name", args: new[] { galaxy, node, name, type, confirm, confirmTarget, llm, Arg("dry-run", "bool") });
Add("instance assign-area", true, "name", args: new[] { galaxy, node, name, Arg("area", required: true), confirm, confirmTarget, llm, Arg("dry-run", "bool") });
Add("instance assign-engine", true, "name", args: new[] { galaxy, node, name, Arg("engine", required: true), confirm, confirmTarget, llm, Arg("dry-run", "bool") });
Add("instance assign-container", true, "name", args: new[] { galaxy, node, name, Arg("container", required: true), confirm, confirmTarget, llm, Arg("dry-run", "bool") });
Add("io assign", true, "name", args: new[] { galaxy, node, name, Arg("attribute", required: true), Arg("value", required: true), Arg("data-type"), confirm, confirmTarget, llm, Arg("dry-run", "bool") });
Add("objects checkout", true, "bulk", args: new[] { galaxy, node, Arg("name", "array"), Arg("pattern", "array"), type, confirm, confirmTarget, llm, Arg("dry-run", "bool") });
Add("objects checkin", true, "bulk", args: new[] { galaxy, node, Arg("name", "array"), Arg("pattern", "array"), type, Arg("comment"), confirm, confirmTarget, llm, Arg("dry-run", "bool") });
Add("objects undo-checkout", true, "bulk", args: new[] { galaxy, node, Arg("name", "array"), Arg("pattern", "array"), type, confirm, confirmTarget, llm, Arg("dry-run", "bool") });
Add("objects deploy", true, "bulk", args: new[] { galaxy, node, Arg("name", "array"), Arg("pattern", "array"), type, confirm, confirmTarget, llm, Arg("dry-run", "bool") });
Add("objects undeploy", true, "bulk", args: new[] { galaxy, node, Arg("name", "array"), Arg("pattern", "array"), type, confirm, confirmTarget, llm, Arg("dry-run", "bool") });
Add("objects upload", true, "bulk", args: new[] { galaxy, node, Arg("name", "array"), Arg("pattern", "array"), type, confirm, confirmTarget, llm, Arg("dry-run", "bool") });
Add("objects delete", true, "bulk", args: new[] { galaxy, node, Arg("name", "array"), Arg("pattern", "array"), type, confirm, confirmTarget, llm, Arg("dry-run", "bool") });
Add("objects export", true, "output", args: new[] { galaxy, node, Arg("name", "array"), Arg("pattern", "array"), type, Arg("output", required: true), Arg("export-type"), confirm, confirmTarget, llm });
Add("objects export-protected", true, "output", args: new[] { galaxy, node, Arg("name", "array"), Arg("pattern", "array"), type, Arg("output", required: true), confirm, confirmTarget, llm });
Add("object scripts list", args: new[] { galaxy, node, name, type, json, llm });
Add("object scripts get", args: new[] { galaxy, node, name, type, Arg("script", required: true), json, llm });
Add("object scripts set", true, "name", args: new[] { galaxy, node, name, type, Arg("script", required: true), Arg("file", required: true), confirm, confirmTarget, llm, Arg("dry-run", "bool") });
Add("object scripts create", true, "name", args: new[] { galaxy, node, name, type, Arg("script", required: true), Arg("file"), Arg("trigger-period-ms"), Arg("trigger-type"), Arg("expression"), Arg("lock-trigger-period", "bool"), confirm, confirmTarget, llm, Arg("dry-run", "bool") });
Add("object scripts settings set", true, "name", args: new[] { galaxy, node, name, type, Arg("script", required: true), Arg("trigger-period-ms"), Arg("trigger-type"), Arg("expression"), Arg("lock-trigger-period", "bool"), confirm, confirmTarget, llm, Arg("dry-run", "bool") });
Add("area list", args: new[] { galaxy, node, Arg("pattern"), json, llm });
Add("area create", true, "template", args: new[] { galaxy, node, Arg("template", required: true), Arg("name", required: true), Arg("create-contained", "bool"), confirm, confirmTarget, llm, Arg("dry-run", "bool") });
Add("engine list", args: new[] { galaxy, node, Arg("pattern"), json, llm });
Add("engine create", true, "template", args: new[] { galaxy, node, Arg("template", required: true), Arg("name", required: true), Arg("create-contained", "bool"), confirm, confirmTarget, llm, Arg("dry-run", "bool") });
Add("toolset list", args: new[] { galaxy, node, json, llm });
Add("toolset tree", args: new[] { galaxy, node, json, llm });
Add("toolset add", true, "name", args: new[] { galaxy, node, name, confirm, confirmTarget, llm, Arg("dry-run", "bool") });
Add("toolset delete", true, "name", args: new[] { galaxy, node, name, confirm, confirmTarget, llm, Arg("dry-run", "bool") });
Add("toolset rename", true, "name", args: new[] { galaxy, node, name, Arg("new-name", required: true), confirm, confirmTarget, llm, Arg("dry-run", "bool") });
Add("toolset move", true, "name", args: new[] { galaxy, node, name, Arg("parent", required: true), confirm, confirmTarget, llm, Arg("dry-run", "bool") });
Add("script-library list", args: new[] { galaxy, node, json, llm });
Add("script-library add", true, "path", args: new[] { galaxy, node, Arg("path", required: true), confirm, confirmTarget, llm, Arg("dry-run", "bool") });
Add("script-library import", true, "path", args: new[] { galaxy, node, Arg("path", required: true), confirm, confirmTarget, llm, Arg("dry-run", "bool") });
Add("script-library export", true, "output", args: new[] { galaxy, node, name, Arg("output", required: true), confirm, confirmTarget, llm });
Add("security info", args: new[] { galaxy, node, json, llm });
Add("security roles", args: new[] { galaxy, node, json, llm });
Add("security users", args: new[] { galaxy, node, json, llm });
Add("security groups", args: new[] { galaxy, node, json, llm });
Add("security permissions", args: new[] { galaxy, node, Arg("role", required: true), json, llm });
Add("settings locale get", args: new[] { galaxy, node, json, llm });
Add("settings time-master get", args: new[] { galaxy, node, json, llm });
return list.OrderBy(c => c.Name, StringComparer.OrdinalIgnoreCase).ToList();
}
private static CommandArgumentCapability Arg(string name, string type = "string", bool required = false, params string[] values)
{
return new CommandArgumentCapability { Name = name, Type = type, Required = required, Values = values };
}
private static (string Command, string Subcommand) SplitStatic(string name)
{
var normalized = NormalizeName(name);
var first = normalized.Split(' ')[0];
return (first, normalized.Substring(first.Length).Trim().Replace(' ', '-'));
}
private static string NormalizeName(string value)
{
return string.Join(" ", (value ?? string.Empty).Trim().Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries));
}
private static string NormalizeToken(string value)
{
return (value ?? string.Empty).Trim().ToLowerInvariant();
}
private static bool ReadBool(IDictionary<string, object> args, string key)
{
return args.TryGetValue(key, out var value) && value != null && (value is bool b ? b : bool.TryParse(Convert.ToString(value), out var parsed) && parsed);
}
private static string ReadString(IDictionary<string, object> args, string key)
{
return args.TryGetValue(key, out var value) && value != null ? Convert.ToString(value) ?? string.Empty : string.Empty;
}
private static IEnumerable<string> ReadList(IDictionary<string, object> args, string key)
{
if (!args.TryGetValue(key, out var value) || value == null)
yield break;
if (value is string s)
{
foreach (var item in s.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
yield return item.Trim();
yield break;
}
if (value is System.Collections.IEnumerable enumerable)
{
foreach (var item in enumerable)
if (item != null)
yield return Convert.ToString(item);
}
}
}
}
@@ -0,0 +1,134 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using CliFx.Exceptions;
using CliFx.Infrastructure;
using ZB.MOM.WW.GRAccess.Cli.Protocol;
using ZB.MOM.WW.GRAccess.Cli.Session;
using ArchestrA.GRAccess;
using ZB.MOM.WW.GRAccess.Cli.GRAccess;
namespace ZB.MOM.WW.GRAccess.Cli.Infrastructure
{
public static class CommandRouter
{
/// <summary>
/// Execute a command either via an active session (named pipe) or direct one-shot connection.
/// </summary>
/// <param name="console">CliFx console for output</param>
/// <param name="galaxyName">Galaxy to connect to</param>
/// <param name="nodeName">GR node (required for one-shot, ignored if session active)</param>
/// <param name="command">Command name (e.g. "query")</param>
/// <param name="subcommand">Subcommand name (e.g. "instances")</param>
/// <param name="args">Command arguments</param>
/// <param name="directExecute">Callback for one-shot execution against a live IGalaxy</param>
/// <returns>Exit code</returns>
public static async Task<int> ExecuteAsync(
IConsole console,
string galaxyName,
string nodeName,
string command,
string subcommand,
Dictionary<string, object> args,
Func<IGalaxy, string> directExecute)
{
nodeName = nodeName ?? string.Empty;
args["galaxy"] = galaxyName;
args["node"] = nodeName;
// 1. Try active session
if (SessionClient.TryConnect(galaxyName, out var client))
{
using (client)
{
var request = PipeRequest.Execute(command, subcommand, args);
var response = await client.SendCommandAsync(request).ConfigureAwait(false);
if (!response.Success)
{
if (IsLlmJson(args))
{
await console.Output.WriteLineAsync(LlmResponse.Fail(
DisplayCommand(command, subcommand),
galaxyName,
Target(args),
new InvalidOperationException(response.Error),
response.ExitCode)).ConfigureAwait(false);
return response.ExitCode;
}
throw new CommandException(response.Error, response.ExitCode);
}
if (!string.IsNullOrEmpty(response.Output))
await console.Output.WriteLineAsync(response.Output).ConfigureAwait(false);
return 0;
}
}
// 2. No session — one-shot mode (already on STA thread via [STAThread])
if (string.IsNullOrEmpty(nodeName))
{
if (IsLlmJson(args))
{
await console.Output.WriteLineAsync(LlmResponse.Fail(
DisplayCommand(command, subcommand),
galaxyName,
Target(args),
new InvalidOperationException("No active session found. Provide --node for one-shot mode, or start a session first."),
1)).ConfigureAwait(false);
return 1;
}
throw new CommandException(
"No active session found. Provide --node for one-shot mode, or start a session first.",
1);
}
using (var connection = new GRAccessConnection(galaxyName, nodeName))
{
try
{
connection.Connect();
var result = directExecute(connection.Galaxy);
if (!string.IsNullOrEmpty(result))
await console.Output.WriteLineAsync(result).ConfigureAwait(false);
return 0;
}
catch (Exception ex)
{
if (IsLlmJson(args))
{
await console.Output.WriteLineAsync(LlmResponse.Fail(DisplayCommand(command, subcommand), galaxyName, Target(args), ex, 1)).ConfigureAwait(false);
return 1;
}
throw new CommandException($"Error: {ex.Message}", 1);
}
}
}
private static bool IsLlmJson(Dictionary<string, object> args)
{
return args != null
&& args.TryGetValue("llm-json", out var value)
&& value != null
&& (value is bool b ? b : bool.TryParse(Convert.ToString(value), out var parsed) && parsed);
}
private static string DisplayCommand(string command, string subcommand)
{
return $"{command} {(subcommand ?? string.Empty).Replace('-', ' ')}".Trim();
}
private static string Target(Dictionary<string, object> args)
{
if (args == null) return string.Empty;
foreach (var key in new[] { "name", "output", "file", "path", "template", "galaxy", "pattern", "default" })
if (args.TryGetValue(key, out var value) && value != null)
return Convert.ToString(value) ?? string.Empty;
return string.Empty;
}
}
}
@@ -0,0 +1,7 @@
// Polyfill for 'init' accessor support on .NET Framework 4.8 (C# 9.0)
// See: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-9.0/init
namespace System.Runtime.CompilerServices
{
internal static class IsExternalInit { }
}
@@ -0,0 +1,41 @@
using System.Linq;
using System.Threading.Tasks;
using CliFx;
using Serilog;
using ZB.MOM.WW.GRAccess.Cli.Session;
namespace ZB.MOM.WW.GRAccess.Cli
{
internal static class Program
{
[System.STAThread]
static async Task<int> Main(string[] args)
{
// Hidden daemon mode: graccess.exe --daemon --galaxy X --node Y
if (args.Length >= 1 && args[0] == "--daemon")
return SessionDaemon.Run(args);
// Normal CLI mode. Machine JSON must remain parseable on stdout.
var llmJson = args.Any(arg => arg == "--llm-json");
Log.Logger = llmJson
? new LoggerConfiguration().CreateLogger()
: new LoggerConfiguration()
.WriteTo.Console()
.CreateLogger();
try
{
return await new CliApplicationBuilder()
.SetTitle("GRAccess CLI")
.SetDescription("Aveva System Platform Galaxy management")
.AddCommandsFromThisAssembly()
.Build()
.RunAsync(args);
}
finally
{
Log.CloseAndFlush();
}
}
}
}
@@ -0,0 +1,36 @@
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;
namespace ZB.MOM.WW.GRAccess.Cli.Protocol
{
public static class PipeProtocol
{
public const string PipePrefix = "graccess-session-";
public static string GetPipeName(string galaxyName)
{
return PipePrefix + galaxyName.ToLowerInvariant();
}
public static async Task WriteMessageAsync<T>(Stream stream, T message)
{
var json = JsonConvert.SerializeObject(message, Formatting.None);
var bytes = Encoding.UTF8.GetBytes(json + "\n");
await stream.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false);
await stream.FlushAsync().ConfigureAwait(false);
}
public static async Task<T> ReadMessageAsync<T>(Stream stream)
{
using (var reader = new StreamReader(stream, Encoding.UTF8, false, 4096, leaveOpen: true))
{
var line = await reader.ReadLineAsync().ConfigureAwait(false);
if (line == null)
return default;
return JsonConvert.DeserializeObject<T>(line);
}
}
}
}
@@ -0,0 +1,35 @@
using System.Collections.Generic;
using Newtonsoft.Json;
namespace ZB.MOM.WW.GRAccess.Cli.Protocol
{
public class PipeRequest
{
[JsonProperty("type")]
public string Type { get; set; } = "execute";
[JsonProperty("command")]
public string Command { get; set; }
[JsonProperty("subcommand")]
public string Subcommand { get; set; }
[JsonProperty("args")]
public Dictionary<string, object> Args { get; set; } = new Dictionary<string, object>();
public static PipeRequest Execute(string command, string subcommand, Dictionary<string, object> args = null)
{
return new PipeRequest
{
Type = "execute",
Command = command,
Subcommand = subcommand,
Args = args ?? new Dictionary<string, object>()
};
}
public static PipeRequest Shutdown() => new PipeRequest { Type = "shutdown" };
public static PipeRequest Status() => new PipeRequest { Type = "status" };
}
}
@@ -0,0 +1,33 @@
using Newtonsoft.Json;
namespace ZB.MOM.WW.GRAccess.Cli.Protocol
{
public class PipeResponse
{
[JsonProperty("success")]
public bool Success { get; set; }
[JsonProperty("output")]
public string Output { get; set; }
[JsonProperty("error")]
public string Error { get; set; }
[JsonProperty("exitCode")]
public int ExitCode { get; set; }
public static PipeResponse Ok(string output) => new PipeResponse
{
Success = true,
Output = output,
ExitCode = 0
};
public static PipeResponse Fail(string error, int exitCode = 1) => new PipeResponse
{
Success = false,
Error = error,
ExitCode = exitCode
};
}
}
@@ -0,0 +1,62 @@
using System;
using System.IO.Pipes;
using System.Threading.Tasks;
using ZB.MOM.WW.GRAccess.Cli.Protocol;
namespace ZB.MOM.WW.GRAccess.Cli.Session
{
public sealed class SessionClient : IDisposable
{
private readonly string _pipeName;
private SessionClient(string pipeName)
{
_pipeName = pipeName;
}
/// <summary>
/// Try to connect to a running session for the given galaxy.
/// Returns false if no session is available.
/// </summary>
public static bool TryConnect(string galaxyName, out SessionClient client)
{
client = null;
var info = SessionInfo.Load(galaxyName);
if (info == null || !info.IsAlive())
{
// Clean up stale file
info?.Delete();
return false;
}
client = new SessionClient(info.PipeName);
return true;
}
public async Task<PipeResponse> SendCommandAsync(PipeRequest request)
{
using (var pipe = new NamedPipeClientStream(".", _pipeName, PipeDirection.InOut))
{
pipe.Connect(5000);
await PipeProtocol.WriteMessageAsync(pipe, request).ConfigureAwait(false);
return await PipeProtocol.ReadMessageAsync<PipeResponse>(pipe).ConfigureAwait(false);
}
}
public Task<PipeResponse> SendShutdownAsync()
{
return SendCommandAsync(PipeRequest.Shutdown());
}
public Task<PipeResponse> SendStatusAsync()
{
return SendCommandAsync(PipeRequest.Status());
}
public void Dispose()
{
// No persistent resources to clean up — each call opens/closes its own pipe
}
}
}
@@ -0,0 +1,302 @@
using System;
using System.IO.Pipes;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Serilog;
using ZB.MOM.WW.GRAccess.Cli.GRAccess;
using ZB.MOM.WW.GRAccess.Cli.Protocol;
namespace ZB.MOM.WW.GRAccess.Cli.Session
{
public sealed class SessionDaemon : IDisposable
{
private static readonly ILogger Log = Serilog.Log.ForContext<SessionDaemon>();
private readonly string _galaxyName;
private readonly string _nodeName;
private readonly string _pipeName;
private readonly TimeSpan _idleTimeout;
private readonly CancellationTokenSource _shutdownCts = new CancellationTokenSource();
private StaComThread _staThread;
private GRAccessConnection _connection;
private Mutex _instanceMutex;
private DateTime _lastActivity;
private Timer _idleTimer;
public SessionDaemon(string galaxyName, string nodeName, TimeSpan idleTimeout)
{
_galaxyName = galaxyName;
_nodeName = nodeName;
_pipeName = PipeProtocol.GetPipeName(galaxyName);
_idleTimeout = idleTimeout;
_lastActivity = DateTime.UtcNow;
}
/// <summary>
/// Static entry point called from Program.Main when --daemon flag is present.
/// </summary>
public static int Run(string[] args)
{
string galaxyName = null;
string nodeName = null;
int idleTimeoutMinutes = 30;
for (int i = 1; i < args.Length; i++)
{
switch (args[i])
{
case "--galaxy" when i + 1 < args.Length:
galaxyName = args[++i];
break;
case "--node" when i + 1 < args.Length:
nodeName = args[++i];
break;
case "--idle-timeout" when i + 1 < args.Length:
int.TryParse(args[++i], out idleTimeoutMinutes);
break;
}
}
if (string.IsNullOrEmpty(galaxyName) || string.IsNullOrEmpty(nodeName))
{
Console.Error.WriteLine("--galaxy and --node are required for daemon mode.");
return 1;
}
// Configure daemon logging
Serilog.Log.Logger = new LoggerConfiguration()
.WriteTo.File(
System.IO.Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"ZB.MOM.WW.GRAccess.Cli", "logs", $"daemon-{galaxyName.ToLowerInvariant()}.log"),
rollingInterval: RollingInterval.Day,
retainedFileCountLimit: 7)
.CreateLogger();
try
{
using (var daemon = new SessionDaemon(galaxyName, nodeName,
TimeSpan.FromMinutes(idleTimeoutMinutes)))
{
return daemon.Start();
}
}
catch (Exception ex)
{
Log.Fatal(ex, "Daemon terminated unexpectedly");
return 1;
}
finally
{
Serilog.Log.CloseAndFlush();
}
}
public int Start()
{
// 1. Acquire mutex
var mutexName = $"Global\\graccess-session-{_galaxyName.ToLowerInvariant()}";
_instanceMutex = new Mutex(true, mutexName, out bool createdNew);
if (!createdNew)
{
Log.Warning("Another daemon is already running for galaxy {Galaxy}", _galaxyName);
return 1;
}
try
{
// 2. Start STA thread
_staThread = new StaComThread();
_staThread.Start();
// 3. Connect to galaxy on STA thread
_connection = new GRAccessConnection(_galaxyName, _nodeName);
_staThread.RunAsync(() => _connection.Connect()).GetAwaiter().GetResult();
// 4. Write session info
var sessionInfo = new SessionInfo
{
GalaxyName = _galaxyName,
NodeName = _nodeName,
PipeName = _pipeName,
ProcessId = System.Diagnostics.Process.GetCurrentProcess().Id,
StartedAtUtc = DateTime.UtcNow
};
sessionInfo.Save();
Log.Information("Daemon started for galaxy {Galaxy} on pipe {Pipe}", _galaxyName, _pipeName);
// 5. Start idle timer
_idleTimer = new Timer(_ => CheckIdleTimeout(), null,
TimeSpan.FromSeconds(30), TimeSpan.FromSeconds(30));
// 6. Accept connections (blocks until shutdown)
AcceptConnectionsAsync(_shutdownCts.Token).GetAwaiter().GetResult();
return 0;
}
catch (Exception ex)
{
Serilog.Log.Error(ex, "Daemon startup failed for galaxy {Galaxy} on node {Node}", _galaxyName, _nodeName);
throw;
}
finally
{
// 7. Cleanup
_idleTimer?.Dispose();
if (_connection != null)
{
try
{
_staThread.RunAsync(() => _connection.Disconnect()).GetAwaiter().GetResult();
}
catch (Exception ex)
{
Log.Warning(ex, "Error disconnecting during shutdown");
}
}
_staThread?.Dispose();
var info = SessionInfo.Load(_galaxyName);
info?.Delete();
_instanceMutex.ReleaseMutex();
_instanceMutex.Dispose();
Log.Information("Daemon stopped for galaxy {Galaxy}", _galaxyName);
}
}
private async Task AcceptConnectionsAsync(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
try
{
using (var server = new NamedPipeServerStream(
_pipeName,
PipeDirection.InOut,
NamedPipeServerStream.MaxAllowedServerInstances,
PipeTransmissionMode.Byte,
PipeOptions.Asynchronous))
{
var waitTask = Task.Factory.FromAsync(
server.BeginWaitForConnection, server.EndWaitForConnection, null);
// Wait for connection or cancellation
var completedTask = await Task.WhenAny(waitTask, Task.Delay(-1, ct))
.ConfigureAwait(false);
if (ct.IsCancellationRequested)
break;
await waitTask.ConfigureAwait(false);
_lastActivity = DateTime.UtcNow;
await HandleClientAsync(server).ConfigureAwait(false);
}
}
catch (OperationCanceledException)
{
break;
}
catch (Exception ex)
{
Log.Error(ex, "Error accepting pipe connection");
}
}
}
private async Task HandleClientAsync(NamedPipeServerStream server)
{
try
{
var request = await PipeProtocol.ReadMessageAsync<PipeRequest>(server).ConfigureAwait(false);
if (request == null)
return;
PipeResponse response;
switch (request.Type)
{
case "shutdown":
response = PipeResponse.Ok("Shutting down");
await PipeProtocol.WriteMessageAsync(server, response).ConfigureAwait(false);
_shutdownCts.Cancel();
return;
case "status":
var status = new
{
galaxy = _galaxyName,
node = _nodeName,
connected = _connection?.IsConnected ?? false,
uptime = (DateTime.UtcNow - _lastActivity).ToString()
};
response = PipeResponse.Ok(JsonConvert.SerializeObject(status));
break;
case "execute":
response = await ExecuteCommandAsync(request).ConfigureAwait(false);
break;
default:
response = PipeResponse.Fail($"Unknown request type: {request.Type}");
break;
}
await PipeProtocol.WriteMessageAsync(server, response).ConfigureAwait(false);
}
catch (Exception ex)
{
Log.Error(ex, "Error handling client request");
}
}
private async Task<PipeResponse> ExecuteCommandAsync(PipeRequest request)
{
try
{
return await _staThread.RunAsync<PipeResponse>(() =>
{
var output = GRAccessCommandDispatcher.Execute(
_connection.Galaxy,
request.Command,
request.Subcommand,
request.Args);
return PipeResponse.Ok(output);
}).ConfigureAwait(false);
}
catch (NotSupportedException ex)
{
return PipeResponse.Fail(ex.Message, 2);
}
catch (Exception ex)
{
return PipeResponse.Fail($"Execution error: {ex.Message}");
}
}
private void CheckIdleTimeout()
{
if (DateTime.UtcNow - _lastActivity > _idleTimeout)
{
Log.Information("Idle timeout reached ({Timeout} min), shutting down",
_idleTimeout.TotalMinutes);
_shutdownCts.Cancel();
}
}
public void Dispose()
{
_shutdownCts?.Cancel();
_idleTimer?.Dispose();
_shutdownCts?.Dispose();
}
}
}
@@ -0,0 +1,84 @@
using System;
using System.Diagnostics;
using System.IO;
using Newtonsoft.Json;
namespace ZB.MOM.WW.GRAccess.Cli.Session
{
public class SessionInfo
{
[JsonProperty("galaxyName")]
public string GalaxyName { get; set; }
[JsonProperty("nodeName")]
public string NodeName { get; set; }
[JsonProperty("pipeName")]
public string PipeName { get; set; }
[JsonProperty("processId")]
public int ProcessId { get; set; }
[JsonProperty("startedAtUtc")]
public DateTime StartedAtUtc { get; set; }
private static string SessionsDir =>
Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"ZB.MOM.WW.GRAccess.Cli",
"sessions");
public static string GetSessionFilePath(string galaxyName)
{
return Path.Combine(SessionsDir, galaxyName.ToLowerInvariant() + ".json");
}
public static SessionInfo Load(string galaxyName)
{
var path = GetSessionFilePath(galaxyName);
if (!File.Exists(path))
return null;
try
{
var json = File.ReadAllText(path);
return JsonConvert.DeserializeObject<SessionInfo>(json);
}
catch
{
return null;
}
}
public void Save()
{
var path = GetSessionFilePath(GalaxyName);
Directory.CreateDirectory(Path.GetDirectoryName(path));
var json = JsonConvert.SerializeObject(this, Formatting.Indented);
File.WriteAllText(path, json);
}
public void Delete()
{
var path = GetSessionFilePath(GalaxyName);
if (File.Exists(path))
File.Delete(path);
}
/// <summary>
/// Check if the daemon process is still running.
/// </summary>
public bool IsAlive()
{
try
{
var process = Process.GetProcessById(ProcessId);
return !process.HasExited;
}
catch
{
return false;
}
}
}
}
@@ -0,0 +1,215 @@
using System;
using System.Collections.Concurrent;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Serilog;
namespace ZB.MOM.WW.GRAccess.Cli.Session
{
/// <summary>
/// Dedicated STA thread with a raw Win32 message pump for COM interop.
/// All GRAccess COM objects must be created and called on this thread.
/// </summary>
public sealed class StaComThread : IDisposable
{
private const uint WM_APP = 0x8000;
private const uint PM_NOREMOVE = 0x0000;
private static readonly ILogger Log = Serilog.Log.ForContext<StaComThread>();
private readonly Thread _thread;
private readonly TaskCompletionSource<bool> _ready = new TaskCompletionSource<bool>();
private readonly ConcurrentQueue<Action> _workItems = new ConcurrentQueue<Action>();
private volatile uint _nativeThreadId;
private bool _disposed;
public StaComThread()
{
_thread = new Thread(ThreadEntry)
{
Name = "GRAccess-STA",
IsBackground = true
};
_thread.SetApartmentState(ApartmentState.STA);
}
/// <summary>
/// Starts the STA thread and waits until the message pump is running.
/// </summary>
public void Start()
{
_thread.Start();
_ready.Task.GetAwaiter().GetResult();
Log.Information("STA COM thread started (ThreadId={ThreadId})", _thread.ManagedThreadId);
}
/// <summary>
/// Marshals a synchronous action onto the STA thread and returns a Task
/// that completes when the action finishes.
/// </summary>
public Task RunAsync(Action action)
{
if (_disposed) throw new ObjectDisposedException(nameof(StaComThread));
var tcs = new TaskCompletionSource<bool>();
_workItems.Enqueue(() =>
{
try
{
action();
tcs.TrySetResult(true);
}
catch (Exception ex)
{
tcs.TrySetException(ex);
}
});
PostThreadMessage(_nativeThreadId, WM_APP, IntPtr.Zero, IntPtr.Zero);
return tcs.Task;
}
/// <summary>
/// Marshals a synchronous function onto the STA thread and returns
/// a Task&lt;T&gt; with the result.
/// </summary>
public Task<T> RunAsync<T>(Func<T> func)
{
if (_disposed) throw new ObjectDisposedException(nameof(StaComThread));
var tcs = new TaskCompletionSource<T>();
_workItems.Enqueue(() =>
{
try
{
tcs.TrySetResult(func());
}
catch (Exception ex)
{
tcs.TrySetException(ex);
}
});
PostThreadMessage(_nativeThreadId, WM_APP, IntPtr.Zero, IntPtr.Zero);
return tcs.Task;
}
public void Dispose()
{
if (_disposed) return;
_disposed = true;
try
{
if (_nativeThreadId != 0)
PostThreadMessage(_nativeThreadId, WM_APP + 1, IntPtr.Zero, IntPtr.Zero);
_thread.Join(TimeSpan.FromSeconds(5));
}
catch (Exception ex)
{
Log.Warning(ex, "Error shutting down STA COM thread");
}
Log.Information("STA COM thread stopped");
}
private void ThreadEntry()
{
try
{
_nativeThreadId = GetCurrentThreadId();
// Force message queue creation by peeking
MSG msg;
PeekMessage(out msg, IntPtr.Zero, 0, 0, PM_NOREMOVE);
_ready.TrySetResult(true);
// Run the message loop — blocks until WM_QUIT
while (GetMessage(out msg, IntPtr.Zero, 0, 0) > 0)
{
if (msg.message == WM_APP)
{
DrainQueue();
}
else if (msg.message == WM_APP + 1)
{
// Shutdown signal — drain remaining work then quit
DrainQueue();
PostQuitMessage(0);
}
else
{
TranslateMessage(ref msg);
DispatchMessage(ref msg);
}
}
}
catch (Exception ex)
{
Log.Error(ex, "STA COM thread crashed");
_ready.TrySetException(ex);
}
}
private void DrainQueue()
{
while (_workItems.TryDequeue(out var workItem))
{
try
{
workItem();
}
catch (Exception ex)
{
Log.Error(ex, "Unhandled exception in STA work item");
}
}
}
#region Win32 PInvoke
[StructLayout(LayoutKind.Sequential)]
private struct MSG
{
public IntPtr hwnd;
public uint message;
public IntPtr wParam;
public IntPtr lParam;
public uint time;
public POINT pt;
}
[StructLayout(LayoutKind.Sequential)]
private struct POINT
{
public int x;
public int y;
}
[DllImport("user32.dll")]
private static extern int GetMessage(out MSG lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool TranslateMessage(ref MSG lpMsg);
[DllImport("user32.dll")]
private static extern IntPtr DispatchMessage(ref MSG lpMsg);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool PostThreadMessage(uint idThread, uint Msg, IntPtr wParam, IntPtr lParam);
[DllImport("user32.dll")]
private static extern void PostQuitMessage(int nExitCode);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool PeekMessage(out MSG lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax, uint wRemoveMsg);
[DllImport("kernel32.dll")]
private static extern uint GetCurrentThreadId();
#endregion
}
}
@@ -0,0 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net48</TargetFramework>
<OutputType>Exe</OutputType>
<LangVersion>9.0</LangVersion>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<RootNamespace>ZB.MOM.WW.GRAccess.Cli</RootNamespace>
<AssemblyName>ZB.MOM.WW.GRAccess.Cli</AssemblyName>
<PlatformTarget>x86</PlatformTarget>
<Platforms>x86</Platforms>
<Prefer32Bit>true</Prefer32Bit>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CliFx" Version="2.3.6" />
<PackageReference Include="Serilog" Version="2.10.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="4.0.1" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
<ItemGroup>
<Reference Include="System.IO.Compression" />
<Reference Include="System.IO.Compression.FileSystem" />
<Reference Include="ArchestrA.GRAccess">
<HintPath>..\..\lib\ArchestrA.GRAccess.dll</HintPath>
<Private>true</Private>
</Reference>
</ItemGroup>
</Project>
@@ -0,0 +1,186 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using CliFx.Attributes;
using Shouldly;
using Xunit;
using ZB.MOM.WW.GRAccess.Cli.Commands;
using ZB.MOM.WW.GRAccess.Cli.Protocol;
namespace ZB.MOM.WW.GRAccess.Cli.Tests.Commands
{
public class GRAccessSurfaceCommandTests
{
[Theory]
[InlineData(typeof(GalaxyInfoCommand), "galaxy info")]
[InlineData(typeof(GalaxyImportScriptLibraryCommand), "galaxy import-script-library")]
[InlineData(typeof(ObjectGetCommand), "object get")]
[InlineData(typeof(ObjectQueryNameCommand), "object query-name")]
[InlineData(typeof(ObjectScriptsCreateCommand), "object scripts create")]
[InlineData(typeof(ObjectScriptsSettingsSetCommand), "object scripts settings set")]
[InlineData(typeof(TemplateDeriveCommand), "template derive")]
[InlineData(typeof(InstanceDeployCommand), "instance deploy")]
[InlineData(typeof(ObjectsExportCommand), "objects export")]
[InlineData(typeof(ToolsetListCommand), "toolset list")]
[InlineData(typeof(ScriptLibraryExportCommand), "script-library export")]
[InlineData(typeof(SecurityRolesCommand), "security roles")]
[InlineData(typeof(SettingsLocaleGetCommand), "settings locale get")]
public void Command_IsRegistered(Type commandType, string expectedName)
{
var attr = (CommandAttribute)Attribute.GetCustomAttribute(
commandType,
typeof(CommandAttribute));
attr.ShouldNotBeNull();
attr.Name.ShouldBe(expectedName);
}
[Fact]
public void PipeRequest_StoresStructuredArgs()
{
var request = PipeRequest.Execute(
"objects",
"checkout",
new Dictionary<string, object>
{
["name"] = new[] { "A", "B" },
["confirm"] = true
});
request.Args["name"].ShouldBeAssignableTo<string[]>();
((string[])request.Args["name"]).ShouldBe(new[] { "A", "B" });
request.Args["confirm"].ShouldBe(true);
}
[Fact]
public void ConfirmedCommand_IncludesConfirmationArgs()
{
var command = new ObjectCheckoutCommand
{
GalaxyName = "ZB",
ObjectName = "DEV",
Confirm = true,
ConfirmTarget = "DEV"
};
var args = command.Args();
args["confirm"].ShouldBe(true);
args["confirm-target"].ShouldBe("DEV");
args["name"].ShouldBe("DEV");
}
[Fact]
public void BulkCommand_PreservesMultipleNames()
{
var command = new ObjectsCheckoutCommand
{
GalaxyName = "ZB",
Names = new[] { "A", "B" },
Confirm = true,
ConfirmTarget = "A,B"
};
var names = ((IReadOnlyList<string>)command.Args()["name"]).ToArray();
names.ShouldBe(new[] { "A", "B" });
}
[Fact]
public void UdaCommands_DefaultToValidWritableCategory()
{
new ObjectUdaAddCommand().Args()["category"].ShouldBe("MxCategoryWriteable_USC");
new ObjectUdaUpdateCommand().Args()["category"].ShouldBe("MxCategoryWriteable_USC");
}
[Fact]
public void TemplateDelete_DefaultsToNonCascadingForceOption()
{
new TemplateDeleteCommand().Args()["force-option"].ShouldBe("dontForceTemplateDelete");
}
[Fact]
public void InstanceDelete_DefaultsToDeleteCleanupForceOption()
{
new InstanceDeleteCommand().Args()["force-option"].ShouldBe("undeployIfDeployed");
}
[Theory]
[InlineData("EForceDeleteTemplateOption", "dontForceTemplateDelete", "dontForceTemplateDelete")]
[InlineData("EForceDeleteTemplateOption", "galaxy_dontForceTemplateDelete", "dontForceTemplateDelete")]
[InlineData("MxAttributeCategory", "mxcategorywriteable_usc", "MxCategoryWriteable_USC")]
public void DispatcherEnumParser_AcceptsCaseInsensitiveNamesAndGalaxyPrefixAliases(
string enumType,
string value,
string expected)
{
var dispatcher = typeof(GalaxyInfoCommand).Assembly.GetType("ZB.MOM.WW.GRAccess.Cli.GRAccess.GRAccessCommandDispatcher");
var enumValue = dispatcher.GetMethod("EnumValue", BindingFlags.Static | BindingFlags.NonPublic, null, new[] { typeof(string), typeof(string) }, null)
.Invoke(null, new object[] { enumType, value });
enumValue.ToString().ShouldBe(expected);
}
[Theory]
[InlineData("AnalogLimitAlarm", "AnalogExtension")]
[InlineData("analoglimitalarm", "AnalogExtension")]
[InlineData("HistoryExtension", "HistoryExtension")]
public void DispatcherExtensionType_NormalizesLegacyAliases(string value, string expected)
{
var dispatcher = typeof(GalaxyInfoCommand).Assembly.GetType("ZB.MOM.WW.GRAccess.Cli.GRAccess.GRAccessCommandDispatcher");
var extensionType = dispatcher.GetMethod("ExtensionType", BindingFlags.Static | BindingFlags.NonPublic)
.Invoke(null, new object[] { value });
extensionType.ShouldBe(expected);
}
[Fact]
public void DispatcherAtomicObjectEdit_UsesExpectedLifecycle()
{
var source = DispatcherSource();
var helper = source.Substring(source.IndexOf("private static string AtomicObjectEdit", StringComparison.Ordinal));
helper.IndexOf("obj.CheckOut();", StringComparison.Ordinal).ShouldBeLessThan(
helper.IndexOf("var summary = mutation(obj);", StringComparison.Ordinal));
helper.IndexOf("var summary = mutation(obj);", StringComparison.Ordinal).ShouldBeLessThan(
helper.IndexOf("obj.Save();", StringComparison.Ordinal));
helper.IndexOf("obj.Save();", StringComparison.Ordinal).ShouldBeLessThan(
helper.IndexOf("obj.CheckIn(string.Empty);", StringComparison.Ordinal));
helper.ShouldContain("obj.UndoCheckOut();");
}
[Fact]
public void DispatcherSingleObjectMutations_UseAtomicEditHelper()
{
var source = DispatcherSource();
source.ShouldContain("return AtomicObjectEdit(obj, editObj =>");
source.ShouldContain("return AtomicObjectEdit(FindSingleObject(galaxy, Kind(args), Arg(args, \"name\")), obj => ObjectScriptCreate");
source.ShouldContain("return AtomicObjectEdit(FindSingleObject(galaxy, Kind(args), Arg(args, \"name\")), obj => ObjectScriptSet");
source.ShouldContain("return AtomicObjectEdit(FindSingleObject(galaxy, Kind(args), Arg(args, \"name\")), obj => ObjectScriptSettingsSet");
}
private static string DispatcherSource()
{
var directory = new DirectoryInfo(Directory.GetCurrentDirectory());
while (directory != null)
{
var sourcePath = Path.Combine(
directory.FullName,
"src",
"ZB.MOM.WW.GRAccess.Cli",
"GRAccess",
"GRAccessCommandDispatcher.cs");
if (File.Exists(sourcePath))
return File.ReadAllText(sourcePath);
directory = directory.Parent;
}
throw new FileNotFoundException("Could not locate GRAccessCommandDispatcher.cs from the test working directory.");
}
}
}
@@ -0,0 +1,56 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using CliFx.Infrastructure;
using Shouldly;
using Xunit;
using ZB.MOM.WW.GRAccess.Cli.Commands.Galaxy;
namespace ZB.MOM.WW.GRAccess.Cli.Tests.Commands.Galaxy
{
public class GalaxyListCommandTests
{
[Fact]
public async Task ExecuteAsync_WithJsonFlag_WritesJsonArray()
{
// Arrange
using var console = new FakeInMemoryConsole();
var command = new GalaxyListCommand
{
NodeName = "TestNode",
Json = true
};
// We can't call ExecuteAsync without a real GRAccess install,
// but we can verify the command is constructable and properties bind.
command.NodeName.ShouldBe("TestNode");
command.Json.ShouldBeTrue();
}
[Fact]
public void Command_IsRegisteredInApplication()
{
// Verify the command type has the correct CliFx attribute
var attr = (CliFx.Attributes.CommandAttribute)System.Attribute.GetCustomAttribute(
typeof(GalaxyListCommand),
typeof(CliFx.Attributes.CommandAttribute));
attr.ShouldNotBeNull();
attr.Name.ShouldBe("galaxy list");
}
[Fact]
public void Command_HasOptionalNodeOption()
{
var prop = typeof(GalaxyListCommand).GetProperty("NodeName");
prop.ShouldNotBeNull();
var optAttr = (CliFx.Attributes.CommandOptionAttribute)System.Attribute.GetCustomAttribute(
prop,
typeof(CliFx.Attributes.CommandOptionAttribute));
optAttr.ShouldNotBeNull();
optAttr.Name.ShouldBe("node");
optAttr.IsRequired.ShouldBeFalse();
}
}
}
@@ -0,0 +1,178 @@
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Text;
using Newtonsoft.Json.Linq;
using Shouldly;
using Xunit;
using ZB.MOM.WW.GRAccess.Cli.GRAccess;
using ZB.MOM.WW.GRAccess.Cli.Infrastructure;
namespace ZB.MOM.WW.GRAccess.Cli.Tests.Commands
{
public class LlmIntegrationCommandTests
{
[Theory]
[InlineData("capabilities", false, "")]
[InlineData("object snapshot", false, "")]
[InlineData("object lineage", false, "")]
[InlineData("object children", false, "")]
[InlineData("object attribute value get", false, "")]
[InlineData("object attribute value set", true, "name")]
[InlineData("object scripts list", false, "")]
[InlineData("object scripts set", true, "name")]
[InlineData("object scripts create", true, "name")]
[InlineData("object scripts settings set", true, "name")]
[InlineData("area create", true, "template")]
[InlineData("engine create", true, "template")]
[InlineData("instance assign-area", true, "name")]
[InlineData("io assign", true, "name")]
[InlineData("batch", false, "")]
[InlineData("validate", false, "")]
public void CapabilityRegistry_IncludesLlmCommands(string commandName, bool mutates, string confirmTarget)
{
var command = CommandCapabilityRegistry.Find(commandName);
command.ShouldNotBeNull();
command.Mutates.ShouldBe(mutates);
command.ConfirmTarget.ShouldBe(confirmTarget);
command.SupportsLlmJson.ShouldBeTrue();
}
[Fact]
public void CapabilityValidation_RejectsMissingMutationConfirmation()
{
var result = CommandCapabilityRegistry.Validate(
"object attribute value set",
new Dictionary<string, object>
{
["galaxy"] = "ZB",
["name"] = "TestMachine",
["attribute"] = "Description",
["value"] = "Updated"
},
requireMutationConfirmation: true);
result.Valid.ShouldBeFalse();
result.Errors.ShouldContain("Missing required confirm=true.");
result.Errors.ShouldContain("confirm-target must be 'TestMachine'.");
}
[Fact]
public void CapabilityValidation_AcceptsConfirmedMutation()
{
var result = CommandCapabilityRegistry.Validate(
"object attribute value set",
new Dictionary<string, object>
{
["galaxy"] = "ZB",
["name"] = "TestMachine",
["attribute"] = "Description",
["value"] = "Updated",
["confirm"] = true,
["confirm-target"] = "TestMachine"
},
requireMutationConfirmation: true);
result.Valid.ShouldBeTrue();
}
[Fact]
public void LlmResponse_EnvelopeSerializesSuccessAndUnavailable()
{
var json = LlmResponse.Ok(
"object snapshot",
"ZB",
"TestMachine",
new { name = "TestMachine" },
unavailable: new[] { new LlmUnavailableField { Field = "scripts", Reason = "not exposed" } });
var token = JObject.Parse(json);
token["success"]?.Value<bool>().ShouldBeTrue();
token["command"]?.Value<string>().ShouldBe("object snapshot");
token["data"]?["name"]?.Value<string>().ShouldBe("TestMachine");
token["unavailable"]?.Count().ShouldBe(1);
}
[Fact]
public void SplitCommandName_UsesRegistrySubcommand()
{
var split = CommandCapabilityRegistry.SplitCommandName("object attribute value get");
split.Command.ShouldBe("object");
split.Subcommand.ShouldBe("attribute-value-get");
}
[Fact]
public void PackageSnapshotParser_ExtractsLineageAttributesAndScripts()
{
var path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + ".aaPKG");
try
{
File.WriteAllText(path, @"
Lineage: $gMachine -> $TestMachine
AttributeValue Name=""Description"" DataType=""MxString"" Value=""Test machine template""
ScriptBody Name=""UpdateTestChangingInt.ExecuteText"">
me.TestChangingInt = me.TestChangingInt + 1;
</ScriptBody>
");
var snapshot = PackageSnapshotParser.Parse(path);
snapshot.PackageFallbackUsed.ShouldBeTrue();
snapshot.Lineage.ShouldBe(new[] { "$gMachine", "$TestMachine" });
snapshot.AttributeValues.Single().Name.ShouldBe("Description");
snapshot.AttributeValues.Single().Value.ShouldBe("Test machine template");
snapshot.ScriptBodies.Single().Name.ShouldBe("UpdateTestChangingInt.ExecuteText");
snapshot.ScriptBodies.Single().Body.ShouldContain("TestChangingInt");
}
finally
{
if (File.Exists(path))
File.Delete(path);
}
}
[Fact]
public void PackageSnapshotParser_ExtractsNestedBinaryScriptExtensionBody()
{
var path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + ".aaPKG");
try
{
var innerBytes = Encoding.Unicode.GetBytes(string.Join("\n", new[]
{
"UpdateTestChangingInt",
"UpdateTestChangingInt_ScriptExtension",
"Me.TestChangingInt = System.Random().Next(1,1000);",
"Periodic"
}));
using (var outer = ZipFile.Open(path, ZipArchiveMode.Create))
{
var entry = outer.CreateEntry("File1.cab");
using (var entryStream = entry.Open())
using (var inner = new ZipArchive(entryStream, ZipArchiveMode.Create))
{
var txt = inner.CreateEntry("1055.txt");
using (var txtStream = txt.Open())
txtStream.Write(innerBytes, 0, innerBytes.Length);
}
}
var snapshot = PackageSnapshotParser.Parse(path);
var script = snapshot.ScriptBodies.Single(s => s.Name == "UpdateTestChangingInt.ExecuteText");
script.Body.ShouldBe("Me.TestChangingInt = System.Random().Next(1,1000);");
script.Source.ShouldBe("export-package:binary");
snapshot.AttributeValues.Single(a => a.Name == "UpdateTestChangingInt.ExecuteText").Value.ShouldBe(script.Body);
}
finally
{
if (File.Exists(path))
File.Delete(path);
}
}
}
}
@@ -0,0 +1,110 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Shouldly;
using Xunit;
using ZB.MOM.WW.GRAccess.Cli.Session;
namespace ZB.MOM.WW.GRAccess.Cli.Tests.Session
{
public class StaComThreadTests : IDisposable
{
private readonly StaComThread _sut;
public StaComThreadTests()
{
_sut = new StaComThread();
_sut.Start();
}
public void Dispose()
{
_sut.Dispose();
}
[Fact]
public async Task RunAsync_ExecutesOnStaThread()
{
var apartmentState = await _sut.RunAsync(() => Thread.CurrentThread.GetApartmentState());
apartmentState.ShouldBe(ApartmentState.STA);
}
[Fact]
public async Task RunAsync_ReturnsResult()
{
var result = await _sut.RunAsync(() => 42);
result.ShouldBe(42);
}
[Fact]
public async Task RunAsync_PropagatesException()
{
var ex = await Should.ThrowAsync<InvalidOperationException>(
_sut.RunAsync<int>(() => throw new InvalidOperationException("test error")));
ex.Message.ShouldBe("test error");
}
[Fact]
public async Task RunAsync_VoidAction_Completes()
{
var executed = false;
await _sut.RunAsync(() => { executed = true; });
executed.ShouldBeTrue();
}
[Fact]
public async Task RunAsync_VoidAction_PropagatesException()
{
var ex = await Should.ThrowAsync<InvalidOperationException>(
_sut.RunAsync(() => throw new InvalidOperationException("void error")));
ex.Message.ShouldBe("void error");
}
[Fact]
public async Task RunAsync_MultipleWorkItems_ExecuteInOrder()
{
var results = new int[3];
await _sut.RunAsync(() => { results[0] = 1; });
await _sut.RunAsync(() => { results[1] = 2; });
await _sut.RunAsync(() => { results[2] = 3; });
results.ShouldBe(new[] { 1, 2, 3 });
}
[Fact]
public async Task RunAsync_AllWorkRunsOnSameThread()
{
var threadId1 = await _sut.RunAsync(() => Thread.CurrentThread.ManagedThreadId);
var threadId2 = await _sut.RunAsync(() => Thread.CurrentThread.ManagedThreadId);
threadId1.ShouldBe(threadId2);
}
[Fact]
public void Dispose_CanBeCalledMultipleTimes()
{
var thread = new StaComThread();
thread.Start();
thread.Dispose();
Should.NotThrow(() => thread.Dispose());
}
[Fact]
public void RunAsync_AfterDispose_ThrowsObjectDisposed()
{
var thread = new StaComThread();
thread.Start();
thread.Dispose();
Should.Throw<ObjectDisposedException>(() => thread.RunAsync(() => { }));
}
}
}
@@ -0,0 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net48</TargetFramework>
<LangVersion>9.0</LangVersion>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<RootNamespace>ZB.MOM.WW.GRAccess.Cli.Tests</RootNamespace>
<PlatformTarget>x86</PlatformTarget>
<Platforms>x86</Platforms>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Shouldly" Version="4.2.1" />
</ItemGroup>
<ItemGroup>
<Reference Include="System.IO.Compression" />
<Reference Include="System.IO.Compression.FileSystem" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\ZB.MOM.WW.GRAccess.Cli\ZB.MOM.WW.GRAccess.Cli.csproj" />
</ItemGroup>
</Project>
+9
View File
@@ -0,0 +1,9 @@
# GRAccess CLI Usage
Canonical usage documentation is stored in `docs/usage.md`.
Keep this compatibility file in place for older references. User-facing command details, including `--llm-json`, `capabilities`, `validate`, `batch`, `object snapshot`, attribute value commands, area/engine wrappers, I/O assignment, and object script commands, are documented in:
```text
docs/usage.md
```