Actions: full CRUD management commands for the CLI - #43
Open
mateuscardosodeveloper wants to merge 5 commits into
Open
mateuscardosodeveloper wants to merge 5 commits into
mateuscardosodeveloper wants to merge 5 commits into
Conversation
Interactive autocomplete over the profile's actions, mirroring pickEntityIDFromTagoIO so the action-* commands share the prompt UX of the device and entity families. Requests 200 items, the Scale plan ceiling for Actions, so the list can never be silently truncated.
Pure flag-to-payload construction shared by action-create and action-edit: six trigger families through typed flags, plus --trigger-json and --action-json for the shapes flat flags cannot express. Encodes three API constraints that are absent from the SDK types and from the docs, taken from profile/export/services/actions-export.ts: - an empty trigger value must omit the key rather than send "" - an empty second_value must omit the key - a resource (tag_key) trigger must never carry unlock Rejects --trigger-unlock as the only trigger: unlock marks the condition that re-arms an action, so on its own it leaves nothing to fire on. The API accepts that payload but the web UI then renders an empty variable/value row. The SDK types 'value' as required on the condition-trigger member and models no mqtt_topic member at all, so both assembly sites need a cast.
Read-only surface: list with name, tag and active filters, and info
rendering the nested trigger and action as indented JSON.
Both normalize last_triggered through mapLastTriggered. The SDK types it
as ExpireTimeOption ("never" | Date) and the API really does return the
string, which mapDate would call toLocaleDateString on. The field is also
absent for actions that never fired, so it is normalized to "never"
rather than dropped from --json output.
action-info renders its human view entirely on stderr. console.table
writes to stdout, which would pollute the stream reserved for --json.
action-list keeps console.table because there the table is the command's
data output, matching device-list and entity-list.
Tag filtering is built locally instead of reusing repeatableTags from
device-list: that helper is private, typed to DeviceQuery, and loops to
<= maxRows, which appends a trailing empty tag entry.
Completes the action lifecycle. Validation lives in the builders, so an
invalid invocation fails before any request.
action-create reads the new id from the SDK's { action } response rather
than { id }: actions.create does not follow the { device_id } shape
devices.create uses, and reading the wrong key yields undefined ids in
--json output.
action-edit replaces trigger and action as whole values, because the API
overwrites those fields rather than merging them; a partial edit would
silently drop whatever the CLI does not model. action-enable and
action-disable reuse its applyActionEdit so every edit-shaped command
goes through one path, and send only { active }.
action-delete confirms unless -y or --silent, and a declined prompt
leaves the action untouched and exits 0.
Wires the family into the program, which also puts it in the man page through buildProgram. Regenerates the snapshot and extends the command-surface regression guard with action-list. The --help text carries the two behaviours that are invisible from the flags alone: how value_type is inferred, and that trigger and target are replaced whole on edit, with the action-info --json round-trip spelled out. It also documents the two shapes only --trigger-json can express, targeting devices by tag and pairing a firing trigger with an unlock.
This was referenced Aug 14, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a complete
action-*command family so an Action's full lifecycle is drivable fromtagoio, following the conventions PR #41 (devices) and PR #39 (files) established. Actions could previously only be created or edited through the web admin, which broke any scripted workflow that provisions automation alongside the devices and analyses it already provisions.Seven commands:
action-list,action-info,action-create,action-edit,action-enable,action-disable,action-delete. All carry the family conventions:--jsonwith parseable error codes,--silentfor non-interactive callers, an interactive picker when the ID is omitted, and confirmation on the destructive path.Trigger and target coverage
action-createexposes typed flags for six of the seven trigger families and the four common targets.--trigger-jsonand--action-jsoncover the shapes flat flags cannot express:condition_geofence, whose value is a nested geometry object, plus targets likequeue-sqsand the Twilio and SendGrid variants.Two shapes are reachable only through
--trigger-json, and both are documented in--helpwith a worked example: targeting every device carrying a tag instead of one device id, and pairing a firing trigger with an unlock trigger.API constraints encoded
Three rules live in
action-builders.tsthat are absent from the SDK types and from the docs. They were taken fromprofile/export/services/actions-export.ts, where the export path learned them, and each is now verified against the live API:""second_valuemust omit the keyresource(tag_key) trigger must never carryunlockWithout these,
action-createfails in production with an opaque API error that no mocked unit test would catch.Notes on the SDK
actions.createresolves{ action: "<id>" }, not the{ device_id }shapedevices.createuses; reading the wrong key yields undefined ids in--json. The SDK also typeslast_triggeredasExpireTimeOption("never" | Date) and models nomqtt_topictrigger member, so both are handled explicitly rather than cast away.Test plan
npm test);commands/actionsat 97.6% statements, 90.2% branch, 100% functionsnpx tsc --noEmitclean,npm run linter0 errors,oxfmt --checkclean.SSentries, no removals, noHeaderleak). Regression guard extended withaction-list--trigger-valueaccepted by the API (normalization rule 1);resourcetrigger stored withoutunlock(rule 3)--header "Authorization=Bearer abc=123"round-trips with the inner=intactaction-info --jsonoutput feeds back intoaction-create --trigger-jsonunmodifiedaction-deletecancel leaves the action and exits 0;-yand--silentremove itaction-disablethenaction-enableround-trip, sending only{ active }Risk (CIA)
Likelihood: 🟢 Low | Impact: 🟡 Medium | Exposure: 🟢 Low
Warning
Impact is Medium because
action-deletepermanently removes an automation andaction-editreplaces trigger and target as whole values. Deletion confirms unless-yor--silentis explicit, and a declined prompt exits 0 without calling the API. The whole-value replacement is deliberate: the API overwrites those fields rather than merging them, so a partial edit would silently drop fields the CLI does not model. Every other command in the family is additive or read-only.