themis-cli 0.1.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.
themis/__init__.py ADDED
File without changes
themis/cli.py ADDED
@@ -0,0 +1,19 @@
1
+ import click
2
+ from themis.commands.init_idea import init_idea
3
+ from themis.commands.init_project import init_project
4
+ from themis.commands.promote import promote
5
+ from themis.commands.list import list_all
6
+ from themis.commands.config import config
7
+
8
+
9
+ @click.group()
10
+ def main():
11
+ """Themis — AI-assisted project workflow manager."""
12
+ pass
13
+
14
+
15
+ main.add_command(init_idea)
16
+ main.add_command(init_project)
17
+ main.add_command(promote)
18
+ main.add_command(list_all, name="list")
19
+ main.add_command(config)
File without changes
@@ -0,0 +1,40 @@
1
+ import click
2
+ from rich.console import Console
3
+ from themis.config import load_config, save_config, get_workspace
4
+
5
+ console = Console()
6
+
7
+
8
+ @click.group()
9
+ def config():
10
+ """Manage Themis configuration."""
11
+ pass
12
+
13
+
14
+ @config.command()
15
+ @click.argument("key")
16
+ @click.argument("value")
17
+ def set(key: str, value: str):
18
+ """Set a configuration value."""
19
+ allowed = ["workspace"]
20
+ if key not in allowed:
21
+ console.print(f"[red]❌ Unknown key:[/red] {key}")
22
+ console.print(f"Allowed keys: {', '.join(allowed)}")
23
+ raise SystemExit(1)
24
+
25
+ data = load_config()
26
+ data[key] = value
27
+ save_config(data)
28
+ console.print(f"[green]✅ {key}[/green] → {value}")
29
+
30
+
31
+ @config.command()
32
+ @click.argument("key")
33
+ def get(key: str):
34
+ """Get a configuration value."""
35
+ data = load_config()
36
+ value = data.get(key)
37
+ if not value:
38
+ console.print(f"[dim]{key} is not set. Default: {get_workspace()}[/dim]")
39
+ return
40
+ console.print(f"{key}: [bold]{value}[/bold]")
@@ -0,0 +1,34 @@
1
+ import click
2
+ from rich.console import Console
3
+ from themis.config import INBOX_DIR
4
+ from themis.templates import (
5
+ render_inbox_claude,
6
+ render_inbox_readme,
7
+ render_decision_template,
8
+ )
9
+
10
+ console = Console()
11
+
12
+
13
+ @click.command()
14
+ @click.argument("name")
15
+ def init_idea(name: str):
16
+ """Create a new idea in inbox."""
17
+ idea_dir = INBOX_DIR / name
18
+
19
+ if idea_dir.exists():
20
+ console.print(f"[red]❌ Idea already exists:[/red] {idea_dir}")
21
+ raise SystemExit(1)
22
+
23
+ decisions_dir = idea_dir / "decisions"
24
+ decisions_dir.mkdir(parents=True)
25
+
26
+ (idea_dir / "CLAUDE.md").write_text(render_inbox_claude(name))
27
+ (idea_dir / "README.md").write_text(render_inbox_readme(name))
28
+ (decisions_dir / "000-template.md").write_text(render_decision_template())
29
+
30
+ console.print(f"[green]✅ Idea created:[/green] {idea_dir}")
31
+ console.print("\nNext steps:")
32
+ console.print(" 1. Open CLAUDE.md and define the problem")
33
+ console.print(" 2. Start researching with Claude")
34
+ console.print(f" 3. When ready: [bold]themis promote {name}[/bold]")
@@ -0,0 +1,83 @@
1
+ import click
2
+ import subprocess
3
+ from rich.console import Console
4
+ from themis.config import PROJECTS_DIR, STACKS
5
+ from themis.templates import render_project_claude, render_decision_template
6
+
7
+ console = Console()
8
+
9
+
10
+ def scaffold_stack(stack: str, project_dir):
11
+ """Run native tooling for the given stack."""
12
+ name = project_dir.name
13
+ parent = project_dir.parent
14
+
15
+ if stack == "python":
16
+ subprocess.run(["uv", "init", name], cwd=parent, check=True)
17
+ elif stack == "nextjs":
18
+ subprocess.run(
19
+ ["pnpm", "create", "next-app", name],
20
+ cwd=parent,
21
+ check=True,
22
+ )
23
+ elif stack == "angular":
24
+ subprocess.run(["ng", "new", name], cwd=parent, check=True)
25
+ elif stack == "node":
26
+ project_dir.mkdir(parents=True)
27
+ subprocess.run(["pnpm", "init"], cwd=project_dir, check=True)
28
+
29
+
30
+ def add_project_files(project_dir, stack: str, research_notes: str = ""):
31
+ """Add CLAUDE.md and docs/ to an existing project directory."""
32
+ docs_decisions = project_dir / "docs" / "decisions"
33
+ docs_decisions.mkdir(parents=True, exist_ok=True)
34
+
35
+ (project_dir / "CLAUDE.md").write_text(
36
+ render_project_claude(project_dir.name, stack, research_notes)
37
+ )
38
+ (docs_decisions / "000-template.md").write_text(render_decision_template())
39
+
40
+ gitignore = project_dir / ".gitignore"
41
+ if not gitignore.exists():
42
+ gitignore.write_text(".env\n.env.*\n.DS_Store\n")
43
+
44
+
45
+ def init_git(project_dir):
46
+ """Initialize git repo and make first commit."""
47
+ subprocess.run(["git", "init", "-b", "main"], cwd=project_dir, check=True)
48
+ subprocess.run(["git", "add", "-A"], cwd=project_dir, check=True)
49
+ subprocess.run(
50
+ ["git", "commit", "-m", "chore: init project"],
51
+ cwd=project_dir,
52
+ check=True,
53
+ )
54
+
55
+
56
+ @click.command()
57
+ @click.argument("name")
58
+ @click.argument("stack", required=False)
59
+ def init_project(name: str, stack: str):
60
+ """Create a new project."""
61
+ if not stack:
62
+ stack = click.prompt(
63
+ "Stack",
64
+ type=click.Choice(STACKS),
65
+ show_choices=True,
66
+ )
67
+
68
+ project_dir = PROJECTS_DIR / name
69
+
70
+ if project_dir.exists():
71
+ console.print(f"[red]❌ Project already exists:[/red] {project_dir}")
72
+ raise SystemExit(1)
73
+
74
+ PROJECTS_DIR.mkdir(parents=True, exist_ok=True)
75
+
76
+ console.print(f"[bold]🚀 Initializing project:[/bold] {name} ({stack})")
77
+
78
+ scaffold_stack(stack, project_dir)
79
+ add_project_files(project_dir, stack)
80
+ init_git(project_dir)
81
+
82
+ console.print(f"\n[green]✅ Project ready:[/green] {project_dir}")
83
+ console.print(f"\nNext: [bold]cd {project_dir}[/bold] and open Claude Code")
@@ -0,0 +1,35 @@
1
+ import click
2
+ from rich.console import Console
3
+ from rich.table import Table
4
+ from themis.config import INBOX_DIR, PROJECTS_DIR
5
+
6
+ console = Console()
7
+
8
+
9
+ def get_subdirs(path):
10
+ if not path.exists():
11
+ return []
12
+ return sorted([d.name for d in path.iterdir() if d.is_dir()])
13
+
14
+
15
+ @click.command()
16
+ def list_all():
17
+ """List all ideas and projects."""
18
+ ideas = get_subdirs(INBOX_DIR)
19
+ projects = get_subdirs(PROJECTS_DIR)
20
+
21
+ if not ideas and not projects:
22
+ console.print("[dim]No ideas or projects found.[/dim]")
23
+ return
24
+
25
+ table = Table(title="Themis Workspace", show_lines=True)
26
+ table.add_column("Type", style="dim")
27
+ table.add_column("Name", style="bold")
28
+
29
+ for idea in ideas:
30
+ table.add_row("💡 idea", idea)
31
+
32
+ for project in projects:
33
+ table.add_row("🚀 project", project)
34
+
35
+ console.print(table)
@@ -0,0 +1,75 @@
1
+ import click
2
+ import shutil
3
+ from rich.console import Console
4
+ from themis.config import INBOX_DIR, PROJECTS_DIR, STACKS
5
+ from themis.commands.init_project import scaffold_stack, add_project_files, init_git
6
+
7
+ console = Console()
8
+
9
+
10
+ def extract_research_notes(claude_md_path) -> str:
11
+ """Extract full content of inbox CLAUDE.md to use as research notes."""
12
+ if not claude_md_path.exists():
13
+ return ""
14
+ return claude_md_path.read_text()
15
+
16
+
17
+ def migrate_decisions(inbox_dir, project_dir):
18
+ """Copy decisions from inbox to project docs/decisions/."""
19
+ src = inbox_dir / "decisions"
20
+ dst = project_dir / "docs" / "decisions"
21
+
22
+ if not src.exists():
23
+ return
24
+
25
+ for f in src.iterdir():
26
+ if f.name == "000-template.md":
27
+ continue
28
+ dest_file = dst / f.name
29
+ shutil.copy2(f, dest_file)
30
+ console.print(f" [dim]Migrated:[/dim] {f.name}")
31
+
32
+
33
+ @click.command()
34
+ @click.argument("name")
35
+ @click.argument("stack", required=False)
36
+ def promote(name: str, stack: str):
37
+ """Promote an idea from inbox to a full project."""
38
+ idea_dir = INBOX_DIR / name
39
+
40
+ if not idea_dir.exists():
41
+ console.print(f"[red]❌ Idea not found:[/red] {idea_dir}")
42
+ raise SystemExit(1)
43
+
44
+ if not stack:
45
+ stack = click.prompt(
46
+ "Stack",
47
+ type=click.Choice(STACKS),
48
+ show_choices=True,
49
+ )
50
+
51
+ project_dir = PROJECTS_DIR / name
52
+
53
+ if project_dir.exists():
54
+ console.print(f"[red]❌ Project already exists:[/red] {project_dir}")
55
+ raise SystemExit(1)
56
+
57
+ PROJECTS_DIR.mkdir(parents=True, exist_ok=True)
58
+
59
+ console.print(f"[bold]🚀 Promoting:[/bold] {name} → projects/ ({stack})")
60
+
61
+ research_notes = extract_research_notes(idea_dir / "CLAUDE.md")
62
+
63
+ scaffold_stack(stack, project_dir)
64
+ add_project_files(project_dir, stack, research_notes)
65
+
66
+ console.print("\n[dim]Migrating decisions...[/dim]")
67
+ migrate_decisions(idea_dir, project_dir)
68
+
69
+ init_git(project_dir)
70
+
71
+ shutil.rmtree(idea_dir)
72
+ console.print(f"[dim]Removed inbox/{name}[/dim]")
73
+
74
+ console.print(f"\n[green]✅ Project ready:[/green] {project_dir}")
75
+ console.print(f"\nNext: [bold]cd {project_dir}[/bold] and open Claude Code")
themis/config.py ADDED
@@ -0,0 +1,30 @@
1
+ import json
2
+ from pathlib import Path
3
+
4
+ CONFIG_DIR = Path.home() / ".config" / "themis"
5
+ CONFIG_FILE = CONFIG_DIR / "config.json"
6
+
7
+ DEFAULT_WORKSPACE = Path.home() / "themis-workspace"
8
+
9
+
10
+ def load_config() -> dict:
11
+ if not CONFIG_FILE.exists():
12
+ return {}
13
+ return json.loads(CONFIG_FILE.read_text())
14
+
15
+
16
+ def save_config(data: dict):
17
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
18
+ CONFIG_FILE.write_text(json.dumps(data, indent=2))
19
+
20
+
21
+ def get_workspace() -> Path:
22
+ config = load_config()
23
+ return Path(config.get("workspace", DEFAULT_WORKSPACE))
24
+
25
+
26
+ BASE_DIR = get_workspace()
27
+ INBOX_DIR = BASE_DIR / "inbox"
28
+ PROJECTS_DIR = BASE_DIR / "projects"
29
+
30
+ STACKS = ["python", "angular", "nextjs", "node"]
themis/templates.py ADDED
@@ -0,0 +1,83 @@
1
+ def render_inbox_claude(name: str) -> str:
2
+ return f"""# Idea: {name}
3
+
4
+ ## Problem
5
+ (What problem are you trying to solve?)
6
+
7
+ ## Hypothesis
8
+ (What's your proposed solution?)
9
+
10
+ ## Open Questions
11
+ (What do you still need to figure out?)
12
+
13
+ ## Context
14
+ (Relevant notes from research)
15
+ """
16
+
17
+
18
+ def render_inbox_readme(name: str) -> str:
19
+ return f"""# {name}
20
+
21
+ ## Problem
22
+ (What real problem does this project solve?)
23
+
24
+ ## Target Audience
25
+ (Who is this for?)
26
+
27
+ ## MVP Scope
28
+ (What is the smallest thing that can validate the idea?)
29
+
30
+ ## Out of Scope
31
+ (What will NOT be built yet?)
32
+
33
+ ## Success Criteria
34
+ (How do we know this is working?)
35
+ """
36
+
37
+
38
+ def render_project_claude(name: str, stack: str, research_notes: str = "") -> str:
39
+ notes_section = ""
40
+ if research_notes:
41
+ notes_section = f"\n## Research Notes\n{research_notes}\n"
42
+
43
+ return f"""# {name}
44
+
45
+ ## Stack
46
+ - Type: {stack}
47
+ - (add frameworks, DB, etc.)
48
+
49
+ ## Conventions
50
+ - Commits in English: feat / fix / chore / docs
51
+ - Tests required for business logic
52
+ - Important decisions → docs/decisions/
53
+
54
+ ## Rules
55
+ - Do not implement without understanding the problem first
56
+ - Prefer simple and reversible solutions
57
+ - Ask before assuming if context is missing
58
+ - Explain trade-offs when more than one valid option exists
59
+
60
+ ## Current Status
61
+ - Stage: MVP
62
+ - Last updated: (update this)
63
+ {notes_section}"""
64
+
65
+
66
+ def render_decision_template() -> str:
67
+ return """# 000 - Decision Title
68
+
69
+ **Date:** YYYY-MM-DD
70
+ **Status:** Proposed | Accepted | Rejected
71
+
72
+ ## Context
73
+ (What situation led to this decision?)
74
+
75
+ ## Decision
76
+ (What did we decide?)
77
+
78
+ ## Alternatives Considered
79
+ (What other options were evaluated?)
80
+
81
+ ## Trade-offs
82
+ (What do we gain and what do we lose?)
83
+ """
@@ -0,0 +1,81 @@
1
+ Metadata-Version: 2.4
2
+ Name: themis-cli
3
+ Version: 0.1.0
4
+ Summary: A CLI tool to manage AI-assisted project workflows
5
+ Requires-Python: >=3.12
6
+ Requires-Dist: click>=8.1.0
7
+ Requires-Dist: rich>=13.0.0
8
+ Description-Content-Type: text/markdown
9
+
10
+ # Themis
11
+
12
+ A CLI tool to manage AI-assisted project workflows.
13
+
14
+ Themis helps you structure the full lifecycle of a project — from the initial
15
+ idea and research phase through to a fully initialized, version-controlled
16
+ codebase — keeping all context, decisions, and documentation in one place.
17
+
18
+ ## Why Themis?
19
+
20
+ When working with AI assistants like Claude, valuable research and
21
+ architectural decisions often get lost in chat history. Themis solves this by
22
+ giving every idea and project a structured home where context is preserved and
23
+ travels with the code.
24
+
25
+ ## Workspace Structure
26
+
27
+ ```text
28
+ ~/themis/ai-workspace/
29
+ ├── inbox/ # Ideas under exploration
30
+ └── projects/ # Active projects
31
+ ```
32
+
33
+ Installation
34
+ Requires Python 3.12+ and uv.
35
+
36
+ bash
37
+ git clone https://github.com/youruser/themis.git
38
+ cd themis
39
+ uv tool install .
40
+ Commands
41
+ themis init-idea <name>
42
+ Creates a new idea in inbox with a structured CLAUDE.md and README.
43
+
44
+ bash
45
+ themis init-idea my-saas-idea
46
+ themis init-project <name> [stack]
47
+ Creates a fully initialized project with native tooling, CLAUDE.md, and Git.
48
+
49
+ Supported stacks: python, angular, nextjs, node
50
+
51
+ bash
52
+ themis init-project my-project python
53
+ # or let Themis ask you
54
+ themis init-project my-project
55
+ themis promote <name> [stack]
56
+ Promotes an idea from inbox to a full project, migrating decisions and
57
+ research context automatically. Removes the idea from inbox after migration.
58
+
59
+ bash
60
+ themis promote my-saas-idea python
61
+ themis list
62
+ Lists all ideas and projects in your workspace.
63
+
64
+ bash
65
+ themis list
66
+ Workflow
67
+ text
68
+ 1. Got an idea?
69
+ themis init-idea my-idea
70
+
71
+ 2. Research and explore with Claude.
72
+ Document decisions in inbox/my-idea/decisions/
73
+
74
+ 3. Ready to build?
75
+ themis promote my-idea python
76
+
77
+ 4. Open your project and start building.
78
+ cd ~/themis/ai-workspace/projects/my-idea
79
+ claude
80
+ License
81
+ MIT
@@ -0,0 +1,14 @@
1
+ themis/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ themis/cli.py,sha256=c_HdWQBCDESCmXK5YvgMVOT2bSJF0EIBXJ5a0x-XrRQ,492
3
+ themis/config.py,sha256=Q2F-ZlvRaiBRvRNByEqdoYQ744ySlaZ8-xQJy-daK-w,708
4
+ themis/templates.py,sha256=6NMwAc2larbgDx9Io2jl-Hw4UTYUsr11wXIQBw32niE,1688
5
+ themis/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ themis/commands/config.py,sha256=soBZtO5lDhHYi7XhhQzy8Cs6D4u7dsEPZ2Li0GuZ2-8,995
7
+ themis/commands/init_idea.py,sha256=0cNsBY0n_i-dyrH5NxAaZEZ0LVO2KUTJhWJVGYzrMpI,1075
8
+ themis/commands/init_project.py,sha256=GlkFBvLd9by_YJeP0s1cWDLkR7W_N_Y3L9bpJEfIqcM,2679
9
+ themis/commands/list.py,sha256=AFWB4zUqENOvP1gvMQf0AT_4WYBzJIj_LbAWxUgZ27I,861
10
+ themis/commands/promote.py,sha256=3lcUb4DaLlzJanlPJb903EpHOQzRXaG8hu4HvUEXyNk,2225
11
+ themis_cli-0.1.0.dist-info/METADATA,sha256=64IaA8vn4IjlaXA1dIWeZEdGyFlkT_AqEoI4ojbBEg4,2077
12
+ themis_cli-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
13
+ themis_cli-0.1.0.dist-info/entry_points.txt,sha256=lc8L8-DisxtChTT_wHXYp6oLJtNhUrcZ2xwD7QEUBzk,43
14
+ themis_cli-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ themis = themis.cli:main