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/init.py ADDED
@@ -0,0 +1,292 @@
1
+ """Init subcommand for agr - create new resources and repos."""
2
+
3
+ from pathlib import Path
4
+ from typing import Annotated
5
+
6
+ import typer
7
+ from rich.console import Console
8
+
9
+ from agr.github import check_gh_cli, create_github_repo, get_github_username, repo_exists
10
+ from agr.scaffold import (
11
+ create_agent_resources_repo,
12
+ init_git,
13
+ scaffold_repo,
14
+ write_gitignore,
15
+ write_readme,
16
+ write_starter_agent,
17
+ write_starter_command,
18
+ write_starter_skill,
19
+ )
20
+
21
+ console = Console()
22
+
23
+ app = typer.Typer(
24
+ help="Create new agent resources or repositories.",
25
+ no_args_is_help=True,
26
+ )
27
+
28
+
29
+ @app.command("repo")
30
+ def init_repo(
31
+ name: Annotated[
32
+ str,
33
+ typer.Argument(
34
+ help="Name of the repository to create, or '.' for current directory",
35
+ metavar="NAME",
36
+ ),
37
+ ] = "agent-resources",
38
+ path: Annotated[
39
+ Path | None,
40
+ typer.Option(
41
+ "--path",
42
+ "-p",
43
+ help="Custom path for the repository (default: ./<name>)",
44
+ ),
45
+ ] = None,
46
+ github: Annotated[
47
+ bool,
48
+ typer.Option(
49
+ "--github",
50
+ "-g",
51
+ help="Create a GitHub repository and push",
52
+ ),
53
+ ] = False,
54
+ ) -> None:
55
+ """Create a new agent-resources repository with starter content.
56
+
57
+ Creates a directory structure with example skill, command, and agent files.
58
+
59
+ Examples:
60
+ agr init repo # Creates ./agent-resources/
61
+ agr init repo my-resources # Creates ./my-resources/
62
+ agr init repo . # Initialize in current directory
63
+ agr init repo agent-resources --github # Creates and pushes to GitHub
64
+ """
65
+ # Determine target path
66
+ if name == ".":
67
+ target_path = Path.cwd()
68
+ # Use directory name for GitHub repo name
69
+ name = target_path.name
70
+ else:
71
+ target_path = path or Path.cwd() / name
72
+
73
+ if target_path.exists():
74
+ console.print(f"[red]Error: Directory already exists: {target_path}[/red]")
75
+ raise typer.Exit(1)
76
+
77
+ # Check if .claude directory already exists
78
+ claude_dir = target_path / ".claude"
79
+ if claude_dir.exists():
80
+ console.print(f"[red]Error: .claude directory already exists at {claude_dir}[/red]")
81
+ raise typer.Exit(1)
82
+
83
+ # Check GitHub CLI if --github flag is set
84
+ if github:
85
+ if not check_gh_cli():
86
+ console.print(
87
+ "[red]Error: GitHub CLI (gh) is not installed or not authenticated.[/red]\n"
88
+ "Install it from https://cli.github.com/ and run 'gh auth login'"
89
+ )
90
+ raise typer.Exit(1)
91
+
92
+ if repo_exists(name):
93
+ console.print(f"[red]Error: GitHub repository '{name}' already exists.[/red]")
94
+ raise typer.Exit(1)
95
+
96
+ # Get GitHub username for README
97
+ username = get_github_username() or "<username>"
98
+
99
+ # Create the repository
100
+ console.print(f"Creating agent-resources repository at {target_path}...")
101
+ create_agent_resources_repo(target_path, username)
102
+
103
+ # Initialize git
104
+ if init_git(target_path):
105
+ console.print("[green]Initialized git repository[/green]")
106
+ else:
107
+ console.print("[yellow]Warning: Could not initialize git repository[/yellow]")
108
+
109
+ # Create GitHub repo if requested
110
+ if github:
111
+ console.print("Creating GitHub repository...")
112
+ repo_url = create_github_repo(target_path, name)
113
+ if repo_url:
114
+ console.print(f"[green]Created and pushed to {repo_url}[/green]")
115
+ console.print(f"\nOthers can now install your resources:")
116
+ console.print(f" agr add skill {username}/hello-world")
117
+ console.print(f" agr add command {username}/hello")
118
+ console.print(f" agr add agent {username}/hello-agent")
119
+ else:
120
+ console.print("[yellow]Warning: Could not create GitHub repository[/yellow]")
121
+ else:
122
+ console.print(f"\n[green]Created agent-resources repository at {target_path}[/green]")
123
+ console.print("\nNext steps:")
124
+ console.print(f" cd {target_path}")
125
+ console.print(" git remote add origin <your-repo-url>")
126
+ console.print(" git push -u origin main")
127
+
128
+
129
+ @app.command("skill")
130
+ def init_skill(
131
+ name: Annotated[
132
+ str,
133
+ typer.Argument(
134
+ help="Name of the skill to create",
135
+ metavar="NAME",
136
+ ),
137
+ ],
138
+ path: Annotated[
139
+ Path | None,
140
+ typer.Option(
141
+ "--path",
142
+ "-p",
143
+ help="Custom path (default: ./.claude/skills/<name>/)",
144
+ ),
145
+ ] = None,
146
+ ) -> None:
147
+ """Create a new skill scaffold.
148
+
149
+ Examples:
150
+ agr init skill my-skill
151
+ agr init skill code-reviewer --path ./custom/path/
152
+ """
153
+ target_path = path or (Path.cwd() / ".claude" / "skills" / name)
154
+ skill_file = target_path / "SKILL.md"
155
+
156
+ if skill_file.exists():
157
+ console.print(f"[red]Error: Skill already exists at {skill_file}[/red]")
158
+ raise typer.Exit(1)
159
+
160
+ target_path.mkdir(parents=True, exist_ok=True)
161
+
162
+ skill_content = f"""\
163
+ ---
164
+ name: {name}
165
+ description: Description of what this skill does
166
+ ---
167
+
168
+ # {name.replace('-', ' ').title()} Skill
169
+
170
+ Describe what this skill does and when Claude should apply it.
171
+
172
+ ## When to Use
173
+
174
+ Describe the situations when this skill should be applied.
175
+
176
+ ## Instructions
177
+
178
+ Provide specific instructions for Claude to follow.
179
+ """
180
+ skill_file.write_text(skill_content)
181
+ console.print(f"[green]Created skill at {skill_file}[/green]")
182
+
183
+
184
+ @app.command("command")
185
+ def init_command(
186
+ name: Annotated[
187
+ str,
188
+ typer.Argument(
189
+ help="Name of the command to create (without leading slash)",
190
+ metavar="NAME",
191
+ ),
192
+ ],
193
+ path: Annotated[
194
+ Path | None,
195
+ typer.Option(
196
+ "--path",
197
+ "-p",
198
+ help="Custom path (default: ./.claude/commands/<name>.md)",
199
+ ),
200
+ ] = None,
201
+ ) -> None:
202
+ """Create a new slash command scaffold.
203
+
204
+ Examples:
205
+ agr init command my-command
206
+ agr init command deploy --path ./custom/path/
207
+ """
208
+ target_path = path or (Path.cwd() / ".claude" / "commands")
209
+ command_file = target_path / f"{name}.md"
210
+
211
+ if command_file.exists():
212
+ console.print(f"[red]Error: Command already exists at {command_file}[/red]")
213
+ raise typer.Exit(1)
214
+
215
+ target_path.mkdir(parents=True, exist_ok=True)
216
+
217
+ command_content = f"""\
218
+ ---
219
+ description: Description of /{name} command
220
+ ---
221
+
222
+ When the user runs /{name}, do the following:
223
+
224
+ 1. First step
225
+ 2. Second step
226
+ 3. Third step
227
+
228
+ Provide clear, actionable instructions for what Claude should do.
229
+ """
230
+ command_file.write_text(command_content)
231
+ console.print(f"[green]Created command at {command_file}[/green]")
232
+
233
+
234
+ @app.command("agent")
235
+ def init_agent(
236
+ name: Annotated[
237
+ str,
238
+ typer.Argument(
239
+ help="Name of the agent to create",
240
+ metavar="NAME",
241
+ ),
242
+ ],
243
+ path: Annotated[
244
+ Path | None,
245
+ typer.Option(
246
+ "--path",
247
+ "-p",
248
+ help="Custom path (default: ./.claude/agents/<name>.md)",
249
+ ),
250
+ ] = None,
251
+ ) -> None:
252
+ """Create a new sub-agent scaffold.
253
+
254
+ Examples:
255
+ agr init agent my-agent
256
+ agr init agent test-writer --path ./custom/path/
257
+ """
258
+ target_path = path or (Path.cwd() / ".claude" / "agents")
259
+ agent_file = target_path / f"{name}.md"
260
+
261
+ if agent_file.exists():
262
+ console.print(f"[red]Error: Agent already exists at {agent_file}[/red]")
263
+ raise typer.Exit(1)
264
+
265
+ target_path.mkdir(parents=True, exist_ok=True)
266
+
267
+ agent_content = f"""\
268
+ ---
269
+ description: Description of the {name} sub-agent
270
+ ---
271
+
272
+ You are a specialized sub-agent for {name.replace('-', ' ')}.
273
+
274
+ ## Purpose
275
+
276
+ Describe the specific purpose and capabilities of this agent.
277
+
278
+ ## Instructions
279
+
280
+ When invoked, you should:
281
+
282
+ 1. First action
283
+ 2. Second action
284
+ 3. Third action
285
+
286
+ ## Constraints
287
+
288
+ - Constraint 1
289
+ - Constraint 2
290
+ """
291
+ agent_file.write_text(agent_content)
292
+ console.print(f"[green]Created agent at {agent_file}[/green]")
agr/cli/main.py ADDED
@@ -0,0 +1,34 @@
1
+ """Main CLI entrypoint for agr."""
2
+
3
+ import typer
4
+
5
+ from agr.cli.add import app as add_app
6
+ from agr.cli.init import app as init_app
7
+ from agr.cli.remove import app as remove_app
8
+ from agr.cli.sync import sync as sync_command
9
+ from agr.cli.update import app as update_app
10
+
11
+ app = typer.Typer(
12
+ name="agr",
13
+ help="Agent Resources - Install and create Claude Code skills, commands, and agents.",
14
+ add_completion=False,
15
+ no_args_is_help=True,
16
+ )
17
+
18
+ # Register subcommands
19
+ app.add_typer(add_app, name="add")
20
+ app.add_typer(init_app, name="init")
21
+ app.add_typer(remove_app, name="remove")
22
+ app.add_typer(update_app, name="update")
23
+
24
+ # Register sync command
25
+ app.command()(sync_command)
26
+
27
+
28
+ def main() -> None:
29
+ """Main entry point."""
30
+ app()
31
+
32
+
33
+ if __name__ == "__main__":
34
+ main()
agr/cli/remove.py ADDED
@@ -0,0 +1,125 @@
1
+ """Remove subcommand for agr - delete local resources."""
2
+
3
+ from typing import Annotated, List, Optional
4
+
5
+ import typer
6
+ from rich.console import Console
7
+
8
+ from agr.cli.common import handle_remove_bundle, handle_remove_resource, handle_remove_unified
9
+ from agr.fetcher import ResourceType
10
+
11
+ console = Console()
12
+
13
+ # Deprecated subcommand names
14
+ DEPRECATED_SUBCOMMANDS = {"skill", "command", "agent", "bundle"}
15
+
16
+
17
+ def extract_type_from_args(
18
+ args: list[str] | None, explicit_type: str | None
19
+ ) -> tuple[list[str], str | None]:
20
+ """
21
+ Extract --type/-t option from args list if present.
22
+
23
+ When --type or -t appears after the resource name, Typer captures it
24
+ as part of the variadic args list. This function extracts it.
25
+
26
+ Args:
27
+ args: The argument list (may contain --type/-t)
28
+ explicit_type: The resource_type value from Typer (may be None if type was in args)
29
+
30
+ Returns:
31
+ Tuple of (cleaned_args, resource_type)
32
+ """
33
+ if not args or explicit_type is not None:
34
+ return args or [], explicit_type
35
+
36
+ cleaned_args = []
37
+ resource_type = None
38
+ i = 0
39
+ while i < len(args):
40
+ if args[i] in ("--type", "-t") and i + 1 < len(args):
41
+ resource_type = args[i + 1]
42
+ i += 2 # Skip both --type and its value
43
+ else:
44
+ cleaned_args.append(args[i])
45
+ i += 1
46
+
47
+ return cleaned_args, resource_type
48
+
49
+ app = typer.Typer(
50
+ help="Remove skills, commands, or agents.",
51
+ )
52
+
53
+
54
+ @app.callback(invoke_without_command=True)
55
+ def remove_unified(
56
+ ctx: typer.Context,
57
+ args: Annotated[
58
+ Optional[List[str]],
59
+ typer.Argument(help="Name of the resource to remove"),
60
+ ] = None,
61
+ resource_type: Annotated[
62
+ Optional[str],
63
+ typer.Option(
64
+ "--type",
65
+ "-t",
66
+ help="Explicit resource type: skill, command, agent, or bundle",
67
+ ),
68
+ ] = None,
69
+ global_install: Annotated[
70
+ bool,
71
+ typer.Option(
72
+ "--global",
73
+ "-g",
74
+ help="Remove from ~/.claude/ instead of ./.claude/",
75
+ ),
76
+ ] = False,
77
+ ) -> None:
78
+ """Remove a resource from the local installation with auto-detection.
79
+
80
+ Auto-detects the resource type (skill, command, agent) from local files.
81
+ Use --type to explicitly specify when a name exists in multiple types.
82
+
83
+ Examples:
84
+ agr remove hello-world
85
+ agr remove hello-world --type skill
86
+ agr remove hello-world --global
87
+ """
88
+ # Extract --type/-t from args if it was captured there (happens when type comes after name)
89
+ cleaned_args, resource_type = extract_type_from_args(args, resource_type)
90
+
91
+ if not cleaned_args:
92
+ console.print(ctx.get_help())
93
+ raise typer.Exit(0)
94
+
95
+ first_arg = cleaned_args[0]
96
+
97
+ # Handle deprecated subcommand syntax: agr remove skill <name>
98
+ if first_arg in DEPRECATED_SUBCOMMANDS:
99
+ if len(cleaned_args) < 2:
100
+ console.print(f"[red]Error: Missing resource name after '{first_arg}'.[/red]")
101
+ raise typer.Exit(1)
102
+
103
+ name = cleaned_args[1]
104
+ if first_arg == "bundle":
105
+ console.print(
106
+ f"[yellow]Warning: 'agr remove bundle' is deprecated. "
107
+ f"Use 'agr remove {name} --type bundle' instead.[/yellow]"
108
+ )
109
+ handle_remove_bundle(name, global_install)
110
+ else:
111
+ console.print(
112
+ f"[yellow]Warning: 'agr remove {first_arg}' is deprecated. "
113
+ f"Use 'agr remove {name}' instead.[/yellow]"
114
+ )
115
+ if first_arg == "skill":
116
+ handle_remove_resource(name, ResourceType.SKILL, "skills", global_install)
117
+ elif first_arg == "command":
118
+ handle_remove_resource(name, ResourceType.COMMAND, "commands", global_install)
119
+ elif first_arg == "agent":
120
+ handle_remove_resource(name, ResourceType.AGENT, "agents", global_install)
121
+ return
122
+
123
+ # Normal unified remove: agr remove <name>
124
+ name = first_arg
125
+ handle_remove_unified(name, resource_type, global_install)