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.
- saasforge/__init__.py +0 -0
- saasforge/__main__.py +4 -0
- saasforge/adapters/__init__.py +40 -0
- saasforge/adapters/aider.py +42 -0
- saasforge/adapters/antigravity.py +29 -0
- saasforge/adapters/base.py +13 -0
- saasforge/adapters/claude.py +22 -0
- saasforge/adapters/codex.py +31 -0
- saasforge/adapters/copilot.py +23 -0
- saasforge/adapters/cursor.py +34 -0
- saasforge/adapters/gemini.py +32 -0
- saasforge/adapters/kilocode.py +36 -0
- saasforge/adapters/opencode.py +74 -0
- saasforge/adapters/openroutercli.py +39 -0
- saasforge/adapters/qoder.py +25 -0
- saasforge/adapters/qwen.py +31 -0
- saasforge/adapters/roo.py +25 -0
- saasforge/adapters/terminal.py +45 -0
- saasforge/adapters/windsurf.py +30 -0
- saasforge/builtins/constitution.md +18 -0
- saasforge/builtins/specs/plan-template.md +20 -0
- saasforge/builtins/specs/spec-template.md +31 -0
- saasforge/builtins/specs/tasks-template.md +18 -0
- saasforge/builtins/tool/Dockerfile.j2 +26 -0
- saasforge/builtins/tool/app.py.j2 +14 -0
- saasforge/builtins/tool/vercel.json.j2 +14 -0
- saasforge/cli.py +354 -0
- saasforge/config.py +79 -0
- saasforge/dna_engine.py +82 -0
- saasforge/memory.py +150 -0
- saasforge/scaffold.py +58 -0
- saasforge/templates.py +36 -0
- saasforge-0.4.0.dist-info/METADATA +309 -0
- saasforge-0.4.0.dist-info/RECORD +39 -0
- saasforge-0.4.0.dist-info/WHEEL +5 -0
- saasforge-0.4.0.dist-info/entry_points.txt +3 -0
- saasforge-0.4.0.dist-info/licenses/LICENSE +23 -0
- saasforge-0.4.0.dist-info/licenses/NOTICE.md +13 -0
- 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,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
|