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 +67 -0
- shappetrack/RELEASING.md +89 -0
- shappetrack/__init__.py +7 -0
- shappetrack/__main__.py +4 -0
- shappetrack/__pyinstaller_entry.py +10 -0
- shappetrack/local.py +121 -0
- shappetrack/shapetrack.py +266 -0
- shappetrack-1.0.0.dist-info/METADATA +84 -0
- shappetrack-1.0.0.dist-info/RECORD +11 -0
- shappetrack-1.0.0.dist-info/WHEEL +4 -0
- shappetrack-1.0.0.dist-info/entry_points.txt +2 -0
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
|
+
```
|
shappetrack/RELEASING.md
ADDED
|
@@ -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 |
|
shappetrack/__init__.py
ADDED
|
@@ -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"
|
shappetrack/__main__.py
ADDED
|
@@ -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,,
|