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