saasforge 0.4.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.
Files changed (39) hide show
  1. saasforge/__init__.py +0 -0
  2. saasforge/__main__.py +4 -0
  3. saasforge/adapters/__init__.py +40 -0
  4. saasforge/adapters/aider.py +42 -0
  5. saasforge/adapters/antigravity.py +29 -0
  6. saasforge/adapters/base.py +13 -0
  7. saasforge/adapters/claude.py +22 -0
  8. saasforge/adapters/codex.py +31 -0
  9. saasforge/adapters/copilot.py +23 -0
  10. saasforge/adapters/cursor.py +34 -0
  11. saasforge/adapters/gemini.py +32 -0
  12. saasforge/adapters/kilocode.py +36 -0
  13. saasforge/adapters/opencode.py +74 -0
  14. saasforge/adapters/openroutercli.py +39 -0
  15. saasforge/adapters/qoder.py +25 -0
  16. saasforge/adapters/qwen.py +31 -0
  17. saasforge/adapters/roo.py +25 -0
  18. saasforge/adapters/terminal.py +45 -0
  19. saasforge/adapters/windsurf.py +30 -0
  20. saasforge/builtins/constitution.md +18 -0
  21. saasforge/builtins/specs/plan-template.md +20 -0
  22. saasforge/builtins/specs/spec-template.md +31 -0
  23. saasforge/builtins/specs/tasks-template.md +18 -0
  24. saasforge/builtins/tool/Dockerfile.j2 +26 -0
  25. saasforge/builtins/tool/app.py.j2 +14 -0
  26. saasforge/builtins/tool/vercel.json.j2 +14 -0
  27. saasforge/cli.py +354 -0
  28. saasforge/config.py +79 -0
  29. saasforge/dna_engine.py +82 -0
  30. saasforge/memory.py +150 -0
  31. saasforge/scaffold.py +58 -0
  32. saasforge/templates.py +36 -0
  33. saasforge-0.4.0.dist-info/METADATA +309 -0
  34. saasforge-0.4.0.dist-info/RECORD +39 -0
  35. saasforge-0.4.0.dist-info/WHEEL +5 -0
  36. saasforge-0.4.0.dist-info/entry_points.txt +3 -0
  37. saasforge-0.4.0.dist-info/licenses/LICENSE +23 -0
  38. saasforge-0.4.0.dist-info/licenses/NOTICE.md +13 -0
  39. saasforge-0.4.0.dist-info/top_level.txt +1 -0
