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
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Validate subcommand with xml and marketplace sub-subcommands."""
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from kanon_cli.core.marketplace_validator import validate_marketplace
|
|
8
|
+
from kanon_cli.core.xml_validator import validate_xml
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def register(subparsers) -> None:
|
|
12
|
+
"""Register the validate subcommand with xml and marketplace sub-subcommands.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
subparsers: The subparsers object from the parent parser.
|
|
16
|
+
"""
|
|
17
|
+
validate_parser = subparsers.add_parser(
|
|
18
|
+
"validate",
|
|
19
|
+
help="Validate XML manifests",
|
|
20
|
+
description="Validate manifest XML files for well-formedness and correctness.",
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
validate_subs = validate_parser.add_subparsers(
|
|
24
|
+
dest="validate_command",
|
|
25
|
+
title="validation targets",
|
|
26
|
+
description="Available validation targets",
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
# xml sub-subcommand
|
|
30
|
+
xml_parser = validate_subs.add_parser(
|
|
31
|
+
"xml",
|
|
32
|
+
help="Validate manifest XML files (well-formedness, required attributes, include chains)",
|
|
33
|
+
description=(
|
|
34
|
+
"Validate all XML manifest files under repo-specs/.\n\n"
|
|
35
|
+
"Checks well-formedness, required attributes on <project> and <remote>\n"
|
|
36
|
+
"elements, and that <include> name attributes point to existing files."
|
|
37
|
+
),
|
|
38
|
+
epilog="Example:\n kanon validate xml\n kanon validate xml --repo-root /path/to/repo",
|
|
39
|
+
formatter_class=__import__("argparse").RawDescriptionHelpFormatter,
|
|
40
|
+
)
|
|
41
|
+
xml_parser.add_argument(
|
|
42
|
+
"--repo-root",
|
|
43
|
+
type=Path,
|
|
44
|
+
default=None,
|
|
45
|
+
help="Repository root directory (default: auto-detect via git rev-parse)",
|
|
46
|
+
)
|
|
47
|
+
xml_parser.set_defaults(func=_run_xml)
|
|
48
|
+
|
|
49
|
+
# marketplace sub-subcommand
|
|
50
|
+
mp_parser = validate_subs.add_parser(
|
|
51
|
+
"marketplace",
|
|
52
|
+
help="Validate marketplace XML manifests (linkfile dest, include chains, name uniqueness, tag format)",
|
|
53
|
+
description=(
|
|
54
|
+
"Validate all marketplace XML manifests under repo-specs/.\n\n"
|
|
55
|
+
"Checks linkfile dest attributes, include chain integrity,\n"
|
|
56
|
+
"project path uniqueness, and revision tag format."
|
|
57
|
+
),
|
|
58
|
+
epilog="Example:\n kanon validate marketplace\n kanon validate marketplace --repo-root /path/to/repo",
|
|
59
|
+
formatter_class=__import__("argparse").RawDescriptionHelpFormatter,
|
|
60
|
+
)
|
|
61
|
+
mp_parser.add_argument(
|
|
62
|
+
"--repo-root",
|
|
63
|
+
type=Path,
|
|
64
|
+
default=None,
|
|
65
|
+
help="Repository root directory (default: auto-detect via git rev-parse)",
|
|
66
|
+
)
|
|
67
|
+
mp_parser.set_defaults(func=_run_marketplace)
|
|
68
|
+
|
|
69
|
+
validate_parser.set_defaults(func=_run_validate_help)
|
|
70
|
+
validate_parser._validate_subs = validate_subs
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _run_validate_help(args) -> None:
|
|
74
|
+
"""Show help when no validate sub-subcommand is given."""
|
|
75
|
+
if args.validate_command is None:
|
|
76
|
+
print(
|
|
77
|
+
"Error: Must specify a validation target: xml or marketplace",
|
|
78
|
+
file=sys.stderr,
|
|
79
|
+
)
|
|
80
|
+
sys.exit(2)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _resolve_repo_root(provided: Path | None) -> Path:
|
|
84
|
+
"""Resolve repository root from argument or git.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
provided: Explicitly provided repo root, or None for auto-detect.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
The resolved repository root path.
|
|
91
|
+
|
|
92
|
+
Raises:
|
|
93
|
+
SystemExit: If auto-detection fails.
|
|
94
|
+
"""
|
|
95
|
+
if provided is not None:
|
|
96
|
+
return provided
|
|
97
|
+
|
|
98
|
+
result = subprocess.run(
|
|
99
|
+
["git", "rev-parse", "--show-toplevel"],
|
|
100
|
+
capture_output=True,
|
|
101
|
+
text=True,
|
|
102
|
+
check=False,
|
|
103
|
+
)
|
|
104
|
+
if result.returncode != 0:
|
|
105
|
+
print(
|
|
106
|
+
"Error: Could not auto-detect repo root. Run from within a git repository or use --repo-root.",
|
|
107
|
+
file=sys.stderr,
|
|
108
|
+
)
|
|
109
|
+
sys.exit(1)
|
|
110
|
+
|
|
111
|
+
return Path(result.stdout.strip())
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _run_xml(args) -> None:
|
|
115
|
+
"""Execute XML validation.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
args: Parsed arguments with optional repo_root.
|
|
119
|
+
"""
|
|
120
|
+
repo_root = _resolve_repo_root(args.repo_root)
|
|
121
|
+
exit_code = validate_xml(repo_root)
|
|
122
|
+
sys.exit(exit_code)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _run_marketplace(args) -> None:
|
|
126
|
+
"""Execute marketplace validation.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
args: Parsed arguments with optional repo_root.
|
|
130
|
+
"""
|
|
131
|
+
repo_root = _resolve_repo_root(args.repo_root)
|
|
132
|
+
exit_code = validate_marketplace(repo_root)
|
|
133
|
+
sys.exit(exit_code)
|
kanon_cli/constants.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Centralized constants for the kanon-cli package.
|
|
2
|
+
|
|
3
|
+
All module-level constants live here to avoid hard-coded values
|
|
4
|
+
scattered across source files.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
|
|
9
|
+
# -- Package identity --
|
|
10
|
+
PYPI_REPO_TOOL_PACKAGE = "rpm-git-repo"
|
|
11
|
+
|
|
12
|
+
# -- Marketplace validation --
|
|
13
|
+
MARKETPLACE_DIR_PREFIX = "${CLAUDE_MARKETPLACES_DIR}/"
|
|
14
|
+
MARKETPLACE_FILE_GLOB = "*-marketplace.xml"
|
|
15
|
+
ALLOWED_BRANCHES = frozenset({"main", "review/caylent-claude"})
|
|
16
|
+
REFS_TAGS_RE = re.compile(r"^refs/tags/.+/\d+\.\d+\.\d+$")
|
|
17
|
+
CONSTRAINT_RE = re.compile(r"^(~=|>=|<=|>|<)\d+\.\d+\.\d+$")
|
|
18
|
+
|
|
19
|
+
# -- Version resolution --
|
|
20
|
+
PEP440_OPERATORS = ("~=", ">=", "<=", "!=", "==", ">", "<")
|
|
21
|
+
|
|
22
|
+
# -- kanonenv parsing --
|
|
23
|
+
SOURCE_PREFIX = "KANON_SOURCE_"
|
|
24
|
+
SOURCE_SUFFIXES = ("_URL", "_REVISION", "_PATH")
|
|
25
|
+
SUFFIX_TO_KEY = {"_URL": "url", "_REVISION": "revision", "_PATH": "path"}
|
|
26
|
+
SHELL_VAR_PATTERN = re.compile(r"\$\{([^}]+)\}")
|
|
27
|
+
|
|
28
|
+
# -- Catalog --
|
|
29
|
+
CATALOG_ENV_VAR = "KANON_CATALOG_SOURCE"
|
|
File without changes
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Bootstrap business logic for scaffolding new Kanon projects.
|
|
2
|
+
|
|
3
|
+
Provides functions to list available catalog entry packages and copy
|
|
4
|
+
them into a target directory with a pre-configured ``.kanon``
|
|
5
|
+
configuration file from the catalog.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import pathlib
|
|
9
|
+
import shutil
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def list_packages(catalog_dir: pathlib.Path) -> list[str]:
|
|
14
|
+
"""List available catalog entry packages from the catalog.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
catalog_dir: Path to the catalog directory.
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
Sorted list of package names (catalog subdirectory names).
|
|
21
|
+
"""
|
|
22
|
+
return sorted(d.name for d in catalog_dir.iterdir() if d.is_dir())
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def bootstrap_package(package: str, output_dir: pathlib.Path, catalog_dir: pathlib.Path) -> None:
|
|
26
|
+
"""Copy catalog entry package files into the output directory.
|
|
27
|
+
|
|
28
|
+
Copies all files from the catalog package directory (including a
|
|
29
|
+
pre-configured ``.kanon``) into the output directory. Refuses to
|
|
30
|
+
overwrite existing files.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
package: Name of the catalog entry package (e.g. ``make``, ``gradle``, ``kanon``).
|
|
34
|
+
output_dir: Target directory for the bootstrapped files.
|
|
35
|
+
catalog_dir: Path to the catalog directory.
|
|
36
|
+
|
|
37
|
+
Raises:
|
|
38
|
+
SystemExit: If the package is unknown, files already exist, or
|
|
39
|
+
the output directory cannot be created.
|
|
40
|
+
"""
|
|
41
|
+
package_dir = catalog_dir / package
|
|
42
|
+
|
|
43
|
+
if not package_dir.is_dir():
|
|
44
|
+
available = list_packages(catalog_dir)
|
|
45
|
+
print(
|
|
46
|
+
f"Error: Unknown package '{package}'. Available packages: {', '.join(available)}",
|
|
47
|
+
file=sys.stderr,
|
|
48
|
+
)
|
|
49
|
+
sys.exit(1)
|
|
50
|
+
|
|
51
|
+
all_files = [f.name for f in package_dir.iterdir() if f.is_file() and f.name != ".gitkeep"]
|
|
52
|
+
|
|
53
|
+
_check_no_conflicts(all_files, output_dir)
|
|
54
|
+
|
|
55
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
56
|
+
|
|
57
|
+
for src_file in package_dir.iterdir():
|
|
58
|
+
if src_file.is_file() and src_file.name != ".gitkeep":
|
|
59
|
+
shutil.copy2(src_file, output_dir / src_file.name)
|
|
60
|
+
|
|
61
|
+
_print_next_steps(package, output_dir, all_files)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _check_no_conflicts(files: list[str], output_dir: pathlib.Path) -> None:
|
|
65
|
+
"""Verify no target files already exist in the output directory.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
files: List of filenames to check.
|
|
69
|
+
output_dir: Target directory.
|
|
70
|
+
|
|
71
|
+
Raises:
|
|
72
|
+
SystemExit: If any file already exists, listing all conflicts.
|
|
73
|
+
"""
|
|
74
|
+
conflicts = [f for f in files if (output_dir / f).exists()]
|
|
75
|
+
if conflicts:
|
|
76
|
+
print("Error: The following files already exist:", file=sys.stderr)
|
|
77
|
+
for f in conflicts:
|
|
78
|
+
print(f" {output_dir / f}", file=sys.stderr)
|
|
79
|
+
print(
|
|
80
|
+
"\nRemove them first or use a different --output-dir.",
|
|
81
|
+
file=sys.stderr,
|
|
82
|
+
)
|
|
83
|
+
sys.exit(1)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _print_next_steps(
|
|
87
|
+
package: str,
|
|
88
|
+
output_dir: pathlib.Path,
|
|
89
|
+
files: list[str],
|
|
90
|
+
) -> None:
|
|
91
|
+
"""Print post-bootstrap instructions.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
package: Catalog entry package name.
|
|
95
|
+
output_dir: Directory where files were created.
|
|
96
|
+
files: List of created filenames.
|
|
97
|
+
"""
|
|
98
|
+
print(f"kanon bootstrap: created {package} project in {output_dir}/")
|
|
99
|
+
print("\nFiles created:")
|
|
100
|
+
for f in sorted(files):
|
|
101
|
+
print(f" {output_dir / f}")
|
|
102
|
+
|
|
103
|
+
print("\nNext steps:")
|
|
104
|
+
|
|
105
|
+
print(" 1. Edit .kanon — set GITBASE, KANON_MARKETPLACE_INSTALL, and source variables")
|
|
106
|
+
print(" 2. Run: kanon install .kanon")
|
|
107
|
+
print(" 3. Commit .kanon to your repository")
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Catalog directory resolution for Kanon bootstrap.
|
|
2
|
+
|
|
3
|
+
Resolves the catalog directory from multiple sources in priority order:
|
|
4
|
+
1. ``--catalog-source`` CLI flag
|
|
5
|
+
2. ``KANON_CATALOG_SOURCE`` environment variable
|
|
6
|
+
3. Bundled catalog shipped with the ``kanon_cli`` package
|
|
7
|
+
|
|
8
|
+
Remote catalog sources use the format ``<git_url>@<ref>`` where ref
|
|
9
|
+
can be a branch name, tag, or ``latest`` (resolves to highest semver tag).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
import pathlib
|
|
14
|
+
import subprocess
|
|
15
|
+
import sys
|
|
16
|
+
import tempfile
|
|
17
|
+
|
|
18
|
+
from kanon_cli.constants import CATALOG_ENV_VAR
|
|
19
|
+
from kanon_cli.version import resolve_version
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def resolve_catalog_dir(catalog_source: str | None = None) -> pathlib.Path:
|
|
23
|
+
"""Resolve the catalog directory from flag, env var, or bundled fallback.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
catalog_source: Remote catalog source from CLI flag (``<git_url>@<ref>``).
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
Path to the resolved catalog directory.
|
|
30
|
+
|
|
31
|
+
Raises:
|
|
32
|
+
SystemExit: If the remote catalog cannot be cloned or has no ``catalog/`` dir.
|
|
33
|
+
ValueError: If the catalog source format is invalid.
|
|
34
|
+
"""
|
|
35
|
+
source = catalog_source or os.environ.get(CATALOG_ENV_VAR)
|
|
36
|
+
|
|
37
|
+
if source:
|
|
38
|
+
return _clone_remote_catalog(source)
|
|
39
|
+
|
|
40
|
+
return _get_bundled_catalog_dir()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _get_bundled_catalog_dir() -> pathlib.Path:
|
|
44
|
+
"""Return the path to the bundled catalog shipped with the package.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
Absolute path to the bundled catalog directory.
|
|
48
|
+
"""
|
|
49
|
+
return pathlib.Path(__file__).parent.parent / "catalog"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _parse_catalog_source(source: str) -> tuple[str, str]:
|
|
53
|
+
"""Parse a catalog source string into URL and ref.
|
|
54
|
+
|
|
55
|
+
The format is ``<git_url>@<ref>`` where the last ``@`` is the delimiter.
|
|
56
|
+
This handles SSH URLs like ``git@github.com:org/repo.git@main``.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
source: Catalog source string.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
Tuple of (url, ref).
|
|
63
|
+
|
|
64
|
+
Raises:
|
|
65
|
+
ValueError: If the format is invalid (no ``@`` or empty ref).
|
|
66
|
+
"""
|
|
67
|
+
idx = source.rfind("@")
|
|
68
|
+
if idx == -1:
|
|
69
|
+
msg = (
|
|
70
|
+
f"Invalid catalog source format: '{source}'. "
|
|
71
|
+
"Expected '<git_url>@<ref>' (e.g. 'https://github.com/org/repo.git@main')"
|
|
72
|
+
)
|
|
73
|
+
raise ValueError(msg)
|
|
74
|
+
|
|
75
|
+
url = source[:idx]
|
|
76
|
+
ref = source[idx + 1 :]
|
|
77
|
+
|
|
78
|
+
if not ref:
|
|
79
|
+
msg = (
|
|
80
|
+
f"Empty ref in catalog source: '{source}'. "
|
|
81
|
+
"Expected '<git_url>@<ref>' (e.g. 'https://github.com/org/repo.git@v1.0.0')"
|
|
82
|
+
)
|
|
83
|
+
raise ValueError(msg)
|
|
84
|
+
|
|
85
|
+
if not url:
|
|
86
|
+
msg = f"Empty URL in catalog source: '{source}'"
|
|
87
|
+
raise ValueError(msg)
|
|
88
|
+
|
|
89
|
+
return url, ref
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _clone_remote_catalog(source: str) -> pathlib.Path:
|
|
93
|
+
"""Clone a remote catalog repo and return the catalog directory path.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
source: Catalog source string (``<git_url>@<ref>``).
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
Path to the ``catalog/`` directory inside the cloned repo.
|
|
100
|
+
|
|
101
|
+
Raises:
|
|
102
|
+
SystemExit: If git clone fails or the repo has no ``catalog/`` directory.
|
|
103
|
+
ValueError: If the source format is invalid.
|
|
104
|
+
"""
|
|
105
|
+
url, ref = _parse_catalog_source(source)
|
|
106
|
+
|
|
107
|
+
if ref == "latest":
|
|
108
|
+
ref = resolve_version(url, "*")
|
|
109
|
+
|
|
110
|
+
clone_dir = pathlib.Path(tempfile.mkdtemp(prefix="kanon-catalog-"))
|
|
111
|
+
|
|
112
|
+
result = subprocess.run(
|
|
113
|
+
["git", "clone", "--depth", "1", "--branch", ref, url, str(clone_dir / "repo")],
|
|
114
|
+
capture_output=True,
|
|
115
|
+
text=True,
|
|
116
|
+
check=False,
|
|
117
|
+
)
|
|
118
|
+
if result.returncode != 0:
|
|
119
|
+
print(
|
|
120
|
+
f"Error: Failed to clone catalog from {url}@{ref}: {result.stderr}",
|
|
121
|
+
file=sys.stderr,
|
|
122
|
+
)
|
|
123
|
+
sys.exit(1)
|
|
124
|
+
|
|
125
|
+
catalog_path = clone_dir / "repo" / "catalog"
|
|
126
|
+
if not catalog_path.is_dir():
|
|
127
|
+
print(
|
|
128
|
+
f"Error: Remote repo {url}@{ref} does not contain a 'catalog/' directory",
|
|
129
|
+
file=sys.stderr,
|
|
130
|
+
)
|
|
131
|
+
sys.exit(1)
|
|
132
|
+
|
|
133
|
+
return catalog_path
|
kanon_cli/core/clean.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Kanon clean business logic for full teardown.
|
|
2
|
+
|
|
3
|
+
Performs full Kanon teardown in the following order:
|
|
4
|
+
1. If KANON_MARKETPLACE_INSTALL=true:
|
|
5
|
+
uninstall marketplace plugins via claude CLI
|
|
6
|
+
2. If KANON_MARKETPLACE_INSTALL=true:
|
|
7
|
+
remove CLAUDE_MARKETPLACES_DIR
|
|
8
|
+
3. Remove .packages/ directory (ignore_errors=True)
|
|
9
|
+
4. Remove .kanon-data/ directory (ignore_errors=True)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import pathlib
|
|
13
|
+
import shutil
|
|
14
|
+
import sys
|
|
15
|
+
|
|
16
|
+
from kanon_cli.core.marketplace import uninstall_marketplace_plugins
|
|
17
|
+
from kanon_cli.core.kanonenv import parse_kanonenv
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def remove_marketplace_dir(marketplace_dir: pathlib.Path) -> None:
|
|
21
|
+
"""Remove the marketplace directory if it exists.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
marketplace_dir: Path to CLAUDE_MARKETPLACES_DIR.
|
|
25
|
+
"""
|
|
26
|
+
if marketplace_dir.exists():
|
|
27
|
+
shutil.rmtree(marketplace_dir)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def remove_packages_dir(base_dir: pathlib.Path) -> None:
|
|
31
|
+
"""Remove .packages/ directory with ignore_errors.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
base_dir: Project root directory.
|
|
35
|
+
"""
|
|
36
|
+
shutil.rmtree(base_dir / ".packages", ignore_errors=True)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def remove_kanon_dir(base_dir: pathlib.Path) -> None:
|
|
40
|
+
"""Remove .kanon-data/ directory with ignore_errors.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
base_dir: Project root directory.
|
|
44
|
+
"""
|
|
45
|
+
shutil.rmtree(base_dir / ".kanon-data", ignore_errors=True)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _print_remove_summary(packages_dir: pathlib.Path) -> None:
|
|
49
|
+
"""Print a summary of packages that will be removed.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
packages_dir: Path to ``.packages/`` directory.
|
|
53
|
+
"""
|
|
54
|
+
if not packages_dir.exists():
|
|
55
|
+
print("kanon clean: no packages to remove.")
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
pkgs = sorted(p.name for p in packages_dir.iterdir() if not p.name.startswith("."))
|
|
59
|
+
if not pkgs:
|
|
60
|
+
print("kanon clean: no packages to remove.")
|
|
61
|
+
return
|
|
62
|
+
|
|
63
|
+
print(f"kanon clean: removing {len(pkgs)} packages...")
|
|
64
|
+
for pkg in pkgs:
|
|
65
|
+
print(f" - {pkg}")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def clean(kanonenv_path: pathlib.Path) -> None:
|
|
69
|
+
"""Execute the full Kanon clean lifecycle.
|
|
70
|
+
|
|
71
|
+
Steps:
|
|
72
|
+
1. Parse .kanon
|
|
73
|
+
2. If KANON_MARKETPLACE_INSTALL=true: run uninstall, remove marketplace dir
|
|
74
|
+
3. Remove .packages/ and .kanon-data/
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
kanonenv_path: Path to the .kanon configuration file.
|
|
78
|
+
|
|
79
|
+
Raises:
|
|
80
|
+
SystemExit: On any failure during the clean process.
|
|
81
|
+
"""
|
|
82
|
+
print(f"kanon clean: parsing {kanonenv_path}...")
|
|
83
|
+
config = parse_kanonenv(kanonenv_path)
|
|
84
|
+
base_dir = kanonenv_path.parent
|
|
85
|
+
marketplace_install = config["KANON_MARKETPLACE_INSTALL"]
|
|
86
|
+
globals_dict = config["globals"]
|
|
87
|
+
|
|
88
|
+
marketplace_dir_str = globals_dict.get("CLAUDE_MARKETPLACES_DIR", "")
|
|
89
|
+
|
|
90
|
+
if marketplace_install and not marketplace_dir_str:
|
|
91
|
+
print(
|
|
92
|
+
"Error: KANON_MARKETPLACE_INSTALL=true but CLAUDE_MARKETPLACES_DIR is not defined in .kanon",
|
|
93
|
+
file=sys.stderr,
|
|
94
|
+
)
|
|
95
|
+
sys.exit(1)
|
|
96
|
+
|
|
97
|
+
packages_dir = base_dir / ".packages"
|
|
98
|
+
_print_remove_summary(packages_dir)
|
|
99
|
+
|
|
100
|
+
if marketplace_install:
|
|
101
|
+
print("kanon clean: running marketplace uninstall...")
|
|
102
|
+
marketplace_dir = pathlib.Path(marketplace_dir_str)
|
|
103
|
+
uninstall_marketplace_plugins(marketplace_dir)
|
|
104
|
+
print("kanon clean: removing marketplace directory...")
|
|
105
|
+
remove_marketplace_dir(marketplace_dir)
|
|
106
|
+
|
|
107
|
+
print("kanon clean: removing .packages/...")
|
|
108
|
+
remove_packages_dir(base_dir)
|
|
109
|
+
print("kanon clean: removing .kanon-data/...")
|
|
110
|
+
remove_kanon_dir(base_dir)
|
|
111
|
+
print("kanon clean: done.")
|