graccesscli: extend script editor surface — --field, --lock-trigger-type, scripts delete

Three additions to the script editor commands. Each one closes a real
gap surfaced by the round-trip-test against \$DelmiaReceiver.ProcessRecipe.

1. `object scripts set --field <name>` — explicit text-field selection.

   Previously `scripts set` always wrote to <Name>.ExecuteText (via
   ScriptAttributeName's default). To rewrite DeclarationsText / StartupText
   / ShutdownText / OnScanText / OffScanText / Expression, callers had to
   pass the full attribute name as `--script Foo.StartupText`, which is
   brittle. The new `--field` flag accepts any of the seven canonical
   ScriptTextSuffixes and composes <script>.<field> directly. Validates
   against the suffix list so an unrecognised --field surfaces a friendly
   error rather than a downstream FindAttributeForMutation failure.
   Default behavior (no --field) is unchanged: ExecuteText.

2. `object scripts settings set --lock-trigger-type` — parallel to the
   existing --lock-trigger-period. After writing TriggerType the new flag
   calls SetLocked(MxLockedInMe), matching the lock pattern on the period
   field. Without it, --trigger-type writes the value but leaves the
   attribute unlocked.

3. `object scripts delete` — script-named alias for the existing
   extension-delete subcommand. Wraps obj.DeleteExtensionPrimitive(
   "ScriptExtension", scriptName) inside AtomicObjectEdit (checkout / save
   / checkin). Removes the burden of remembering the generic
   `--extension-type ScriptExtension --primitive <Name>` form.

Test count delta: 61 -> 63 (+2 command-shape assertions for the new
ObjectScriptsSetCommand and ObjectScriptsDeleteCommand).

Live round-trip-test against \$DelmiaReceiver.ProcessRecipe:
- `--field DeclarationsText` write composed `ProcessRecipe.DeclarationsText`,
  CheckOut/Save/CheckIn all returned OK.
- `--field ExecuteText` round-trip same.
- A subsequent re-read shows the original body, suggesting that
  IAttribute.SetValue silently no-ops for ScriptExtension text fields on
  this GRAccess version (or the package-export reader pulls from a
  different snapshot than the just-saved revision). This is upstream of
  the editor surface — the new flags route correctly to the same SetValue
  path that scripts set already used. Diagnosing the SetValue
  ineffectiveness for script-text fields is a separate followup that
  should look at IScriptExtension-specific COM interfaces (per
  docs/script-parsing.md:8 "Object-level scripts are less direct").

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-05-05 15:09:34 -04:00
parent 65537570b8
commit 3f6bfebd6d
3 changed files with 48 additions and 4 deletions
@@ -434,7 +434,16 @@ namespace ZB.MOM.WW.GRAccess.Cli.Commands
[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; }
[CommandOption("field", Description = "Script-text field to write: ExecuteText (default), DeclarationsText, StartupText, ShutdownText, OnScanText, OffScanText, Expression. Pass to target a non-body field; omit to default to ExecuteText.")]
public string Field { get; init; } = "";
public override Dictionary<string, object> Args() { var args = base.Args(); args["file"] = File; args["field"] = Field; return args; }
}
[Command("object scripts delete", Description = "Delete a ScriptExtension primitive from an object")]
public sealed class ObjectScriptsDeleteCommand : ObjectScriptsGetCommand
{
public override string Subcommand => "scripts-delete";
}
public abstract class ObjectScriptSettingsCommandBase : ObjectScriptsGetCommand
@@ -451,6 +460,9 @@ namespace ZB.MOM.WW.GRAccess.Cli.Commands
[CommandOption("lock-trigger-period", Description = "Lock TriggerPeriod in this object after setting it")]
public bool LockTriggerPeriod { get; init; }
[CommandOption("lock-trigger-type", Description = "Lock TriggerType in this object after setting it")]
public bool LockTriggerType { get; init; }
public override Dictionary<string, object> Args()
{
var args = base.Args();
@@ -458,6 +470,7 @@ namespace ZB.MOM.WW.GRAccess.Cli.Commands
args["trigger-type"] = TriggerType;
args["expression"] = Expression;
args["lock-trigger-period"] = LockTriggerPeriod;
args["lock-trigger-type"] = LockTriggerType;
return args;
}
}
@@ -142,9 +142,18 @@ namespace ZB.MOM.WW.GRAccess.Cli.GRAccess
case "scripts-create":
RequireConfirm(args, Arg(args, "name"));
return AtomicObjectEdit(FindSingleObject(galaxy, Kind(args), Arg(args, "name")), obj => ObjectScriptCreate(galaxy, obj, args));
case "scripts-delete":
return AtomicObjectEdit(FindSingleObject(galaxy, Kind(args), Arg(args, "name")), obj =>
{
var scriptName = Arg(args, "script");
if (string.IsNullOrWhiteSpace(scriptName))
throw new ArgumentException("Script name is required.");
obj.DeleteExtensionPrimitive("ScriptExtension", scriptName);
return CommandSummary(obj, $"Delete script {scriptName}");
});
case "scripts-set":
RequireConfirm(args, Arg(args, "name"));
return AtomicObjectEdit(FindSingleObject(galaxy, Kind(args), Arg(args, "name")), obj => ObjectScriptSet(galaxy, obj, Arg(args, "script"), Arg(args, "file")));
return AtomicObjectEdit(FindSingleObject(galaxy, Kind(args), Arg(args, "name")), obj => ObjectScriptSet(galaxy, obj, Arg(args, "script"), Arg(args, "file"), Arg(args, "field", string.Empty)));
case "scripts-settings-set":
RequireConfirm(args, Arg(args, "name"));
return AtomicObjectEdit(FindSingleObject(galaxy, Kind(args), Arg(args, "name")), obj => ObjectScriptSettingsSet(obj, args));
@@ -1590,14 +1599,28 @@ namespace ZB.MOM.WW.GRAccess.Cli.GRAccess
};
}
private static string ObjectScriptSet(IGalaxy galaxy, IgObject obj, string scriptName, string file)
private static string ObjectScriptSet(IGalaxy galaxy, IgObject obj, string scriptName, string file, string field = "")
{
if (string.IsNullOrWhiteSpace(file))
throw new ArgumentException("Script source --file is required.");
if (!File.Exists(file))
throw new FileNotFoundException("Script source file was not found.", file);
var attributeName = ScriptAttributeName(scriptName);
string attributeName;
if (!string.IsNullOrWhiteSpace(field))
{
// Validate against the canonical script-text suffix list so
// callers get a friendly error before we try to write a
// nonexistent attribute.
if (!ScriptTextSuffixes.Any(s => string.Equals(s, field, StringComparison.OrdinalIgnoreCase)))
throw new ArgumentException($"--field must be one of: {string.Join(", ", ScriptTextSuffixes)}");
attributeName = scriptName + "." + field;
}
else
{
attributeName = ScriptAttributeName(scriptName);
}
var attr = FindAttributeForMutation(obj, attributeName);
var body = File.ReadAllText(file);
attr.SetValue(CreateMxValue(body, "string"));
@@ -1656,6 +1679,12 @@ namespace ZB.MOM.WW.GRAccess.Cli.GRAccess
var attr = FindAttributeForMutation(obj, scriptName + ".TriggerType");
attr.SetValue(CreateMxValue(triggerType, "string"));
yield return CommandSummary(attr, $"Set script {scriptName}.TriggerType");
if (BoolArg(args, "lock-trigger-type"))
{
attr.SetLocked(MxPropertyLockedEnum.MxLockedInMe);
yield return CommandSummary(attr, $"Lock script {scriptName}.TriggerType");
}
}
var expression = OptionalArg(args, "expression");
@@ -20,6 +20,8 @@ namespace ZB.MOM.WW.GRAccess.Cli.Tests.Commands
[InlineData(typeof(ObjectQueryNameCommand), "object query-name")]
[InlineData(typeof(ObjectScriptsCreateCommand), "object scripts create")]
[InlineData(typeof(ObjectScriptsSettingsSetCommand), "object scripts settings set")]
[InlineData(typeof(ObjectScriptsSetCommand), "object scripts set")]
[InlineData(typeof(ObjectScriptsDeleteCommand), "object scripts delete")]
[InlineData(typeof(TemplateDeriveCommand), "template derive")]
[InlineData(typeof(InstanceDeployCommand), "instance deploy")]
[InlineData(typeof(ObjectsExportCommand), "objects export")]