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