systemlink-cli 1.3.1__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.
Files changed (74) hide show
  1. slcli/__init__.py +1 -0
  2. slcli/__main__.py +23 -0
  3. slcli/_version.py +4 -0
  4. slcli/asset_click.py +1289 -0
  5. slcli/cli_formatters.py +218 -0
  6. slcli/cli_utils.py +504 -0
  7. slcli/comment_click.py +602 -0
  8. slcli/completion_click.py +418 -0
  9. slcli/config.py +81 -0
  10. slcli/config_click.py +498 -0
  11. slcli/dff_click.py +979 -0
  12. slcli/dff_decorators.py +24 -0
  13. slcli/example_click.py +404 -0
  14. slcli/example_loader.py +274 -0
  15. slcli/example_provisioner.py +2777 -0
  16. slcli/examples/README.md +134 -0
  17. slcli/examples/_schema/schema-v1.0.json +169 -0
  18. slcli/examples/demo-complete-workflow/README.md +323 -0
  19. slcli/examples/demo-complete-workflow/config.yaml +638 -0
  20. slcli/examples/demo-test-plans/README.md +132 -0
  21. slcli/examples/demo-test-plans/config.yaml +154 -0
  22. slcli/examples/exercise-5-1-parametric-insights/README.md +101 -0
  23. slcli/examples/exercise-5-1-parametric-insights/config.yaml +1589 -0
  24. slcli/examples/exercise-7-1-test-plans/README.md +93 -0
  25. slcli/examples/exercise-7-1-test-plans/config.yaml +323 -0
  26. slcli/examples/spec-compliance-notebooks/README.md +140 -0
  27. slcli/examples/spec-compliance-notebooks/config.yaml +112 -0
  28. slcli/examples/spec-compliance-notebooks/notebooks/SpecAnalysis_ComplianceCalculation.ipynb +1553 -0
  29. slcli/examples/spec-compliance-notebooks/notebooks/SpecComplianceCalculation.ipynb +1577 -0
  30. slcli/examples/spec-compliance-notebooks/notebooks/SpecfileExtractionAndIngestion.ipynb +912 -0
  31. slcli/examples/spec-compliance-notebooks/spec_template.xlsx +0 -0
  32. slcli/feed_click.py +892 -0
  33. slcli/file_click.py +932 -0
  34. slcli/function_click.py +1400 -0
  35. slcli/function_templates.py +85 -0
  36. slcli/main.py +406 -0
  37. slcli/mcp_click.py +269 -0
  38. slcli/mcp_server.py +748 -0
  39. slcli/notebook_click.py +1770 -0
  40. slcli/platform.py +345 -0
  41. slcli/policy_click.py +679 -0
  42. slcli/policy_utils.py +411 -0
  43. slcli/profiles.py +411 -0
  44. slcli/response_handlers.py +359 -0
  45. slcli/routine_click.py +763 -0
  46. slcli/skill_click.py +253 -0
  47. slcli/skills/slcli/SKILL.md +713 -0
  48. slcli/skills/slcli/references/analysis-recipes.md +474 -0
  49. slcli/skills/slcli/references/filtering.md +236 -0
  50. slcli/skills/systemlink-webapp/SKILL.md +744 -0
  51. slcli/skills/systemlink-webapp/references/deployment.md +123 -0
  52. slcli/skills/systemlink-webapp/references/nimble-angular.md +380 -0
  53. slcli/skills/systemlink-webapp/references/systemlink-services.md +192 -0
  54. slcli/ssl_trust.py +93 -0
  55. slcli/system_click.py +2216 -0
  56. slcli/table_utils.py +124 -0
  57. slcli/tag_click.py +794 -0
  58. slcli/templates_click.py +599 -0
  59. slcli/testmonitor_click.py +1667 -0
  60. slcli/universal_handlers.py +305 -0
  61. slcli/user_click.py +1218 -0
  62. slcli/utils.py +832 -0
  63. slcli/web_editor.py +295 -0
  64. slcli/webapp_click.py +981 -0
  65. slcli/workflow_preview.py +287 -0
  66. slcli/workflows_click.py +988 -0
  67. slcli/workitem_click.py +2258 -0
  68. slcli/workspace_click.py +576 -0
  69. slcli/workspace_utils.py +206 -0
  70. systemlink_cli-1.3.1.dist-info/METADATA +20 -0
  71. systemlink_cli-1.3.1.dist-info/RECORD +74 -0
  72. systemlink_cli-1.3.1.dist-info/WHEEL +4 -0
  73. systemlink_cli-1.3.1.dist-info/entry_points.txt +7 -0
  74. systemlink_cli-1.3.1.dist-info/licenses/LICENSE +21 -0
