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.
@@ -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}")