agr 0.4.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.
agr/cli/update.py ADDED
@@ -0,0 +1,140 @@
1
+ """Update subcommand for agr - re-fetch resources from GitHub."""
2
+
3
+ from typing import Annotated
4
+
5
+ import typer
6
+
7
+ from agr.cli.common import handle_update_bundle, handle_update_resource
8
+ from agr.fetcher import ResourceType
9
+
10
+ app = typer.Typer(
11
+ help="Update skills, commands, or agents from GitHub.",
12
+ no_args_is_help=True,
13
+ )
14
+
15
+
16
+ @app.command("skill")
17
+ def update_skill(
18
+ skill_ref: Annotated[
19
+ str,
20
+ typer.Argument(
21
+ help="Skill reference: <username>/<skill-name> or <username>/<repo>/<skill-name>",
22
+ metavar="REFERENCE",
23
+ ),
24
+ ],
25
+ global_install: Annotated[
26
+ bool,
27
+ typer.Option(
28
+ "--global",
29
+ "-g",
30
+ help="Update in ~/.claude/ instead of ./.claude/",
31
+ ),
32
+ ] = False,
33
+ ) -> None:
34
+ """Update a skill by re-fetching from GitHub.
35
+
36
+ REFERENCE format:
37
+ - username/skill-name: re-fetches from github.com/username/agent-resources
38
+ - username/repo/skill-name: re-fetches from github.com/username/repo
39
+
40
+ Examples:
41
+ agr update skill kasperjunge/hello-world
42
+ agr update skill kasperjunge/my-repo/hello-world --global
43
+ """
44
+ handle_update_resource(skill_ref, ResourceType.SKILL, "skills", global_install)
45
+
46
+
47
+ @app.command("command")
48
+ def update_command(
49
+ command_ref: Annotated[
50
+ str,
51
+ typer.Argument(
52
+ help="Command reference: <username>/<command-name> or <username>/<repo>/<command-name>",
53
+ metavar="REFERENCE",
54
+ ),
55
+ ],
56
+ global_install: Annotated[
57
+ bool,
58
+ typer.Option(
59
+ "--global",
60
+ "-g",
61
+ help="Update in ~/.claude/ instead of ./.claude/",
62
+ ),
63
+ ] = False,
64
+ ) -> None:
65
+ """Update a slash command by re-fetching from GitHub.
66
+
67
+ REFERENCE format:
68
+ - username/command-name: re-fetches from github.com/username/agent-resources
69
+ - username/repo/command-name: re-fetches from github.com/username/repo
70
+
71
+ Examples:
72
+ agr update command kasperjunge/hello
73
+ agr update command kasperjunge/my-repo/hello-world --global
74
+ """
75
+ handle_update_resource(command_ref, ResourceType.COMMAND, "commands", global_install)
76
+
77
+
78
+ @app.command("agent")
79
+ def update_agent(
80
+ agent_ref: Annotated[
81
+ str,
82
+ typer.Argument(
83
+ help="Agent reference: <username>/<agent-name> or <username>/<repo>/<agent-name>",
84
+ metavar="REFERENCE",
85
+ ),
86
+ ],
87
+ global_install: Annotated[
88
+ bool,
89
+ typer.Option(
90
+ "--global",
91
+ "-g",
92
+ help="Update in ~/.claude/ instead of ./.claude/",
93
+ ),
94
+ ] = False,
95
+ ) -> None:
96
+ """Update a sub-agent by re-fetching from GitHub.
97
+
98
+ REFERENCE format:
99
+ - username/agent-name: re-fetches from github.com/username/agent-resources
100
+ - username/repo/agent-name: re-fetches from github.com/username/repo
101
+
102
+ Examples:
103
+ agr update agent kasperjunge/hello-agent
104
+ agr update agent kasperjunge/my-repo/hello-agent --global
105
+ """
106
+ handle_update_resource(agent_ref, ResourceType.AGENT, "agents", global_install)
107
+
108
+
109
+ @app.command("bundle")
110
+ def update_bundle(
111
+ bundle_ref: Annotated[
112
+ str,
113
+ typer.Argument(
114
+ help="Bundle reference: <username>/<bundle-name> or <username>/<repo>/<bundle-name>",
115
+ metavar="REFERENCE",
116
+ ),
117
+ ],
118
+ global_install: Annotated[
119
+ bool,
120
+ typer.Option(
121
+ "--global",
122
+ "-g",
123
+ help="Update in ~/.claude/ instead of ./.claude/",
124
+ ),
125
+ ] = False,
126
+ ) -> None:
127
+ """Update a bundle by re-fetching from GitHub.
128
+
129
+ Re-downloads all resources from the bundle and overwrites local copies.
130
+ Also adds any new resources that were added to the bundle upstream.
131
+
132
+ REFERENCE format:
133
+ - username/bundle-name: re-fetches from github.com/username/agent-resources
134
+ - username/repo/bundle-name: re-fetches from github.com/username/repo
135
+
136
+ Examples:
137
+ agr update bundle kasperjunge/productivity
138
+ agr update bundle kasperjunge/my-repo/productivity --global
139
+ """
140
+ handle_update_bundle(bundle_ref, global_install)
agr/config.py ADDED
@@ -0,0 +1,187 @@
1
+ """Configuration management for agr.toml."""
2
+
3
+ from dataclasses import dataclass, field
4
+ from pathlib import Path
5
+
6
+ import tomlkit
7
+ from tomlkit import TOMLDocument
8
+ from tomlkit.exceptions import TOMLKitError
9
+
10
+ from agr.exceptions import ConfigParseError
11
+
12
+
13
+ @dataclass
14
+ class DependencySpec:
15
+ """Specification for a dependency in agr.toml."""
16
+
17
+ type: str | None = None # "skill", "command", "agent"
18
+
19
+
20
+ @dataclass
21
+ class AgrConfig:
22
+ """
23
+ Configuration loaded from agr.toml.
24
+
25
+ The config file tracks dependencies with fully qualified references:
26
+
27
+ [dependencies]
28
+ "kasperjunge/commit" = {}
29
+ "alice/review" = { type = "skill" }
30
+ """
31
+
32
+ dependencies: dict[str, DependencySpec] = field(default_factory=dict)
33
+ _document: TOMLDocument | None = field(default=None, repr=False)
34
+ _path: Path | None = field(default=None, repr=False)
35
+
36
+ @classmethod
37
+ def load(cls, path: Path) -> "AgrConfig":
38
+ """
39
+ Load configuration from an agr.toml file.
40
+
41
+ Args:
42
+ path: Path to the agr.toml file
43
+
44
+ Returns:
45
+ AgrConfig instance with loaded dependencies
46
+
47
+ Raises:
48
+ ConfigParseError: If the file contains invalid TOML
49
+ """
50
+ if not path.exists():
51
+ config = cls()
52
+ config._path = path
53
+ return config
54
+
55
+ try:
56
+ content = path.read_text()
57
+ doc = tomlkit.parse(content)
58
+ except TOMLKitError as e:
59
+ raise ConfigParseError(f"Invalid TOML in {path}: {e}")
60
+
61
+ config = cls()
62
+ config._document = doc
63
+ config._path = path
64
+
65
+ # Parse dependencies section
66
+ deps_section = doc.get("dependencies", {})
67
+ for ref, spec in deps_section.items():
68
+ if isinstance(spec, dict):
69
+ config.dependencies[ref] = DependencySpec(
70
+ type=spec.get("type")
71
+ )
72
+ else:
73
+ config.dependencies[ref] = DependencySpec()
74
+
75
+ return config
76
+
77
+ def save(self, path: Path | None = None) -> None:
78
+ """
79
+ Save configuration to an agr.toml file.
80
+
81
+ Args:
82
+ path: Path to save to (uses original path if not specified)
83
+ """
84
+ save_path = path or self._path
85
+ if save_path is None:
86
+ raise ValueError("No path specified for saving config")
87
+
88
+ # Use existing document to preserve comments, or create new one
89
+ if self._document is not None:
90
+ doc = self._document
91
+ else:
92
+ doc = tomlkit.document()
93
+
94
+ # Update dependencies section
95
+ if "dependencies" not in doc:
96
+ doc["dependencies"] = tomlkit.table()
97
+
98
+ deps_table = doc["dependencies"]
99
+
100
+ # Clear existing dependencies and rebuild
101
+ # First, collect keys to remove
102
+ existing_keys = list(deps_table.keys())
103
+ for key in existing_keys:
104
+ del deps_table[key]
105
+
106
+ # Add current dependencies
107
+ for ref, spec in self.dependencies.items():
108
+ if spec.type:
109
+ deps_table[ref] = {"type": spec.type}
110
+ else:
111
+ deps_table[ref] = {}
112
+
113
+ save_path.write_text(tomlkit.dumps(doc))
114
+ self._document = doc
115
+ self._path = save_path
116
+
117
+ def add_dependency(self, ref: str, spec: DependencySpec) -> None:
118
+ """
119
+ Add or update a dependency.
120
+
121
+ Args:
122
+ ref: Dependency reference (e.g., "kasperjunge/commit")
123
+ spec: Dependency specification
124
+ """
125
+ self.dependencies[ref] = spec
126
+
127
+ def remove_dependency(self, ref: str) -> None:
128
+ """
129
+ Remove a dependency.
130
+
131
+ Args:
132
+ ref: Dependency reference to remove
133
+ """
134
+ self.dependencies.pop(ref, None)
135
+
136
+
137
+ def find_config(start_path: Path | None = None) -> Path | None:
138
+ """
139
+ Find agr.toml by walking up from the start path to the git root.
140
+
141
+ Args:
142
+ start_path: Directory to start searching from (defaults to cwd)
143
+
144
+ Returns:
145
+ Path to agr.toml if found, None otherwise
146
+ """
147
+ current = start_path or Path.cwd()
148
+
149
+ while True:
150
+ config_path = current / "agr.toml"
151
+ if config_path.exists():
152
+ return config_path
153
+
154
+ # Check if we've reached git root
155
+ if (current / ".git").exists():
156
+ return None
157
+
158
+ # Move to parent
159
+ parent = current.parent
160
+ if parent == current:
161
+ # Reached filesystem root
162
+ return None
163
+ current = parent
164
+
165
+
166
+ def get_or_create_config(start_path: Path | None = None) -> tuple[Path, AgrConfig]:
167
+ """
168
+ Get existing config or create a new one in cwd.
169
+
170
+ Args:
171
+ start_path: Directory to start searching from (defaults to cwd)
172
+
173
+ Returns:
174
+ Tuple of (path to config, AgrConfig instance)
175
+ """
176
+ existing = find_config(start_path)
177
+ if existing:
178
+ return existing, AgrConfig.load(existing)
179
+
180
+ # Create new config in cwd
181
+ cwd = start_path or Path.cwd()
182
+ config_path = cwd / "agr.toml"
183
+
184
+ config = AgrConfig()
185
+ config.save(config_path)
186
+
187
+ return config_path, config
agr/exceptions.py ADDED
@@ -0,0 +1,33 @@
1
+ """Shared exception classes for agr."""
2
+
3
+
4
+ class AgrError(Exception):
5
+ """Base exception for agr errors."""
6
+
7
+
8
+ class RepoNotFoundError(AgrError):
9
+ """Raised when the GitHub repo doesn't exist."""
10
+
11
+
12
+ class ResourceNotFoundError(AgrError):
13
+ """Raised when the skill/command/agent doesn't exist in the repo."""
14
+
15
+
16
+ class ResourceExistsError(AgrError):
17
+ """Raised when the resource already exists locally."""
18
+
19
+
20
+ class BundleNotFoundError(AgrError):
21
+ """Raised when no bundle directory exists in any resource type."""
22
+
23
+
24
+ class MultipleResourcesFoundError(AgrError):
25
+ """Raised when a resource name exists in multiple resource types."""
26
+
27
+
28
+ class ConfigNotFoundError(AgrError):
29
+ """Raised when agr.toml is required but not found."""
30
+
31
+
32
+ class ConfigParseError(AgrError):
33
+ """Raised when agr.toml contains invalid TOML syntax."""