archi-cli 0.1.0__tar.gz → 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,167 @@
1
+ Metadata-Version: 2.5
2
+ Name: archi-cli
3
+ Version: 0.2.0
4
+ Summary: ar·cli·mate — de CLI in je ArchiMate: deterministisch native .archimate-modellen inspecteren, muteren, valideren en renderen
5
+ Project-URL: Homepage, https://github.com/BureauArchitectuurDigitaleOverheid/ai-assisted-architecting
6
+ Project-URL: Repository, https://github.com/BureauArchitectuurDigitaleOverheid/ai-assisted-architecting
7
+ Project-URL: Issues, https://github.com/BureauArchitectuurDigitaleOverheid/ai-assisted-architecting/issues
8
+ Author: Bureau Architectuur Digitale Overheid
9
+ License-Expression: EUPL-1.2
10
+ License-File: LICENSE
11
+ Keywords: archi,archimate,cli,enterprise-architecture,modeling
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Topic :: Software Development :: Documentation
18
+ Requires-Python: >=3.12
19
+ Requires-Dist: lxml>=6.1.3
20
+ Requires-Dist: typer>=0.26
21
+ Description-Content-Type: text/markdown
22
+
23
+ # AI-assisted architecting
24
+
25
+ [![checks](https://github.com/BureauArchitectuurDigitaleOverheid/ai-assisted-architecting/actions/workflows/ci.yml/badge.svg)](https://github.com/BureauArchitectuurDigitaleOverheid/ai-assisted-architecting/actions/workflows/ci.yml)
26
+ [![licentie: EUPL-1.2](https://img.shields.io/badge/licentie-EUPL--1.2-blue.svg)](https://github.com/BureauArchitectuurDigitaleOverheid/ai-assisted-architecting/blob/main/LICENSE)
27
+ [![PyPI](https://img.shields.io/pypi/v/archi-cli.svg)](https://pypi.org/project/archi-cli/)
28
+
29
+ Een manier van architecteren waarbij een ArchiMate-model in het native
30
+ Archi-formaat de bron van waarheid is, en je het met AI-assistentie bijhoudt in
31
+ plaats van met de hand door de GUI te klikken. Deze repo levert de twee dingen
32
+ die daarvoor nodig zijn:
33
+
34
+ - **`archi-cli`**: een deterministische command line tool die een
35
+ `.archimate`-model inspecteert, muteert, valideert en rendert, en weigert een
36
+ kapot model op te slaan. Op PyPI, werkt op elk model.
37
+ - **De skills** (`archi-model`, `archi-view`, `archi-slides`): de werkwijze
38
+ die een AI-sessie zoals Claude Code volgt om via de CLI aan het model te
39
+ werken. Te installeren als Claude Code plugin.
40
+
41
+ Het model zelf hoort niet hier: dat woont in een eigen repo en gebruikt
42
+ `archi-cli` als tool. Het ADO-model waarvoor dit ontstond is daar het
43
+ voorbeeld.
44
+
45
+ ## De CLI
46
+
47
+ ```bash
48
+ uv tool install archi-cli
49
+ archi --help
50
+ ```
51
+
52
+ Dat zet het commando `archi` op je `$PATH`. Het model wordt gevonden via
53
+ `--model <pad>` (vóór het subcommando), via een `archi.toml` in de map
54
+ (`[tool.archi] model = "..."`), of als het enige `.archimate`-bestand in de
55
+ werkmap.
56
+
57
+ ```bash
58
+ archi stats # tellingen per type
59
+ archi list --type Capability # elementen, gefilterd
60
+ archi show "<elementnaam of id>" # één element met relaties
61
+ archi tree # folderstructuur
62
+
63
+ archi add-element --type Capability --name "..." --documentation "..."
64
+ archi add-relation --type Aggregation --source "..." --target "..." --name "bevat"
65
+ archi set-property <ref> "<key>=<waarde>" # property zetten of bijwerken
66
+ archi remove-property <ref> "<key>" # property verwijderen
67
+ archi set-documentation <ref> "..." # Archi's documentatieveld
68
+ archi add-element ... --subfolder "Gebied X" # in een submap van de laag
69
+ archi move <ref> --subfolder "Gebied X" # naar een submap verplaatsen
70
+ archi rename <ref> "Nieuwe naam"
71
+ archi remove <ref> --cascade
72
+
73
+ archi add-view --name "..." --layout cluster --type Capability --relation Aggregation
74
+ archi slides --deck decks/<naam>.toml
75
+
76
+ archi validate # integriteitschecks
77
+ archi normalize # canonieke serialisatie via Archi
78
+ archi render # views naar Mermaid en HTML
79
+ ```
80
+
81
+ Voor `normalize` is de Archi-engine nodig; die haalt `archi` bij het eerste
82
+ gebruik zelf op (of expliciet met `archi setup`), dus je hoeft Archi niet apart
83
+ te installeren. `archi <commando> --help` toont de volledige set vlaggen;
84
+ shell-completion zet je aan met `archi --install-completion`.
85
+
86
+ Wat de tool bijzonder maakt:
87
+
88
+ - **Muteren met een vangnet.** Elke wijziging valideert het model in het
89
+ geheugen; bij een fout weigert de CLI op te slaan. Ids zijn onveranderlijk.
90
+ - **Canonieke serialisatie.** `normalize` laat de headless Archi-engine het
91
+ bestand herschrijven, zodat git-diffs klein blijven, of de wijziging nu van
92
+ de tool of van de Archi-GUI kwam.
93
+ - **Renderen.** Elke view wordt een Mermaid-diagram (rendert op GitHub) en een
94
+ HTML-pagina die de layout uit het model volgt, plus een presentatie per
95
+ deckdefinitie.
96
+ - **Doorklikken tussen views.** Met een links-bestand
97
+ (`[tool.archi] links = "…"` in `archi.toml`) worden elementen in de
98
+ HTML-views en slides klikbaar naar een andere view of een ander bestand,
99
+ bijvoorbeeld een SVG die een project met eigen scripts maakt. Zie
100
+ hieronder.
101
+
102
+ ### Doorklikken met een links-bestand
103
+
104
+ Welk element waarheen doorklikt is een redactionele keuze, geen modelfeit;
105
+ het staat daarom in een apart TOML-bestand (zie ADR 0010):
106
+
107
+ ```toml
108
+ # archi.toml
109
+ [tool.archi]
110
+ links = "tools/links.toml"
111
+
112
+ # tools/links.toml
113
+ ["overzicht-van-de-gebieden"] # bestandsnaam van de bronplaat, zonder extensie
114
+ "Gebied A" = "gebied-a-in-detail.html" # elementnaam = pad, relatief aan de bronplaat
115
+ "Gebied B" = "../eigen-svg/gebied-b.svg"
116
+ ```
117
+
118
+ Een sectie hoort bij de HTML-view met die bestandsnaam (de slug van de
119
+ viewnaam; bij views met botsende namen de naam mét id-suffix die `archi
120
+ render` kiest), of bij een bestand dat een project zelf genereert: dezelfde
121
+ sectievorm is dan bruikbaar voor eigen scripts, zodat één bestand over alle
122
+ doorkliks gaat. In slides worden relatieve paden automatisch gecorrigeerd
123
+ voor de submap. `archi validate` waarschuwt voor elementnamen die niet in de
124
+ view (of het model) staan, `archi render` voor doelen die niet bestaan en
125
+ voor secties die bij geen view of bestand horen.
126
+
127
+ ## De skills als Claude Code plugin
128
+
129
+ Deze repo is tegelijk de plugin (`archi-tools`) en de marketplace die hem
130
+ aanbiedt (`archi-marketplace`). Zo installeer je de skills bij je eigen model:
131
+
132
+ ```
133
+ /plugin marketplace add BureauArchitectuurDigitaleOverheid/ai-assisted-architecting
134
+ /plugin install archi-tools@archi-marketplace
135
+ ```
136
+
137
+ De plugin brengt de drie skills mee; het `archi`-commando zelf komt van
138
+ `uv tool install archi-cli`. Zie `adr/0007-claude-code-plugin.md`.
139
+
140
+ ## Ontwikkelen aan de tool
141
+
142
+ ```bash
143
+ just setup # venv, dependencies en de pre-commit hook
144
+ just test # de testsuite (draait op een fixture-model)
145
+ just demo # de CLI op het fixture-model
146
+ just build # distributies bouwen en de metadata controleren
147
+ ```
148
+
149
+ De testsuite draait op `tests/fixtures/klein-model.archimate`, niet op een echt
150
+ model. Wijzigingen lopen via een branch en een PR; CI draait de suite op Ubuntu
151
+ en Windows. Publiceren naar PyPI gebeurt op een versie-tag, zie `CLAUDE.md` en
152
+ `adr/0006-publiceren-op-pypi.md`.
153
+
154
+ ## Structuur
155
+
156
+ ```
157
+ tools/archi_tool/ De archi-CLI (Python, lxml, Typer; beheerd met uv)
158
+ tests/ pytest-suite met een klein fixture-model
159
+ .claude/skills/ De skills (archi-model, archi-view, archi-slides)
160
+ .claude-plugin/ Plugin- en marketplace-manifest
161
+ adr/ Architectuurbeslissingen over de tool
162
+ justfile Vaste taken
163
+ ```
164
+
165
+ ## Licentie
166
+
167
+ [EUPL-1.2](https://github.com/BureauArchitectuurDigitaleOverheid/ai-assisted-architecting/blob/main/LICENSE).
@@ -0,0 +1,145 @@
1
+ # AI-assisted architecting
2
+
3
+ [![checks](https://github.com/BureauArchitectuurDigitaleOverheid/ai-assisted-architecting/actions/workflows/ci.yml/badge.svg)](https://github.com/BureauArchitectuurDigitaleOverheid/ai-assisted-architecting/actions/workflows/ci.yml)
4
+ [![licentie: EUPL-1.2](https://img.shields.io/badge/licentie-EUPL--1.2-blue.svg)](https://github.com/BureauArchitectuurDigitaleOverheid/ai-assisted-architecting/blob/main/LICENSE)
5
+ [![PyPI](https://img.shields.io/pypi/v/archi-cli.svg)](https://pypi.org/project/archi-cli/)
6
+
7
+ Een manier van architecteren waarbij een ArchiMate-model in het native
8
+ Archi-formaat de bron van waarheid is, en je het met AI-assistentie bijhoudt in
9
+ plaats van met de hand door de GUI te klikken. Deze repo levert de twee dingen
10
+ die daarvoor nodig zijn:
11
+
12
+ - **`archi-cli`**: een deterministische command line tool die een
13
+ `.archimate`-model inspecteert, muteert, valideert en rendert, en weigert een
14
+ kapot model op te slaan. Op PyPI, werkt op elk model.
15
+ - **De skills** (`archi-model`, `archi-view`, `archi-slides`): de werkwijze
16
+ die een AI-sessie zoals Claude Code volgt om via de CLI aan het model te
17
+ werken. Te installeren als Claude Code plugin.
18
+
19
+ Het model zelf hoort niet hier: dat woont in een eigen repo en gebruikt
20
+ `archi-cli` als tool. Het ADO-model waarvoor dit ontstond is daar het
21
+ voorbeeld.
22
+
23
+ ## De CLI
24
+
25
+ ```bash
26
+ uv tool install archi-cli
27
+ archi --help
28
+ ```
29
+
30
+ Dat zet het commando `archi` op je `$PATH`. Het model wordt gevonden via
31
+ `--model <pad>` (vóór het subcommando), via een `archi.toml` in de map
32
+ (`[tool.archi] model = "..."`), of als het enige `.archimate`-bestand in de
33
+ werkmap.
34
+
35
+ ```bash
36
+ archi stats # tellingen per type
37
+ archi list --type Capability # elementen, gefilterd
38
+ archi show "<elementnaam of id>" # één element met relaties
39
+ archi tree # folderstructuur
40
+
41
+ archi add-element --type Capability --name "..." --documentation "..."
42
+ archi add-relation --type Aggregation --source "..." --target "..." --name "bevat"
43
+ archi set-property <ref> "<key>=<waarde>" # property zetten of bijwerken
44
+ archi remove-property <ref> "<key>" # property verwijderen
45
+ archi set-documentation <ref> "..." # Archi's documentatieveld
46
+ archi add-element ... --subfolder "Gebied X" # in een submap van de laag
47
+ archi move <ref> --subfolder "Gebied X" # naar een submap verplaatsen
48
+ archi rename <ref> "Nieuwe naam"
49
+ archi remove <ref> --cascade
50
+
51
+ archi add-view --name "..." --layout cluster --type Capability --relation Aggregation
52
+ archi slides --deck decks/<naam>.toml
53
+
54
+ archi validate # integriteitschecks
55
+ archi normalize # canonieke serialisatie via Archi
56
+ archi render # views naar Mermaid en HTML
57
+ ```
58
+
59
+ Voor `normalize` is de Archi-engine nodig; die haalt `archi` bij het eerste
60
+ gebruik zelf op (of expliciet met `archi setup`), dus je hoeft Archi niet apart
61
+ te installeren. `archi <commando> --help` toont de volledige set vlaggen;
62
+ shell-completion zet je aan met `archi --install-completion`.
63
+
64
+ Wat de tool bijzonder maakt:
65
+
66
+ - **Muteren met een vangnet.** Elke wijziging valideert het model in het
67
+ geheugen; bij een fout weigert de CLI op te slaan. Ids zijn onveranderlijk.
68
+ - **Canonieke serialisatie.** `normalize` laat de headless Archi-engine het
69
+ bestand herschrijven, zodat git-diffs klein blijven, of de wijziging nu van
70
+ de tool of van de Archi-GUI kwam.
71
+ - **Renderen.** Elke view wordt een Mermaid-diagram (rendert op GitHub) en een
72
+ HTML-pagina die de layout uit het model volgt, plus een presentatie per
73
+ deckdefinitie.
74
+ - **Doorklikken tussen views.** Met een links-bestand
75
+ (`[tool.archi] links = "…"` in `archi.toml`) worden elementen in de
76
+ HTML-views en slides klikbaar naar een andere view of een ander bestand,
77
+ bijvoorbeeld een SVG die een project met eigen scripts maakt. Zie
78
+ hieronder.
79
+
80
+ ### Doorklikken met een links-bestand
81
+
82
+ Welk element waarheen doorklikt is een redactionele keuze, geen modelfeit;
83
+ het staat daarom in een apart TOML-bestand (zie ADR 0010):
84
+
85
+ ```toml
86
+ # archi.toml
87
+ [tool.archi]
88
+ links = "tools/links.toml"
89
+
90
+ # tools/links.toml
91
+ ["overzicht-van-de-gebieden"] # bestandsnaam van de bronplaat, zonder extensie
92
+ "Gebied A" = "gebied-a-in-detail.html" # elementnaam = pad, relatief aan de bronplaat
93
+ "Gebied B" = "../eigen-svg/gebied-b.svg"
94
+ ```
95
+
96
+ Een sectie hoort bij de HTML-view met die bestandsnaam (de slug van de
97
+ viewnaam; bij views met botsende namen de naam mét id-suffix die `archi
98
+ render` kiest), of bij een bestand dat een project zelf genereert: dezelfde
99
+ sectievorm is dan bruikbaar voor eigen scripts, zodat één bestand over alle
100
+ doorkliks gaat. In slides worden relatieve paden automatisch gecorrigeerd
101
+ voor de submap. `archi validate` waarschuwt voor elementnamen die niet in de
102
+ view (of het model) staan, `archi render` voor doelen die niet bestaan en
103
+ voor secties die bij geen view of bestand horen.
104
+
105
+ ## De skills als Claude Code plugin
106
+
107
+ Deze repo is tegelijk de plugin (`archi-tools`) en de marketplace die hem
108
+ aanbiedt (`archi-marketplace`). Zo installeer je de skills bij je eigen model:
109
+
110
+ ```
111
+ /plugin marketplace add BureauArchitectuurDigitaleOverheid/ai-assisted-architecting
112
+ /plugin install archi-tools@archi-marketplace
113
+ ```
114
+
115
+ De plugin brengt de drie skills mee; het `archi`-commando zelf komt van
116
+ `uv tool install archi-cli`. Zie `adr/0007-claude-code-plugin.md`.
117
+
118
+ ## Ontwikkelen aan de tool
119
+
120
+ ```bash
121
+ just setup # venv, dependencies en de pre-commit hook
122
+ just test # de testsuite (draait op een fixture-model)
123
+ just demo # de CLI op het fixture-model
124
+ just build # distributies bouwen en de metadata controleren
125
+ ```
126
+
127
+ De testsuite draait op `tests/fixtures/klein-model.archimate`, niet op een echt
128
+ model. Wijzigingen lopen via een branch en een PR; CI draait de suite op Ubuntu
129
+ en Windows. Publiceren naar PyPI gebeurt op een versie-tag, zie `CLAUDE.md` en
130
+ `adr/0006-publiceren-op-pypi.md`.
131
+
132
+ ## Structuur
133
+
134
+ ```
135
+ tools/archi_tool/ De archi-CLI (Python, lxml, Typer; beheerd met uv)
136
+ tests/ pytest-suite met een klein fixture-model
137
+ .claude/skills/ De skills (archi-model, archi-view, archi-slides)
138
+ .claude-plugin/ Plugin- en marketplace-manifest
139
+ adr/ Architectuurbeslissingen over de tool
140
+ justfile Vaste taken
141
+ ```
142
+
143
+ ## Licentie
144
+
145
+ [EUPL-1.2](https://github.com/BureauArchitectuurDigitaleOverheid/ai-assisted-architecting/blob/main/LICENSE).
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "archi-cli"
3
- version = "0.1.0"
3
+ version = "0.2.0"
4
4
  description = "ar·cli·mate — de CLI in je ArchiMate: deterministisch native .archimate-modellen inspecteren, muteren, valideren en renderen"
5
5
  readme = "README.md"
6
6
  license = "EUPL-1.2"
@@ -18,7 +18,8 @@ classifiers = [
18
18
  "Programming Language :: Python :: 3.12",
19
19
  "Programming Language :: Python :: 3.13",
20
20
  ]
21
- dependencies = ["lxml>=5.0", "typer>=0.26"]
21
+ # lxml 6.1.3 stops resolving external parameter entities by default
22
+ dependencies = ["lxml>=6.1.3", "typer>=0.26"]
22
23
 
23
24
  [project.urls]
24
25
  Homepage = "https://github.com/BureauArchitectuurDigitaleOverheid/ai-assisted-architecting"
@@ -38,12 +39,28 @@ build-backend = "hatchling.build"
38
39
  packages = ["tools/archi_tool"]
39
40
 
40
41
  [tool.hatch.build.targets.sdist]
42
+ # only these paths; the leading ./ anchors README.md so views/README.md and
43
+ # other nested READMEs don't get pulled in by a bare glob
41
44
  include = [
42
- "tools/archi_tool",
43
- "README.md",
44
- "LICENSE",
45
- "NOTICE",
45
+ "/tools/archi_tool",
46
+ "/README.md",
47
+ "/LICENSE",
48
+ "/NOTICE",
49
+ "/pyproject.toml",
46
50
  ]
47
51
 
48
52
  [dependency-groups]
49
53
  dev = ["pytest>=8", "pre-commit>=4"]
54
+
55
+ [tool.ruff]
56
+ target-version = "py312"
57
+
58
+ [tool.ruff.lint]
59
+ # Bugs and dead code first. Import sorting and the formatter follow in a
60
+ # separate PR once the open feature branches have landed, so this one does
61
+ # not cause merge conflicts in every file.
62
+ select = ["E", "F", "W", "B"]
63
+ ignore = [
64
+ "E501", # line length is left to the formatter
65
+ "B008", # Typer declares options as call defaults by design
66
+ ]
@@ -16,10 +16,11 @@ from typing import Optional
16
16
  import typer
17
17
  from lxml import etree
18
18
 
19
- from .discovery import discover_conventions, discover_model
19
+ from .discovery import discover_conventions, discover_links, discover_model
20
+ from .links import check_link_files, load_links
20
21
  from .model import ArchiModel, ModelError, is_element, xsi_type
21
22
  from .normalize import normalize
22
- from .render import render_all, write_if_changed
23
+ from .render import render_all, view_stems, write_if_changed
23
24
  from .render_html import render_all_html
24
25
  from .render_slides import load_deck, render_all_slides, render_deck_html
25
26
  from .validate import validate
@@ -42,7 +43,15 @@ def report_validation(model, model_path) -> bool:
42
43
  # validating a model from another project uses that project's conventions
43
44
  allowed_keys, _ = discover_conventions(
44
45
  model_path, start=Path(model_path).resolve().parent)
45
- errors, warnings = validate(model, allowed_keys=allowed_keys)
46
+ # validation runs after every mutation: a broken links file must not
47
+ # block modelling (ADR 0010), so here it degrades to a warning; render
48
+ # and slides, which actually use the links, still fail on it
49
+ try:
50
+ links = load_links(discover_links(model_path))
51
+ except ModelError as exc:
52
+ print(f"WAARSCHUWING: links-bestand genegeerd: {exc}")
53
+ links = {}
54
+ errors, warnings = validate(model, allowed_keys=allowed_keys, links=links)
46
55
  for warning in warnings:
47
56
  print(f"WAARSCHUWING: {warning}")
48
57
  for error in errors:
@@ -144,7 +153,9 @@ def cmd_validate(model, args):
144
153
  def cmd_add_element(model, args):
145
154
  el = model.add_element(args.type, args.name, folder_type=args.folder,
146
155
  properties=parse_properties(args.property),
147
- documentation=args.documentation)
156
+ documentation=args.documentation,
157
+ subfolder=args.subfolder,
158
+ create_subfolder=args.create_subfolder)
148
159
  status = save_validated(model, args)
149
160
  if status == 0:
150
161
  print(f"Toegevoegd: {describe(model, el)}")
@@ -153,7 +164,8 @@ def cmd_add_element(model, args):
153
164
 
154
165
  def cmd_add_relation(model, args):
155
166
  rel = model.add_relation(args.type, args.source, args.target,
156
- name=args.name)
167
+ name=args.name, subfolder=args.subfolder,
168
+ create_subfolder=args.create_subfolder)
157
169
  status = save_validated(model, args)
158
170
  if status == 0:
159
171
  print(f"Toegevoegd: {rel.get('id')} [{xsi_type(rel)}] "
@@ -172,6 +184,23 @@ def cmd_set_property(model, args):
172
184
  return status
173
185
 
174
186
 
187
+ def cmd_move(model, args):
188
+ target = model.move(args.ref, args.subfolder,
189
+ create_subfolder=args.create_subfolder)
190
+ status = save_validated(model, args)
191
+ if status == 0:
192
+ print(f"Verplaatst: '{args.ref}' naar folder '{target.get('name')}'")
193
+ return status
194
+
195
+
196
+ def cmd_remove_property(model, args):
197
+ model.remove_property(args.ref, args.key)
198
+ status = save_validated(model, args)
199
+ if status == 0:
200
+ print(f"Property verwijderd van '{args.ref}': {args.key}")
201
+ return status
202
+
203
+
175
204
  def cmd_rename(model, args):
176
205
  model.rename(args.ref, args.name)
177
206
  status = save_validated(model, args)
@@ -244,10 +273,12 @@ def cmd_setup(model, args):
244
273
 
245
274
 
246
275
  def cmd_render(model, args):
276
+ links = load_links(discover_links(args.model))
247
277
  written, removed = render_all(model, args.out)
248
- html_written, html_removed = render_all_html(model, Path(args.out) / "html")
278
+ html_dir = Path(args.out) / "html"
279
+ html_written, html_removed = render_all_html(model, html_dir, links=links)
249
280
  deck_written, deck_removed = render_all_slides(
250
- model, Path(args.decks), Path(args.out) / "html" / "slides")
281
+ model, Path(args.decks), html_dir / "slides", links=links)
251
282
  for path in written + html_written + deck_written:
252
283
  print(f"Geschreven: {path}")
253
284
  for path in removed + html_removed:
@@ -257,22 +288,27 @@ def cmd_render(model, args):
257
288
  if not (written or removed or html_written or html_removed
258
289
  or deck_written or deck_removed):
259
290
  print("Views zijn al actueel.")
291
+ view_files = set(view_stems(model).values())
292
+ for warning in check_link_files(links, args.out, html_dir, view_files):
293
+ print(f"WAARSCHUWING: {warning}")
260
294
  return 0
261
295
 
262
296
 
263
297
  def cmd_slides(model, args):
298
+ links = load_links(discover_links(args.model))
264
299
  if args.deck:
265
300
  written = []
266
301
  for deck_path in args.deck:
267
302
  deck = load_deck(Path(deck_path), model)
268
303
  path = Path(args.out) / f"{deck['slug']}.html"
269
304
  path.parent.mkdir(parents=True, exist_ok=True)
270
- if write_if_changed(path, render_deck_html(model, deck)):
305
+ if write_if_changed(path, render_deck_html(model, deck,
306
+ links=links)):
271
307
  written.append(path)
272
308
  removed = []
273
309
  else:
274
310
  written, removed = render_all_slides(
275
- model, Path(args.decks), Path(args.out))
311
+ model, Path(args.decks), Path(args.out), links=links)
276
312
  for path in written:
277
313
  print(f"Geschreven: {path}")
278
314
  for path in removed:
@@ -330,10 +366,10 @@ def _run(command_fn, *, load_model=True, need_model=True, **fields) -> None:
330
366
  status = command_fn(model, args)
331
367
  except ModelError as exc:
332
368
  typer.echo(f"FOUT: {exc}", err=True)
333
- raise typer.Exit(1)
369
+ raise typer.Exit(1) from None
334
370
  except (etree.XMLSyntaxError, OSError) as exc:
335
371
  typer.echo(f"FOUT: kan {args.model} niet lezen: {exc}", err=True)
336
- raise typer.Exit(1)
372
+ raise typer.Exit(1) from None
337
373
  raise typer.Exit(status)
338
374
 
339
375
 
@@ -395,9 +431,15 @@ def add_element(
395
431
  property: Optional[list[str]] = typer.Option(
396
432
  None, "--property", help="key=value (herhaalbaar)"),
397
433
  documentation: Optional[str] = typer.Option(None),
434
+ subfolder: Optional[str] = typer.Option(
435
+ None, help="submap binnen de laagfolder, geneste mappen met '/'"),
436
+ create_subfolder: bool = typer.Option(
437
+ False, "--create-subfolder",
438
+ help="ontbrekende submap aanmaken (anders: fout)"),
398
439
  ):
399
440
  _run(cmd_add_element, type=type, name=name, folder=folder,
400
- property=property, documentation=documentation)
441
+ property=property, documentation=documentation,
442
+ subfolder=subfolder, create_subfolder=create_subfolder)
401
443
 
402
444
 
403
445
  @app.command("add-relation", help="relatie toevoegen")
@@ -407,9 +449,29 @@ def add_relation(
407
449
  source: str = typer.Option(..., help="id of unieke naam"),
408
450
  target: str = typer.Option(..., help="id of unieke naam"),
409
451
  name: Optional[str] = typer.Option(None, help="NL-label op de relatie"),
452
+ subfolder: Optional[str] = typer.Option(
453
+ None, help="submap binnen Relations, geneste mappen met '/'"),
454
+ create_subfolder: bool = typer.Option(
455
+ False, "--create-subfolder",
456
+ help="ontbrekende submap aanmaken (anders: fout)"),
410
457
  ):
411
458
  _run(cmd_add_relation, type=type, source=source,
412
- target=target, name=name)
459
+ target=target, name=name, subfolder=subfolder,
460
+ create_subfolder=create_subfolder)
461
+
462
+
463
+ @app.command(help="element, relatie of view naar een submap verplaatsen")
464
+ def move(
465
+ ref: str = typer.Argument(help="id of (unieke) naam"),
466
+ subfolder: str = typer.Option(
467
+ ..., help="submap binnen de huidige laagfolder, geneste mappen met "
468
+ "'/'; leeg ('') = terug naar de laagfolder zelf"),
469
+ create_subfolder: bool = typer.Option(
470
+ False, "--create-subfolder",
471
+ help="ontbrekende submap aanmaken (anders: fout)"),
472
+ ):
473
+ _run(cmd_move, ref=ref, subfolder=subfolder,
474
+ create_subfolder=create_subfolder)
413
475
 
414
476
 
415
477
  @app.command("set-property", help="property zetten of bijwerken")
@@ -420,6 +482,14 @@ def set_property(
420
482
  _run(cmd_set_property, ref=ref, pair=pair)
421
483
 
422
484
 
485
+ @app.command("remove-property", help="property verwijderen")
486
+ def remove_property(
487
+ ref: str = typer.Argument(help="id of (unieke) naam"),
488
+ key: str = typer.Argument(help="property-key"),
489
+ ):
490
+ _run(cmd_remove_property, ref=ref, key=key)
491
+
492
+
423
493
  @app.command(help="element of relatie hernoemen")
424
494
  def rename(
425
495
  ref: str = typer.Argument(help="id of (unieke) naam"),
@@ -1,9 +1,9 @@
1
- """Locate the model file and the conventions list without assuming this repo.
1
+ """Locate the model file and the conventions list without assuming a layout.
2
2
 
3
- The CLI used to hardcode ``models/ado.archimate`` and find the conventions at
4
- ``<model>/../../docs/conventies.md``. Both assumptions are specific to this
5
- repository. Discovery makes ``archi`` work on any model, in any layout, while
6
- keeping the ADO defaults working through an ``archi.toml`` in the repo root.
3
+ An earlier version hardcoded a fixed model path and found the conventions doc
4
+ at a fixed location. Discovery makes ``archi`` work on any model, in any
5
+ layout: a project points at its model and conventions through an ``archi.toml``
6
+ (or ``pyproject.toml``), or the tool falls back to sensible defaults.
7
7
 
8
8
  Resolution order for the model:
9
9
  1. an explicit ``--model`` path (handled by the caller)
@@ -27,19 +27,14 @@ from .model import ModelError
27
27
  CONFIG_NAMES = ("archi.toml", "pyproject.toml")
28
28
  CONVENTION_NEIGHBOURS = ("conventies.md", "conventions.md")
29
29
 
30
- # Keys that ship with the tool. A project can override this by pointing at its
31
- # own conventions doc; without one, these keep the property-key check useful.
32
- # Mirrors docs/conventies.md §3 so behaviour is identical for this repo when no
33
- # doc is found (it normally is, via archi.toml).
30
+ # Generic keys that ship with the tool as a fallback allowlist. A project
31
+ # overrides this by pointing at its own conventions doc (with a Property-keys
32
+ # section); without one, these keep the property-key check useful instead of
33
+ # silently disabling it. Descriptions belong in Archi's documentation field,
34
+ # not in a property, so "Omschrijving"/"Toelichting" are deliberately absent
35
+ # (ADR 0009).
34
36
  DEFAULT_PROPERTY_KEYS = {
35
- "Omschrijving",
36
- "Toelichting",
37
37
  "Bron",
38
- "Capability-niveau",
39
- "Niveau (herkomst)",
40
- "Driver-categorie",
41
- "constraint-type",
42
- "Artefact-type",
43
38
  }
44
39
 
45
40
 
@@ -101,6 +96,24 @@ def discover_model(explicit=None, start=None):
101
96
  "--model <pad> of leg het vast in archi.toml.")
102
97
 
103
98
 
99
+ def discover_links(model_path, start=None):
100
+ """Return the path of the links file, or None when none is configured.
101
+
102
+ Only ``[tool.archi] links`` in a config file counts (there is no
103
+ filesystem fallback: click-through is opt-in). A configured path that
104
+ does not exist is a config error.
105
+ """
106
+ start = Path(start or Path(model_path).resolve().parent)
107
+ table, config_path = _read_archi_config(start)
108
+ if "links" not in table:
109
+ return None
110
+ path = (config_path.parent / table["links"]).resolve()
111
+ if not path.exists():
112
+ raise ModelError(
113
+ f"Linkspad uit {config_path.name} bestaat niet: {path}")
114
+ return path
115
+
116
+
104
117
  def discover_conventions(model_path, start=None):
105
118
  """Return (allowed_property_keys, source_label).
106
119
 
@@ -153,7 +153,7 @@ def _safe_extract_tar(tar, target):
153
153
  try:
154
154
  tar.extractall(target, filter="data")
155
155
  except tarfile.FilterError as exc:
156
- raise ModelError(f"Onveilig pad in archief: {exc}")
156
+ raise ModelError(f"Onveilig pad in archief: {exc}") from exc
157
157
 
158
158
 
159
159
  def _safe_extract_zip(zf, target):
@@ -259,7 +259,7 @@ def download_engine(*, quiet=False) -> Path:
259
259
  os.replace(staging, version_dir)
260
260
  except OSError as exc:
261
261
  raise ModelError(
262
- f"Kon de Archi-engine niet ophalen van {url}: {exc}")
262
+ f"Kon de Archi-engine niet ophalen van {url}: {exc}") from exc
263
263
  finally:
264
264
  shutil.rmtree(staging, ignore_errors=True)
265
265