blank-agentic-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.
Files changed (32) hide show
  1. blank_agentic_cli-0.1.0/LICENSE +21 -0
  2. blank_agentic_cli-0.1.0/PKG-INFO +88 -0
  3. blank_agentic_cli-0.1.0/README.md +65 -0
  4. blank_agentic_cli-0.1.0/pyproject.toml +54 -0
  5. blank_agentic_cli-0.1.0/setup.cfg +4 -0
  6. blank_agentic_cli-0.1.0/src/blank_agentic_cli.egg-info/PKG-INFO +88 -0
  7. blank_agentic_cli-0.1.0/src/blank_agentic_cli.egg-info/SOURCES.txt +30 -0
  8. blank_agentic_cli-0.1.0/src/blank_agentic_cli.egg-info/dependency_links.txt +1 -0
  9. blank_agentic_cli-0.1.0/src/blank_agentic_cli.egg-info/entry_points.txt +2 -0
  10. blank_agentic_cli-0.1.0/src/blank_agentic_cli.egg-info/top_level.txt +1 -0
  11. blank_agentic_cli-0.1.0/src/blank_cli/__init__.py +3 -0
  12. blank_agentic_cli-0.1.0/src/blank_cli/__main__.py +4 -0
  13. blank_agentic_cli-0.1.0/src/blank_cli/cli.py +116 -0
  14. blank_agentic_cli-0.1.0/src/blank_cli/scaffold.py +191 -0
  15. blank_agentic_cli-0.1.0/src/blank_cli/templates/.claude/settings.local.json.tpl +5 -0
  16. blank_agentic_cli-0.1.0/src/blank_cli/templates/.codex/config.toml.tpl +8 -0
  17. blank_agentic_cli-0.1.0/src/blank_cli/templates/.codex/project.md.tpl +19 -0
  18. blank_agentic_cli-0.1.0/src/blank_cli/templates/.gitignore.tpl +11 -0
  19. blank_agentic_cli-0.1.0/src/blank_cli/templates/.here.tpl +0 -0
  20. blank_agentic_cli-0.1.0/src/blank_cli/templates/README.md.tpl +20 -0
  21. blank_agentic_cli-0.1.0/src/blank_cli/templates/analysis/data/codebook.md.tpl +9 -0
  22. blank_agentic_cli-0.1.0/src/blank_cli/templates/analysis/output/figures/.gitkeep.tpl +0 -0
  23. blank_agentic_cli-0.1.0/src/blank_cli/templates/analysis/output/results/.gitkeep.tpl +0 -0
  24. blank_agentic_cli-0.1.0/src/blank_cli/templates/analysis/output/tables/.gitkeep.tpl +0 -0
  25. blank_agentic_cli-0.1.0/src/blank_cli/templates/analysis/scripts/00_setup.R.tpl +9 -0
  26. blank_agentic_cli-0.1.0/src/blank_cli/templates/analysis/scripts/20_data.R.tpl +6 -0
  27. blank_agentic_cli-0.1.0/src/blank_cli/templates/analysis/scripts/30_results_main.R.tpl +7 -0
  28. blank_agentic_cli-0.1.0/src/blank_cli/templates/paper/aesthetics.typ.tpl +1 -0
  29. blank_agentic_cli-0.1.0/src/blank_cli/templates/paper/paper_blank.typ.tpl +1 -0
  30. blank_agentic_cli-0.1.0/src/blank_cli/templates/paper/paper_latex.typ.tpl +12 -0
  31. blank_agentic_cli-0.1.0/src/blank_cli/templates/paper/ref.bib.tpl +6 -0
  32. blank_agentic_cli-0.1.0/tests/test_cli.py +71 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Shusuke Ioku
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,88 @@
1
+ Metadata-Version: 2.4
2
+ Name: blank-agentic-cli
3
+ Version: 0.1.0
4
+ Summary: CLI scaffolder for agentic AI research projects
5
+ Author: Shusuke Ioku
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/shusuke-ioku/blank
8
+ Project-URL: Repository, https://github.com/shusuke-ioku/blank
9
+ Project-URL: Issues, https://github.com/shusuke-ioku/blank/issues
10
+ Keywords: cli,scaffold,research,agentic-ai,typst
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: Topic :: Software Development :: Code Generators
14
+ Classifier: Topic :: Utilities
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: License :: OSI Approved :: MIT License
18
+ Classifier: Operating System :: OS Independent
19
+ Requires-Python: >=3.9
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Dynamic: license-file
23
+
24
+ # blank
25
+
26
+ `blank` is a CLI scaffolder for research projects driven by agentic AI workflows.
27
+
28
+ ## Install
29
+
30
+ From PyPI (after release):
31
+
32
+ ```bash
33
+ pipx install blank-agentic-cli
34
+ ```
35
+
36
+ From GitHub:
37
+
38
+ ```bash
39
+ pipx install git+https://github.com/shusuke-ioku/blank.git
40
+ ```
41
+
42
+ ## Usage
43
+
44
+ ```bash
45
+ blank init
46
+ blank init my-project
47
+ blank init my-project --project-name "My Project"
48
+ blank init my-project --dry-run
49
+ blank init my-project --force
50
+ blank init my-project --no-agents
51
+ blank init my-project --paper-template latex
52
+ blank init my-project --paper-template blank
53
+ ```
54
+
55
+ If `--paper-template` is omitted and you run in a terminal, `blank init` asks you to choose:
56
+ - `latex`: LaTeX-like Typst starter
57
+ - `blank`: minimal empty Typst file
58
+
59
+ ## Generated scaffold
60
+
61
+ - `analysis/scripts/`
62
+ - `analysis/data/`
63
+ - `analysis/output/`
64
+ - `paper/`
65
+ - `idea/`
66
+ - `.codex/` and `.claude/` by default
67
+
68
+ ## Development
69
+
70
+ ```bash
71
+ python3 -m venv .venv
72
+ source .venv/bin/activate
73
+ pip install -e . pytest
74
+ pytest
75
+ ```
76
+
77
+ ## Release (PyPI)
78
+
79
+ 1. Create PyPI project `blank-agentic-cli` and enable Trusted Publishing for this GitHub repo.
80
+ 2. (Optional) Run GitHub Action `publish` manually with `testpypi` to verify packaging.
81
+ 3. Tag a release:
82
+
83
+ ```bash
84
+ git tag v0.1.0
85
+ git push origin v0.1.0
86
+ ```
87
+
88
+ 4. The `publish` workflow builds and uploads to PyPI.
@@ -0,0 +1,65 @@
1
+ # blank
2
+
3
+ `blank` is a CLI scaffolder for research projects driven by agentic AI workflows.
4
+
5
+ ## Install
6
+
7
+ From PyPI (after release):
8
+
9
+ ```bash
10
+ pipx install blank-agentic-cli
11
+ ```
12
+
13
+ From GitHub:
14
+
15
+ ```bash
16
+ pipx install git+https://github.com/shusuke-ioku/blank.git
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ ```bash
22
+ blank init
23
+ blank init my-project
24
+ blank init my-project --project-name "My Project"
25
+ blank init my-project --dry-run
26
+ blank init my-project --force
27
+ blank init my-project --no-agents
28
+ blank init my-project --paper-template latex
29
+ blank init my-project --paper-template blank
30
+ ```
31
+
32
+ If `--paper-template` is omitted and you run in a terminal, `blank init` asks you to choose:
33
+ - `latex`: LaTeX-like Typst starter
34
+ - `blank`: minimal empty Typst file
35
+
36
+ ## Generated scaffold
37
+
38
+ - `analysis/scripts/`
39
+ - `analysis/data/`
40
+ - `analysis/output/`
41
+ - `paper/`
42
+ - `idea/`
43
+ - `.codex/` and `.claude/` by default
44
+
45
+ ## Development
46
+
47
+ ```bash
48
+ python3 -m venv .venv
49
+ source .venv/bin/activate
50
+ pip install -e . pytest
51
+ pytest
52
+ ```
53
+
54
+ ## Release (PyPI)
55
+
56
+ 1. Create PyPI project `blank-agentic-cli` and enable Trusted Publishing for this GitHub repo.
57
+ 2. (Optional) Run GitHub Action `publish` manually with `testpypi` to verify packaging.
58
+ 3. Tag a release:
59
+
60
+ ```bash
61
+ git tag v0.1.0
62
+ git push origin v0.1.0
63
+ ```
64
+
65
+ 4. The `publish` workflow builds and uploads to PyPI.
@@ -0,0 +1,54 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "blank-agentic-cli"
7
+ version = "0.1.0"
8
+ description = "CLI scaffolder for agentic AI research projects"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ authors = [
12
+ { name = "Shusuke Ioku" }
13
+ ]
14
+ license = { text = "MIT" }
15
+ keywords = ["cli", "scaffold", "research", "agentic-ai", "typst"]
16
+ classifiers = [
17
+ "Environment :: Console",
18
+ "Intended Audience :: Science/Research",
19
+ "Topic :: Software Development :: Code Generators",
20
+ "Topic :: Utilities",
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3 :: Only",
23
+ "License :: OSI Approved :: MIT License",
24
+ "Operating System :: OS Independent"
25
+ ]
26
+
27
+ [project.urls]
28
+ Homepage = "https://github.com/shusuke-ioku/blank"
29
+ Repository = "https://github.com/shusuke-ioku/blank"
30
+ Issues = "https://github.com/shusuke-ioku/blank/issues"
31
+
32
+ [project.scripts]
33
+ blank = "blank_cli.cli:main"
34
+
35
+ [tool.setuptools]
36
+ package-dir = {"" = "src"}
37
+
38
+ [tool.setuptools.packages.find]
39
+ where = ["src"]
40
+
41
+ [tool.setuptools.package-data]
42
+ blank_cli = [
43
+ "templates/**/*.tpl",
44
+ "templates/.gitignore.tpl",
45
+ "templates/.here.tpl",
46
+ "templates/analysis/output/figures/.gitkeep.tpl",
47
+ "templates/analysis/output/tables/.gitkeep.tpl",
48
+ "templates/analysis/output/results/.gitkeep.tpl",
49
+ "templates/.codex/*.tpl",
50
+ "templates/.claude/*.tpl",
51
+ ]
52
+
53
+ [tool.pytest.ini_options]
54
+ testpaths = ["tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,88 @@
1
+ Metadata-Version: 2.4
2
+ Name: blank-agentic-cli
3
+ Version: 0.1.0
4
+ Summary: CLI scaffolder for agentic AI research projects
5
+ Author: Shusuke Ioku
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/shusuke-ioku/blank
8
+ Project-URL: Repository, https://github.com/shusuke-ioku/blank
9
+ Project-URL: Issues, https://github.com/shusuke-ioku/blank/issues
10
+ Keywords: cli,scaffold,research,agentic-ai,typst
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: Topic :: Software Development :: Code Generators
14
+ Classifier: Topic :: Utilities
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: License :: OSI Approved :: MIT License
18
+ Classifier: Operating System :: OS Independent
19
+ Requires-Python: >=3.9
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Dynamic: license-file
23
+
24
+ # blank
25
+
26
+ `blank` is a CLI scaffolder for research projects driven by agentic AI workflows.
27
+
28
+ ## Install
29
+
30
+ From PyPI (after release):
31
+
32
+ ```bash
33
+ pipx install blank-agentic-cli
34
+ ```
35
+
36
+ From GitHub:
37
+
38
+ ```bash
39
+ pipx install git+https://github.com/shusuke-ioku/blank.git
40
+ ```
41
+
42
+ ## Usage
43
+
44
+ ```bash
45
+ blank init
46
+ blank init my-project
47
+ blank init my-project --project-name "My Project"
48
+ blank init my-project --dry-run
49
+ blank init my-project --force
50
+ blank init my-project --no-agents
51
+ blank init my-project --paper-template latex
52
+ blank init my-project --paper-template blank
53
+ ```
54
+
55
+ If `--paper-template` is omitted and you run in a terminal, `blank init` asks you to choose:
56
+ - `latex`: LaTeX-like Typst starter
57
+ - `blank`: minimal empty Typst file
58
+
59
+ ## Generated scaffold
60
+
61
+ - `analysis/scripts/`
62
+ - `analysis/data/`
63
+ - `analysis/output/`
64
+ - `paper/`
65
+ - `idea/`
66
+ - `.codex/` and `.claude/` by default
67
+
68
+ ## Development
69
+
70
+ ```bash
71
+ python3 -m venv .venv
72
+ source .venv/bin/activate
73
+ pip install -e . pytest
74
+ pytest
75
+ ```
76
+
77
+ ## Release (PyPI)
78
+
79
+ 1. Create PyPI project `blank-agentic-cli` and enable Trusted Publishing for this GitHub repo.
80
+ 2. (Optional) Run GitHub Action `publish` manually with `testpypi` to verify packaging.
81
+ 3. Tag a release:
82
+
83
+ ```bash
84
+ git tag v0.1.0
85
+ git push origin v0.1.0
86
+ ```
87
+
88
+ 4. The `publish` workflow builds and uploads to PyPI.
@@ -0,0 +1,30 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/blank_agentic_cli.egg-info/PKG-INFO
5
+ src/blank_agentic_cli.egg-info/SOURCES.txt
6
+ src/blank_agentic_cli.egg-info/dependency_links.txt
7
+ src/blank_agentic_cli.egg-info/entry_points.txt
8
+ src/blank_agentic_cli.egg-info/top_level.txt
9
+ src/blank_cli/__init__.py
10
+ src/blank_cli/__main__.py
11
+ src/blank_cli/cli.py
12
+ src/blank_cli/scaffold.py
13
+ src/blank_cli/templates/.gitignore.tpl
14
+ src/blank_cli/templates/.here.tpl
15
+ src/blank_cli/templates/README.md.tpl
16
+ src/blank_cli/templates/.claude/settings.local.json.tpl
17
+ src/blank_cli/templates/.codex/config.toml.tpl
18
+ src/blank_cli/templates/.codex/project.md.tpl
19
+ src/blank_cli/templates/analysis/data/codebook.md.tpl
20
+ src/blank_cli/templates/analysis/output/figures/.gitkeep.tpl
21
+ src/blank_cli/templates/analysis/output/results/.gitkeep.tpl
22
+ src/blank_cli/templates/analysis/output/tables/.gitkeep.tpl
23
+ src/blank_cli/templates/analysis/scripts/00_setup.R.tpl
24
+ src/blank_cli/templates/analysis/scripts/20_data.R.tpl
25
+ src/blank_cli/templates/analysis/scripts/30_results_main.R.tpl
26
+ src/blank_cli/templates/paper/aesthetics.typ.tpl
27
+ src/blank_cli/templates/paper/paper_blank.typ.tpl
28
+ src/blank_cli/templates/paper/paper_latex.typ.tpl
29
+ src/blank_cli/templates/paper/ref.bib.tpl
30
+ tests/test_cli.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ blank = blank_cli.cli:main
@@ -0,0 +1,3 @@
1
+ __all__ = ["__version__"]
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,4 @@
1
+ from .cli import main
2
+
3
+ if __name__ == "__main__":
4
+ raise SystemExit(main())
@@ -0,0 +1,116 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import sys
5
+ from importlib import resources
6
+ from pathlib import Path
7
+
8
+ from .scaffold import ScaffoldConflictError, apply_actions, plan_actions
9
+
10
+
11
+ def _build_parser() -> argparse.ArgumentParser:
12
+ parser = argparse.ArgumentParser(prog="blank", description="Scaffold research projects")
13
+ subparsers = parser.add_subparsers(dest="command", required=True)
14
+
15
+ init_parser = subparsers.add_parser("init", help="Initialize a research project scaffold")
16
+ init_parser.add_argument("target_dir", nargs="?", default=".", help="Target directory (default: current directory)")
17
+ init_parser.add_argument(
18
+ "--project-name",
19
+ help="Project name used in generated template files (default: target directory name)",
20
+ )
21
+ init_parser.add_argument(
22
+ "--force",
23
+ action="store_true",
24
+ help="Overwrite scaffold files when they already exist and differ",
25
+ )
26
+ init_parser.add_argument("--dry-run", action="store_true", help="Show planned actions without writing files")
27
+ init_parser.add_argument(
28
+ "--no-agents",
29
+ action="store_true",
30
+ help="Skip generating .codex/ and .claude/ files",
31
+ )
32
+ init_parser.add_argument(
33
+ "--paper-template",
34
+ choices=["latex", "blank"],
35
+ help="Paper template style for paper/paper.typ (latex or blank)",
36
+ )
37
+
38
+ return parser
39
+
40
+
41
+ def _prompt_paper_template() -> str:
42
+ print("Choose paper template for paper/paper.typ:")
43
+ print("1) latex (LaTeX-like layout) [recommended]")
44
+ print("2) blank (empty starter file)")
45
+ while True:
46
+ choice = input("Enter 1 or 2 [1]: ").strip()
47
+ if choice in ("", "1"):
48
+ return "latex"
49
+ if choice == "2":
50
+ return "blank"
51
+ print("Invalid choice. Please enter 1 or 2.")
52
+
53
+
54
+ def _command_init(args: argparse.Namespace) -> int:
55
+ target_dir = Path(args.target_dir).expanduser().resolve()
56
+ project_name = args.project_name or target_dir.name
57
+ include_agents = not args.no_agents
58
+ paper_template = args.paper_template
59
+ if paper_template is None:
60
+ if sys.stdin.isatty():
61
+ paper_template = _prompt_paper_template()
62
+ else:
63
+ paper_template = "latex"
64
+
65
+ templates_dir = resources.files("blank_cli") / "templates"
66
+ templates_path = Path(str(templates_dir))
67
+
68
+ target_dir.mkdir(parents=True, exist_ok=True)
69
+
70
+ try:
71
+ actions = plan_actions(
72
+ target_dir=target_dir,
73
+ templates_dir=templates_path,
74
+ project_name=project_name,
75
+ include_agents=include_agents,
76
+ paper_template=paper_template,
77
+ force=args.force,
78
+ )
79
+ except ScaffoldConflictError as exc:
80
+ print(str(exc), file=sys.stderr)
81
+ return 3
82
+
83
+ counts = apply_actions(
84
+ actions=actions,
85
+ target_dir=target_dir,
86
+ templates_dir=templates_path,
87
+ project_name=project_name,
88
+ include_agents=include_agents,
89
+ paper_template=paper_template,
90
+ dry_run=args.dry_run,
91
+ )
92
+
93
+ mode = "DRY RUN" if args.dry_run else "DONE"
94
+ print(f"[{mode}] blank init -> {target_dir}")
95
+ print(f"paper_template={paper_template}")
96
+ print(
97
+ f"created_dirs={counts['created_dirs']} created_files={counts['created_files']} "
98
+ f"replaced_files={counts['replaced_files']} skipped={counts['skipped']}"
99
+ )
100
+
101
+ return 0
102
+
103
+
104
+ def main(argv: list[str] | None = None) -> int:
105
+ parser = _build_parser()
106
+ args = parser.parse_args(argv)
107
+
108
+ if args.command == "init":
109
+ return _command_init(args)
110
+
111
+ parser.print_usage()
112
+ return 2
113
+
114
+
115
+ if __name__ == "__main__":
116
+ raise SystemExit(main())
@@ -0,0 +1,191 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from datetime import date
5
+ from pathlib import Path
6
+ from typing import Dict, List
7
+
8
+
9
+ @dataclass(frozen=True)
10
+ class ScaffoldAction:
11
+ action: str
12
+ path: Path
13
+
14
+
15
+ class ScaffoldConflictError(RuntimeError):
16
+ pass
17
+
18
+
19
+ DIRECTORIES: List[str] = [
20
+ "analysis/scripts",
21
+ "analysis/data/base",
22
+ "analysis/data/covariates",
23
+ "analysis/data/rworg",
24
+ "analysis/data/ocr_tables",
25
+ "analysis/output/figures",
26
+ "analysis/output/tables",
27
+ "analysis/output/results",
28
+ "paper",
29
+ "idea",
30
+ ]
31
+
32
+ TEMPLATE_FILES: List[str] = [
33
+ "README.md.tpl",
34
+ ".gitignore.tpl",
35
+ ".here.tpl",
36
+ "analysis/scripts/00_setup.R.tpl",
37
+ "analysis/scripts/20_data.R.tpl",
38
+ "analysis/scripts/30_results_main.R.tpl",
39
+ "analysis/data/codebook.md.tpl",
40
+ "paper/ref.bib.tpl",
41
+ "paper/aesthetics.typ.tpl",
42
+ "analysis/output/figures/.gitkeep.tpl",
43
+ "analysis/output/tables/.gitkeep.tpl",
44
+ "analysis/output/results/.gitkeep.tpl",
45
+ ".codex/project.md.tpl",
46
+ ".codex/config.toml.tpl",
47
+ ".claude/settings.local.json.tpl",
48
+ ]
49
+
50
+ PAPER_TEMPLATE_MAP = {
51
+ "latex": ("paper/paper_latex.typ.tpl", "paper/paper.typ"),
52
+ "blank": ("paper/paper_blank.typ.tpl", "paper/paper.typ"),
53
+ }
54
+
55
+
56
+ def _render_template(content: str, variables: Dict[str, str]) -> str:
57
+ rendered = content
58
+ for key, value in variables.items():
59
+ rendered = rendered.replace(f"{{{{{key}}}}}", value)
60
+ return rendered
61
+
62
+
63
+ def _is_agent_file(template_path: str) -> bool:
64
+ return template_path.startswith(".codex/") or template_path.startswith(".claude/")
65
+
66
+
67
+ def _output_path_from_template(template_path: str) -> Path:
68
+ if not template_path.endswith(".tpl"):
69
+ raise ValueError(f"Template path must end with .tpl: {template_path}")
70
+ return Path(template_path[:-4])
71
+
72
+
73
+ def _effective_templates(paper_template: str) -> List[str]:
74
+ if paper_template not in PAPER_TEMPLATE_MAP:
75
+ raise ValueError(f"Unsupported paper template: {paper_template}")
76
+ _paper_src, paper_out = PAPER_TEMPLATE_MAP[paper_template]
77
+ return TEMPLATE_FILES + [f"{paper_out}.tpl"]
78
+
79
+
80
+ def plan_actions(
81
+ target_dir: Path,
82
+ templates_dir: Path,
83
+ project_name: str,
84
+ include_agents: bool,
85
+ paper_template: str,
86
+ force: bool,
87
+ ) -> List[ScaffoldAction]:
88
+ variables = {
89
+ "project_name": project_name,
90
+ "today": str(date.today()),
91
+ }
92
+ actions: List[ScaffoldAction] = []
93
+
94
+ for rel_dir in DIRECTORIES:
95
+ out_dir = target_dir / rel_dir
96
+ if out_dir.exists():
97
+ actions.append(ScaffoldAction("skip_dir", out_dir))
98
+ else:
99
+ actions.append(ScaffoldAction("create_dir", out_dir))
100
+
101
+ effective_templates = _effective_templates(paper_template)
102
+ paper_src, paper_out = PAPER_TEMPLATE_MAP[paper_template]
103
+ virtual_paths = {f"{paper_out}.tpl": paper_src}
104
+
105
+ for template_rel in effective_templates:
106
+ source_template_rel = virtual_paths.get(template_rel, template_rel)
107
+ if not include_agents and _is_agent_file(template_rel):
108
+ continue
109
+
110
+ template_file = templates_dir / source_template_rel
111
+ if not template_file.exists():
112
+ raise FileNotFoundError(f"Missing template file: {template_file}")
113
+
114
+ out_file = target_dir / _output_path_from_template(template_rel)
115
+ rendered_content = _render_template(template_file.read_text(encoding="utf-8"), variables)
116
+
117
+ if out_file.exists():
118
+ existing_content = out_file.read_text(encoding="utf-8")
119
+ if existing_content == rendered_content:
120
+ actions.append(ScaffoldAction("skip_file", out_file))
121
+ elif force:
122
+ actions.append(ScaffoldAction("replace_file", out_file))
123
+ else:
124
+ raise ScaffoldConflictError(
125
+ f"Refusing to overwrite existing file: {out_file}. Use --force to replace scaffold files."
126
+ )
127
+ else:
128
+ actions.append(ScaffoldAction("create_file", out_file))
129
+
130
+ return actions
131
+
132
+
133
+ def apply_actions(
134
+ actions: List[ScaffoldAction],
135
+ target_dir: Path,
136
+ templates_dir: Path,
137
+ project_name: str,
138
+ include_agents: bool,
139
+ paper_template: str,
140
+ dry_run: bool,
141
+ ) -> Dict[str, int]:
142
+ variables = {
143
+ "project_name": project_name,
144
+ "today": str(date.today()),
145
+ }
146
+ counts = {
147
+ "created_dirs": 0,
148
+ "created_files": 0,
149
+ "replaced_files": 0,
150
+ "skipped": 0,
151
+ }
152
+
153
+ effective_templates = _effective_templates(paper_template)
154
+ paper_src, paper_out = PAPER_TEMPLATE_MAP[paper_template]
155
+ virtual_paths = {f"{paper_out}.tpl": paper_src}
156
+ template_index = {}
157
+ for template_rel in effective_templates:
158
+ source_template_rel = virtual_paths.get(template_rel, template_rel)
159
+ if include_agents or not _is_agent_file(template_rel):
160
+ template_index[_output_path_from_template(template_rel)] = templates_dir / source_template_rel
161
+
162
+ for item in actions:
163
+ if item.action == "create_dir":
164
+ counts["created_dirs"] += 1
165
+ if not dry_run:
166
+ item.path.mkdir(parents=True, exist_ok=True)
167
+ continue
168
+
169
+ if item.action == "skip_dir" or item.action == "skip_file":
170
+ counts["skipped"] += 1
171
+ continue
172
+
173
+ output_rel = item.path.relative_to(target_dir)
174
+ template_path = template_index.get(output_rel)
175
+ if template_path is None:
176
+ raise RuntimeError(f"No template found for output file {output_rel}")
177
+
178
+ content = _render_template(template_path.read_text(encoding="utf-8"), variables)
179
+
180
+ if item.action == "create_file":
181
+ counts["created_files"] += 1
182
+ elif item.action == "replace_file":
183
+ counts["replaced_files"] += 1
184
+ else:
185
+ raise RuntimeError(f"Unsupported action: {item.action}")
186
+
187
+ if not dry_run:
188
+ item.path.parent.mkdir(parents=True, exist_ok=True)
189
+ item.path.write_text(content, encoding="utf-8")
190
+
191
+ return counts
@@ -0,0 +1,5 @@
1
+ {
2
+ "permissions": {
3
+ "allow": []
4
+ }
5
+ }
@@ -0,0 +1,8 @@
1
+ [mcp_servers.zotero]
2
+ command = "zotero-mcp"
3
+ args = []
4
+ env = { ZOTERO_LOCAL = "true" }
5
+
6
+ [mcp_servers.markitdown]
7
+ command = "uvx"
8
+ args = ["markitdown-mcp"]
@@ -0,0 +1,19 @@
1
+ # Project: {{project_name}}
2
+
3
+ ## Overview
4
+
5
+ Research workspace scaffolded by `blank` on {{today}}.
6
+
7
+ ## Agent Rules
8
+
9
+ - Ask before destructive file operations.
10
+ - Keep `analysis/data/codebook.md` synchronized with data changes.
11
+ - Regenerate outputs when analysis scripts change.
12
+
13
+ ## Directory Structure
14
+
15
+ - `analysis/scripts/`
16
+ - `analysis/data/`
17
+ - `analysis/output/`
18
+ - `paper/`
19
+ - `idea/`
@@ -0,0 +1,11 @@
1
+ .DS_Store
2
+ .venv/
3
+ .tmp/
4
+ __pycache__/
5
+ *.pyc
6
+ analysis/output/figures/*
7
+ analysis/output/tables/*
8
+ analysis/output/results/*
9
+ !analysis/output/figures/.gitkeep
10
+ !analysis/output/tables/.gitkeep
11
+ !analysis/output/results/.gitkeep
@@ -0,0 +1,20 @@
1
+ # {{project_name}}
2
+
3
+ Research project scaffold initialized by `blank` on {{today}}.
4
+
5
+ ## Structure
6
+
7
+ - `analysis/`: data pipelines, scripts, generated outputs
8
+ - `paper/`: manuscript files
9
+ - `idea/`: brainstorming notes
10
+ - `.codex/`: agent workflow config
11
+
12
+ ## Quickstart
13
+
14
+ ```bash
15
+ # initialize from repo root
16
+ blank init .
17
+
18
+ # run the analysis skeleton
19
+ Rscript analysis/scripts/30_results_main.R
20
+ ```
@@ -0,0 +1,9 @@
1
+ # Codebook: {{project_name}}
2
+
3
+ Last scaffold update: {{today}}
4
+
5
+ ## Variables
6
+
7
+ | variable | type | source | description |
8
+ |---|---|---|---|
9
+ | example_var | numeric | placeholder | Replace with actual variable docs |
@@ -0,0 +1,9 @@
1
+ # 00_setup.R
2
+ # Shared setup for analysis scripts.
3
+
4
+ message("[setup] loading configuration")
5
+
6
+ cfg <- list(
7
+ project_name = "{{project_name}}",
8
+ generated_on = "{{today}}"
9
+ )
@@ -0,0 +1,6 @@
1
+ # 20_data.R
2
+ # Data preparation entrypoint.
3
+
4
+ source("analysis/scripts/00_setup.R")
5
+
6
+ message("[data] build data objects here")
@@ -0,0 +1,7 @@
1
+ # 30_results_main.R
2
+ # Main analysis entrypoint.
3
+
4
+ source("analysis/scripts/00_setup.R")
5
+ source("analysis/scripts/20_data.R")
6
+
7
+ message("[results] write analysis outputs into analysis/output/")
@@ -0,0 +1 @@
1
+ // Placeholder style helpers for Typst manuscript customization.
@@ -0,0 +1,12 @@
1
+ #set page(margin: (x: 1in, y: 1in), numbering: "1")
2
+ #set text(font: "Libertinus Serif", size: 11pt)
3
+ #set par(justify: true, leading: 0.7em)
4
+ #set heading(numbering: "1.")
5
+
6
+ #heading(level: 1)[{{project_name}}]
7
+
8
+ #outline(title: [Contents])
9
+
10
+ #heading(level: 1)[Introduction]
11
+
12
+ Write your manuscript here.
@@ -0,0 +1,6 @@
1
+ @article{example2026,
2
+ title={Example Reference},
3
+ author={Doe, Jane},
4
+ journal={Journal of Placeholder Studies},
5
+ year={2026}
6
+ }
@@ -0,0 +1,71 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ from blank_cli.cli import main
6
+
7
+
8
+ def test_init_creates_scaffold(tmp_path: Path) -> None:
9
+ target = tmp_path / "demo"
10
+
11
+ rc = main(["init", str(target), "--project-name", "Demo Project"])
12
+
13
+ assert rc == 0
14
+ assert (target / "analysis/scripts/00_setup.R").exists()
15
+ assert (target / "paper/paper.typ").exists()
16
+ assert (target / ".codex/project.md").exists()
17
+ assert (target / ".claude/settings.local.json").exists()
18
+ assert "Demo Project" in (target / "README.md").read_text(encoding="utf-8")
19
+ assert "Introduction" in (target / "paper/paper.typ").read_text(encoding="utf-8")
20
+
21
+
22
+ def test_init_no_agents(tmp_path: Path) -> None:
23
+ target = tmp_path / "demo"
24
+
25
+ rc = main(["init", str(target), "--no-agents"])
26
+
27
+ assert rc == 0
28
+ assert not (target / ".codex").exists()
29
+ assert not (target / ".claude").exists()
30
+
31
+
32
+ def test_conflict_without_force(tmp_path: Path) -> None:
33
+ target = tmp_path / "demo"
34
+ target.mkdir(parents=True)
35
+ readme = target / "README.md"
36
+ readme.write_text("custom", encoding="utf-8")
37
+
38
+ rc = main(["init", str(target)])
39
+
40
+ assert rc == 3
41
+ assert readme.read_text(encoding="utf-8") == "custom"
42
+
43
+
44
+ def test_force_replaces_conflict(tmp_path: Path) -> None:
45
+ target = tmp_path / "demo"
46
+ target.mkdir(parents=True)
47
+ readme = target / "README.md"
48
+ readme.write_text("custom", encoding="utf-8")
49
+
50
+ rc = main(["init", str(target), "--force", "--project-name", "Forced"])
51
+
52
+ assert rc == 0
53
+ assert "Forced" in readme.read_text(encoding="utf-8")
54
+
55
+
56
+ def test_dry_run_writes_nothing(tmp_path: Path) -> None:
57
+ target = tmp_path / "demo"
58
+
59
+ rc = main(["init", str(target), "--dry-run"])
60
+
61
+ assert rc == 0
62
+ assert not (target / "README.md").exists()
63
+
64
+
65
+ def test_init_blank_paper_template(tmp_path: Path) -> None:
66
+ target = tmp_path / "demo"
67
+
68
+ rc = main(["init", str(target), "--paper-template", "blank", "--project-name", "Bare"])
69
+
70
+ assert rc == 0
71
+ assert (target / "paper/paper.typ").read_text(encoding="utf-8").strip() == "# Bare"