katalyst-taxonomy-cli 2.0.1a3__tar.gz → 2.0.1a5__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.
Files changed (104) hide show
  1. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/PKG-INFO +1 -1
  2. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/pyproject.toml +1 -1
  3. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/src/katalyst_taxonomy_cli/app.py +21 -5
  4. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/src/katalyst_taxonomy_cli/commands/config_home.py +49 -10
  5. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/src/katalyst_taxonomy_cli/commands/config_profile.py +2 -1
  6. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/src/katalyst_taxonomy_cli/commands/config_schemas.py +26 -2
  7. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/src/katalyst_taxonomy_cli/commands/config_sources.py +9 -3
  8. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/src/katalyst_taxonomy_cli/commands/core.py +121 -0
  9. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/src/katalyst_taxonomy_cli/commands/create.py +34 -3
  10. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/src/katalyst_taxonomy_cli/commands/lifecycle.py +22 -6
  11. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/src/katalyst_taxonomy_cli/commands/plugin.py +17 -2
  12. katalyst_taxonomy_cli-2.0.1a5/src/katalyst_taxonomy_cli/commands/rescaffold.py +149 -0
  13. katalyst_taxonomy_cli-2.0.1a5/src/katalyst_taxonomy_cli/commands/self_update.py +623 -0
  14. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/src/katalyst_taxonomy_cli/commands/update.py +55 -4
  15. katalyst_taxonomy_cli-2.0.1a5/src/katalyst_taxonomy_cli/fields.py +117 -0
  16. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/src/katalyst_taxonomy_cli/install.py +64 -73
  17. katalyst_taxonomy_cli-2.0.1a5/src/katalyst_taxonomy_cli/installer.py +407 -0
  18. katalyst_taxonomy_cli-2.0.1a5/src/katalyst_taxonomy_cli/releases.py +273 -0
  19. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/tests/test_cli_version.py +29 -0
  20. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/tests/test_config_home.py +59 -0
  21. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/tests/test_config_schemas_command.py +34 -0
  22. katalyst_taxonomy_cli-2.0.1a5/tests/test_core_schema.py +226 -0
  23. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/tests/test_create.py +41 -1
  24. katalyst_taxonomy_cli-2.0.1a5/tests/test_create_owners.py +58 -0
  25. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/tests/test_deps.py +9 -9
  26. katalyst_taxonomy_cli-2.0.1a5/tests/test_import_change_set.py +171 -0
  27. katalyst_taxonomy_cli-2.0.1a5/tests/test_import_inside_repo.py +89 -0
  28. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/tests/test_install.py +2 -2
  29. katalyst_taxonomy_cli-2.0.1a5/tests/test_installer.py +190 -0
  30. katalyst_taxonomy_cli-2.0.1a5/tests/test_labels_annotations.py +289 -0
  31. katalyst_taxonomy_cli-2.0.1a5/tests/test_lifecycle_kind_selector.py +113 -0
  32. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/tests/test_plugin_command.py +32 -1
  33. katalyst_taxonomy_cli-2.0.1a5/tests/test_releases.py +128 -0
  34. katalyst_taxonomy_cli-2.0.1a5/tests/test_self_channels.py +484 -0
  35. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/tests/test_self_update.py +4 -180
  36. katalyst_taxonomy_cli-2.0.1a5/tests/test_template_merge.py +201 -0
  37. katalyst_taxonomy_cli-2.0.1a5/tests/test_update_in_place.py +93 -0
  38. katalyst_taxonomy_cli-2.0.1a3/src/katalyst_taxonomy_cli/commands/self_update.py +0 -530
  39. katalyst_taxonomy_cli-2.0.1a3/src/katalyst_taxonomy_cli/fields.py +0 -70
  40. katalyst_taxonomy_cli-2.0.1a3/tests/test_core_schema.py +0 -95
  41. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/.gitignore +0 -0
  42. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/src/katalyst_taxonomy_cli/__init__.py +0 -0
  43. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/src/katalyst_taxonomy_cli/__main__.py +0 -0
  44. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/src/katalyst_taxonomy_cli/_wiring.py +0 -0
  45. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/src/katalyst_taxonomy_cli/commands/__init__.py +0 -0
  46. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/src/katalyst_taxonomy_cli/commands/api.py +0 -0
  47. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/src/katalyst_taxonomy_cli/commands/backend.py +0 -0
  48. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/src/katalyst_taxonomy_cli/commands/bundle.py +0 -0
  49. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/src/katalyst_taxonomy_cli/commands/clone.py +0 -0
  50. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/src/katalyst_taxonomy_cli/commands/config.py +0 -0
  51. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/src/katalyst_taxonomy_cli/commands/config_filters.py +0 -0
  52. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/src/katalyst_taxonomy_cli/commands/config_plugins.py +0 -0
  53. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/src/katalyst_taxonomy_cli/commands/deps.py +0 -0
  54. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/src/katalyst_taxonomy_cli/commands/diff.py +0 -0
  55. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/src/katalyst_taxonomy_cli/commands/drafts.py +0 -0
  56. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/src/katalyst_taxonomy_cli/commands/export.py +0 -0
  57. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/src/katalyst_taxonomy_cli/commands/init.py +0 -0
  58. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/src/katalyst_taxonomy_cli/commands/kinds.py +0 -0
  59. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/src/katalyst_taxonomy_cli/commands/lint.py +0 -0
  60. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/src/katalyst_taxonomy_cli/commands/migrate.py +0 -0
  61. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/src/katalyst_taxonomy_cli/commands/promote.py +0 -0
  62. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/src/katalyst_taxonomy_cli/commands/query.py +0 -0
  63. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/src/katalyst_taxonomy_cli/commands/relate.py +0 -0
  64. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/src/katalyst_taxonomy_cli/commands/relocate.py +0 -0
  65. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/src/katalyst_taxonomy_cli/commands/remove.py +0 -0
  66. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/src/katalyst_taxonomy_cli/commands/resolve.py +0 -0
  67. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/src/katalyst_taxonomy_cli/commands/rollback.py +0 -0
  68. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/src/katalyst_taxonomy_cli/commands/show.py +0 -0
  69. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/src/katalyst_taxonomy_cli/commands/status.py +0 -0
  70. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/src/katalyst_taxonomy_cli/commands/sync.py +0 -0
  71. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/src/katalyst_taxonomy_cli/commands/versions.py +0 -0
  72. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/src/katalyst_taxonomy_cli/context.py +0 -0
  73. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/src/katalyst_taxonomy_cli/discovery.py +0 -0
  74. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/src/katalyst_taxonomy_cli/options.py +0 -0
  75. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/src/katalyst_taxonomy_cli/output.py +0 -0
  76. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/src/katalyst_taxonomy_cli/py.typed +0 -0
  77. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/tests/conftest.py +0 -0
  78. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/tests/test_add.py +0 -0
  79. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/tests/test_add_bundle.py +0 -0
  80. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/tests/test_api_command.py +0 -0
  81. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/tests/test_backend_option.py +0 -0
  82. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/tests/test_clone.py +0 -0
  83. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/tests/test_command_signatures.py +0 -0
  84. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/tests/test_config_command.py +0 -0
  85. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/tests/test_config_explain.py +0 -0
  86. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/tests/test_config_plugins_command.py +0 -0
  87. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/tests/test_config_profiles.py +0 -0
  88. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/tests/test_config_sources.py +0 -0
  89. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/tests/test_create_typed_and_variants.py +0 -0
  90. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/tests/test_git_backend_cli.py +0 -0
  91. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/tests/test_github_backend_cli.py +0 -0
  92. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/tests/test_kinds.py +0 -0
  93. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/tests/test_kinds_plugin_settings.py +0 -0
  94. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/tests/test_lint_output.py +0 -0
  95. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/tests/test_lint_profile_graph.py +0 -0
  96. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/tests/test_lint_strict.py +0 -0
  97. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/tests/test_migrate.py +0 -0
  98. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/tests/test_optional_extras.py +0 -0
  99. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/tests/test_output.py +0 -0
  100. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/tests/test_plugin_gating.py +0 -0
  101. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/tests/test_relate_update.py +0 -0
  102. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/tests/test_relocate.py +0 -0
  103. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/tests/test_remove.py +0 -0
  104. {katalyst_taxonomy_cli-2.0.1a3 → katalyst_taxonomy_cli-2.0.1a5}/tests/test_repository_targets.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: katalyst-taxonomy-cli