slcli/skill_click.py ADDED
@@ -0,0 +1,253 @@
1
+ """Agent Skills install command for slcli."""
2
+
3
+ import shutil
4
+ import sys
5
+ from pathlib import Path
6
+ from typing import Any, Dict, List, Optional, Tuple
7
+
8
+ import click
9
+ import questionary
10
+
11
+ from .utils import ExitCodes
12
+
13
+ SKILL_NAME = "slcli"
14
+ SKILL_CHOICES = ["slcli", "systemlink-webapp"]
15
+
16
+ # Mapping of client name → (personal skills dir, project subdir relative to repo root)
17
+ # personal dir uses Path.home() so it's always resolved at call time via _personal_dir().
18
+ _CLIENT_TABLE: Dict[str, Tuple[str, str]] = {
19
+ "copilot": ("~/.copilot/skills", ".github/skills"),
20
+ "claude": ("~/.claude/skills", ".claude/skills"),
21
+ "codex": ("~/.agents/skills", ".agents/skills"),
22
+ }
23
+
24
+ CLIENT_CHOICES = list(_CLIENT_TABLE.keys())
25
+
26
+
27
+ def _personal_dir(client: str) -> Path:
28
+ """Return the resolved personal skills directory for a client."""
29
+ return Path(_CLIENT_TABLE[client][0]).expanduser()
30
+
31
+
32
+ def _project_subdir(client: str) -> str:
33
+ """Return the project-relative skills subdirectory for a client."""
34
+ return _CLIENT_TABLE[client][1]
35
+
36
+
37
+ def _find_repo_root() -> Optional[Path]:
38
+ """Walk up from cwd looking for a .git directory.
39
+
40
+ Returns:
41
+ The repository root Path, or None if not inside a git repository.
42
+ """
43
+ cwd = Path.cwd()
44
+ for directory in [cwd, *cwd.parents]:
45
+ if (directory / ".git").exists():
46
+ return directory
47
+ return None
48
+
49
+
50
+ def _find_bundled_skills_dir() -> Path:
51
+ """Locate the bundled skills/ directory.
52
+
53
+ Handles PyInstaller (onefile + onedir) and development/source layouts.
54
+
55
+ Returns:
56
+ Path to the bundled skills/ directory.
57
+
58
+ Raises:
59
+ FileNotFoundError: If the skills directory cannot be found.
60
+ """
61
+ candidates: List[Path] = []
62
+
63
+ # PyInstaller onefile mode
64
+ meipass = getattr(sys, "_MEIPASS", None)
65
+ if meipass:
66
+ candidates.append(Path(meipass) / "skills")
67
+
68
+ # PyInstaller onedir / frozen executable
69
+ if getattr(sys, "frozen", False):
70
+ candidates.append(Path(sys.executable).resolve().parent / "skills")
71
+
72
+ # pip install / development: skills/ bundled inside the slcli package
73
+ candidates.append(Path(__file__).resolve().parent / "skills")
74
+
75
+ for candidate in candidates:
76
+ if candidate.exists() and any(
77
+ (candidate / name / "SKILL.md").exists() for name in SKILL_CHOICES
78
+ ):
79
+ return candidate
80
+
81
+ raise FileNotFoundError(
82
+ "Bundled skills directory not found. "
83
+ "Try reinstalling slcli: pip install --force-reinstall slcli"
84
+ )
85
+
86
+
87
+ def _resolve_destinations(clients: List[str], scope: str) -> List[Path]:
88
+ """Build the list of destination skill parent directories.
89
+
90
+ Args:
91
+ clients: List of client names (subset of CLIENT_CHOICES).
92
+ scope: One of 'personal', 'project', or 'both'.
93
+
94
+ Returns:
95
+ Deduplicated list of directories into which slcli/ should be copied.
96
+ """
97
+ dirs: List[Path] = []
98
+
99
+ repo_root: Optional[Path] = None
100
+ if scope in ("project", "both"):
101
+ repo_root = _find_repo_root() or Path.cwd()
102
+
103
+ for client in clients:
104
+ if scope in ("personal", "both"):
105
+ dirs.append(_personal_dir(client))
106
+ if scope in ("project", "both") and repo_root is not None:
107
+ dirs.append(repo_root / _project_subdir(client))
108
+
109
+ # Preserve order, deduplicate (e.g. same path selected via two clients)
110
+ seen: List[Path] = []
111
+ for d in dirs:
112
+ if d not in seen:
113
+ seen.append(d)
114
+ return seen
115
+
116
+
117
+ def register_skill_commands(cli: Any) -> None:
118
+ """Register the skill command group with the CLI.
119
+
120
+ Args:
121
+ cli: The Click CLI group to register commands with.
122
+ """
123
+
124
+ @cli.group()
125
+ def skill() -> None:
126
+ """Manage AI agent skills for Copilot, Claude, and Codex."""
127
+
128
+ @skill.command(name="install")
129
+ @click.option(
130
+ "--skill",
131
+ "-k",
132
+ type=click.Choice(SKILL_CHOICES + ["all"], case_sensitive=False),
133
+ default=None,
134
+ help="Skill to install (slcli, systemlink-webapp, or all).",
135
+ )
136
+ @click.option(
137
+ "--client",
138
+ "-c",
139
+ type=click.Choice(CLIENT_CHOICES + ["all"], case_sensitive=False),
140
+ default=None,
141
+ help="AI client to install for (copilot, claude, codex, or all).",
142
+ )
143
+ @click.option(
144
+ "--scope",
145
+ "-s",
146
+ type=click.Choice(["personal", "project", "both"], case_sensitive=False),
147
+ default=None,
148
+ help="personal (~/ home dirs), project (current repo), or both.",
149
+ )
150
+ @click.option(
151
+ "--force",
152
+ "-F",
153
+ is_flag=True,
154
+ default=False,
155
+ help="Overwrite existing skill installation without prompting.",
156
+ )
157
+ def install_skill(
158
+ skill: Optional[str], client: Optional[str], scope: Optional[str], force: bool
159
+ ) -> None:
160
+ """Install agent skills for AI coding assistants.
161
+
162
+ Copies bundled skills into the skills directory of one or more AI clients.
163
+ Available skills: slcli, systemlink-webapp.
164
+ Supported clients and their skill locations:
165
+
166
+ \b
167
+ copilot personal: ~/.copilot/skills/ project: .github/skills/
168
+ claude personal: ~/.claude/skills/ project: .claude/skills/
169
+ codex personal: ~/.agents/skills/ project: .agents/skills/
170
+
171
+ When options are omitted you will be prompted interactively.
172
+ """
173
+ # ── interactive prompts when options not supplied ─────────────────────
174
+ if skill is None:
175
+ skill = questionary.select(
176
+ "Which skill to install?",
177
+ choices=SKILL_CHOICES + ["all"],
178
+ default="all",
179
+ ).ask()
180
+ if skill is None:
181
+ raise click.Abort()
182
+
183
+ if client is None:
184
+ client = questionary.select(
185
+ "Install for which AI client?",
186
+ choices=CLIENT_CHOICES + ["all"],
187
+ default="all",
188
+ ).ask()
189
+ if client is None:
190
+ raise click.Abort()
191
+
192
+ if scope is None:
193
+ scope = questionary.select(
194
+ "Install scope?",
195
+ choices=["personal", "project", "both"],
196
+ default="personal",
197
+ ).ask()
198
+ if scope is None:
199
+ raise click.Abort()
200
+
201
+ # ── resolve skill and client lists ────────────────────────────────────
202
+ skill_names: List[str] = SKILL_CHOICES if skill == "all" else [skill]
203
+ clients: List[str] = CLIENT_CHOICES if client == "all" else [client]
204
+
205
+ # ── locate source ─────────────────────────────────────────────────────
206
+ try:
207
+ skills_dir = _find_bundled_skills_dir()
208
+ except FileNotFoundError as exc:
209
+ click.echo(f"✗ {exc}", err=True)
210
+ sys.exit(ExitCodes.GENERAL_ERROR)
211
+
212
+ destinations = _resolve_destinations(clients, scope)
213
+
214
+ installed_any = False
215
+ errors = 0
216
+
217
+ for skill_name in skill_names:
218
+ source = skills_dir / skill_name
219
+ if not source.exists():
220
+ click.echo(
221
+ f"✗ Skill '{skill_name}' not found in bundled skills directory.", err=True
222
+ )
223
+ errors += 1
224
+ continue
225
+
226
+ for dest_parent in destinations:
227
+ dest = dest_parent / skill_name
228
+
229
+ if dest.exists() and not force:
230
+ confirm = questionary.confirm(
231
+ f"Skill already installed at {dest}. Overwrite?",
232
+ default=False,
233
+ ).ask()
234
+ if not confirm:
235
+ click.echo(f" Skipped {dest}")
236
+ continue
237
+
238
+ try:
239
+ dest_parent.mkdir(parents=True, exist_ok=True)
240
+ if dest.exists():
241
+ shutil.rmtree(dest)
242
+ shutil.copytree(source, dest)
243
+ click.echo(f"✓ Installed {skill_name} skill → {dest}")
244
+ installed_any = True
245
+ except OSError as exc:
246
+ click.echo(f"✗ Failed to install to {dest}: {exc}", err=True)
247
+ errors += 1
248
+
249
+ if not installed_any and errors == 0:
250
+ click.echo("No skill locations were updated.")
251
+
252
+ if errors:
253
+ sys.exit(ExitCodes.GENERAL_ERROR)