themis-cli 0.1.0__tar.gz
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_cli-0.1.0/PKG-INFO +81 -0
- themis_cli-0.1.0/README.md +72 -0
- themis_cli-0.1.0/pyproject.toml +20 -0
- themis_cli-0.1.0/src/themis/__init__.py +0 -0
- themis_cli-0.1.0/src/themis/cli.py +19 -0
- themis_cli-0.1.0/src/themis/commands/__init__.py +0 -0
- themis_cli-0.1.0/src/themis/commands/config.py +40 -0
- themis_cli-0.1.0/src/themis/commands/init_idea.py +34 -0
- themis_cli-0.1.0/src/themis/commands/init_project.py +83 -0
- themis_cli-0.1.0/src/themis/commands/list.py +35 -0
- themis_cli-0.1.0/src/themis/commands/promote.py +75 -0
- themis_cli-0.1.0/src/themis/config.py +30 -0
- themis_cli-0.1.0/src/themis/templates.py +83 -0
- themis_cli-0.1.0/tests/__init__.py +0 -0
- themis_cli-0.1.0/uv.lock +82 -0
|
@@ -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,72 @@
|
|
|
1
|
+
# Themis
|
|
2
|
+
|
|
3
|
+
A CLI tool to manage AI-assisted project workflows.
|
|
4
|
+
|
|
5
|
+
Themis helps you structure the full lifecycle of a project — from the initial
|
|
6
|
+
idea and research phase through to a fully initialized, version-controlled
|
|
7
|
+
codebase — keeping all context, decisions, and documentation in one place.
|
|
8
|
+
|
|
9
|
+
## Why Themis?
|
|
10
|
+
|
|
11
|
+
When working with AI assistants like Claude, valuable research and
|
|
12
|
+
architectural decisions often get lost in chat history. Themis solves this by
|
|
13
|
+
giving every idea and project a structured home where context is preserved and
|
|
14
|
+
travels with the code.
|
|
15
|
+
|
|
16
|
+
## Workspace Structure
|
|
17
|
+
|
|
18
|
+
```text
|
|
19
|
+
~/themis/ai-workspace/
|
|
20
|
+
├── inbox/ # Ideas under exploration
|
|
21
|
+
└── projects/ # Active projects
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Installation
|
|
25
|
+
Requires Python 3.12+ and uv.
|
|
26
|
+
|
|
27
|
+
bash
|
|
28
|
+
git clone https://github.com/youruser/themis.git
|
|
29
|
+
cd themis
|
|
30
|
+
uv tool install .
|
|
31
|
+
Commands
|
|
32
|
+
themis init-idea <name>
|
|
33
|
+
Creates a new idea in inbox with a structured CLAUDE.md and README.
|
|
34
|
+
|
|
35
|
+
bash
|
|
36
|
+
themis init-idea my-saas-idea
|
|
37
|
+
themis init-project <name> [stack]
|
|
38
|
+
Creates a fully initialized project with native tooling, CLAUDE.md, and Git.
|
|
39
|
+
|
|
40
|
+
Supported stacks: python, angular, nextjs, node
|
|
41
|
+
|
|
42
|
+
bash
|
|
43
|
+
themis init-project my-project python
|
|
44
|
+
# or let Themis ask you
|
|
45
|
+
themis init-project my-project
|
|
46
|
+
themis promote <name> [stack]
|
|
47
|
+
Promotes an idea from inbox to a full project, migrating decisions and
|
|
48
|
+
research context automatically. Removes the idea from inbox after migration.
|
|
49
|
+
|
|
50
|
+
bash
|
|
51
|
+
themis promote my-saas-idea python
|
|
52
|
+
themis list
|
|
53
|
+
Lists all ideas and projects in your workspace.
|
|
54
|
+
|
|
55
|
+
bash
|
|
56
|
+
themis list
|
|
57
|
+
Workflow
|
|
58
|
+
text
|
|
59
|
+
1. Got an idea?
|
|
60
|
+
themis init-idea my-idea
|
|
61
|
+
|
|
62
|
+
2. Research and explore with Claude.
|
|
63
|
+
Document decisions in inbox/my-idea/decisions/
|
|
64
|
+
|
|
65
|
+
3. Ready to build?
|
|
66
|
+
themis promote my-idea python
|
|
67
|
+
|
|
68
|
+
4. Open your project and start building.
|
|
69
|
+
cd ~/themis/ai-workspace/projects/my-idea
|
|
70
|
+
claude
|
|
71
|
+
License
|
|
72
|
+
MIT
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "themis-cli"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "A CLI tool to manage AI-assisted project workflows"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"click>=8.1.0",
|
|
9
|
+
"rich>=13.0.0",
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
[project.scripts]
|
|
13
|
+
themis = "themis.cli:main"
|
|
14
|
+
|
|
15
|
+
[build-system]
|
|
16
|
+
requires = ["hatchling"]
|
|
17
|
+
build-backend = "hatchling.build"
|
|
18
|
+
|
|
19
|
+
[tool.hatch.build.targets.wheel]
|
|
20
|
+
packages = ["src/themis"]
|
|
File without changes
|
|
@@ -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")
|
|
@@ -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"]
|
|
@@ -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
|
+
"""
|
|
File without changes
|
themis_cli-0.1.0/uv.lock
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
version = 1
|
|
2
|
+
revision = 3
|
|
3
|
+
requires-python = ">=3.12"
|
|
4
|
+
|
|
5
|
+
[[package]]
|
|
6
|
+
name = "click"
|
|
7
|
+
version = "8.3.2"
|
|
8
|
+
source = { registry = "https://pypi.org/simple" }
|
|
9
|
+
dependencies = [
|
|
10
|
+
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
|
11
|
+
]
|
|
12
|
+
sdist = { url = "https://files.pythonhosted.org/packages/57/75/31212c6bf2503fdf920d87fee5d7a86a2e3bcf444984126f13d8e4016804/click-8.3.2.tar.gz", hash = "sha256:14162b8b3b3550a7d479eafa77dfd3c38d9dc8951f6f69c78913a8f9a7540fd5", size = 302856, upload-time = "2026-04-03T19:14:45.118Z" }
|
|
13
|
+
wheels = [
|
|
14
|
+
{ url = "https://files.pythonhosted.org/packages/e4/20/71885d8b97d4f3dde17b1fdb92dbd4908b00541c5a3379787137285f602e/click-8.3.2-py3-none-any.whl", hash = "sha256:1924d2c27c5653561cd2cae4548d1406039cb79b858b747cfea24924bbc1616d", size = 108379, upload-time = "2026-04-03T19:14:43.505Z" },
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[[package]]
|
|
18
|
+
name = "colorama"
|
|
19
|
+
version = "0.4.6"
|
|
20
|
+
source = { registry = "https://pypi.org/simple" }
|
|
21
|
+
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
|
22
|
+
wheels = [
|
|
23
|
+
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[[package]]
|
|
27
|
+
name = "markdown-it-py"
|
|
28
|
+
version = "4.0.0"
|
|
29
|
+
source = { registry = "https://pypi.org/simple" }
|
|
30
|
+
dependencies = [
|
|
31
|
+
{ name = "mdurl" },
|
|
32
|
+
]
|
|
33
|
+
sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" }
|
|
34
|
+
wheels = [
|
|
35
|
+
{ url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" },
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[[package]]
|
|
39
|
+
name = "mdurl"
|
|
40
|
+
version = "0.1.2"
|
|
41
|
+
source = { registry = "https://pypi.org/simple" }
|
|
42
|
+
sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
|
|
43
|
+
wheels = [
|
|
44
|
+
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
[[package]]
|
|
48
|
+
name = "pygments"
|
|
49
|
+
version = "2.20.0"
|
|
50
|
+
source = { registry = "https://pypi.org/simple" }
|
|
51
|
+
sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
|
|
52
|
+
wheels = [
|
|
53
|
+
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
[[package]]
|
|
57
|
+
name = "rich"
|
|
58
|
+
version = "15.0.0"
|
|
59
|
+
source = { registry = "https://pypi.org/simple" }
|
|
60
|
+
dependencies = [
|
|
61
|
+
{ name = "markdown-it-py" },
|
|
62
|
+
{ name = "pygments" },
|
|
63
|
+
]
|
|
64
|
+
sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" }
|
|
65
|
+
wheels = [
|
|
66
|
+
{ url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" },
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
[[package]]
|
|
70
|
+
name = "themis"
|
|
71
|
+
version = "0.1.0"
|
|
72
|
+
source = { editable = "." }
|
|
73
|
+
dependencies = [
|
|
74
|
+
{ name = "click" },
|
|
75
|
+
{ name = "rich" },
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
[package.metadata]
|
|
79
|
+
requires-dist = [
|
|
80
|
+
{ name = "click", specifier = ">=8.1.0" },
|
|
81
|
+
{ name = "rich", specifier = ">=13.0.0" },
|
|
82
|
+
]
|