3
- Version: 2.0.1a3
3
+ Version: 2.0.1a5
4
4
  Summary: CLI for katalyst-taxonomy — the kata command
5
5
  Author: eSimplicity
6
6
  License: Proprietary
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "katalyst-taxonomy-cli"
3
- version = "2.0.1a3"
3
+ version = "2.0.1a5"
4
4
  description = "CLI for katalyst-taxonomy — the kata command"
5
5
  license = { text = "Proprietary" }
6
6
  requires-python = ">=3.12"
@@ -206,6 +206,13 @@ def _installed_katalyst_packages() -> list[tuple[str, str]]:
206
206
  return sorted(found.items())
207
207
 
208
208
 
209
+ def _schema_versions() -> list[tuple[str, str]]:
210
+ """Every schema installed (katalyst-core, and each bundle), with its version."""
211
+ from katalyst_engine.evolution import installed_releases
212
+
213
+ return [(release.name, str(release.version)) for release in installed_releases()]
214
+
215
+
209
216
  def _print_version(ctx: click.Context, _param: click.Parameter, value: bool) -> None:
210
217
  if not value or ctx.resilient_parsing:
211
218
  return
@@ -217,6 +224,12 @@ def _print_version(ctx: click.Context, _param: click.Parameter, value: bool) ->
217
224
  click.echo(f"kata {__version__}")
