shappetrack 1.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.
shappetrack/README.md ADDED
@@ -0,0 +1,67 @@
1
+ # ShappeTrack CLI
2
+
3
+ Clone, commit, and push ShappeTrack repositories with **one app token**. Real
4
+ `git` runs under the hood; the git engine is never exposed.
5
+
6
+ ```
7
+ shappetrack login --server https://track-api.shappe.ai/api/v1
8
+ shappetrack clone my-repo
9
+ ```
10
+
11
+ ## Install
12
+
13
+ The CLI is a small Python package (`shappetrack-cli`). The recommended way is
14
+ [`pipx`](https://pipx.pypa.io/), which installs it in an isolated environment so
15
+ it never clashes with your other Python tools.
16
+
17
+ ### For end users (once published to PyPI)
18
+
19
+ ```bash
20
+ pipx install shappetrack
21
+ ```
22
+
23
+ ### From source (internal / development)
24
+
25
+ From the directory containing `pyproject.toml`:
26
+
27
+ ```bash
28
+ pipx install . # isolated, recommended
29
+ # or
30
+ pip install . # into the current environment
31
+ ```
32
+
33
+ Requires **Python 3.9+** and **git** on your PATH (the CLI runs real git under the hood).
34
+
35
+ ## Usage
36
+
37
+ 1. Create an app token in the web app: **Settings → Access Tokens** (starts with `shp_`).
38
+ 2. Log in once — this configures git credentials automatically:
39
+
40
+ ```bash
41
+ shappetrack login --server https://track-api.shappe.ai/api/v1
42
+ ```
43
+
44
+ 3. Then use the familiar commands (they run standard git underneath):
45
+
46
+ ```bash
47
+ shappetrack clone my-repo
48
+ shappetrack status
49
+ shappetrack add .
50
+ shappetrack commit -m "message" --task MT-101
51
+ shappetrack push
52
+ shappetrack pull
53
+ shappetrack branch feature-x
54
+ ```
55
+
56
+ ## How it works
57
+
58
+ `shappetrack clone my-repo` runs `git clone https://track-api.shappe.ai/repos/my-repo`
59
+ using the app token you logged in with. The backend proxies git traffic to the
60
+ internal engine — you never see or handle the engine, its host, or its token.
61
+
62
+ You can also clone with plain git directly (no CLI needed):
63
+
64
+ ```bash
65
+ git clone https://track-api.shappe.ai/repos/my-repo
66
+ # username: your ShappeTrack username, password: your shp_ app token
67
+ ```
@@ -0,0 +1,89 @@
1
+ # Releasing the ShappeTrack CLI
2
+
3
+ Three distribution channels, in order of how real CLIs ship (`gh`, `aws`, `stripe`):
4
+
5
+ 1. **PyPI** — `pipx install shappetrack` (primary for Python users)
6
+ 2. **Standalone binary** — a single `shappetrack.exe` / `shappetrack`, no Python needed
7
+ 3. **One-line installer** — `curl … | sh` that fetches the right binary
8
+
9
+ ---
10
+
11
+ ## 0. Build the artifacts
12
+
13
+ From `shappetrack-backend/` (the folder with `pyproject.toml`):
14
+
15
+ ```bash
16
+ python -m build # → dist/shappetrack-1.0.0-{.whl,.tar.gz}
17
+ python -m twine check dist/* # metadata validation (must PASS)
18
+ ```
19
+
20
+ Standalone binary:
21
+
22
+ ```bash
23
+ python -m PyInstaller --onefile --name shappetrack \
24
+ --collect-all typer --collect-all click --collect-all httpx \
25
+ --paths . shappetrack/__pyinstaller_entry.py --distpath dist/bin
26
+ # → dist/bin/shappetrack.exe (or dist/bin/shappetrack on Linux/macOS)
27
+ ```
28
+
29
+ Both are already built and verified (twine PASSED; the exe runs standalone).
30
+
31
+ ---
32
+
33
+ ## 1. Publish to PyPI ⚠️ needs a PyPI account + API token (only you can do this)
34
+
35
+ The name `shappetrack` is currently FREE on PyPI (verified). To claim + publish:
36
+
37
+ 1. Create an account at https://pypi.org and an **API token**
38
+ (Account settings → API tokens → scope "Entire account" for the first upload).
39
+ 2. Test on **TestPyPI** first (optional but recommended):
40
+
41
+ ```bash
42
+ python -m twine upload --repository testpypi dist/*
43
+ pipx install --index-url https://test.pypi.org/simple/ shappetrack
44
+ ```
45
+
46
+ 3. Publish for real:
47
+
48
+ ```bash
49
+ python -m twine upload dist/*
50
+ # username: __token__
51
+ # password: <your PyPI API token, starts with pypi-…>
52
+ ```
53
+
54
+ Then anyone can:
55
+
56
+ ```bash
57
+ pipx install shappetrack
58
+ ```
59
+
60
+ **Bump `version` in `pyproject.toml` for every release** — PyPI rejects re-uploading the same version.
61
+
62
+ ---
63
+
64
+ ## 2. Standalone binary (no Python for the user)
65
+
66
+ Attach `dist/bin/shappetrack.exe` (Windows) and a Linux/macOS build to a GitHub
67
+ Release. Build the exe **on each target OS** (PyInstaller isn't cross-platform).
68
+
69
+ ---
70
+
71
+ ## 3. One-line installer (later polish)
72
+
73
+ Host `install.sh` at e.g. `get.shappe.ai`; it detects OS/arch, downloads the
74
+ matching binary from the Releases page, and drops it on the user's PATH:
75
+
76
+ ```bash
77
+ curl -fsSL https://get.shappe.ai | sh
78
+ ```
79
+
80
+ ---
81
+
82
+ ## User-facing install summary (put in main docs)
83
+
84
+ | Method | Command | Needs Python? |
85
+ |--------|---------|---------------|
86
+ | pipx (recommended) | `pipx install shappetrack` | yes |
87
+ | pip | `pip install shappetrack` | yes |
88
+ | standalone binary | download `shappetrack.exe`, add to PATH | no |
89
+ | one-line (future) | `curl -fsSL https://get.shappe.ai \| sh` | no |
@@ -0,0 +1,7 @@
1
+ """ShappeTrack CLI — clone/commit/push ShappeTrack repos with one app token.
2
+
3
+ Real git runs underneath; the git engine stays invisible. Installed as the
4
+ `shappetrack` command (see ../pyproject.toml). Run standalone with `python -m shappetrack`.
5
+ """
6
+
7
+ __version__ = "1.0.0"
@@ -0,0 +1,4 @@
1
+ from .shapetrack import app
2
+
3
+ if __name__ == "__main__":
4
+ app()
@@ -0,0 +1,10 @@
1
+ """PyInstaller entry point — builds the standalone `shappetrack` executable.
2
+
3
+ PyInstaller needs a real script (not a module target), so this just invokes the
4
+ Typer app. Not used at runtime by the pip-installed CLI (that uses the console
5
+ entry point in pyproject.toml).
6
+ """
7
+ from shappetrack.shapetrack import app
8
+
9
+ if __name__ == "__main__":
10
+ app()
shappetrack/local.py ADDED
@@ -0,0 +1,121 @@
1
+ """Local working-copy format for ShapeTrack (the `.shapetrack/` folder)."""
2
+ import hashlib
3
+ import json
4
+ import os
5
+ from pathlib import Path
6
+ from typing import Dict, List, Optional
7
+
8
+ ST_DIR = ".shapetrack"
9
+ CRED_PATH = Path.home() / ".shapetrack" / "credentials.json"
10
+
11
+ # Directories never tracked
12
+ _IGNORE_DIRS = {".shapetrack", ".git", "node_modules", "__pycache__", "venv", ".venv", "dist", "build"}
13
+
14
+
15
+ # ---------- credentials (global) ----------
16
+ def save_credentials(server_url: str, token: str) -> None:
17
+ CRED_PATH.parent.mkdir(parents=True, exist_ok=True)
18
+ CRED_PATH.write_text(json.dumps({"server_url": server_url, "token": token}, indent=2))
19
+
20
+
21
+ def load_credentials() -> Optional[dict]:
22
+ if CRED_PATH.exists():
23
+ return json.loads(CRED_PATH.read_text())
24
+ return None
25
+
26
+
27
+ # ---------- local repo (.shapetrack/) ----------
28
+ def st_root(start: Optional[Path] = None) -> Optional[Path]:
29
+ """Walk up from `start` to find a directory containing `.shapetrack/`."""
30
+ cur = (start or Path.cwd()).resolve()
31
+ for d in [cur, *cur.parents]:
32
+ if (d / ST_DIR).is_dir():
33
+ return d
34
+ return None
35
+
36
+
37
+ def _meta_dir(root: Path) -> Path:
38
+ return root / ST_DIR
39
+
40
+
41
+ def init_local(root: Path, server_url: str, repo_slug: str, branch: str = "main") -> None:
42
+ meta = _meta_dir(root)
43
+ meta.mkdir(parents=True, exist_ok=True)
44
+ write_config(root, {"server_url": server_url, "repo_slug": repo_slug})
45
+ write_head(root, {"branch": branch, "commit_sha": None})
46
+ write_tree(root, {})
47
+ write_index(root, {})
48
+
49
+
50
+ def _read_json(path: Path, default):
51
+ return json.loads(path.read_text()) if path.exists() else default
52
+
53
+
54
+ def read_config(root: Path) -> dict:
55
+ return _read_json(_meta_dir(root) / "config.json", {})
56
+
57
+
58
+ def write_config(root: Path, data: dict) -> None:
59
+ (_meta_dir(root) / "config.json").write_text(json.dumps(data, indent=2))
60
+
61
+
62
+ def read_head(root: Path) -> dict:
63
+ return _read_json(_meta_dir(root) / "HEAD", {"branch": "main", "commit_sha": None})
64
+
65
+
66
+ def write_head(root: Path, data: dict) -> None:
67
+ (_meta_dir(root) / "HEAD").write_text(json.dumps(data, indent=2))
68
+
69
+
70
+ def read_tree(root: Path) -> Dict[str, str]:
71
+ """The committed file-tree at HEAD ({path: blob_sha})."""
72
+ return _read_json(_meta_dir(root) / "tree.json", {})
73
+
74
+
75
+ def write_tree(root: Path, tree: Dict[str, str]) -> None:
76
+ (_meta_dir(root) / "tree.json").write_text(json.dumps(tree, indent=2, sort_keys=True))
77
+
78
+
79
+ def read_index(root: Path) -> Dict[str, str]:
80
+ """Staged paths ({path: blob_sha})."""
81
+ return _read_json(_meta_dir(root) / "index.json", {})
82
+
83
+
84
+ def write_index(root: Path, index: Dict[str, str]) -> None:
85
+ (_meta_dir(root) / "index.json").write_text(json.dumps(index, indent=2, sort_keys=True))
86
+
87
+
88
+ # ---------- working tree ----------
89
+ def hash_bytes(data: bytes) -> str:
90
+ return hashlib.sha256(data).hexdigest()
91
+
92
+
93
+ def scan_working_tree(root: Path) -> Dict[str, str]:
94
+ """Map every tracked working file (relative posix path) -> content sha."""
95
+ tree: Dict[str, str] = {}
96
+ root = root.resolve()
97
+ for dirpath, dirnames, filenames in os.walk(root):
98
+ dirnames[:] = [d for d in dirnames if d not in _IGNORE_DIRS]
99
+ for fn in filenames:
100
+ full = Path(dirpath) / fn
101
+ rel = full.relative_to(root).as_posix()
102
+ try:
103
+ tree[rel] = hash_bytes(full.read_bytes())
104
+ except OSError:
105
+ continue
106
+ return tree
107
+
108
+
109
+ def diff_trees(base: Dict[str, str], target: Dict[str, str]):
110
+ """Return (added, modified, deleted) paths comparing base -> target."""
111
+ base_keys, target_keys = set(base), set(target)
112
+ added = sorted(target_keys - base_keys)
113
+ deleted = sorted(base_keys - target_keys)
114
+ modified = sorted(k for k in (base_keys & target_keys) if base[k] != target[k])
115
+ return added, modified, deleted
116
+
117
+
118
+ def write_working_file(root: Path, rel_path: str, data: bytes) -> None:
119
+ dest = root / rel_path
120
+ dest.parent.mkdir(parents=True, exist_ok=True)
121
+ dest.write_bytes(data)
@@ -0,0 +1,266 @@
1
+ """
2
+ shapetrack — ShapeTrack's CLI. Your command, real git underneath, talking to the
3
+ ShapeTrack engine. Commands stay identical; internally we run standard git.
4
+
5
+ Run via: python -m cli <command>
6
+ Commands: login, whoami, clone, init, status, add, commit, push, pull, branch, checkout, logout
7
+ """
8
+ import json
9
+ import subprocess
10
+ from pathlib import Path
11
+ from typing import List, Optional
12
+
13
+ import httpx
14
+ import typer
15
+
16
+ from . import local
17
+
18
+ app = typer.Typer(add_completion=False, help="ShappeTrack CLI — one app token, git-powered. The engine stays invisible.")
19
+
20
+ # Point the CLI at the ShappeTrack APP. You log in with ONE app token (made in the
21
+ # web app: Settings → Access Tokens). The app bridges it to git access on the engine,
22
+ # so you never touch the engine directly.
23
+ DEFAULT_SERVER = "http://127.0.0.1:8000"
24
+
25
+
26
+ # ---------- helpers ----------
27
+ def _fail(msg: str):
28
+ typer.secho(f"error: {msg}", fg=typer.colors.RED)
29
+ raise typer.Exit(code=1)
30
+
31
+
32
+ def _creds() -> dict:
33
+ c = local.load_credentials()
34
+ if not c or not c.get("token"):
35
+ _fail("not logged in. Run `shapetrack login` first.")
36
+ return c
37
+
38
+
39
+ def _save_creds(server: str, token: str, username: str):
40
+ """Store ONLY the public app URL + the user's app token. The engine URL/token
41
+ are never handed to the client anymore — git auths to the public proxy with the
42
+ app token, and the proxy swaps in the engine token server-side."""
43
+ local.CRED_PATH.parent.mkdir(parents=True, exist_ok=True)
44
+ local.CRED_PATH.write_text(json.dumps({
45
+ "server_url": server.rstrip("/"), # the ShappeTrack app (REST/chat + git proxy)
46
+ "token": token, # your app token (shp_…)
47
+ "username": username,
48
+ }, indent=2))
49
+
50
+
51
+ def _host(server: str) -> str:
52
+ return server.split("://", 1)[-1].rstrip("/")
53
+
54
+
55
+ def _clean_url(creds: dict, owner: str, name: str) -> str:
56
+ """Public, slug-based clone/push URL through the ShappeTrack proxy — the engine
57
+ host/owner never appear. Auth comes from the git credential store (set at login).
58
+ `name` is the repo slug; owner is unused (the proxy resolves it internally)."""
59
+ return f"{creds['server_url'].rstrip('/')}/repos/{name}"
60
+
61
+
62
+ def _setup_git_credentials(server_url: str, app_token: str, username: str):
63
+ """Teach git to authenticate to the ShappeTrack PUBLIC proxy using the user's
64
+ app token. After this, the IDE (native git), the terminal, and this CLI all
65
+ auth automatically — the engine is never contacted directly and the user never
66
+ handles the engine token. Scoped to the public host so other creds are untouched."""
67
+ from urllib.parse import urlsplit
68
+ parts = urlsplit(server_url)
69
+ scheme = parts.scheme or "https"
70
+ host = parts.netloc or _host(server_url)
71
+ subprocess.run(["git", "config", "--global", f"credential.{scheme}://{host}.helper", "store"],
72
+ text=True, capture_output=True)
73
+ payload = (f"protocol={scheme}\nhost={host}\n"
74
+ f"username={username or 'shappetrack'}\npassword={app_token}\n\n")
75
+ subprocess.run(["git", "credential", "approve"], input=payload, text=True, capture_output=True)
76
+
77
+
78
+ def _split_repo(repo: str, default_owner: str):
79
+ """'owner/name' -> (owner, name); 'name' -> (default_owner, name)."""
80
+ if "/" in repo:
81
+ owner, name = repo.split("/", 1)
82
+ return owner, name
83
+ return default_owner, repo
84
+
85
+
86
+ def _git(*args: str, cwd: Optional[str] = None, check: bool = True) -> subprocess.CompletedProcess:
87
+ proc = subprocess.run(["git", *args], cwd=cwd, text=True, capture_output=True)
88
+ if check and proc.returncode != 0:
89
+ _fail((proc.stderr or proc.stdout or f"git {' '.join(args)} failed").strip())
90
+ return proc
91
+
92
+
93
+ def _redact(url: str) -> str:
94
+ # hide the token when printing a remote URL
95
+ import re
96
+ return re.sub(r"://[^@/]+@", "://<token>@", url)
97
+
98
+
99
+ # ---------- commands ----------
100
+ @app.command()
101
+ def login(server: str = typer.Option(DEFAULT_SERVER, help="ShappeTrack app URL"),
102
+ token: Optional[str] = typer.Option(None, "--token", help="Your ShappeTrack app token (shp_…)")):
103
+ """Log in with ONE ShappeTrack app token (made in the web app: Settings → Access Tokens).
104
+
105
+ The app bridges it to git access on the engine automatically — you never touch the engine."""
106
+ if not token:
107
+ token = typer.prompt("ShappeTrack app token (shp_…)", hide_input=True)
108
+ server = server.rstrip("/")
109
+ try:
110
+ r = httpx.post(f"{server}/api/v1/cli/bootstrap",
111
+ headers={"Authorization": f"Bearer {token}"}, timeout=15)
112
+ except httpx.HTTPError as e:
113
+ _fail(f"could not reach ShappeTrack at {server} — {e}")
114
+ if r.status_code == 401:
115
+ _fail("login failed — invalid app token. Make one in the web app: Settings → Access Tokens.")
116
+ if r.status_code == 409:
117
+ _fail(r.json().get("detail", "git access not linked — log into the web app once, then retry."))
118
+ if r.status_code != 200:
119
+ _fail(f"login failed ({r.status_code}): {r.text[:200]}")
120
+ data = r.json()
121
+ username = data["username"]
122
+ _save_creds(server, token, username)
123
+ # git auths to the PUBLIC proxy with the app token; the engine stays invisible.
124
+ _setup_git_credentials(server, token, username)
125
+ typer.secho(f"Logged in as {username}", fg=typer.colors.GREEN)
126
+ typer.echo("git configured — the IDE, terminal, and CLI can now clone/push ShappeTrack repos.")
127
+
128
+
129
+ @app.command()
130
+ def whoami():
131
+ """Show the logged-in user."""
132
+ creds = _creds()
133
+ typer.echo(f"{creds.get('username')} @ {creds.get('server_url')}")
134
+
135
+
136
+ @app.command()
137
+ def logout():
138
+ """Forget stored credentials."""
139
+ if local.CRED_PATH.exists():
140
+ local.CRED_PATH.unlink()
141
+ typer.secho("Logged out.", fg=typer.colors.GREEN)
142
+
143
+
144
+ @app.command("setup-git")
145
+ def setup_git():
146
+ """(Re)configure git to authenticate to the engine — for the IDE / terminal git."""
147
+ creds = _creds()
148
+ _setup_git_credentials(creds["server_url"], creds["token"], creds["username"])
149
+ typer.secho(f"git configured for {creds['username']} (engine access bridged automatically).",
150
+ fg=typer.colors.GREEN)
151
+ typer.echo("Now the UniversalCode IDE can Clone/Push ShappeTrack repos with no token prompts.")
152
+
153
+
154
+ @app.command()
155
+ def clone(repo: str = typer.Argument(..., help="repo name, or owner/repo"),
156
+ directory: Optional[str] = typer.Argument(None)):
157
+ """Clone a ShapeTrack repository (real git clone under the hood)."""
158
+ creds = _creds()
159
+ owner, name = _split_repo(repo, creds["username"])
160
+ dest = directory or name
161
+ url = _clean_url(creds, owner, name)
162
+ typer.echo(f"Cloning {owner}/{name} …")
163
+ _git("clone", url, dest)
164
+ typer.secho(f"Cloned into {dest}", fg=typer.colors.GREEN)
165
+
166
+
167
+ @app.command()
168
+ def init(name: str,
169
+ private: bool = typer.Option(True, help="Create the repo as private"),
170
+ owner: Optional[str] = typer.Option(None, help="Org owner (defaults to you)")):
171
+ """Create a new ShapeTrack repository and link the current directory to it."""
172
+ creds = _creds()
173
+ # Create via the ShappeTrack APP API (not the engine) — the app owns repo
174
+ # creation and returns the slug; the engine stays invisible.
175
+ headers = {"Authorization": f"Bearer {creds['token']}"}
176
+ endpoint = f"{creds['server_url'].rstrip('/')}/api/v1/repositories"
177
+ r = httpx.post(endpoint, headers=headers,
178
+ json={"name": name, "is_private": private}, timeout=20)
179
+ if r.status_code not in (200, 201):
180
+ _fail(f"could not create repo: {r.text[:200]}")
181
+ slug = (r.json() or {}).get("slug") or name
182
+
183
+ root = Path.cwd()
184
+ if not (root / ".git").exists():
185
+ _git("init", "-b", "main", cwd=str(root))
186
+ url = f"{creds['server_url'].rstrip('/')}/repos/{slug}"
187
+ # set or replace origin
188
+ if _git("remote", cwd=str(root)).stdout.find("origin") >= 0:
189
+ _git("remote", "set-url", "origin", url, cwd=str(root))
190
+ else:
191
+ _git("remote", "add", "origin", url, cwd=str(root))
192
+ typer.secho(f"Initialized '{owner}/{name}'. Remote 'origin' set ({_redact(url)}).", fg=typer.colors.GREEN)
193
+ typer.echo("Next: shapetrack add . && shapetrack commit -m \"#MT-100 first\" && shapetrack push")
194
+
195
+
196
+ @app.command()
197
+ def status():
198
+ """Show working-tree status (git status)."""
199
+ typer.echo(_git("status", "-sb").stdout.rstrip())
200
+
201
+
202
+ @app.command()
203
+ def add(paths: List[str] = typer.Argument(None, help="Files to stage, or '.' for all")):
204
+ """Stage files for the next commit (git add)."""
205
+ _git("add", *(paths or ["."]))
206
+ typer.secho("Staged.", fg=typer.colors.GREEN)
207
+
208
+
209
+ @app.command()
210
+ def commit(message: str = typer.Option(..., "-m", "--message"),
211
+ task: Optional[str] = typer.Option(None, "--task", help="Task id, e.g. MT-104 (added to the message)")):
212
+ """Commit staged changes (git commit). A --task id is appended as #MT-… for linking."""
213
+ msg = message
214
+ if task:
215
+ tag = task if task.startswith("#") else f"#{task}"
216
+ if tag not in msg:
217
+ msg = f"{msg} {tag}"
218
+ proc = _git("commit", "-m", msg, check=False)
219
+ out = (proc.stdout + proc.stderr).strip()
220
+ if proc.returncode != 0:
221
+ _fail(out or "nothing to commit.")
222
+ typer.secho(out, fg=typer.colors.GREEN)
223
+
224
+
225
+ @app.command()
226
+ def push(branch: Optional[str] = typer.Argument(None)):
227
+ """Push commits to the engine (git push)."""
228
+ cur = _git("rev-parse", "--abbrev-ref", "HEAD").stdout.strip()
229
+ target = branch or cur
230
+ proc = _git("push", "-u", "origin", target, check=False)
231
+ out = (proc.stdout + proc.stderr).strip()
232
+ if proc.returncode != 0:
233
+ _fail(_redact(out))
234
+ typer.secho(_redact(out) or f"Pushed {target}.", fg=typer.colors.GREEN)
235
+
236
+
237
+ @app.command()
238
+ def pull(branch: Optional[str] = typer.Argument(None)):
239
+ """Pull the latest from the engine (git pull)."""
240
+ proc = _git("pull", "origin", *( [branch] if branch else [] ), check=False)
241
+ out = (proc.stdout + proc.stderr).strip()
242
+ if proc.returncode != 0:
243
+ _fail(_redact(out))
244
+ typer.secho(_redact(out), fg=typer.colors.GREEN)
245
+
246
+
247
+ @app.command()
248
+ def branch(name: Optional[str] = typer.Argument(None, help="Create+switch to a branch; omit to list"),
249
+ switch: bool = typer.Option(True, help="Switch to the new branch")):
250
+ """Create a branch (or list branches)."""
251
+ if not name:
252
+ typer.echo(_git("branch", "--all").stdout.rstrip())
253
+ return
254
+ _git("checkout", "-b", name) if switch else _git("branch", name)
255
+ typer.secho(f"Branch '{name}'" + (" (switched)" if switch else ""), fg=typer.colors.GREEN)
256
+
257
+
258
+ @app.command()
259
+ def checkout(name: str):
260
+ """Switch branches (git checkout)."""
261
+ _git("checkout", name)
262
+ typer.secho(f"Switched to '{name}'.", fg=typer.colors.GREEN)
263
+
264
+
265
+ if __name__ == "__main__":
266
+ app()
@@ -0,0 +1,84 @@
1
+ Metadata-Version: 2.4
2
+ Name: shappetrack
3
+ Version: 1.0.0
4
+ Summary: ShappeTrack CLI — clone, commit, and push ShappeTrack repositories with one app token. The git engine stays invisible.
5
+ Project-URL: Homepage, https://track.shappe.ai
6
+ Author: ShappeTrack
7
+ License: Proprietary
8
+ Keywords: cli,git,shappetrack,vcs
9
+ Classifier: Environment :: Console
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Topic :: Software Development :: Version Control :: Git
13
+ Requires-Python: >=3.9
14
+ Requires-Dist: httpx==0.26.0
15
+ Requires-Dist: typer<1.0,>=0.12
16
+ Description-Content-Type: text/markdown
17
+
18
+ # ShappeTrack CLI
19
+
20
+ Clone, commit, and push ShappeTrack repositories with **one app token**. Real
21
+ `git` runs under the hood; the git engine is never exposed.
22
+
23
+ ```
24
+ shappetrack login --server https://track-api.shappe.ai/api/v1
25
+ shappetrack clone my-repo
26
+ ```
27
+
28
+ ## Install
29
+
30
+ The CLI is a small Python package (`shappetrack-cli`). The recommended way is
31
+ [`pipx`](https://pipx.pypa.io/), which installs it in an isolated environment so
32
+ it never clashes with your other Python tools.
33
+
34
+ ### For end users (once published to PyPI)
35
+
36
+ ```bash
37
+ pipx install shappetrack
38
+ ```
39
+
40
+ ### From source (internal / development)
41
+
42
+ From the directory containing `pyproject.toml`:
43
+
44
+ ```bash
45
+ pipx install . # isolated, recommended
46
+ # or
47
+ pip install . # into the current environment
48
+ ```
49
+
50
+ Requires **Python 3.9+** and **git** on your PATH (the CLI runs real git under the hood).
51
+
52
+ ## Usage
53
+
54
+ 1. Create an app token in the web app: **Settings → Access Tokens** (starts with `shp_`).
55
+ 2. Log in once — this configures git credentials automatically:
56
+
57
+ ```bash
58
+ shappetrack login --server https://track-api.shappe.ai/api/v1
59
+ ```
60
+
61
+ 3. Then use the familiar commands (they run standard git underneath):
62
+
63
+ ```bash
64
+ shappetrack clone my-repo
65
+ shappetrack status
66
+ shappetrack add .
67
+ shappetrack commit -m "message" --task MT-101
68
+ shappetrack push
69
+ shappetrack pull
70
+ shappetrack branch feature-x
71
+ ```
72
+
73
+ ## How it works
74
+
75
+ `shappetrack clone my-repo` runs `git clone https://track-api.shappe.ai/repos/my-repo`
76
+ using the app token you logged in with. The backend proxies git traffic to the
77
+ internal engine — you never see or handle the engine, its host, or its token.
78
+
79
+ You can also clone with plain git directly (no CLI needed):
80
+
81
+ ```bash
82
+ git clone https://track-api.shappe.ai/repos/my-repo
83
+ # username: your ShappeTrack username, password: your shp_ app token
84
+ ```
@@ -0,0 +1,11 @@
1
+ shappetrack/README.md,sha256=6nasLIP4ZsJJ_kzwB2B1KBy8ub7qz_PkU6SNovHz-qw,1853
2
+ shappetrack/RELEASING.md,sha256=FtUgoA8NmBbGRGUuCVGlNVHVZYQ9Qlp0XCVfZMumlkw,2655
3
+ shappetrack/__init__.py,sha256=1Xgjp1irKJHStHO7QX8HbAPCVWV087Br3Tq4NtVRnI0,274
4
+ shappetrack/__main__.py,sha256=tJk1nRDKqMbRPSvlCJrlQ96cDbV8GKgCBmZW63EH-58,70
5
+ shappetrack/__pyinstaller_entry.py,sha256=clO1Qsh83eVRw4KkvPJWNbty-unLdT9r54lDsLvd4V4,352
6
+ shappetrack/local.py,sha256=rrmNJJZMaE0OZL7zTqGL5QJtPIEP-enWJMyPrNLgf9Q,4136
7
+ shappetrack/shapetrack.py,sha256=jDTZQhcN9BYdxQ_ApmMzZ--Wo-gwrsGAHdrHT_Fn7cI,11070
8
+ shappetrack-1.0.0.dist-info/METADATA,sha256=2QNpIuFe05wYKgKDv3Sg887TSlXGWctmFq_BKqhS38M,2480
9
+ shappetrack-1.0.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
10
+ shappetrack-1.0.0.dist-info/entry_points.txt,sha256=tEuHE3fitA1AIPHh6W7jzKfotIlHaBFDWL3R8o05ZsM,59
11
+ shappetrack-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ shappetrack = shappetrack.shapetrack:app