sparkstart 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.
@@ -0,0 +1,10 @@
1
+ Metadata-Version: 2.1
2
+ Name: sparkstart
3
+ Version: 0.1.0
4
+ Summary: Spin-up a ready-to-code project with one command
5
+ Author-Email: Jordan Longval <majorlongval@gmail.com>
6
+ Requires-Python: >=3.8
7
+ Requires-Dist: typer
8
+ Requires-Dist: requests
9
+ Requires-Dist: python-dotenv
10
+
@@ -0,0 +1,24 @@
1
+ [project]
2
+ name = "sparkstart"
3
+ version = "0.1.0"
4
+ description = "Spin-up a ready-to-code project with one command"
5
+ authors = [
6
+ { name = "Jordan Longval", email = "majorlongval@gmail.com" },
7
+ ]
8
+ requires-python = ">=3.8"
9
+ dependencies = [
10
+ "typer",
11
+ "requests",
12
+ "python-dotenv",
13
+ ]
14
+
15
+ [project.scripts]
16
+ sparkstart = "sparkstart.cli:app"
17
+
18
+ [build-system]
19
+ requires = [
20
+ "pdm-backend",
21
+ ]
22
+ build-backend = "pdm.backend"
23
+
24
+ [tool]
@@ -0,0 +1,2 @@
1
+ "Projinit package init"
2
+ __all__ = ["create_project", "cli"]
@@ -0,0 +1,61 @@
1
+ import pathlib
2
+ import typer
3
+ import shutil
4
+ from projinit.core import create_project, delete_project
5
+
6
+
7
+ app = typer.Typer(
8
+ help="projinit – create a new project repository quickly",
9
+ invoke_without_command=True, # allows root alias
10
+ no_args_is_help=True,
11
+ )
12
+
13
+
14
+ # --- root alias ----------------------------------------------------
15
+ @app.callback(invoke_without_command=True)
16
+ def main(
17
+ ctx: typer.Context,
18
+ # name argument removed to avoid "stealing" the subcommand
19
+ ):
20
+ """
21
+ projinit – Start your new project in seconds.
22
+
23
+ Usage:
24
+ projinit new <name>
25
+ projinit delete <name>
26
+ """
27
+ if ctx.invoked_subcommand is None:
28
+ # If no subcommand is provided, show the help message
29
+ print(ctx.get_help())
30
+ raise typer.Exit()
31
+
32
+
33
+ # --- explicit sub‑command -----------------------------------------
34
+ @app.command()
35
+ def new(
36
+ name: str,
37
+ github: bool = typer.Option(False, "--github", help="Push to GitHub"),
38
+ lang: str = typer.Option("python", "--lang", "-l", help="Language: python, rust, javascript"),
39
+ ):
40
+ """Create a new project folder NAME (optionally push to GitHub)."""
41
+ create_project(pathlib.Path.cwd() / name, github, lang)
42
+
43
+
44
+
45
+ @app.command()
46
+ def delete(
47
+ name: str,
48
+ github: bool = typer.Option(False, "--github"),
49
+ force: bool = typer.Option(False, "--yes", "-y"),
50
+ ):
51
+ target = pathlib.Path.cwd() / name
52
+ if not force:
53
+ typer.confirm(f"Delete {'and remote ' if github else ''}{target} ?", abort=True)
54
+
55
+ try:
56
+ from projinit.core import delete_project
57
+
58
+ delete_project(target, github)
59
+ typer.secho("Project deleted !", fg=typer.colors.GREEN)
60
+ except Exception as e:
61
+ typer.secho(f"Failed : {e}", fg=typer.colors.RED)
@@ -0,0 +1,301 @@
1
+ """
2
+ core.py – all the heavy lifting for projinit
3
+ • folder scaffold (src/, README, .gitignore, etc.)
4
+ • local virtual-env (.venv) for Python
5
+ • git repo + first commit
6
+ • optional --github push (per-project token in .projinit.env or $GITHUB_TOKEN)
7
+
8
+ Requires: requests, python-dotenv
9
+ pip install requests python-dotenv
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import os
15
+ import pathlib
16
+ import shutil
17
+ import subprocess
18
+ import textwrap
19
+ import venv
20
+ import webbrowser
21
+ from typing import List
22
+
23
+ import requests
24
+ from dotenv import dotenv_values
25
+
26
+ # ----------------------------------------------------------------------------- #
27
+ # Constants #
28
+ # ----------------------------------------------------------------------------- #
29
+
30
+ README_TEXT = "# {name}\n\nProject initialized by `projinit`."
31
+
32
+ # Language-specific gitignore patterns
33
+ GITIGNORE_PYTHON = textwrap.dedent("""
34
+ __pycache__/
35
+ .venv/
36
+ *.pyc
37
+ .DS_Store
38
+ .projinit.env
39
+ """).strip()
40
+
41
+ GITIGNORE_RUST = textwrap.dedent("""
42
+ /target
43
+ .DS_Store
44
+ .projinit.env
45
+ """).strip()
46
+
47
+ GITIGNORE_JAVASCRIPT = textwrap.dedent("""
48
+ node_modules/
49
+ .DS_Store
50
+ .projinit.env
51
+ """).strip()
52
+
53
+ NEW_TOKEN_URL = (
54
+ "https://github.com/settings/tokens/new?description=projinit:{name}&scopes=repo,delete_repo,user"
55
+ )
56
+
57
+
58
+ # ----------------------------------------------------------------------------- #
59
+ # Helper functions #
60
+ # ----------------------------------------------------------------------------- #
61
+
62
+
63
+ def _sh(cmd: List[str], cwd: pathlib.Path) -> None:
64
+ """Run *cmd* in *cwd*; raise RuntimeError on non-zero exit."""
65
+ result = subprocess.run(cmd, cwd=cwd, capture_output=True, text=True)
66
+ if result.returncode != 0:
67
+ raise RuntimeError(
68
+ f"$ {' '.join(cmd)}\n{result.stderr.strip() or 'command failed'}"
69
+ )
70
+
71
+
72
+ # ---------- per-project token helpers ---------------------------------------- #
73
+
74
+
75
+ def _project_token(project_root: pathlib.Path) -> str | None:
76
+ """Return token from .projinit.env or '' if file missing/empty."""
77
+ return dotenv_values(project_root / ".projinit.env").get("GITHUB_TOKEN", "")
78
+
79
+
80
+ def _save_project_token(project_root: pathlib.Path, token: str | None) -> None:
81
+ """Persist token to .projinit.env and ensure it's git-ignored."""
82
+ (project_root / ".projinit.env").write_text(f"GITHUB_TOKEN={token}\n")
83
+
84
+ gi = project_root / ".gitignore"
85
+ lines: list[str] = gi.read_text().splitlines() if gi.exists() else []
86
+ if ".projinit.env" not in lines:
87
+ lines.append(".projinit.env")
88
+ gi.write_text("\n".join(lines) + "\n")
89
+
90
+
91
+ # ---------- GitHub API ------------------------------------------------------- #
92
+
93
+
94
+ def _github_user(token: str) -> str:
95
+ """Get the authenticated GitHub username."""
96
+ r = requests.get(
97
+ "https://api.github.com/user",
98
+ headers={
99
+ "Authorization": f"token {token}",
100
+ "Accept": "application/vnd.github+json",
101
+ },
102
+ timeout=10,
103
+ )
104
+ if r.status_code >= 300:
105
+ raise RuntimeError(f"GitHub API error {r.status_code}: {r.text.strip()}")
106
+ return r.json()["login"]
107
+
108
+
109
+ def _create_github_repo(repo_name: str, token: str | None = None) -> str:
110
+ """
111
+ Create repo under authenticated user; return clone URL.
112
+ *token* optional – falls back to $GITHUB_TOKEN.
113
+ """
114
+ token = token or os.getenv("GITHUB_TOKEN")
115
+ if not token:
116
+ raise RuntimeError(
117
+ "GitHub token not provided.\n"
118
+ "Save one in .projinit.env, set $GITHUB_TOKEN, or pass --github without a token to be prompted."
119
+ )
120
+
121
+ r = requests.post(
122
+ "https://api.github.com/user/repos",
123
+ headers={
124
+ "Authorization": f"token {token}",
125
+ "Accept": "application/vnd.github+json",
126
+ },
127
+ json={"name": repo_name, "private": False},
128
+ timeout=10,
129
+ )
130
+ if r.status_code >= 300:
131
+ raise RuntimeError(f"GitHub API error {r.status_code}: {r.text.strip()}")
132
+ return r.json()["clone_url"] # e.g. https://github.com/user/repo.git
133
+
134
+
135
+ def _delete_github_repo(owner: str, repo_name: str, token: str) -> None:
136
+ """Delete a GitHub repository."""
137
+ r = requests.delete(
138
+ f"https://api.github.com/repos/{owner}/{repo_name}",
139
+ headers={
140
+ "Authorization": f"token {token}",
141
+ "Accept": "application/vnd.github+json",
142
+ },
143
+ timeout=10,
144
+ )
145
+ if r.status_code >= 300:
146
+ raise RuntimeError(f"GitHub API error {r.status_code}: {r.text.strip()}")
147
+
148
+
149
+ # ----------------------------------------------------------------------------- #
150
+ # Language-specific scaffolding #
151
+ # ----------------------------------------------------------------------------- #
152
+
153
+
154
+ def _scaffold_python(path: pathlib.Path) -> None:
155
+ """Create Python project structure with Hello World."""
156
+ (path / "src").mkdir()
157
+ (path / "src" / "__init__.py").touch()
158
+ (path / "src" / "main.py").write_text('print("Hello, world!")\n')
159
+ (path / ".gitignore").write_text(GITIGNORE_PYTHON + "\n")
160
+ (path / "requirements.txt").touch()
161
+
162
+ # Create pyproject.toml
163
+ pyproject = textwrap.dedent(f'''
164
+ [project]
165
+ name = "{path.name}"
166
+ version = "0.1.0"
167
+ description = ""
168
+ requires-python = ">=3.8"
169
+ dependencies = []
170
+ ''').strip()
171
+ (path / "pyproject.toml").write_text(pyproject + "\n")
172
+
173
+ # Create virtual environment
174
+ venv.create(path / ".venv", with_pip=True)
175
+
176
+
177
+ def _scaffold_rust(path: pathlib.Path) -> None:
178
+ """Create Rust project structure with Hello World."""
179
+ (path / "src").mkdir()
180
+ (path / "src" / "main.rs").write_text('fn main() {\n println!("Hello, world!");\n}\n')
181
+ (path / ".gitignore").write_text(GITIGNORE_RUST + "\n")
182
+
183
+ # Create Cargo.toml
184
+ cargo_toml = textwrap.dedent(f'''
185
+ [package]
186
+ name = "{path.name}"
187
+ version = "0.1.0"
188
+ edition = "2021"
189
+
190
+ [dependencies]
191
+ ''').strip()
192
+ (path / "Cargo.toml").write_text(cargo_toml + "\n")
193
+
194
+
195
+ def _scaffold_javascript(path: pathlib.Path) -> None:
196
+ """Create JavaScript project structure with Hello World."""
197
+ (path / "index.js").write_text('console.log("Hello, world!");\n')
198
+ (path / ".gitignore").write_text(GITIGNORE_JAVASCRIPT + "\n")
199
+
200
+ # Create package.json
201
+ package_json = textwrap.dedent(f'''
202
+ {{
203
+ "name": "{path.name}",
204
+ "version": "0.1.0",
205
+ "description": "",
206
+ "main": "index.js",
207
+ "scripts": {{
208
+ "start": "node index.js"
209
+ }}
210
+ }}
211
+ ''').strip()
212
+ (path / "package.json").write_text(package_json + "\n")
213
+
214
+
215
+ # ----------------------------------------------------------------------------- #
216
+ # Public API #
217
+ # ----------------------------------------------------------------------------- #
218
+
219
+
220
+ def create_project(path: pathlib.Path, github: bool = False, lang: str = "python") -> None:
221
+ """
222
+ Make a fully-initialised project directory.
223
+
224
+ Parameters
225
+ ----------
226
+ path : pathlib.Path target directory (must not already exist)
227
+ github : bool if True, also create & push remote repo
228
+ lang : str language: "python", "rust", or "javascript"
229
+ """
230
+ # Create project folder
231
+ path.mkdir(parents=False, exist_ok=False)
232
+
233
+ # Add README
234
+ (path / "README.md").write_text(README_TEXT.format(name=path.name))
235
+
236
+ # Language-specific scaffolding
237
+ if lang == "python":
238
+ _scaffold_python(path)
239
+ elif lang == "rust":
240
+ _scaffold_rust(path)
241
+ elif lang == "javascript":
242
+ _scaffold_javascript(path)
243
+ else:
244
+ raise ValueError(f"Unknown language: {lang}. Choose: python, rust, javascript")
245
+
246
+ # git repository
247
+ if shutil.which("git") is None:
248
+ raise RuntimeError("`git` executable not found in PATH")
249
+
250
+ # GitHub remote + push (optional)
251
+ token: str | None = None
252
+ if github:
253
+ # token preference: .projinit.env > $GITHUB_TOKEN > prompt user
254
+ token = _project_token(path) or os.getenv("GITHUB_TOKEN", "")
255
+ if not token:
256
+ import typer # lazy import to avoid hard dependency in library mode
257
+
258
+ typer.secho("Opening GitHub to generate a token...", fg=typer.colors.YELLOW)
259
+ webbrowser.open(NEW_TOKEN_URL.format(name=path.name))
260
+ token = typer.prompt(
261
+ "Paste your new GitHub token here (saved to .projinit.env, never committed)"
262
+ )
263
+ _save_project_token(path, token)
264
+
265
+ _sh(["git", "init", "-b", "main"], cwd=path)
266
+ _sh(["git", "add", "."], cwd=path)
267
+ _sh(["git", "commit", "-m", "Initial commit"], cwd=path)
268
+
269
+ if github:
270
+ repo_url = _create_github_repo(path.name, token)
271
+ # inject token into HTTPS URL for authentication: https://TOKEN@github.com/...
272
+ auth_repo_url = repo_url.replace("https://", f"https://{token}@", 1)
273
+
274
+ _sh(["git", "remote", "add", "origin", auth_repo_url], cwd=path)
275
+ _sh(["git", "push", "-u", "origin", "main"], cwd=path)
276
+
277
+
278
+ def delete_project(path: pathlib.Path, github: bool = False) -> None:
279
+ """
280
+ Delete *path* directory; optionally delete its remote GitHub repo.
281
+
282
+ Token resolution order:
283
+ 1. .projinit.env inside the project
284
+ 2. $GITHUB_TOKEN
285
+ 3. raise RuntimeError if --github but no token found
286
+ """
287
+ if not path.exists():
288
+ raise RuntimeError(f"{path} does not exist")
289
+
290
+ # remove remote first (so local folder still has .projinit.env if needed)
291
+ if github:
292
+ token = _project_token(path) or os.getenv("GITHUB_TOKEN")
293
+ if not token:
294
+ raise RuntimeError(
295
+ "No GitHub token found in .projinit.env or $GITHUB_TOKEN"
296
+ )
297
+ owner = _github_user(token)
298
+ _delete_github_repo(owner, path.name, token)
299
+
300
+ # finally delete local directory
301
+ shutil.rmtree(path)