newtex-cli 0.1.1.dev1__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.
- newtex_cli-0.1.1.dev1/.gitignore +34 -0
- newtex_cli-0.1.1.dev1/PKG-INFO +145 -0
- newtex_cli-0.1.1.dev1/README.md +120 -0
- newtex_cli-0.1.1.dev1/pyproject.toml +67 -0
- newtex_cli-0.1.1.dev1/src/newtex/__init__.py +1 -0
- newtex_cli-0.1.1.dev1/src/newtex/cli.py +303 -0
- newtex_cli-0.1.1.dev1/src/newtex/config.py +104 -0
- newtex_cli-0.1.1.dev1/src/newtex/gitignore_utils.py +31 -0
- newtex_cli-0.1.1.dev1/src/newtex/resources/__init__.py +1 -0
- newtex_cli-0.1.1.dev1/src/newtex/resources/gitignore/tex-base.gitignore +47 -0
- newtex_cli-0.1.1.dev1/src/newtex/scaffold.py +37 -0
- newtex_cli-0.1.1.dev1/src/newtex/validators.py +14 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.so
|
|
5
|
+
.pytest_cache/
|
|
6
|
+
.mypy_cache/
|
|
7
|
+
.ruff_cache/
|
|
8
|
+
|
|
9
|
+
# Virtual environments
|
|
10
|
+
.venv/
|
|
11
|
+
venv/
|
|
12
|
+
|
|
13
|
+
# Build artifacts
|
|
14
|
+
build/
|
|
15
|
+
dist/
|
|
16
|
+
*.egg-info/
|
|
17
|
+
|
|
18
|
+
# Local environment files
|
|
19
|
+
.env.*
|
|
20
|
+
!.env
|
|
21
|
+
!.env.example
|
|
22
|
+
|
|
23
|
+
# PyPI credentials (never commit real tokens)
|
|
24
|
+
.pypirc
|
|
25
|
+
.pypirc.*
|
|
26
|
+
!.pypirc.example
|
|
27
|
+
|
|
28
|
+
# OS / editor
|
|
29
|
+
.DS_Store
|
|
30
|
+
.idea/
|
|
31
|
+
.vscode/
|
|
32
|
+
|
|
33
|
+
# Personal
|
|
34
|
+
note.txt
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: newtex-cli
|
|
3
|
+
Version: 0.1.1.dev1
|
|
4
|
+
Summary: Local LaTeX project scaffolder
|
|
5
|
+
Project-URL: Homepage, https://example.internal/newtex
|
|
6
|
+
Project-URL: Repository, https://example.internal/newtex/repo
|
|
7
|
+
License: Proprietary
|
|
8
|
+
Classifier: Environment :: Console
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Topic :: Utilities
|
|
13
|
+
Requires-Python: >=3.11
|
|
14
|
+
Requires-Dist: python-dotenv>=1.0
|
|
15
|
+
Requires-Dist: pyyaml>=6.0
|
|
16
|
+
Requires-Dist: questionary>=2.0
|
|
17
|
+
Requires-Dist: rich>=13.7
|
|
18
|
+
Requires-Dist: typer>=0.12
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
21
|
+
Provides-Extra: publish
|
|
22
|
+
Requires-Dist: build>=1.2; extra == 'publish'
|
|
23
|
+
Requires-Dist: twine>=5.0; extra == 'publish'
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
<h1 align="center">newtex-cli</h1>
|
|
27
|
+
|
|
28
|
+
<p align="center">Scaffold local LaTeX projects from reusable templates.</p>
|
|
29
|
+
|
|
30
|
+
## 1. Installation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pipx install newtex-scaffold
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## 2. Configuration
|
|
37
|
+
|
|
38
|
+
### 2.1 Local environment file
|
|
39
|
+
|
|
40
|
+
Create `.env.local` in the project root:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
NEWTEX_DEFAULT_TEMPLATE=acm
|
|
44
|
+
NEWTEX_TEMPLATE_ACM_PATH=/path/to/your/acm-template
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### 2.2 Global template config (recommended)
|
|
48
|
+
|
|
49
|
+
Configure templates once and use `newtex` from anywhere:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
newtex --template-set acm=/absolute/path/or/url/to/template
|
|
53
|
+
newtex --set-default-template acm
|
|
54
|
+
newtex --templates-list
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
This writes to `~/.config/newtex/templates.yml`.
|
|
58
|
+
|
|
59
|
+
## 3. CLI Commands
|
|
60
|
+
|
|
61
|
+
### 3.1 Command reference
|
|
62
|
+
|
|
63
|
+
| Command | Description |
|
|
64
|
+
| --- | --- |
|
|
65
|
+
| `newtex --help` | Show CLI help |
|
|
66
|
+
| `newtex --tests` | Run the full test suite |
|
|
67
|
+
| `newtex --publish-check` | Validate build/upload prerequisites |
|
|
68
|
+
| `newtex --template-set <alias>=<path-or-url>` | Add or update a global template alias |
|
|
69
|
+
| `newtex --template-set <alias>=<path-or-url> --template-description "..."` | Add alias with description |
|
|
70
|
+
| `newtex --set-default-template <alias>` | Set the global default template alias |
|
|
71
|
+
| `newtex --templates-list` | Show configured global templates |
|
|
72
|
+
| `newtex` | Start interactive project creation |
|
|
73
|
+
| `newtex <project-name> <template>` | Create a project in non-interactive mode |
|
|
74
|
+
| `newtex <project-name> <template> --no-git` | Skip `git init` |
|
|
75
|
+
| `newtex <project-name> <template> --track-pdf` | Keep compiled PDFs tracked |
|
|
76
|
+
| `newtex <project-name> <template> --no-vscode` | Exclude shared `.vscode/` settings |
|
|
77
|
+
| `newtex <project-name> <template> --open` | Open generated project in VS Code |
|
|
78
|
+
|
|
79
|
+
### 3.2 Quick examples
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
newtex --help
|
|
83
|
+
newtex
|
|
84
|
+
newtex exlang-paper acm
|
|
85
|
+
newtex exlang-paper acm --no-git
|
|
86
|
+
newtex exlang-paper acm --track-pdf
|
|
87
|
+
newtex exlang-paper acm --no-vscode
|
|
88
|
+
newtex exlang-paper acm --open
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
<details>
|
|
92
|
+
<summary><strong>Advanced commands</strong></summary>
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
newtex --tests
|
|
96
|
+
newtex --publish-check
|
|
97
|
+
newtex --template-set acm=/absolute/path/to/template
|
|
98
|
+
newtex --set-default-template acm
|
|
99
|
+
newtex --templates-list
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
</details>
|
|
103
|
+
|
|
104
|
+
## 4. Notes
|
|
105
|
+
|
|
106
|
+
- Project names must be lowercase kebab-case (example: `exlang-paper`).
|
|
107
|
+
- If a template path is invalid, the CLI exits with an error message.
|
|
108
|
+
|
|
109
|
+
## 5. Build & Publish
|
|
110
|
+
|
|
111
|
+
### 5.1 Build distributions
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
python -m pip install -e ".[publish]"
|
|
115
|
+
newtex --publish-check
|
|
116
|
+
python -m build
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### 5.2 Upload to PyPI
|
|
120
|
+
|
|
121
|
+
You need a PyPI account to publish public packages.
|
|
122
|
+
|
|
123
|
+
Set token-based auth (recommended):
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
export TWINE_USERNAME=__token__
|
|
127
|
+
export TWINE_PASSWORD=<your-pypi-api-token>
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
python -m twine upload dist/*
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### 5.3 Install on another Mac
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
pipx install newtex-scaffold
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Then configure templates on that machine:
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
newtex --template-set acm=/absolute/path/or/url/to/template
|
|
144
|
+
newtex --set-default-template acm
|
|
145
|
+
```
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
<h1 align="center">newtex-cli</h1>
|
|
2
|
+
|
|
3
|
+
<p align="center">Scaffold local LaTeX projects from reusable templates.</p>
|
|
4
|
+
|
|
5
|
+
## 1. Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pipx install newtex-scaffold
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## 2. Configuration
|
|
12
|
+
|
|
13
|
+
### 2.1 Local environment file
|
|
14
|
+
|
|
15
|
+
Create `.env.local` in the project root:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
NEWTEX_DEFAULT_TEMPLATE=acm
|
|
19
|
+
NEWTEX_TEMPLATE_ACM_PATH=/path/to/your/acm-template
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### 2.2 Global template config (recommended)
|
|
23
|
+
|
|
24
|
+
Configure templates once and use `newtex` from anywhere:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
newtex --template-set acm=/absolute/path/or/url/to/template
|
|
28
|
+
newtex --set-default-template acm
|
|
29
|
+
newtex --templates-list
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
This writes to `~/.config/newtex/templates.yml`.
|
|
33
|
+
|
|
34
|
+
## 3. CLI Commands
|
|
35
|
+
|
|
36
|
+
### 3.1 Command reference
|
|
37
|
+
|
|
38
|
+
| Command | Description |
|
|
39
|
+
| --- | --- |
|
|
40
|
+
| `newtex --help` | Show CLI help |
|
|
41
|
+
| `newtex --tests` | Run the full test suite |
|
|
42
|
+
| `newtex --publish-check` | Validate build/upload prerequisites |
|
|
43
|
+
| `newtex --template-set <alias>=<path-or-url>` | Add or update a global template alias |
|
|
44
|
+
| `newtex --template-set <alias>=<path-or-url> --template-description "..."` | Add alias with description |
|
|
45
|
+
| `newtex --set-default-template <alias>` | Set the global default template alias |
|
|
46
|
+
| `newtex --templates-list` | Show configured global templates |
|
|
47
|
+
| `newtex` | Start interactive project creation |
|
|
48
|
+
| `newtex <project-name> <template>` | Create a project in non-interactive mode |
|
|
49
|
+
| `newtex <project-name> <template> --no-git` | Skip `git init` |
|
|
50
|
+
| `newtex <project-name> <template> --track-pdf` | Keep compiled PDFs tracked |
|
|
51
|
+
| `newtex <project-name> <template> --no-vscode` | Exclude shared `.vscode/` settings |
|
|
52
|
+
| `newtex <project-name> <template> --open` | Open generated project in VS Code |
|
|
53
|
+
|
|
54
|
+
### 3.2 Quick examples
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
newtex --help
|
|
58
|
+
newtex
|
|
59
|
+
newtex exlang-paper acm
|
|
60
|
+
newtex exlang-paper acm --no-git
|
|
61
|
+
newtex exlang-paper acm --track-pdf
|
|
62
|
+
newtex exlang-paper acm --no-vscode
|
|
63
|
+
newtex exlang-paper acm --open
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
<details>
|
|
67
|
+
<summary><strong>Advanced commands</strong></summary>
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
newtex --tests
|
|
71
|
+
newtex --publish-check
|
|
72
|
+
newtex --template-set acm=/absolute/path/to/template
|
|
73
|
+
newtex --set-default-template acm
|
|
74
|
+
newtex --templates-list
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
</details>
|
|
78
|
+
|
|
79
|
+
## 4. Notes
|
|
80
|
+
|
|
81
|
+
- Project names must be lowercase kebab-case (example: `exlang-paper`).
|
|
82
|
+
- If a template path is invalid, the CLI exits with an error message.
|
|
83
|
+
|
|
84
|
+
## 5. Build & Publish
|
|
85
|
+
|
|
86
|
+
### 5.1 Build distributions
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
python -m pip install -e ".[publish]"
|
|
90
|
+
newtex --publish-check
|
|
91
|
+
python -m build
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### 5.2 Upload to PyPI
|
|
95
|
+
|
|
96
|
+
You need a PyPI account to publish public packages.
|
|
97
|
+
|
|
98
|
+
Set token-based auth (recommended):
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
export TWINE_USERNAME=__token__
|
|
102
|
+
export TWINE_PASSWORD=<your-pypi-api-token>
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
python -m twine upload dist/*
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### 5.3 Install on another Mac
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
pipx install newtex-scaffold
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Then configure templates on that machine:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
newtex --template-set acm=/absolute/path/or/url/to/template
|
|
119
|
+
newtex --set-default-template acm
|
|
120
|
+
```
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27", "hatch-vcs>=0.4"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "newtex-cli"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Local LaTeX project scaffolder"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = {text = "Proprietary"}
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Programming Language :: Python :: 3",
|
|
14
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
15
|
+
"Programming Language :: Python :: 3.11",
|
|
16
|
+
"Environment :: Console",
|
|
17
|
+
"Topic :: Utilities",
|
|
18
|
+
]
|
|
19
|
+
dependencies = [
|
|
20
|
+
"typer>=0.12",
|
|
21
|
+
"questionary>=2.0",
|
|
22
|
+
"pyyaml>=6.0",
|
|
23
|
+
"python-dotenv>=1.0",
|
|
24
|
+
"rich>=13.7",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.scripts]
|
|
28
|
+
newtex = "newtex.cli:app"
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://example.internal/newtex"
|
|
32
|
+
Repository = "https://example.internal/newtex/repo"
|
|
33
|
+
|
|
34
|
+
[tool.hatch.version]
|
|
35
|
+
source = "vcs"
|
|
36
|
+
|
|
37
|
+
[tool.hatch.version.raw-options]
|
|
38
|
+
version_scheme = "python-simplified-semver"
|
|
39
|
+
local_scheme = "no-local-version"
|
|
40
|
+
fallback_version = "0.1.0"
|
|
41
|
+
|
|
42
|
+
[tool.hatch.build.targets.wheel]
|
|
43
|
+
packages = ["src/newtex"]
|
|
44
|
+
|
|
45
|
+
[tool.hatch.build.targets.sdist]
|
|
46
|
+
include = [
|
|
47
|
+
"/src/newtex",
|
|
48
|
+
"/pyproject.toml",
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
[tool.hatch.build]
|
|
52
|
+
include = [
|
|
53
|
+
"src/newtex/resources/gitignore/*.gitignore",
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
[project.optional-dependencies]
|
|
57
|
+
dev = [
|
|
58
|
+
"pytest>=8.0",
|
|
59
|
+
]
|
|
60
|
+
publish = [
|
|
61
|
+
"build>=1.2",
|
|
62
|
+
"twine>=5.0",
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
[tool.pytest.ini_options]
|
|
66
|
+
pythonpath = ["src"]
|
|
67
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__all__ = []
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import shutil
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import questionary
|
|
8
|
+
import typer
|
|
9
|
+
from questionary import Style
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
from rich.panel import Panel
|
|
12
|
+
from rich.text import Text
|
|
13
|
+
|
|
14
|
+
from .config import load_config, set_default_template, upsert_template
|
|
15
|
+
from .scaffold import scaffold_project
|
|
16
|
+
from .validators import is_kebab_case, suggest_kebab_case
|
|
17
|
+
|
|
18
|
+
app = typer.Typer(help="Create local LaTeX projects from templates.")
|
|
19
|
+
console = Console()
|
|
20
|
+
PROMPT_STYLE = Style(
|
|
21
|
+
[
|
|
22
|
+
("qmark", "fg:#8b5cf6 bold"),
|
|
23
|
+
("question", "bold"),
|
|
24
|
+
("answer", "fg:#22c55e bold"),
|
|
25
|
+
("pointer", "fg:#06b6d4 bold"),
|
|
26
|
+
("highlighted", "fg:#06b6d4 bold"),
|
|
27
|
+
("selected", "fg:#22c55e"),
|
|
28
|
+
]
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _show_banner() -> None:
|
|
33
|
+
title = Text("newtex", style="bold magenta")
|
|
34
|
+
subtitle = Text("Create local LaTeX projects from templates", style="cyan")
|
|
35
|
+
console.print(Panel.fit(Text.assemble(title, "\n", subtitle), border_style="magenta"))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _info(message: str) -> None:
|
|
39
|
+
console.print(f"[cyan]•[/cyan] {message}")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _success(message: str) -> None:
|
|
43
|
+
console.print(f"[green]✔[/green] {message}")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _error(message: str) -> None:
|
|
47
|
+
console.print(f"[bold red]✖ {message}[/bold red]")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _run_tests() -> int:
|
|
51
|
+
local_venv_python = Path(".venv/bin/python")
|
|
52
|
+
command = [str(local_venv_python), "-m", "pytest", "-q"] if local_venv_python.exists() else ["pytest", "-q"]
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
result = subprocess.run(command, check=False)
|
|
56
|
+
except FileNotFoundError:
|
|
57
|
+
result = subprocess.run([sys.executable, "-m", "pytest", "-q"], check=False)
|
|
58
|
+
|
|
59
|
+
return result.returncode
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _run_publish_check() -> int:
|
|
63
|
+
checks_ok = True
|
|
64
|
+
|
|
65
|
+
python_candidates = []
|
|
66
|
+
local_venv_python = Path(".venv/bin/python")
|
|
67
|
+
if local_venv_python.exists():
|
|
68
|
+
python_candidates.append(str(local_venv_python))
|
|
69
|
+
python_candidates.append(sys.executable)
|
|
70
|
+
|
|
71
|
+
if shutil.which("pipx"):
|
|
72
|
+
_success("pipx is available")
|
|
73
|
+
else:
|
|
74
|
+
_error("pipx is not available (recommended for global install)")
|
|
75
|
+
checks_ok = False
|
|
76
|
+
|
|
77
|
+
build_available = False
|
|
78
|
+
for python_exec in python_candidates:
|
|
79
|
+
try:
|
|
80
|
+
build_check = subprocess.run([python_exec, "-m", "build", "--version"], check=False)
|
|
81
|
+
if build_check.returncode == 0:
|
|
82
|
+
build_available = True
|
|
83
|
+
break
|
|
84
|
+
except Exception:
|
|
85
|
+
continue
|
|
86
|
+
|
|
87
|
+
if build_available:
|
|
88
|
+
_success("python -m build is available")
|
|
89
|
+
else:
|
|
90
|
+
_error("python -m build is not available (install with: python -m pip install -e '.[publish]')")
|
|
91
|
+
checks_ok = False
|
|
92
|
+
|
|
93
|
+
twine_available = False
|
|
94
|
+
for python_exec in python_candidates:
|
|
95
|
+
try:
|
|
96
|
+
twine_check = subprocess.run([python_exec, "-m", "twine", "--version"], check=False)
|
|
97
|
+
if twine_check.returncode == 0:
|
|
98
|
+
twine_available = True
|
|
99
|
+
break
|
|
100
|
+
except Exception:
|
|
101
|
+
continue
|
|
102
|
+
|
|
103
|
+
if twine_available:
|
|
104
|
+
_success("twine is available")
|
|
105
|
+
else:
|
|
106
|
+
_error("twine is not available (install with: python -m pip install -e '.[publish]')")
|
|
107
|
+
checks_ok = False
|
|
108
|
+
|
|
109
|
+
token_present = bool(os.getenv("TWINE_PASSWORD"))
|
|
110
|
+
username = os.getenv("TWINE_USERNAME", "")
|
|
111
|
+
trusted_publishing = bool(os.getenv("PYPI_API_TOKEN"))
|
|
112
|
+
|
|
113
|
+
if token_present and username == "__token__":
|
|
114
|
+
_success("Twine token credentials detected (TWINE_USERNAME/TWINE_PASSWORD)")
|
|
115
|
+
elif trusted_publishing:
|
|
116
|
+
_info("PYPI_API_TOKEN found (if your flow uses this env var)")
|
|
117
|
+
else:
|
|
118
|
+
_info("No publish token detected. Set TWINE_USERNAME=__token__ and TWINE_PASSWORD=<pypi-token> before upload")
|
|
119
|
+
|
|
120
|
+
if checks_ok:
|
|
121
|
+
_success("Publish check passed")
|
|
122
|
+
return 0
|
|
123
|
+
|
|
124
|
+
_error("Publish check failed")
|
|
125
|
+
return 1
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _parse_template_set(value: str) -> tuple[str, str]:
|
|
129
|
+
if "=" not in value:
|
|
130
|
+
raise typer.BadParameter("--template-set must be in the form <alias>=<path-or-url>")
|
|
131
|
+
|
|
132
|
+
alias, template_value = value.split("=", 1)
|
|
133
|
+
alias = alias.strip()
|
|
134
|
+
template_value = template_value.strip()
|
|
135
|
+
|
|
136
|
+
if not alias:
|
|
137
|
+
raise typer.BadParameter("Template alias cannot be empty")
|
|
138
|
+
if not template_value:
|
|
139
|
+
raise typer.BadParameter("Template path/url cannot be empty")
|
|
140
|
+
|
|
141
|
+
return alias, template_value
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _pick_template(cfg: dict, template_arg: str | None) -> str:
|
|
145
|
+
templates = cfg["templates"]
|
|
146
|
+
|
|
147
|
+
if template_arg:
|
|
148
|
+
if template_arg not in templates:
|
|
149
|
+
raise typer.BadParameter(f"Unknown template '{template_arg}'. Available: {', '.join(templates.keys())}")
|
|
150
|
+
return template_arg
|
|
151
|
+
|
|
152
|
+
choices = [
|
|
153
|
+
questionary.Choice(title=f"{alias} ({meta.get('description', '')})", value=alias)
|
|
154
|
+
for alias, meta in templates.items()
|
|
155
|
+
]
|
|
156
|
+
|
|
157
|
+
selected = questionary.select(
|
|
158
|
+
"Which template should I use?",
|
|
159
|
+
choices=choices,
|
|
160
|
+
default=cfg.get("default_template", "acm"),
|
|
161
|
+
style=PROMPT_STYLE,
|
|
162
|
+
).ask()
|
|
163
|
+
|
|
164
|
+
if not selected:
|
|
165
|
+
raise typer.Exit(code=1)
|
|
166
|
+
|
|
167
|
+
return selected
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _get_project_name(name_arg: str | None) -> str:
|
|
171
|
+
if name_arg:
|
|
172
|
+
if not is_kebab_case(name_arg):
|
|
173
|
+
raise typer.BadParameter("Project name must be lowercase kebab-case (e.g. exlang-paper)")
|
|
174
|
+
return name_arg
|
|
175
|
+
|
|
176
|
+
while True:
|
|
177
|
+
raw = questionary.text("What is your project name?", style=PROMPT_STYLE).ask()
|
|
178
|
+
if not raw:
|
|
179
|
+
raise typer.Exit(code=1)
|
|
180
|
+
|
|
181
|
+
if is_kebab_case(raw):
|
|
182
|
+
return raw
|
|
183
|
+
|
|
184
|
+
suggested = suggest_kebab_case(raw)
|
|
185
|
+
use_suggested = questionary.confirm(f'Use "{suggested}" instead?', style=PROMPT_STYLE).ask()
|
|
186
|
+
|
|
187
|
+
if use_suggested:
|
|
188
|
+
return suggested
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@app.command()
|
|
192
|
+
def main(
|
|
193
|
+
project_name: str | None = typer.Argument(None, help="Project folder name (kebab-case)"),
|
|
194
|
+
template: str | None = typer.Argument(None, help="Template alias, e.g. acm"),
|
|
195
|
+
tests: bool = typer.Option(False, "--tests", help="Run full test suite and exit"),
|
|
196
|
+
publish_check: bool = typer.Option(False, "--publish-check", help="Validate publishing prerequisites and exit"),
|
|
197
|
+
templates_list: bool = typer.Option(False, "--templates-list", help="List configured templates and exit"),
|
|
198
|
+
template_set: str | None = typer.Option(None, "--template-set", help="Set template alias/path using alias=path-or-url"),
|
|
199
|
+
set_default: str | None = typer.Option(None, "--set-default-template", help="Set default template alias and exit"),
|
|
200
|
+
template_desc: str | None = typer.Option(None, "--template-description", help="Description used with --template-set"),
|
|
201
|
+
no_git: bool = typer.Option(False, "--no-git", help="Do not run git init"),
|
|
202
|
+
track_pdf: bool = typer.Option(False, "--track-pdf", help="Track compiled PDFs in git"),
|
|
203
|
+
no_vscode: bool = typer.Option(False, "--no-vscode", help="Do not keep shared .vscode settings"),
|
|
204
|
+
open_code: bool = typer.Option(False, "--open", help="Open in VS Code after creation"),
|
|
205
|
+
) -> None:
|
|
206
|
+
_show_banner()
|
|
207
|
+
|
|
208
|
+
if tests:
|
|
209
|
+
if project_name or template:
|
|
210
|
+
raise typer.BadParameter("Do not pass PROJECT_NAME or TEMPLATE with --tests")
|
|
211
|
+
|
|
212
|
+
_info("Running full test suite")
|
|
213
|
+
test_exit_code = _run_tests()
|
|
214
|
+
if test_exit_code == 0:
|
|
215
|
+
_success("All tests passed")
|
|
216
|
+
return
|
|
217
|
+
|
|
218
|
+
_error("Test run failed")
|
|
219
|
+
raise typer.Exit(code=test_exit_code)
|
|
220
|
+
|
|
221
|
+
if publish_check:
|
|
222
|
+
if project_name or template:
|
|
223
|
+
raise typer.BadParameter("Do not pass PROJECT_NAME or TEMPLATE with --publish-check")
|
|
224
|
+
|
|
225
|
+
_info("Running publish readiness checks")
|
|
226
|
+
publish_exit_code = _run_publish_check()
|
|
227
|
+
if publish_exit_code != 0:
|
|
228
|
+
raise typer.Exit(code=publish_exit_code)
|
|
229
|
+
return
|
|
230
|
+
|
|
231
|
+
if template_set:
|
|
232
|
+
if project_name or template:
|
|
233
|
+
raise typer.BadParameter("Do not pass PROJECT_NAME or TEMPLATE with --template-set")
|
|
234
|
+
|
|
235
|
+
alias, template_value = _parse_template_set(template_set)
|
|
236
|
+
upsert_template(alias=alias, template_path=template_value, description=template_desc)
|
|
237
|
+
_success(f"Saved template alias '{alias}'")
|
|
238
|
+
return
|
|
239
|
+
|
|
240
|
+
if set_default:
|
|
241
|
+
if project_name or template:
|
|
242
|
+
raise typer.BadParameter("Do not pass PROJECT_NAME or TEMPLATE with --set-default-template")
|
|
243
|
+
|
|
244
|
+
try:
|
|
245
|
+
set_default_template(set_default)
|
|
246
|
+
except KeyError as error:
|
|
247
|
+
_error(str(error))
|
|
248
|
+
raise typer.Exit(code=1)
|
|
249
|
+
|
|
250
|
+
_success(f"Default template set to '{set_default}'")
|
|
251
|
+
return
|
|
252
|
+
|
|
253
|
+
if templates_list:
|
|
254
|
+
if project_name or template:
|
|
255
|
+
raise typer.BadParameter("Do not pass PROJECT_NAME or TEMPLATE with --templates-list")
|
|
256
|
+
|
|
257
|
+
cfg = load_config()
|
|
258
|
+
templates = cfg.get("templates", {})
|
|
259
|
+
default_alias = cfg.get("default_template")
|
|
260
|
+
|
|
261
|
+
if not templates:
|
|
262
|
+
_info("No templates configured")
|
|
263
|
+
return
|
|
264
|
+
|
|
265
|
+
_info("Configured templates")
|
|
266
|
+
for alias, meta in templates.items():
|
|
267
|
+
marker = " (default)" if alias == default_alias else ""
|
|
268
|
+
path_value = meta.get("path", "")
|
|
269
|
+
description = meta.get("description", "")
|
|
270
|
+
_info(f"{alias}{marker} -> {path_value} ({description})")
|
|
271
|
+
return
|
|
272
|
+
|
|
273
|
+
cfg = load_config()
|
|
274
|
+
|
|
275
|
+
name = _get_project_name(project_name)
|
|
276
|
+
template_alias = _pick_template(cfg, template)
|
|
277
|
+
template_path = cfg["templates"][template_alias]["path"]
|
|
278
|
+
_info(f"Template: [bold]{template_alias}[/bold]")
|
|
279
|
+
_info(f"Target project: [bold]{name}[/bold]")
|
|
280
|
+
|
|
281
|
+
if not Path(template_path).exists():
|
|
282
|
+
_error(f"Template path not found: {template_path}")
|
|
283
|
+
raise typer.Exit(code=1)
|
|
284
|
+
|
|
285
|
+
try:
|
|
286
|
+
with console.status("[bold cyan]Scaffolding project...[/bold cyan]"):
|
|
287
|
+
scaffold_project(
|
|
288
|
+
template_path=template_path,
|
|
289
|
+
project_name=name,
|
|
290
|
+
init_git=not no_git,
|
|
291
|
+
track_pdf=track_pdf,
|
|
292
|
+
share_vscode=not no_vscode,
|
|
293
|
+
open_code=open_code,
|
|
294
|
+
)
|
|
295
|
+
except Exception as error:
|
|
296
|
+
_error(f"Error: {error}")
|
|
297
|
+
raise typer.Exit(code=1)
|
|
298
|
+
|
|
299
|
+
_success(f"Done. Created ./{name} using template '{template_alias}'")
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
if __name__ == "__main__":
|
|
303
|
+
app()
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import yaml
|
|
5
|
+
from dotenv import load_dotenv
|
|
6
|
+
|
|
7
|
+
CONFIG_DIR = Path.home() / ".config" / "newtex"
|
|
8
|
+
CONFIG_FILE = CONFIG_DIR / "templates.yml"
|
|
9
|
+
ACM_TEMPLATE_ENV = "NEWTEX_TEMPLATE_ACM_PATH"
|
|
10
|
+
DEFAULT_TEMPLATE_ENV = "NEWTEX_DEFAULT_TEMPLATE"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _load_environment() -> None:
|
|
14
|
+
load_dotenv(override=False)
|
|
15
|
+
load_dotenv(Path.cwd() / ".env.local", override=True)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _default_config() -> dict:
|
|
19
|
+
return {
|
|
20
|
+
"default_template": os.getenv(DEFAULT_TEMPLATE_ENV, "acm"),
|
|
21
|
+
"templates": {
|
|
22
|
+
"acm": {
|
|
23
|
+
"path": os.getenv(ACM_TEMPLATE_ENV, ""),
|
|
24
|
+
"description": "ACM Conference Proceedings Primary Article",
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _apply_env_overrides(config: dict) -> dict:
|
|
31
|
+
default_template = os.getenv(DEFAULT_TEMPLATE_ENV)
|
|
32
|
+
acm_path = os.getenv(ACM_TEMPLATE_ENV)
|
|
33
|
+
|
|
34
|
+
if default_template:
|
|
35
|
+
config["default_template"] = default_template
|
|
36
|
+
|
|
37
|
+
templates = config.setdefault("templates", {})
|
|
38
|
+
acm_template = templates.setdefault(
|
|
39
|
+
"acm",
|
|
40
|
+
{
|
|
41
|
+
"path": "",
|
|
42
|
+
"description": "ACM Conference Proceedings Primary Article",
|
|
43
|
+
},
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
if acm_path:
|
|
47
|
+
acm_template["path"] = acm_path
|
|
48
|
+
|
|
49
|
+
return config
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def ensure_config() -> None:
|
|
53
|
+
_load_environment()
|
|
54
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
55
|
+
if not CONFIG_FILE.exists():
|
|
56
|
+
with open(CONFIG_FILE, "w", encoding="utf-8") as file:
|
|
57
|
+
yaml.safe_dump(_default_config(), file, sort_keys=False)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def load_persisted_config() -> dict:
|
|
61
|
+
ensure_config()
|
|
62
|
+
with open(CONFIG_FILE, "r", encoding="utf-8") as file:
|
|
63
|
+
return yaml.safe_load(file) or {}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def save_config(config: dict) -> None:
|
|
67
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
68
|
+
with open(CONFIG_FILE, "w", encoding="utf-8") as file:
|
|
69
|
+
yaml.safe_dump(config, file, sort_keys=False)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def upsert_template(alias: str, template_path: str, description: str | None = None) -> dict:
|
|
73
|
+
config = load_persisted_config()
|
|
74
|
+
templates = config.setdefault("templates", {})
|
|
75
|
+
existing = templates.get(alias, {})
|
|
76
|
+
|
|
77
|
+
templates[alias] = {
|
|
78
|
+
"path": template_path,
|
|
79
|
+
"description": description if description is not None else existing.get("description", ""),
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if "default_template" not in config:
|
|
83
|
+
config["default_template"] = alias
|
|
84
|
+
|
|
85
|
+
save_config(config)
|
|
86
|
+
return config
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def set_default_template(alias: str) -> dict:
|
|
90
|
+
config = load_persisted_config()
|
|
91
|
+
templates = config.get("templates", {})
|
|
92
|
+
if alias not in templates:
|
|
93
|
+
raise KeyError(f"Unknown template alias: {alias}")
|
|
94
|
+
|
|
95
|
+
config["default_template"] = alias
|
|
96
|
+
save_config(config)
|
|
97
|
+
return config
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def load_config() -> dict:
|
|
101
|
+
_load_environment()
|
|
102
|
+
config = load_persisted_config()
|
|
103
|
+
|
|
104
|
+
return _apply_env_overrides(config)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
import importlib.resources as pkg_resources
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def _read_base_gitignore() -> str:
|
|
6
|
+
from newtex import resources
|
|
7
|
+
|
|
8
|
+
return pkg_resources.files(resources).joinpath("gitignore/tex-base.gitignore").read_text(encoding="utf-8")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def apply_gitignore(project_dir: Path, track_pdf: bool, share_vscode: bool) -> None:
|
|
12
|
+
gitignore_path = project_dir / ".gitignore"
|
|
13
|
+
|
|
14
|
+
base_content = _read_base_gitignore().strip() + "\n"
|
|
15
|
+
|
|
16
|
+
extras = []
|
|
17
|
+
if not track_pdf:
|
|
18
|
+
extras.append("*.pdf")
|
|
19
|
+
if not share_vscode:
|
|
20
|
+
extras.append(".vscode/")
|
|
21
|
+
|
|
22
|
+
extra_content = ""
|
|
23
|
+
if extras:
|
|
24
|
+
extra_content = "\n# newtex preferences\n" + "\n".join(extras) + "\n"
|
|
25
|
+
|
|
26
|
+
if gitignore_path.exists():
|
|
27
|
+
existing = gitignore_path.read_text(encoding="utf-8")
|
|
28
|
+
merged = existing.rstrip() + "\n\n# --- newtex additions ---\n" + base_content + extra_content
|
|
29
|
+
gitignore_path.write_text(merged, encoding="utf-8")
|
|
30
|
+
else:
|
|
31
|
+
gitignore_path.write_text(base_content + extra_content, encoding="utf-8")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__all__ = []
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# macOS
|
|
2
|
+
.DS_Store
|
|
3
|
+
|
|
4
|
+
# LaTeX intermediate files
|
|
5
|
+
*.aux
|
|
6
|
+
*.bbl
|
|
7
|
+
*.bcf
|
|
8
|
+
*.blg
|
|
9
|
+
*.fdb_latexmk
|
|
10
|
+
*.fls
|
|
11
|
+
*.lof
|
|
12
|
+
*.log
|
|
13
|
+
*.lot
|
|
14
|
+
*.out
|
|
15
|
+
*.run.xml
|
|
16
|
+
*.synctex.gz
|
|
17
|
+
*.toc
|
|
18
|
+
*.xdv
|
|
19
|
+
|
|
20
|
+
# Bibliography / indexing extras
|
|
21
|
+
*.acn
|
|
22
|
+
*.acr
|
|
23
|
+
*.alg
|
|
24
|
+
*.glg
|
|
25
|
+
*.glo
|
|
26
|
+
*.gls
|
|
27
|
+
*.idx
|
|
28
|
+
*.ilg
|
|
29
|
+
*.ind
|
|
30
|
+
*.ist
|
|
31
|
+
|
|
32
|
+
# Latexmk / temp
|
|
33
|
+
_latexmk*
|
|
34
|
+
latexmk*.log
|
|
35
|
+
|
|
36
|
+
# Minted / pygments
|
|
37
|
+
_minted*
|
|
38
|
+
*.pyg
|
|
39
|
+
|
|
40
|
+
# Build dirs (common)
|
|
41
|
+
build/
|
|
42
|
+
out/
|
|
43
|
+
|
|
44
|
+
# Editor extras
|
|
45
|
+
.idea/
|
|
46
|
+
__pycache__/
|
|
47
|
+
.venv/
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
import subprocess
|
|
3
|
+
|
|
4
|
+
from .gitignore_utils import apply_gitignore
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def run_cmd(cmd: list[str], cwd: Path | None = None) -> None:
|
|
8
|
+
result = subprocess.run(cmd, cwd=str(cwd) if cwd else None, check=False)
|
|
9
|
+
if result.returncode != 0:
|
|
10
|
+
raise RuntimeError(f"Command failed: {' '.join(cmd)}")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def scaffold_project(
|
|
14
|
+
template_path: str,
|
|
15
|
+
project_name: str,
|
|
16
|
+
init_git: bool = True,
|
|
17
|
+
track_pdf: bool = False,
|
|
18
|
+
share_vscode: bool = True,
|
|
19
|
+
open_code: bool = False,
|
|
20
|
+
) -> None:
|
|
21
|
+
project_dir = Path.cwd() / project_name
|
|
22
|
+
|
|
23
|
+
if project_dir.exists():
|
|
24
|
+
raise FileExistsError(f"Target folder already exists: {project_dir}")
|
|
25
|
+
|
|
26
|
+
run_cmd(["copier", "copy", template_path, project_name])
|
|
27
|
+
|
|
28
|
+
apply_gitignore(project_dir, track_pdf=track_pdf, share_vscode=share_vscode)
|
|
29
|
+
|
|
30
|
+
if init_git:
|
|
31
|
+
run_cmd(["git", "init"], cwd=project_dir)
|
|
32
|
+
|
|
33
|
+
if open_code:
|
|
34
|
+
try:
|
|
35
|
+
run_cmd(["code", "."], cwd=project_dir)
|
|
36
|
+
except (FileNotFoundError, RuntimeError):
|
|
37
|
+
pass
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
KEBAB_CASE_PATTERN = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def is_kebab_case(name: str) -> bool:
|
|
7
|
+
return bool(KEBAB_CASE_PATTERN.fullmatch(name))
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def suggest_kebab_case(name: str) -> str:
|
|
11
|
+
s = name.strip().lower()
|
|
12
|
+
s = re.sub(r"[^a-z0-9]+", "-", s)
|
|
13
|
+
s = re.sub(r"-+", "-", s).strip("-")
|
|
14
|
+
return s
|