kanon-cli 1.0.0__py3-none-any.whl

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.
kanon_cli/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """Kanon CLI -- Kanon Package Manager."""
2
+
3
+ __version__ = "1.0.0"
kanon_cli/__main__.py ADDED
@@ -0,0 +1,5 @@
1
+ """Allow running as ``python -m kanon_cli``."""
2
+
3
+ from kanon_cli.cli import main
4
+
5
+ main()
@@ -0,0 +1,35 @@
1
+ # Kanon Configuration
2
+ # Environment variables override these values (for pipeline integration)
3
+
4
+ # Repo Tool
5
+ # By default, kanon installs the latest rpm-git-repo from PyPI.
6
+ # To override (e.g., test an unreleased version), uncomment both lines:
7
+ # REPO_URL=https://github.com/caylent-solutions/rpm-git-repo.git
8
+ # REPO_REV=main
9
+
10
+ # Shared env vars for envsubst (resolved by 'repo envsubst' in remote.xml)
11
+ GITBASE=<YOUR_GIT_ORG_BASE_URL>
12
+ CLAUDE_MARKETPLACES_DIR=${HOME}/.claude-marketplaces
13
+
14
+ # Marketplace install toggle (set to true to enable Claude Code plugin lifecycle)
15
+ KANON_MARKETPLACE_INSTALL=<true|false>
16
+
17
+ # --- Sources ---
18
+ # Define one or more sources. Each source needs three variables:
19
+ # KANON_SOURCE_<name>_URL — Git URL of the manifest repository
20
+ # KANON_SOURCE_<name>_REVISION — Branch, tag, or PEP 440 constraint
21
+ # KANON_SOURCE_<name>_PATH — Path to the entry-point manifest XML
22
+ #
23
+ # Example: single source (marketplace plugins only)
24
+ # KANON_SOURCE_plugins_URL=https://github.com/your-org/your-manifest-repo.git
25
+ # KANON_SOURCE_plugins_REVISION=main
26
+ # KANON_SOURCE_plugins_PATH=repo-specs/marketplace-manifest.xml
27
+ #
28
+ # Example: two sources (build packages + marketplace plugins)
29
+ # KANON_SOURCE_packages_URL=https://github.com/your-org/your-packages-repo.git
30
+ # KANON_SOURCE_packages_REVISION=refs/tags/>=1.0.0,<2.0.0
31
+ # KANON_SOURCE_packages_PATH=repo-specs/build/packages.xml
32
+ #
33
+ # KANON_SOURCE_plugins_URL=https://github.com/your-org/your-marketplace-repo.git
34
+ # KANON_SOURCE_plugins_REVISION=main
35
+ # KANON_SOURCE_plugins_PATH=repo-specs/marketplace-manifest.xml
@@ -0,0 +1,155 @@
1
+ # Kanon -- Standalone Catalog Entry Package
2
+
3
+ Getting started with Kanon using the CLI directly. This is the simplest setup -- you get a `.kanon` configuration file and invoke `kanon install` and `kanon clean` directly.
4
+
5
+ ---
6
+
7
+ ## Prerequisites
8
+
9
+ - Python 3.11+
10
+ - [pipx](https://pipx.pypa.io/) on PATH
11
+ - [uv](https://docs.astral.sh/uv/) -- Python package installer
12
+ - Git
13
+
14
+ The Kanon CLI is already installed (it just ran `kanon bootstrap` to create this file).
15
+
16
+ > **SSH Users:** Kanon uses HTTPS Git URLs internally. If you authenticate with GitHub via SSH, configure Git to rewrite HTTPS URLs to SSH globally:
17
+ >
18
+ > ```bash
19
+ > git config --global url."git@github.com:".insteadOf "https://github.com/"
20
+ > ```
21
+ >
22
+ > The `--global` flag is required -- `--local` will not work because Kanon uses the `repo` tool under the hood, which operates in its own working directories with their own local Git configuration. For other Git hosts, adjust the URL accordingly (e.g., `git@gitlab.com:` for GitLab).
23
+
24
+ ---
25
+
26
+ ## Setup
27
+
28
+ ### 1. Install (sync all packages)
29
+
30
+ Edit `.kanon` to set `GITBASE`, `KANON_MARKETPLACE_INSTALL`, and your source variables, then run install:
31
+
32
+ ```bash
33
+ kanon install .kanon
34
+ ```
35
+
36
+ This syncs all packages to `.packages/`, creates source workspaces in `.kanon-data/sources/`, and updates `.gitignore`.
37
+
38
+ ### 2. Verify
39
+
40
+ ```bash
41
+ ls .packages/
42
+ ```
43
+
44
+ After install, `.packages/` contains symlinks to all synced packages.
45
+
46
+ ---
47
+
48
+ ## Usage
49
+
50
+ **Install (sync packages):**
51
+
52
+ ```bash
53
+ kanon install .kanon
54
+ ```
55
+
56
+ **Clean (full teardown):**
57
+
58
+ ```bash
59
+ kanon clean .kanon
60
+ ```
61
+
62
+ **Validate manifests:**
63
+
64
+ ```bash
65
+ kanon validate xml
66
+ kanon validate marketplace
67
+ ```
68
+
69
+ ---
70
+
71
+ ## `.kanon` Variable Reference
72
+
73
+ ### Core Variables
74
+
75
+ | Variable | Required | Purpose |
76
+ |---|---|---|
77
+ | `REPO_URL` | No | Git URL of the [rpm-git-repo](https://github.com/caylent-solutions/rpm-git-repo) tool. Optional — omit to install from PyPI (default). Set both `REPO_URL` and `REPO_REV` to override with a git source (e.g., to test an unreleased version). |
78
+ | `REPO_REV` | No | rpm-git-repo version (branch or tag) for git override. Only used when `REPO_URL` is also set. Supports PEP 440 specifiers (e.g., `~=2.0.0`, `>=2.0.0,<3.0.0`, `*`). |
79
+ | `GITBASE` | Yes | Base Git URL for your organization (e.g., `https://github.com/your-org/`). Used by `repo envsubst` to resolve `${GITBASE}` placeholders in manifest XML files. |
80
+ | `CLAUDE_MARKETPLACES_DIR` | Conditional | Directory for marketplace plugin symlinks. Required when `KANON_MARKETPLACE_INSTALL=true`. Typically `${HOME}/.claude-marketplaces`. |
81
+ | `KANON_MARKETPLACE_INSTALL` | No | Set to `true` to enable the marketplace plugin install/uninstall lifecycle during install and clean. Default: `false`. When `false`, marketplace-related operations are skipped entirely. |
82
+
83
+ ### Source Variables
84
+
85
+ Sources are auto-discovered from `KANON_SOURCE_<name>_URL` patterns and processed in alphabetical order by name. Each source requires three variables:
86
+
87
+ | Variable | Required | Purpose |
88
+ |---|---|---|
89
+ | `KANON_SOURCE_<name>_URL` | Yes | Git URL of the manifest repository for this source. |
90
+ | `KANON_SOURCE_<name>_REVISION` | Yes | Branch, tag, or PEP 440 constraint to track for this source's manifest repository. |
91
+ | `KANON_SOURCE_<name>_PATH` | Yes | Path to the entry-point manifest XML file within the manifest repository (e.g., `repo-specs/build/meta.xml`). |
92
+
93
+ You can define multiple sources. Add new source blocks in `.kanon` as needed.
94
+
95
+ ---
96
+
97
+ ## How It Works
98
+
99
+ The Kanon CLI reads `.kanon` and for each source:
100
+
101
+ 1. `repo init` -- Clones the manifest repository
102
+ 2. `repo envsubst` -- Resolves `${VARIABLE}` placeholders in manifest XML
103
+ 3. `repo sync` -- Syncs packages into `.kanon-data/sources/<name>/.packages/`
104
+
105
+ After all sources are synced, Kanon aggregates their packages into `.packages/` using symlinks, giving a unified view regardless of which source provided each package.
106
+
107
+ **Committed files:** `.kanon`, `kanon-readme.md`
108
+
109
+ **Ephemeral files (gitignored):** `.packages/`, `.kanon-data/`
110
+
111
+ ---
112
+
113
+ ## When to Use This Package
114
+
115
+ Use the `kanon` catalog entry package when:
116
+
117
+ - Your project uses a build tool not directly supported by Kanon's catalog (npm, Maven, Bazel, etc.)
118
+ - You prefer to invoke the Kanon CLI directly from scripts or CI/CD pipelines
119
+ - You want the simplest possible setup with no wrapper files
120
+
121
+ You can integrate Kanon with any build tool by wrapping `kanon install .kanon` and `kanon clean .kanon` in your task runner of choice.
122
+
123
+ ---
124
+
125
+ ## Adding Sources
126
+
127
+ Add source blocks in `.kanon`:
128
+
129
+ ```properties
130
+ KANON_SOURCE_tools_URL=https://github.com/your-org/tools-manifests.git
131
+ KANON_SOURCE_tools_REVISION=main
132
+ KANON_SOURCE_tools_PATH=repo-specs/tools/meta.xml
133
+ ```
134
+
135
+ ---
136
+
137
+ ## Marketplace Plugins (Optional)
138
+
139
+ To enable Claude Code marketplace plugins, set in `.kanon`:
140
+
141
+ ```properties
142
+ KANON_MARKETPLACE_INSTALL=true
143
+ CLAUDE_MARKETPLACES_DIR=${HOME}/.claude-marketplaces
144
+ ```
145
+
146
+ Then add a marketplace source and re-run `kanon install .kanon`. When `KANON_MARKETPLACE_INSTALL` is `false` (the default), the marketplace lifecycle is skipped entirely -- no plugins are installed or uninstalled.
147
+
148
+ ---
149
+
150
+ ## Troubleshooting
151
+
152
+ - **`kanon: command not found`** -- Reinstall the Kanon CLI: `pipx install kanon-cli`
153
+ - **`kanon install` fails with ".kanon not found"** -- Pass the path: `kanon install .kanon`
154
+ - **`repo envsubst` fails** -- Ensure `GITBASE` is set in `.kanon` and is a valid URL ending with `/`
155
+ - **Authentication errors during sync** -- If you use SSH for Git auth, ensure the HTTPS-to-SSH rewrite is configured globally: `git config --global url."git@github.com:".insteadOf "https://github.com/"`. If you use HTTPS, ensure your credential helper is configured.
kanon_cli/cli.py ADDED
@@ -0,0 +1,67 @@
1
+ """Kanon CLI entry point with argparse subcommands.
2
+
3
+ Provides the top-level ``kanon`` command with subcommands:
4
+ - ``kanon install <kanonenv-path>`` -- Full lifecycle: prereqs + repo install + multi-source sync
5
+ - ``kanon clean <kanonenv-path>`` -- Full teardown: uninstall, remove dirs
6
+ - ``kanon validate xml [--repo-root PATH]`` -- Validate manifest XML files
7
+ - ``kanon validate marketplace [--repo-root PATH]`` -- Validate marketplace XML manifests
8
+ - ``kanon bootstrap <package>`` -- Scaffold a new Kanon project from a catalog entry package
9
+ - ``kanon bootstrap list`` -- List available catalog entry packages
10
+ """
11
+
12
+ import argparse
13
+ import sys
14
+
15
+ from kanon_cli import __version__
16
+ from kanon_cli.commands.bootstrap import register as register_bootstrap
17
+ from kanon_cli.commands.clean import register as register_clean
18
+ from kanon_cli.commands.install import register as register_install
19
+ from kanon_cli.commands.validate import register as register_validate
20
+
21
+
22
+ def build_parser() -> argparse.ArgumentParser:
23
+ """Build the top-level argument parser with subcommands.
24
+
25
+ Returns:
26
+ The configured ArgumentParser.
27
+ """
28
+ parser = argparse.ArgumentParser(
29
+ prog="kanon",
30
+ description="Kanon (Kanon Package Manager) CLI tool. Manages the full Kanon lifecycle: install, clean, and validate.",
31
+ epilog="Examples:\n kanon install .kanon\n kanon clean .kanon\n kanon validate xml\n kanon validate marketplace --repo-root /path/to/repo",
32
+ formatter_class=argparse.RawDescriptionHelpFormatter,
33
+ )
34
+ parser.add_argument(
35
+ "--version",
36
+ action="version",
37
+ version=f"%(prog)s {__version__}",
38
+ )
39
+
40
+ subparsers = parser.add_subparsers(
41
+ dest="command",
42
+ title="subcommands",
43
+ description="Available subcommands",
44
+ )
45
+
46
+ register_bootstrap(subparsers)
47
+ register_install(subparsers)
48
+ register_clean(subparsers)
49
+ register_validate(subparsers)
50
+
51
+ return parser
52
+
53
+
54
+ def main(argv: list[str] | None = None) -> None:
55
+ """Parse arguments and dispatch to the appropriate subcommand.
56
+
57
+ Args:
58
+ argv: Command-line arguments. Defaults to sys.argv[1:].
59
+ """
60
+ parser = build_parser()
61
+ args = parser.parse_args(argv)
62
+
63
+ if args.command is None:
64
+ parser.print_help()
65
+ sys.exit(2)
66
+
67
+ args.func(args)
File without changes
@@ -0,0 +1,70 @@
1
+ """Bootstrap subcommand: scaffold a new Kanon project.
2
+
3
+ Copies catalog entry package files from the catalog and provides
4
+ a pre-configured ``.kanon`` configuration file. Supports remote
5
+ catalog sources via ``--catalog-source`` flag or
6
+ ``KANON_CATALOG_SOURCE`` environment variable.
7
+ """
8
+
9
+ import argparse
10
+ import pathlib
11
+
12
+ from kanon_cli.core.bootstrap import bootstrap_package, list_packages
13
+ from kanon_cli.core.catalog import resolve_catalog_dir
14
+
15
+
16
+ def register(subparsers) -> None:
17
+ """Register the bootstrap subcommand.
18
+
19
+ Args:
20
+ subparsers: The subparsers object from the parent parser.
21
+ """
22
+ parser = subparsers.add_parser(
23
+ "bootstrap",
24
+ help="Scaffold a new Kanon project with catalog entry package files",
25
+ description=(
26
+ "Copy catalog entry package files (including a pre-configured\n"
27
+ ".kanon) into the target directory.\n\n"
28
+ "Use 'kanon bootstrap list' to see available packages."
29
+ ),
30
+ epilog="Examples:\n kanon bootstrap list\n kanon bootstrap kanon\n kanon bootstrap kanon --output-dir my-project",
31
+ formatter_class=argparse.RawDescriptionHelpFormatter,
32
+ )
33
+ parser.add_argument(
34
+ "package",
35
+ help="Catalog entry package name (e.g. kanon) or 'list' to show available packages",
36
+ )
37
+ parser.add_argument(
38
+ "--output-dir",
39
+ type=pathlib.Path,
40
+ default=pathlib.Path("."),
41
+ help="Target directory for bootstrapped files (default: current directory)",
42
+ )
43
+ parser.add_argument(
44
+ "--catalog-source",
45
+ default=None,
46
+ help=(
47
+ "Remote catalog source as '<git_url>@<ref>' where ref is a branch, "
48
+ "tag, or 'latest'. Overrides KANON_CATALOG_SOURCE env var. "
49
+ "Default: bundled catalog."
50
+ ),
51
+ )
52
+ parser.set_defaults(func=_run)
53
+
54
+
55
+ def _run(args) -> None:
56
+ """Execute the bootstrap command.
57
+
58
+ Args:
59
+ args: Parsed arguments with package, output_dir, and catalog_source.
60
+ """
61
+ catalog_dir = resolve_catalog_dir(args.catalog_source)
62
+
63
+ if args.package == "list":
64
+ packages = list_packages(catalog_dir)
65
+ print("Available packages:")
66
+ for pkg in packages:
67
+ print(f" {pkg}")
68
+ return
69
+
70
+ bootstrap_package(args.package, args.output_dir, catalog_dir)
@@ -0,0 +1,40 @@
1
+ """Clean subcommand handler."""
2
+
3
+ import pathlib
4
+
5
+ from kanon_cli.core.clean import clean
6
+
7
+
8
+ def register(subparsers) -> None:
9
+ """Register the clean subcommand.
10
+
11
+ Args:
12
+ subparsers: The subparsers object from the parent parser.
13
+ """
14
+ parser = subparsers.add_parser(
15
+ "clean",
16
+ help="Full teardown: uninstall, remove dirs",
17
+ description=(
18
+ "Execute the full Kanon clean lifecycle.\n\n"
19
+ "If KANON_MARKETPLACE_INSTALL=true, runs the uninstall script\n"
20
+ "and removes the marketplace directory. Then removes .packages/\n"
21
+ "and .kanon-data/ directories."
22
+ ),
23
+ epilog="Example:\n kanon clean .kanon",
24
+ formatter_class=__import__("argparse").RawDescriptionHelpFormatter,
25
+ )
26
+ parser.add_argument(
27
+ "kanonenv_path",
28
+ type=pathlib.Path,
29
+ help="Path to the .kanon configuration file",
30
+ )
31
+ parser.set_defaults(func=_run)
32
+
33
+
34
+ def _run(args) -> None:
35
+ """Execute the clean command.
36
+
37
+ Args:
38
+ args: Parsed arguments with kanonenv_path.
39
+ """
40
+ clean(args.kanonenv_path)
@@ -0,0 +1,186 @@
1
+ """Install subcommand: prereqs + repo install + lifecycle.
2
+
3
+ Checks that pipx is available, ensures the repo tool is installed
4
+ (from PyPI by default, or from a git URL when REPO_URL and REPO_REV
5
+ are set), then delegates to the core install logic.
6
+ """
7
+
8
+ import json
9
+ import pathlib
10
+ import shutil
11
+ import subprocess
12
+ import sys
13
+
14
+ from kanon_cli.constants import PYPI_REPO_TOOL_PACKAGE
15
+ from kanon_cli.core.install import install
16
+ from kanon_cli.version import resolve_version
17
+
18
+
19
+ def register(subparsers) -> None:
20
+ """Register the install subcommand.
21
+
22
+ Args:
23
+ subparsers: The subparsers object from the parent parser.
24
+ """
25
+ parser = subparsers.add_parser(
26
+ "install",
27
+ help="Full lifecycle: check prereqs, install repo, multi-source sync",
28
+ description=(
29
+ "Execute the full Kanon install lifecycle.\n\n"
30
+ "Checks prerequisites (pipx on PATH), installs the repo tool,\n"
31
+ "then runs repo init/envsubst/sync for each source defined in\n"
32
+ "the .kanon file. Aggregates packages into .packages/ via symlinks."
33
+ ),
34
+ epilog="Example:\n kanon install .kanon",
35
+ formatter_class=__import__("argparse").RawDescriptionHelpFormatter,
36
+ )
37
+ parser.add_argument(
38
+ "kanonenv_path",
39
+ type=pathlib.Path,
40
+ help="Path to the .kanon configuration file",
41
+ )
42
+ parser.set_defaults(func=_run)
43
+
44
+
45
+ def _run(args) -> None:
46
+ """Execute the install command.
47
+
48
+ Args:
49
+ args: Parsed arguments with kanonenv_path.
50
+ """
51
+ print("kanon install: checking prerequisites...")
52
+ _check_pipx()
53
+
54
+ from kanon_cli.core.kanonenv import parse_kanonenv
55
+
56
+ config = parse_kanonenv(args.kanonenv_path)
57
+ globals_dict = config["globals"]
58
+
59
+ repo_url = globals_dict.get("REPO_URL", "")
60
+ repo_rev = globals_dict.get("REPO_REV", "")
61
+
62
+ if repo_url and repo_rev:
63
+ resolved_rev = resolve_version(repo_url, repo_rev)
64
+ print(f"kanon install: installing repo tool from git ({resolved_rev})...")
65
+ _install_repo_tool_from_git(repo_url, resolved_rev)
66
+ elif repo_url or repo_rev:
67
+ print(
68
+ "Error: REPO_URL and REPO_REV must both be set or both be omitted.\n"
69
+ "Set both for git override, or omit both to install from PyPI.",
70
+ file=sys.stderr,
71
+ )
72
+ sys.exit(1)
73
+ else:
74
+ _ensure_repo_tool_from_pypi()
75
+
76
+ install(args.kanonenv_path)
77
+
78
+
79
+ def _check_pipx() -> None:
80
+ """Verify that pipx is available on PATH.
81
+
82
+ Raises:
83
+ SystemExit: If pipx is not found.
84
+ """
85
+ if shutil.which("pipx") is None:
86
+ print(
87
+ "Error: pipx is not installed or not on PATH.\n\n"
88
+ "Install pipx before running kanon install.\n"
89
+ " - pip: python3 -m pip install --user pipx\n"
90
+ " - apt: sudo apt install pipx\n"
91
+ " - brew: brew install pipx\n"
92
+ "After installing: pipx ensurepath",
93
+ file=sys.stderr,
94
+ )
95
+ sys.exit(1)
96
+
97
+
98
+ def _is_repo_tool_installed() -> bool:
99
+ """Check if rpm-git-repo is installed via pipx.
100
+
101
+ Returns:
102
+ True if the package is found in pipx list output.
103
+
104
+ Raises:
105
+ SystemExit: If pipx list fails or returns unparseable output.
106
+ """
107
+ result = subprocess.run(
108
+ ["pipx", "list", "--json"],
109
+ capture_output=True,
110
+ text=True,
111
+ check=False,
112
+ )
113
+ if result.returncode != 0:
114
+ print(
115
+ f"Error: pipx list --json failed: {result.stderr}",
116
+ file=sys.stderr,
117
+ )
118
+ sys.exit(1)
119
+
120
+ try:
121
+ data = json.loads(result.stdout)
122
+ except json.JSONDecodeError as exc:
123
+ print(
124
+ f"Error: pipx list --json returned invalid JSON: {exc}",
125
+ file=sys.stderr,
126
+ )
127
+ sys.exit(1)
128
+
129
+ if "venvs" not in data:
130
+ print(
131
+ "Error: pipx list --json output missing 'venvs' key.",
132
+ file=sys.stderr,
133
+ )
134
+ sys.exit(1)
135
+
136
+ return PYPI_REPO_TOOL_PACKAGE in data["venvs"]
137
+
138
+
139
+ def _ensure_repo_tool_from_pypi() -> None:
140
+ """Install rpm-git-repo from PyPI if not already installed.
141
+
142
+ Raises:
143
+ SystemExit: If pipx install fails.
144
+ """
145
+ if _is_repo_tool_installed():
146
+ print("kanon install: repo tool already installed, skipping.")
147
+ return
148
+
149
+ print(f"kanon install: installing repo tool from PyPI ({PYPI_REPO_TOOL_PACKAGE})...")
150
+ result = subprocess.run(
151
+ ["pipx", "install", PYPI_REPO_TOOL_PACKAGE],
152
+ capture_output=True,
153
+ text=True,
154
+ check=False,
155
+ )
156
+ if result.returncode != 0:
157
+ print(
158
+ f"Error: Failed to install {PYPI_REPO_TOOL_PACKAGE} via pipx: {result.stderr}",
159
+ file=sys.stderr,
160
+ )
161
+ sys.exit(1)
162
+
163
+
164
+ def _install_repo_tool_from_git(repo_url: str, repo_rev: str) -> None:
165
+ """Install the repo tool from a git URL via pipx (override mode).
166
+
167
+ Args:
168
+ repo_url: Git URL of the repo tool.
169
+ repo_rev: Resolved version/branch/tag to install.
170
+
171
+ Raises:
172
+ SystemExit: If pipx install fails.
173
+ """
174
+ install_spec = f"git+{repo_url}@{repo_rev}"
175
+ result = subprocess.run(
176
+ ["pipx", "install", "--force", install_spec],
177
+ capture_output=True,
178
+ text=True,
179
+ check=False,
180
+ )
181
+ if result.returncode != 0:
182
+ print(
183
+ f"Error: Failed to install repo tool via pipx: {result.stderr}",
184
+ file=sys.stderr,
185
+ )
186
+ sys.exit(1)