rai-cli 2.0.0__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.
- rai_cli/__init__.py +38 -0
- rai_cli/__main__.py +30 -0
- rai_cli/cli/__init__.py +3 -0
- rai_cli/cli/commands/__init__.py +3 -0
- rai_cli/cli/commands/backlog.py +510 -0
- rai_cli/cli/commands/base.py +101 -0
- rai_cli/cli/commands/discover.py +545 -0
- rai_cli/cli/commands/init.py +457 -0
- rai_cli/cli/commands/memory.py +1649 -0
- rai_cli/cli/commands/profile.py +51 -0
- rai_cli/cli/commands/publish.py +264 -0
- rai_cli/cli/commands/release.py +89 -0
- rai_cli/cli/commands/session.py +375 -0
- rai_cli/cli/commands/skill.py +226 -0
- rai_cli/cli/error_handler.py +158 -0
- rai_cli/cli/main.py +143 -0
- rai_cli/config/__init__.py +11 -0
- rai_cli/config/paths.py +344 -0
- rai_cli/config/settings.py +180 -0
- rai_cli/context/__init__.py +42 -0
- rai_cli/context/analyzers/__init__.py +16 -0
- rai_cli/context/analyzers/models.py +36 -0
- rai_cli/context/analyzers/protocol.py +43 -0
- rai_cli/context/analyzers/python.py +291 -0
- rai_cli/context/builder.py +1598 -0
- rai_cli/context/diff.py +213 -0
- rai_cli/context/extractors/__init__.py +13 -0
- rai_cli/context/extractors/skills.py +121 -0
- rai_cli/context/graph.py +300 -0
- rai_cli/context/models.py +135 -0
- rai_cli/context/query.py +527 -0
- rai_cli/core/__init__.py +37 -0
- rai_cli/core/files.py +66 -0
- rai_cli/core/text.py +174 -0
- rai_cli/core/tools.py +441 -0
- rai_cli/discovery/__init__.py +50 -0
- rai_cli/discovery/analyzer.py +600 -0
- rai_cli/discovery/drift.py +355 -0
- rai_cli/discovery/scanner.py +1204 -0
- rai_cli/engines/__init__.py +3 -0
- rai_cli/exceptions.py +215 -0
- rai_cli/governance/__init__.py +11 -0
- rai_cli/governance/extractor.py +324 -0
- rai_cli/governance/models.py +134 -0
- rai_cli/governance/parsers/__init__.py +35 -0
- rai_cli/governance/parsers/adr.py +255 -0
- rai_cli/governance/parsers/backlog.py +304 -0
- rai_cli/governance/parsers/constitution.py +100 -0
- rai_cli/governance/parsers/epic.py +299 -0
- rai_cli/governance/parsers/glossary.py +297 -0
- rai_cli/governance/parsers/guardrails.py +326 -0
- rai_cli/governance/parsers/prd.py +93 -0
- rai_cli/governance/parsers/roadmap.py +99 -0
- rai_cli/governance/parsers/vision.py +97 -0
- rai_cli/handlers/__init__.py +3 -0
- rai_cli/memory/__init__.py +58 -0
- rai_cli/memory/loader.py +247 -0
- rai_cli/memory/migration.py +241 -0
- rai_cli/memory/models.py +169 -0
- rai_cli/memory/writer.py +485 -0
- rai_cli/onboarding/__init__.py +98 -0
- rai_cli/onboarding/bootstrap.py +162 -0
- rai_cli/onboarding/claudemd.py +207 -0
- rai_cli/onboarding/conventions.py +742 -0
- rai_cli/onboarding/detection.py +155 -0
- rai_cli/onboarding/governance.py +443 -0
- rai_cli/onboarding/manifest.py +115 -0
- rai_cli/onboarding/memory_md.py +387 -0
- rai_cli/onboarding/migration.py +207 -0
- rai_cli/onboarding/profile.py +567 -0
- rai_cli/onboarding/skills.py +114 -0
- rai_cli/output/__init__.py +28 -0
- rai_cli/output/console.py +394 -0
- rai_cli/output/formatters/__init__.py +9 -0
- rai_cli/output/formatters/discover.py +439 -0
- rai_cli/output/formatters/skill.py +293 -0
- rai_cli/publish/__init__.py +3 -0
- rai_cli/publish/changelog.py +80 -0
- rai_cli/publish/check.py +179 -0
- rai_cli/publish/version.py +168 -0
- rai_cli/rai_base/__init__.py +22 -0
- rai_cli/rai_base/framework/__init__.py +7 -0
- rai_cli/rai_base/framework/methodology.yaml +235 -0
- rai_cli/rai_base/governance/__init__.py +1 -0
- rai_cli/rai_base/governance/architecture/__init__.py +1 -0
- rai_cli/rai_base/governance/architecture/domain-model.md +20 -0
- rai_cli/rai_base/governance/architecture/system-context.md +34 -0
- rai_cli/rai_base/governance/architecture/system-design.md +24 -0
- rai_cli/rai_base/governance/backlog.md +8 -0
- rai_cli/rai_base/governance/guardrails.md +18 -0
- rai_cli/rai_base/governance/prd.md +25 -0
- rai_cli/rai_base/governance/vision.md +16 -0
- rai_cli/rai_base/identity/__init__.py +8 -0
- rai_cli/rai_base/identity/core.md +119 -0
- rai_cli/rai_base/identity/perspective.md +119 -0
- rai_cli/rai_base/memory/__init__.py +7 -0
- rai_cli/rai_base/memory/patterns-base.jsonl +20 -0
- rai_cli/schemas/__init__.py +3 -0
- rai_cli/schemas/session_state.py +112 -0
- rai_cli/session/__init__.py +5 -0
- rai_cli/session/bundle.py +486 -0
- rai_cli/session/close.py +263 -0
- rai_cli/session/resolver.py +113 -0
- rai_cli/session/state.py +187 -0
- rai_cli/skills/__init__.py +44 -0
- rai_cli/skills/locator.py +129 -0
- rai_cli/skills/name_checker.py +199 -0
- rai_cli/skills/parser.py +145 -0
- rai_cli/skills/scaffold.py +185 -0
- rai_cli/skills/schema.py +129 -0
- rai_cli/skills/validator.py +172 -0
- rai_cli/skills_base/__init__.py +65 -0
- rai_cli/skills_base/rai-debug/SKILL.md +297 -0
- rai_cli/skills_base/rai-discover-document/SKILL.md +293 -0
- rai_cli/skills_base/rai-discover-scan/SKILL.md +326 -0
- rai_cli/skills_base/rai-discover-start/SKILL.md +214 -0
- rai_cli/skills_base/rai-discover-validate/SKILL.md +311 -0
- rai_cli/skills_base/rai-docs-update/SKILL.md +271 -0
- rai_cli/skills_base/rai-epic-close/SKILL.md +384 -0
- rai_cli/skills_base/rai-epic-design/SKILL.md +627 -0
- rai_cli/skills_base/rai-epic-plan/SKILL.md +676 -0
- rai_cli/skills_base/rai-epic-plan/_references/sequencing-strategies.md +67 -0
- rai_cli/skills_base/rai-epic-start/SKILL.md +241 -0
- rai_cli/skills_base/rai-project-create/SKILL.md +486 -0
- rai_cli/skills_base/rai-project-onboard/SKILL.md +534 -0
- rai_cli/skills_base/rai-research/SKILL.md +265 -0
- rai_cli/skills_base/rai-research/references/research-prompt-template.md +317 -0
- rai_cli/skills_base/rai-session-close/SKILL.md +170 -0
- rai_cli/skills_base/rai-session-start/SKILL.md +116 -0
- rai_cli/skills_base/rai-story-close/SKILL.md +368 -0
- rai_cli/skills_base/rai-story-design/SKILL.md +340 -0
- rai_cli/skills_base/rai-story-design/references/tech-design-story-v2.md +293 -0
- rai_cli/skills_base/rai-story-implement/SKILL.md +257 -0
- rai_cli/skills_base/rai-story-plan/SKILL.md +308 -0
- rai_cli/skills_base/rai-story-review/SKILL.md +277 -0
- rai_cli/skills_base/rai-story-start/SKILL.md +290 -0
- rai_cli/skills_base/rai-welcome/SKILL.md +227 -0
- rai_cli/telemetry/__init__.py +42 -0
- rai_cli/telemetry/schemas.py +285 -0
- rai_cli/telemetry/writer.py +214 -0
- rai_cli/viz/__init__.py +7 -0
- rai_cli/viz/generator.py +406 -0
- rai_cli-2.0.0.dist-info/METADATA +381 -0
- rai_cli-2.0.0.dist-info/RECORD +148 -0
- rai_cli-2.0.0.dist-info/WHEEL +4 -0
- rai_cli-2.0.0.dist-info/entry_points.txt +2 -0
- rai_cli-2.0.0.dist-info/licenses/LICENSE +190 -0
- rai_cli-2.0.0.dist-info/licenses/NOTICE +4 -0
rai_cli/__init__.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""RaiSE CLI - Reliable AI Software Engineering.
|
|
2
|
+
|
|
3
|
+
A governance framework for AI-assisted software development.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from rai_cli.exceptions import (
|
|
9
|
+
ArtifactNotFoundError,
|
|
10
|
+
ConfigurationError,
|
|
11
|
+
DependencyError,
|
|
12
|
+
GateFailedError,
|
|
13
|
+
GateNotFoundError,
|
|
14
|
+
KataNotFoundError,
|
|
15
|
+
RaiError,
|
|
16
|
+
StateError,
|
|
17
|
+
ValidationError,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
__version__ = "2.0.0"
|
|
21
|
+
__author__ = "Emilio Osorio"
|
|
22
|
+
__license__ = "MIT"
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"__version__",
|
|
26
|
+
"__author__",
|
|
27
|
+
"__license__",
|
|
28
|
+
# Exceptions
|
|
29
|
+
"RaiError",
|
|
30
|
+
"ConfigurationError",
|
|
31
|
+
"KataNotFoundError",
|
|
32
|
+
"GateNotFoundError",
|
|
33
|
+
"ArtifactNotFoundError",
|
|
34
|
+
"DependencyError",
|
|
35
|
+
"StateError",
|
|
36
|
+
"ValidationError",
|
|
37
|
+
"GateFailedError",
|
|
38
|
+
]
|
rai_cli/__main__.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Entry point for python -m rai_cli."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
from rai_cli.cli.main import app
|
|
8
|
+
from rai_cli.exceptions import RaiError
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def main() -> None:
|
|
12
|
+
"""Run the CLI application with error handling.
|
|
13
|
+
|
|
14
|
+
Catches RaiError exceptions and displays them with proper formatting,
|
|
15
|
+
then exits with the appropriate exit code.
|
|
16
|
+
"""
|
|
17
|
+
try:
|
|
18
|
+
app()
|
|
19
|
+
except RaiError as error:
|
|
20
|
+
# Import here to avoid circular imports and for lazy loading
|
|
21
|
+
from rai_cli.cli.error_handler import handle_error
|
|
22
|
+
from rai_cli.cli.main import get_output_format
|
|
23
|
+
|
|
24
|
+
output_format = get_output_format()
|
|
25
|
+
exit_code = handle_error(error, output_format=output_format)
|
|
26
|
+
sys.exit(exit_code)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
if __name__ == "__main__":
|
|
30
|
+
main()
|
rai_cli/cli/__init__.py
ADDED
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
"""CLI commands for backlog management and external provider integration.
|
|
2
|
+
|
|
3
|
+
This module provides the `rai backlog` command group for managing project backlogs
|
|
4
|
+
and integrating with external providers (JIRA, GitLab, etc.).
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
$ rai backlog auth --provider jira # Authenticate with JIRA
|
|
8
|
+
$ rai backlog pull --source jira --epic DEMO-1 # Pull epic from JIRA
|
|
9
|
+
$ rai backlog push --source jira --epic E-DEMO # Push stories to JIRA
|
|
10
|
+
$ rai backlog status --epic E-DEMO # Check authorization status
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
import typer
|
|
19
|
+
from rich.console import Console
|
|
20
|
+
|
|
21
|
+
backlog_app = typer.Typer(
|
|
22
|
+
name="backlog",
|
|
23
|
+
help="Manage backlog and external provider integrations",
|
|
24
|
+
no_args_is_help=True,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
console = Console()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _get_sync_dir(project: str) -> Path:
|
|
31
|
+
"""Get sync directory path for project."""
|
|
32
|
+
return Path(project).resolve() / ".raise" / "rai" / "sync"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _init_jira_client() -> tuple[object, str]:
|
|
36
|
+
"""Initialize JIRA client from stored credentials with auto-refresh.
|
|
37
|
+
|
|
38
|
+
Checks token expiry and refreshes automatically if needed.
|
|
39
|
+
Stores refreshed token back to credentials file.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Tuple of (JiraClient, cloud_id)
|
|
43
|
+
|
|
44
|
+
Raises:
|
|
45
|
+
typer.Exit: If credentials not found or refresh fails
|
|
46
|
+
"""
|
|
47
|
+
try:
|
|
48
|
+
from rai_cli.config.paths import get_credentials_path
|
|
49
|
+
from rai_pro.providers.auth.credentials import load_token, store_token
|
|
50
|
+
from rai_pro.providers.jira.client import JiraClient
|
|
51
|
+
from rai_pro.providers.jira.oauth import (
|
|
52
|
+
OAuthError,
|
|
53
|
+
is_token_expired,
|
|
54
|
+
refresh_access_token,
|
|
55
|
+
)
|
|
56
|
+
except ImportError:
|
|
57
|
+
console.print(
|
|
58
|
+
"[red]Error:[/red] rai-pro is required for JIRA integration.\n"
|
|
59
|
+
"Install with: pip install rai-cli[pro]"
|
|
60
|
+
)
|
|
61
|
+
raise typer.Exit(code=1) from None
|
|
62
|
+
|
|
63
|
+
credentials_path = get_credentials_path()
|
|
64
|
+
token = load_token("jira", credentials_path)
|
|
65
|
+
|
|
66
|
+
if not token:
|
|
67
|
+
console.print(
|
|
68
|
+
"[red]Error:[/red] No JIRA credentials found.\n"
|
|
69
|
+
"Run: rai backlog auth --provider jira"
|
|
70
|
+
)
|
|
71
|
+
raise typer.Exit(code=1)
|
|
72
|
+
|
|
73
|
+
# Auto-refresh expired tokens
|
|
74
|
+
if is_token_expired(token):
|
|
75
|
+
client_id = os.getenv("JIRA_CLIENT_ID", "")
|
|
76
|
+
client_secret = os.getenv("JIRA_CLIENT_SECRET", "")
|
|
77
|
+
|
|
78
|
+
if not client_id or not client_secret:
|
|
79
|
+
console.print(
|
|
80
|
+
"[red]Error:[/red] Token expired and JIRA_CLIENT_ID/JIRA_CLIENT_SECRET "
|
|
81
|
+
"not set for refresh.\nRun: rai backlog auth --provider jira"
|
|
82
|
+
)
|
|
83
|
+
raise typer.Exit(code=1)
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
token = refresh_access_token(token, client_id, client_secret)
|
|
87
|
+
store_token("jira", token, credentials_path)
|
|
88
|
+
console.print("[dim]Token refreshed automatically.[/dim]")
|
|
89
|
+
except OAuthError as e:
|
|
90
|
+
console.print(
|
|
91
|
+
f"[red]Error:[/red] Token refresh failed: {e}\n"
|
|
92
|
+
"Run: rai backlog auth --provider jira"
|
|
93
|
+
)
|
|
94
|
+
raise typer.Exit(code=1) from e
|
|
95
|
+
|
|
96
|
+
cloud_id = os.getenv("JIRA_CLOUD_ID", "")
|
|
97
|
+
if not cloud_id:
|
|
98
|
+
console.print(
|
|
99
|
+
"[red]Error:[/red] JIRA_CLOUD_ID environment variable not set.\n"
|
|
100
|
+
"Get it from: https://api.atlassian.com/oauth/token/accessible-resources"
|
|
101
|
+
)
|
|
102
|
+
raise typer.Exit(code=1)
|
|
103
|
+
|
|
104
|
+
client = JiraClient(
|
|
105
|
+
cloud_id=cloud_id,
|
|
106
|
+
access_token=token["access_token"],
|
|
107
|
+
)
|
|
108
|
+
return client, cloud_id
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@backlog_app.command()
|
|
112
|
+
def auth(
|
|
113
|
+
provider: str = typer.Option(
|
|
114
|
+
...,
|
|
115
|
+
"--provider",
|
|
116
|
+
"-p",
|
|
117
|
+
help="External provider to authenticate with (e.g., 'jira')",
|
|
118
|
+
),
|
|
119
|
+
) -> None:
|
|
120
|
+
"""Authenticate with an external backlog provider.
|
|
121
|
+
|
|
122
|
+
Initiates OAuth 2.0 flow to authenticate with the specified provider.
|
|
123
|
+
Credentials are stored securely in ~/.rai/credentials.json with encryption.
|
|
124
|
+
|
|
125
|
+
Supported providers:
|
|
126
|
+
- jira: Atlassian JIRA Cloud
|
|
127
|
+
|
|
128
|
+
Environment variables (optional):
|
|
129
|
+
- JIRA_CLIENT_ID: Custom OAuth client ID
|
|
130
|
+
- JIRA_CLIENT_SECRET: Custom OAuth client secret
|
|
131
|
+
|
|
132
|
+
Examples:
|
|
133
|
+
$ rai backlog auth --provider jira
|
|
134
|
+
$ JIRA_CLIENT_ID=xxx JIRA_CLIENT_SECRET=yyy rai backlog auth --provider jira
|
|
135
|
+
"""
|
|
136
|
+
# Validate provider
|
|
137
|
+
if provider.lower() not in ["jira"]:
|
|
138
|
+
console.print(
|
|
139
|
+
f"[red]Error:[/red] Provider '{provider}' is not supported.\n"
|
|
140
|
+
f"Supported providers: jira",
|
|
141
|
+
style="red",
|
|
142
|
+
)
|
|
143
|
+
raise typer.Exit(code=1)
|
|
144
|
+
|
|
145
|
+
# Import provider modules
|
|
146
|
+
try:
|
|
147
|
+
from rai_cli.config.paths import get_credentials_path
|
|
148
|
+
from rai_pro.providers.jira.oauth import (
|
|
149
|
+
OAuthError,
|
|
150
|
+
authenticate,
|
|
151
|
+
get_current_user,
|
|
152
|
+
)
|
|
153
|
+
except ImportError:
|
|
154
|
+
console.print(
|
|
155
|
+
"[red]Error:[/red] rai-pro is required for JIRA integration.\n"
|
|
156
|
+
"Install with: pip install rai-cli[pro]"
|
|
157
|
+
)
|
|
158
|
+
raise typer.Exit(code=1) from None
|
|
159
|
+
|
|
160
|
+
# Get credentials path
|
|
161
|
+
credentials_path = get_credentials_path()
|
|
162
|
+
|
|
163
|
+
# Get OAuth credentials from environment or use defaults
|
|
164
|
+
if provider.lower() == "jira":
|
|
165
|
+
client_id = os.getenv("JIRA_CLIENT_ID", "")
|
|
166
|
+
client_secret = os.getenv("JIRA_CLIENT_SECRET", "")
|
|
167
|
+
|
|
168
|
+
if not client_id or not client_secret:
|
|
169
|
+
console.print(
|
|
170
|
+
"[yellow]Warning:[/yellow] Using demo OAuth credentials.\n"
|
|
171
|
+
"For production use, set JIRA_CLIENT_ID and JIRA_CLIENT_SECRET environment variables.",
|
|
172
|
+
style="yellow",
|
|
173
|
+
)
|
|
174
|
+
# Demo credentials (replace with actual demo app credentials)
|
|
175
|
+
client_id = os.getenv("JIRA_CLIENT_ID", "demo-client-id")
|
|
176
|
+
client_secret = os.getenv("JIRA_CLIENT_SECRET", "demo-client-secret")
|
|
177
|
+
|
|
178
|
+
# Run OAuth flow
|
|
179
|
+
try:
|
|
180
|
+
console.print(
|
|
181
|
+
f"[bold]Authenticating with {provider.upper()}...[/bold]"
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
token = authenticate(
|
|
185
|
+
client_id=client_id,
|
|
186
|
+
client_secret=client_secret,
|
|
187
|
+
credentials_path=credentials_path,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
# Get user info to display confirmation
|
|
191
|
+
user_info = get_current_user(token["access_token"])
|
|
192
|
+
email = user_info.get("email", "Unknown")
|
|
193
|
+
|
|
194
|
+
console.print(
|
|
195
|
+
f"\n[green]✓ Authenticated as {email}[/green]",
|
|
196
|
+
style="bold green",
|
|
197
|
+
)
|
|
198
|
+
console.print(
|
|
199
|
+
f"Credentials stored securely in: {credentials_path}",
|
|
200
|
+
style="dim",
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
except OAuthError as e:
|
|
204
|
+
console.print(
|
|
205
|
+
f"\n[red]OAuth Error:[/red] {e}",
|
|
206
|
+
style="red",
|
|
207
|
+
)
|
|
208
|
+
raise typer.Exit(code=1) from e
|
|
209
|
+
|
|
210
|
+
except Exception as e:
|
|
211
|
+
# Handle network errors and other unexpected errors
|
|
212
|
+
console.print(
|
|
213
|
+
f"\n[red]Error:[/red] {e}",
|
|
214
|
+
style="red",
|
|
215
|
+
)
|
|
216
|
+
raise typer.Exit(code=1) from e
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
@backlog_app.command()
|
|
220
|
+
def pull(
|
|
221
|
+
source: str = typer.Option(
|
|
222
|
+
..., "--source", "-s", help="Provider to pull from (jira)"
|
|
223
|
+
),
|
|
224
|
+
epic: str = typer.Option(
|
|
225
|
+
..., "--epic", "-e", help="JIRA epic key (e.g., DEMO-1)"
|
|
226
|
+
),
|
|
227
|
+
epic_id: str = typer.Option(
|
|
228
|
+
"", "--epic-id", help="Local epic ID to assign (default: auto-generate)"
|
|
229
|
+
),
|
|
230
|
+
dry_run: bool = typer.Option(
|
|
231
|
+
False, "--dry-run", help="Preview without executing"
|
|
232
|
+
),
|
|
233
|
+
project: str = typer.Option(
|
|
234
|
+
".", "--project", "-p", help="Project root path"
|
|
235
|
+
),
|
|
236
|
+
) -> None:
|
|
237
|
+
"""Pull epic and stories from JIRA to local sync state.
|
|
238
|
+
|
|
239
|
+
Reads epic and its stories from JIRA, maps them to local IDs,
|
|
240
|
+
and stores sync state in .raise/rai/sync/state.json.
|
|
241
|
+
|
|
242
|
+
Examples:
|
|
243
|
+
$ rai backlog pull --source jira --epic DEMO-1
|
|
244
|
+
$ rai backlog pull --source jira --epic DEMO-1 --epic-id E-DEMO --dry-run
|
|
245
|
+
"""
|
|
246
|
+
if source.lower() != "jira":
|
|
247
|
+
console.print(f"[red]Error:[/red] Source '{source}' not supported. Use 'jira'.")
|
|
248
|
+
raise typer.Exit(code=1)
|
|
249
|
+
|
|
250
|
+
try:
|
|
251
|
+
from rai_pro.providers.jira.sync import pull_epic as _pull_epic
|
|
252
|
+
from rai_pro.providers.jira.sync_state import SyncState, load_state, save_state
|
|
253
|
+
except ImportError:
|
|
254
|
+
console.print(
|
|
255
|
+
"[red]Error:[/red] rai-pro is required for JIRA integration.\n"
|
|
256
|
+
"Install with: pip install rai-cli[pro]"
|
|
257
|
+
)
|
|
258
|
+
raise typer.Exit(code=1) from None
|
|
259
|
+
|
|
260
|
+
client, cloud_id = _init_jira_client()
|
|
261
|
+
sync_dir = _get_sync_dir(project)
|
|
262
|
+
|
|
263
|
+
# Load or create state
|
|
264
|
+
state = load_state(sync_dir)
|
|
265
|
+
project_key = epic.split("-")[0]
|
|
266
|
+
if state is None:
|
|
267
|
+
state = SyncState(cloud_id=cloud_id, project_key=project_key)
|
|
268
|
+
|
|
269
|
+
# Auto-generate epic_id if not provided
|
|
270
|
+
local_epic_id = epic_id or f"E-{project_key}"
|
|
271
|
+
|
|
272
|
+
try:
|
|
273
|
+
result = _pull_epic(
|
|
274
|
+
client=client, # type: ignore[arg-type]
|
|
275
|
+
epic_key=epic,
|
|
276
|
+
epic_id=local_epic_id,
|
|
277
|
+
state=state,
|
|
278
|
+
dry_run=dry_run,
|
|
279
|
+
)
|
|
280
|
+
except Exception as e:
|
|
281
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
282
|
+
raise typer.Exit(code=1) from e
|
|
283
|
+
|
|
284
|
+
# Display results
|
|
285
|
+
prefix = "[yellow][DRY RUN][/yellow] " if dry_run else ""
|
|
286
|
+
|
|
287
|
+
console.print(f"\n{prefix}[bold]Pulling from JIRA...[/bold]\n")
|
|
288
|
+
status_label = "new" if result.epic_imported else "updated"
|
|
289
|
+
console.print(
|
|
290
|
+
f"Epic: {result.epic_key} \"{result.epic_summary}\" "
|
|
291
|
+
f"[{result.epic_status}] → {local_epic_id} ({status_label})"
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
if result.story_details:
|
|
295
|
+
console.print("\nStories:")
|
|
296
|
+
for detail in result.story_details:
|
|
297
|
+
s_action = detail.get("action", "imported")
|
|
298
|
+
s_icon = "✓" if s_action == "imported" else "↻"
|
|
299
|
+
console.print(
|
|
300
|
+
f" {s_icon} {detail.get('jira_key', '?')}: "
|
|
301
|
+
f"\"{detail.get('summary', '')}\" "
|
|
302
|
+
f"[{detail.get('status', '?')}] → {detail.get('local_id', '?')}"
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
console.print(
|
|
306
|
+
f"\nSummary: {result.stories_imported} imported, "
|
|
307
|
+
f"{result.stories_updated} updated."
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
if not dry_run:
|
|
311
|
+
save_state(state, sync_dir)
|
|
312
|
+
console.print(f"State saved to {sync_dir / 'state.json'}", style="dim")
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
@backlog_app.command()
|
|
316
|
+
def push(
|
|
317
|
+
source: str = typer.Option(
|
|
318
|
+
..., "--source", "-s", help="Provider to push to (jira)"
|
|
319
|
+
),
|
|
320
|
+
epic: str = typer.Option(
|
|
321
|
+
..., "--epic", "-e", help="Local epic ID (e.g., E-DEMO)"
|
|
322
|
+
),
|
|
323
|
+
stories_input: str = typer.Option(
|
|
324
|
+
"", "--stories", help="Comma-separated story definitions: id:title,id:title"
|
|
325
|
+
),
|
|
326
|
+
dry_run: bool = typer.Option(
|
|
327
|
+
False, "--dry-run", help="Preview without executing"
|
|
328
|
+
),
|
|
329
|
+
project: str = typer.Option(
|
|
330
|
+
".", "--project", "-p", help="Project root path"
|
|
331
|
+
),
|
|
332
|
+
) -> None:
|
|
333
|
+
"""Push local stories to JIRA under mapped epic.
|
|
334
|
+
|
|
335
|
+
Creates JIRA stories linked to the parent epic. Idempotent — re-running
|
|
336
|
+
won't create duplicates. Requires prior pull to map the epic.
|
|
337
|
+
|
|
338
|
+
Examples:
|
|
339
|
+
$ rai backlog push --source jira --epic E-DEMO \\
|
|
340
|
+
--stories "S-DEMO.1:Define governance,S-DEMO.2:Create checklist"
|
|
341
|
+
$ rai backlog push --source jira --epic E-DEMO --dry-run
|
|
342
|
+
"""
|
|
343
|
+
if source.lower() != "jira":
|
|
344
|
+
console.print(f"[red]Error:[/red] Source '{source}' not supported. Use 'jira'.")
|
|
345
|
+
raise typer.Exit(code=1)
|
|
346
|
+
|
|
347
|
+
try:
|
|
348
|
+
from rai_pro.providers.jira.sync import LocalStory # noqa: I001
|
|
349
|
+
from rai_pro.providers.jira.sync import push_stories as _push_stories
|
|
350
|
+
from rai_pro.providers.jira.sync_state import load_state, save_state
|
|
351
|
+
except ImportError:
|
|
352
|
+
console.print(
|
|
353
|
+
"[red]Error:[/red] rai-pro is required for JIRA integration.\n"
|
|
354
|
+
"Install with: pip install rai-cli[pro]"
|
|
355
|
+
)
|
|
356
|
+
raise typer.Exit(code=1) from None
|
|
357
|
+
|
|
358
|
+
client, _cloud_id = _init_jira_client()
|
|
359
|
+
sync_dir = _get_sync_dir(project)
|
|
360
|
+
|
|
361
|
+
state = load_state(sync_dir)
|
|
362
|
+
if state is None:
|
|
363
|
+
console.print(
|
|
364
|
+
"[red]Error:[/red] No sync state found. Run pull first:\n"
|
|
365
|
+
" rai backlog pull --source jira --epic <JIRA_KEY>"
|
|
366
|
+
)
|
|
367
|
+
raise typer.Exit(code=1)
|
|
368
|
+
|
|
369
|
+
# Parse stories from input
|
|
370
|
+
local_stories: list[LocalStory] = []
|
|
371
|
+
if stories_input:
|
|
372
|
+
for part in stories_input.split(","):
|
|
373
|
+
parts = part.strip().split(":", 1)
|
|
374
|
+
if len(parts) == 2:
|
|
375
|
+
local_stories.append(
|
|
376
|
+
LocalStory(story_id=parts[0].strip(), title=parts[1].strip())
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
if not local_stories:
|
|
380
|
+
console.print(
|
|
381
|
+
"[red]Error:[/red] No stories provided.\n"
|
|
382
|
+
"Use --stories 'S-DEMO.1:Title One,S-DEMO.2:Title Two'"
|
|
383
|
+
)
|
|
384
|
+
raise typer.Exit(code=1)
|
|
385
|
+
|
|
386
|
+
try:
|
|
387
|
+
result = _push_stories(
|
|
388
|
+
client=client, # type: ignore[arg-type]
|
|
389
|
+
epic_id=epic,
|
|
390
|
+
stories=local_stories,
|
|
391
|
+
state=state,
|
|
392
|
+
dry_run=dry_run,
|
|
393
|
+
)
|
|
394
|
+
except ValueError as e:
|
|
395
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
396
|
+
raise typer.Exit(code=1) from e
|
|
397
|
+
except Exception as e:
|
|
398
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
399
|
+
raise typer.Exit(code=1) from e
|
|
400
|
+
|
|
401
|
+
# Display results
|
|
402
|
+
prefix = "[yellow][DRY RUN][/yellow] " if dry_run else ""
|
|
403
|
+
console.print(f"\n{prefix}[bold]Pushing stories to JIRA...[/bold]\n")
|
|
404
|
+
console.print(f"Epic: {epic} → {result.jira_epic_key}\n")
|
|
405
|
+
|
|
406
|
+
if result.created_details:
|
|
407
|
+
label = "Would create" if dry_run else "Created"
|
|
408
|
+
console.print(f"{label}:")
|
|
409
|
+
for detail in result.created_details:
|
|
410
|
+
jira_key = detail.get("jira_key", "pending")
|
|
411
|
+
console.print(
|
|
412
|
+
f" ✓ {detail['story_id']}: \"{detail['title']}\" → {jira_key}"
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
if result.skipped_details:
|
|
416
|
+
console.print("\nSkipped (already synced):")
|
|
417
|
+
for sid in result.skipped_details:
|
|
418
|
+
jira_key = state.stories.get(sid, None)
|
|
419
|
+
key_str = jira_key.jira_key if jira_key else "?"
|
|
420
|
+
console.print(f" - {sid} ({key_str})")
|
|
421
|
+
|
|
422
|
+
console.print(
|
|
423
|
+
f"\nSummary: {result.created} created, {result.skipped} skipped."
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
if not dry_run:
|
|
427
|
+
save_state(state, sync_dir)
|
|
428
|
+
console.print(f"State saved to {sync_dir / 'state.json'}", style="dim")
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
@backlog_app.command()
|
|
432
|
+
def status(
|
|
433
|
+
epic: str = typer.Option(
|
|
434
|
+
..., "--epic", "-e", help="Local epic ID (e.g., E-DEMO)"
|
|
435
|
+
),
|
|
436
|
+
project: str = typer.Option(
|
|
437
|
+
".", "--project", "-p", help="Project root path"
|
|
438
|
+
),
|
|
439
|
+
) -> None:
|
|
440
|
+
"""Show sync and authorization status for epic stories.
|
|
441
|
+
|
|
442
|
+
Reads local sync state (no JIRA API calls). Shows which stories
|
|
443
|
+
are authorized to work on based on their JIRA status.
|
|
444
|
+
|
|
445
|
+
Examples:
|
|
446
|
+
$ rai backlog status --epic E-DEMO
|
|
447
|
+
"""
|
|
448
|
+
try:
|
|
449
|
+
from rai_pro.providers.jira.sync import check_authorization
|
|
450
|
+
from rai_pro.providers.jira.sync_state import load_state
|
|
451
|
+
except ImportError:
|
|
452
|
+
console.print(
|
|
453
|
+
"[red]Error:[/red] rai-pro is required for JIRA integration.\n"
|
|
454
|
+
"Install with: pip install rai-cli[pro]"
|
|
455
|
+
)
|
|
456
|
+
raise typer.Exit(code=1) from None
|
|
457
|
+
|
|
458
|
+
sync_dir = _get_sync_dir(project)
|
|
459
|
+
state = load_state(sync_dir)
|
|
460
|
+
|
|
461
|
+
if state is None:
|
|
462
|
+
console.print(
|
|
463
|
+
"[red]Error:[/red] No sync state found. Run pull first:\n"
|
|
464
|
+
" rai backlog pull --source jira --epic <JIRA_KEY>"
|
|
465
|
+
)
|
|
466
|
+
raise typer.Exit(code=1)
|
|
467
|
+
|
|
468
|
+
# Check epic exists
|
|
469
|
+
if epic not in state.epics:
|
|
470
|
+
console.print(f"[red]Error:[/red] Epic {epic} not found in sync state.")
|
|
471
|
+
raise typer.Exit(code=1)
|
|
472
|
+
|
|
473
|
+
epic_mapping = state.epics[epic]
|
|
474
|
+
console.print(
|
|
475
|
+
f"\n[bold]Authorization status for {epic}[/bold] "
|
|
476
|
+
f"(JIRA: {epic_mapping.jira_key})\n"
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
# Check each story
|
|
480
|
+
epic_prefix = epic.removeprefix("E-")
|
|
481
|
+
story_ids = sorted(
|
|
482
|
+
[sid for sid in state.stories if sid.startswith(f"S-{epic_prefix}.")]
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
if not story_ids:
|
|
486
|
+
console.print(" No stories synced for this epic.")
|
|
487
|
+
return
|
|
488
|
+
|
|
489
|
+
for story_id in story_ids:
|
|
490
|
+
result = check_authorization(state, story_id)
|
|
491
|
+
if result.authorized:
|
|
492
|
+
console.print(
|
|
493
|
+
f" [green]✓[/green] {story_id}: {result.jira_status} "
|
|
494
|
+
f"({result.jira_key}) — Ready to work"
|
|
495
|
+
)
|
|
496
|
+
else:
|
|
497
|
+
console.print(
|
|
498
|
+
f" [red]✗[/red] {story_id}: {result.jira_status} "
|
|
499
|
+
f"({result.jira_key}) — Awaiting authorization"
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
if state.last_sync_at:
|
|
503
|
+
console.print(
|
|
504
|
+
f"\nLast sync: {state.last_sync_at.strftime('%Y-%m-%d %H:%M UTC')}",
|
|
505
|
+
style="dim",
|
|
506
|
+
)
|
|
507
|
+
console.print(
|
|
508
|
+
"Run 'rai backlog pull --source jira --epic <KEY>' to refresh.",
|
|
509
|
+
style="dim",
|
|
510
|
+
)
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""CLI commands for base Rai package information.
|
|
2
|
+
|
|
3
|
+
This module provides the `raise base` command group for viewing
|
|
4
|
+
information about the bundled base Rai package.
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
$ raise base show # View base package info and install status
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from importlib.resources import files
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
import typer
|
|
16
|
+
|
|
17
|
+
from rai_cli.config.paths import get_identity_dir
|
|
18
|
+
from rai_cli.rai_base import __version__ as base_version
|
|
19
|
+
|
|
20
|
+
base_app = typer.Typer(
|
|
21
|
+
name="base",
|
|
22
|
+
help="View base Rai package info",
|
|
23
|
+
no_args_is_help=True,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _get_project_root() -> Path:
|
|
28
|
+
"""Get the project root directory.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
Current working directory as project root.
|
|
32
|
+
"""
|
|
33
|
+
return Path.cwd()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _count_base_patterns() -> int:
|
|
37
|
+
"""Count patterns in the bundled base package.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
Number of base patterns in patterns-base.jsonl.
|
|
41
|
+
"""
|
|
42
|
+
base = files("rai_cli.rai_base")
|
|
43
|
+
source = base / "memory" / "patterns-base.jsonl"
|
|
44
|
+
content = source.read_text() # type: ignore[union-attr]
|
|
45
|
+
return sum(1 for line in content.splitlines() if line.strip())
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _check_installed(project_root: Path) -> bool:
|
|
49
|
+
"""Check if base Rai has been bootstrapped to this project.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
project_root: Project root directory.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
True if .raise/rai/identity/ exists with files.
|
|
56
|
+
"""
|
|
57
|
+
identity_dir = get_identity_dir(project_root)
|
|
58
|
+
return identity_dir.exists() and any(identity_dir.iterdir())
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@base_app.command()
|
|
62
|
+
def show() -> None:
|
|
63
|
+
"""Display base Rai package information.
|
|
64
|
+
|
|
65
|
+
Shows the bundled base version, contents (identity, patterns,
|
|
66
|
+
methodology), and whether it has been installed in the current project.
|
|
67
|
+
|
|
68
|
+
Examples:
|
|
69
|
+
$ raise base show
|
|
70
|
+
"""
|
|
71
|
+
project_root = _get_project_root()
|
|
72
|
+
pattern_count = _count_base_patterns()
|
|
73
|
+
installed = _check_installed(project_root)
|
|
74
|
+
|
|
75
|
+
# Check for methodology
|
|
76
|
+
base = files("rai_cli.rai_base")
|
|
77
|
+
has_methodology = (base / "framework" / "methodology.yaml").is_file()
|
|
78
|
+
|
|
79
|
+
# Check identity files
|
|
80
|
+
identity_base = base / "identity"
|
|
81
|
+
identity_files: list[str] = []
|
|
82
|
+
try:
|
|
83
|
+
for item in identity_base.iterdir(): # type: ignore[union-attr]
|
|
84
|
+
name = getattr(item, "name", str(item))
|
|
85
|
+
if name.endswith(".md"):
|
|
86
|
+
identity_files.append(name)
|
|
87
|
+
except (AttributeError, TypeError):
|
|
88
|
+
pass
|
|
89
|
+
|
|
90
|
+
typer.echo(f"Base Rai v{base_version}")
|
|
91
|
+
typer.echo()
|
|
92
|
+
typer.echo("Contents:")
|
|
93
|
+
typer.echo(f" Identity: {', '.join(sorted(identity_files)) or 'none'}")
|
|
94
|
+
typer.echo(f" Patterns: {pattern_count} base patterns")
|
|
95
|
+
typer.echo(f" Methodology: {'yes' if has_methodology else 'no'}")
|
|
96
|
+
typer.echo()
|
|
97
|
+
|
|
98
|
+
if installed:
|
|
99
|
+
typer.echo(f"Status: Installed in {project_root}")
|
|
100
|
+
else:
|
|
101
|
+
typer.echo("Status: Not installed (run `raise init` to bootstrap)")
|