218
225
  for name, version in _installed_katalyst_packages():
219
226
  click.echo(f" {name} {version}")
227
+ click.echo("schemas:")
228
+ try:
229
+ for name, version in _schema_versions():
230
+ click.echo(f" {name} {version}")
231
+ except Exception as exc: # a bundle that does not load must not hide the versions
232
+ click.echo(f" unavailable: {exc}")
220
233
  for line in install_lines():
221
234
  click.echo(line)
222
235
  try:
@@ -274,8 +287,8 @@ def _select_home(ctx: click.Context, _param: click.Parameter, value: str | None)
274
287
  # Not eager, so it runs after --home (which is) and reports the home chosen.
275
288
  callback=_print_version,
276
289
  help=(
277
- "Show the kata version, every installed Katalyst package, the install (and what it "
278
- "was built from) and the home, then exit."
290
+ "Show the kata version, every installed Katalyst package, the schema bundles with "
291
+ "their versions, the install (and what it was built from) and the home, then exit."
279
292
  ),
280
293
  )
281
294
  @click.option(
@@ -298,14 +311,15 @@ def cli(ctx: click.Context, profile: str | None) -> None:
298
311
  ctx.meta[ROOT_PROFILE_META_KEY] = profile
299
312
  _configure_logging()
300
313
  # A KATALYST_HOME that names no home fails every command here, before any of them
301
- # reads or writes anything (`--home` was checked as it was parsed).
302
- from katalyst_taxonomy.adapters.filesystem.homes import resolve_existing_home
314
+ # reads or writes anything (`--home` was checked as it was parsed) — unless
315
+ # KATALYST_HOME_SOURCE says what to create it from.
316
+ from katalyst_taxonomy.adapters.config_sources.homes import resolve_home_on_demand
303
317
  from katalyst_taxonomy.domain.homes import HomeError
304
318
 
305
319
  from .install import refuse_default_home_for_development
306
320
 
307
321
  try:
308
- home = resolve_existing_home()
322
+ home = resolve_home_on_demand(announce=lambda m: click.echo(m, err=True))
309
323
  except HomeError as exc:
310
324
  raise click.ClickException(str(exc)) from exc
311
325
  # `kata config` decides for itself: `kata config home …` manages homes, not uses one.
@@ -525,6 +539,7 @@ def _register_core_commands() -> None:
525
539
  relate,
526
540
  relocate,
527
541
  remove,
542
+ rescaffold,
528
543
  resolve,
529
544
  rollback,
530
545
  self_update,
@@ -582,6 +597,7 @@ def _register_core_commands() -> None:
582
597
  tax.add_command(export.export)
583
598
  tax.add_command(backend.backend)
584
599
  tax.add_command(remove.remove)
600
+ tax.add_command(rescaffold.rescaffold)
585
601
 
586
602
 
587
603
  def resolve_plugin_commands() -> None:
@@ -4,7 +4,7 @@ A home is the config, state and cache a process uses (:mod:`katalyst_taxonomy.do
4
4
  ``kata --home NAME`` or ``KATALYST_HOME=NAME`` selects one; a named home must exist first::
5
5
 
6
6
  kata config home list
7
- kata config home create NAME [--from HOME]
7
+ kata config home create NAME [--from HOME | --source URI [--source-profile P]]
8
8
  kata config home rm NAME [--yes]
9
9
 
10
10
  Path homes (an absolute ``KATALYST_HOME``, for tests and CI) are never listed: they are
@@ -14,7 +14,6 @@ wherever their caller put them.
14
14
  from __future__ import annotations
15
15
 
16
16
  import json
17
- import os
18
17
  import shutil
19
18
  from typing import Any
20
19
 
@@ -32,6 +31,7 @@ from katalyst_taxonomy.domain.homes import (
32
31
  resolve_home,
33
32
  )
34
33
  from katalyst_taxonomy.domain.paths import CONFIG_FILE_NAME
34
+ from katalyst_taxonomy.processes import is_process_running
35
35
 
36
36
  from ..output import ConsoleOutput
37
37
 
@@ -52,9 +52,7 @@ def _running_api(home: Home) -> dict[str, Any] | None:
52
52
  pid, host, port = int(record["pid"]), str(record["host"]), int(record["port"])
53
53
  except (OSError, ValueError, KeyError, TypeError):
54
54
  return None
55
- try:
56
- os.kill(pid, 0)
57
- except OSError:
55
+ if not is_process_running(pid):
58
56
  return None
59
57
  return {"pid": pid, "url": f"http://{host}:{port}"}
60
58
 
@@ -121,15 +119,56 @@ def home_list(fmt: str) -> None:
121
119
  @click.argument("name")
122
120
  @click.option(
123
121
  "--from",
124
- "source",
122
+ "copy_from",
125
123
  default=None,
126
124
  metavar="HOME",
127
125
  help="Copy config.yaml (only) from this home: 'default', a name, or an absolute path.",
128
126
  )
129
- def home_create(name: str, source: str | None) -> None:
130
- """Create the named home NAME, empty or with another home's config.yaml."""
127
+ @click.option(
128
+ "--source",
129
+ "profile_source",
130
+ default=None,
131
+ metavar="URI",
132
+ help=(
133
+ "Give the home one read-only profile sourced from URI, such as home:default "
134
+ "(another home: the same repositories), then sync it."
135
+ ),
136
+ )
137
+ @click.option(
138
+ "--source-profile",
139
+ default=None,
140
+ metavar="PROFILE",
141
+ help="The profile in the source's configuration (default: @active, whichever is active there).",
142
+ )
143
+ def home_create(
144
+ name: str, copy_from: str | None, profile_source: str | None, source_profile: str | None
145
+ ) -> None:
146
+ """Create the named home NAME: empty, with another home's config.yaml, or sourced.
147
+
148
+ With --source, NAME's config.yaml holds one profile sourced from URI, and nothing
149
+ else. By default it follows the source's active profile (@active), at every sync. A
150
+ home: source's profile reads the other home's repositories; its state (the API's
151
+ record and token) and cache are never read.
152
+ """
153
+ if copy_from is not None and profile_source is not None:
154
+ raise click.ClickException("give --from or --source, not both")
155
+ if source_profile is not None and profile_source is None:
156
+ raise click.ClickException("--source-profile needs --source")
157
+ out = ConsoleOutput()
158
+ if profile_source is not None:
159
+ from katalyst_taxonomy.adapters.config_sources.homes import create_sourced_home
160
+ from katalyst_taxonomy.config.profile_sources import ACTIVE_PROFILE
161
+
162
+ wanted = source_profile or ACTIVE_PROFILE
163
+ try:
164
+ created, pin = create_sourced_home(name, profile_source, source_profile=wanted)
165
+ except HomeError as exc:
166
+ raise click.ClickException(str(exc)) from exc
167
+ out.success(f"Created home {created.label}, with profile {wanted!r} of {profile_source}")
168
+ out.info(f"Synced it: pinned {pin}")
169
+ return
131
170
  try:
132
- origin = home_for(source) if source is not None else None
171
+ origin = home_for(copy_from) if copy_from is not None else None
133
172
  created = create_home(name)
134
173
  except HomeError as exc:
135
174
  raise click.ClickException(str(exc)) from exc
@@ -139,7 +178,7 @@ def home_create(name: str, source: str | None) -> None:
139
178
  if config_file.is_file():
140
179
  shutil.copy2(config_file, created.config_dir / CONFIG_FILE_NAME)
141
180
  copied = f", with config.yaml from {origin.name}"
142
- ConsoleOutput().success(f"Created home {created.label}{copied}")
181
+ out.success(f"Created home {created.label}{copied}")
143
182
 
144
183
 
145
184
  @home.command("rm")
@@ -318,7 +318,8 @@ def profile_show(name: str, fmt: str) -> None:
318
318
  metavar="URI",
319
319
  help=(
320
320
  "Read the profile from elsewhere: git:<url>[@<ref>][#path=<file>], "
321
- "github:<owner>/<repo>[@<ref>][#path=<file>], or a kata API's https:// URL."
321
+ "github:<owner>/<repo>[@<ref>][#path=<file>], a kata API's https:// URL, or "
322
+ "home:<home> (another home on this machine)."
322
323
  ),
323
324
  )
324
325
  @click.option(
@@ -60,7 +60,11 @@ def build_schemas_payload(
60
60
  from katalyst_taxonomy.config.schemas import selection_from_config
61
61
  from katalyst_taxonomy.engine import bridge
62
62
  from katalyst_taxonomy.services.schema_gate import check_declared_bundles
63
- from katalyst_taxonomy.services.schema_report import schemas_payload
63
+ from katalyst_taxonomy.services.schema_report import (
64
+ core_schema,
65
+ schema_versions,
66
+ schemas_payload,
67
+ )
64
68
  from katalyst_taxonomy.sources.template_resolver import TemplateResolver
65
69
 
66
70
  effective = config_resolver().resolve(profile=profile)
@@ -90,6 +94,8 @@ def build_schemas_payload(
90
94
  if candidate.distribution
91
95
  },
92
96
  declared=declared,
97
+ schema_versions=schema_versions(candidates),
98
+ core_schema=core_schema(),
93
99
  scaffold_providers=resolver.scaffold_template_providers(),
94
100
  repo_root=None,
95
101
  filesystem=RealFilesystem(),
@@ -155,19 +161,24 @@ def _render_text(out: OutputPort, payload: dict[str, Any]) -> None:
155
161
  ]
156
162
  )
157
163
 
164
+ core = payload.get("core")
165
+ if core:
166
+ out.key_value([("katalyst-core schema", _schema_text(core))])
167
+
158
168
  out.heading("Bundles")
159
169
  bundles: list[dict[str, Any]] = payload["bundles"]
160
170
  if not bundles:
161
171
  out.info("No schema bundles discovered.")
162
172
  else:
163
173
  out.table(
164
- ["", "bundle", "distribution", "version", "kinds", "note"],
174
+ ["", "bundle", "distribution", "version", "schema", "kinds", "note"],
165
175
  [
166
176
  [
167
177
  "*" if entry["enabled"] else "x",
168
178
  str(entry["name"]),
169
179
  str(entry.get("distribution") or "-"),
170
180
  str(entry.get("resolvedVersion") or entry["version"] or "-"),
181
+ _schema_text(entry.get("schema")),
171
182
  str(len(entry["kinds"])),
172
183
  str(
173
184
  entry["reason"] or entry.get("sourceWarning") or entry["entryPoint"] or "-"
@@ -239,6 +250,19 @@ def _render_text(out: OutputPort, payload: dict[str, Any]) -> None:
239
250
  )
240
251
 
241
252
 
253
+ def _schema_text(schema: dict[str, Any] | None) -> str:
254
+ """``9.0.0 (unreleased; latest archived 8.0.0)``: a schema's version and archive state."""
255
+ if not schema:
256
+ return "-"
257
+ from katalyst_engine.core.version import Version
258
+ from katalyst_engine.evolution import ArchiveState
259
+
260
+ archive = schema["archive"]
261
+ latest = archive["latest"]
262
+ state = ArchiveState(archive["state"], Version.parse(latest) if latest else None)
263
+ return f"{schema['version']} ({state})"
264
+
265
+
242
266
  def attach(group: click.Group) -> None:
243
267
  """Add ``schemas`` to the ``kata config`` group.
244
268
 
@@ -109,9 +109,7 @@ def config_status(offline: bool, fmt: str) -> None:
109
109
  ["profile", "source", "pin", "fetched", "upstream"],
110
110
  [
111
111
  [
112
- row.profile
113
- if row.source_profile == row.profile
114
- else f"{row.profile} ({row.source_profile})",
112
+ _profile_label(row),
115
113
  row.source,
116
114
  _short(row.pin) if row.pin else "never fetched",
117
115
  row.fetched_at or "-",
@@ -125,6 +123,14 @@ def config_status(offline: bool, fmt: str) -> None:
125
123
  out.warning(f"{row.profile}: {row.error}")
126
124
 
127
125
 
126
+ def _profile_label(row: Any) -> str:
127
+ if row.source_profile == row.profile:
128
+ return str(row.profile)
129
+ if row.pinned_profile and row.pinned_profile != row.source_profile:
130
+ return f"{row.profile} ({row.source_profile}: {row.pinned_profile})"
131
+ return f"{row.profile} ({row.source_profile})"
132
+
133
+
128
134
  def _upstream(row: Any, offline: bool) -> str:
129
135
  if offline:
130
136
  return "not checked"
@@ -23,6 +23,12 @@
23
23
  the diff
24
24
  kata core schema archive --bundle B
25
25
  archive the current modules as the released version
26
+ kata core schema docs [PATH...] [--bundle B] [--check] [--kind K]
27
+ write the generated field tables of the pages
28
+ under PATH; --check fails when any is stale
29
+ kata core schema changelog [--bundle B] [--from VERSION]
30
+ the "Schema changes" section of a release: what was
31
+ added, what broke, and the migration step for each
26
32
 
27
33
  Records and graphs are read against every installed module (``katalyst_core.modules``).
28
34
  """
@@ -466,3 +472,118 @@ def schema_archive(bundle: str) -> None:
466
472
  except (FileExistsError, SchemaVersionMismatchError) as exc:
467
473
  raise click.ClickException(str(exc)) from exc
468
474
  click.echo(f"Archived {release.name} {release.version} in {target}")
475
+
476
+
477
+ # Named from the function (``docs``), so no command-name literal reads as a bundle name.
478
+ @schema.command()
479
+ @click.argument("paths", nargs=-1, type=click.Path(exists=True, path_type=Path))
480
+ @click.option(
481
+ "--bundle",
482
+ "bundle",
483
+ default=None,
484
+ help="Read kinds from this schema only (needed when two define the same kind).",
485
+ )
486
+ @click.option(
487
+ "--check",
488
+ is_flag=True,
489
+ help="Write nothing; fail when a generated region is stale (for CI).",
490
+ )
491
+ @click.option(
492
+ "--kind",
493
+ "kind",
494
+ default=None,
495
+ help="Print the markdown for one kind (to start a page), and write nothing.",
496
+ )
497
+ def docs(paths: tuple[Path, ...], bundle: str | None, check: bool, kind: str | None) -> None:
498
+ """Write the generated field tables of the markdown pages under PATH.
499
+
500
+ A page marks where a table goes with a line of its own, ``<!-- generated:kind:NAME -->``
501
+ (a kind's parent and fields: type, required, allowed values, references and their
502
+ relationship types, descriptions) or ``<!-- generated:wing:NAME -->`` (a wing's kinds:
503
+ where each is stored and its parent), and closes it with ``<!-- /generated -->``. The
504
+ text between is rewritten from the schemas' LinkML modules; everything else on the page
505
+ is yours.
506
+ """
507
+ from katalyst_engine.schema_docs import (
508
+ DocsError,
509
+ RegionError,
510
+ catalog_from_releases,
511
+ page_files,
512
+ refresh_page,
513
+ render_kind,
514
+ )
515
+
516
+ if kind is None and not paths:
517
+ raise click.UsageError("name the pages to write: one or more files or directories")
518
+ catalog = catalog_from_releases(_releases(bundle))
519
+ try:
520
+ if kind is not None:
521
+ click.echo(render_kind(catalog.kind(kind, bundle=bundle)))
522
+ return
523
+ stale_total = 0
524
+ for page in page_files(paths):
525
+ text = page.read_text(encoding="utf-8")
526
+ try:
527
+ new_text, stale = refresh_page(text, catalog, bundle=bundle)
528
+ except (RegionError, DocsError) as exc:
529
+ raise click.ClickException(f"{page}: {exc}") from exc
530
+ stale_total += len(stale)
531
+ for region in stale:
532
+ click.echo(f"{page}:{region.line}: generated:{region.label} is stale")
533
+ if stale and not check:
534
+ page.write_text(new_text, encoding="utf-8")
535
+ except DocsError as exc:
536
+ raise click.ClickException(str(exc)) from exc
537
+ if check:
538
+ if stale_total:
539
+ raise click.ClickException(
540
+ f"{stale_total} generated region(s) are stale; "
541
+ f"run kata core schema docs {' '.join(str(p) for p in paths)}"
542
+ )
543
+ click.echo("generated regions are current")
544
+ else:
545
+ click.echo(f"Updated {stale_total} generated region(s)")
546
+
547
+
548
+ @schema.command("changelog")
549
+ @_BUNDLE
550
+ @click.option(
551
+ "--from",
552
+ "since",
553
+ default=None,
554
+ help="The archived version to compare with (default: the latest).",
555
+ )
556
+ @click.option(
557
+ "--heading-level",
558
+ "level",
559
+ type=click.IntRange(1, 5),
560
+ default=2,
561
+ show_default=True,
562
+ help="The level of the section's heading (2: ##).",
563
+ )
564
+ def schema_changelog(bundle: str | None, since: str | None, level: int) -> None:
565
+ """The "Schema changes" section of a release, from the classified diff and the migration.
566
+
567
+ For each schema whose modules changed since its last archived version: what was added,
568
+ what broke, and for each breaking change the `kata tax migrate` step that handles it
569
+ (or the reason it is accepted). Markdown on stdout, for the release notes.
570
+ """
571
+ from katalyst_engine.core.version import Version
572
+ from katalyst_engine.evolution import build_changelog, render_changelog
573
+
574
+ logs: list[Any] = []
575
+ for release in _releases(bundle):
576
+ try:
577
+ old = Version.parse(since) if since else None
578
+ log = build_changelog(release, old)
579
+ except (ValueError, FileNotFoundError) as exc:
580
+ raise click.ClickException(str(exc)) from exc
581
+ if log is None:
582
+ click.echo(f"{release.name}: no released version is archived", err=True)
583
+ else:
584
+ logs.append(log)
585
+ text = render_changelog(logs, level=level)
586
+ if not text:
587
+ click.echo("No schema changes since the last released version.", err=True)
588
+ return
589
+ click.echo(text, nl=False)
@@ -41,7 +41,24 @@ from katalyst_taxonomy_cli.context import CliContext
41
41
  "from_file",
42
42
  default=None,
43
43
  metavar="PATH",
44
- help="Spec fields from a YAML or JSON file (- for stdin); --set applies over them.",
44
+ help=(
45
+ "Spec fields from a YAML or JSON file (- for stdin), or a whole document's spec, "
46
+ "labels and annotations; --set, --label and --annotation apply over them."
47
+ ),
48
+ )
49
+ @click.option(
50
+ "--label",
51
+ "label_pairs",
52
+ multiple=True,
53
+ metavar="KEY=VALUE",
54
+ help="Set a label (metadata.labels), which filters and permission grants match. Repeat.",
55
+ )
56
+ @click.option(
57
+ "--annotation",
58
+ "annotation_pairs",
59
+ multiple=True,
60
+ metavar="KEY=VALUE",
61
+ help="Set an annotation (metadata.annotations), a record nothing matches on. Repeat.",
45
62
  )
46
63
  @click.option("--target-dir", default=None, help="Override output directory.")
47
64
  @click.option(
@@ -75,6 +92,8 @@ def create(
75
92
  scaffold_variant: str | None,
76
93
  set_fields: tuple[str, ...],
77
94
  from_file: str | None,
95
+ label_pairs: tuple[str, ...],
96
+ annotation_pairs: tuple[str, ...],
78
97
  target_dir: str | None,
79
98
  backend_target: str | None,
80
99
  dry_run: bool,
@@ -96,6 +115,11 @@ def create(
96
115
  a value of its type (boolean, integer, list, map) and allowed values, a reference
97
116
  naming a node of the referenced kind.
98
117
 
118
+ --label KEY=VALUE and --annotation KEY=VALUE go in the node's metadata, on any kind.
119
+ A key is a name of letters, digits, '-', '_' and '.' (at most 63), optionally after a
120
+ DNS prefix and '/' (example.org/team); a label's value is such a name (or empty), an
121
+ annotation's any text.
122
+
99
123
  Examples:
100
124
 
101
125
  kata tax create layer api --under shop.backend --set audited=false --set tier=gold
@@ -105,15 +129,20 @@ def create(
105
129
  kata tax create capability checkout --under shop --set 'hooks=[{"on":"push","branches":["main"]}]'
106
130
 
107
131
  kata tax create stack web --under shop --set limits.log_level=debug
132
+
133
+ kata tax create layer api --under shop.backend --label team=payments --annotation source=docs/api.md
108
134
  """
109
135
  from katalyst_taxonomy.adapters.orchestration import run_create
110
136
  from katalyst_taxonomy.config.routing import split_backend_prefix
111
137
 
112
- from katalyst_taxonomy_cli.fields import parse_set, read_spec_file
138
+ from katalyst_taxonomy_cli.fields import parse_pairs, parse_set, read_node_file
113
139
 
114
140
  from .._wiring import resolve_backend
115
141
 
116
142
  resolved_parent = parent or parent_alt
143
+ node_file = read_node_file(from_file)
144
+ labels = {**node_file.labels, **parse_pairs(label_pairs, option="--label")}
145
+ annotations = {**node_file.annotations, **parse_pairs(annotation_pairs, option="--annotation")}
117
146
 
118
147
  # The backend is `--backend`, or the `backend:` prefix of NAME. Without either, the
119
148
  # repository is the one the current directory is inside — even with one repository,
@@ -134,7 +163,9 @@ def create(
134
163
  description=description,
135
164
  owners=owners or (),
136
165
  fields=parse_set(set_fields),
137
- spec=read_spec_file(from_file),
166
+ spec=node_file.spec,
167
+ labels=labels,
168
+ annotations=annotations,
138
169
  parent=resolved_parent,
139
170
  scaffold_variant=scaffold_variant,
140
171
  target_dir=target_dir,
@@ -101,27 +101,43 @@ def restore(ctx: CliContext, address: str, backend_target: str | None) -> None:
101
101
  def import_(ctx: CliContext, file: Path, backend_target: str) -> None:
102
102
  """Create every node document of FILE (a YAML stream) in the backend, or none of them.
103
103
 
104
- The whole change set passes the local shapes first, with the nodes it refers to;
105
- then the nodes are created parents first, each as ``kata tax create`` creates it.
106
- If one fails, the ones already created are removed again.
104
+ The stream is one change set: a reference resolves against every node in it as well
105
+ as the backend, and the whole set passes the reference checks and the local shapes
106
+ first. Then the nodes are created, each as ``kata tax create`` creates it, after the
107
+ nodes it needs (its parent, the nodes it refers to). If one fails, the ones already
108
+ created are removed again; so are those of an import that was killed part-way, by
109
+ the next one.
107
110
  """
108
111
  import yaml
109
- from katalyst_taxonomy.adapters.orchestration import create_node
110
- from katalyst_taxonomy.services.change_import import ImportError_, import_documents
112
+ from katalyst_taxonomy.adapters.orchestration import create_node, import_journal
113
+ from katalyst_taxonomy.services.change_import import (
114
+ ImportError_,
115
+ import_documents,
116
+ recover_import,
117
+ )
111
118
 
112
119
  from .._wiring import resolve_backend
113
120
 
114
121
  resolved = resolve_backend(ctx, backend_target)
115
122
  documents = [d for d in yaml.safe_load_all(file.read_text(encoding="utf-8")) if d]
123
+ journal = import_journal(resolved.backend)
116
124
  try:
125
+ rolled_back = recover_import(resolved.backend, journal)
126
+ if rolled_back:
127
+ ctx.output.echo(
128
+ "Rolled back an import that stopped part-way; removed: " + ", ".join(rolled_back)
129
+ )
117
130
  created = import_documents(
118
131
  resolved.backend,
119
132
  [cast("dict[str, Any]", d) for d in documents],
120
133
  create=lambda request: create_node(resolved.backend, request),
134
+ source=file,
135
+ journal=journal,
121
136
  )
122
137
  except ImportError_ as exc:
123
138
  ctx.output.echo(f"Error: {exc}")
124
- ctx.output.echo("Error: nothing was imported.")
139
+ if not exc.left_behind:
140
+ ctx.output.echo("Error: nothing was imported.")
125
141
  raise SystemExit(1) from exc
126
142
  for fqtn in created:
127
143
  ctx.output.echo(f"Imported: {resolved.name}:{fqtn}")
@@ -79,7 +79,11 @@ def add_plugin(ctx: CliContext, source_uri: str, yes: bool) -> None:
79
79
  @click.option("--json", "as_json", is_flag=True, help="Machine-readable output.")
80
80
  @click.pass_obj
81
81
  def list_plugins(ctx: CliContext, as_json: bool) -> None:
82
- """List plugins: entry-point packages and source installs, with their state."""
82
+ """List plugins: entry-point packages and source installs, with their state.
83
+
84
+ A plugin whose declared schema ranges the loaded schemas don't satisfy is
85
+ refused: listed, not activated, with the range and the loaded version.
86
+ """
83
87
  from katalyst_taxonomy.plugins.activation import current_activation
84
88
  from katalyst_taxonomy.plugins.registry import ORIGIN_SOURCE
85
89
  from katalyst_taxonomy.services.source_providers import declared_plugin_sources
@@ -102,6 +106,8 @@ def list_plugins(ctx: CliContext, as_json: bool) -> None:
102
106
  "source": declared.get(name),
103
107
  "enabled": activation.selection.is_enabled(name),
104
108
  "active": activation.activated(name),
109
+ "schemas": dict(manifest.schemas),
110
+ "refused": activation.refused(name) or None,
105
111
  }
106
112
  )
107
113
  for name, source in declared.items():
@@ -114,6 +120,8 @@ def list_plugins(ctx: CliContext, as_json: bool) -> None:
114
120
  "source": source,
115
121
  "enabled": activation.selection.is_enabled(name) if activation else True,
116
122
  "active": False,
123
+ "schemas": {},
124
+ "refused": None,
117
125
  }
118
126
  )
119
127
  rows.sort(key=lambda row: str(row["name"]))
@@ -125,9 +133,16 @@ def list_plugins(ctx: CliContext, as_json: bool) -> None:
125
133
  return
126
134
  ctx.output.echo(f"Plugins ({len(rows)}):")
127
135
  for row in rows:
128
- state = "active" if row["active"] else ("enabled" if row["enabled"] else "disabled")
136
+ if row["active"]:
137
+ state = "active"
138
+ elif row["refused"]:
139
+ state = "refused"
140
+ else:
141
+ state = "enabled" if row["enabled"] else "disabled"
129
142
  source = f" [{row['source']}]" if row["source"] else ""
130
143
  ctx.output.echo(f" {row['name']} v{row['version']} {row['origin']} {state}{source}")
144
+ if row["refused"]:
145
+ ctx.output.echo(f" {row['refused']}")
131
146
 
132
147
 
133
148
  @plugin.command("remove")