devflow-cli 1.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.
- devflow_cli/__init__.py +51 -0
- devflow_cli/cli.py +52 -0
- devflow_cli/commands/__init__.py +0 -0
- devflow_cli/commands/check.py +167 -0
- devflow_cli/commands/context.py +173 -0
- devflow_cli/commands/export_cmd.py +149 -0
- devflow_cli/commands/extension.py +137 -0
- devflow_cli/commands/feature.py +102 -0
- devflow_cli/commands/init_cmd.py +94 -0
- devflow_cli/commands/migrate_cmd.py +106 -0
- devflow_cli/commands/regen.py +93 -0
- devflow_cli/commands/status.py +118 -0
- devflow_cli/commands/upgrade_cmd.py +142 -0
- devflow_cli/core/__init__.py +0 -0
- devflow_cli/core/catalog.py +101 -0
- devflow_cli/core/git.py +68 -0
- devflow_cli/core/hooks.py +89 -0
- devflow_cli/core/installer.py +139 -0
- devflow_cli/core/manifest.py +167 -0
- devflow_cli/core/scanner.py +136 -0
- devflow_cli/core/staleness.py +135 -0
- devflow_cli/core/state.py +69 -0
- devflow_cli/utils/__init__.py +0 -0
- devflow_cli/utils/console.py +34 -0
- devflow_cli/utils/paths.py +141 -0
- devflow_cli-1.0.0.dist-info/METADATA +142 -0
- devflow_cli-1.0.0.dist-info/RECORD +30 -0
- devflow_cli-1.0.0.dist-info/WHEEL +4 -0
- devflow_cli-1.0.0.dist-info/entry_points.txt +2 -0
- devflow_cli-1.0.0.dist-info/licenses/LICENSE +21 -0
devflow_cli/__init__.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""devflow CLI — Spec-Driven Development workflow."""
|
|
2
|
+
|
|
3
|
+
VERSION = "1.0.0"
|
|
4
|
+
|
|
5
|
+
STEPS = [
|
|
6
|
+
"constitution",
|
|
7
|
+
"spec",
|
|
8
|
+
"clarify",
|
|
9
|
+
"review-spec",
|
|
10
|
+
"research",
|
|
11
|
+
"plan",
|
|
12
|
+
"contracts",
|
|
13
|
+
"tasks",
|
|
14
|
+
"review-tasks",
|
|
15
|
+
"implement",
|
|
16
|
+
"review-impl",
|
|
17
|
+
"docs",
|
|
18
|
+
"done",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
STEP_LINEAR_MAPPING = {
|
|
22
|
+
"spec": "Spec",
|
|
23
|
+
"clarify": "Spec",
|
|
24
|
+
"review-spec": "Review",
|
|
25
|
+
"research": "Research",
|
|
26
|
+
"plan": "Plan",
|
|
27
|
+
"contracts": "Plan",
|
|
28
|
+
"tasks": "Tasks",
|
|
29
|
+
"review-tasks": "Review",
|
|
30
|
+
"implement": "In Progress",
|
|
31
|
+
"review-impl": "Doc",
|
|
32
|
+
"docs": "Doc",
|
|
33
|
+
"done": "Done",
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
SUPPORTED_AGENTS = ["claude-code"]
|
|
37
|
+
|
|
38
|
+
ARTIFACTS = [
|
|
39
|
+
"spec.md",
|
|
40
|
+
"clarify-log.md",
|
|
41
|
+
"research.md",
|
|
42
|
+
"plan.md",
|
|
43
|
+
"contracts/",
|
|
44
|
+
"data-model.md",
|
|
45
|
+
"quickstart.md",
|
|
46
|
+
"tasks.md",
|
|
47
|
+
"docs.md",
|
|
48
|
+
"review-log.md",
|
|
49
|
+
"analysis.md",
|
|
50
|
+
"checklist.md",
|
|
51
|
+
]
|
devflow_cli/cli.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""devflow CLI — Point d'entree principal."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from devflow_cli import VERSION
|
|
8
|
+
from devflow_cli.commands.init_cmd import init
|
|
9
|
+
from devflow_cli.commands.check import check
|
|
10
|
+
from devflow_cli.commands.status import status
|
|
11
|
+
from devflow_cli.commands.feature import feature
|
|
12
|
+
from devflow_cli.commands.context import context
|
|
13
|
+
from devflow_cli.commands.export_cmd import export
|
|
14
|
+
from devflow_cli.commands.regen import regen
|
|
15
|
+
from devflow_cli.commands.migrate_cmd import migrate
|
|
16
|
+
from devflow_cli.commands.upgrade_cmd import upgrade
|
|
17
|
+
from devflow_cli.commands import extension
|
|
18
|
+
|
|
19
|
+
app = typer.Typer(
|
|
20
|
+
name="devflow",
|
|
21
|
+
help="Spec-Driven Development workflow CLI",
|
|
22
|
+
no_args_is_help=True,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def version_callback(value: bool) -> None:
|
|
27
|
+
if value:
|
|
28
|
+
typer.echo(f"devflow CLI v{VERSION}")
|
|
29
|
+
raise typer.Exit()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@app.callback()
|
|
33
|
+
def main(
|
|
34
|
+
version: bool = typer.Option(False, "--version", callback=version_callback, is_eager=True, help="Affiche la version"),
|
|
35
|
+
) -> None:
|
|
36
|
+
"""devflow — Spec-Driven Development workflow CLI."""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
app.command()(init)
|
|
40
|
+
app.command()(check)
|
|
41
|
+
app.command()(status)
|
|
42
|
+
app.command()(feature)
|
|
43
|
+
app.command()(context)
|
|
44
|
+
app.command(name="export")(export)
|
|
45
|
+
app.command()(regen)
|
|
46
|
+
app.command()(migrate)
|
|
47
|
+
app.command()(upgrade)
|
|
48
|
+
app.add_typer(extension.app, name="extension")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
if __name__ == "__main__":
|
|
52
|
+
app()
|
|
File without changes
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""devflow check — Verifie les prerequis devflow."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
|
|
12
|
+
from devflow_cli.utils.console import console, ok, warn, fail
|
|
13
|
+
from devflow_cli.utils.paths import get_claude_dir
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _cmd_exists(name: str) -> str | None:
|
|
17
|
+
"""Retourne le chemin de la commande si elle existe."""
|
|
18
|
+
return shutil.which(name)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _count_files(directory: Path, pattern: str) -> int:
|
|
22
|
+
return len(list(directory.glob(pattern)))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def check() -> None:
|
|
26
|
+
"""Verifie les prerequis (outils, fichiers, config)."""
|
|
27
|
+
ok_count = 0
|
|
28
|
+
warn_count = 0
|
|
29
|
+
fail_count = 0
|
|
30
|
+
|
|
31
|
+
claude_dir = get_claude_dir()
|
|
32
|
+
|
|
33
|
+
console.print("Verification des prerequis devflow...")
|
|
34
|
+
console.print()
|
|
35
|
+
|
|
36
|
+
# --- Outils ---
|
|
37
|
+
|
|
38
|
+
# git
|
|
39
|
+
git_path = _cmd_exists("git")
|
|
40
|
+
if git_path:
|
|
41
|
+
result = subprocess.run(["git", "--version"], capture_output=True, text=True)
|
|
42
|
+
version = result.stdout.strip().replace("git version ", "")
|
|
43
|
+
ok(f"git -- {git_path} (version {version})")
|
|
44
|
+
ok_count += 1
|
|
45
|
+
else:
|
|
46
|
+
fail("git -- non installe")
|
|
47
|
+
fail_count += 1
|
|
48
|
+
|
|
49
|
+
# claude CLI
|
|
50
|
+
claude_path = _cmd_exists("claude")
|
|
51
|
+
if claude_path:
|
|
52
|
+
ok(f"claude CLI -- {claude_path}")
|
|
53
|
+
ok_count += 1
|
|
54
|
+
else:
|
|
55
|
+
fail("claude CLI -- non installee")
|
|
56
|
+
fail_count += 1
|
|
57
|
+
|
|
58
|
+
# --- Structure ~/.claude/ ---
|
|
59
|
+
|
|
60
|
+
if claude_dir.is_dir():
|
|
61
|
+
ok("~/.claude/ -- present")
|
|
62
|
+
ok_count += 1
|
|
63
|
+
else:
|
|
64
|
+
fail("~/.claude/ -- absent")
|
|
65
|
+
fail_count += 1
|
|
66
|
+
|
|
67
|
+
for subdir in ("commands", "agents", "templates"):
|
|
68
|
+
d = claude_dir / subdir
|
|
69
|
+
if d.is_dir():
|
|
70
|
+
ok(f"~/.claude/{subdir}/ -- present")
|
|
71
|
+
ok_count += 1
|
|
72
|
+
else:
|
|
73
|
+
fail(f"~/.claude/{subdir}/ -- absent")
|
|
74
|
+
fail_count += 1
|
|
75
|
+
|
|
76
|
+
# --- Installation devflow ---
|
|
77
|
+
|
|
78
|
+
# Commandes devflow
|
|
79
|
+
cmd_dir = claude_dir / "commands"
|
|
80
|
+
if cmd_dir.is_dir():
|
|
81
|
+
count = _count_files(cmd_dir, "devflow*.md")
|
|
82
|
+
if count > 0:
|
|
83
|
+
ok(f"commandes devflow -- {count} installees")
|
|
84
|
+
ok_count += 1
|
|
85
|
+
else:
|
|
86
|
+
fail("commandes devflow -- non installees")
|
|
87
|
+
fail_count += 1
|
|
88
|
+
else:
|
|
89
|
+
fail("commandes devflow -- non installees")
|
|
90
|
+
fail_count += 1
|
|
91
|
+
|
|
92
|
+
# Agents devflow
|
|
93
|
+
agents_dir = claude_dir / "agents"
|
|
94
|
+
if agents_dir.is_dir():
|
|
95
|
+
count = _count_files(agents_dir, "devflow*.md")
|
|
96
|
+
if count > 0:
|
|
97
|
+
ok(f"agents devflow -- {count} installes")
|
|
98
|
+
ok_count += 1
|
|
99
|
+
else:
|
|
100
|
+
fail("agents devflow -- non installes")
|
|
101
|
+
fail_count += 1
|
|
102
|
+
else:
|
|
103
|
+
fail("agents devflow -- non installes")
|
|
104
|
+
fail_count += 1
|
|
105
|
+
|
|
106
|
+
# Templates
|
|
107
|
+
tpl_dir = claude_dir / "templates"
|
|
108
|
+
if tpl_dir.is_dir() and any(tpl_dir.iterdir()):
|
|
109
|
+
count = _count_files(tpl_dir, "*-template.md")
|
|
110
|
+
ok(f"templates -- {count} disponibles")
|
|
111
|
+
ok_count += 1
|
|
112
|
+
else:
|
|
113
|
+
warn("templates -- ~/.claude/templates/ vide ou absent")
|
|
114
|
+
warn_count += 1
|
|
115
|
+
|
|
116
|
+
# Scripts executables
|
|
117
|
+
scripts_ok = True
|
|
118
|
+
scripts_dir = claude_dir / "scripts"
|
|
119
|
+
for script_name in ("setup-feature.sh", "devflow-cli.sh", "check-prerequisites.sh"):
|
|
120
|
+
script = scripts_dir / script_name
|
|
121
|
+
if script.is_file() and not (script.stat().st_mode & 0o111):
|
|
122
|
+
scripts_ok = False
|
|
123
|
+
if scripts_ok:
|
|
124
|
+
ok("scripts -- permissions +x OK")
|
|
125
|
+
ok_count += 1
|
|
126
|
+
else:
|
|
127
|
+
warn("scripts -- certains scripts sans permission +x")
|
|
128
|
+
warn_count += 1
|
|
129
|
+
|
|
130
|
+
# --- Projet courant ---
|
|
131
|
+
|
|
132
|
+
if Path("docs").is_dir():
|
|
133
|
+
ok("docs/ -- present dans le projet courant")
|
|
134
|
+
ok_count += 1
|
|
135
|
+
else:
|
|
136
|
+
warn("docs/ -- absent du projet courant")
|
|
137
|
+
warn_count += 1
|
|
138
|
+
|
|
139
|
+
# --- Optionnel ---
|
|
140
|
+
|
|
141
|
+
linear_configured = False
|
|
142
|
+
plugins_file = claude_dir / "plugins.json"
|
|
143
|
+
claude_json = Path.home() / ".claude.json"
|
|
144
|
+
for cfg_file in (plugins_file, claude_json):
|
|
145
|
+
if cfg_file.is_file():
|
|
146
|
+
try:
|
|
147
|
+
content = cfg_file.read_text()
|
|
148
|
+
if "linear" in content.lower():
|
|
149
|
+
linear_configured = True
|
|
150
|
+
break
|
|
151
|
+
except OSError:
|
|
152
|
+
pass
|
|
153
|
+
if linear_configured:
|
|
154
|
+
ok("Linear MCP -- configure")
|
|
155
|
+
ok_count += 1
|
|
156
|
+
else:
|
|
157
|
+
warn("Linear MCP -- non configure (optionnel)")
|
|
158
|
+
warn_count += 1
|
|
159
|
+
|
|
160
|
+
# --- Resume ---
|
|
161
|
+
|
|
162
|
+
console.print()
|
|
163
|
+
total = ok_count + warn_count + fail_count
|
|
164
|
+
console.print(f"Resume : {ok_count} OK, {warn_count} WARN, {fail_count} FAIL ({total} checks)")
|
|
165
|
+
|
|
166
|
+
if fail_count > 0:
|
|
167
|
+
raise typer.Exit(1)
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""devflow context — Genere le contexte projet pour agents IA."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from datetime import date
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
import typer
|
|
11
|
+
|
|
12
|
+
from devflow_cli.core.scanner import (
|
|
13
|
+
detect_stack,
|
|
14
|
+
build_tree,
|
|
15
|
+
detect_extensions,
|
|
16
|
+
detect_conventions,
|
|
17
|
+
read_constitution,
|
|
18
|
+
)
|
|
19
|
+
from devflow_cli.utils.console import console, ok
|
|
20
|
+
from devflow_cli.utils.paths import get_devflow_root
|
|
21
|
+
|
|
22
|
+
FORMATS = ("claude", "cursor", "copilot", "windsurf", "all")
|
|
23
|
+
MANUAL_START = "<!-- DEVFLOW:MANUAL_START -->"
|
|
24
|
+
MANUAL_END = "<!-- DEVFLOW:MANUAL_END -->"
|
|
25
|
+
CONTEXT_START = "<!-- DEVFLOW:CONTEXT_START -->"
|
|
26
|
+
CONTEXT_END = "<!-- DEVFLOW:CONTEXT_END -->"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def context(
|
|
30
|
+
format: str = typer.Argument("claude", help="Format de sortie: claude|cursor|copilot|windsurf|all"),
|
|
31
|
+
project_dir: Optional[Path] = typer.Argument(None, help="Repertoire du projet"),
|
|
32
|
+
) -> None:
|
|
33
|
+
"""Genere le contexte projet pour agents IA."""
|
|
34
|
+
if format not in FORMATS:
|
|
35
|
+
console.print(f"[red]Format inconnu : {format}[/red]")
|
|
36
|
+
console.print(f"Formats valides : {', '.join(FORMATS)}")
|
|
37
|
+
raise typer.Exit(1)
|
|
38
|
+
|
|
39
|
+
proj = (project_dir or Path(".")).resolve()
|
|
40
|
+
project_name = proj.name
|
|
41
|
+
today = date.today().isoformat()
|
|
42
|
+
|
|
43
|
+
content = _generate_content(proj, project_name, today)
|
|
44
|
+
|
|
45
|
+
if format == "all":
|
|
46
|
+
for fmt in ("claude", "cursor", "copilot", "windsurf"):
|
|
47
|
+
_write_format(fmt, proj, content)
|
|
48
|
+
else:
|
|
49
|
+
_write_format(format, proj, content)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _generate_content(proj: Path, project_name: str, today: str) -> str:
|
|
53
|
+
"""Genere le contenu du contexte."""
|
|
54
|
+
template_path = get_devflow_root() / "templates" / "agent-context-template.md"
|
|
55
|
+
|
|
56
|
+
stack = detect_stack(proj)
|
|
57
|
+
tree = build_tree(proj)
|
|
58
|
+
extensions = detect_extensions(proj)
|
|
59
|
+
conventions = detect_conventions(proj)
|
|
60
|
+
constitution = read_constitution(proj)
|
|
61
|
+
|
|
62
|
+
if template_path.is_file():
|
|
63
|
+
content = template_path.read_text(encoding="utf-8")
|
|
64
|
+
content = content.replace("{{PROJECT_NAME}}", project_name)
|
|
65
|
+
content = content.replace("{{DATE}}", today)
|
|
66
|
+
content = content.replace("{{STACK}}", stack)
|
|
67
|
+
content = content.replace("{{TREE}}", tree)
|
|
68
|
+
content = content.replace("{{EXTENSIONS}}", extensions)
|
|
69
|
+
content = content.replace("{{CONVENTIONS}}", conventions)
|
|
70
|
+
content = content.replace("{{CONSTITUTION}}", constitution)
|
|
71
|
+
return content
|
|
72
|
+
|
|
73
|
+
# Fallback sans template
|
|
74
|
+
return (
|
|
75
|
+
f"# Contexte projet — {project_name}\n\n"
|
|
76
|
+
f"> Genere par devflow le {today}\n\n"
|
|
77
|
+
f"## Stack technique\n\n{stack}\n\n"
|
|
78
|
+
f"## Arborescence (2 niveaux)\n\n```\n{tree}\n```\n\n"
|
|
79
|
+
f"## Extensions de fichiers (top 5)\n\n{extensions}\n\n"
|
|
80
|
+
f"## Conventions detectees\n\n{conventions}\n\n"
|
|
81
|
+
f"## Constitution devflow\n\n{constitution}\n\n"
|
|
82
|
+
f"{MANUAL_START}\n{MANUAL_END}\n"
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _extract_manual_block(file_path: Path) -> str | None:
|
|
87
|
+
"""Extrait le bloc manuel existant d'un fichier."""
|
|
88
|
+
if not file_path.is_file():
|
|
89
|
+
return None
|
|
90
|
+
text = file_path.read_text(encoding="utf-8")
|
|
91
|
+
match = re.search(
|
|
92
|
+
rf"{re.escape(MANUAL_START)}.*?{re.escape(MANUAL_END)}",
|
|
93
|
+
text,
|
|
94
|
+
re.DOTALL,
|
|
95
|
+
)
|
|
96
|
+
return match.group(0) if match else None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _inject_manual_block(content: str, manual_block: str | None) -> str:
|
|
100
|
+
"""Reinjecte le bloc manuel dans le contenu genere."""
|
|
101
|
+
if not manual_block:
|
|
102
|
+
return content
|
|
103
|
+
return re.sub(
|
|
104
|
+
rf"{re.escape(MANUAL_START)}.*?{re.escape(MANUAL_END)}",
|
|
105
|
+
manual_block,
|
|
106
|
+
content,
|
|
107
|
+
flags=re.DOTALL,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _write_format(fmt: str, proj: Path, content: str) -> None:
|
|
112
|
+
"""Ecrit le contexte dans le format cible."""
|
|
113
|
+
if fmt == "claude":
|
|
114
|
+
_write_claude(proj, content)
|
|
115
|
+
elif fmt == "cursor":
|
|
116
|
+
_write_cursor(proj, content)
|
|
117
|
+
elif fmt == "copilot":
|
|
118
|
+
_write_copilot(proj, content)
|
|
119
|
+
elif fmt == "windsurf":
|
|
120
|
+
_write_windsurf(proj, content)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _write_claude(proj: Path, content: str) -> None:
|
|
124
|
+
target = proj / "CLAUDE.md"
|
|
125
|
+
manual = _extract_manual_block(target)
|
|
126
|
+
content = _inject_manual_block(content, manual)
|
|
127
|
+
|
|
128
|
+
if target.is_file():
|
|
129
|
+
text = target.read_text(encoding="utf-8")
|
|
130
|
+
if CONTEXT_START in text:
|
|
131
|
+
# Remplacer la section existante
|
|
132
|
+
before = text[:text.index(CONTEXT_START)]
|
|
133
|
+
after = text[text.index(CONTEXT_END) + len(CONTEXT_END):]
|
|
134
|
+
text = f"{before}{CONTEXT_START}\n\n{content}\n\n{CONTEXT_END}{after}"
|
|
135
|
+
else:
|
|
136
|
+
text += f"\n{CONTEXT_START}\n\n{content}\n\n{CONTEXT_END}\n"
|
|
137
|
+
target.write_text(text, encoding="utf-8")
|
|
138
|
+
else:
|
|
139
|
+
target.write_text(f"{CONTEXT_START}\n\n{content}\n\n{CONTEXT_END}\n", encoding="utf-8")
|
|
140
|
+
|
|
141
|
+
ok(f"Contexte ecrit dans {target}")
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _write_cursor(proj: Path, content: str) -> None:
|
|
145
|
+
rules_dir = proj / ".cursor" / "rules"
|
|
146
|
+
rules_dir.mkdir(parents=True, exist_ok=True)
|
|
147
|
+
target = rules_dir / "project-context.mdc"
|
|
148
|
+
manual = _extract_manual_block(target)
|
|
149
|
+
content = _inject_manual_block(content, manual)
|
|
150
|
+
|
|
151
|
+
frontmatter = '---\ndescription: Contexte projet genere par devflow\nglobs: ["**/*"]\n---\n\n'
|
|
152
|
+
target.write_text(frontmatter + content, encoding="utf-8")
|
|
153
|
+
ok(f"Contexte ecrit dans {target}")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _write_copilot(proj: Path, content: str) -> None:
|
|
157
|
+
github_dir = proj / ".github"
|
|
158
|
+
github_dir.mkdir(parents=True, exist_ok=True)
|
|
159
|
+
target = github_dir / "copilot-instructions.md"
|
|
160
|
+
manual = _extract_manual_block(target)
|
|
161
|
+
content = _inject_manual_block(content, manual)
|
|
162
|
+
|
|
163
|
+
target.write_text(content, encoding="utf-8")
|
|
164
|
+
ok(f"Contexte ecrit dans {target}")
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _write_windsurf(proj: Path, content: str) -> None:
|
|
168
|
+
target = proj / ".windsurfrules"
|
|
169
|
+
manual = _extract_manual_block(target)
|
|
170
|
+
content = _inject_manual_block(content, manual)
|
|
171
|
+
|
|
172
|
+
target.write_text(content, encoding="utf-8")
|
|
173
|
+
ok(f"Contexte ecrit dans {target}")
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""devflow export — Exporte devflow pour d'autres agents IA."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
|
|
11
|
+
from devflow_cli.utils.console import console, ok
|
|
12
|
+
from devflow_cli.utils.paths import get_devflow_root
|
|
13
|
+
|
|
14
|
+
FORMATS = ("cursor", "copilot", "windsurf")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def export(
|
|
18
|
+
format: str = typer.Argument(..., help="Format: cursor|copilot|windsurf"),
|
|
19
|
+
output_dir: Optional[Path] = typer.Argument(None, help="Dossier de sortie"),
|
|
20
|
+
) -> None:
|
|
21
|
+
"""Exporte les commandes/agents devflow vers Cursor/Copilot/Windsurf."""
|
|
22
|
+
if format not in FORMATS:
|
|
23
|
+
console.print(f"[red]Format inconnu : {format}[/red]")
|
|
24
|
+
console.print(f"Formats valides : {', '.join(FORMATS)}")
|
|
25
|
+
raise typer.Exit(1)
|
|
26
|
+
|
|
27
|
+
out = (output_dir or Path(".")).resolve()
|
|
28
|
+
content = _build_content()
|
|
29
|
+
|
|
30
|
+
if format == "cursor":
|
|
31
|
+
_export_cursor(out, content)
|
|
32
|
+
elif format == "copilot":
|
|
33
|
+
_export_copilot(out, content)
|
|
34
|
+
elif format == "windsurf":
|
|
35
|
+
_export_windsurf(out, content)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _build_content() -> str:
|
|
39
|
+
"""Genere le contenu a partir des commandes et agents."""
|
|
40
|
+
root = get_devflow_root()
|
|
41
|
+
lines: list[str] = []
|
|
42
|
+
|
|
43
|
+
lines.append("# devflow — Workflow de developpement spec-driven")
|
|
44
|
+
lines.append("")
|
|
45
|
+
lines.append("## Flux")
|
|
46
|
+
lines.append("")
|
|
47
|
+
lines.append("constitution -> spec -> clarify -> review-spec -> research -> plan -> contracts -> tasks -> review-tasks -> implement -> review-impl -> docs -> done")
|
|
48
|
+
lines.append("")
|
|
49
|
+
lines.append("## Commandes disponibles")
|
|
50
|
+
lines.append("")
|
|
51
|
+
|
|
52
|
+
# Extraire commandes
|
|
53
|
+
commands_dir = root / "commands"
|
|
54
|
+
if commands_dir.is_dir():
|
|
55
|
+
for cmd_file in sorted(commands_dir.glob("devflow*.md")):
|
|
56
|
+
text = cmd_file.read_text(encoding="utf-8")
|
|
57
|
+
file_lines = text.splitlines()
|
|
58
|
+
|
|
59
|
+
# Titre
|
|
60
|
+
title = file_lines[0].lstrip("# ") if file_lines else cmd_file.stem
|
|
61
|
+
lines.append(f"### {title}")
|
|
62
|
+
lines.append("")
|
|
63
|
+
|
|
64
|
+
# Objectif
|
|
65
|
+
objective = _extract_section(text, "Objectif", max_lines=5)
|
|
66
|
+
if objective:
|
|
67
|
+
lines.append(objective)
|
|
68
|
+
lines.append("")
|
|
69
|
+
|
|
70
|
+
lines.append("## Agents")
|
|
71
|
+
lines.append("")
|
|
72
|
+
|
|
73
|
+
# Extraire agents
|
|
74
|
+
agents_dir = root / "agents"
|
|
75
|
+
if agents_dir.is_dir():
|
|
76
|
+
for agent_file in sorted(agents_dir.glob("devflow*.md")):
|
|
77
|
+
text = agent_file.read_text(encoding="utf-8")
|
|
78
|
+
basename = agent_file.stem
|
|
79
|
+
|
|
80
|
+
lines.append(f"### {basename}")
|
|
81
|
+
lines.append("")
|
|
82
|
+
|
|
83
|
+
mission = _extract_section(text, "Mission", max_lines=3)
|
|
84
|
+
if mission:
|
|
85
|
+
lines.append(mission)
|
|
86
|
+
lines.append("")
|
|
87
|
+
|
|
88
|
+
return "\n".join(lines)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _extract_section(text: str, section_name: str, max_lines: int = 5) -> str:
|
|
92
|
+
"""Extrait le contenu d'une section markdown."""
|
|
93
|
+
pattern = rf"^## {re.escape(section_name)}\s*$"
|
|
94
|
+
match = re.search(pattern, text, re.MULTILINE)
|
|
95
|
+
if not match:
|
|
96
|
+
return ""
|
|
97
|
+
|
|
98
|
+
start = match.end()
|
|
99
|
+
remaining = text[start:].strip().splitlines()
|
|
100
|
+
result_lines: list[str] = []
|
|
101
|
+
for line in remaining:
|
|
102
|
+
if line.startswith("## "):
|
|
103
|
+
break
|
|
104
|
+
result_lines.append(line)
|
|
105
|
+
if len(result_lines) >= max_lines:
|
|
106
|
+
break
|
|
107
|
+
|
|
108
|
+
return "\n".join(result_lines).strip()
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _export_cursor(out: Path, content: str) -> None:
|
|
112
|
+
rules_dir = out / ".cursor" / "rules"
|
|
113
|
+
rules_dir.mkdir(parents=True, exist_ok=True)
|
|
114
|
+
target = rules_dir / "devflow.mdc"
|
|
115
|
+
|
|
116
|
+
# Verifier template
|
|
117
|
+
template = get_devflow_root() / "templates" / "export" / "cursor-rules-template.md"
|
|
118
|
+
body = template.read_text(encoding="utf-8") if template.is_file() else content
|
|
119
|
+
|
|
120
|
+
frontmatter = (
|
|
121
|
+
"---\n"
|
|
122
|
+
"description: devflow workflow spec-driven\n"
|
|
123
|
+
'globs: ["docs/features/**/*", ".specify/memory/constitution.md"]\n'
|
|
124
|
+
"---\n\n"
|
|
125
|
+
)
|
|
126
|
+
target.write_text(frontmatter + body, encoding="utf-8")
|
|
127
|
+
ok(f"Export Cursor : {target}")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _export_copilot(out: Path, content: str) -> None:
|
|
131
|
+
github_dir = out / ".github"
|
|
132
|
+
github_dir.mkdir(parents=True, exist_ok=True)
|
|
133
|
+
target = github_dir / "copilot-instructions.md"
|
|
134
|
+
|
|
135
|
+
template = get_devflow_root() / "templates" / "export" / "copilot-instructions-template.md"
|
|
136
|
+
body = template.read_text(encoding="utf-8") if template.is_file() else content
|
|
137
|
+
|
|
138
|
+
target.write_text(body, encoding="utf-8")
|
|
139
|
+
ok(f"Export Copilot : {target}")
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _export_windsurf(out: Path, content: str) -> None:
|
|
143
|
+
target = out / ".windsurfrules"
|
|
144
|
+
|
|
145
|
+
template = get_devflow_root() / "templates" / "export" / "cursor-rules-template.md"
|
|
146
|
+
body = template.read_text(encoding="utf-8") if template.is_file() else content
|
|
147
|
+
|
|
148
|
+
target.write_text(body, encoding="utf-8")
|
|
149
|
+
ok(f"Export Windsurf : {target}")
|