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 +3 -0
- kanon_cli/__main__.py +5 -0
- kanon_cli/catalog/kanon/.kanon +35 -0
- kanon_cli/catalog/kanon/kanon-readme.md +155 -0
- kanon_cli/cli.py +67 -0
- kanon_cli/commands/__init__.py +0 -0
- kanon_cli/commands/bootstrap.py +70 -0
- kanon_cli/commands/clean.py +40 -0
- kanon_cli/commands/install.py +186 -0
- kanon_cli/commands/validate.py +133 -0
- kanon_cli/constants.py +29 -0
- kanon_cli/core/__init__.py +0 -0
- kanon_cli/core/bootstrap.py +107 -0
- kanon_cli/core/catalog.py +133 -0
- kanon_cli/core/clean.py +111 -0
- kanon_cli/core/install.py +349 -0
- kanon_cli/core/kanonenv.py +322 -0
- kanon_cli/core/marketplace.py +433 -0
- kanon_cli/core/marketplace_validator.py +237 -0
- kanon_cli/core/xml_validator.py +94 -0
- kanon_cli/version.py +189 -0
- kanon_cli-1.0.0.dist-info/METADATA +844 -0
- kanon_cli-1.0.0.dist-info/RECORD +26 -0
- kanon_cli-1.0.0.dist-info/WHEEL +4 -0
- kanon_cli-1.0.0.dist-info/entry_points.txt +2 -0
- kanon_cli-1.0.0.dist-info/licenses/LICENSE +201 -0
kanon_cli/__init__.py
ADDED
kanon_cli/__main__.py
ADDED
|
@@ -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)
|