Commands
| Command | What it does | Notes |
|---|---|---|
ign version | CLI version; gateway check when a profile resolves | unreachable gateway degrades to a warning (exit 0) |
ign status | Identity, platform (Java/OS), uptime, CPU/memory/disk, license incl. trial countdown | merges gateway-info + /overview + unauthenticated /StatusPing; authed read (exit 3 without a secret, exit 5 on bad credentials) |
ign modules [--quarantined] | Every module: id name version state licenseState | default = healthy list; --quarantined swaps to the quarantined list (usually empty) |
ign metrics [--history] | Current CPU %/heap and thread execution counts | --history appends first/last datapoint summaries per series (systemPerformance endpoints) |
ign sessions [--type designer|perspective|vision] | All three session families in one merged output (designers (N) / perspective (N) / vision (N) sections) | --type filters to one family; JSON always carries all three keys (filtered-out = []); replaces the webpage's Sessions pages |
ign sessions terminate --type <T> --id <ID> [--message MSG] | Terminate a session (designer: prune / vision: close / perspective: terminate, --message shown to the user) | destructive: exit 2 (confirmation_required) without --yes or IGNITION_YES=1; a nonexistent id exits 6 (not_found); pruning a LIVE Designer session answers 409 on the gateway and exits 6 (session_not_prunable) — prune removes stale entries only, close the Designer first |
ign connections [--type database|opc] | Database/OPC connections: name enabled healthchecks | healthchecks is passthrough as the gateway reports it (populated detail LOW-confidence until captured live); replaces the webpage's Connections pages |
ign project list | Every runnable project: name title enabled parent inheritable | inheritance info comes from the list items themselves; JSON items also carry description (all six keys always present, null when unset); replaces the webpage's Projects list |
ign project new <NAME> [--title --description --parent --inheritable --disabled] | Create a project | only provided fields ride the create body (never empty-string references); the result is a find read-back; audit-logged server-side |
ign rig module uninstall <REGISTRY-ID> | Remove a provisioned module from the rig's gateway (DELETE /data/api/v1/modules/uninstall) | destructive: exit 2 (confirmation_required) without --yes or IGNITION_YES=1; refuses with exit 7 while the rig still declares the module, and exit 3 (module_not_registered) for an unknown registry id; the gateway refuses a module whose .modl is still mounted, answering exit 6 (module_uninstall_denied). NOT reversible by re-declaring — recovery is ign rig reset, which destroys the data volume |
ign project copy <SRC> <DST> | Copy a project with all its resources | non-destructive (creates DST) — no --yes; audit-logged server-side |
ign project rename <OLD> <NEW> | Rename a project (native rename, not copy+delete) | non-destructive relabel — no --yes; audit-logged server-side |
ign project set <NAME> [--title --description --parent --set-enabled|--disabled --inheritable BOOL] | Set project fields — --parent IS the inheritance move (reparent) | only provided flags ride the modify body (absent = untouched); at least one field required; audit-logged server-side |
ign project delete <NAME> | Delete a project | destructive: exit 2 (confirmation_required) without --yes; the wire DELETE always carries the server's own confirm=true query param (both guard layers); a nonexistent name exits 6 (not_found); audit-logged server-side |
ign project export <NAME> [-o FILE] | Export a project as a ZIP archive | the ZIP STREAMS to disk chunk-by-chunk (no memory buffering; 120 s per-request timeout); default filename from Content-Disposition, else <name>.zip; stdout stays data-only — JSON carries {project, file, bytes, scope} (see scope metadata below) |
ign project import <NAME> --file PATH|--file - [--collision-policy abort|overwrite] | Import a project from a ZIP (- reads stdin) | default policy abort: importing over an existing name exits 6 (project_exists) BEFORE any upload; overwrite is destructive — exit 2 without --yes and it REPLACES the entire project (resources absent from the ZIP are deleted; merge is Designer-only); a non-ZIP, >512 MB, or structurally-corrupt (truncated) input exits 2 (invalid_import_file) before any network I/O — every member is validated up front because the gateway would otherwise accept a truncated ZIP and wipe the target (live-witnessed); a gateway import refusal riding HTTP 200 ({success:false}) exits 6 (import_denied) with the gateway's problem text; 300 s per-request timeout |
ign project diff <PROFILE_A> <PROFILE_B> --project <NAME> | Compare a project across two gateway profiles — per-resource added/removed/changed/same statuses (read-only, no guard) | statuses are B-relative-to-A: added = in B only, removed = in A only, changed = differing content after resource.json normalization (attributes.lastModification/…Signature stripped, keys canonicalized — identical content exported from two gateways reports same); each side exports once and NOTHING is imported; the envelope's profile stays the ACTIVE profile while the data carries profile_a/profile_b; the root project.json rides project_meta (title/enabled/parent deltas), never the resource entries; diffing a profile against itself exits 2 invalid_input; scope is project-only (see the tag-promotion pipe below); a missing project on either side exits 6 not_found |
ign project sync <PROFILE_A> <PROFILE_B> --project <NAME> --resource PATH... [--all-changed] [--delete] | Promote selected resources from A into B (direction is ALWAYS A→B — source A, target B) | destructive on B: the whole project is overwrite-imported — exit 2 (confirmation_required, profile null, ZERO requests) without --yes; at least one of --resource (repeatable) or --all-changed required (else exit 2 pre-resolution); --all-changed promotes everything A has that B lacks or differs on (the diff's removed+changed under B-relative-to-A labels); default is upsert-ONLY — B's extra resources are never deleted unless --delete is passed (then the diff's added set — B-only — is removed; an explicit --resource path absent in A is a deletion request under --delete, not_found without); replace_member's descriptor-merge landing rules ride free; B's project.json is never touched; --all-changed with nothing changed performs NO import (zero-write honesty); JSON data {scope, profile_a, profile_b, project, synced, removed} |
ign workspace checkout <PROJECT> <TARGET> [--decode-scripts] | Check a project's resources out to a local tree — mapped paths + the recorded .ign-workspace.json manifest + an idempotent .gitignore | read-only on the wire (one export GET, ZERO imports); an unrelated non-empty target refuses exit 2 (never clobbered — the pre-existing file is untouched), a corrupt or foreign-manifest target refuses naming what it found, a same-project re-checkout refreshes; --decode-scripts decodes embedded JSON scripts to editable .py sidecars (nvim-editable; the UNEDITED re-encode is byte-exact — see the workspace section and Script decode/encode below); profile NAME rides into the manifest; JSON data {project, target, member_count, scripts_decoded} |
ign workspace status [PATH] | Report workspace drift against the gateway — a PATH/STATE table (clean rows INCLUDED — agents diff full state) + the x local edits, y gateway drift, z conflicts, n untracked summary | states are push-relative (each row names what push would do — the table in the workspace section below); the project comes from the manifest, never re-typed; a missing/corrupt/foreign-schema manifest refuses exit 2 PRE-resolution (profile null, ZERO requests — the usage-guard convention); read-only (one export GET); JSON data {project, clean, rows} |
ign workspace push [PATH] [--delete] | Push local edits to the gateway — manifest-recorded local bytes spliced into a FRESH export, imported exactly once | destructive: exit 2 (confirmation_required) without --yes and the refusal message IS the blast-radius preview (exactly what would be written/deleted); conflicts refuse EVEN WITH --yes (manual reconciliation — see the workspace section); locally-deleted members need --delete (default: reported as skipped); untracked files are NEVER imported; an empty selection writes nothing and never prompts; JSON data {project, wrote, deleted, skipped}; over MCP the tool carries an OPTIONAL confirm boolean and confirm: true is the MCP-native confirmation for it — --yes/IGNITION_YES remain CLI-only, and the conflict refusal is still never confirmable on either surface |
ign deploy render --env <ENV> [--repo DIR] [--targets DIR] [--out DIR] | Render a declared gateways/<name>/ tree for one environment into the bytes the gateway holds on disk, and report file and byte counts plus the render_sha256 digest | offline: profile: null, no network, no credentials, no profile resolution; deterministic render_sha256 (listing format in the deploy section); refusals exit 2 deploy_tree_invalid / embedded_secret; --env must be a safe single path component (invalid_input otherwise); MCP tool deploy_render is unguarded (Decision D; the --out clobber ladder is the safety); JSON data {env, gateway, mode, out, files, bytes, render_sha256, resources, tag_providers, uuids_minted, opaque_files, warnings} |
ign deploy plan --env <ENV> [--repo DIR] [--targets DIR] [--base DIR] [--diff] | Plan one environment against the master node's live gateway files: each unit is added, changed, removed, same or unmanaged, with where live copies sit (live_in) and the core relation, plus the canonical plan_sha256 | read-only; exits 0 whether or not anything changed (SF-2); resolves no session, so no secret is needed and no gateway is contacted; profile is the master node's profile name from the target; reads the host through nodes.master.local (or ssh); refusals exit 2 deploy_tree_invalid (master with neither ssh nor local) and exit 4 transport_failed (unreadable host); MCP tool deploy_plan is unguarded (Decision D); JSON data {env, gateway, mode, node, transport, render_sha256, plan_sha256, summary, entries, delete_set, base, warnings}; --base DIR adds three-way drift against a re-hashed previous render, --diff adds a redacted text diff (secret-bearing members withheld) |
ign deploy pull --env <ENV> [--repo DIR] [--targets DIR] | Bootstrap gateways/<name>/ in the local repo from the master node's live tree: core files into resources/ and tags/, the mode collection's files into modes/<ENV>/, a manifest generated when none exists | read-only against the gateway (it reads the host through nodes.master.local or ssh, resolves no session, sends no REST request, writes nothing to the host); writes only the local repo; units holding a secret, or that render would not reproduce byte for byte, are skipped and listed in data.skipped[] with a reason (they show as unmanaged in plan); guarded: replacing existing files in resources/, tags/ or modes/<ENV>/ exits 2 confirmation_required without the global --yes (the message names the file count and the first five paths), manifest.yaml and other environments' overlays are never touched; MCP tool deploy_pull advertises confirm; JSON data {env, gateway, mode, node, transport, out, files_written, manifest, render_sha256, skipped, warnings} |
ign deploy verify --env <ENV> [--repo DIR] [--targets DIR] [--project NAME] [--node master|backup|both] | Verify that the master gateway runs what the target declares, through six checks: scan, mode, read_back, files, loaded, probes; --node backup|both also checks the backup node of a redundant pair and that every secret name the tree references exists on each node | read-only (no scan is triggered, no scan-lock taken, nothing written); the master's profile from the target resolves through the normal secret chain (profile_not_found exit 3, secret_unavailable exit 3, network_error exit 4 when unreachable, transport_failed exit 4 when the host cannot be read); verdict exit: green exits 0, a RED verdict renders the success envelope with data.ok: false, data.slug: deploy_verify_failed and the findings in data.checks, and exits 6 after the envelope (see the verdict exception); failures before a verdict exists stay ordinary error envelopes with their own exit codes; --project (default ign-cli) names the project hosting the WebDev tags route used for probes; MCP tool deploy_verify is unguarded (Decision D); JSON data {env, gateway, mode, node, project, ok, slug, checks, resources_checked, warnings}, plus backup and secrets for --node backup|both only |
ign deploy apply --env <ENV> [--project NAME] [--repo DIR] [--targets DIR] [--base DIR] [--plan-sha256 HASH] [--allow-destructive] [--allow-drift REASON] [--designers fail|wait|proceed] [--designers-timeout SECS] [--skip-gwbk] [--backup-timeout SECS] | Apply one environment: write the declared tree into the master node's mode collection under the gateway scan-lock, scan, then verify it; a red verify restores the pre-apply snapshot automatically; when the target declares nodes.backup, then wait (default 300 s, --backup-timeout 1 to 86400, twice at most) for the backup node to hold the same files, reading it only (never writing to it) | guarded: exit 2 confirmation_required without the global --yes, and the refusal message IS the plan (env, gateway, mode, each changing entry with its flags, the delete-set count and plan_sha256); Decision C: with --json, or when stdin or stdout is not a terminal, a missing --plan-sha256 is invalid_input (exit 2) before any host or gateway contact, and a hash that is not the current plan's is plan_stale (exit 6); an empty plan prints outcome noop, exits 0 and creates no .ign/ directory and sends no lock, backup, scan or restart request; a red post-apply verify renders the success envelope with data.outcome rolled_back, data.ok: false, data.slug deploy_rolled_back and data.cause, and exits 6 after the envelope; a master that applied with a backup that did not converge renders the success envelope with data.outcome applied_backup_unsynced, data.ok: false, data.slug deploy_backup_unsynced and data.backup.reason, and also exits 6 after the envelope (the master is not rolled back); one fixed 176 s lock hold serves every target, and a change of 1,528 or more files (writes plus deletes) is refused by the hold_budget preflight (deploy_preflight_failed, exit 6); staging a large tree is silent on the wire (up to about 9 minutes at 1,520 files), so in human mode a stderr line announces it, and progress lines are prefixed ign deploy apply: (under --json stderr stays silent until an error envelope); the flag set has no option that skips the plan hash, the verify or the rollback; not in the MCP catalog (Decision D); JSON data {env, gateway, mode, node, transport, run_id, run_dir, outcome, ok, slug, cause, plan_sha256, render_sha256, summary, written, deleted, warnings, gwbk, skip_gwbk, restart_pending, restart_pending_new, backup} |
ign deploy rollback --env <ENV> --run <ID> [--repo DIR] [--targets DIR] [--discard-live-changes] [--designers fail|wait|proceed] [--designers-timeout SECS] | Roll back one past run by id, from a fresh process: restore the on-box snapshot the run took into the master node's mode collection, delete the files the run created, scan, and record the rollback in a run directory of its own. Also recovers an interrupted run (see deploy rollback) | guarded: exit 2 confirmation_required without the global --yes, and the refusal message names the run, the gateway, the mode and how many files it would restore and delete (and any live change it would overwrite); a malformed --run is invalid_input (exit 2) before any host or gateway contact; a file changed since the run is deploy_live_changed (exit 6) unless --discard-live-changes; a journal of another run is deploy_journal_present (exit 6); one fixed 176 s lock hold serves the rollback; in human mode progress lines are prefixed ign deploy rollback: on stderr (under --json stderr stays silent until an error envelope); not in the MCP catalog (Decision D); JSON data {env, gateway, mode, run_id, target_run_id, run_dir, outcome, ok, restored, deleted, discarded_live_changes, warnings, restart_pending, restart_pending_new} |
ign deploy projects --env <ENV> --release <TAG> [--allow-downgrade] [--allow-first-change REASON] [--check | --artifact-sha256 HASH --yes] [--designers fail|wait|proceed] [--designers-timeout SECS] [--repo DIR] [--targets DIR] | Deploy one Ignition project from its own repo, or with --check report what that would do. Reads deploy/project.yaml (strict, schema_version: 1, project, environments) and the root project.json, reads targets/<ENV>.yaml below --targets (default --repo), builds the artifact from the checkout, reads the live project, and reports the action: create for a project absent from the gateway, skip when the live project equals the artifact and the release record already names this release, record when it equals the artifact and the record is absent or names another release, import otherwise. Without --check it is guarded: exit 2 confirmation_required without the global --yes, and the refusal message IS the plan (action, project, gateway, environment, release, member counts, the paths an overwrite deletes and the hash); Decision C: with --json, or when stdin or stdout is not a terminal, a missing --artifact-sha256 is invalid_input (exit 2) before any host or gateway contact, and a hash that is not the checkout's artifact is project_hash_mismatch (exit 6) with nothing uploaded. It saves the live project, imports the artifact (create uses overwrite=false), reads the project back, compares it by canonical hash and records the release. Outcomes: checked (--check), skipped, created (a project absent from the gateway, D-13), imported, recorded, imported_unverified; a read-back that does not equal the artifact renders the success envelope with data.outcome imported_unverified, data.ok: false, data.slug deploy_project_unverified and data.restore_command, writes no record, sends no second import, and exits 6 after the envelope | --check sends GET requests only (projects/list, projects/find/<name>, projects/export/<name>), creates no .ign/ directory and writes nothing; a deploy goes to the master node only (never nodes.backup), declared with local or ssh (Linux or Windows); --env must be listed in deploy/project.yaml (deploy_tree_invalid, exit 2, before any request); the artifact holds the root project.json and, below the top-level ignition and module-id directories, each resource.json with the files it lists, and a symlink in that walk refuses deploy_tree_invalid; artifact_sha256 and live_sha256 are canonical digests that ignore zip entry order and the fields the gateway rewrites on import; other projects on the gateway are listed by name in unmanaged and never read or changed; not in the MCP catalog (Decision D); JSON data {env, gateway, project, release, check, action, would_change, artifact_sha256, live_sha256, members, removed, removed_count, excluded, excluded_count, unmanaged, unmanaged_count, parent, previous_release, recorded_sha256, live_matches_last_deploy, allow_downgrade, allow_first_change, warnings, run_id, run_dir, record_written, restore_command, outcome, ok, slug}, with members as {added, changed, removed, same} and the lists byte-sorted and capped at 200 entries |
ign edit <PROJECT> <RESOURCE_PATH> [--yes] | Fetch a gateway resource, edit it in $EDITOR, push back — the kubectl-edit loop over ONE resource (whole-tree authoring is ign workspace's job) | destructive: the push re-imports the project — exit 2 (confirmation_required) without --yes and the refusal message IS the blast-radius summary; ZERO stdout in every mode (the editor owns the terminal — all prose on stderr, see the edit section); an unchanged save is a clean no-op (nothing pushed, never prompted); a gateway changed since fetch refuses EVEN WITH --yes (changed on gateway since fetch — re-run to fetch fresh); a JSON-breaking save refuses FAIL-CLOSED with the temp tree kept (path printed); no $EDITOR/VISUAL refuses exit 2 pre-resolution (profile null, zero requests) |
ign resource list <PROJECT> [--prefix PREFIX] | A project's resource members, one path per line | rides project-export ZIP surgery (05-02): the project is exported, the member tree under <collection>/resources/… is mapped to user paths (resources/ stripped — ignition/script-python/…); --prefix filters client-side; JSON items carry exactly the typed path |
ign resource get <PROJECT> <PATH> | Read ONE resource: JSON pretty-printed, text raw — the surgical edit loop's first half | PATH keeps its slashes (e.g. ignition/script-python/myscript) and addresses a ZIP MEMBER (the file at <collection>/resources/<rest>); a binary member (data.bin-class, sniffed from the member bytes) refuses with exit 6 resource_binary (use export/import instead — never corrupted through the JSON loop); JSON data carries {project, path, content_kind, content} |
ign resource put <PROJECT> <PATH> --file PATH|--file - | Write ONE resource member (upsert: created if absent, replaced if present) | destructive: the whole project is re-imported (overwrite=true) after the member surgery — exit 2 (confirmation_required) without --yes; content is sniffed (json/text); binary input refuses exit 6 resource_binary before any network I/O; an unreadable file/stdin exits 2 invalid_input; concurrent Designer edits are REPLACED (see the resource section) |
ign resource delete <PROJECT> <PATH> | Delete ONE resource member | destructive: exit 2 (confirmation_required) without --yes; the surgery drops the member and re-imports the project; a nonexistent path/member exits 6 (not_found) |
ign logs [--logger L] [--min-level L] [--since SPAN] [--limit N] | Recent log entries, newest first (ISO-UTC LEVEL logger message) | --limit is ALWAYS explicit (default 200 — the server default is unlimited); --since takes EPOCH-MS or 500ms/30s/5min/2h; sorts desc(timestamp) so you see the NEWEST entries, never the oldest 200 |
ign logs -f [--interval S] [--timeout S] | Live tail: entries stream to stdout as they occur | poll-based (no server push exists — GET /logs?startTime=<cursor> IS the tail); --timeout expiry ends cleanly (exit 0); without it, run until Ctrl-C (default process kill, no envelope); see the streaming exception below |
ign logs download [-o FILE] | Download the log archive — a SQLite .idb, never a zip | bytes written exactly as received; default filename from Content-Disposition, else <profile>-logs-<ts>.idb; --json data is {file, bytes, content_type} |
ign logs loggers [--search S] | Logger registry: name level context | explicit limit 200 (same unlimited-default guard) |
ign logs loggers set <NAME> <LEVEL> | Set one logger's level (TRACE..OFF) | mutation: exit 2 (confirmation_required) without --yes; audit-logged server-side |
ign logs loggers reset | Reset ALL custom logger levels to defaults | mutation: exit 2 without --yes; audit-logged server-side |
ign restart [--wait] [--timeout S] [--interval S] | Restart the gateway — the one big red button | always --yes-guarded (it takes the whole gateway down); without --wait: POST + "READY in ~1 min" advisory; with --wait: POST → 5 s floor → polls the unauthenticated /StatusPing until RUNNING (default 300 s budget; a timeout exits 4 naming the last observed state) |
ign wait gateway [--interval S --timeout S] | Wait until the gateway reports RUNNING | unauthenticated /StatusPing poll — works with no/broken credential; already-RUNNING = immediate success (default 120 s) |
ign wait restart [--interval S --timeout S] | Wait for a restart to complete | shares restart --wait's semantics: a non-RUNNING state observed once → RUNNING completes immediately (witnessed restart, no floor wait); an all-RUNNING wait reports success only after the same 5 s floor — no false positive when run right after ign restart |
ign wait module <ID> [--interval S --timeout S] | Wait until a module reports ACTIVE | polls modules/healthy?search=<id> (authed); timeout names the id + last observed state |
ign doctor [--check-write] [--webdev-route NAME] | Diagnose the setup: url (parse + TCP dial), liveness (unauth /StatusPing), commissioning (302→/welcome), auth (401 vs 403), the permissions deep-dive (security-properties), write permission, WebDev-route presence, Docker/rig presence | exits 0 whenever the diagnosis completes — failing checks are data, not CLI errors (agents parse checks[]; humans read the table); --check-write fires the harmless scan/projects rescan (2xx = write OK, 403 = read-only token); --webdev-route NAME probes that route's version action in the CLI's ign-cli WebDev project — 405 = absent (the live-proven 8.3 marker; the earlier 404 assumption was wrong), 402 = module unlicensed, 200 = present (+ handshake version); config errors (no profile) still exit 3 |
ign adopt [--user NAME] [--key-name NAME] [--level PATH] [--project NAME] [--testing] [--checkout DIR] [--bake FILE] | Bootstrap a commissioned gateway to a working profile: native OIDC login → mint an Administrator-level API key (ign-cli, secure-connections off, idempotent by name) → merge the level root into gateway read/write permissions → live-probe the fresh name:key → persist (OS keyring + profile rewrite; env-var fallback prints the token exactly once) | password from IGNITION_PASSWORD env-only (missing → exit 3, rejected → exit 5); mutating-verb exits — a failed step is a hard error (doctor-shaped steps[] rows, SKIP = already satisfied; re-run on an adopted gateway is an all-skip no-op); a same-name key that is disabled or secure-only is a hard error (delete it and re-run); composition flags run after the key is proven: --project deploys the WebDev routes, --testing (requires --project) ships the Jython test framework + testing routes and asserts discover ≥1 module + a green smoke run, --checkout DIR lands every enabled project (scripts decoded), --bake FILE saves a restore-ready roaming gwbk; see the Gateway adoption section |
ign session login [--user NAME] | Obtain a live gateway login session through the native OIDC dance — the SAME flow ign adopt and the trial-reset ladder already run, exposed as JSON so a browser harness can skip the login form entirely. Returns {cookie_name, cookie_value, csrf_token, gateway_url, storage_state}, where storage_state is a ready-to-write Playwright storageState document (one cookie, empty origins) | this verb emits a LIVE credential by design — that is its product, and the exposure is disciplined: the session material (cookie value, CSRF token, storage_state) rides the envelope only under the global --json flag; the human render deliberately prints just the cookie NAME and the gateway URL and withholds the rest (pinned by a unit test over the pure render function, so a future edit cannot leak it silently). Password is env-only via IGNITION_PASSWORD (never a flag — the rig family's rule): missing → exit 3 password_unavailable, rejected → exit 5 auth_rejected, and neither refusal path emits any session material. --user resolves flag → $IGNITION_USER → admin. The profile resolves DEGRADED (no API token required — logging in is what happens before one exists). The storage-state cookie follows Playwright's rules: domain is the gateway host without its port, path is /, httpOnly true, sameSite Strict, expires -1 (a session cookie), and secure true only for an https gateway. Consumed verbatim by the ign e2e init scaffold's global-setup.mjs |
ign webdev deploy [--project NAME] [--with-script-exec] [--rotate-secret] [--with-testing] | Install the CLI's own WebDev route bundle into the dedicated ign-cli project (default) — tags, tagConfig, alarms, tagHistory (+ scriptExec only with --with-script-exec, + the testing bundle only with --with-testing) | not --yes-guarded by design: the dedicated project is CLI-OWNED — born from the first deploy zip, overwrite-REPLACED on every deploy (replace-not-merge is the contract here; user projects are never touched); every WebDev-dependent tag command (Phase 5) refuses exit 6 routes_not_deployed naming ign webdev deploy until this runs; --with-script-exec generates a fresh hex secret (stored in the profile config at 0600) when none exists, --rotate-secret regenerates unconditionally (requires --with-script-exec); the secret NEVER appears in any output, envelope, or log (it lives in exactly one place: the baked route zip member); --with-testing appends the embedded TESTING bundle (the Jython framework, the permanent testing.__tests__ smoke sentinel, and the POST-only testing/run/testing/tags routes) — NOTE: a testing-only deploy OVERWRITES and drops scriptExec; compose both flags (or let ign adopt --project … --testing do it); JSON data {project, routes, script_exec, secret_rotated, import} |
ign webdev status [--project NAME] | The version-handshake sweep: probe every route's version action — per-route {route, status, deployed_version, expected_version} | a read — exits 0 whenever the sweep completes (per-route degradation is DATA, the doctor precedent): present/absent/unlicensed/auth_gated/secret_mismatch/version_mismatch per route; the ok flag (data, not exit code) is true only when every always-on route is present with a matching version; scriptExec is probed ONLY when a secret is configured for the profile and never gates ok |
ign tags provider list | The gateway's tag providers: name enabled tags health (+ (managed) marker) | NATIVE config-resource REST (no deployed routes needed) — the healthy seam: metrics.tagCount + healthchecks.status ride as the gateway reports them; the built-in System provider (and any MANAGED-type) is flagged; JSON rows carry {name, enabled, tag_count, health, managed} (all keys always) |
ign tags provider create <NAME> | Create a STANDARD tag provider | MVP creates the fixed STANDARD shape only ({profile:{type:"STANDARD"}, settings:{}} — the live-proven array-body POST); DB-backed providers are out of scope; audit-logged server-side |
ign tags provider delete <NAME> | Delete a tag provider (find → signature → delete chain) | destructive: exit 2 (confirmation_required) without --yes — the guard fires before ANY resolution (zero network work); a nonexistent provider exits 6 (provider_not_found, hint names ign tags provider list); the delete embeds the record's server signature on the path; audit-logged server-side |
ign tags browse [PATH] [--filter SUBSTR] [--include-properties] [--project NAME] | Browse tags as a tree — providers at the root, folders/tags nested | needs the deployed routes (see the version-negotiation matrix above — refuses exit 6 pre-deploy); Property children are filtered out by default (--include-properties keeps them — the display default); --filter is a case-insensitive substring on name and full path; human mode renders the indented tree with tagType badges, JSON is the flat {path, name, tag_type, has_children, data_type}[] (nesting derivable from path) |
ign tags browse --from-export PATH [--filter SUBSTR] [--include-properties] | Browse a tag export OFFLINE — no gateway, no credential, no deployed routes (profile: null; the positional browse path and this flag are mutually exclusive) | THREE layouts accepted: the CLI's own tags export JSON (the interchange file — provider = file stem), a legacy <provider>.json whole tree, or a git-module directory (a tags/ root, or the dir itself when it holds provider folders/.json files) — per provider: individual .json-per-leaf files (folders = directories, _types_/*.json = UDT definitions, names decoded from %XX-encoded filenames, dot-entries skipped, System excluded, .tag-config.json ignored) OR the legacy single-file tree; the output reuses the SAME tree render + flat JSON row shape as the live browse; a nonexistent path or unparseable JSON exits 2 invalid_input (zero network) |
ign tags read <PATH>... [--project NAME] | Read one or more tag values — path = value [quality] timestamp | needs the deployed routes; always batch on the wire (a single path is a one-element batch); rows pass through VERBATIM — quality strings carry their own detail (Good, Bad_NotFound, …) and are never parsed further: a missing tag is DATA (exit 0, quality Bad_NotFound), not an error |
ign tags write <PATH> --value V [--project NAME] | Write a value to a tag — returns the post-write quality | needs the deployed routes; the write-scalar-is-JSON rule: --value parses as a JSON scalar (42, 1.5, true, null, "quoted"); text that does not parse is sent as the bare string (--value hello is the string hello); arrays/objects refuse exit 2 (invalid_input) before any network I/O — the tag value wire slot is a scalar; a nonexistent target writes back a Bad… quality (quality is data) |
ign tags config get <PATH> [--project NAME] | A tag's configuration as (pretty) JSON — the surgical edit loop's read half | needs the deployed routes; the gateway hands value/defaultValue back as STRINGIFIED JSON — the CLI re-parses them into real JSON objects/arrays so agents see structured data, not JSON-in-a-string (unparseable and scalar-parse strings stay strings); JSON data carries {project, path, tag_type, config}; a missing tag exits 6 (not_found — the route's own denial); provider-ROOT paths ([default] alone) exit 6 provider_root_unsupported (see the bulk export note below); a config carrying history bindings appends an additive history: block in human mode (see Tag↔historian bindings below) |
ign tags config create <PATH> --file FILE|- [--project NAME] | Create a tag from a JSON definition (- = stdin) | needs the deployed routes; the definition is the configure shape — see the configure-shape traps table below; the CLI splits the path into configure's basePath + per-tag name (a bare path rides under [default]; the path-derived name wins over any name in the definition) and does NOT otherwise reshape the dict; collision policy 'a' (abort): creating over an existing node refuses server-side; --file JSON errors exit 2 (invalid_input) pre-resolution |
ign tags config edit <PATH> --file FILE|- [--project NAME] | Edit a tag's configuration from a JSON definition (- = stdin) | the same configure call with collision policy 'o' scoped to the single named node (edit = overwrite that node); NOT --yes-guarded — a single-node edit is not a project-wide destructive |
ign tags config delete <PATH>... [--project NAME] | Delete tag configurations | destructive: exit 2 (confirmation_required) without --yes — the guard fires before ANY resolution (zero network work); the delete is batch on the wire (deleteTags {paths}); JSON data {project, deleted} |
ign tags udt types [--provider NAME] [--project NAME] | List a provider's UDT types ([provider]_types_ browse) | needs the deployed routes; JSON data {project, provider, types: [{name, tag_type}]} |
ign tags udt def <NAME> [--provider NAME] [--project NAME] | A UDT definition (parameters + nested children, recursive) | needs the deployed routes; the SAME stringified re-parse applies (parameter defaultValues and child values become real JSON); JSON data {project, provider, name, definition} |
ign tags export <PATH>... [-o FILE] [--format json|xml|csv] [--project NAME] | Export tag subtrees — the bulk-transfer half | needs the deployed routes; --format json (default) fetches the gateway's native JSON interchange: the payload is parsed, validated (a list of subtrees), and written PRETTY — default file <last-path-segment>.json in the cwd, -o FILE overrides, -o - prints the raw pretty payload in every mode (the fourth sanctioned stdout exception — pipe it into tags import --file -); --format xml passes the gateway's own XML through RAW-BYTE (CRLF/no-declaration document verbatim, no added trailing newline; default file .xml); --format csv is CLI-GENERATED and LOSSY — see the CSV honesty note below (default file .csv; what was dropped prints on stderr and rides data.loss_report); JSON data {project, paths, file, stdout, tag_count, loss_report?}; provider-ROOT paths ([default] alone, or a bare provider name like default) exit 6 provider_root_unsupported — see the bulk export note below |
ign tags import --file FILE|- --provider NAME [--format json|xml|csv] [--collision-policy abort|overwrite] [--project NAME] | Import a tag export into a target provider | needs the deployed routes; the provider must exist (ign tags provider create NAME); the loss gate (TAGS-12): xml/csv inputs are SCANNED before anything is sent — if the scan reports losses, the CLI prints the loss report and refuses exit 2 invalid_input (profile null, zero requests) unless --yes; with --yes the structured scan summary rides data.loss_report; the locked collision matrix: abort (default) pre-checks by browsing the target and refuses exit 6 (tag_collision, hint names --collision-policy overwrite) BEFORE any write, then imports with server-side abort as the backstop; overwrite replaces existing tags — destructive: exit 2 without --yes, no pre-check (the server is the authority); merge is Designer-only (not a value); JSON data {project, provider, collision_policy, imported, format, top_level_names, loss_facts, failed?, loss_report?} |
ign tags alarms active [--source S] [--priority P] [--state S] [--project NAME] | List ACTIVE alarms — eventId (FULL uuid) source state priority name | needs the deployed routes; only present filters ride the wire (kwargs passthrough to system.alarm.queryStatus); state strings read 'Active, Unacknowledged' verbatim — never parsed; JSON rows carry {event_id, source, state, priority, name} (name null when the event carries none); the printed eventId is copy-pasteable straight into tags alarms ack |
ign tags alarms history --start T --end T [--project NAME] | Query alarm history (journal rows, columns dataset-dependent) | needs the deployed routes; a journal-provisioned gateway only — default rigs refuse exit 6 alarm_journal_missing with the hint naming the provisioning chain (see Alarm history below); --start/--end take RFC3339 or epoch-ms; rows ride VERBATIM (the journal schema varies by Ignition version — the header IS the column list) |
ign tags alarms ack ID... --username NAME [--note NOTE] [--project NAME] | Acknowledge alarms — the count + the unacknowledged remainder | needs the deployed routes; the gateway-scope 3-arg wire form needs the username, so --username is REQUIRED (the CLI never guesses one); NOT --yes-guarded by design — acknowledging never un-acknowledges anything (a state-advancing, read-adjacent verb); ids: full UUIDs pass through verbatim, SHORT prefixes expand against the active-alarm list (ambiguous → exit 2 naming the candidates; unknown → exit 2 naming the miss); the 8.3 return IS the unacknowledged remainder — acknowledged is computed honestly (requested − remainder); JSON data {project, acknowledged, unacknowledged} |
ign tags history query PATH... --start T --end T [--return-size N] [--aggregation MODE] [--project NAME] | Query HISTORICAL tag values — the t_stamp column + one column per tag | needs the deployed routes; structurally safe on ANY rig (zero historians → a well-formed dataset with null values, exit 0); DATA requires a provisioned historian (see Tag history below); t_stamp is preserved EXACTLY (never renamed) and tag columns ride PROVIDER-RELATIVE ([default]P5H/T1 surfaces as P5H/T1 — live-proven); --start/--end take RFC3339 or epoch-ms; --aggregation defaults to the route's LastValue; JSON data {project, paths, columns, rows, row_count} |
ign rig [--rig NAME] up [--timeout S] | Bring a Docker compose rig up (compose up -d --wait) and wait for the gateway | docker-only (profile: null envelope); --timeout is BOTH compose's --wait-timeout and the commissioned-probe deadline (default 300 s); a fresh-volume rig reports "up, uncommissioned" as DATA (exit 0, wizard URL in warnings) |
ign rig [--rig NAME] down | Stop the rig (compose down --remove-orphans; volumes KEPT) | docker-only; the volume-deleting teardown belongs to rig reset |
ign rig [--rig NAME] reset [--timeout S] | Tear the rig down AND remove its volumes, then bring it back up fresh (down -v --remove-orphans → pre-flight → up --wait → commissioned wait) | destructive: exit 2 (confirmation_required) without --yes or IGNITION_YES=1, BEFORE any discovery runs; removed_volumes in the data reports exactly what -v took; no stale project/trial state survives (a fresh volume usually boots uncommissioned — exit 0, wizard URL in warnings) |
ign rig [--rig NAME] status | Structured rig status: services (state/health/ports), volumes, ports occupancy | docker-only; an ALLOWLIST only — never a compose-config passthrough (the resolved config contains gateway passwords); a down rig is exit-0 data |
ign rig [--rig NAME] logs [--tail N] [-f] [SERVICE] | Stream the rig's container logs (compose logs passthrough) | raw lines in EVERY mode — the third stdout exception (see §Streaming); --tail default 200; -f follows until Ctrl-C (default process kill); compose diagnostics go to stderr, never the data stream |
ign rig [--rig NAME] trial status | Show the rig gateway's trial state: licenseMode, trialState, seconds left, expired — plus the banners cross-check | credential-free (the trial/banners endpoints answer unauthenticated — verified live on 8.3.3 AND 8.3.6; a fresh rig with no token reports fine); addresses the RIG's derived gateway URL (never the profile's); data {license_mode, trial_state, trial_remaining_s, expired, emergency, emergency_remaining_s, development, banners: {severity, expire_time_ms, active}, warnings} — banners.active is the Pitfall-7 cross-check (severity=="info" AND expireTime>now_ms), never the primary truth (expired is) |
ign rig [--rig NAME] trial reset [--user NAME] | Reset an EXPIRED trial to a fresh ~2 h window via the mechanism ladder | destructive: exit 2 (confirmation_required) without --yes, BEFORE any discovery; ladder = tier 0 POST /data/api/v1/trial with X-Ignition-API-Token (token from IGNITION_TOKEN) → tier 1 native gateway login (internal-IdP OIDC challenge dance → session cookie + CSRF header), creds --user/IGNITION_USER + IGNITION_PASSWORD (password NEVER a flag); success REQUIRES the read-back flip (expired false on re-fetch — a bare 2xx never suffices); a NON-expired trial refuses exit 6 trial_not_expired (the gateway 403s resets while active — live-verified); no creds at all → exit 3 |
ign rig [--rig NAME] snapshot [-o DIR] | Snapshot the rig's gateway: native gwbk (GET /backup?type=roaming, STREAMED to disk) + per-project exports + manifest.json — composed in a timestamped dir | addresses the rig's derived gateway URL; requires IGNITION_TOKEN (the backup route 401s unauthenticated — live-verified shape); default dir ./ign-rig-snapshots/<rig>-<yyyyMMdd-HHmmss>/; data {dir, gwbk_bytes, projects, manifest_path} — the manifest names BOTH composition exclusions (see §rig snapshot/restore) |
ign rig [--rig NAME] restore --file PATH [--timeout S] | Restore a gwbk onto the rig's gateway (raw octet-stream POST), wait for the witnessed post-restore RUNNING | destructive: exit 2 without --yes, BEFORE any discovery; the restore is synchronous and the gateway RESTARTS after — success is a WITNESSED StatusPing→RUNNING (deadline floored at 300 s), never a bare 2xx; data always carries the token-clobber warning (API tokens may have been reset by restore…, see §rig snapshot/restore); requires IGNITION_TOKEN |
ign backup download [-o FILE] [--type roaming|all] | Download a gwbk from ANY profiled gateway (streamed to disk — the standalone sibling of rig snapshot's gwbk leg) | --type roaming (default) = the portable backup; all includes gateway-specific state; default filename from Content-Disposition, else <profile>-backup.gwbk; read, unguarded; JSON data {file, type} |
ign backup restore <FILE> --yes | Restore a gwbk onto THIS gateway (the ACTIVE profile's — rig restore's standalone sibling without the rig wait) | destructive — the 8th --yes-guarded verb: exit 2 (confirmation_required, profile null, ZERO network) without --yes; REPLACES this gateway's state, then the gateway restarts and blocks for minutes (the 2xx is acceptance — see §Backups); a nonexistent/empty/non-regular file exits 2 invalid_input pre-network; JSON data {restored: true} |
ign eam history [--limit N] [--search TEXT] | EAM task run history — ISO taskName [level] target detail rows (newest first) | rides the RUNTIME seam: a stock (non-controller) gateway refuses exit 6 eam_not_controller with the manual-flip hint (definitions still list — see below); --limit defaults to 200 and is ALWAYS sent explicitly (the server default is unlimited); outcomes are DATA (Failed + GNET-not-connected detail read exit 0); JSON data {items, count} — items passthrough under the gateway's own camelCase keys (taskName, taskStart epoch-ms, taskType) |
ign eam tasks [NAME] | Task definitions: bare form lists (name type schedule state); with a name shows the full definition + its scheduled state | rides the config-resource seam (com.inductiveautomation.eam/eam-tasks) — definitions answer on STOCK gateways (no controller needed); JSON list data {tasks: [{name, task_type, schedule_mode, current_state}]} (all keys always; current_state null on list records — find answers carry it); detail data {name, definition, state}; an unknown name exits 6 not_found |
ign eam task new <NAME> <TYPE> [--target NAME]... [--setting K=V]... [--definition PATH] [--schedule-mode MODE] | Create a task definition — the typed guard ladder | the planner-locked ladder: eam_backup + OnDemand (the default schedule) fires UNGUARDED (it never auto-fires and only acts when forced); MUTATING types (eam_restart, eam_sendProject, eam_sendResource, eam_sendTags, eam_activateLicense, eam_updateLicense, eam_unactivateLicense) and ANY non-OnDemand --schedule-mode (Immediate/Scheduled/AtTime/AtDelay — they arm autonomous actions) need --yes (exit 2 pre-resolution, zero network); the FLEET-DESTRUCTIVE trio (eam_restoreBackup, eam_installModules, eam_remoteUpgrade) REFUSES outright — exit 6 eam_task_type_refused naming the EXT-03 (v2) scope (run them from the EAM console); --setting K=V auto-types scalars (bool/int ride typed, else string); --definition PATH deep-merges a full-JSON settings file over the composed config.settings (objects merge, arrays/scalars replace) — mutually exclusive with --setting; the POST body is the config-resource ARRAY shape with the live 8.3.3 profile/settings split (config.profile = {type, scheduleMode} only; config.settings = {targetGateways, targetGroups, …}); targetGateways defaults to ["_controller"] when no --target is given (the controller itself); JSON data carries the composed definition verbatim |
ign eam task force <NAME> --yes | Force-dispatch a task NOW (find → owner → POST → history read-back) | destructive — always --yes-guarded (dispatches to the agent targets immediately; exit 2 pre-resolution without); the guard is the two-tier blast-radius gate (see below): the refusal message IS the preview line naming the task, its agent targets, and the factual impact; the owner resolves from the healthcheck's scheduledTaskState.details.owner (fallback eam); a 2xx is DISPATCH acceptance — the run's OUTCOME lands in history as data (Failed + GNET-not-connected detail is the honest shape of an unconfigured agent; trial expiry blocks runs); JSON data {task, owner, dispatched, history} |
ign eam task suspend <NAME> --yes | Suspend a task definition's scheduled dispatches (runtime seam) | guarded — the two-tier blast-radius gate (exit 2 pre-check + preview-in-the-refusal without --yes, zero writes; exit 6 not_found on a bad name BEFORE any prompt); on a stock gateway the runtime seam honestly refuses exit 6 eam_not_controller (manual-flip hint); TASK-scoped — there is NO agent-level suspend on the wire (see the reconciliation note below); JSON data {task, action, previous_state, config_suspended, pending, fired, reason} |
ign eam task resume <NAME> --yes | Resume a suspended task definition (runtime seam) | the suspend inverse — same guard, same seams, same honesty; JSON data as suspend |
ign eam task cancel <NAME> --yes | Cancel a task's PENDING execution (runtime seam) | same guard; nothing pending is an honest no-op (fired: false + the reason ride the data — a write we declined is reported, never disguised); a canCancel: false row surfaces the gateway's own refusal |
ign eam task modify <NAME> [--enable|--disable] [--schedule-mode MODE] [--setting K=V]... [--description TEXT] --yes | Rewrite targeted keys of a task definition (config seam — works on stock gateways) | full-record read-modify-write: every key the gateway answered rides back, only the targeted keys change, and the body carries the record's signature (a stale signature is refused by the gateway); --enable/--disable conflict (clap-validated); --setting reuses the eam task new K=V auto-typing and deep-merges over config.settings (repeatable); a modify that targets nothing refuses exit 2 pre-network (a no-op PUT would still rotate the server-side signature); no --rename by wire honesty — the gateway answers a renamed PUT with 404 empty; rename is the create-new + delete-old composite; the guard is the two-tier gate (preview reflects the post-change impact); JSON data {task, changed, definition, put_outcome, readback} |
ign eam task delete <NAME> --yes | Delete a task definition (config seam — works on stock gateways) | signature-keyed (the gateway refuses a stale signature) behind the two-tier gate; JSON data {task, deleted, affected} — affected names the resources the gateway reported |
ign script run --code PY|--file PATH|- [--project NAME] | Execute gateway-side Python (Jython) through the secret-gated scriptExec route — non-interactive, the route's entire purpose | the opt-in is STRUCTURAL, not a flag: scriptExec deploys only via ign webdev deploy --with-script-exec (which generates + persists the secret); without it the verb exits 6 script_exec_not_configured with ZERO HTTP, hint naming the deploy flag; no --yes by design — the deploy flag IS the opt-in and agents need it non-interactive; --code/--file are mutually exclusive (both or neither → exit 2 invalid_input before any resolution; --file - reads stdin — the agent pipe path); each run probes the route's version handshake then execs (two round trips); JSON data {stdout, result, elapsedMs} — ALL keys always; a route-side Python exception surfaces its traceback verbatim (exit 6 webdev_route_error); NO server-side execution timeout exists — a long-running script holds the HTTP connection (the client's per-request timeout class applies); the secret NEVER appears in any output mode (see the scriptExec posture below) |
ign testing run --project NAME [--discover] [--module DOTTED|--package PREFIX] [--format json|junit|text] | Run the gateway's own Jython test suite through the deployed testing bundle and return a machine verdict — the agent/CI verb (no Designer, no ign adopt) | --project is required (the bundle deploys per project and a run executes THAT project's gateway-side code — there is deliberately no ign-cli default); --discover lists the discovered test modules; --discover/--module/--package are mutually exclusive (exit 2 invalid_input naming both flags, with ZERO HTTP requests); the verdict rides the BODY, never the status line — the route answers 200 on green and 207 on failures/errors and both are success transport shapes, so failed + errors > 0 is the only oracle; a green run exits 0; a RED run renders the FULL results in the success envelope and exits 6 with data.slug = "tests_failed" (the ign lint --strict precedent — the error envelope has no data field to carry results, and every test runner puts the report on stdout and the verdict in the exit code; tests_failed is NOT a CoreError slug and never appears in the exit-code table); an undeployed bundle exits 6 routes_not_deployed with a hint naming ign webdev deploy --with-testing / ign adopt --project NAME --testing (404/405/501 are all absent markers — a raw-status probe runs BEFORE the run POST so an absent bundle can never read as a passing suite); a first-touch lazy-compile 500 is retried exactly once (3 s) on each leg; --format renders client-side — the route is ALWAYS asked for json, so the verdict and exit code are identical in every format (the route's own junit/text answers stay HTTP 200 and carry no counts), with the rendered document in data.report and the structured truth in data.results; JSON data {mode, project, modules, count, verdict, slug, passed, failed, skipped, errors, total, durationMs, report, results} — ALL keys always; NO server-side execution timeout exists — a long-running suite holds the HTTP connection (the client's per-request timeout class applies) |
ign lint PATH... [--strict] [-- ARGS...] | Lint local project files by delegating to ignition-lint (PATH-discovered; no gateway, profile: null) | doctor posture: exit 0 whenever the tool RAN — findings, child_exit_code, and the parsed JSON report ride as data (ALL keys always; report null + stdout verbatim when unparseable; stderr_preview capped at 4000 chars); --strict exits with the tool's own code for CI (envelope prints first — the one sanctioned success-path exit exception; 1 = findings at the --fail-on threshold); PATHS map to --target <path> pairs + --report-format json on an ARG VECTOR (never a shell string); anything after -- passes through verbatim; no tool on PATH → exit 6 lint_tool_absent with the install hint (uv tool install ignition-lint-toolkit); pair with project export --decode-scripts to lint the decoded sidecars |
ign e2e doctor [DIR] [--project NAME] | Diagnose the browser-E2E setup before (or instead of) scaffolding one: node (≥20, version-probed), npm, @playwright/test inside DIR (default ./e2e), a downloaded chromium in the Playwright browsers cache, the gateway's testing bundle, and the gateway's trial state | doctor posture — exits 0 whenever the diagnosis COMPLETES: every finding is a checks[] row, never an exit code, so a toolless host, an unreachable gateway, and an undeployed bundle all still return the full report (agents parse checks[]; humans read the table). Six rows in a fixed order — node, npm, playwright, chromium, testing-bundle, trial — each in the standard {name, status, detail, hint} shape with status one of ok/warn/fail/skip. Read-only and offline-safe: chromium presence is a pure filesystem scan of PLAYWRIGHT_BROWSERS_PATH (else the per-OS default — ~/Library/Caches/ms-playwright on macOS, ~/.cache/ms-playwright on Linux) for a chromium-* or chromium_headless_shell-* entry, deliberately not npx playwright install --dry-run, which would reach for the npm registry. A missing browser or an uninstalled runner is warn (one command away), an absent or too-old node is fail with the install hint. The two gateway rows report skip when no profile resolves or no --project is given — --project has no default, and a profile that will not resolve is not an error for this verb (envelope profile is null); an absent bundle is fail hinting at ign webdev deploy --with-testing, an expired trial is warn hinting at ign rig trial reset |
ign e2e init [DIR] [--project NAME] [--run-project NAME] [--browsers] [--yes] | Scaffold a Playwright browser-E2E suite into DIR (default ./e2e) and npm install it: playwright.config.mjs, a global-setup.mjs that logs in via ign session login and gates on ign testing run, lib/gateway.mjs (tag/script helpers over the testing routes), tests/example.spec.mjs, .gitignore, README.md — seven members, all embedded in the binary | requires the global --yes; without it the verb writes NOTHING and spawns NOTHING and refuses exit 2 confirmation_required after previewing the target, every member with its would-be status, and both command lines verbatim — the refusal IS the dry run, which is why no separate --dry-run flag exists. Idempotent: an existing file is reported skipped and never overwritten, so a re-init over a suite you have edited is safe. A non-empty target that is not already a scaffold (no playwright.config.mjs) refuses exit 2 invalid_input naming files it found, before any write. A host with no node on PATH refuses exit 6 node_tool_absent with the same install hint ign e2e doctor's node row carries. Both gates run before any write AND are re-run inside the action, so in-process TUI/MCP callers get the same refusals. npm install (and, with --browsers, the Chromium download) run with ARG VECTORS, never shell strings, streaming their output through; a non-zero installer is DATA, not an error — a network-less host is still left with a usable scaffold on disk. --project defaults to ign-cli (the CLI's own WebDev project), --run-project defaults to the effective --project; the gateway URL comes from the profile, falling back to http://localhost:8088 when none resolves (scaffolding must work before a profile exists). JSON data {dir, files[{path, status}], npm_install, browsers} — ALL keys always |
ign api call --method M --path /P [--data TEXT] [--header "Name: Value"]... [--query k=v]... | Raw passthrough to any gateway REST endpoint — the escape hatch for the uncurated endpoint families (any HTTP verb accepted) | the envelope's data.result.data IS the gateway's JSON verbatim (no field dropped, no value coerced, key order preserved — the documented contract exception above); a non-JSON 2xx body exits 1 internal with the explanation (binary endpoints: use logs/backup downloads); auth-pattern headers (Authorization, X-Ignition-API-Token, Cookie — case-insensitive) refuse exit 2 pre-resolve with ZERO gateway requests (credentials come from the profile); --path must start with /, refuses host-shaped URLs and embedded ? (use repeatable --query k=v — the ONE query mechanism); --header splits on the FIRST : (repeatable), --data is raw text on ANY method (GET/DELETE bodies allowed — curl parity); exit partition: 401/403→5, 404→6, 503→6, 500→1, every other 4xx→2 gateway_client_error with the body verbatim (4 KiB cap + ... [truncated] marker) |
ign license status | The license morning-check in ONE command: license mode + countdown, then per-hardware-key item rows (name title version) | merges TWO reads — /data/api/v1/licenses (the inventory) + /data/api/v1/trial (the trial companion); the license MODE lives on the trial wire, not the licenses payload (live-capture fact — fresh-rig /licenses carries no mode/edition keys); models are version-tolerant: unknown keys ride the JSON passthrough, license-array ELEMENT shapes were not capturable on fresh rigs (no activations) and stay lenient; JSON data {license, trial} — trial carries license_mode, trial_seconds_left, expired; authed read (exit 3 without a credential, exit 5 on bad auth) |
ign redundancy status [--peer PROFILE] | The redundancy morning-check: role, project state, activity, peer connection, config access, sync/failover pending, uptime, last-sync | units AS CAPTURED on 8.3.3 + 8.3.6: uptime = ms since gateway start (wall-clock proven twice); lastSyncTimestamp = epoch-ms (proven on a live 8.3.6 pair), -1 = never synced (JSON rides the raw value); role is a String (Independent/Master/Backup seen — unknown future roles ride, never refuse), and activity (Active/Cold) decides which node serves; role does not; peer-connected shapes were not capturable on a single fresh rig (peer fields lenient); JSON data {status} with the gateway's own camelCase keys; --peer reads the second node (through PROFILE's own secret chain) and adds active_node (this, peer, both or neither), decided by activity level, never role; both is a split-brain signal; a Cold backup with uptime: -1 is a healthy standby; naming the active profile as --peer is exit 2 invalid_input before any request; without --peer the output is unchanged; authed read |
ign gan status | The Gateway Area Network morning-check: total/running connections, in/out byte rates, remote gateways | the zero-connection capture IS the canonical shape — all zeros on a non-GAN gateway are healthy data, not absence of feature; byte rates ride JSON floats (0.0), counts ints; one-command truth (overview/gan) — the full remote-gateway list stays with ign api call; JSON data {status}; authed read |
ign diagnostics bundle generate | Start a diagnostics support-bundle generation (POST, no body) — the 200 answer IS the fresh status | capture-encoded state machine (8.3.3 + 8.3.6, 09-02 rigs; Invalid added from the 2026-09-07 UAT): the captured vocabulary is Generating (mid-generation), Valid (terminal/ready), and Invalid (terminal "no current bundle" — see wait); PascalCase; state is a String, never an enum, so an unobserved future state rides verbatim and never refuses; generation takes ~2–6 s on a fresh rig — pair with wait; JSON data {state}; authed read |
ign diagnostics bundle status | The bundle status: captured state + size | fileSize (bytes) is ABSENT while Generating (the key is missing on the wire — not null, not 0) and appears with Valid, equal to the download's byte count exactly; JSON data {state, fileSize}; authed read |
ign diagnostics bundle download [-o FILE] | Download the bundle ZIP (streamed to disk) | the request rides a 300 s per-request timeout override (the 30 s client default would truncate MB-sized bundles — Pitfall 8); bytes land verbatim (PK\x03\x04 ZIP magic live-verified); default filename is the gateway's Content-Disposition basename, else ignition-diagnostics-bundle-<unix_ts>.zip; the ready state persists after download (repeatable — live-captured); JSON data {file, bytes, content_type} |
ign diagnostics bundle wait [--interval SECS] [--timeout SECS] | Poll the status until the bundle is ready — defaults 2 s / 300 s (the restart --wait shape) | the captured vocabulary drives termination (three states, live-proven on the 09-02 rigs + the 2026-09-07 UAT): Valid (terminal/ready) exits 0 with {state, fileSize}; Generating (mid-generation) keeps polling; Invalid is a TERMINAL steady state — the gateway's "no current bundle" answer (observed 2026-09-07 on 8.3.6: a Valid bundle decays to Invalid within ~2 min unprompted and stays Invalid) — wait exits IMMEDIATELY exit 6 bundle_not_available naming the state; only a fresh generate changes it (polling cannot); an UNKNOWN state (outside the captured vocabulary) is honestly NOT terminal — it keeps polling until the deadline, and the deadline message carries the last observation instead of claiming unreachability (the gateway answered, so it never says the gateway was unreachable); a deadline with NO observation stays exit 4 network_error; JSON data {state, fileSize}; authed read |
ign jvm sample [--duration-secs 5–120] [--interval-ms 5–1000] [--top 1–100] [--folded-out PATH] [--follow] --i-accept-the-risk [--project NAME] | SPECIAL MODE — sample every JVM thread's stack for a capped window through the secret-gated scriptExec route and rank the hottest methods/stacks (folded output is flamegraph-ready); --follow is CONTINUOUS mode: bounded windows back-to-back until Ctrl-C | ACTIVE instrumentation — never a routine diagnostic: refused exit 2 profiling_risk_not_accepted without --i-accept-the-risk (the global --yes/IGNITION_YES deliberately does NOT satisfy this gate — zero HTTP either way); durations are HARD-CAPPED (120 s; refused, never clamped — over-cap is exit 2 invalid_input, profile null, before any resolution); CPU overhead grows with thread count × frequency — never run on a production server outside an approved change window; every render leads with the production warning (stderr banner in human mode, data.warning in JSON); --follow streams each window as it lands — NDJSON envelopes under --json/--compact (the logs -f streaming exception, data.warning on every line), defaults drop to the gentler 10 s @ 50 ms cadence, and --folded-out is refused with it (exit 2 — one sidecar cannot hold every window); requires the scriptExec deploy (ign webdev deploy --with-script-exec, else exit 6 script_exec_not_configured); JSON data {warning, duration_secs, interval_ms, samples, thread_peak, observations, wall_ms, hot_methods, top_stacks, folded, elapsedMs} — ALL keys always, warning first; every pct is count / observations (per-THREAD counts normalize against total thread stacks observed, never against samples), and wall_ms is the gateway-measured loop time (the requested interval rides on dump cost — the effective cadence is longer on busy gateways) |
ign jvm jfr start [--duration-secs 10–300] [--settings default|profile] --i-accept-the-risk [--project NAME] | Start a Java Flight Recording named ign-cli via the JVM's DiagnosticCommand MBean | same special-mode gate as sample (--i-accept-the-risk required, --yes does not satisfy); memory-buffered (nothing touches gateway disk until stop dumps) and duration-capped so it AUTO-STOPS even if you forget (hard cap 300 s, refused not clamped); a leftover ign-cli recording refuses route-side naming ign jvm jfr stop; JSON data {warning, name, duration_secs, settings, detail, elapsedMs} |
ign jvm jfr status [--project NAME] | List the JVM's flight recordings verbatim (jfrCheck) | READ-only and ungated — checking is how you confirm cleanup; JSON data {warning, active, recordings, elapsedMs} (active = an ign-cli recording exists) |
ign jvm jfr stop [-o FILE] [--project NAME] | Stop the ign-cli recording, dump it, fetch the .jfr (base64 through the envelope), and DELETE the gateway-side file | cleanup — ungated by design (stopping reduces risk and must never be refused); dumps over 32 MiB are NOT streamed: the recording stops, the file stays on the gateway, and fetched:false + remote_file report it (never silently truncated); default local name ign-cli-<unix_ts>.jfr; JSON data {warning, name, file?, fetched, bytes, remote_file, remote_deleted, elapsedMs} |
ign profile add/list/use | Manage gateway profiles | — |
ign completions <SHELL> | Shell completion scripts | raw stdout regardless of --json |
All gateway commands honor the envelope (--json/--compact) with the
[profile: NAME] header in human mode. The inspection trio (status,
modules, metrics) replaces the gateway webpage's Status Overview,
Config > Modules, and Performance & Diagnostics pages; sessions,
connections, the logs tree, and the project tree replace its
Sessions, Connections, Logs console, logger-config, and Projects pages.