agnostic-prompt-aps 1.1.5__py3-none-any.whl → 1.1.7__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.
- {agnostic_prompt_aps-1.1.5.dist-info → agnostic_prompt_aps-1.1.7.dist-info}/METADATA +2 -1
- {agnostic_prompt_aps-1.1.5.dist-info → agnostic_prompt_aps-1.1.7.dist-info}/RECORD +18 -14
- {agnostic_prompt_aps-1.1.5.dist-info → agnostic_prompt_aps-1.1.7.dist-info}/WHEEL +1 -1
- aps_cli/__init__.py +1 -1
- aps_cli/__main__.py +1 -0
- aps_cli/cli.py +505 -124
- aps_cli/core.py +222 -13
- aps_cli/payload/agnostic-prompt-standard/SKILL.md +2 -1
- aps_cli/payload/agnostic-prompt-standard/assets/formats/format-docs-index-v1.0.0.example.md +25 -0
- aps_cli/payload/agnostic-prompt-standard/platforms/_schemas/platform-manifest.schema.json +6 -2
- aps_cli/payload/agnostic-prompt-standard/platforms/claude-code/manifest.json +26 -1
- aps_cli/payload/agnostic-prompt-standard/platforms/opencode/manifest.json +32 -0
- aps_cli/payload/agnostic-prompt-standard/platforms/opencode/tools-registry.json +9 -0
- aps_cli/payload/agnostic-prompt-standard/platforms/vscode-copilot/manifest.json +24 -1
- aps_cli/schemas.py +170 -0
- {agnostic_prompt_aps-1.1.5.dist-info → agnostic_prompt_aps-1.1.7.dist-info}/entry_points.txt +0 -0
- {agnostic_prompt_aps-1.1.5.dist-info → agnostic_prompt_aps-1.1.7.dist-info}/licenses/LICENSE +0 -0
- {agnostic_prompt_aps-1.1.5.dist-info → agnostic_prompt_aps-1.1.7.dist-info}/top_level.txt +0 -0
aps_cli/cli.py
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
+
from dataclasses import dataclass
|
|
4
5
|
from pathlib import Path
|
|
5
|
-
from typing import Optional
|
|
6
|
+
from typing import Literal, Optional
|
|
6
7
|
|
|
7
8
|
import questionary
|
|
8
9
|
import typer
|
|
@@ -11,34 +12,239 @@ from rich.table import Table
|
|
|
11
12
|
|
|
12
13
|
from . import __version__
|
|
13
14
|
from .core import (
|
|
14
|
-
|
|
15
|
+
AdapterDetection,
|
|
16
|
+
Platform,
|
|
17
|
+
compute_skill_destinations,
|
|
15
18
|
copy_dir,
|
|
16
19
|
copy_template_tree,
|
|
17
20
|
default_personal_skill_path,
|
|
18
21
|
default_project_skill_path,
|
|
22
|
+
detect_adapters,
|
|
19
23
|
ensure_dir,
|
|
20
24
|
find_repo_root,
|
|
21
|
-
|
|
25
|
+
format_detection_label,
|
|
26
|
+
is_claude_platform,
|
|
22
27
|
is_tty,
|
|
28
|
+
list_files_recursive,
|
|
23
29
|
load_platforms,
|
|
30
|
+
pick_workspace_root,
|
|
24
31
|
remove_dir,
|
|
25
32
|
resolve_payload_skill_dir,
|
|
33
|
+
sort_platforms_for_ui,
|
|
34
|
+
SKILL_ID,
|
|
26
35
|
)
|
|
27
36
|
|
|
28
37
|
app = typer.Typer(add_completion=False)
|
|
29
38
|
console = Console()
|
|
30
39
|
|
|
31
40
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
41
|
+
@app.callback(invoke_without_command=True)
|
|
42
|
+
def _root(
|
|
43
|
+
ctx: typer.Context,
|
|
44
|
+
version: bool = typer.Option(
|
|
45
|
+
False,
|
|
46
|
+
"--version",
|
|
47
|
+
help="Print CLI version",
|
|
48
|
+
is_eager=True,
|
|
49
|
+
),
|
|
50
|
+
) -> None:
|
|
51
|
+
"""Root command for the APS CLI."""
|
|
52
|
+
|
|
53
|
+
if version:
|
|
54
|
+
# Match Node: `aps --version` prints version and exits.
|
|
55
|
+
typer.echo(__version__)
|
|
56
|
+
raise typer.Exit()
|
|
57
|
+
|
|
58
|
+
# Match Node: invoking `aps` with no subcommand prints help and exits with code 2.
|
|
59
|
+
if ctx.invoked_subcommand is None:
|
|
60
|
+
typer.echo(ctx.get_help(), err=True)
|
|
61
|
+
raise typer.Exit(code=2)
|
|
62
|
+
|
|
63
|
+
InstallScope = Literal["repo", "personal"]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _normalize_platform_args(platforms: Optional[list[str]]) -> Optional[list[str]]:
|
|
67
|
+
"""Normalize platform arguments, handling 'none' and comma-separated values."""
|
|
68
|
+
if not platforms:
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
# Flatten comma-separated values
|
|
72
|
+
raw: list[str] = []
|
|
73
|
+
for v in platforms:
|
|
74
|
+
raw.extend(s.strip() for s in v.split(",") if s.strip())
|
|
75
|
+
|
|
76
|
+
if any(v.lower() == "none" for v in raw):
|
|
77
|
+
return []
|
|
78
|
+
|
|
79
|
+
# Deduplicate while preserving order
|
|
80
|
+
seen: set[str] = set()
|
|
81
|
+
out: list[str] = []
|
|
82
|
+
for v in raw:
|
|
83
|
+
if v not in seen:
|
|
84
|
+
seen.add(v)
|
|
85
|
+
out.append(v)
|
|
86
|
+
return out
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _fmt_path(p: Path) -> str:
|
|
90
|
+
"""Format path for display, replacing home with ~."""
|
|
91
|
+
home = str(Path.home())
|
|
92
|
+
s = str(p)
|
|
93
|
+
if s.startswith(home):
|
|
94
|
+
return "~" + s[len(home) :]
|
|
95
|
+
return s
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _select_all_choice_label() -> str:
|
|
99
|
+
return "Select all adapters"
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _platform_display_name(platform: Platform) -> str:
|
|
103
|
+
"""Get display name for platform in UI."""
|
|
104
|
+
return f"{platform.display_name} ({platform.platform_id})"
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _detection_for(
|
|
108
|
+
platform_id: str, detections: dict[str, AdapterDetection]
|
|
109
|
+
) -> Optional[AdapterDetection]:
|
|
110
|
+
"""Get detection result for a platform ID."""
|
|
111
|
+
return detections.get(platform_id)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@dataclass
|
|
115
|
+
class PlannedTemplateFile:
|
|
116
|
+
rel_path: str
|
|
117
|
+
dst_path: Path
|
|
118
|
+
exists: bool
|
|
119
|
+
will_write: bool
|
|
120
|
+
|
|
38
121
|
|
|
122
|
+
@dataclass
|
|
123
|
+
class PlannedPlatformTemplates:
|
|
124
|
+
platform_id: str
|
|
125
|
+
templates_dir: Path
|
|
126
|
+
template_root: Path
|
|
127
|
+
files: list[PlannedTemplateFile]
|
|
39
128
|
|
|
40
|
-
|
|
41
|
-
|
|
129
|
+
|
|
130
|
+
@dataclass
|
|
131
|
+
class PlannedSkillInstall:
|
|
132
|
+
dst: Path
|
|
133
|
+
exists: bool
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@dataclass
|
|
137
|
+
class InitPlan:
|
|
138
|
+
scope: InstallScope
|
|
139
|
+
workspace_root: Optional[Path]
|
|
140
|
+
selected_platforms: list[str]
|
|
141
|
+
payload_skill_dir: Path
|
|
142
|
+
skills: list[PlannedSkillInstall]
|
|
143
|
+
templates: list[PlannedPlatformTemplates]
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _plan_platform_templates(
|
|
147
|
+
payload_skill_dir: Path,
|
|
148
|
+
scope: InstallScope,
|
|
149
|
+
workspace_root: Optional[Path],
|
|
150
|
+
selected_platforms: list[str],
|
|
151
|
+
force: bool,
|
|
152
|
+
) -> list[PlannedPlatformTemplates]:
|
|
153
|
+
"""Plan template files to be copied for selected platforms."""
|
|
154
|
+
template_root = Path.home() if scope == "personal" else workspace_root
|
|
155
|
+
if not template_root:
|
|
156
|
+
return []
|
|
157
|
+
|
|
158
|
+
plans: list[PlannedPlatformTemplates] = []
|
|
159
|
+
|
|
160
|
+
for platform_id in selected_platforms:
|
|
161
|
+
templates_dir = payload_skill_dir / "platforms" / platform_id / "templates"
|
|
162
|
+
if not templates_dir.is_dir():
|
|
163
|
+
continue
|
|
164
|
+
|
|
165
|
+
all_files = list_files_recursive(templates_dir)
|
|
166
|
+
|
|
167
|
+
def filter_fn(rel_path: str) -> bool:
|
|
168
|
+
# Skip .github/** for personal installs
|
|
169
|
+
if scope == "personal" and rel_path.startswith(".github"):
|
|
170
|
+
return False
|
|
171
|
+
return True
|
|
172
|
+
|
|
173
|
+
files: list[PlannedTemplateFile] = []
|
|
174
|
+
for src in all_files:
|
|
175
|
+
rel_path = str(src.relative_to(templates_dir)).replace("\\", "/")
|
|
176
|
+
if not filter_fn(rel_path):
|
|
177
|
+
continue
|
|
178
|
+
|
|
179
|
+
dst_path = template_root / rel_path
|
|
180
|
+
exists = dst_path.exists()
|
|
181
|
+
files.append(
|
|
182
|
+
PlannedTemplateFile(
|
|
183
|
+
rel_path=rel_path,
|
|
184
|
+
dst_path=dst_path,
|
|
185
|
+
exists=exists,
|
|
186
|
+
will_write=not exists or force,
|
|
187
|
+
)
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
plans.append(
|
|
191
|
+
PlannedPlatformTemplates(
|
|
192
|
+
platform_id=platform_id,
|
|
193
|
+
templates_dir=templates_dir,
|
|
194
|
+
template_root=template_root,
|
|
195
|
+
files=files,
|
|
196
|
+
)
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
return plans
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _render_plan(plan: InitPlan, force: bool) -> str:
|
|
203
|
+
"""Render plan as human-readable text."""
|
|
204
|
+
lines: list[str] = []
|
|
205
|
+
|
|
206
|
+
lines.append("Selected adapters:")
|
|
207
|
+
if not plan.selected_platforms:
|
|
208
|
+
lines.append(" (none)")
|
|
209
|
+
else:
|
|
210
|
+
for p in plan.selected_platforms:
|
|
211
|
+
lines.append(f" - {p}")
|
|
212
|
+
lines.append("")
|
|
213
|
+
|
|
214
|
+
lines.append("Skill install destinations:")
|
|
215
|
+
for s in plan.skills:
|
|
216
|
+
status = (
|
|
217
|
+
"overwrite" if s.exists and force else "overwrite (needs confirmation)" if s.exists else "create"
|
|
218
|
+
)
|
|
219
|
+
lines.append(f" - {_fmt_path(s.dst)} [{status}]")
|
|
220
|
+
lines.append("")
|
|
221
|
+
|
|
222
|
+
if not plan.templates:
|
|
223
|
+
lines.append("Platform templates: (none)")
|
|
224
|
+
return "\n".join(lines)
|
|
225
|
+
|
|
226
|
+
lines.append("Platform templates:")
|
|
227
|
+
for t in plan.templates:
|
|
228
|
+
will_write = sum(1 for f in t.files if f.will_write)
|
|
229
|
+
skipped = len(t.files) - will_write
|
|
230
|
+
skip_msg = f", {skipped} skipped (exists)" if skipped > 0 else ""
|
|
231
|
+
lines.append(f" - {t.platform_id}: {will_write} file(s) to write{skip_msg}")
|
|
232
|
+
|
|
233
|
+
preview = [f for f in t.files if f.will_write][:30]
|
|
234
|
+
for f in preview:
|
|
235
|
+
lines.append(f" {f.rel_path}")
|
|
236
|
+
if will_write > 30:
|
|
237
|
+
lines.append(" ...")
|
|
238
|
+
|
|
239
|
+
return "\n".join(lines)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _render_empty_platform_warning() -> str:
|
|
243
|
+
"""Return warning message for empty platform selection."""
|
|
244
|
+
return (
|
|
245
|
+
"Note: No platform adapters selected. Only the APS skill will be installed.\n"
|
|
246
|
+
" Templates will not be copied. Use --platform <id> to include platform templates."
|
|
247
|
+
)
|
|
42
248
|
|
|
43
249
|
|
|
44
250
|
@app.command()
|
|
@@ -48,170 +254,344 @@ def init(
|
|
|
48
254
|
"--root",
|
|
49
255
|
help="Workspace root to install project skill under (defaults to repo root or cwd)",
|
|
50
256
|
),
|
|
51
|
-
repo: bool = typer.Option(
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
257
|
+
repo: bool = typer.Option(
|
|
258
|
+
False,
|
|
259
|
+
"--repo",
|
|
260
|
+
help="Force install as project skill (workspace/.github/skills or workspace/.claude/skills)",
|
|
261
|
+
),
|
|
262
|
+
personal: bool = typer.Option(
|
|
263
|
+
False,
|
|
264
|
+
"--personal",
|
|
265
|
+
help="Force install as personal skill (~/.copilot/skills or ~/.claude/skills)",
|
|
266
|
+
),
|
|
267
|
+
platform: Optional[list[str]] = typer.Option(
|
|
268
|
+
None,
|
|
269
|
+
"--platform",
|
|
270
|
+
help='Platform adapter(s) to apply (e.g. vscode-copilot, claude-code). Use "none" to skip platform templates.',
|
|
271
|
+
),
|
|
272
|
+
yes: bool = typer.Option(
|
|
273
|
+
False, "--yes", "-y", help="Non-interactive: accept inferred/default choices"
|
|
274
|
+
),
|
|
275
|
+
force: bool = typer.Option(False, "--force", "-f", help="Overwrite existing files"),
|
|
276
|
+
dry_run: bool = typer.Option(
|
|
277
|
+
False, "--dry-run", help="Print the plan only, do not write files"
|
|
278
|
+
),
|
|
57
279
|
):
|
|
58
280
|
"""Install APS into a repo (.github/skills/...) or as a personal skill (~/.copilot/skills/...)."""
|
|
59
281
|
|
|
60
|
-
if repo and personal:
|
|
61
|
-
raise typer.BadParameter("Use at most one of --repo or --personal")
|
|
62
|
-
|
|
63
282
|
payload_skill_dir = resolve_payload_skill_dir()
|
|
64
|
-
|
|
65
283
|
repo_root = find_repo_root(Path.cwd())
|
|
66
|
-
|
|
284
|
+
guessed_workspace_root = pick_workspace_root(root)
|
|
285
|
+
|
|
286
|
+
platforms = load_platforms(payload_skill_dir)
|
|
287
|
+
platforms = sort_platforms_for_ui(platforms)
|
|
288
|
+
platforms_by_id = {p.platform_id: p for p in platforms}
|
|
289
|
+
available_platform_ids = [p.platform_id for p in platforms]
|
|
290
|
+
|
|
291
|
+
detections = (
|
|
292
|
+
detect_adapters(guessed_workspace_root, platforms)
|
|
293
|
+
if guessed_workspace_root
|
|
294
|
+
else {}
|
|
295
|
+
)
|
|
67
296
|
|
|
68
|
-
|
|
69
|
-
|
|
297
|
+
cli_platforms = _normalize_platform_args(platform)
|
|
298
|
+
|
|
299
|
+
# Determine platform selection
|
|
300
|
+
selected_platforms: list[str] = []
|
|
301
|
+
|
|
302
|
+
if cli_platforms is not None:
|
|
303
|
+
selected_platforms = cli_platforms
|
|
304
|
+
elif not yes and is_tty():
|
|
305
|
+
choices = [
|
|
306
|
+
questionary.Choice(title=_select_all_choice_label(), value="__all__")
|
|
307
|
+
]
|
|
308
|
+
for platform_id in available_platform_ids:
|
|
309
|
+
det = _detection_for(platform_id, detections)
|
|
310
|
+
label = format_detection_label(det) if det else ""
|
|
311
|
+
p = platforms_by_id[platform_id]
|
|
312
|
+
checked = bool(det and det.detected)
|
|
313
|
+
choices.append(
|
|
314
|
+
questionary.Choice(
|
|
315
|
+
title=f"{_platform_display_name(p)}{label}",
|
|
316
|
+
value=platform_id,
|
|
317
|
+
checked=checked,
|
|
318
|
+
)
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
picked = questionary.checkbox(
|
|
322
|
+
"Select platform adapters to apply (press <space> to select, <a> to toggle all):",
|
|
323
|
+
choices=choices,
|
|
324
|
+
).ask()
|
|
325
|
+
|
|
326
|
+
if picked is None:
|
|
327
|
+
raise typer.Abort()
|
|
328
|
+
|
|
329
|
+
has_all = "__all__" in picked
|
|
330
|
+
picked_platforms = [p for p in picked if p != "__all__"]
|
|
331
|
+
|
|
332
|
+
if has_all and not picked_platforms:
|
|
333
|
+
selected_platforms = list(available_platform_ids)
|
|
334
|
+
else:
|
|
335
|
+
selected_platforms = picked_platforms
|
|
336
|
+
else:
|
|
337
|
+
# Non-interactive defaults
|
|
338
|
+
if yes and detections:
|
|
339
|
+
selected_platforms = [
|
|
340
|
+
pid for pid, det in detections.items() if det.detected
|
|
341
|
+
]
|
|
342
|
+
else:
|
|
343
|
+
selected_platforms = []
|
|
70
344
|
|
|
71
|
-
|
|
345
|
+
# Determine scope
|
|
346
|
+
install_scope: InstallScope = (
|
|
347
|
+
"personal" if personal else "repo" if repo else ("repo" if repo_root else "personal")
|
|
348
|
+
)
|
|
349
|
+
workspace_root = guessed_workspace_root
|
|
72
350
|
|
|
73
351
|
if not yes and is_tty():
|
|
74
|
-
# Platform selection (first, so platform choice can inform installation location)
|
|
75
|
-
if not platform:
|
|
76
|
-
platforms = load_platforms(payload_skill_dir)
|
|
77
|
-
choices = [questionary.Choice(title="None (skill only)", value="none")]
|
|
78
|
-
if inferred_platform:
|
|
79
|
-
choices.insert(0, questionary.Choice(title=f"Auto-detected: {inferred_platform}", value=inferred_platform))
|
|
80
|
-
for p in platforms:
|
|
81
|
-
if p.platform_id == inferred_platform:
|
|
82
|
-
continue
|
|
83
|
-
choices.append(questionary.Choice(title=f"{p.display_name} ({p.platform_id})", value=p.platform_id))
|
|
84
|
-
|
|
85
|
-
platform_id = questionary.select("Select a platform adapter to apply:", choices=choices, default=inferred_platform or "none").ask()
|
|
86
|
-
assert isinstance(platform_id, str)
|
|
87
|
-
|
|
88
|
-
# Scope
|
|
89
352
|
if not (repo or personal):
|
|
90
|
-
|
|
91
|
-
|
|
353
|
+
# Compute likely destinations for display
|
|
354
|
+
personal_bases: set[str] = set()
|
|
355
|
+
wants_claude = any(is_claude_platform(p) for p in selected_platforms)
|
|
356
|
+
wants_non_claude = (
|
|
357
|
+
any(not is_claude_platform(p) for p in selected_platforms)
|
|
358
|
+
or not selected_platforms
|
|
359
|
+
)
|
|
360
|
+
if wants_non_claude:
|
|
361
|
+
base = str(default_personal_skill_path(claude=False)).replace(
|
|
362
|
+
SKILL_ID, ""
|
|
363
|
+
)
|
|
364
|
+
personal_bases.add(_fmt_path(Path(base)))
|
|
365
|
+
if wants_claude:
|
|
366
|
+
base = str(default_personal_skill_path(claude=True)).replace(
|
|
367
|
+
SKILL_ID, ""
|
|
368
|
+
)
|
|
369
|
+
personal_bases.add(_fmt_path(Path(base)))
|
|
370
|
+
|
|
371
|
+
scope_answer = questionary.select(
|
|
92
372
|
"Where should APS be installed?",
|
|
93
373
|
choices=[
|
|
94
374
|
questionary.Choice(
|
|
95
|
-
title=
|
|
375
|
+
title=(
|
|
376
|
+
f"Project skill in this repo ({_fmt_path(repo_root)})"
|
|
377
|
+
if repo_root
|
|
378
|
+
else "Project skill (choose a workspace folder)"
|
|
379
|
+
),
|
|
96
380
|
value="repo",
|
|
97
381
|
),
|
|
98
382
|
questionary.Choice(
|
|
99
|
-
title=f"Personal skill ({
|
|
383
|
+
title=f"Personal skill for your user ({', '.join(sorted(personal_bases))})",
|
|
100
384
|
value="personal",
|
|
101
385
|
),
|
|
102
386
|
],
|
|
103
387
|
default="repo" if repo_root else "personal",
|
|
104
388
|
).ask()
|
|
105
|
-
assert
|
|
389
|
+
assert scope_answer in ("repo", "personal")
|
|
390
|
+
install_scope = scope_answer
|
|
106
391
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
392
|
+
if install_scope == "repo" and not workspace_root:
|
|
393
|
+
root_answer = questionary.text(
|
|
394
|
+
"Workspace root path (the folder that contains .github/):",
|
|
395
|
+
default=str(Path.cwd()),
|
|
396
|
+
).ask()
|
|
397
|
+
workspace_root = Path(root_answer).expanduser().resolve()
|
|
111
398
|
|
|
112
|
-
|
|
113
|
-
|
|
399
|
+
if install_scope == "repo" and not workspace_root:
|
|
400
|
+
raise typer.BadParameter(
|
|
401
|
+
"Repo install selected but no workspace root found. Run in a git repo or pass --root <path>."
|
|
402
|
+
)
|
|
114
403
|
|
|
115
404
|
# Compute destinations
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
default_project_skill_path(workspace_root, claude=claude)
|
|
119
|
-
if install_scope == "repo"
|
|
120
|
-
else default_personal_skill_path(claude=claude)
|
|
405
|
+
skill_dests = compute_skill_destinations(
|
|
406
|
+
install_scope, workspace_root, selected_platforms
|
|
121
407
|
)
|
|
408
|
+
skills = [
|
|
409
|
+
PlannedSkillInstall(dst=dst, exists=dst.exists()) for dst in skill_dests
|
|
410
|
+
]
|
|
122
411
|
|
|
123
|
-
#
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
templates_dir = payload_skill_dir / "platforms" / platform_id / "templates"
|
|
128
|
-
if templates_dir.is_dir():
|
|
129
|
-
template_root = Path.home() if install_scope == "personal" else workspace_root
|
|
130
|
-
else:
|
|
131
|
-
templates_dir = None
|
|
132
|
-
|
|
133
|
-
plan = {
|
|
134
|
-
"install_scope": install_scope,
|
|
135
|
-
"workspace_root": str(workspace_root),
|
|
136
|
-
"platform_id": platform_id or None,
|
|
137
|
-
"claude": bool(claude),
|
|
138
|
-
"skill_source": str(payload_skill_dir),
|
|
139
|
-
"skill_dest": str(skill_dest),
|
|
140
|
-
"templates_source": str(templates_dir) if templates_dir else None,
|
|
141
|
-
"templates_dest": str(template_root) if template_root else None,
|
|
142
|
-
"force": bool(force),
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
if dry_run:
|
|
146
|
-
console.print_json(json.dumps({"plan": plan}, indent=2))
|
|
147
|
-
raise typer.Exit(code=0)
|
|
412
|
+
# Plan templates
|
|
413
|
+
templates = _plan_platform_templates(
|
|
414
|
+
payload_skill_dir, install_scope, workspace_root, selected_platforms, force
|
|
415
|
+
)
|
|
148
416
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
417
|
+
plan = InitPlan(
|
|
418
|
+
scope=install_scope,
|
|
419
|
+
workspace_root=workspace_root,
|
|
420
|
+
selected_platforms=selected_platforms,
|
|
421
|
+
payload_skill_dir=payload_skill_dir,
|
|
422
|
+
skills=skills,
|
|
423
|
+
templates=templates,
|
|
424
|
+
)
|
|
154
425
|
|
|
155
|
-
|
|
156
|
-
|
|
426
|
+
if dry_run:
|
|
427
|
+
console.print("Dry run — planned actions:\n")
|
|
428
|
+
console.print(_render_plan(plan, force))
|
|
429
|
+
if not plan.selected_platforms:
|
|
430
|
+
console.print()
|
|
431
|
+
console.print(_render_empty_platform_warning())
|
|
432
|
+
return
|
|
157
433
|
|
|
158
|
-
|
|
159
|
-
|
|
434
|
+
if not yes and is_tty():
|
|
435
|
+
console.print(_render_plan(plan, force))
|
|
436
|
+
console.print()
|
|
437
|
+
|
|
438
|
+
if not plan.selected_platforms:
|
|
439
|
+
console.print(_render_empty_platform_warning())
|
|
440
|
+
console.print()
|
|
441
|
+
|
|
442
|
+
if any(s.exists for s in skills) and not force:
|
|
443
|
+
console.print(
|
|
444
|
+
"Note: One or more skill destinations already exist. Confirming will overwrite them."
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
ok = questionary.confirm("Proceed with these changes?", default=False).ask()
|
|
448
|
+
if not ok:
|
|
449
|
+
console.print("Cancelled.")
|
|
450
|
+
return
|
|
451
|
+
else:
|
|
452
|
+
# Non-interactive: refuse to overwrite without --force
|
|
453
|
+
conflicts = [s for s in skills if s.exists]
|
|
454
|
+
if conflicts and not force:
|
|
455
|
+
first = conflicts[0]
|
|
456
|
+
raise typer.BadParameter(
|
|
457
|
+
f"Destination exists: {first.dst} (use --force to overwrite)"
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
# Execute skill copies
|
|
461
|
+
for s in skills:
|
|
462
|
+
if s.exists:
|
|
463
|
+
if force or (is_tty() and not yes):
|
|
464
|
+
remove_dir(s.dst)
|
|
465
|
+
|
|
466
|
+
ensure_dir(s.dst.parent)
|
|
467
|
+
copy_dir(payload_skill_dir, s.dst)
|
|
468
|
+
console.print(f"Installed APS skill -> {s.dst}")
|
|
469
|
+
|
|
470
|
+
# Copy templates
|
|
471
|
+
for t in templates:
|
|
160
472
|
|
|
161
|
-
# Copy templates (if platform has templates)
|
|
162
|
-
if templates_dir and template_root:
|
|
163
473
|
def filter_fn(rel_path: str) -> bool:
|
|
164
|
-
# Skip .github/** for personal installs (shouldn't put .github in home dir)
|
|
165
474
|
if install_scope == "personal" and rel_path.startswith(".github"):
|
|
166
475
|
return False
|
|
167
476
|
return True
|
|
168
477
|
|
|
169
478
|
copied = copy_template_tree(
|
|
170
|
-
templates_dir,
|
|
171
|
-
template_root,
|
|
479
|
+
t.templates_dir,
|
|
480
|
+
t.template_root,
|
|
172
481
|
force=force,
|
|
173
482
|
filter_fn=filter_fn,
|
|
174
483
|
)
|
|
484
|
+
|
|
175
485
|
if copied:
|
|
176
|
-
console.print(
|
|
486
|
+
console.print(
|
|
487
|
+
f"Installed {len(copied)} template file(s) for {t.platform_id}:"
|
|
488
|
+
)
|
|
177
489
|
for f in copied:
|
|
178
|
-
console.print(f"
|
|
490
|
+
console.print(f" - {f}")
|
|
491
|
+
|
|
492
|
+
console.print("\nNext steps:")
|
|
493
|
+
console.print("- Ensure your IDE has Agent Skills enabled as needed.")
|
|
494
|
+
for d in skill_dests:
|
|
495
|
+
console.print(f"- Skill location: {d}")
|
|
179
496
|
|
|
180
497
|
|
|
181
498
|
@app.command()
|
|
182
|
-
def doctor(
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
499
|
+
def doctor(
|
|
500
|
+
root: Optional[str] = typer.Option(
|
|
501
|
+
None,
|
|
502
|
+
"--root",
|
|
503
|
+
help="Workspace root path (defaults to git repo root if found)",
|
|
504
|
+
),
|
|
505
|
+
json_out: bool = typer.Option(False, "--json", help="Output JSON format"),
|
|
506
|
+
):
|
|
507
|
+
"""Check APS installation status + basic platform detection."""
|
|
508
|
+
workspace_root = pick_workspace_root(root)
|
|
509
|
+
|
|
510
|
+
payload_skill_dir = resolve_payload_skill_dir()
|
|
511
|
+
platforms = sort_platforms_for_ui(load_platforms(payload_skill_dir))
|
|
512
|
+
detected_adapters = (
|
|
513
|
+
detect_adapters(workspace_root, platforms) if workspace_root else None
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
# Build installations list matching Node structure
|
|
517
|
+
installations: list[dict] = []
|
|
518
|
+
|
|
519
|
+
if workspace_root:
|
|
520
|
+
repo_skill = default_project_skill_path(workspace_root, claude=False)
|
|
521
|
+
repo_skill_claude = default_project_skill_path(workspace_root, claude=True)
|
|
522
|
+
installations.append(
|
|
523
|
+
{
|
|
524
|
+
"scope": "repo",
|
|
525
|
+
"path": str(repo_skill),
|
|
526
|
+
"installed": (repo_skill / "SKILL.md").exists(),
|
|
527
|
+
}
|
|
528
|
+
)
|
|
529
|
+
installations.append(
|
|
530
|
+
{
|
|
531
|
+
"scope": "repo (claude)",
|
|
532
|
+
"path": str(repo_skill_claude),
|
|
533
|
+
"installed": (repo_skill_claude / "SKILL.md").exists(),
|
|
534
|
+
}
|
|
535
|
+
)
|
|
186
536
|
|
|
187
|
-
# Check both Copilot and Claude locations.
|
|
188
|
-
project_skill = default_project_skill_path(workspace_root, claude=False)
|
|
189
|
-
project_skill_claude = default_project_skill_path(workspace_root, claude=True)
|
|
190
537
|
personal_skill = default_personal_skill_path(claude=False)
|
|
191
538
|
personal_skill_claude = default_personal_skill_path(claude=True)
|
|
539
|
+
installations.append(
|
|
540
|
+
{
|
|
541
|
+
"scope": "personal",
|
|
542
|
+
"path": str(personal_skill),
|
|
543
|
+
"installed": (personal_skill / "SKILL.md").exists(),
|
|
544
|
+
}
|
|
545
|
+
)
|
|
546
|
+
installations.append(
|
|
547
|
+
{
|
|
548
|
+
"scope": "personal (claude)",
|
|
549
|
+
"path": str(personal_skill_claude),
|
|
550
|
+
"installed": (personal_skill_claude / "SKILL.md").exists(),
|
|
551
|
+
}
|
|
552
|
+
)
|
|
192
553
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
554
|
+
# Format detected_adapters for JSON output
|
|
555
|
+
adapters_out = None
|
|
556
|
+
if detected_adapters:
|
|
557
|
+
adapters_out = {
|
|
558
|
+
pid: {
|
|
559
|
+
"platformId": det.platform_id,
|
|
560
|
+
"detected": det.detected,
|
|
561
|
+
"reasons": list(det.reasons),
|
|
562
|
+
}
|
|
563
|
+
for pid, det in detected_adapters.items()
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
result = {
|
|
567
|
+
"workspace_root": str(workspace_root) if workspace_root else None,
|
|
568
|
+
"detected_adapters": adapters_out,
|
|
569
|
+
"installations": installations,
|
|
201
570
|
}
|
|
202
571
|
|
|
203
572
|
if json_out:
|
|
204
|
-
|
|
205
|
-
|
|
573
|
+
# Match Node: print raw JSON to stdout (no Rich formatting).
|
|
574
|
+
typer.echo(json.dumps(result, indent=2))
|
|
575
|
+
return
|
|
576
|
+
|
|
577
|
+
console.print("APS Doctor")
|
|
578
|
+
console.print("----------")
|
|
579
|
+
console.print(f"Workspace root: {workspace_root or '(not detected)'}")
|
|
580
|
+
|
|
581
|
+
if detected_adapters:
|
|
582
|
+
detected = [d for d in detected_adapters.values() if d.detected]
|
|
583
|
+
if detected:
|
|
584
|
+
console.print(
|
|
585
|
+
f"Detected adapters: {', '.join(d.platform_id for d in detected)}"
|
|
586
|
+
)
|
|
587
|
+
else:
|
|
588
|
+
console.print("Detected adapters: (none)")
|
|
589
|
+
console.print("")
|
|
206
590
|
|
|
207
|
-
console.print("
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
console.print(f" project skill: {'OK' if out['project_skill']['installed'] else 'missing'} — {out['project_skill']['path']}")
|
|
212
|
-
console.print(f" project skill (claude): {'OK' if out['project_skill_claude']['installed'] else 'missing'} — {out['project_skill_claude']['path']}")
|
|
213
|
-
console.print(f" personal skill: {'OK' if out['personal_skill']['installed'] else 'missing'} — {out['personal_skill']['path']}")
|
|
214
|
-
console.print(f" personal skill (claude): {'OK' if out['personal_skill_claude']['installed'] else 'missing'} — {out['personal_skill_claude']['path']}")
|
|
591
|
+
console.print("Installed skills:")
|
|
592
|
+
for inst in installations:
|
|
593
|
+
status = "✓" if inst["installed"] else "✗"
|
|
594
|
+
console.print(f"- {inst['scope']}: {inst['path']} {status}")
|
|
215
595
|
|
|
216
596
|
|
|
217
597
|
@app.command()
|
|
@@ -219,6 +599,7 @@ def platforms():
|
|
|
219
599
|
"""List available platform adapters bundled with this APS release."""
|
|
220
600
|
payload_skill_dir = resolve_payload_skill_dir()
|
|
221
601
|
plats = load_platforms(payload_skill_dir)
|
|
602
|
+
plats = sort_platforms_for_ui(plats)
|
|
222
603
|
|
|
223
604
|
table = Table(title="APS Platform Adapters")
|
|
224
605
|
table.add_column("platform_id")
|
|
@@ -234,7 +615,7 @@ def platforms():
|
|
|
234
615
|
@app.command()
|
|
235
616
|
def version():
|
|
236
617
|
"""Print CLI version."""
|
|
237
|
-
|
|
618
|
+
typer.echo(__version__)
|
|
238
619
|
|
|
239
620
|
|
|
240
621
|
def main():
|
|
@@ -243,4 +624,4 @@ def main():
|
|
|
243
624
|
|
|
244
625
|
|
|
245
626
|
if __name__ == "__main__":
|
|
246
|
-
main()
|
|
627
|
+
main()
|