taken-cli 0.1.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.
taken/__init__.py ADDED
File without changes
File without changes
taken/commands/add.py ADDED
@@ -0,0 +1,214 @@
1
+ import re
2
+ from datetime import datetime
3
+ from pathlib import Path
4
+
5
+ import typer
6
+ from rich.panel import Panel
7
+
8
+ from taken.core import paths
9
+ from taken.core.config import is_config_exists, read_config
10
+ from taken.core.editor import open_in_editor
11
+ from taken.core.git import auto_commit_and_push
12
+ from taken.core.registry import read_registry, write_registry
13
+ from taken.core.skills import (
14
+ adopt_skill,
15
+ is_path_argument,
16
+ lookup_lock_entry,
17
+ scaffold_skill,
18
+ )
19
+ from taken.models.config import TakenConfig
20
+ from taken.models.registry import RegistryEntry, SkillSource
21
+ from taken.utils.console import console, err_console
22
+
23
+ _VALID_NAME = re.compile(r"^[a-zA-Z0-9][a-zA-Z0-9_-]*$")
24
+
25
+
26
+ def add(
27
+ skill_or_path: str = typer.Argument(..., help="Skill name to create, or path to existing skill folder to adopt"),
28
+ ) -> None:
29
+ """Add a new personal skill or adopt an existing skill into taken management."""
30
+ if not is_config_exists(paths.TAKEN_HOME):
31
+ err_console.print(
32
+ Panel(
33
+ "Taken is not initialized. Run [bold]taken init[/bold] to get started.",
34
+ title="[red]Not Initialized[/red]",
35
+ border_style="red",
36
+ )
37
+ )
38
+ raise typer.Exit(code=1)
39
+
40
+ config = read_config(paths.TAKEN_HOME)
41
+
42
+ if is_path_argument(skill_or_path):
43
+ _adopt_mode(skill_or_path, config)
44
+ else:
45
+ _create_mode(skill_or_path, config)
46
+
47
+
48
+ def _create_mode(name: str, config: TakenConfig) -> None:
49
+ if "/" in name:
50
+ err_console.print(
51
+ Panel(
52
+ f"Skill name must not contain a namespace. Got: [bold]{name}[/bold]\n"
53
+ f"Your namespace [bold]{config.username}[/bold] is added automatically.\n"
54
+ "Example: [bold]taken add my-skill[/bold]",
55
+ title="[red]Invalid Skill Name[/red]",
56
+ border_style="red",
57
+ )
58
+ )
59
+ raise typer.Exit(code=1)
60
+
61
+ if not _VALID_NAME.match(name):
62
+ err_console.print(
63
+ Panel(
64
+ f"[bold]{name}[/bold] is not a valid skill name.\n"
65
+ "Use lowercase letters, numbers, hyphens, and underscores only.\n"
66
+ "Must start with a letter or number.",
67
+ title="[red]Invalid Skill Name[/red]",
68
+ border_style="red",
69
+ )
70
+ )
71
+ raise typer.Exit(code=1)
72
+
73
+ registry = read_registry(paths.TAKEN_HOME)
74
+ full_name = f"{config.username}/{name}"
75
+
76
+ if registry.exists(full_name):
77
+ err_console.print(
78
+ Panel(
79
+ f"Skill [bold]{full_name}[/bold] is already in your registry.\nUse [bold]taken list[/bold] to see it.",
80
+ title="[red]Already Exists[/red]",
81
+ border_style="red",
82
+ )
83
+ )
84
+ raise typer.Exit(code=1)
85
+
86
+ try:
87
+ skill_md = scaffold_skill(config.username, name, paths.TAKEN_HOME)
88
+ except FileExistsError:
89
+ err_console.print(
90
+ Panel(
91
+ f"Skill directory already exists on disk but is not in the registry.\n"
92
+ f"Path: [dim]{paths.TAKEN_HOME}/skills/{config.username}/{name}[/dim]\n"
93
+ "Run [bold]taken doctor[/bold] to repair registry drift.",
94
+ title="[red]Directory Conflict[/red]",
95
+ border_style="red",
96
+ )
97
+ )
98
+ raise typer.Exit(code=1) from None
99
+
100
+ now = datetime.now()
101
+ entry = RegistryEntry(
102
+ namespace=config.username,
103
+ name=name,
104
+ source=SkillSource.PERSONAL,
105
+ version="1",
106
+ created_at=now,
107
+ updated_at=now,
108
+ )
109
+ registry.add(entry)
110
+ write_registry(registry, paths.TAKEN_HOME)
111
+ auto_commit_and_push(paths.TAKEN_HOME, f"add: {full_name}")
112
+
113
+ console.print(
114
+ Panel(
115
+ f"[green]✓[/green] Created [bold]{full_name}[/bold]\n"
116
+ f"[green]✓[/green] Registered as [bold]personal[/bold] skill\n\n"
117
+ f"[dim]Opening editor…[/dim]",
118
+ title="[green]Skill Created[/green]",
119
+ border_style="green",
120
+ padding=(1, 2),
121
+ )
122
+ )
123
+
124
+ open_in_editor(skill_md)
125
+
126
+
127
+ def _adopt_mode(path_str: str, config: TakenConfig) -> None:
128
+ source_dir = Path(path_str).resolve()
129
+
130
+ if not source_dir.is_dir():
131
+ err_console.print(
132
+ Panel(
133
+ f"[bold]{path_str}[/bold] is not a directory.",
134
+ title="[red]Invalid Path[/red]",
135
+ border_style="red",
136
+ )
137
+ )
138
+ raise typer.Exit(code=1)
139
+
140
+ name = source_dir.name
141
+ lock_entry = lookup_lock_entry(name, Path.cwd())
142
+
143
+ if lock_entry is not None:
144
+ namespace = lock_entry.source.split("/")[0]
145
+ source = SkillSource.NPX
146
+ repo = lock_entry.source
147
+ version = lock_entry.ref
148
+ source_url = lock_entry.source_url
149
+ skill_path = lock_entry.skill_path
150
+ skill_folder_hash = lock_entry.skill_folder_hash
151
+ else:
152
+ namespace = config.username
153
+ source = SkillSource.PERSONAL
154
+ repo = None
155
+ version = None
156
+ source_url = None
157
+ skill_path = None
158
+ skill_folder_hash = None
159
+
160
+ registry = read_registry(paths.TAKEN_HOME)
161
+ full_name = f"{namespace}/{name}"
162
+
163
+ if registry.exists(full_name):
164
+ err_console.print(
165
+ Panel(
166
+ f"Skill [bold]{full_name}[/bold] is already in your registry.\nUse [bold]taken list[/bold] to see it.",
167
+ title="[red]Already Exists[/red]",
168
+ border_style="red",
169
+ )
170
+ )
171
+ raise typer.Exit(code=1)
172
+
173
+ try:
174
+ adopt_skill(source_dir, namespace, name, paths.TAKEN_HOME)
175
+ except FileExistsError:
176
+ err_console.print(
177
+ Panel(
178
+ f"Skill directory already exists at [dim]{paths.TAKEN_HOME}/skills/{namespace}/{name}[/dim]\n"
179
+ "Run [bold]taken doctor[/bold] to repair registry drift.",
180
+ title="[red]Directory Conflict[/red]",
181
+ border_style="red",
182
+ )
183
+ )
184
+ raise typer.Exit(code=1) from None
185
+
186
+ now = datetime.now()
187
+ entry = RegistryEntry(
188
+ namespace=namespace,
189
+ name=name,
190
+ source=source,
191
+ repo=repo,
192
+ version=version,
193
+ installed_at=now if source == SkillSource.NPX else None,
194
+ created_at=now if source == SkillSource.PERSONAL else None,
195
+ updated_at=now,
196
+ source_url=source_url,
197
+ skill_path=skill_path,
198
+ skill_folder_hash=skill_folder_hash,
199
+ )
200
+ registry.add(entry)
201
+ write_registry(registry, paths.TAKEN_HOME)
202
+ auto_commit_and_push(paths.TAKEN_HOME, f"add: {full_name}")
203
+
204
+ source_detail = f" ({repo})" if repo else ""
205
+ console.print(
206
+ Panel(
207
+ f"[green]✓[/green] Adopted [bold]{name}[/bold] → [bold]{full_name}[/bold]\n"
208
+ f"[green]✓[/green] Source: [bold]{source.value}[/bold]{source_detail}\n\n"
209
+ f"[dim]Copied to ~/.taken/skills/{namespace}/{name}/[/dim]",
210
+ title="[green]Skill Adopted[/green]",
211
+ border_style="green",
212
+ padding=(1, 2),
213
+ )
214
+ )
taken/commands/git.py ADDED
@@ -0,0 +1,10 @@
1
+ import typer
2
+
3
+ from taken.core import paths
4
+ from taken.core.git import run_passthrough
5
+
6
+
7
+ def git(ctx: typer.Context) -> None:
8
+ """Run a git command in the taken home directory (~/.taken/)."""
9
+ returncode = run_passthrough(paths.TAKEN_HOME, ctx.args)
10
+ raise SystemExit(returncode)
taken/commands/init.py ADDED
@@ -0,0 +1,171 @@
1
+ import shutil
2
+ import subprocess
3
+ from datetime import datetime
4
+
5
+ import typer
6
+ from rich.panel import Panel
7
+ from rich.prompt import Confirm, Prompt
8
+
9
+ from taken.core import paths
10
+ from taken.core.config import is_config_exists, write_config
11
+ from taken.core.git import init_repo
12
+ from taken.core.registry import write_registry
13
+ from taken.models.config import TakenConfig
14
+ from taken.models.registry import Registry
15
+ from taken.utils.console import console, err_console
16
+
17
+
18
+ def _resolve_username() -> str:
19
+ """
20
+ Prompt the user to choose their namespace username via sequential prompts.
21
+ Options: system username, git config name, or manual override.
22
+ """
23
+ # Gather candidates
24
+ import getpass
25
+
26
+ whoami = getpass.getuser()
27
+
28
+ git_name: str | None = None
29
+ try:
30
+ result = subprocess.run(
31
+ ["git", "config", "user.name"],
32
+ capture_output=True,
33
+ text=True,
34
+ timeout=3,
35
+ )
36
+ git_name = result.stdout.strip() or None
37
+ except (FileNotFoundError, subprocess.TimeoutExpired):
38
+ git_name = None
39
+
40
+ # Present options
41
+ console.print("\n[bold]Choose your skill namespace username:[/bold]")
42
+ console.print(f" [cyan]1[/cyan] System username → [green]{whoami}[/green]")
43
+
44
+ if git_name:
45
+ console.print(f" [cyan]2[/cyan] Git config name → [green]{git_name}[/green]")
46
+ console.print(" [cyan]3[/cyan] Enter manually")
47
+ else:
48
+ console.print(" [cyan]2[/cyan] Enter manually [dim](git config name not found)[/dim]")
49
+
50
+ choice = Prompt.ask(
51
+ "\nPick an option",
52
+ choices=["1", "2", "3"] if git_name else ["1", "2"],
53
+ default="1",
54
+ )
55
+
56
+ if choice == "1":
57
+ return whoami
58
+ elif choice == "2" and git_name:
59
+ return git_name
60
+ else:
61
+ # Manual override
62
+ username = Prompt.ask("Enter your username").strip()
63
+ if not username:
64
+ err_console.print(
65
+ Panel(
66
+ "Username cannot be empty.",
67
+ title="[red]Error[/red]",
68
+ border_style="red",
69
+ )
70
+ )
71
+ raise typer.Exit(code=1)
72
+ return username
73
+
74
+
75
+ def _handle_existing_init() -> bool:
76
+ """
77
+ Handle the case where ~/.taken/ already exists.
78
+ Prompts user to choose between resetting config only or full wipe.
79
+ Returns True if we should proceed, False if user aborted.
80
+ """
81
+ err_console.print(
82
+ Panel(
83
+ "[yellow]Taken is already initialized at[/yellow] [bold]~/.taken/[/bold]\n\n"
84
+ "Reinitializing will overwrite your config. Choose carefully.",
85
+ title="[yellow]Already Initialized[/yellow]",
86
+ border_style="yellow",
87
+ )
88
+ )
89
+
90
+ proceed = Confirm.ask("Do you want to reinitialize?", default=False)
91
+ if not proceed:
92
+ console.print("[dim]Aborted. Nothing was changed.[/dim]")
93
+ return False
94
+
95
+ # Give user two options
96
+ console.print("\n[bold]What would you like to reset?[/bold]")
97
+ console.print(" [cyan]1[/cyan] Reset config only [dim](keeps your skills and registry intact)[/dim]")
98
+ console.print(" [cyan]2[/cyan] Full wipe [dim](deletes everything — skills, registry, config)[/dim]")
99
+
100
+ choice = Prompt.ask("\nPick an option", choices=["1", "2"], default="1")
101
+
102
+ if choice == "2":
103
+ confirm_wipe = Confirm.ask(
104
+ "[red]This will delete ALL your skills and registry. Are you sure?[/red]",
105
+ default=False,
106
+ )
107
+ if not confirm_wipe:
108
+ console.print("[dim]Aborted. Nothing was changed.[/dim]")
109
+ return False
110
+
111
+ # Full wipe — delete ~/.taken/ entirely
112
+ shutil.rmtree(paths.TAKEN_HOME)
113
+ console.print("[dim]Wiped ~/.taken/ — starting fresh.[/dim]")
114
+
115
+ # Reset config only — just let init proceed and overwrite config.yaml
116
+ return True
117
+
118
+
119
+ def init() -> None:
120
+ """
121
+ Initialize Taken — sets up ~/.taken/ with config.yaml and registry.yaml.
122
+ """
123
+ console.print(
124
+ Panel(
125
+ "[bold white]Welcome to Taken[/bold white]\n[dim]A very particular set of skills, managed.[/dim]",
126
+ border_style="bright_blue",
127
+ padding=(1, 4),
128
+ )
129
+ )
130
+
131
+ # Handle existing init
132
+ if is_config_exists(paths.TAKEN_HOME):
133
+ should_proceed = _handle_existing_init()
134
+ if not should_proceed:
135
+ raise typer.Exit(code=0)
136
+
137
+ # Resolve username
138
+ username = _resolve_username()
139
+
140
+ # Create directory structure
141
+ skills_dir = paths.TAKEN_HOME / "skills" / username
142
+ skills_dir.mkdir(parents=True, exist_ok=True)
143
+
144
+ # Write config
145
+ config = TakenConfig(
146
+ username=username,
147
+ taken_home=paths.TAKEN_HOME,
148
+ initialized_at=datetime.now(),
149
+ )
150
+ write_config(config)
151
+
152
+ # Write empty registry (only if it doesn't exist — preserve existing on config-only reset)
153
+ from taken.core.registry import is_registry_exists
154
+
155
+ if not is_registry_exists(paths.TAKEN_HOME):
156
+ write_registry(Registry(), paths.TAKEN_HOME)
157
+
158
+ # Initialize git repo and make initial commit
159
+ init_repo(paths.TAKEN_HOME)
160
+
161
+ # Success
162
+ console.print(
163
+ Panel(
164
+ f"[green]✓[/green] Initialized at [bold]~/.taken/[/bold]\n"
165
+ f"[green]✓[/green] Username set to [bold]{username}[/bold]\n\n"
166
+ f"[dim]Your personal skills namespace: [bold]{username}/<skill-name>[/bold][/dim]",
167
+ title="[green]Taken Initialized[/green]",
168
+ border_style="green",
169
+ padding=(1, 2),
170
+ )
171
+ )
@@ -0,0 +1,227 @@
1
+ import shutil
2
+ from datetime import datetime
3
+
4
+ import typer
5
+ from InquirerPy import inquirer
6
+ from InquirerPy.enum import INQUIRERPY_EMPTY_CIRCLE_SEQUENCE, INQUIRERPY_FILL_CIRCLE_SEQUENCE
7
+ from rich.panel import Panel
8
+
9
+ from taken.core import paths
10
+ from taken.core.config import is_config_exists
11
+ from taken.core.git import auto_commit_and_push
12
+ from taken.core.github import (
13
+ GitHubSkill,
14
+ discover_skills,
15
+ download_skill,
16
+ get_commit_sha,
17
+ get_default_branch,
18
+ normalize_source,
19
+ parse_source,
20
+ )
21
+ from taken.core.registry import read_registry, write_registry
22
+ from taken.models.registry import RegistryEntry, SkillSource, VersionPin
23
+ from taken.utils.console import console, err_console
24
+
25
+
26
+ def _select_skills(skills: list[GitHubSkill]) -> list[GitHubSkill]:
27
+ if len(skills) == 1:
28
+ return skills
29
+
30
+ choices = [{"name": s.name, "value": s} for s in skills]
31
+ selected: list[GitHubSkill] = inquirer.fuzzy( # type: ignore[attr-defined]
32
+ message="Select skills to install:",
33
+ choices=choices,
34
+ multiselect=True,
35
+ marker=INQUIRERPY_FILL_CIRCLE_SEQUENCE,
36
+ marker_pl=INQUIRERPY_EMPTY_CIRCLE_SEQUENCE,
37
+ instruction="(type to filter space/tab to select enter to confirm)",
38
+ keybindings={"toggle": [{"key": "space"}, {"key": "tab"}]},
39
+ validate=lambda x: len(x) > 0,
40
+ invalid_message="Select at least one skill.",
41
+ ).execute()
42
+ return selected
43
+
44
+
45
+ def _filter_skills(
46
+ skills: list[GitHubSkill],
47
+ skill_filter: list[str],
48
+ owner: str,
49
+ repo: str,
50
+ ) -> list[GitHubSkill]:
51
+ """Apply name filter; raises typer.Exit(1) if no skills match."""
52
+ if not skill_filter:
53
+ return skills
54
+ filtered = [s for s in skills if s.name in skill_filter]
55
+ if not filtered:
56
+ available = ", ".join(s.name for s in skills)
57
+ err_console.print(
58
+ Panel(
59
+ f"No skill named [bold]{', '.join(skill_filter)}[/bold] found in [bold]{owner}/{repo}[/bold].\n"
60
+ f"Available: [dim]{available}[/dim]",
61
+ title="[red]Skill Not Found[/red]",
62
+ border_style="red",
63
+ )
64
+ )
65
+ raise typer.Exit(code=1)
66
+ return filtered
67
+
68
+
69
+ def _print_install_results(
70
+ installed: list[str],
71
+ skipped: list[str],
72
+ owner: str,
73
+ repo: str,
74
+ sha: str,
75
+ pin: bool,
76
+ ) -> None:
77
+ if installed:
78
+ lines = "\n".join(f"[green]✓[/green] [bold]{n}[/bold]" for n in installed)
79
+ lines += f"\n\n[dim]{owner}/{repo} @ {sha[:8]}[/dim]"
80
+ if pin:
81
+ lines += "\n[dim]Pinned to commit[/dim]"
82
+ console.print(Panel(lines, title="[green]Skills Installed[/green]", border_style="green", padding=(1, 2)))
83
+ if skipped:
84
+ console.print(f"[dim]Already in registry (skipped): {', '.join(skipped)}[/dim]")
85
+ if not installed and not skipped:
86
+ console.print("[dim]Nothing installed.[/dim]")
87
+
88
+
89
+ def _fetch_github_skills(owner: str, repo: str, ref: str) -> tuple[str, list[GitHubSkill]]:
90
+ """Resolve ref to a commit SHA and discover skills in the repo."""
91
+ resolved_ref = ref if ref else get_default_branch(owner, repo)
92
+ sha = get_commit_sha(owner, repo, resolved_ref)
93
+ skills = discover_skills(owner, repo, sha)
94
+ return sha, skills
95
+
96
+
97
+ def _install_skills(
98
+ selected: list[GitHubSkill],
99
+ owner: str,
100
+ repo: str,
101
+ sha: str,
102
+ pin: bool,
103
+ ) -> tuple[list[str], list[str]]:
104
+ """Download and register each selected skill; return (installed, skipped)."""
105
+ registry = read_registry(paths.TAKEN_HOME)
106
+ installed: list[str] = []
107
+ skipped: list[str] = []
108
+
109
+ for gh_skill in selected:
110
+ full_name = f"{owner}/{gh_skill.name}"
111
+ dest = paths.TAKEN_HOME / "skills" / owner / gh_skill.name
112
+
113
+ if registry.exists(full_name) or dest.exists():
114
+ skipped.append(full_name)
115
+ continue
116
+
117
+ try:
118
+ with console.status(f"[dim]Downloading {full_name}…[/dim]"):
119
+ download_skill(owner, repo, gh_skill.skill_path, sha, dest)
120
+ except Exception as e:
121
+ err_console.print(Panel(str(e), title=f"[red]Failed: {full_name}[/red]", border_style="red"))
122
+ if dest.exists():
123
+ shutil.rmtree(dest)
124
+ continue
125
+
126
+ now = datetime.now()
127
+ entry = RegistryEntry(
128
+ namespace=owner,
129
+ name=gh_skill.name,
130
+ source=SkillSource.TAKEN,
131
+ repo=f"{owner}/{repo}",
132
+ version=sha,
133
+ pin=VersionPin.PINNED if pin else VersionPin.FLOATING,
134
+ installed_at=now,
135
+ updated_at=now,
136
+ source_url=f"https://github.com/{owner}/{repo}",
137
+ skill_path=gh_skill.skill_path or None,
138
+ skill_folder_hash=gh_skill.skill_folder_hash or None,
139
+ )
140
+ registry.add(entry)
141
+ installed.append(full_name)
142
+
143
+ if installed:
144
+ write_registry(registry, paths.TAKEN_HOME)
145
+
146
+ return installed, skipped
147
+
148
+
149
+ def install(
150
+ source: str = typer.Argument(
151
+ ...,
152
+ metavar="source",
153
+ help="GitHub repo, URL, or npx skills add command. E.g. vercel-labs/agent-skills",
154
+ ),
155
+ skill: list[str] = typer.Option( # noqa: B008
156
+ [],
157
+ "--skill",
158
+ "-s",
159
+ help="Skill name(s) to install (repeatable). Alternative to owner/repo/skill path form.",
160
+ ),
161
+ ref: str = typer.Option(
162
+ "", "--ref", metavar="REF", help="Branch, tag, or commit SHA (default: repo default branch)."
163
+ ),
164
+ pin: bool = typer.Option(False, "--pin", help="Pin to the exact commit SHA."),
165
+ ) -> None:
166
+ """Install one or more skills from a GitHub repository."""
167
+ if not is_config_exists(paths.TAKEN_HOME):
168
+ err_console.print(
169
+ Panel(
170
+ "Taken is not initialized. Run [bold]taken init[/bold] to get started.",
171
+ title="[red]Not Initialized[/red]",
172
+ border_style="red",
173
+ )
174
+ )
175
+ raise typer.Exit(code=1)
176
+
177
+ try:
178
+ normalized = normalize_source(source)
179
+ owner, repo, path_filter = parse_source(normalized)
180
+ except ValueError as e:
181
+ err_console.print(Panel(str(e), title="[red]Invalid Source[/red]", border_style="red"))
182
+ raise typer.Exit(code=1) from None
183
+
184
+ # Merge skill selection: --skill flag wins; path filter is fallback; empty = all
185
+ skill_filter: list[str] = list(skill) or ([path_filter] if path_filter else [])
186
+
187
+ try:
188
+ with console.status("[dim]Contacting GitHub…[/dim]"):
189
+ sha, skills = _fetch_github_skills(owner, repo, ref)
190
+ except FileNotFoundError:
191
+ err_console.print(
192
+ Panel(
193
+ f"Repository [bold]{owner}/{repo}[/bold] not found on GitHub.\nCheck the name and try again.",
194
+ title="[red]Not Found[/red]",
195
+ border_style="red",
196
+ )
197
+ )
198
+ raise typer.Exit(code=1) from None
199
+ except PermissionError as e:
200
+ err_console.print(
201
+ Panel(
202
+ f"{e}\n\nTip: set [bold]GITHUB_TOKEN[/bold] in your environment for higher rate limits.",
203
+ title="[red]GitHub API Error[/red]",
204
+ border_style="red",
205
+ )
206
+ )
207
+ raise typer.Exit(code=1) from e
208
+ except Exception as e:
209
+ err_console.print(Panel(str(e), title="[red]Error[/red]", border_style="red"))
210
+ raise typer.Exit(code=1) from e
211
+
212
+ if not skills:
213
+ err_console.print(
214
+ Panel(
215
+ f"No skills (SKILL.md files) found in [bold]{owner}/{repo}[/bold].",
216
+ title="[red]No Skills Found[/red]",
217
+ border_style="red",
218
+ )
219
+ )
220
+ raise typer.Exit(code=1)
221
+
222
+ skills = _filter_skills(skills, skill_filter, owner, repo)
223
+ selected = skills if skill_filter else _select_skills(skills)
224
+ installed, skipped = _install_skills(selected, owner, repo, sha, pin)
225
+ if installed:
226
+ auto_commit_and_push(paths.TAKEN_HOME, f"install: {' '.join(installed)}")
227
+ _print_install_results(installed, skipped, owner, repo, sha, pin)
taken/commands/list.py ADDED
@@ -0,0 +1,57 @@
1
+ import typer
2
+ from rich.box import SIMPLE
3
+ from rich.panel import Panel
4
+ from rich.table import Table
5
+
6
+ from taken.core import paths
7
+ from taken.core.config import is_config_exists
8
+ from taken.core.registry import read_registry
9
+ from taken.models.registry import SkillSource
10
+ from taken.utils.console import console, err_console
11
+
12
+ _SOURCE_STYLE: dict[SkillSource, str] = {
13
+ SkillSource.PERSONAL: "green",
14
+ SkillSource.NPX: "blue",
15
+ SkillSource.TAKEN: "magenta",
16
+ }
17
+
18
+
19
+ def list() -> None:
20
+ """List all skills in the registry."""
21
+ if not is_config_exists(paths.TAKEN_HOME):
22
+ err_console.print(
23
+ Panel(
24
+ "Taken is not initialized. Run [bold]taken init[/bold] to get started.",
25
+ title="[red]Not Initialized[/red]",
26
+ border_style="red",
27
+ )
28
+ )
29
+ raise typer.Exit(code=1)
30
+
31
+ registry = read_registry(paths.TAKEN_HOME)
32
+
33
+ if not registry.skills:
34
+ console.print("[yellow]No skills registered yet. Use [bold]taken add[/bold] to create one.[/yellow]")
35
+ return
36
+
37
+ table = Table(show_header=True, header_style="bold", box=SIMPLE, padding=(0, 1))
38
+ table.add_column("Skill", style="bold cyan", no_wrap=True)
39
+ table.add_column("Source")
40
+ table.add_column("Version")
41
+ table.add_column("Date")
42
+
43
+ for entry in sorted(registry.skills.values(), key=lambda e: e.full_name):
44
+ style = _SOURCE_STYLE.get(entry.source, "")
45
+ version = entry.version[:8] if entry.version else "—"
46
+ date = entry.created_at or entry.installed_at
47
+ date_str = date.strftime("%Y-%m-%d") if date else "—"
48
+ table.add_row(
49
+ entry.full_name,
50
+ f"[{style}]{entry.source.value}[/{style}]",
51
+ version,
52
+ date_str,
53
+ )
54
+
55
+ console.print(table)
56
+ count = len(registry.skills)
57
+ console.print(f"[dim]{count} skill{'s' if count != 1 else ''}[/dim]")