raise-cli 2.2.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.
- raise_cli/__init__.py +38 -0
- raise_cli/__main__.py +30 -0
- raise_cli/adapters/__init__.py +91 -0
- raise_cli/adapters/declarative/__init__.py +26 -0
- raise_cli/adapters/declarative/adapter.py +267 -0
- raise_cli/adapters/declarative/discovery.py +94 -0
- raise_cli/adapters/declarative/expressions.py +150 -0
- raise_cli/adapters/declarative/reference/__init__.py +1 -0
- raise_cli/adapters/declarative/reference/github.yaml +143 -0
- raise_cli/adapters/declarative/schema.py +98 -0
- raise_cli/adapters/filesystem.py +299 -0
- raise_cli/adapters/mcp_bridge.py +10 -0
- raise_cli/adapters/mcp_confluence.py +246 -0
- raise_cli/adapters/mcp_jira.py +405 -0
- raise_cli/adapters/models.py +205 -0
- raise_cli/adapters/protocols.py +180 -0
- raise_cli/adapters/registry.py +90 -0
- raise_cli/adapters/sync.py +149 -0
- raise_cli/agents/__init__.py +14 -0
- raise_cli/agents/antigravity.yaml +8 -0
- raise_cli/agents/claude.yaml +8 -0
- raise_cli/agents/copilot.yaml +8 -0
- raise_cli/agents/copilot_plugin.py +124 -0
- raise_cli/agents/cursor.yaml +7 -0
- raise_cli/agents/roo.yaml +8 -0
- raise_cli/agents/windsurf.yaml +8 -0
- raise_cli/artifacts/__init__.py +30 -0
- raise_cli/artifacts/models.py +43 -0
- raise_cli/artifacts/reader.py +55 -0
- raise_cli/artifacts/renderer.py +104 -0
- raise_cli/artifacts/story_design.py +69 -0
- raise_cli/artifacts/writer.py +45 -0
- raise_cli/backlog/__init__.py +1 -0
- raise_cli/backlog/sync.py +115 -0
- raise_cli/cli/__init__.py +3 -0
- raise_cli/cli/commands/__init__.py +3 -0
- raise_cli/cli/commands/_resolve.py +153 -0
- raise_cli/cli/commands/adapters.py +362 -0
- raise_cli/cli/commands/artifact.py +137 -0
- raise_cli/cli/commands/backlog.py +333 -0
- raise_cli/cli/commands/base.py +31 -0
- raise_cli/cli/commands/discover.py +551 -0
- raise_cli/cli/commands/docs.py +130 -0
- raise_cli/cli/commands/doctor.py +177 -0
- raise_cli/cli/commands/gate.py +223 -0
- raise_cli/cli/commands/graph.py +1086 -0
- raise_cli/cli/commands/info.py +81 -0
- raise_cli/cli/commands/init.py +746 -0
- raise_cli/cli/commands/journal.py +167 -0
- raise_cli/cli/commands/mcp.py +524 -0
- raise_cli/cli/commands/memory.py +467 -0
- raise_cli/cli/commands/pattern.py +348 -0
- raise_cli/cli/commands/profile.py +59 -0
- raise_cli/cli/commands/publish.py +80 -0
- raise_cli/cli/commands/release.py +338 -0
- raise_cli/cli/commands/session.py +528 -0
- raise_cli/cli/commands/signal.py +410 -0
- raise_cli/cli/commands/skill.py +350 -0
- raise_cli/cli/commands/skill_set.py +145 -0
- raise_cli/cli/error_handler.py +158 -0
- raise_cli/cli/main.py +163 -0
- raise_cli/compat.py +66 -0
- raise_cli/config/__init__.py +41 -0
- raise_cli/config/agent_plugin.py +105 -0
- raise_cli/config/agent_registry.py +233 -0
- raise_cli/config/agents.py +120 -0
- raise_cli/config/ide.py +32 -0
- raise_cli/config/paths.py +379 -0
- raise_cli/config/settings.py +180 -0
- raise_cli/context/__init__.py +42 -0
- raise_cli/context/analyzers/__init__.py +16 -0
- raise_cli/context/analyzers/models.py +36 -0
- raise_cli/context/analyzers/protocol.py +43 -0
- raise_cli/context/analyzers/python.py +292 -0
- raise_cli/context/builder.py +1569 -0
- raise_cli/context/diff.py +213 -0
- raise_cli/context/extractors/__init__.py +13 -0
- raise_cli/context/extractors/skills.py +121 -0
- raise_cli/core/__init__.py +37 -0
- raise_cli/core/files.py +66 -0
- raise_cli/core/text.py +174 -0
- raise_cli/core/tools.py +441 -0
- raise_cli/discovery/__init__.py +50 -0
- raise_cli/discovery/analyzer.py +691 -0
- raise_cli/discovery/drift.py +355 -0
- raise_cli/discovery/scanner.py +1687 -0
- raise_cli/doctor/__init__.py +4 -0
- raise_cli/doctor/checks/__init__.py +1 -0
- raise_cli/doctor/checks/environment.py +110 -0
- raise_cli/doctor/checks/project.py +238 -0
- raise_cli/doctor/fix.py +80 -0
- raise_cli/doctor/models.py +56 -0
- raise_cli/doctor/protocol.py +43 -0
- raise_cli/doctor/registry.py +100 -0
- raise_cli/doctor/report.py +141 -0
- raise_cli/doctor/runner.py +95 -0
- raise_cli/engines/__init__.py +3 -0
- raise_cli/exceptions.py +215 -0
- raise_cli/gates/__init__.py +19 -0
- raise_cli/gates/builtin/__init__.py +1 -0
- raise_cli/gates/builtin/coverage.py +52 -0
- raise_cli/gates/builtin/lint.py +48 -0
- raise_cli/gates/builtin/tests.py +48 -0
- raise_cli/gates/builtin/types.py +48 -0
- raise_cli/gates/models.py +40 -0
- raise_cli/gates/protocol.py +41 -0
- raise_cli/gates/registry.py +141 -0
- raise_cli/governance/__init__.py +11 -0
- raise_cli/governance/extractor.py +412 -0
- raise_cli/governance/models.py +134 -0
- raise_cli/governance/parsers/__init__.py +35 -0
- raise_cli/governance/parsers/_convert.py +38 -0
- raise_cli/governance/parsers/adr.py +274 -0
- raise_cli/governance/parsers/backlog.py +356 -0
- raise_cli/governance/parsers/constitution.py +119 -0
- raise_cli/governance/parsers/epic.py +323 -0
- raise_cli/governance/parsers/glossary.py +316 -0
- raise_cli/governance/parsers/guardrails.py +345 -0
- raise_cli/governance/parsers/prd.py +112 -0
- raise_cli/governance/parsers/roadmap.py +118 -0
- raise_cli/governance/parsers/vision.py +116 -0
- raise_cli/graph/__init__.py +1 -0
- raise_cli/graph/backends/__init__.py +57 -0
- raise_cli/graph/backends/api.py +137 -0
- raise_cli/graph/backends/dual.py +139 -0
- raise_cli/graph/backends/pending.py +84 -0
- raise_cli/handlers/__init__.py +3 -0
- raise_cli/hooks/__init__.py +54 -0
- raise_cli/hooks/builtin/__init__.py +1 -0
- raise_cli/hooks/builtin/backlog.py +216 -0
- raise_cli/hooks/builtin/gate_bridge.py +83 -0
- raise_cli/hooks/builtin/jira_sync.py +127 -0
- raise_cli/hooks/builtin/memory.py +117 -0
- raise_cli/hooks/builtin/telemetry.py +72 -0
- raise_cli/hooks/emitter.py +184 -0
- raise_cli/hooks/events.py +262 -0
- raise_cli/hooks/protocol.py +38 -0
- raise_cli/hooks/registry.py +117 -0
- raise_cli/mcp/__init__.py +33 -0
- raise_cli/mcp/bridge.py +218 -0
- raise_cli/mcp/models.py +43 -0
- raise_cli/mcp/registry.py +77 -0
- raise_cli/mcp/schema.py +41 -0
- raise_cli/memory/__init__.py +58 -0
- raise_cli/memory/loader.py +247 -0
- raise_cli/memory/migration.py +241 -0
- raise_cli/memory/models.py +169 -0
- raise_cli/memory/writer.py +598 -0
- raise_cli/onboarding/__init__.py +103 -0
- raise_cli/onboarding/bootstrap.py +324 -0
- raise_cli/onboarding/claudemd.py +17 -0
- raise_cli/onboarding/conventions.py +742 -0
- raise_cli/onboarding/detection.py +374 -0
- raise_cli/onboarding/governance.py +443 -0
- raise_cli/onboarding/instructions.py +672 -0
- raise_cli/onboarding/manifest.py +201 -0
- raise_cli/onboarding/memory_md.py +399 -0
- raise_cli/onboarding/migration.py +207 -0
- raise_cli/onboarding/profile.py +624 -0
- raise_cli/onboarding/skill_conflict.py +100 -0
- raise_cli/onboarding/skill_manifest.py +176 -0
- raise_cli/onboarding/skills.py +437 -0
- raise_cli/onboarding/workflows.py +101 -0
- raise_cli/output/__init__.py +28 -0
- raise_cli/output/console.py +394 -0
- raise_cli/output/formatters/__init__.py +9 -0
- raise_cli/output/formatters/adapters.py +135 -0
- raise_cli/output/formatters/discover.py +439 -0
- raise_cli/output/formatters/skill.py +298 -0
- raise_cli/publish/__init__.py +3 -0
- raise_cli/publish/changelog.py +80 -0
- raise_cli/publish/check.py +179 -0
- raise_cli/publish/version.py +172 -0
- raise_cli/rai_base/__init__.py +22 -0
- raise_cli/rai_base/framework/__init__.py +7 -0
- raise_cli/rai_base/framework/methodology.yaml +233 -0
- raise_cli/rai_base/governance/__init__.py +1 -0
- raise_cli/rai_base/governance/architecture/__init__.py +1 -0
- raise_cli/rai_base/governance/architecture/domain-model.md +20 -0
- raise_cli/rai_base/governance/architecture/system-context.md +34 -0
- raise_cli/rai_base/governance/architecture/system-design.md +24 -0
- raise_cli/rai_base/governance/backlog.md +8 -0
- raise_cli/rai_base/governance/guardrails.md +17 -0
- raise_cli/rai_base/governance/prd.md +25 -0
- raise_cli/rai_base/governance/vision.md +16 -0
- raise_cli/rai_base/identity/__init__.py +8 -0
- raise_cli/rai_base/identity/core.md +119 -0
- raise_cli/rai_base/identity/perspective.md +119 -0
- raise_cli/rai_base/memory/__init__.py +7 -0
- raise_cli/rai_base/memory/patterns-base.jsonl +55 -0
- raise_cli/schemas/__init__.py +3 -0
- raise_cli/schemas/journal.py +49 -0
- raise_cli/schemas/session_state.py +117 -0
- raise_cli/session/__init__.py +5 -0
- raise_cli/session/bundle.py +820 -0
- raise_cli/session/close.py +268 -0
- raise_cli/session/journal.py +119 -0
- raise_cli/session/resolver.py +126 -0
- raise_cli/session/state.py +187 -0
- raise_cli/skills/__init__.py +44 -0
- raise_cli/skills/locator.py +141 -0
- raise_cli/skills/name_checker.py +199 -0
- raise_cli/skills/parser.py +145 -0
- raise_cli/skills/scaffold.py +212 -0
- raise_cli/skills/schema.py +132 -0
- raise_cli/skills/skillsets.py +195 -0
- raise_cli/skills/validator.py +197 -0
- raise_cli/skills_base/__init__.py +80 -0
- raise_cli/skills_base/contract-template.md +60 -0
- raise_cli/skills_base/preamble.md +37 -0
- raise_cli/skills_base/rai-architecture-review/SKILL.md +137 -0
- raise_cli/skills_base/rai-debug/SKILL.md +171 -0
- raise_cli/skills_base/rai-discover/SKILL.md +167 -0
- raise_cli/skills_base/rai-discover-document/SKILL.md +128 -0
- raise_cli/skills_base/rai-discover-scan/SKILL.md +147 -0
- raise_cli/skills_base/rai-discover-start/SKILL.md +145 -0
- raise_cli/skills_base/rai-discover-validate/SKILL.md +142 -0
- raise_cli/skills_base/rai-docs-update/SKILL.md +142 -0
- raise_cli/skills_base/rai-doctor/SKILL.md +120 -0
- raise_cli/skills_base/rai-epic-close/SKILL.md +165 -0
- raise_cli/skills_base/rai-epic-close/templates/retrospective.md +68 -0
- raise_cli/skills_base/rai-epic-design/SKILL.md +146 -0
- raise_cli/skills_base/rai-epic-design/templates/design.md +24 -0
- raise_cli/skills_base/rai-epic-design/templates/scope.md +76 -0
- raise_cli/skills_base/rai-epic-plan/SKILL.md +153 -0
- raise_cli/skills_base/rai-epic-plan/_references/sequencing-strategies.md +67 -0
- raise_cli/skills_base/rai-epic-plan/templates/plan-section.md +49 -0
- raise_cli/skills_base/rai-epic-run/SKILL.md +208 -0
- raise_cli/skills_base/rai-epic-start/SKILL.md +136 -0
- raise_cli/skills_base/rai-epic-start/templates/brief.md +34 -0
- raise_cli/skills_base/rai-mcp-add/SKILL.md +176 -0
- raise_cli/skills_base/rai-mcp-remove/SKILL.md +120 -0
- raise_cli/skills_base/rai-mcp-status/SKILL.md +147 -0
- raise_cli/skills_base/rai-problem-shape/SKILL.md +138 -0
- raise_cli/skills_base/rai-project-create/SKILL.md +144 -0
- raise_cli/skills_base/rai-project-onboard/SKILL.md +162 -0
- raise_cli/skills_base/rai-quality-review/SKILL.md +189 -0
- raise_cli/skills_base/rai-research/SKILL.md +143 -0
- raise_cli/skills_base/rai-research/references/research-prompt-template.md +317 -0
- raise_cli/skills_base/rai-session-close/SKILL.md +176 -0
- raise_cli/skills_base/rai-session-start/SKILL.md +110 -0
- raise_cli/skills_base/rai-story-close/SKILL.md +198 -0
- raise_cli/skills_base/rai-story-design/SKILL.md +203 -0
- raise_cli/skills_base/rai-story-design/references/tech-design-story-v2.md +293 -0
- raise_cli/skills_base/rai-story-implement/SKILL.md +115 -0
- raise_cli/skills_base/rai-story-plan/SKILL.md +135 -0
- raise_cli/skills_base/rai-story-review/SKILL.md +178 -0
- raise_cli/skills_base/rai-story-run/SKILL.md +282 -0
- raise_cli/skills_base/rai-story-start/SKILL.md +166 -0
- raise_cli/skills_base/rai-story-start/templates/story.md +38 -0
- raise_cli/skills_base/rai-welcome/SKILL.md +134 -0
- raise_cli/telemetry/__init__.py +42 -0
- raise_cli/telemetry/schemas.py +285 -0
- raise_cli/telemetry/writer.py +217 -0
- raise_cli/tier/__init__.py +0 -0
- raise_cli/tier/context.py +134 -0
- raise_cli/viz/__init__.py +7 -0
- raise_cli/viz/generator.py +406 -0
- raise_cli-2.2.1.dist-info/METADATA +433 -0
- raise_cli-2.2.1.dist-info/RECORD +264 -0
- raise_cli-2.2.1.dist-info/WHEEL +4 -0
- raise_cli-2.2.1.dist-info/entry_points.txt +40 -0
- raise_cli-2.2.1.dist-info/licenses/LICENSE +190 -0
- raise_cli-2.2.1.dist-info/licenses/NOTICE +4 -0
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
"""Output formatters for skill commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
|
|
11
|
+
from raise_cli.skills.name_checker import NameCheckResult
|
|
12
|
+
from raise_cli.skills.scaffold import ScaffoldResult
|
|
13
|
+
from raise_cli.skills.schema import Skill
|
|
14
|
+
from raise_cli.skills.validator import ValidationResult
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def format_skill_list_human(
|
|
18
|
+
skills: list[Skill],
|
|
19
|
+
grouped: dict[str, list[Skill]],
|
|
20
|
+
console: Console,
|
|
21
|
+
) -> None:
|
|
22
|
+
"""Format skill list for human output.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
skills: All skills (for count).
|
|
26
|
+
grouped: Skills grouped by lifecycle.
|
|
27
|
+
console: Rich console for output.
|
|
28
|
+
"""
|
|
29
|
+
if not skills:
|
|
30
|
+
console.print("No skills found in .claude/skills/")
|
|
31
|
+
return
|
|
32
|
+
|
|
33
|
+
console.print(f"[bold]Skills[/bold] ({len(skills)} found)\n")
|
|
34
|
+
|
|
35
|
+
# Define lifecycle order for consistent output
|
|
36
|
+
lifecycle_order = [
|
|
37
|
+
"session",
|
|
38
|
+
"epic",
|
|
39
|
+
"story",
|
|
40
|
+
"discovery",
|
|
41
|
+
"utility",
|
|
42
|
+
"meta",
|
|
43
|
+
"unknown",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
# Show all lifecycles: ordered first, then any unknown ones alphabetically
|
|
47
|
+
ordered_set = set(lifecycle_order)
|
|
48
|
+
extra_lifecycles = sorted(lc for lc in grouped if lc not in ordered_set)
|
|
49
|
+
display_order = lifecycle_order + extra_lifecycles
|
|
50
|
+
|
|
51
|
+
for lifecycle in display_order:
|
|
52
|
+
if lifecycle not in grouped:
|
|
53
|
+
continue
|
|
54
|
+
|
|
55
|
+
lifecycle_skills = grouped[lifecycle]
|
|
56
|
+
console.print(f"[bold cyan]{lifecycle.capitalize()}[/bold cyan]")
|
|
57
|
+
|
|
58
|
+
table = Table(show_header=False, box=None, padding=(0, 2, 0, 0))
|
|
59
|
+
table.add_column("Name", style="green")
|
|
60
|
+
table.add_column("Version", style="dim")
|
|
61
|
+
table.add_column("Description")
|
|
62
|
+
|
|
63
|
+
for skill in sorted(lifecycle_skills, key=lambda s: s.name):
|
|
64
|
+
# Truncate description if too long
|
|
65
|
+
desc = skill.description
|
|
66
|
+
if len(desc) > 50:
|
|
67
|
+
desc = desc[:47] + "..."
|
|
68
|
+
table.add_row(
|
|
69
|
+
skill.name,
|
|
70
|
+
skill.version or "-",
|
|
71
|
+
desc,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
console.print(table)
|
|
75
|
+
console.print()
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def format_skill_list_json(
|
|
79
|
+
skills: list[Skill],
|
|
80
|
+
skill_dir: str,
|
|
81
|
+
) -> str:
|
|
82
|
+
"""Format skill list as JSON.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
skills: List of skills to format.
|
|
86
|
+
skill_dir: Path to skill directory.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
JSON string.
|
|
90
|
+
"""
|
|
91
|
+
skill_data: list[dict[str, Any]] = []
|
|
92
|
+
for skill in sorted(skills, key=lambda s: s.name):
|
|
93
|
+
skill_data.append(
|
|
94
|
+
{
|
|
95
|
+
"name": skill.name,
|
|
96
|
+
"version": skill.version,
|
|
97
|
+
"lifecycle": skill.lifecycle,
|
|
98
|
+
"description": skill.description,
|
|
99
|
+
"path": skill.path,
|
|
100
|
+
}
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
output = {
|
|
104
|
+
"skills": skill_data,
|
|
105
|
+
"skill_dir": skill_dir,
|
|
106
|
+
"count": len(skills),
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return json.dumps(output, indent=2)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def format_validation_human(
|
|
113
|
+
results: list[ValidationResult],
|
|
114
|
+
console: Console,
|
|
115
|
+
) -> None:
|
|
116
|
+
"""Format validation results for human output.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
results: List of validation results.
|
|
120
|
+
console: Rich console for output.
|
|
121
|
+
"""
|
|
122
|
+
total_errors = sum(r.error_count for r in results)
|
|
123
|
+
total_warnings = sum(r.warning_count for r in results)
|
|
124
|
+
|
|
125
|
+
for result in results:
|
|
126
|
+
console.print(f"\n[bold]Validating:[/bold] {result.path}")
|
|
127
|
+
|
|
128
|
+
if result.is_valid and result.warning_count == 0:
|
|
129
|
+
console.print("[green]✓ All checks passed[/green]")
|
|
130
|
+
continue
|
|
131
|
+
|
|
132
|
+
# Show errors
|
|
133
|
+
for error in result.errors:
|
|
134
|
+
console.print(f"[red]✗ {error}[/red]")
|
|
135
|
+
|
|
136
|
+
# Show warnings
|
|
137
|
+
for warning in result.warnings:
|
|
138
|
+
console.print(f"[yellow]⚠ {warning}[/yellow]")
|
|
139
|
+
|
|
140
|
+
# Summary
|
|
141
|
+
console.print()
|
|
142
|
+
if total_errors == 0 and total_warnings == 0:
|
|
143
|
+
console.print(f"[green]All {len(results)} skill(s) valid[/green]")
|
|
144
|
+
else:
|
|
145
|
+
parts: list[str] = []
|
|
146
|
+
if total_errors > 0:
|
|
147
|
+
parts.append(f"[red]{total_errors} error(s)[/red]")
|
|
148
|
+
if total_warnings > 0:
|
|
149
|
+
parts.append(f"[yellow]{total_warnings} warning(s)[/yellow]")
|
|
150
|
+
console.print(f"{len(results)} skill(s) checked: {', '.join(parts)}")
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def format_validation_json(results: list[ValidationResult]) -> str:
|
|
154
|
+
"""Format validation results as JSON.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
results: List of validation results.
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
JSON string.
|
|
161
|
+
"""
|
|
162
|
+
output: list[dict[str, Any]] = []
|
|
163
|
+
for result in results:
|
|
164
|
+
output.append(
|
|
165
|
+
{
|
|
166
|
+
"path": result.path,
|
|
167
|
+
"valid": result.is_valid,
|
|
168
|
+
"errors": result.errors,
|
|
169
|
+
"warnings": result.warnings,
|
|
170
|
+
}
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
return json.dumps(
|
|
174
|
+
{
|
|
175
|
+
"results": output,
|
|
176
|
+
"total_errors": sum(r.error_count for r in results),
|
|
177
|
+
"total_warnings": sum(r.warning_count for r in results),
|
|
178
|
+
"all_valid": all(r.is_valid for r in results),
|
|
179
|
+
},
|
|
180
|
+
indent=2,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def format_name_check_human(result: NameCheckResult, console: Console) -> None:
|
|
185
|
+
"""Format name check result for human output.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
result: Name check result.
|
|
189
|
+
console: Rich console for output.
|
|
190
|
+
"""
|
|
191
|
+
console.print(f"\n[bold]Checking name:[/bold] {result.name}\n")
|
|
192
|
+
|
|
193
|
+
# Pattern check
|
|
194
|
+
if result.valid_pattern:
|
|
195
|
+
console.print("[green]✓ Follows {domain}-{action} pattern[/green]")
|
|
196
|
+
else:
|
|
197
|
+
console.print("[red]✗ Does not follow {domain}-{action} pattern[/red]")
|
|
198
|
+
|
|
199
|
+
# Skill conflict
|
|
200
|
+
if result.no_skill_conflict:
|
|
201
|
+
console.print("[green]✓ No conflict with existing skills[/green]")
|
|
202
|
+
else:
|
|
203
|
+
console.print(
|
|
204
|
+
f"[red]✗ Conflicts with existing skill: {result.conflicting_skill}[/red]"
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
# CLI conflict
|
|
208
|
+
if result.no_cli_conflict:
|
|
209
|
+
console.print("[green]✓ No CLI command conflict[/green]")
|
|
210
|
+
else:
|
|
211
|
+
console.print(
|
|
212
|
+
f"[red]✗ Conflicts with CLI command: {result.conflicting_command}[/red]"
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
# Lifecycle check
|
|
216
|
+
if result.known_lifecycle:
|
|
217
|
+
console.print("[green]✓ Domain is a known lifecycle[/green]")
|
|
218
|
+
else:
|
|
219
|
+
console.print("[yellow]⚠ Domain is not a standard lifecycle[/yellow]")
|
|
220
|
+
|
|
221
|
+
# Final verdict
|
|
222
|
+
console.print()
|
|
223
|
+
if result.is_valid:
|
|
224
|
+
console.print(f"[bold green]Name '{result.name}' is valid.[/bold green]")
|
|
225
|
+
else:
|
|
226
|
+
console.print(f"[bold red]Name '{result.name}' is not valid.[/bold red]")
|
|
227
|
+
|
|
228
|
+
# Suggestions
|
|
229
|
+
if result.suggestions:
|
|
230
|
+
console.print()
|
|
231
|
+
for suggestion in result.suggestions:
|
|
232
|
+
console.print(f"[dim]→ {suggestion}[/dim]")
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def format_name_check_json(result: NameCheckResult) -> str:
|
|
236
|
+
"""Format name check result as JSON.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
result: Name check result.
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
JSON string.
|
|
243
|
+
"""
|
|
244
|
+
return json.dumps(
|
|
245
|
+
{
|
|
246
|
+
"name": result.name,
|
|
247
|
+
"valid": result.is_valid,
|
|
248
|
+
"checks": {
|
|
249
|
+
"valid_pattern": result.valid_pattern,
|
|
250
|
+
"no_skill_conflict": result.no_skill_conflict,
|
|
251
|
+
"no_cli_conflict": result.no_cli_conflict,
|
|
252
|
+
"known_lifecycle": result.known_lifecycle,
|
|
253
|
+
},
|
|
254
|
+
"conflicts": {
|
|
255
|
+
"skill": result.conflicting_skill,
|
|
256
|
+
"command": result.conflicting_command,
|
|
257
|
+
},
|
|
258
|
+
"suggestions": result.suggestions,
|
|
259
|
+
},
|
|
260
|
+
indent=2,
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def format_scaffold_human(result: ScaffoldResult, console: Console) -> None:
|
|
265
|
+
"""Format scaffold result for human output.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
result: Scaffold result.
|
|
269
|
+
console: Rich console for output.
|
|
270
|
+
"""
|
|
271
|
+
if result.created:
|
|
272
|
+
console.print(f"\n[green]✓ Created skill at:[/green] {result.path}")
|
|
273
|
+
console.print("\n[dim]Next steps:[/dim]")
|
|
274
|
+
console.print(" 1. Edit the SKILL.md to add description and steps")
|
|
275
|
+
console.print(" 2. Run [cyan]raise skill validate[/cyan] to check structure")
|
|
276
|
+
console.print(" 3. Test the skill with Claude Code")
|
|
277
|
+
else:
|
|
278
|
+
console.print("\n[red]✗ Failed to create skill[/red]")
|
|
279
|
+
console.print(f"[red] {result.error}[/red]")
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def format_scaffold_json(result: ScaffoldResult) -> str:
|
|
283
|
+
"""Format scaffold result as JSON.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
result: Scaffold result.
|
|
287
|
+
|
|
288
|
+
Returns:
|
|
289
|
+
JSON string.
|
|
290
|
+
"""
|
|
291
|
+
return json.dumps(
|
|
292
|
+
{
|
|
293
|
+
"created": result.created,
|
|
294
|
+
"path": result.path,
|
|
295
|
+
"error": result.error,
|
|
296
|
+
},
|
|
297
|
+
indent=2,
|
|
298
|
+
)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Changelog parsing and updating for Keep a Changelog format."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def has_unreleased_entries(content: str) -> bool:
|
|
9
|
+
"""Check if the changelog has entries under the [Unreleased] section.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
content: Full changelog text.
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
True if there are non-whitespace entries between [Unreleased] and the next section.
|
|
16
|
+
"""
|
|
17
|
+
match = re.search(
|
|
18
|
+
r"^## \[Unreleased\]\s*$(.*?)(?=^## \[|\Z)",
|
|
19
|
+
content,
|
|
20
|
+
re.DOTALL | re.MULTILINE,
|
|
21
|
+
)
|
|
22
|
+
if not match:
|
|
23
|
+
return False
|
|
24
|
+
return len(match.group(1).strip()) > 0
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def promote_unreleased(content: str, version: str, date: str) -> str:
|
|
28
|
+
"""Move unreleased entries into a new versioned section.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
content: Full changelog text.
|
|
32
|
+
version: New version string (e.g. "2.0.0").
|
|
33
|
+
date: Release date string (e.g. "2026-02-14").
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
Updated changelog text.
|
|
37
|
+
|
|
38
|
+
Raises:
|
|
39
|
+
ValueError: If there are no unreleased entries to promote.
|
|
40
|
+
"""
|
|
41
|
+
if not has_unreleased_entries(content):
|
|
42
|
+
msg = "No unreleased entries to promote"
|
|
43
|
+
raise ValueError(msg)
|
|
44
|
+
|
|
45
|
+
# Extract the unreleased body
|
|
46
|
+
match = re.search(
|
|
47
|
+
r"(^## \[Unreleased\])\s*$(.*?)(?=^## \[)",
|
|
48
|
+
content,
|
|
49
|
+
re.DOTALL | re.MULTILINE,
|
|
50
|
+
)
|
|
51
|
+
if not match:
|
|
52
|
+
msg = "No unreleased entries to promote"
|
|
53
|
+
raise ValueError(msg)
|
|
54
|
+
|
|
55
|
+
unreleased_header = match.group(1)
|
|
56
|
+
unreleased_body = match.group(2).rstrip()
|
|
57
|
+
|
|
58
|
+
# Build replacement: empty Unreleased + new version section
|
|
59
|
+
replacement = f"{unreleased_header}\n\n## [{version}] - {date}\n{unreleased_body}"
|
|
60
|
+
content = content[: match.start()] + replacement + content[match.end() :]
|
|
61
|
+
|
|
62
|
+
# Update link references if they exist
|
|
63
|
+
# Replace: [Unreleased]: .../compare/vOLD...HEAD
|
|
64
|
+
# With: [Unreleased]: .../compare/vNEW...HEAD
|
|
65
|
+
# [NEW]: .../compare/vOLD...vNEW
|
|
66
|
+
old_link_match = re.search(
|
|
67
|
+
r"\[Unreleased\]:\s*(https?://\S+/compare/)v([\d.]+\S*)\.\.\.HEAD",
|
|
68
|
+
content,
|
|
69
|
+
)
|
|
70
|
+
if old_link_match:
|
|
71
|
+
base_url = old_link_match.group(1)
|
|
72
|
+
old_version = old_link_match.group(2)
|
|
73
|
+
new_unreleased_link = f"[Unreleased]: {base_url}v{version}...HEAD"
|
|
74
|
+
new_version_link = f"[{version}]: {base_url}v{old_version}...v{version}"
|
|
75
|
+
content = content.replace(
|
|
76
|
+
old_link_match.group(0),
|
|
77
|
+
f"{new_unreleased_link}\n{new_version_link}",
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
return content
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""Quality gate runner for pre-publish checks."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
import subprocess
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class Gate:
|
|
13
|
+
"""Definition of a quality gate."""
|
|
14
|
+
|
|
15
|
+
name: str
|
|
16
|
+
command: str
|
|
17
|
+
required: bool = True
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class CheckResult:
|
|
22
|
+
"""Result of a single quality gate check."""
|
|
23
|
+
|
|
24
|
+
gate: str
|
|
25
|
+
passed: bool
|
|
26
|
+
message: str
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# Subprocess gates (executed via shell)
|
|
30
|
+
_COMMAND_GATES: list[Gate] = [
|
|
31
|
+
Gate(name="Tests pass", command="uv run pytest --cov --tb=no -q"),
|
|
32
|
+
Gate(name="Type checks clean", command="uv run pyright src/"),
|
|
33
|
+
Gate(name="Lint clean", command="uv run ruff check src/"),
|
|
34
|
+
Gate(name="Security scan", command="uv run bandit -r src/ -q -ll"),
|
|
35
|
+
Gate(name="Build succeeds", command="uv build"),
|
|
36
|
+
Gate(name="Package validates", command="uv run twine check dist/*"),
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _run_command(command: str, cwd: Path) -> tuple[bool, str]:
|
|
41
|
+
"""Run a shell command and return (success, output).
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
command: Shell command string.
|
|
45
|
+
cwd: Working directory.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
Tuple of (passed, message).
|
|
49
|
+
"""
|
|
50
|
+
try:
|
|
51
|
+
import shlex
|
|
52
|
+
|
|
53
|
+
result = subprocess.run(
|
|
54
|
+
shlex.split(command),
|
|
55
|
+
cwd=cwd,
|
|
56
|
+
capture_output=True,
|
|
57
|
+
text=True,
|
|
58
|
+
timeout=300,
|
|
59
|
+
)
|
|
60
|
+
output = result.stdout.strip() or result.stderr.strip()
|
|
61
|
+
if result.returncode == 0:
|
|
62
|
+
return (True, output or "OK")
|
|
63
|
+
return (False, output or f"Exit code {result.returncode}")
|
|
64
|
+
except subprocess.TimeoutExpired:
|
|
65
|
+
return (False, "Timed out after 300s")
|
|
66
|
+
except FileNotFoundError:
|
|
67
|
+
return (False, f"Command not found: {command}")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _extract_version(path: Path, pattern: str) -> str | None:
|
|
71
|
+
"""Extract a version string from a file using a regex pattern.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
path: File to read.
|
|
75
|
+
pattern: Regex with a capture group for the version.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
Extracted version string or None.
|
|
79
|
+
"""
|
|
80
|
+
if not path.exists():
|
|
81
|
+
return None
|
|
82
|
+
content = path.read_text(encoding="utf-8")
|
|
83
|
+
match = re.search(pattern, content)
|
|
84
|
+
if match:
|
|
85
|
+
return match.group(1)
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def run_checks(
|
|
90
|
+
*,
|
|
91
|
+
project_root: Path,
|
|
92
|
+
pyproject_path: Path,
|
|
93
|
+
init_path: Path,
|
|
94
|
+
changelog_path: Path,
|
|
95
|
+
) -> list[CheckResult]:
|
|
96
|
+
"""Run all quality gates and return results.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
project_root: Project root directory.
|
|
100
|
+
pyproject_path: Path to pyproject.toml.
|
|
101
|
+
init_path: Path to __init__.py with __version__.
|
|
102
|
+
changelog_path: Path to CHANGELOG.md.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
List of CheckResult for each gate.
|
|
106
|
+
"""
|
|
107
|
+
from raise_cli.publish.changelog import has_unreleased_entries
|
|
108
|
+
from raise_cli.publish.version import is_pep440
|
|
109
|
+
|
|
110
|
+
results: list[CheckResult] = []
|
|
111
|
+
|
|
112
|
+
# 1-7: Command-based gates
|
|
113
|
+
for gate in _COMMAND_GATES:
|
|
114
|
+
passed, message = _run_command(gate.command, project_root)
|
|
115
|
+
results.append(CheckResult(gate=gate.name, passed=passed, message=message))
|
|
116
|
+
|
|
117
|
+
# 8: Changelog has unreleased entries
|
|
118
|
+
if changelog_path.exists():
|
|
119
|
+
content = changelog_path.read_text(encoding="utf-8")
|
|
120
|
+
has_entries = has_unreleased_entries(content)
|
|
121
|
+
results.append(
|
|
122
|
+
CheckResult(
|
|
123
|
+
gate="CHANGELOG has unreleased entries",
|
|
124
|
+
passed=has_entries,
|
|
125
|
+
message="Unreleased entries found"
|
|
126
|
+
if has_entries
|
|
127
|
+
else "No unreleased entries",
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
else:
|
|
131
|
+
results.append(
|
|
132
|
+
CheckResult(
|
|
133
|
+
gate="CHANGELOG has unreleased entries",
|
|
134
|
+
passed=False,
|
|
135
|
+
message=f"File not found: {changelog_path}",
|
|
136
|
+
)
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
# 9: Version is PEP 440 compliant
|
|
140
|
+
pyproject_version = _extract_version(pyproject_path, r'version\s*=\s*"([^"]*)"')
|
|
141
|
+
if pyproject_version and is_pep440(pyproject_version):
|
|
142
|
+
results.append(
|
|
143
|
+
CheckResult(
|
|
144
|
+
gate="Version PEP 440 compliant",
|
|
145
|
+
passed=True,
|
|
146
|
+
message=f"{pyproject_version} is valid PEP 440",
|
|
147
|
+
)
|
|
148
|
+
)
|
|
149
|
+
else:
|
|
150
|
+
results.append(
|
|
151
|
+
CheckResult(
|
|
152
|
+
gate="Version PEP 440 compliant",
|
|
153
|
+
passed=False,
|
|
154
|
+
message=f"'{pyproject_version}' is not valid PEP 440"
|
|
155
|
+
if pyproject_version
|
|
156
|
+
else "Could not read version from pyproject.toml",
|
|
157
|
+
)
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# 10: Version sync between pyproject.toml and __init__.py
|
|
161
|
+
init_version = _extract_version(init_path, r'__version__\s*=\s*"([^"]*)"')
|
|
162
|
+
if pyproject_version and init_version and pyproject_version == init_version:
|
|
163
|
+
results.append(
|
|
164
|
+
CheckResult(
|
|
165
|
+
gate="Version sync",
|
|
166
|
+
passed=True,
|
|
167
|
+
message=f"Both files: {pyproject_version}",
|
|
168
|
+
)
|
|
169
|
+
)
|
|
170
|
+
else:
|
|
171
|
+
results.append(
|
|
172
|
+
CheckResult(
|
|
173
|
+
gate="Version sync",
|
|
174
|
+
passed=False,
|
|
175
|
+
message=f"pyproject.toml={pyproject_version}, __init__.py={init_version}",
|
|
176
|
+
)
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
return results
|