- name
- amcli
- description
- Read, search, traverse, edit, validate and draw ArchiMate architecture models (.archimate files) from the command line with the `amcli` binary. Use when the user asks about an enterprise, solution or application architecture model; when they mention ArchiMate, Archi, coArchi, a .archimate file, application components, business processes, capabilities or an EA repository; or when a repository contains a *.archimate file. Also use for: finding which services depend on a component, tracing dependencies, assessing the blast radius of a change, listing what reads or writes a data object, adding or renaming elements and relationships, checking a model for rule violations, and producing a diagram. Prefer this over opening the model XML directly — these files run to megabytes, reading one wastes context, and hand-editing the XML corrupts models in ways Archi then refuses to open.
- license
- Apache-2.0
- compatibility
- Needs a shell and the `amcli` binary; if the binary is missing, the Setup section installs it and needs network for that one step. After that: no network, no daemon, and no Archi installation — amcli works directly on the model file. The one optional server is `amcli web`, a local read-only viewer that runs only while asked to and stops with Ctrl-C.
- metadata
- {"homepage":"https://github.com/arslanrakh/amcli","binary":"amcli","spec":"ArchiMate 3.2"}
# amcli — ArchiMate models from the command line
A single binary that reads and edits ArchiMate model files directly. No GUI, no
JVM, no daemon — and, when a person wants to *look* at the model, `amcli web`
serves it read-only to their browser from the same binary.
**Never read a `.archimate` file with Read or cat.** They run to megabytes of
XML. Every question you have is one `amcli` command, and editing that XML by
hand corrupts models.
## Setup
Run this before anything else, **every session, without exception** — it
installs amcli if it is missing, updates it if a newer release exists, and
otherwise does nothing but print where the binary is. Never start work on a
binary you have not tried to update this session; an old amcli lays views out
differently and refuses flags this file describes:
AMCLI=$(sh ~/.agents/skills/amcli/scripts/install.sh)
$AMCLI --version
The installer sits next to this file, which is normally `~/.agents/skills/amcli`,
or `./.agents/skills/amcli` for a project install. It prints the absolute path
of the binary on stdout and nothing else. **Use `$AMCLI` for the rest of the
session.** A newly installed binary is usually not on the current shell's PATH
yet, so plain `amcli` will still report "command not found" even though the
install succeeded — that is the single most likely way this goes wrong.
Running it every time is cheap and safe: it always asks GitHub for the newest
release (one HTTP redirect); if that is what is installed it downloads
nothing; if it is newer it fetches it, checked against the release's SHA256SUMS
before it is unpacked and with no flag that skips the check; and only when
there is no network at all does it keep whatever is installed, and it says so
on stderr.
**Native Windows PowerShell**, where there is no `sh`, use the PowerShell one
instead. It follows the same contract:
$AMCLI = & ~\.agents\skills\amcli\scripts\install.ps1
Either installer asks for nothing, never elevates, and never edits a shell
config. If no prebuilt binary matches the platform it builds one with cargo on
its own. Do not pipe either from a URL — they are already on disk.
This skill is written for **amcli 0.17.5**. The installer always gives you the
newest *release*, and this file ships from the repository's main branch, so
for a short while after a change lands the binary can be one release behind
what is described here. If a command or flag below is refused, the binary
says so and names the fix; carry on with what the binary you have does
support, and do not reinstall the skill to match the binary — that would only
take you backwards.
## Finding the model
amcli finds the model on its own: `-m PATH`, else `$AMCLI_MODEL`, else the
nearest `*.archimate` walking up from the working directory. If several are
found it exits 4 and lists them — pass `-m`, or `export AMCLI_MODEL=…` once
for the session. It never guesses: `apply` with ten scratch copies beside you
and the real model beside the batch file names that model in the hint rather
than quietly applying to it, because a dry run that picks a model you did not
name is a real run.
## The loop
Start every architecture question here. Do not open source code first.
amcli stats # how big is this thing, and of what
amcli search <term> # find the concept, get its id
amcli get <id-or-name> # what it is, and everything it touches
amcli trace <id-or-name> -n 2 # the neighbourhood
# only now read source code, and only the files the model pointed you at
## Flags mean one thing each
-t concept type -r relationship type -f folder -D direction(out|in|both)
-n depth -l limit -m model -F format -o output file
Subjects are positional, never flags:
amcli element add ApplicationComponent "Refund Service" -f /Application
amcli relation add Serving "Refund Service" "Checkout Service"
## Output and token economy
The default output is tab-separated records, one per line — cheap to read and
easy to `cut -f2`. Counts, hints and a `# id<TAB>name<TAB>…` column header go to
stderr, so read stderr to learn what the columns are. Add `-F json` only when you
need nested structure, such as the relationship ids inside `get`. JSON is always
the same shape — `{"ok":…,"data":[…],"meta":{…}}`, success or failure — so
`.data[]` is the path whether or not `-q` is there; `-q` only quietens stderr.
amcli query 'layer=Application' --count # ask "how many" FIRST, always
amcli search auth -l 10 --fields id,name # project down
amcli list --fields -documentation # or drop a field
amcli query 'prop:reg-id=RG-14' --fields name,prop:reg-id # print what you filtered on
**A field you can filter on is a field you can print.** Besides the columns a
command prints by default, `--fields` takes `doc`, `layer`, `kind`, `deg`
(`in + out`, the number the filter compares), `properties` (the whole list —
in JSON the same array `get` carries, in text a count) and any `prop:KEY` —
matched case-insensitively, as the filter matches it — and the value is
appended as a column, empty where the concept has none. That is the route to
one property: you do not need `-F json` to read it. It works on `view list`
too. A field no record has is refused before any row is printed: exit 2 on a
read, naming the columns the record does have; on a write, where the file has
already changed, a warning ahead of the row instead, said whatever the flags.
**JSON shapes.** A concept row from `list`, `search` or `query` has no
`properties` key at all — `jq` prints `null` for a missing key — while `get`
always carries `properties` as `[{"key","value"}]`; ask a list for
`--fields …,properties` to get the same array, or `prop:KEY` for one value.
A relationship reads the same wherever it turns up: `query 'kind=relation'`
rows and the `relations` nested in `get` both carry `id`, `type`, `name`,
`source`, `source_name`, `target` and `target_name` — `get` adds `direction`
and `other_*` for the end that is not the one you asked about.
**A capped answer says so on stderr, and `-q` cannot silence it.** Every list
is cut to `-l` (50 by default) and prints `showing 50 of 83` when it cuts;
`-l 0` gives all of them, and `--count` gives the true total without the rows.
`-q` drops the header and the notes — never that line, and never the
`"truncated"` field in the JSON envelope. If you are counting, count with
`--count` or check the line.
`--count` and `--dry-run` never write, on any command.
Never run an unfiltered `list` on an unfamiliar model. Run `amcli stats` first.
## Addressing concepts
id:id-5dde26f7… an id — always unambiguous, always prefer it
id:5dde26f7 the hex without `id-`, or a prefix of it (4+ characters)
"Payment API" an exact name
ApplicationComponent:"Payment" a name qualified by type
"*Payment*" a glob
Ids in an Archi model are `id-` and thirty-two hex characters; `id:` takes
the whole thing, the hex alone, or a prefix of it of at least four characters,
so what you copy off a row resolves whether or not you kept the `id-`. A
prefix two ids share is exit 4 with both listed. A miss says what ids look like
in this model. Views take the same three forms.
Filter expressions, quoted as one argument:
amcli query 'type=ApplicationComponent and name~payment'
amcli query 'prop:owner=team-a and not folder^=/Technology'
amcli query 'layer=Application and deg>10'
amcli query 'out:Access~Customer' # everything that accesses something matching
amcli query 'kind=element and view=0' # in the model but on no diagram
amcli query 'kind=element and view~"Refunds"' # what a view holds
Operators: `=` exact · `~` contains · `^=` prefix · `=~` regex · `!=` · `>` `<`
on `deg` and `view`. Fields: `id name type kind layer folder doc deg view
prop:KEY in:RelType out:RelType`.
Two of those are easy to get wrong:
- `kind` is `element` or `relation`. Without it, filters like `layer!=none`
return relationships mixed in with elements, and relationships have no name.
A relationship row carries `source`, `source_name`, `target` and `target_name`
after the usual columns, so `amcli query 'kind=relation'` and `amcli get` on a
relationship both say what it joins — read that before deleting one.
- `type` takes one ArchiMate type and accepts either spelling — `Triggering` and
`TriggeringRelationship` are the same thing. So does `-t`. A type that does not
exist is exit 2 with the model's own types listed, never an empty result.
- `view` compares as a **number** against how many views a concept is drawn on
(`view=0`, `view>1`), and as a **name** otherwise (`view~"Payments"`). The
`views` column on every concept row is that same count.
## Exit codes — branch on these, do not parse messages
0 ok 2 usage 3 not found 4 ambiguous 5 invalid 6 conflict
7 io 8 unsupported
On **3**, the response lists the nearest names — retry with one, do not run
another search. On **4**, it lists candidates, each with a ready-to-paste
`id:` selector. Never work around an ambiguity by guessing; re-run with the id.
## Graph questions
amcli path "Web App" "Customer Database" # how are these connected?
amcli impact id:5dde26f7 -D in # what breaks if this changes?
amcli neighbors id:5dde26f7 -r Serving # only Serving relationships
amcli ancestors "Payment API" # what composes or aggregates it
amcli descendants "Payments Capability" # the composition tree
amcli cycles # dependency cycles
## Editing
amcli init "Model Name" -o model.archimate # a new, empty model
amcli element add ApplicationComponent "Refund Service" --doc "…"
amcli element rename id:c40a19b7 "Refunds Service"
amcli relation add Access "Refunds Service" "Refund Record" --access rw
amcli relation add Association "Refunds Service" "Ledger" --name "posts to"
amcli element doc id:c40a19b7 "What it is for"
amcli element move id:c40a19b7 -f /Application/Payments
amcli element retype id:c40a19b7 TechnologyService
amcli element merge "Old" --into "New"
amcli prop set id:c40a19b7 owner team-payments
amcli element delete id:c40a19b7 -y
Never hand-write the XML skeleton for a new model — `amcli init` writes it with
the nine folders Archi expects.
`element retype` changes the type in place — same id, relationships and
boxes, only the figure drawn changes — and is refused, listing them, when a
relationship it has would break the matrix for the new type; fix those first,
there is no force. `element merge` folds one element into another that means
the same thing: relationships and boxes are repointed to the survivor,
duplicates and would-be loops dropped, documentation appended and properties
copied (`--drop-doc` leaves the words behind), and the merged element deleted.
Every write is checked against the ArchiMate relationship matrix first and
refused (exit 5) if the standard forbids it — and the refusal names what *is*
permitted between those two types, so read it rather than guessing again.
**A new relationship is drawn on every view that already shows both of its
ends** — at the prompt and from a batch alike — because a view is a drawing
of the model and what the model now says between two boxes belongs on the
line between them. The row's `views` column counts the views it reached and a
note names them; when one end is drawn somewhere but no view shows both, the
note says so, and `amcli view add` of the missing end draws it there. `--no-draw`
(`"no_draw":true` in a batch) adds it to the model only. `--name` puts a label
on the line — Archi reads an Association as "related to" without one and
"owns" with it — and `query` and `get` print it as `name`.
Deleting refuses by default when it would take other things with it, and the
refusal is the impact report. Add `-y` once you have read it.
Use `--dry-run` when unsure. Use `--expect-checksum` when you read the model on
an earlier turn and are writing now:
CS=$(amcli info -F json | jq -r '.data[0].checksum')
amcli element rename id:x "New" --expect-checksum "$CS" # exit 6 if it moved
**For more than two edits, use one atomic batch rather than a sequence.**
amcli apply - <<'EOF'
{"op":"element.add","type":"ApplicationComponent","name":"Refund Service","ref":"r","if_absent":true}
{"op":"element.add","type":"DataObject","name":"Refund Record","ref":"rec","if_absent":true}
{"op":"relation.add","type":"Access","source":"ref:r","target":"ref:rec","access":"rw","if_absent":true}
EOF
`ref` names a line's result so a later line can point at it before its id
exists. `if_absent` makes the batch safe to re-run. If any line fails, nothing
is written and the file is byte-identical. View operations go in the same
batch — `view.create`, `view.add` (with `into` to nest), `view.group`,
`view.note`, `view.nest`, `view.sync`, `view.auto`, `view.layout`,
`view.rename`, `view.move`, `view.delete`, same fields as the commands — so a
view is built and laid out with the concepts it shows, in one write;
`references/batch.md` has those and the folder ops, `folder.rename` and
`folder.move` included.
Deletes go in a batch too — `element.delete`, `relation.delete`, `prop.unset` —
which is what lets a change that is really a *replacement* land as one write.
Retyping a relationship is the common one, and the model never passes through a
state where it says the wrong thing:
amcli apply - <<'EOF'
{"op":"relation.delete","target":"id:8f3c1a02","if_present":true}
{"op":"relation.add","type":"Realization","source":"Payment API","target":"Payments","if_absent":true}
EOF
`if_present` is `if_absent` for deletes: with it, the second run of that batch
finds nothing to do and writes a file identical to the one it read.
**If the model is regenerated from batches you keep in the repository**, pass the
same `--id-seed` every time (or set `$AMCLI_ID_SEED`). Ids are then derived from
what they name instead of drawn at random, so rebuilding an unchanged model
produces an unchanged file and the diff shows only what you meant to change.
Without a seed every rebuild reissues every id and the whole file appears to
change. Use one seed per model and do not change it afterwards.
## Before you finish any edit
amcli validate
Exit 5 means the model has errors. Each finding names a line in the file and
carries a `fix` command. `amcli validate --fix` applies only the repairs that
are derived rather than chosen — orphaned diagram objects and stale view
mirrors — and never deletes anyone's modelling.
## Views and diagrams
amcli view list # columns are named on stderr
amcli view create "Refund Flow" -f /Views/Payments
amcli view auto "Refund Flow" --from "Refund Service" -n 2
amcli view add "Refund Flow" "Fraud Check" # drawn *and* wired to what is there; a repeat adds nothing
amcli view add "Refund Flow" "Fraud Rules" --into "Fraud Check" # nested inside it; no line, the nesting says it
amcli view group "Refund Flow" "Card rails" # a titled box that stands for no concept
amcli view note "Refund Flow" "Sandbox until go-live" --into "Card rails"
amcli view nest "Refund Flow" "Fraud Rules" --into "Card rails" # move a box into another; no --into puts it back on top
amcli view sync "Refund Flow" # draw what the model says between members and the view does not show
amcli view style "Refund Flow" "Card rails" --fill "#eef5fc" --font-size 20 --font-style bold --border rectangle
amcli view style "Refund Flow" "rel:id:8f3c1a02" --line "#2563a6" --label "P7 · bureaus" # every line drawing that relationship
amcli view connect "Refund Flow" "Fraud Rules" "Acquirer" # one line, where sync would draw them all
amcli view route "Refund Flow" id:<connection> --points 640,200 900,200
amcli view layout "Refund Flow" --relayout-all
amcli view rename "Refund Flow" "Refunds"
amcli view move "Refunds" -f /Views/Payments # re-file, id unchanged
amcli view viewpoint "Refunds" application_cooperation # or "" to clear
amcli view doc "Refunds" "What this drawing is for." # or "" to clear
amcli view delete "Refunds" # removes the drawing, no concept
amcli view render "Refunds" -o refund.svg
amcli view render "Refunds" -o refund.png --scale 2 # a raster, from the extension
amcli export views # the batch that rebuilds every view
amcli export mermaid # a quick inline diagram for chat
`view add` of a concept already on the view is a no-op with a note — the box
stays where it is, the row says `added false`, and any relationship it can
newly draw to what is there is drawn — so a rebuild batch that re-adds every
member is re-runnable and never leaves a second box for one element.
**A box inside a box is how Archi shows the relationship between them**, and
it draws no line for it — by its default preferences, for every relationship
type. amcli follows that rule exactly. `view add --into` draws the new box
inside a concept already on the view, a group (by its name) or any object (by
its id); the container grows to hold it, and no line is drawn between the two.
A relationship whose two ends a view nests counts as *on* that view, so
`views=0` does not report it, and `relation add` does not draw a line
between a container and what it holds. `view nest` moves a box already on
the view into another one — keeping its place on the canvas and removing any
line between the two, exactly as dragging does in Archi — or, with no
`--into`, back to the top, after which `view sync` draws the lines again.
`view layout` lays each container's children out inside it, deepest first,
and sizes every container to what it holds. `view group` puts a titled box
that stands for no concept on the view, `view note` free text; both take
`--into`, and `view note --object ID "…"` rewrites a note already there.
**A poster — a drawing laid out by hand — is a batch too.** `view add`
takes `--x --y --width --height` and `--again` (a second box for a concept
the view already shows), `view group` and `view note` take sizes, `view
style` sets what a person sets in Archi's properties on a box, a group, a
note or a line (fill, line, width, font, text colour, alignment, position,
border, opacity, a label expression, the icon), `view connect` draws one
chosen relationship between two objects, and `view route` bends a line
through canvas points. `export views` writes a view that carries any style
as it was drawn — every bound, every line, every style and route, and no
layout — so a poster is a reviewable, re-runnable file rather than
something only Archi can edit; `references/batch.md` has the fields.
A view name is unique: creating a second view with a name already in use is exit
6. Pass `--replace` to overwrite the old one, which is what makes a
regenerate-everything script re-runnable:
amcli view auto "Refund Flow" --from "Refund Service" --replace
A replace redraws the view; it does not forget what the view said about
itself. The viewpoint, the documentation and the properties of the view being
replaced are kept unless the command sets them — `--viewpoint ""` clears the
viewpoint, and a `view.doc` line later in the batch overwrites the
documentation as it always did.
A **viewpoint** narrows what a view is meant to say, and Archi shows it in the
properties. `create` and `auto` take `--viewpoint` (`"viewpoint"` in a batch);
`view viewpoint` sets or clears it on a drawing that already exists, which is
what you want when a view grew past the one it was filed under. The id must be
one of the 25 ArchiMate ones — a wrong one is exit 2 and the hint lists them
all — and `""` clears it. Nothing is enforced by it: putting a concept the
viewpoint does not cover on the view is a note on stderr, not a refusal.
A view carries **documentation** exactly as a concept does, and Archi shows it
in the properties. `view doc` writes it (`"op":"view.doc"` in a batch), `""`
clears it, and `view list --fields name,doc` reads it back — a drawing is the
one thing you hand to a person, so say on it what it is for.
**Past about a dozen views, file them in folders.** `create` and `auto` take
View on GitHub