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,437 @@
|
|
|
1
|
+
"""Scaffold bundled skills into a project with version-aware sync.
|
|
2
|
+
|
|
3
|
+
Copies RaiSE skills from the raise_cli.skills_base package to the project's
|
|
4
|
+
IDE skill directory during `rai init`. Uses the dpkg three-hash algorithm
|
|
5
|
+
to safely update skills: auto-update untouched files, keep customized,
|
|
6
|
+
prompt on conflict.
|
|
7
|
+
|
|
8
|
+
Uses importlib.resources to read bundled skill files (Python 3.9+).
|
|
9
|
+
Handles reference subdirectories (e.g., references/, _references/).
|
|
10
|
+
|
|
11
|
+
Example:
|
|
12
|
+
from raise_cli.onboarding.skills import scaffold_skills
|
|
13
|
+
|
|
14
|
+
result = scaffold_skills(project_path)
|
|
15
|
+
if result.skills_updated:
|
|
16
|
+
print(f"Updated {len(result.skills_updated)} skills")
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import logging
|
|
22
|
+
from importlib.resources import files
|
|
23
|
+
from importlib.resources.abc import Traversable
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
from pydantic import BaseModel, Field
|
|
27
|
+
|
|
28
|
+
from raise_cli.config.agent_plugin import AgentPlugin
|
|
29
|
+
from raise_cli.config.agents import AgentConfig, get_agent_config
|
|
30
|
+
from raise_cli.onboarding.skill_manifest import (
|
|
31
|
+
SkillEntry,
|
|
32
|
+
SkillManifest,
|
|
33
|
+
SkillSyncAction,
|
|
34
|
+
classify_skill,
|
|
35
|
+
compute_content_hash,
|
|
36
|
+
load_skill_manifest,
|
|
37
|
+
save_skill_manifest,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
logger = logging.getLogger(__name__)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class SkillScaffoldResult(BaseModel):
|
|
44
|
+
"""Result of skill scaffolding operation."""
|
|
45
|
+
|
|
46
|
+
# New sync-aware fields
|
|
47
|
+
skills_installed: list[str] = Field(default_factory=list)
|
|
48
|
+
skills_updated: list[str] = Field(default_factory=list)
|
|
49
|
+
skills_conflicted: list[str] = Field(default_factory=list)
|
|
50
|
+
skills_kept: list[str] = Field(default_factory=list)
|
|
51
|
+
skills_overwritten: list[str] = Field(default_factory=list)
|
|
52
|
+
skills_current: list[str] = Field(default_factory=list)
|
|
53
|
+
|
|
54
|
+
# Backward-compat fields
|
|
55
|
+
skills_copied: int = 0
|
|
56
|
+
already_existed: bool = False
|
|
57
|
+
files_copied: list[str] = Field(default_factory=list)
|
|
58
|
+
files_skipped: list[str] = Field(default_factory=list)
|
|
59
|
+
skills_skipped_names: list[str] = Field(default_factory=list)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _apply_plugin_transform(
|
|
63
|
+
content: str,
|
|
64
|
+
plugin: AgentPlugin,
|
|
65
|
+
agent_config: AgentConfig,
|
|
66
|
+
) -> str:
|
|
67
|
+
"""Apply plugin.transform_skill to a SKILL.md content string."""
|
|
68
|
+
import yaml
|
|
69
|
+
|
|
70
|
+
from raise_cli.skills.parser import parse_frontmatter
|
|
71
|
+
|
|
72
|
+
fm, body = parse_frontmatter(content)
|
|
73
|
+
fm_out, body_out = plugin.transform_skill(fm, body, agent_config)
|
|
74
|
+
# Re-serialize: frontmatter + body
|
|
75
|
+
if fm_out:
|
|
76
|
+
return f"---\n{yaml.dump(fm_out, default_flow_style=False, allow_unicode=True)}---\n{body_out}"
|
|
77
|
+
return body_out
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _read_bundled_content(
|
|
81
|
+
base: Traversable,
|
|
82
|
+
skill_name: str,
|
|
83
|
+
*,
|
|
84
|
+
plugin: AgentPlugin | None = None,
|
|
85
|
+
agent_config: AgentConfig | None = None,
|
|
86
|
+
) -> str:
|
|
87
|
+
"""Read bundled SKILL.md content, applying plugin transforms if needed.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
base: The skills_base package traversable.
|
|
91
|
+
skill_name: Name of the skill directory.
|
|
92
|
+
plugin: Optional plugin for content transforms.
|
|
93
|
+
agent_config: Agent config for plugin.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
The SKILL.md content as it would be written to disk.
|
|
97
|
+
"""
|
|
98
|
+
raw = (base / skill_name / "SKILL.md").read_text(encoding="utf-8")
|
|
99
|
+
if plugin is not None and agent_config is not None:
|
|
100
|
+
raw = _apply_plugin_transform(raw, plugin, agent_config)
|
|
101
|
+
return raw
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def copy_skill_tree(
|
|
105
|
+
source_dir: Path | Traversable,
|
|
106
|
+
dest_dir: Path,
|
|
107
|
+
result: SkillScaffoldResult,
|
|
108
|
+
*,
|
|
109
|
+
plugin: AgentPlugin | None = None,
|
|
110
|
+
agent_config: AgentConfig | None = None,
|
|
111
|
+
overwrite: bool = False,
|
|
112
|
+
) -> int:
|
|
113
|
+
"""Recursively copy skill files from source to destination.
|
|
114
|
+
|
|
115
|
+
Accepts both importlib Traversable (for bundled skills) and Path
|
|
116
|
+
(for filesystem overlay skills). Both types support iterdir(),
|
|
117
|
+
is_file(), is_dir(), read_text(), and name. (AR R1, S340.1)
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
source_dir: Resource directory (Traversable or Path).
|
|
121
|
+
dest_dir: Target directory on filesystem.
|
|
122
|
+
result: Result object to track copied/skipped files.
|
|
123
|
+
plugin: Optional plugin to transform SKILL.md files.
|
|
124
|
+
agent_config: Agent config passed to plugin.
|
|
125
|
+
overwrite: If True, overwrite existing files.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
Number of files copied.
|
|
129
|
+
"""
|
|
130
|
+
copied = 0
|
|
131
|
+
for item in source_dir.iterdir():
|
|
132
|
+
if item.name == "__init__.py" or item.name == "__pycache__":
|
|
133
|
+
continue
|
|
134
|
+
dest = dest_dir / item.name
|
|
135
|
+
if item.is_file():
|
|
136
|
+
if dest.exists() and not overwrite:
|
|
137
|
+
result.files_skipped.append(str(dest))
|
|
138
|
+
logger.debug("Skipped (exists): %s", dest)
|
|
139
|
+
continue
|
|
140
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
141
|
+
raw_content = item.read_text(encoding="utf-8")
|
|
142
|
+
if (
|
|
143
|
+
plugin is not None
|
|
144
|
+
and agent_config is not None
|
|
145
|
+
and item.name == "SKILL.md"
|
|
146
|
+
):
|
|
147
|
+
raw_content = _apply_plugin_transform(raw_content, plugin, agent_config)
|
|
148
|
+
dest.write_text(raw_content, encoding="utf-8")
|
|
149
|
+
result.files_copied.append(str(dest))
|
|
150
|
+
copied += 1
|
|
151
|
+
logger.debug("Copied: %s", dest)
|
|
152
|
+
elif item.is_dir():
|
|
153
|
+
copied += copy_skill_tree(
|
|
154
|
+
item,
|
|
155
|
+
dest,
|
|
156
|
+
result,
|
|
157
|
+
plugin=plugin,
|
|
158
|
+
agent_config=agent_config,
|
|
159
|
+
overwrite=overwrite,
|
|
160
|
+
)
|
|
161
|
+
return copied
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _get_cli_version() -> str:
|
|
165
|
+
"""Get current skills_base version for manifest."""
|
|
166
|
+
try:
|
|
167
|
+
from raise_cli.skills_base import __version__
|
|
168
|
+
|
|
169
|
+
return __version__
|
|
170
|
+
except ImportError:
|
|
171
|
+
return "unknown"
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def scaffold_skills(
|
|
175
|
+
project_root: Path,
|
|
176
|
+
*,
|
|
177
|
+
agent_config: AgentConfig | None = None,
|
|
178
|
+
plugin: AgentPlugin | None = None,
|
|
179
|
+
force: bool = False,
|
|
180
|
+
skip_updates: bool = False,
|
|
181
|
+
dry_run: bool = False,
|
|
182
|
+
skill_set: str | None = None,
|
|
183
|
+
) -> SkillScaffoldResult:
|
|
184
|
+
"""Copy bundled skills to project skill directory with version-aware sync.
|
|
185
|
+
|
|
186
|
+
Uses the dpkg three-hash algorithm to detect changes:
|
|
187
|
+
- Untouched files are auto-updated silently
|
|
188
|
+
- Customized files are preserved (user's version kept)
|
|
189
|
+
- Conflicts (both changed) default to keep in non-interactive mode
|
|
190
|
+
|
|
191
|
+
When ``skill_set`` is provided, overlay skills from
|
|
192
|
+
``.raise/skills/{skill_set}/`` are copied on top of builtins
|
|
193
|
+
after the standard deployment. Same-name overlay wins. (S340.1)
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
project_root: Project root directory.
|
|
197
|
+
agent_config: Agent configuration. Defaults to Claude.
|
|
198
|
+
plugin: Optional plugin to transform SKILL.md files during copy.
|
|
199
|
+
force: If True, overwrite all files without prompting.
|
|
200
|
+
skip_updates: If True, only install new skills (legacy behavior).
|
|
201
|
+
dry_run: If True, compute actions but don't write files.
|
|
202
|
+
skill_set: Skill set name for overlay (e.g. "my-team").
|
|
203
|
+
None = builtins only.
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
SkillScaffoldResult with details of what was done.
|
|
207
|
+
"""
|
|
208
|
+
from raise_cli.skills_base import DISTRIBUTABLE_SKILLS
|
|
209
|
+
|
|
210
|
+
config = agent_config or get_agent_config()
|
|
211
|
+
if config.skills_dir is None:
|
|
212
|
+
return SkillScaffoldResult()
|
|
213
|
+
|
|
214
|
+
base = files("raise_cli.skills_base")
|
|
215
|
+
skills_dir = project_root / config.skills_dir
|
|
216
|
+
result = SkillScaffoldResult()
|
|
217
|
+
manifest = load_skill_manifest(project_root) or SkillManifest()
|
|
218
|
+
cli_version = _get_cli_version()
|
|
219
|
+
batch_keep = False
|
|
220
|
+
batch_overwrite = False
|
|
221
|
+
|
|
222
|
+
for skill_name in DISTRIBUTABLE_SKILLS:
|
|
223
|
+
skill_dest = skills_dir / skill_name
|
|
224
|
+
skill_md = skill_dest / "SKILL.md"
|
|
225
|
+
source = base / skill_name
|
|
226
|
+
|
|
227
|
+
bundled_content = _read_bundled_content(
|
|
228
|
+
base, skill_name, plugin=plugin, agent_config=config
|
|
229
|
+
)
|
|
230
|
+
hash_new = compute_content_hash(bundled_content)
|
|
231
|
+
|
|
232
|
+
# --- Case: skill doesn't exist on disk → install ---
|
|
233
|
+
if not skill_md.exists():
|
|
234
|
+
if not dry_run:
|
|
235
|
+
copied = copy_skill_tree(
|
|
236
|
+
source,
|
|
237
|
+
skill_dest,
|
|
238
|
+
result,
|
|
239
|
+
plugin=plugin,
|
|
240
|
+
agent_config=config,
|
|
241
|
+
overwrite=True,
|
|
242
|
+
)
|
|
243
|
+
manifest.skills[skill_name] = SkillEntry(
|
|
244
|
+
sha256=hash_new,
|
|
245
|
+
version=cli_version,
|
|
246
|
+
)
|
|
247
|
+
if copied > 0:
|
|
248
|
+
result.skills_copied += 1
|
|
249
|
+
result.skills_installed.append(skill_name)
|
|
250
|
+
continue
|
|
251
|
+
|
|
252
|
+
# --- Skill exists on disk → classify with three-hash ---
|
|
253
|
+
hash_on_disk = compute_content_hash(skill_md.read_text(encoding="utf-8"))
|
|
254
|
+
entry = manifest.skills.get(skill_name)
|
|
255
|
+
hash_distributed = entry.sha256 if entry else None
|
|
256
|
+
|
|
257
|
+
action = classify_skill(hash_distributed, hash_on_disk, hash_new)
|
|
258
|
+
|
|
259
|
+
if action == SkillSyncAction.CURRENT:
|
|
260
|
+
result.skills_current.append(skill_name)
|
|
261
|
+
# Ensure manifest entry exists (legacy fixup)
|
|
262
|
+
if skill_name not in manifest.skills:
|
|
263
|
+
manifest.skills[skill_name] = SkillEntry(
|
|
264
|
+
sha256=hash_on_disk,
|
|
265
|
+
version=cli_version,
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
elif action == SkillSyncAction.AUTO_UPDATE:
|
|
269
|
+
if skip_updates:
|
|
270
|
+
result.skills_current.append(skill_name)
|
|
271
|
+
result.skills_skipped_names.append(skill_name)
|
|
272
|
+
elif not dry_run:
|
|
273
|
+
# Safe to overwrite — user hasn't touched it
|
|
274
|
+
skill_md.write_text(bundled_content, encoding="utf-8")
|
|
275
|
+
result.files_copied.append(str(skill_md))
|
|
276
|
+
# Also update reference files
|
|
277
|
+
copy_skill_tree(
|
|
278
|
+
source,
|
|
279
|
+
skill_dest,
|
|
280
|
+
result,
|
|
281
|
+
plugin=plugin,
|
|
282
|
+
agent_config=config,
|
|
283
|
+
overwrite=True,
|
|
284
|
+
)
|
|
285
|
+
manifest.skills[skill_name] = SkillEntry(
|
|
286
|
+
sha256=hash_new,
|
|
287
|
+
version=cli_version,
|
|
288
|
+
)
|
|
289
|
+
result.skills_updated.append(skill_name)
|
|
290
|
+
else:
|
|
291
|
+
result.skills_updated.append(skill_name)
|
|
292
|
+
|
|
293
|
+
elif action == SkillSyncAction.KEEP_USER:
|
|
294
|
+
result.skills_current.append(skill_name)
|
|
295
|
+
result.skills_skipped_names.append(skill_name)
|
|
296
|
+
|
|
297
|
+
elif action == SkillSyncAction.CONFLICT:
|
|
298
|
+
if force or batch_overwrite:
|
|
299
|
+
if not dry_run:
|
|
300
|
+
skill_md.write_text(bundled_content, encoding="utf-8")
|
|
301
|
+
result.files_copied.append(str(skill_md))
|
|
302
|
+
copy_skill_tree(
|
|
303
|
+
source,
|
|
304
|
+
skill_dest,
|
|
305
|
+
result,
|
|
306
|
+
plugin=plugin,
|
|
307
|
+
agent_config=config,
|
|
308
|
+
overwrite=True,
|
|
309
|
+
)
|
|
310
|
+
manifest.skills[skill_name] = SkillEntry(
|
|
311
|
+
sha256=hash_new,
|
|
312
|
+
version=cli_version,
|
|
313
|
+
)
|
|
314
|
+
result.skills_overwritten.append(skill_name)
|
|
315
|
+
elif skip_updates or batch_keep:
|
|
316
|
+
result.skills_conflicted.append(skill_name)
|
|
317
|
+
result.skills_skipped_names.append(skill_name)
|
|
318
|
+
elif dry_run:
|
|
319
|
+
result.skills_conflicted.append(skill_name)
|
|
320
|
+
else:
|
|
321
|
+
# Interactive conflict resolution
|
|
322
|
+
from raise_cli.onboarding.skill_conflict import (
|
|
323
|
+
ConflictAction,
|
|
324
|
+
prompt_skill_conflict,
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
on_disk_content = skill_md.read_text(encoding="utf-8")
|
|
328
|
+
user_action = prompt_skill_conflict(
|
|
329
|
+
skill_name,
|
|
330
|
+
on_disk_content,
|
|
331
|
+
bundled_content,
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
if user_action == ConflictAction.KEEP:
|
|
335
|
+
result.skills_kept.append(skill_name)
|
|
336
|
+
elif user_action == ConflictAction.KEEP_ALL:
|
|
337
|
+
result.skills_kept.append(skill_name)
|
|
338
|
+
batch_keep = True
|
|
339
|
+
elif user_action in (
|
|
340
|
+
ConflictAction.OVERWRITE,
|
|
341
|
+
ConflictAction.OVERWRITE_ALL,
|
|
342
|
+
):
|
|
343
|
+
skill_md.write_text(bundled_content, encoding="utf-8")
|
|
344
|
+
result.files_copied.append(str(skill_md))
|
|
345
|
+
copy_skill_tree(
|
|
346
|
+
source,
|
|
347
|
+
skill_dest,
|
|
348
|
+
result,
|
|
349
|
+
plugin=plugin,
|
|
350
|
+
agent_config=config,
|
|
351
|
+
overwrite=True,
|
|
352
|
+
)
|
|
353
|
+
manifest.skills[skill_name] = SkillEntry(
|
|
354
|
+
sha256=hash_new,
|
|
355
|
+
version=cli_version,
|
|
356
|
+
)
|
|
357
|
+
result.skills_overwritten.append(skill_name)
|
|
358
|
+
if user_action == ConflictAction.OVERWRITE_ALL:
|
|
359
|
+
batch_overwrite = True
|
|
360
|
+
elif user_action == ConflictAction.BACKUP_OVERWRITE:
|
|
361
|
+
# Save backup before overwriting
|
|
362
|
+
backup_path = skill_md.with_suffix(".md.bak")
|
|
363
|
+
backup_path.write_text(on_disk_content, encoding="utf-8")
|
|
364
|
+
skill_md.write_text(bundled_content, encoding="utf-8")
|
|
365
|
+
result.files_copied.append(str(skill_md))
|
|
366
|
+
copy_skill_tree(
|
|
367
|
+
source,
|
|
368
|
+
skill_dest,
|
|
369
|
+
result,
|
|
370
|
+
plugin=plugin,
|
|
371
|
+
agent_config=config,
|
|
372
|
+
overwrite=True,
|
|
373
|
+
)
|
|
374
|
+
manifest.skills[skill_name] = SkillEntry(
|
|
375
|
+
sha256=hash_new,
|
|
376
|
+
version=cli_version,
|
|
377
|
+
)
|
|
378
|
+
result.skills_overwritten.append(skill_name)
|
|
379
|
+
|
|
380
|
+
elif action == SkillSyncAction.LEGACY:
|
|
381
|
+
# No manifest entry — first encounter
|
|
382
|
+
if hash_on_disk == hash_new:
|
|
383
|
+
# File matches bundled — safe to record
|
|
384
|
+
manifest.skills[skill_name] = SkillEntry(
|
|
385
|
+
sha256=hash_new,
|
|
386
|
+
version=cli_version,
|
|
387
|
+
)
|
|
388
|
+
else:
|
|
389
|
+
# File differs — treat as customized, record on-disk hash
|
|
390
|
+
manifest.skills[skill_name] = SkillEntry(
|
|
391
|
+
sha256=hash_on_disk,
|
|
392
|
+
version=cli_version,
|
|
393
|
+
)
|
|
394
|
+
result.skills_current.append(skill_name)
|
|
395
|
+
result.files_skipped.append(str(skill_md))
|
|
396
|
+
result.skills_skipped_names.append(skill_name)
|
|
397
|
+
|
|
398
|
+
# --- Skill set overlay (S340.1) ---
|
|
399
|
+
if skill_set is not None and not dry_run:
|
|
400
|
+
from raise_cli.config.paths import get_raise_dir
|
|
401
|
+
|
|
402
|
+
overlay_dir = get_raise_dir(project_root) / "skills" / skill_set
|
|
403
|
+
if overlay_dir.is_dir():
|
|
404
|
+
for skill_dir in sorted(overlay_dir.iterdir()):
|
|
405
|
+
if not skill_dir.is_dir():
|
|
406
|
+
continue
|
|
407
|
+
if not (skill_dir / "SKILL.md").exists():
|
|
408
|
+
continue
|
|
409
|
+
copy_skill_tree(
|
|
410
|
+
skill_dir,
|
|
411
|
+
skills_dir / skill_dir.name,
|
|
412
|
+
result,
|
|
413
|
+
plugin=plugin,
|
|
414
|
+
agent_config=config,
|
|
415
|
+
overwrite=True,
|
|
416
|
+
)
|
|
417
|
+
overlay_content = (skill_dir / "SKILL.md").read_text(encoding="utf-8")
|
|
418
|
+
manifest.skills[skill_dir.name] = SkillEntry(
|
|
419
|
+
sha256=compute_content_hash(overlay_content),
|
|
420
|
+
version=cli_version,
|
|
421
|
+
origin="project",
|
|
422
|
+
)
|
|
423
|
+
manifest.skill_set = skill_set
|
|
424
|
+
else:
|
|
425
|
+
logger.warning("Skill set '%s' not found at %s", skill_set, overlay_dir)
|
|
426
|
+
|
|
427
|
+
# Update backward-compat flag
|
|
428
|
+
result.already_existed = (
|
|
429
|
+
len(result.skills_installed) == 0 and len(result.skills_updated) == 0
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
# Persist manifest
|
|
433
|
+
if not dry_run:
|
|
434
|
+
manifest.raise_cli_version = cli_version
|
|
435
|
+
save_skill_manifest(manifest, project_root)
|
|
436
|
+
|
|
437
|
+
return result
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Scaffold workflow shims for IDEs that support workflows.
|
|
2
|
+
|
|
3
|
+
Generates one workflow file per distributable skill.
|
|
4
|
+
Each workflow is a minimal .md with YAML frontmatter (name + description)
|
|
5
|
+
and a one-line body referencing the skill.
|
|
6
|
+
|
|
7
|
+
IDEs without workflows (e.g., Claude Code) get a no-op.
|
|
8
|
+
Per-file idempotency: existing files are never overwritten.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
from importlib.resources import files
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from pydantic import BaseModel, Field
|
|
18
|
+
|
|
19
|
+
from raise_cli.config.agents import AgentConfig, get_agent_config
|
|
20
|
+
from raise_cli.skills.parser import parse_frontmatter
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class WorkflowScaffoldResult(BaseModel):
|
|
26
|
+
"""Result of workflow scaffolding operation."""
|
|
27
|
+
|
|
28
|
+
workflows_created: int = 0
|
|
29
|
+
already_existed: bool = False
|
|
30
|
+
skipped_no_workflows_dir: bool = False
|
|
31
|
+
files_created: list[str] = Field(default_factory=list)
|
|
32
|
+
files_skipped: list[str] = Field(default_factory=list)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def scaffold_workflows(
|
|
36
|
+
project_root: Path,
|
|
37
|
+
*,
|
|
38
|
+
agent_config: AgentConfig | None = None,
|
|
39
|
+
) -> WorkflowScaffoldResult:
|
|
40
|
+
"""Generate workflow shim files for each distributable skill.
|
|
41
|
+
|
|
42
|
+
Reads each skill's SKILL.md frontmatter (name + description)
|
|
43
|
+
and writes a minimal workflow file that references the skill.
|
|
44
|
+
Skips when the IDE has no workflows_dir (e.g., Claude Code).
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
project_root: Project root directory.
|
|
48
|
+
ide_config: IDE configuration. Defaults to Claude.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
WorkflowScaffoldResult with details of what was created or skipped.
|
|
52
|
+
"""
|
|
53
|
+
from raise_cli.skills_base import DISTRIBUTABLE_SKILLS
|
|
54
|
+
|
|
55
|
+
config = agent_config or get_agent_config()
|
|
56
|
+
result = WorkflowScaffoldResult()
|
|
57
|
+
|
|
58
|
+
if config.workflows_dir is None:
|
|
59
|
+
result.skipped_no_workflows_dir = True
|
|
60
|
+
return result
|
|
61
|
+
|
|
62
|
+
base = files("raise_cli.skills_base")
|
|
63
|
+
workflows_dir = project_root / config.workflows_dir
|
|
64
|
+
workflows_dir.mkdir(parents=True, exist_ok=True)
|
|
65
|
+
|
|
66
|
+
for skill_name in DISTRIBUTABLE_SKILLS:
|
|
67
|
+
workflow_file = workflows_dir / f"{skill_name}.md"
|
|
68
|
+
|
|
69
|
+
if workflow_file.exists():
|
|
70
|
+
result.files_skipped.append(skill_name)
|
|
71
|
+
logger.debug("Skipped (exists): %s", workflow_file)
|
|
72
|
+
continue
|
|
73
|
+
|
|
74
|
+
# Read skill frontmatter for name + description
|
|
75
|
+
skill_md = base / skill_name / "SKILL.md"
|
|
76
|
+
content = skill_md.read_text(encoding="utf-8")
|
|
77
|
+
frontmatter, _body = parse_frontmatter(content)
|
|
78
|
+
|
|
79
|
+
name = frontmatter.get("name", skill_name)
|
|
80
|
+
description = frontmatter.get("description", "")
|
|
81
|
+
|
|
82
|
+
# Write workflow shim
|
|
83
|
+
skills_path = f"{config.skills_dir}/{skill_name}/SKILL.md"
|
|
84
|
+
workflow_content = (
|
|
85
|
+
f"---\nname: {name}\n"
|
|
86
|
+
f"description: >\n"
|
|
87
|
+
f" {description.strip()}\n"
|
|
88
|
+
f"---\n\n"
|
|
89
|
+
f"Run the `{skill_name}` skill from `{skills_path}`.\n"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
workflow_file.write_text(workflow_content, encoding="utf-8")
|
|
93
|
+
result.files_created.append(str(workflow_file))
|
|
94
|
+
result.workflows_created += 1
|
|
95
|
+
logger.debug("Created: %s", workflow_file)
|
|
96
|
+
|
|
97
|
+
result.already_existed = (
|
|
98
|
+
result.workflows_created == 0 and len(result.files_skipped) > 0
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
return result
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Output formatting module for raise-cli.
|
|
2
|
+
|
|
3
|
+
Provides format-aware output (human, JSON, table) for CLI commands.
|
|
4
|
+
|
|
5
|
+
Example:
|
|
6
|
+
>>> from raise_cli.output import get_console
|
|
7
|
+
>>>
|
|
8
|
+
>>> console = get_console()
|
|
9
|
+
>>> console.print_success("Done!", details={"duration": "2.3s"})
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from raise_cli.output.console import (
|
|
15
|
+
OutputConsole,
|
|
16
|
+
OutputFormat,
|
|
17
|
+
configure_console,
|
|
18
|
+
get_console,
|
|
19
|
+
set_console,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"OutputConsole",
|
|
24
|
+
"OutputFormat",
|
|
25
|
+
"get_console",
|
|
26
|
+
"set_console",
|
|
27
|
+
"configure_console",
|
|
28
|
+
]
|