saasforge/__init__.py ADDED
File without changes
saasforge/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from .cli import app
2
+
3
+ if __name__ == "__main__":
4
+ app()
@@ -0,0 +1,40 @@
1
+ from .opencode import OpenCodeAdapter
2
+ from .claude import ClaudeAdapter
3
+ from .gemini import GeminiAdapter
4
+ from .copilot import CopilotAdapter
5
+ from .antigravity import AntigravityAdapter
6
+ from .aider import AiderAdapter
7
+ from .kilocode import KilocodeAdapter
8
+ from .openroutercli import OpenRouterCLIAdapter
9
+ from .terminal import TerminalAdapter
10
+ from .cursor import CursorAdapter
11
+ from .windsurf import WindsurfAdapter
12
+ from .roo import RooAdapter
13
+ from .codex import CodexAdapter
14
+ from .qwen import QwenAdapter
15
+ from .qoder import QoderAdapter
16
+
17
+ ADAPTERS = {
18
+ "opencode": OpenCodeAdapter,
19
+ "claude": ClaudeAdapter,
20
+ "gemini": GeminiAdapter,
21
+ "copilot": CopilotAdapter,
22
+ "antigravity": AntigravityAdapter,
23
+ "aider": AiderAdapter,
24
+ "kilocode": KilocodeAdapter,
25
+ "openroutercli": OpenRouterCLIAdapter,
26
+ "terminal": TerminalAdapter,
27
+ "cursor": CursorAdapter,
28
+ "windsurf": WindsurfAdapter,
29
+ "roo": RooAdapter,
30
+ "codex": CodexAdapter,
31
+ "qwen": QwenAdapter,
32
+ "qoder": QoderAdapter,
33
+ }
34
+
35
+ def get_adapter(name: str):
36
+ cls = ADAPTERS.get(name.lower())
37
+ if cls is None:
38
+ available = ", ".join(ADAPTERS.keys())
39
+ raise ValueError(f"Unknown adapter '{name}'. Available: {available}")
40
+ return cls()
@@ -0,0 +1,42 @@
1
+ from pathlib import Path
2
+
3
+ from .base import CLIAdapter
4
+
5
+
6
+ class AiderAdapter(CLIAdapter):
7
+ name = "aider"
8
+ folder = ".aider"
9
+
10
+ def generate_commands(self, target_dir: Path, tool_name: str) -> list[Path]:
11
+ rules_dir = target_dir / self.folder
12
+ rules_dir.mkdir(parents=True, exist_ok=True)
13
+ created = []
14
+
15
+ conventions = rules_dir / "CONVENTIONS.md"
16
+ if not conventions.exists():
17
+ conventions.write_text(
18
+ f"# Aider Conventions for {tool_name}\n\n"
19
+ f"## Stack\n"
20
+ f"- Frontend: Next.js + Tailwind + Shadcn\n"
21
+ f"- Backend: Python FastAPI\n"
22
+ f"- Database: Turso (libsql-client)\n"
23
+ f"- Auth: Better Auth\n"
24
+ f"- Storage: Cloudflare R2\n\n"
25
+ f"## Workflow\n"
26
+ f"1. Read spec before implementing\n"
27
+ f"2. Write tests alongside code\n"
28
+ f"3. Follow existing patterns\n",
29
+ encoding="utf-8",
30
+ )
31
+ created.append(conventions)
32
+
33
+ for cmd in ["specify", "plan", "implement"]:
34
+ path = rules_dir / f"{cmd}.md"
35
+ if not path.exists():
36
+ path.write_text(f"# {cmd}\n\nSaaSForge instruction for {tool_name}.", encoding="utf-8")
37
+ created.append(path)
38
+
39
+ return created
40
+
41
+ def generate_init(self, target_dir: Path) -> list[Path]:
42
+ return self.generate_commands(target_dir, "project")
@@ -0,0 +1,29 @@
1
+ from pathlib import Path
2
+
3
+ from .base import CLIAdapter
4
+
5
+
6
+ class AntigravityAdapter(CLIAdapter):
7
+ name = "antigravity"
8
+ folder = ".antigravity"
9
+
10
+ def generate_commands(self, target_dir: Path, tool_name: str) -> list[Path]:
11
+ rules_dir = target_dir / self.folder / "rules"
12
+ rules_dir.mkdir(parents=True, exist_ok=True)
13
+ created = []
14
+
15
+ for cmd in ["constitute", "specify", "plan", "tasks", "implement"]:
16
+ path = rules_dir / f"{cmd}.md"
17
+ if not path.exists():
18
+ path.write_text(f"# {cmd}\n\nSaaSForge instruction for {tool_name}.", encoding="utf-8")
19
+ created.append(path)
20
+
21
+ readme = target_dir / self.folder / "README.md"
22
+ if not readme.exists():
23
+ readme.write_text(f"# Antigravity Rules for {tool_name}\n\nGenerated by SaaSForge.", encoding="utf-8")
24
+ created.append(readme)
25
+
26
+ return created
27
+
28
+ def generate_init(self, target_dir: Path) -> list[Path]:
29
+ return self.generate_commands(target_dir, "project")
@@ -0,0 +1,13 @@
1
+ from pathlib import Path
2
+ from typing import Protocol
3
+
4
+
5
+ class CLIAdapter(Protocol):
6
+ name: str
7
+ folder: str
8
+
9
+ def generate_commands(self, target_dir: Path, tool_name: str) -> list[Path]:
10
+ ...
11
+
12
+ def generate_init(self, target_dir: Path) -> list[Path]:
13
+ ...
@@ -0,0 +1,22 @@
1
+ from pathlib import Path
2
+
3
+ from .base import CLIAdapter
4
+
5
+
6
+ class ClaudeAdapter(CLIAdapter):
7
+ name = "claude"
8
+ folder = ".claude"
9
+
10
+ def generate_commands(self, target_dir: Path, tool_name: str) -> list[Path]:
11
+ cmds_dir = target_dir / self.folder / "commands"
12
+ cmds_dir.mkdir(parents=True, exist_ok=True)
13
+ created = []
14
+ for cmd in ["constitute", "specify", "plan", "tasks", "implement"]:
15
+ path = cmds_dir / f"{cmd}.md"
16
+ if not path.exists():
17
+ path.write_text(f"# {cmd}\n\nSaaSForge command for {tool_name}.", encoding="utf-8")
18
+ created.append(path)
19
+ return created
20
+
21
+ def generate_init(self, target_dir: Path) -> list[Path]:
22
+ return self.generate_commands(target_dir, "project")
@@ -0,0 +1,31 @@
1
+ from pathlib import Path
2
+
3
+ from .base import CLIAdapter
4
+
5
+
6
+ class CodexAdapter(CLIAdapter):
7
+ name = "codex"
8
+ folder = ".codex"
9
+
10
+ def generate_commands(self, target_dir: Path, tool_name: str) -> list[Path]:
11
+ cmds_dir = target_dir / self.folder / "instructions"
12
+ cmds_dir.mkdir(parents=True, exist_ok=True)
13
+ created = []
14
+ rules = cmds_dir / "saasforge.md"
15
+ if not rules.exists():
16
+ rules.write_text(
17
+ f"# Codex SaaSForge Instructions — {tool_name}\n\n"
18
+ f"## Workflow\n"
19
+ f"Use these slash commands in order:\n"
20
+ f"- `/sp.specify` — Define what to build\n"
21
+ f"- `/sp.plan` — Plan with tech stack\n"
22
+ f"- `/sp.tasks` — Break into tasks\n"
23
+ f"- `/sp.implement` — Execute all tasks\n"
24
+ f"- `/sp.analyze` — Check consistency\n",
25
+ encoding="utf-8",
26
+ )
27
+ created.append(rules)
28
+ return created
29
+
30
+ def generate_init(self, target_dir: Path) -> list[Path]:
31
+ return self.generate_commands(target_dir, "project")
@@ -0,0 +1,23 @@
1
+ from pathlib import Path
2
+
3
+ from .base import CLIAdapter
4
+
5
+
6
+ class CopilotAdapter(CLIAdapter):
7
+ name = "copilot"
8
+ folder = ".github"
9
+
10
+ def generate_commands(self, target_dir: Path, tool_name: str) -> list[Path]:
11
+ instructions_dir = target_dir / self.folder / "instructions"
12
+ instructions_dir.mkdir(parents=True, exist_ok=True)
13
+ created = []
14
+
15
+ for cmd in ["constitute", "specify", "plan", "tasks", "implement"]:
16
+ path = instructions_dir / f"{cmd}.md"
17
+ if not path.exists():
18
+ path.write_text(f"# {cmd}\n\nSaaSForge instruction for {tool_name}.", encoding="utf-8")
19
+ created.append(path)
20
+ return created
21
+
22
+ def generate_init(self, target_dir: Path) -> list[Path]:
23
+ return self.generate_commands(target_dir, "project")
@@ -0,0 +1,34 @@
1
+ from pathlib import Path
2
+
3
+ from .base import CLIAdapter
4
+
5
+
6
+ class CursorAdapter(CLIAdapter):
7
+ name = "cursor"
8
+ folder = ".cursor"
9
+
10
+ def generate_commands(self, target_dir: Path, tool_name: str) -> list[Path]:
11
+ rules_dir = target_dir / self.folder / "rules"
12
+ rules_dir.mkdir(parents=True, exist_ok=True)
13
+ created = []
14
+ rules = rules_dir / "saasforge-workflow.mdc"
15
+ if not rules.exists():
16
+ rules.write_text(
17
+ f"---\n"
18
+ f"description: SaaSForge spec-driven workflow for {tool_name}\n"
19
+ f"---\n"
20
+ f"\n"
21
+ f"# SaaSForge Workflow — {tool_name}\n\n"
22
+ f"Use these commands in order:\n"
23
+ f"1. `/sp.specify` — Define requirements\n"
24
+ f"2. `/sp.plan` — Create implementation plan\n"
25
+ f"3. `/sp.tasks` — Break into tasks\n"
26
+ f"4. `/sp.implement` — Execute\n"
27
+ f"5. `/sp.analyze` — Review consistency\n",
28
+ encoding="utf-8",
29
+ )
30
+ created.append(rules)
31
+ return created
32
+
33
+ def generate_init(self, target_dir: Path) -> list[Path]:
34
+ return self.generate_commands(target_dir, "project")
@@ -0,0 +1,32 @@
1
+ from pathlib import Path
2
+
3
+ from .base import CLIAdapter
4
+
5
+
6
+ class GeminiAdapter(CLIAdapter):
7
+ name = "gemini"
8
+ folder = ".gemini"
9
+
10
+ def generate_commands(self, target_dir: Path, tool_name: str) -> list[Path]:
11
+ cmds_dir = target_dir / self.folder
12
+ cmds_dir.mkdir(parents=True, exist_ok=True)
13
+ created = []
14
+ config = f"""# Gemini CLI config for {tool_name}
15
+ instruction_dirs:
16
+ - {self.folder}/instructions
17
+ """
18
+ path = cmds_dir / "config.yaml"
19
+ path.write_text(config, encoding="utf-8")
20
+ created.append(path)
21
+
22
+ instr_dir = cmds_dir / "instructions"
23
+ instr_dir.mkdir(exist_ok=True)
24
+ for cmd in ["constitute", "specify", "plan", "tasks", "implement"]:
25
+ ipath = instr_dir / f"{cmd}.md"
26
+ if not ipath.exists():
27
+ ipath.write_text(f"# {cmd}\n\nSaaSForge instruction for {tool_name}.", encoding="utf-8")
28
+ created.append(ipath)
29
+ return created
30
+
31
+ def generate_init(self, target_dir: Path) -> list[Path]:
32
+ return self.generate_commands(target_dir, "project")
@@ -0,0 +1,36 @@
1
+ from pathlib import Path
2
+
3
+ from .base import CLIAdapter
4
+
5
+
6
+ class KilocodeAdapter(CLIAdapter):
7
+ name = "kilocode"
8
+ folder = ".kilocode"
9
+
10
+ def generate_commands(self, target_dir: Path, tool_name: str) -> list[Path]:
11
+ cmds_dir = target_dir / self.folder / "instructions"
12
+ cmds_dir.mkdir(parents=True, exist_ok=True)
13
+ created = []
14
+
15
+ for cmd in ["constitute", "specify", "plan", "tasks", "implement", "analyze"]:
16
+ path = cmds_dir / f"{cmd}.md"
17
+ if not path.exists():
18
+ path.write_text(f"# {cmd}\n\nSaaSForge instruction for {tool_name}.", encoding="utf-8")
19
+ created.append(path)
20
+
21
+ config = target_dir / self.folder / "config.yml"
22
+ if not config.exists():
23
+ config.write_text(
24
+ f"# KiloCode config for {tool_name}\n"
25
+ f"project: {tool_name}\n"
26
+ f"framework: spec-driven\n"
27
+ f"instruction_dirs:\n"
28
+ f" - {self.folder}/instructions\n",
29
+ encoding="utf-8",
30
+ )
31
+ created.append(config)
32
+
33
+ return created
34
+
35
+ def generate_init(self, target_dir: Path) -> list[Path]:
36
+ return self.generate_commands(target_dir, "project")
@@ -0,0 +1,74 @@
1
+ from pathlib import Path
2
+
3
+ from .base import CLIAdapter
4
+
5
+
6
+ COMMANDS = {
7
+ "sp.constitution.md": """# Constitute Project
8
+
9
+ Analyze the project requirements and establish core principles:
10
+ 1. Read _system/DNA.md for architecture patterns
11
+ 2. Read project README or requirements
12
+ 3. Establish coding standards and conventions
13
+ 4. Document architectural decisions
14
+ """,
15
+ "sp.specify.md": """# Specify
16
+
17
+ Create a detailed specification for the next component:
18
+ 1. Review existing specs and requirements
19
+ 2. Define acceptance criteria
20
+ 3. Document API contracts if applicable
21
+ 4. Specify data models and schemas
22
+ """,
23
+ "sp.plan.md": """# Plan
24
+
25
+ Create implementation plan from specification:
26
+ 1. Break down into discrete tasks
27
+ 2. Estimate complexity
28
+ 3. Identify dependencies
29
+ 4. Define testing strategy
30
+ """,
31
+ "sp.tasks.md": """# Tasks
32
+
33
+ Generate actionable tasks from the plan:
34
+ 1. Create individual task descriptions
35
+ 2. Define completion criteria
36
+ 3. Assign ownership (if applicable)
37
+ 4. Set priority levels
38
+ """,
39
+ "sp.implement.md": """# Implement
40
+
41
+ Execute the implementation plan:
42
+ 1. Follow the tech stack from DNA.md
43
+ 2. Create tests alongside implementation
44
+ 3. Follow existing code patterns
45
+ 4. Update TOOL_PLAN.md with progress
46
+ """,
47
+ "sp.analyze.md": """# Analyze
48
+
49
+ Cross-artifact consistency and alignment check:
50
+ 1. Verify spec matches requirements
51
+ 2. Verify plan matches spec
52
+ 3. Verify implementation matches plan
53
+ 4. Report any gaps or inconsistencies
54
+ """,
55
+ }
56
+
57
+
58
+ class OpenCodeAdapter(CLIAdapter):
59
+ name = "opencode"
60
+ folder = ".opencode"
61
+
62
+ def generate_commands(self, target_dir: Path, tool_name: str) -> list[Path]:
63
+ commands_dir = target_dir / self.folder / "command"
64
+ commands_dir.mkdir(parents=True, exist_ok=True)
65
+ created = []
66
+ for filename, content in COMMANDS.items():
67
+ path = commands_dir / filename
68
+ if not path.exists():
69
+ path.write_text(f"# {tool_name} — {filename}\n\n{content}", encoding="utf-8")
70
+ created.append(path)
71
+ return created
72
+
73
+ def generate_init(self, target_dir: Path) -> list[Path]:
74
+ return self.generate_commands(target_dir, "project")
@@ -0,0 +1,39 @@
1
+ from pathlib import Path
2
+
3
+ from .base import CLIAdapter
4
+
5
+
6
+ class OpenRouterCLIAdapter(CLIAdapter):
7
+ name = "openroutercli"
8
+ folder = ".openrouter"
9
+
10
+ def generate_commands(self, target_dir: Path, tool_name: str) -> list[Path]:
11
+ config_dir = target_dir / self.folder
12
+ config_dir.mkdir(parents=True, exist_ok=True)
13
+ created = []
14
+
15
+ config = config_dir / "config.yml"
16
+ if not config.exists():
17
+ config.write_text(
18
+ f"# OpenRouter CLI config for {tool_name}\n"
19
+ f"project: {tool_name}\n"
20
+ f"model: deepseek/deepseek-chat\n"
21
+ f"system_prompt: |\n"
22
+ f" You are building a Micro-SaaS tool called {tool_name}.\n"
23
+ f" Follow SaaSForge spec-driven workflow.\n",
24
+ encoding="utf-8",
25
+ )
26
+ created.append(config)
27
+
28
+ prompts_dir = config_dir / "prompts"
29
+ prompts_dir.mkdir(exist_ok=True)
30
+ for cmd in ["specify", "plan", "implement", "review"]:
31
+ path = prompts_dir / f"{cmd}.md"
32
+ if not path.exists():
33
+ path.write_text(f"# {cmd}\n\nPrompt for {tool_name}.", encoding="utf-8")
34
+ created.append(path)
35
+
36
+ return created
37
+
38
+ def generate_init(self, target_dir: Path) -> list[Path]:
39
+ return self.generate_commands(target_dir, "project")
@@ -0,0 +1,25 @@
1
+ from pathlib import Path
2
+
3
+ from .base import CLIAdapter
4
+
5
+
6
+ class QoderAdapter(CLIAdapter):
7
+ name = "qoder"
8
+ folder = ".qoder"
9
+
10
+ def generate_commands(self, target_dir: Path, tool_name: str) -> list[Path]:
11
+ cmds_dir = target_dir / self.folder / "commands"
12
+ cmds_dir.mkdir(parents=True, exist_ok=True)
13
+ created = []
14
+ for cmd in ["specify", "plan", "tasks", "implement"]:
15
+ path = cmds_dir / f"sp.{cmd}.md"
16
+ if not path.exists():
17
+ path.write_text(
18
+ f"# /sp.{cmd}\n\nSaaSForge workflow command for {tool_name}.",
19
+ encoding="utf-8",
20
+ )
21
+ created.append(path)
22
+ return created
23
+
24
+ def generate_init(self, target_dir: Path) -> list[Path]:
25
+ return self.generate_commands(target_dir, "project")
@@ -0,0 +1,31 @@
1
+ from pathlib import Path
2
+
3
+ from .base import CLIAdapter
4
+
5
+
6
+ class QwenAdapter(CLIAdapter):
7
+ name = "qwen"
8
+ folder = ".qwen"
9
+
10
+ def generate_commands(self, target_dir: Path, tool_name: str) -> list[Path]:
11
+ cmds_dir = target_dir / self.folder
12
+ cmds_dir.mkdir(parents=True, exist_ok=True)
13
+ created = []
14
+ rules = cmds_dir / "saasforge.md"
15
+ if not rules.exists():
16
+ rules.write_text(
17
+ f"# Qwen SaaSForge — {tool_name}\n\n"
18
+ f"## Spec-Driven Workflow\n\n"
19
+ f"Always follow this order:\n"
20
+ f"1. **Specify** (`/sp.specify`): Write functional requirements\n"
21
+ f"2. **Plan** (`/sp.plan`): Choose tech stack and architecture\n"
22
+ f"3. **Tasks** (`/sp.tasks`): Create actionable task list\n"
23
+ f"4. **Implement** (`/sp.implement`): Build incrementally\n"
24
+ f"5. **Analyze** (`/sp.analyze`): Verify cross-artifact consistency\n",
25
+ encoding="utf-8",
26
+ )
27
+ created.append(rules)
28
+ return created
29
+
30
+ def generate_init(self, target_dir: Path) -> list[Path]:
31
+ return self.generate_commands(target_dir, "project")
@@ -0,0 +1,25 @@
1
+ from pathlib import Path
2
+
3
+ from .base import CLIAdapter
4
+
5
+
6
+ class RooAdapter(CLIAdapter):
7
+ name = "roo"
8
+ folder = ".roo"
9
+
10
+ def generate_commands(self, target_dir: Path, tool_name: str) -> list[Path]:
11
+ cmds_dir = target_dir / self.folder / "commands"
12
+ cmds_dir.mkdir(parents=True, exist_ok=True)
13
+ created = []
14
+ for cmd in ["specify", "plan", "tasks", "implement", "analyze"]:
15
+ path = cmds_dir / f"{cmd}.md"
16
+ if not path.exists():
17
+ path.write_text(
18
+ f"# /sp.{cmd}\n\nSaaSForge {cmd} command for {tool_name}.",
19
+ encoding="utf-8",
20
+ )
21
+ created.append(path)
22
+ return created
23
+
24
+ def generate_init(self, target_dir: Path) -> list[Path]:
25
+ return self.generate_commands(target_dir, "project")
@@ -0,0 +1,45 @@
1
+ from pathlib import Path
2
+
3
+ from .base import CLIAdapter
4
+
5
+
6
+ class TerminalAdapter(CLIAdapter):
7
+ name = "terminal"
8
+ folder = ".saasforge"
9
+
10
+ def generate_commands(self, target_dir: Path, tool_name: str) -> list[Path]:
11
+ config_dir = target_dir / self.folder
12
+ config_dir.mkdir(parents=True, exist_ok=True)
13
+ created = []
14
+
15
+ config = config_dir / "config.yml"
16
+ if not config.exists():
17
+ config.write_text(
18
+ f"# SaaSForge config for {tool_name}\n"
19
+ f"project: {tool_name}\n"
20
+ f"stack:\n"
21
+ f" frontend: nextjs\n"
22
+ f" backend: fastapi\n"
23
+ f" database: turso\n"
24
+ f" auth: better-auth\n"
25
+ f"workflow: spec-driven\n",
26
+ encoding="utf-8",
27
+ )
28
+ created.append(config)
29
+
30
+ readme = config_dir / "README.md"
31
+ if not readme.exists():
32
+ readme.write_text(
33
+ f"# SaaSForge — {tool_name}\n\n"
34
+ f"Run `saasforge` commands to manage this project:\n"
35
+ f"- `saasforge workflow --ai terminal`: Generate workflow\n"
36
+ f"- `saasforge deploy .`: Check deploy readiness\n"
37
+ f"- `saasforge dna --validate`: Validate DNA\n",
38
+ encoding="utf-8",
39
+ )
40
+ created.append(readme)
41
+
42
+ return created
43
+
44
+ def generate_init(self, target_dir: Path) -> list[Path]:
45
+ return self.generate_commands(target_dir, Path.cwd().name)
@@ -0,0 +1,30 @@
1
+ from pathlib import Path
2
+
3
+ from .base import CLIAdapter
4
+
5
+
6
+ class WindsurfAdapter(CLIAdapter):
7
+ name = "windsurf"
8
+ folder = ".windsurf"
9
+
10
+ def generate_commands(self, target_dir: Path, tool_name: str) -> list[Path]:
11
+ rules_dir = target_dir / self.folder
12
+ rules_dir.mkdir(parents=True, exist_ok=True)
13
+ created = []
14
+ rules = rules_dir / "saasforge_rules.md"
15
+ if not rules.exists():
16
+ rules.write_text(
17
+ f"# Windsurf SaaSForge Rules — {tool_name}\n\n"
18
+ f"Follow this spec-driven workflow:\n"
19
+ f"1. Specify requirements (/sp.specify)\n"
20
+ f"2. Plan implementation (/sp.plan)\n"
21
+ f"3. Create tasks (/sp.tasks)\n"
22
+ f"4. Implement (/sp.implement)\n"
23
+ f"5. Analyze consistency (/sp.analyze)\n",
24
+ encoding="utf-8",
25
+ )
26
+ created.append(rules)
27
+ return created
28
+
29
+ def generate_init(self, target_dir: Path) -> list[Path]:
30
+ return self.generate_commands(target_dir, "project")
@@ -0,0 +1,18 @@
1
+ # Project Constitution
2
+
3
+ ## Core Principles
4
+
5
+ 1. **Free-first infrastructure** — Always start with free tiers (Vercel $0, HF $0, Turso $0)
6
+ 2. **Isolation** — Every tool gets its own DB, own Space, own Vercel project
7
+ 3. **AI-first** — Use free AI models (Gemini Free, DeepSeek V4 Free) before any paid API
8
+ 4. **Security** — 6-layer security: Auth, API, DB, File, Deploy, AI Bill Protection
9
+ 5. **Spec-driven** — Spec → Plan → Tasks → Implement → Review
10
+
11
+ ## Tech Stack (Locked)
12
+
13
+ - Frontend: Next.js + Tailwind + Shadcn → Vercel
14
+ - Backend: Python FastAPI → Hugging Face Spaces (Docker SDK)
15
+ - Database: Turso (libsql-client)
16
+ - Auth: Better Auth (JWT, self-hosted)
17
+ - Storage: Cloudflare R2
18
+ - Payments: Lemon Squeezy + Keenu
@@ -0,0 +1,20 @@
1
+ # Implementation Plan: {{ tool_name }}
2
+
3
+ ## Phase 1: CLI Prototype
4
+
5
+ - [ ] Set up Python environment + dependencies
6
+ - [ ] Implement core logic
7
+ - [ ] Write unit tests
8
+
9
+ ## Phase 2: API + UI
10
+
11
+ - [ ] Build FastAPI endpoints
12
+ - [ ] Create frontend pages
13
+ - [ ] API tests
14
+
15
+ ## Phase 3: Production
16
+
17
+ - [ ] Dockerfile for HF Spaces
18
+ - [ ] Vercel deployment
19
+ - [ ] Security audit
20
+ - [ ] Tag v1.0.0
@@ -0,0 +1,31 @@
1
+ # Specification: {{ tool_name }}
2
+
3
+ ## Overview
4
+
5
+ Brief description of what this tool does.
6
+
7
+ ## User Stories
8
+
9
+ - As a [user], I want to [action] so that [benefit]
10
+
11
+ ## API Endpoints
12
+
13
+ | Method | Path | Description |
14
+ |--------|------|-------------|
15
+ | GET | /api/health | Health check |
16
+ | POST | /api/upload | Upload file |
17
+
18
+ ## Data Models
19
+
20
+ ```python
21
+ class Item(BaseModel):
22
+ id: str
23
+ name: str
24
+ created_at: str
25
+ ```
26
+
27
+ ## Acceptance Criteria
28
+
29
+ - [ ] Feature 1 works
30
+ - [ ] Feature 2 works
31
+ - [ ] Tests pass