naissance 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
naissance/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """naissance — Git repository genesis utility."""
2
+
3
+ __version__ = "2.0.0"
naissance/__main__.py ADDED
@@ -0,0 +1,6 @@
1
+ """Allow running as `python -m naissance`."""
2
+
3
+ from naissance.cli import parse_args
4
+ from naissance.core import run
5
+
6
+ run(parse_args())
naissance/cli.py ADDED
@@ -0,0 +1,66 @@
1
+ """
2
+ cli.py — Argument parsing for naissance.
3
+
4
+ Usage:
5
+ naissance <repo_name> # CWD-relative (default)
6
+ naissance <absolute_path> --apath # Absolute path
7
+ naissance . # CWD, folder name = repo name
8
+ naissance <repo_name> --public # Override visibility
9
+ """
10
+
11
+ import argparse
12
+
13
+ from naissance import __version__
14
+
15
+
16
+ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
17
+ """
18
+ Parse command-line arguments and return a Namespace.
19
+
20
+ Parameters
21
+ ----------
22
+ argv : list[str] | None
23
+ Explicit argument list (for testing). Defaults to sys.argv[1:].
24
+ """
25
+ parser = argparse.ArgumentParser(
26
+ prog="naissance",
27
+ description="Git Repository Genesis — create local + remote repos in one shot.",
28
+ )
29
+
30
+ parser.add_argument(
31
+ "repo_name",
32
+ help=(
33
+ 'Name (or path) of the repository to create. '
34
+ 'Resolved relative to CWD by default. '
35
+ 'Use "." for current directory. '
36
+ 'Use --apath for absolute paths.'
37
+ ),
38
+ )
39
+
40
+ parser.add_argument(
41
+ "--apath",
42
+ action="store_true",
43
+ default=False,
44
+ help="Treat repo_name as an absolute path.",
45
+ )
46
+
47
+ parser.add_argument(
48
+ "--name",
49
+ default=None,
50
+ help="Explicit name for the remote repository (overrides directory name).",
51
+ )
52
+
53
+ parser.add_argument(
54
+ "--public",
55
+ action="store_true",
56
+ default=False,
57
+ help="Create the remote repository as public (default: private).",
58
+ )
59
+
60
+ parser.add_argument(
61
+ "-v", "--version",
62
+ action="version",
63
+ version=f"naissance {__version__}",
64
+ )
65
+
66
+ return parser.parse_args(argv)
naissance/core.py ADDED
@@ -0,0 +1,261 @@
1
+ """
2
+ core.py — Main orchestrator for naissance.
3
+
4
+ Executes the full repo-genesis workflow with ZERO interactive prompts.
5
+ All decisions are driven by defaults.yml.
6
+ """
7
+
8
+ import enum
9
+ from pathlib import Path
10
+
11
+ import yaml
12
+
13
+ from naissance import errors
14
+ from naissance import git_ops
15
+ from naissance import path_resolver
16
+
17
+
18
+ # ── Directory state enum ──────────────────────────────────────────────
19
+
20
+ class DirState(enum.Enum):
21
+ NOT_EXISTS = "not_exists"
22
+ EMPTY = "empty"
23
+ NON_EMPTY = "non_empty"
24
+ GIT_INITIALIZED = "git_initialized"
25
+
26
+
27
+ def _detect_state(path: Path) -> DirState:
28
+ """Detect the current state of the target directory."""
29
+ if not path.exists():
30
+ return DirState.NOT_EXISTS
31
+
32
+ if not path.is_dir():
33
+ errors.fail(f"Path exists but is not a directory: {path}")
34
+
35
+ # Check for .git/ first — a git repo is always "non-empty" but
36
+ # we treat it as its own state.
37
+ if (path / ".git").is_dir():
38
+ return DirState.GIT_INITIALIZED
39
+
40
+ # Check if directory is truly empty
41
+ children = list(path.iterdir())
42
+ if not children:
43
+ return DirState.EMPTY
44
+
45
+ return DirState.NON_EMPTY
46
+
47
+
48
+ # ── Defaults loader ──────────────────────────────────────────────────
49
+
50
+ def _load_defaults() -> dict:
51
+ """Load defaults.yml from the package directory."""
52
+ defaults_path = Path(__file__).parent / "defaults" / "defaults.yml"
53
+
54
+ if not defaults_path.exists():
55
+ errors.fail(f"defaults.yml not found at: {defaults_path}")
56
+
57
+ try:
58
+ with open(defaults_path, "r", encoding="utf-8") as f:
59
+ data = yaml.safe_load(f)
60
+ except yaml.YAMLError as exc:
61
+ errors.fail(f"Failed to parse defaults.yml: {exc}")
62
+
63
+ return data or {}
64
+
65
+
66
+ # ── Scaffold helpers ─────────────────────────────────────────────────
67
+
68
+ def _create_scaffold(path: Path, defaults: dict) -> list[str]:
69
+ """
70
+ Create README.md and .gitignore based on defaults.
71
+ Returns list of created filenames (for staging).
72
+ """
73
+ created: list[str] = []
74
+
75
+ if defaults.get("create_readme", True):
76
+ readme = path / "README.md"
77
+ if not readme.exists():
78
+ readme.write_text("", encoding="utf-8")
79
+ errors.success("README.md")
80
+ created.append("README.md")
81
+ else:
82
+ errors.warn("README.md already exists, skipping")
83
+
84
+ if defaults.get("create_gitignore", True):
85
+ gitignore = path / ".gitignore"
86
+ if not gitignore.exists():
87
+ gitignore.write_text("", encoding="utf-8")
88
+ errors.success(".gitignore")
89
+ created.append(".gitignore")
90
+ else:
91
+ errors.warn(".gitignore already exists, skipping")
92
+
93
+ return created
94
+
95
+
96
+ # ── State handlers ────────────────────────────────────────────────────
97
+
98
+ def _handle_not_exists(
99
+ path: Path,
100
+ repo_name: str,
101
+ visibility: str,
102
+ defaults: dict,
103
+ ) -> None:
104
+ """Directory doesn't exist → create it, clone remote into it."""
105
+ path.mkdir(parents=True, exist_ok=True)
106
+ errors.success(f"Directory created: {path}")
107
+
108
+ # Create remote and clone into the directory.
109
+ # gh repo create <name> --clone creates ./<name> in the parent dir,
110
+ # so we need the parent, and the folder name must match repo_name.
111
+ git_ops.gh_repo_create_clone(
112
+ repo_name,
113
+ visibility=visibility,
114
+ clone_into=path,
115
+ )
116
+
117
+ # Scaffold files inside the now-cloned repo
118
+ created = _create_scaffold(path, defaults)
119
+
120
+ if created:
121
+ git_ops.git_add(path, *created)
122
+ git_ops.git_commit(path, defaults.get("initial_commit_message", "initial commit"))
123
+ branch = git_ops.git_branch_current(path, defaults.get("default_branch", "main"))
124
+ git_ops.git_push(path, branch)
125
+
126
+
127
+ def _handle_empty(
128
+ path: Path,
129
+ repo_name: str,
130
+ visibility: str,
131
+ defaults: dict,
132
+ ) -> None:
133
+ """Directory exists but is empty → clone remote into it."""
134
+ # gh repo create --clone will try to create the subdir, but it
135
+ # already exists (empty). We init locally instead, then use --source.
136
+ git_ops.git_init(path)
137
+
138
+ # Create remote from the local source
139
+ created = _create_scaffold(path, defaults)
140
+ if created:
141
+ git_ops.git_add(path, *created)
142
+ git_ops.git_commit(path, defaults.get("initial_commit_message", "initial commit"))
143
+
144
+ git_ops.gh_repo_create_source(
145
+ path,
146
+ repo_name,
147
+ visibility=visibility,
148
+ push=True,
149
+ )
150
+
151
+
152
+ def _handle_non_empty(
153
+ path: Path,
154
+ repo_name: str,
155
+ visibility: str,
156
+ defaults: dict,
157
+ ) -> None:
158
+ """Directory has files but no .git → init, scaffold, push."""
159
+ git_ops.git_init(path)
160
+
161
+ _create_scaffold(path, defaults)
162
+
163
+ # Stage everything
164
+ git_ops.git_add(path, ".")
165
+ git_ops.git_commit(path, defaults.get("initial_commit_message", "initial commit"))
166
+
167
+ git_ops.gh_repo_create_source(
168
+ path,
169
+ repo_name,
170
+ visibility=visibility,
171
+ push=True,
172
+ )
173
+
174
+
175
+ def _handle_git_initialized(
176
+ path: Path,
177
+ repo_name: str,
178
+ visibility: str,
179
+ defaults: dict,
180
+ ) -> None:
181
+ """Directory already has .git → just link remote."""
182
+ errors.info("Git already initialized")
183
+
184
+ if git_ops.git_has_remote(path):
185
+ errors.fail("Repository already has a remote origin configured")
186
+
187
+ git_ops.gh_repo_create_source(
188
+ path,
189
+ repo_name,
190
+ visibility=visibility,
191
+ push=True,
192
+ )
193
+
194
+
195
+ # ── Handler dispatch ─────────────────────────────────────────────────
196
+
197
+ _HANDLERS = {
198
+ DirState.NOT_EXISTS: _handle_not_exists,
199
+ DirState.EMPTY: _handle_empty,
200
+ DirState.NON_EMPTY: _handle_non_empty,
201
+ DirState.GIT_INITIALIZED: _handle_git_initialized,
202
+ }
203
+
204
+
205
+ # ── Main entry ────────────────────────────────────────────────────────
206
+
207
+ def run(args) -> None:
208
+ """
209
+ Execute the full repository genesis workflow.
210
+
211
+ Parameters
212
+ ----------
213
+ args : argparse.Namespace
214
+ Parsed CLI arguments (repo_name, apath, public).
215
+ """
216
+ errors.header()
217
+ print(" Birthing repository...\n")
218
+
219
+ # ── Load configuration ────────────────────────────────────────────
220
+ defaults = _load_defaults()
221
+
222
+ # ── Preflight checks ──────────────────────────────────────────────
223
+ errors.require_tool("git")
224
+ errors.success("git found")
225
+
226
+ errors.require_tool("gh", friendly="GitHub CLI (gh)")
227
+ errors.require_gh_auth()
228
+ errors.success("gh authenticated")
229
+
230
+ # ── Resolve path ──────────────────────────────────────────────────
231
+ resolved_path, repo_name = path_resolver.resolve(
232
+ args.repo_name,
233
+ apath=args.apath,
234
+ )
235
+
236
+ # --name overrides the directory-derived repo name
237
+ if args.name:
238
+ repo_name = args.name.strip()
239
+
240
+ errors.success(f"Path resolved: {resolved_path}")
241
+
242
+ # ── Determine visibility ──────────────────────────────────────────
243
+ if args.public:
244
+ visibility = "public"
245
+ else:
246
+ visibility = defaults.get("visibility", "private")
247
+
248
+ # ── Check if remote already exists ────────────────────────────────
249
+ if git_ops.gh_repo_exists(repo_name):
250
+ errors.fail(f"Remote repository already exists: {repo_name}")
251
+
252
+ # ── Detect directory state ────────────────────────────────────────
253
+ state = _detect_state(resolved_path)
254
+ errors.info(f"Directory state: {state.value}")
255
+
256
+ # ── Dispatch to the appropriate handler ───────────────────────────
257
+ handler = _HANDLERS[state]
258
+ handler(resolved_path, repo_name, visibility, defaults)
259
+
260
+ # ── Done ──────────────────────────────────────────────────────────
261
+ errors.banner(repo_name)
naissance/errors.py ADDED
@@ -0,0 +1,94 @@
1
+ """
2
+ errors.py — Centralized error handler for naissance.
3
+
4
+ Provides TUI-style output (✓ / ⨯ / ⚠) and prevents raw subprocess
5
+ tracebacks from ever reaching the user's terminal.
6
+ """
7
+
8
+ import sys
9
+
10
+
11
+ # ── ANSI color codes ──────────────────────────────────────────────────
12
+ _GREEN = "\033[92m"
13
+ _RED = "\033[91m"
14
+ _YELLOW = "\033[93m"
15
+ _CYAN = "\033[96m"
16
+ _DIM = "\033[90m"
17
+ _BOLD = "\033[1m"
18
+ _RESET = "\033[0m"
19
+
20
+
21
+ # ── Symbols ───────────────────────────────────────────────────────────
22
+ _CHECK = "✓"
23
+ _CROSS = "⨯"
24
+ _WARN = "⚠"
25
+ _SPARKLE = "✨"
26
+
27
+
28
+ def success(msg: str) -> None:
29
+ """Print a green success line: ✓ msg"""
30
+ print(f" {_GREEN}{_CHECK}{_RESET} {msg}")
31
+
32
+
33
+ def fail(msg: str, *, exit_code: int = 1) -> None:
34
+ """Print a red failure line: ⨯ msg — then exit."""
35
+ print(f" {_RED}{_CROSS} {msg}{_RESET}", file=sys.stderr)
36
+ sys.exit(exit_code)
37
+
38
+
39
+ def warn(msg: str) -> None:
40
+ """Print a yellow warning line: ⚠ msg (non-fatal)."""
41
+ print(f" {_YELLOW}{_WARN} {msg}{_RESET}")
42
+
43
+
44
+ def info(msg: str) -> None:
45
+ """Print a dim informational line."""
46
+ print(f" {_DIM}{msg}{_RESET}")
47
+
48
+
49
+ def banner(repo_name: str) -> None:
50
+ """Print the final celebration line."""
51
+ print(f"\n {_SPARKLE} {_CYAN}{_BOLD}{repo_name}{_RESET} is alive.\n")
52
+
53
+
54
+ def header() -> None:
55
+ """Print the naissance header."""
56
+ print(f"""
57
+ {_DIM}┼─────────────────────────────────────────────────────────────┼
58
+ | _ __ _
59
+ | / | / /___ _(_)_____________ _____ ________
60
+ | / |/ / __ `/ / ___/ ___/ __ `/ __ \\/ ___/ _ \\
61
+ | / /| / /_/ / (__ |__ ) /_/ / / / / /__/ __/
62
+ | /_/ |_/\\__,_/_/____/____/\\__,_/_/ /_/\\___/\\___/
63
+ | v2.0.0
64
+ |
65
+ | Git Repository Genesis Utility
66
+ ┼─────────────────────────────────────────────────────────────┼{_RESET}
67
+ """)
68
+
69
+
70
+ # ── Preflight error helpers ───────────────────────────────────────────
71
+ # Each checks a single condition and calls fail() if it's not met.
72
+
73
+ def require_tool(tool: str, friendly: str | None = None) -> None:
74
+ """Verify a CLI tool is available on PATH."""
75
+ import shutil
76
+
77
+ label = friendly or tool
78
+ if shutil.which(tool) is None:
79
+ fail(f"{label} is not installed or not on PATH")
80
+
81
+
82
+ def require_gh_auth() -> None:
83
+ """Verify the GitHub CLI is authenticated."""
84
+ import subprocess
85
+
86
+ result = subprocess.run(
87
+ ["gh", "auth", "status"],
88
+ capture_output=True,
89
+ text=True,
90
+ encoding="utf-8",
91
+ errors="replace",
92
+ )
93
+ if result.returncode != 0:
94
+ fail("GitHub CLI is not authenticated. Run: gh auth login")
naissance/git_ops.py ADDED
@@ -0,0 +1,166 @@
1
+ """
2
+ git_ops.py — Git and GitHub CLI subprocess wrappers.
3
+
4
+ Every function routes errors through the centralized ErrorHandler so
5
+ that raw tracebacks never leak into the user's terminal.
6
+ """
7
+
8
+ import subprocess
9
+ from pathlib import Path
10
+
11
+ from naissance import errors
12
+
13
+
14
+ def _run(
15
+ cmd: list[str],
16
+ *,
17
+ cwd: Path | None = None,
18
+ check: bool = True,
19
+ label: str | None = None,
20
+ ) -> subprocess.CompletedProcess[str]:
21
+ """
22
+ Run a subprocess, suppressing raw output.
23
+
24
+ Parameters
25
+ ----------
26
+ cmd : list[str]
27
+ The command and its arguments.
28
+ cwd : Path | None
29
+ Working directory for the command.
30
+ check : bool
31
+ If True, call errors.fail() on non-zero exit.
32
+ label : str | None
33
+ Human-readable label for error messages (e.g. "git init").
34
+
35
+ Returns
36
+ -------
37
+ subprocess.CompletedProcess
38
+ """
39
+ try:
40
+ result = subprocess.run(
41
+ cmd,
42
+ cwd=cwd,
43
+ text=True,
44
+ capture_output=True,
45
+ encoding="utf-8",
46
+ errors="replace",
47
+ )
48
+ except FileNotFoundError:
49
+ errors.fail(f"{cmd[0]} is not installed or not on PATH")
50
+ except PermissionError:
51
+ errors.fail(f"Permission denied while running: {' '.join(cmd)}")
52
+ except Exception as exc:
53
+ # Catch-all — no raw tracebacks should ever leak
54
+ errors.fail(f"Unexpected error running {cmd[0]}: {exc}")
55
+
56
+ if check and result.returncode != 0:
57
+ detail = result.stderr.strip() or result.stdout.strip() or "unknown error"
58
+ tag = label or " ".join(cmd[:2])
59
+ errors.fail(f"{tag} failed: {detail}")
60
+
61
+ return result
62
+
63
+
64
+ # ── Git operations ────────────────────────────────────────────────────
65
+
66
+ def git_init(path: Path) -> None:
67
+ """Initialize a git repository at *path*."""
68
+ _run(["git", "init"], cwd=path, label="git init")
69
+ errors.success("Git initialized")
70
+
71
+
72
+ def git_add(path: Path, *targets: str) -> None:
73
+ """Stage files. Pass '.' to stage everything."""
74
+ _run(["git", "add", *targets], cwd=path, label="git add")
75
+
76
+
77
+ def git_commit(path: Path, message: str) -> None:
78
+ """Create a commit with *message*."""
79
+ _run(["git", "commit", "-m", message], cwd=path, label="git commit")
80
+ errors.success("Initial commit")
81
+
82
+
83
+ def git_push(path: Path, branch: str) -> None:
84
+ """Push to origin with upstream tracking."""
85
+ _run(["git", "push", "-u", "origin", branch], cwd=path, label="git push")
86
+ errors.success(f"Pushed to {branch}")
87
+
88
+
89
+ def git_branch_current(path: Path, default: str = "main") -> str:
90
+ """Return the current branch name, falling back to *default*."""
91
+ result = _run(
92
+ ["git", "branch", "--show-current"],
93
+ cwd=path,
94
+ check=False,
95
+ label="git branch",
96
+ )
97
+ branch = result.stdout.strip() if result.returncode == 0 else ""
98
+ return branch or default
99
+
100
+
101
+ def git_has_remote(path: Path, name: str = "origin") -> bool:
102
+ """Check whether a git remote named *name* already exists."""
103
+ result = _run(
104
+ ["git", "remote", "get-url", name],
105
+ cwd=path,
106
+ check=False,
107
+ label="git remote",
108
+ )
109
+ return result.returncode == 0
110
+
111
+
112
+ # ── GitHub CLI operations ─────────────────────────────────────────────
113
+
114
+ def gh_repo_create_clone(
115
+ name: str,
116
+ *,
117
+ visibility: str = "private",
118
+ clone_into: Path | None = None,
119
+ ) -> None:
120
+ """
121
+ Create a remote repo and clone it locally.
122
+
123
+ Used when the target directory does NOT exist or is EMPTY.
124
+ """
125
+ vis_flag = "--public" if visibility == "public" else "--private"
126
+ cmd = ["gh", "repo", "create", name, vis_flag, "--clone"]
127
+
128
+ if clone_into is not None:
129
+ # gh repo create <name> --clone clones into ./<name>
130
+ # We run it from the parent so the folder lands at the right spot.
131
+ _run(cmd, cwd=clone_into.parent, label="gh repo create --clone")
132
+ else:
133
+ _run(cmd, label="gh repo create --clone")
134
+
135
+ errors.success(f"GitHub repository created ({visibility})")
136
+
137
+
138
+ def gh_repo_create_source(
139
+ path: Path,
140
+ name: str,
141
+ *,
142
+ visibility: str = "private",
143
+ push: bool = True,
144
+ ) -> None:
145
+ """
146
+ Create a remote repo from an existing local repo.
147
+
148
+ Used when the target directory already has files / is git-initialized.
149
+ """
150
+ vis_flag = "--public" if visibility == "public" else "--private"
151
+ cmd = ["gh", "repo", "create", name, vis_flag, "--source=."]
152
+ if push:
153
+ cmd.append("--push")
154
+
155
+ _run(cmd, cwd=path, label="gh repo create --source")
156
+ errors.success(f"GitHub repository created ({visibility})")
157
+
158
+
159
+ def gh_repo_exists(name: str) -> bool:
160
+ """Check whether a remote repository with *name* already exists."""
161
+ result = _run(
162
+ ["gh", "repo", "view", name],
163
+ check=False,
164
+ label="gh repo view",
165
+ )
166
+ return result.returncode == 0
naissance/main.py ADDED
@@ -0,0 +1,14 @@
1
+ """
2
+ main.py — Thin wrapper so `python main.py <args>` still works.
3
+ """
4
+
5
+ from naissance.cli import parse_args
6
+ from naissance.core import run
7
+
8
+
9
+ def main():
10
+ run(parse_args())
11
+
12
+
13
+ if __name__ == "__main__":
14
+ main()
@@ -0,0 +1,62 @@
1
+ """
2
+ path_resolver.py — Resolve CLI input to a concrete (Path, repo_name) pair.
3
+
4
+ Resolution rules:
5
+ naissance horizon → CWD / "horizon", repo_name = "horizon"
6
+ naissance code/horizon → CWD / "code/horizon", repo_name = "horizon"
7
+ naissance . → CWD, repo_name = CWD folder name
8
+ naissance "C:\\x\\y" --apath → Path("C:\\x\\y"), repo_name = "y"
9
+ """
10
+
11
+ from pathlib import Path
12
+
13
+ from naissance import errors
14
+
15
+
16
+ def resolve(repo_name: str, *, apath: bool = False) -> tuple[Path, str]:
17
+ """
18
+ Resolve *repo_name* into an absolute Path and a clean repo name.
19
+
20
+ Parameters
21
+ ----------
22
+ repo_name : str
23
+ The raw positional argument from the CLI.
24
+ apath : bool
25
+ If True, treat *repo_name* as an absolute path.
26
+
27
+ Returns
28
+ -------
29
+ (resolved_path, repo_name)
30
+ The fully-resolved directory path and the repository name
31
+ (last component of the path).
32
+ """
33
+ raw = repo_name.strip().strip('"').strip("'")
34
+
35
+ # ── Dot notation: use CWD itself ──────────────────────────────────
36
+ if raw in (".", "./", ".\\"):
37
+ resolved = Path.cwd().resolve()
38
+ name = resolved.name
39
+ return resolved, name
40
+
41
+ # ── Absolute path mode ────────────────────────────────────────────
42
+ if apath:
43
+ resolved = Path(raw).resolve()
44
+ else:
45
+ # Default: relative to CWD
46
+ resolved = (Path.cwd() / raw).resolve()
47
+
48
+ name = resolved.name
49
+
50
+ if not name:
51
+ errors.fail(f"Could not determine repository name from path: {resolved}")
52
+
53
+ # Validate that the *parent* directory exists (the repo dir itself
54
+ # may be created later). For absolute paths the parent must exist;
55
+ # for relative paths CWD is always valid so this is a safety net.
56
+ parent = resolved.parent
57
+ if not parent.exists():
58
+ errors.fail(f"Parent directory does not exist: {parent}")
59
+ if parent.exists() and not parent.is_dir():
60
+ errors.fail(f"Parent path is not a directory: {parent}")
61
+
62
+ return resolved, name
@@ -0,0 +1,193 @@
1
+ Metadata-Version: 2.4
2
+ Name: naissance
3
+ Version: 2.0.0
4
+ Summary: Git Repository Genesis Utility
5
+ License: MIT
6
+ Project-URL: Homepage, https://github.com/DODO-unique/Naissance
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Programming Language :: Python :: 3.10
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.10
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: pyyaml>=6.0
15
+ Dynamic: license-file
16
+
17
+ # Naissance
18
+
19
+ > *A more dramatic Git Repository Genesis Utility.*
20
+
21
+ Naissance creates both local and remote repositories in one shot. No context switching between terminal and browser — just run one command and your repo is alive.
22
+
23
+ <!-- ![Naissance Demo](./docs/demo.gif) -->
24
+
25
+
26
+ ### Philosophy
27
+
28
+ Naissance is meant to **get out of the way**. The goal is always to minimize friction between "I want a repo" and "the repo exists." No prompts, no decisions at runtime, no context switching. You type one command, naissance handles the rest, and you're already writing code. Every design choice should serve this principle.
29
+
30
+ <!-- TODO: Add hero banner/screenshot here -->
31
+
32
+ ---
33
+
34
+ ## Installation
35
+
36
+ ```bash
37
+ # Clone and install
38
+ git clone https://github.com/DODO-unique/Naissance.git
39
+ cd Naissance
40
+ pip install .
41
+ ```
42
+
43
+ ### Requirements
44
+
45
+ - Python ≥ 3.10
46
+ - [Git](https://git-scm.com/)
47
+ - [GitHub CLI (`gh`)](https://cli.github.com/) — authenticated via `gh auth login`
48
+
49
+ > **Note:** `gh` is a crucial dependency — naissance uses it to create remote repositories. Without it, the tool cannot function. If `gh` is not installed, naissance will detect this at startup and suggest how to install it:
50
+ > ```
51
+ > ⨯ GitHub CLI (gh) is not installed or not on PATH
52
+ > ```
53
+ > Install it from [cli.github.com](https://cli.github.com/), then run `gh auth login` to authenticate.
54
+
55
+ ---
56
+
57
+ ## Quick Start
58
+
59
+ ```bash
60
+ # Create a new repo "horizon" in the current directory
61
+ naissance horizon
62
+
63
+ # That's it. Local + remote, done.
64
+ ```
65
+
66
+ <!-- TODO: Add terminal recording / screenshot of output here -->
67
+ <!-- ![Quick Start Output](./docs/quickstart.png) -->
68
+
69
+ ### What happens
70
+
71
+ ```
72
+ ┼─────────────────────────────────────────────────────────────┼
73
+ | _ __ _
74
+ | / | / /___ _(_)_____________ _____ ________
75
+ | / |/ / __ `/ / ___/ ___/ __ `/ __ \/ ___/ _ \
76
+ | / /| / /_/ / (__ |__ ) /_/ / / / / /__/ __/
77
+ | /_/ |_/\__,_/_/____/____/\__,_/_/ /_/\___/\___/
78
+ | v2.0.0
79
+ |
80
+ | Git Repository Genesis Utility
81
+ ┼─────────────────────────────────────────────────────────────┼
82
+
83
+ Birthing repository...
84
+
85
+ ✓ git found
86
+ ✓ gh authenticated
87
+ ✓ Path resolved: C:\Code\horizon
88
+ ✓ Directory created
89
+ ✓ GitHub repository created (private)
90
+ ✓ Git initialized
91
+ ✓ README.md
92
+ ✓ .gitignore
93
+ ✓ Initial commit
94
+ ✓ Pushed to main
95
+
96
+ ✨ horizon is alive.
97
+ ```
98
+
99
+ ---
100
+
101
+ ## Usage
102
+
103
+ ```bash
104
+ naissance <repo_name> [--apath] [--name NAME] [--public]
105
+ ```
106
+
107
+ | Flag | Description |
108
+ |---|---|
109
+ | `repo_name` | Name or path of the repo. Resolved relative to CWD. Use `.` for current directory. |
110
+ | `--apath` | Treat `repo_name` as an absolute path |
111
+ | `--name NAME` | Override the remote repository name (defaults to directory name) |
112
+ | `--public` | Create the remote repo as public (default: private) |
113
+ | `-v, --version` | Show version |
114
+
115
+ ### Examples
116
+
117
+ ```bash
118
+ # Relative path — creates ./horizon
119
+ naissance horizon
120
+
121
+ # Nested path — creates ./projects/horizon
122
+ naissance projects/horizon
123
+
124
+ # Current directory — uses folder name as repo name
125
+ naissance .
126
+
127
+ # Absolute path
128
+ naissance "C:\Code\horizon" --apath
129
+
130
+ # Different remote name than the directory
131
+ naissance horizon --name my-cool-project
132
+
133
+ # Public repo
134
+ naissance horizon --public
135
+ ```
136
+
137
+ ---
138
+
139
+ ## How It Works
140
+
141
+ Naissance detects the state of the target directory and adapts:
142
+
143
+ | State | What naissance does |
144
+ |---|---|
145
+ | **Doesn't exist** | Creates the directory → `gh repo create --clone` → scaffolds README + .gitignore → commits → pushes |
146
+ | **Empty directory** | `git init` → scaffolds → `gh repo create --source` → pushes |
147
+ | **Has files, no git** | `git init` → scaffolds → stages all → commits → `gh repo create --source` → pushes |
148
+ | **Already git-initialized** | Checks no remote exists → `gh repo create --source` → pushes |
149
+
150
+ All decisions (visibility, scaffold files, commit message) are driven by [`defaults.yml`](./defaults.yml) — **zero interactive prompts** during execution.
151
+
152
+ ---
153
+
154
+ ## Configuration
155
+
156
+ Edit `defaults.yml` next to the package to change defaults:
157
+
158
+ ```yaml
159
+ version: "2.0.0"
160
+ visibility: "private" # "private" or "public"
161
+ create_readme: true
162
+ create_gitignore: true
163
+ prompt_description: false
164
+ default_branch: "main"
165
+ initial_commit_message: "initial commit"
166
+ ```
167
+
168
+ ---
169
+
170
+ ## Error Handling
171
+
172
+ Naissance never leaks raw tracebacks. All errors are caught and displayed cleanly:
173
+
174
+ ```
175
+ ⨯ git is not installed or not on PATH
176
+ ⨯ GitHub CLI is not authenticated. Run: gh auth login
177
+ ⨯ Remote repository already exists: horizon
178
+ ⨯ Permission denied: C:\Protected\path
179
+ ```
180
+
181
+ ---
182
+
183
+ ## License
184
+
185
+ [MIT](./LICENSE)
186
+
187
+ ---
188
+
189
+ ## See Also
190
+
191
+ - [CHANGELOG.md](./CHANGELOG.md) — Version history
192
+ - [ROADMAP.md](./ROADMAP.md) — What's coming next
193
+ - [KNOWN_ISSUES.md](./KNOWN_ISSUES.md) — Bugs and workarounds
@@ -0,0 +1,14 @@
1
+ naissance/__init__.py,sha256=k6FrpPbBT4qc4UaIJ0kEP2dmTffqu4HH54MxnriLZVw,75
2
+ naissance/__main__.py,sha256=bFjYJ-yK5LcwFu9B6NiCsirX25944Orq2DsYEl4d99A,134
3
+ naissance/cli.py,sha256=gPPxNMGcVgEKehlEZI5DfnSii4ZvxbfPczsTHO80Lns,1748
4
+ naissance/core.py,sha256=5vwxo66gqZH8YZDvkgThENSKva5L0HzKH_nK7XA9w4U,8856
5
+ naissance/errors.py,sha256=QB1dg6iDayjZQonMRtE8kb3_Wap-2u3r9Q14EKXk23g,3166
6
+ naissance/git_ops.py,sha256=AVtGWG0vyJU3l87D4QEc5OIwIXtNfy3a1yqAjNomM3I,5026
7
+ naissance/main.py,sha256=PoUCMlYcTu69uqZ63txJQqk-4W8xrCSzwZgndRcp8SI,218
8
+ naissance/path_resolver.py,sha256=pMsN3gk3ohSBEjeMblZiVP1IlvqQk45ygonxO5osP6Q,2224
9
+ naissance-2.0.0.dist-info/licenses/LICENSE,sha256=hDNBiwtJIJpBVn9UydxtyCD0UWfgeL_DSLF4VeuMZ_o,1071
10
+ naissance-2.0.0.dist-info/METADATA,sha256=6c1twIycYIT0wjywMgY1_v-Z1Ct99EKj9tcwF6pA6G0,5770
11
+ naissance-2.0.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
12
+ naissance-2.0.0.dist-info/entry_points.txt,sha256=h5g7DSITd1UpnKtZcWf4Io3cF7h5KN4oXmSoTXnrst4,50
13
+ naissance-2.0.0.dist-info/top_level.txt,sha256=ZOhXtHEDhA2i6Aa_pgAvGOjrg70We8AOn2CozMUiaSI,10
14
+ naissance-2.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ naissance = naissance.main:main
@@ -0,0 +1,9 @@
1
+ MIT License
2
+
3
+ Copyright 2026 DODO-unique
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1 @@
1
+ naissance