jpsync 1.0.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.
Files changed (64) hide show
  1. jpsync-1.0.0/.gitignore +58 -0
  2. jpsync-1.0.0/CHANGELOG.md +113 -0
  3. jpsync-1.0.0/LICENSE +21 -0
  4. jpsync-1.0.0/PKG-INFO +406 -0
  5. jpsync-1.0.0/README.md +370 -0
  6. jpsync-1.0.0/docs/architecture.md +60 -0
  7. jpsync-1.0.0/docs/commands.md +170 -0
  8. jpsync-1.0.0/docs/security.md +56 -0
  9. jpsync-1.0.0/docs/vscode-remote-cwd.md +216 -0
  10. jpsync-1.0.0/pyproject.toml +93 -0
  11. jpsync-1.0.0/src/jp/__init__.py +23 -0
  12. jpsync-1.0.0/src/jp/__main__.py +10 -0
  13. jpsync-1.0.0/src/jp/api.py +538 -0
  14. jpsync-1.0.0/src/jp/cli.py +91 -0
  15. jpsync-1.0.0/src/jp/commands/__init__.py +39 -0
  16. jpsync-1.0.0/src/jp/commands/_context.py +99 -0
  17. jpsync-1.0.0/src/jp/commands/_mirror.py +100 -0
  18. jpsync-1.0.0/src/jp/commands/_report.py +47 -0
  19. jpsync-1.0.0/src/jp/commands/clone.py +97 -0
  20. jpsync-1.0.0/src/jp/commands/config_cmd.py +132 -0
  21. jpsync-1.0.0/src/jp/commands/diff.py +93 -0
  22. jpsync-1.0.0/src/jp/commands/doctor.py +103 -0
  23. jpsync-1.0.0/src/jp/commands/ignore_cmd.py +41 -0
  24. jpsync-1.0.0/src/jp/commands/init.py +66 -0
  25. jpsync-1.0.0/src/jp/commands/kernel.py +238 -0
  26. jpsync-1.0.0/src/jp/commands/login.py +137 -0
  27. jpsync-1.0.0/src/jp/commands/ls.py +40 -0
  28. jpsync-1.0.0/src/jp/commands/pull.py +58 -0
  29. jpsync-1.0.0/src/jp/commands/push.py +58 -0
  30. jpsync-1.0.0/src/jp/commands/rm.py +204 -0
  31. jpsync-1.0.0/src/jp/commands/status.py +69 -0
  32. jpsync-1.0.0/src/jp/commands/update.py +210 -0
  33. jpsync-1.0.0/src/jp/commands/version.py +18 -0
  34. jpsync-1.0.0/src/jp/config.py +246 -0
  35. jpsync-1.0.0/src/jp/credentials.py +259 -0
  36. jpsync-1.0.0/src/jp/errors.py +108 -0
  37. jpsync-1.0.0/src/jp/ignore.py +141 -0
  38. jpsync-1.0.0/src/jp/index.py +121 -0
  39. jpsync-1.0.0/src/jp/paths.py +427 -0
  40. jpsync-1.0.0/src/jp/settings_schema.py +86 -0
  41. jpsync-1.0.0/src/jp/sync.py +594 -0
  42. jpsync-1.0.0/src/jp/tui.py +533 -0
  43. jpsync-1.0.0/src/jp/ui.py +184 -0
  44. jpsync-1.0.0/src/jp/urls.py +103 -0
  45. jpsync-1.0.0/tests/conftest.py +273 -0
  46. jpsync-1.0.0/tests/test_api.py +306 -0
  47. jpsync-1.0.0/tests/test_cli.py +106 -0
  48. jpsync-1.0.0/tests/test_clone_init.py +66 -0
  49. jpsync-1.0.0/tests/test_config.py +143 -0
  50. jpsync-1.0.0/tests/test_credentials.py +101 -0
  51. jpsync-1.0.0/tests/test_doctor.py +60 -0
  52. jpsync-1.0.0/tests/test_gitignore_guard.py +49 -0
  53. jpsync-1.0.0/tests/test_index_ignore.py +74 -0
  54. jpsync-1.0.0/tests/test_kernel.py +132 -0
  55. jpsync-1.0.0/tests/test_login.py +100 -0
  56. jpsync-1.0.0/tests/test_mirror.py +111 -0
  57. jpsync-1.0.0/tests/test_paths.py +293 -0
  58. jpsync-1.0.0/tests/test_redact.py +100 -0
  59. jpsync-1.0.0/tests/test_rm.py +136 -0
  60. jpsync-1.0.0/tests/test_sync_pull.py +149 -0
  61. jpsync-1.0.0/tests/test_sync_push.py +216 -0
  62. jpsync-1.0.0/tests/test_tui.py +122 -0
  63. jpsync-1.0.0/tests/test_update.py +47 -0
  64. jpsync-1.0.0/tests/test_urls.py +95 -0
@@ -0,0 +1,58 @@
1
+ # Local agent / editor state
2
+ .claude/
3
+
4
+ # Byte-compiled / optimized / caches
5
+ __pycache__/
6
+ *.py[cod]
7
+ *.egg-info/
8
+ .eggs/
9
+ *.egg
10
+
11
+ # Build artifacts
12
+ build/
13
+ dist/
14
+ *.pyz
15
+ *.spec
16
+
17
+ # Distribution binaries produced by PyInstaller (anchored to repo root so the
18
+ # `src/jp/` package directory is NEVER ignored)
19
+ /jp
20
+ /jp.exe
21
+
22
+ # Virtual environments
23
+ .venv/
24
+ venv/
25
+ env/
26
+ ENV/
27
+
28
+ # Tooling caches
29
+ .pytest_cache/
30
+ .ruff_cache/
31
+ .mypy_cache/
32
+ .coverage
33
+ htmlcov/
34
+ .tox/
35
+
36
+ # Editors / OS
37
+ .DS_Store
38
+ *.swp
39
+ .idea/
40
+ .vscode/
41
+ .serena/
42
+
43
+ # Secrets / credentials -- NEVER commit a token
44
+ *.token
45
+ *token*
46
+ .jupyter_token
47
+ .env
48
+ .env.*
49
+
50
+ # jp workspace metadata (created in a user's synced folder, never in this repo)
51
+ .jp/
52
+
53
+ # Internal research notes and scratch (kept locally, not published)
54
+ research/
55
+ refine_review_workflow.js
56
+
57
+ # uv lockfile (project is zero-dependency; not tracked)
58
+ uv.lock
@@ -0,0 +1,113 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [1.0.0](https://github.com/pehqge/jpsync/compare/v0.3.1...v1.0.0) (2026-05-31)
9
+
10
+
11
+ ### Miscellaneous Chores
12
+
13
+ * rebrand PyPI package and repo to jpsync ([a26db5c](https://github.com/pehqge/jpsync/commit/a26db5c4981f462e5695d7432e003426dac694eb))
14
+
15
+ ## [0.3.1](https://github.com/pehqge/jpsync/compare/v0.3.0...v0.3.1) (2026-05-31)
16
+
17
+
18
+ ### Bug Fixes
19
+
20
+ * **readme:** use shields static/v1 badge so release-please can't eat the color ([7c9377b](https://github.com/pehqge/jpsync/commit/7c9377b860b5f75942a93c662b7bdab5b7afa83f))
21
+ * **update:** detect editable installs instead of faking a successful upgrade ([d12722b](https://github.com/pehqge/jpsync/commit/d12722b47d9ba6192198c1062e01502f4d87be22))
22
+
23
+ ## [0.3.0](https://github.com/pehqge/jpsync/compare/v0.2.0...v0.3.0) (2026-05-31)
24
+
25
+
26
+ ### Features
27
+
28
+ * **kernel:** add --link, fix docs, print privacy verdict ([13d42ca](https://github.com/pehqge/jpsync/commit/13d42cab82df2463d444c5387da59609e8a84e12))
29
+ * **kernel:** add `jp kernel` to fix VS Code remote-kernel cwd ([7ba887c](https://github.com/pehqge/jpsync/commit/7ba887c782d7cdf341e262fbd398609d8e7d0864))
30
+
31
+
32
+ ### Bug Fixes
33
+
34
+ * **credentials:** skip world-readable warning on Windows ([44fa06a](https://github.com/pehqge/jpsync/commit/44fa06a4af1b9f3983bfaad12e1a9380257f36c9))
35
+ * **readme:** use a static release badge auto-bumped by release-please ([1176d07](https://github.com/pehqge/jpsync/commit/1176d07d4885562db9dd2f4fb9ccfab9128a97ee))
36
+ * **tests:** isolate Windows home dir and simplify release badge ([9eeb6ac](https://github.com/pehqge/jpsync/commit/9eeb6acbd7931ee30384b6c74dd9c3214805e425))
37
+ * **tests:** keep USERPROFILE comment within ruff line-length ([86f3d68](https://github.com/pehqge/jpsync/commit/86f3d68fea3614e930a1d06278ba11017f8d02bb))
38
+
39
+
40
+ ### Documentation
41
+
42
+ * fix jp login flow to match actual behavior ([c378c4e](https://github.com/pehqge/jpsync/commit/c378c4e7538632155a17fd1d70a28901fe7fac40))
43
+ * fix two more stale README details ([1fefd67](https://github.com/pehqge/jpsync/commit/1fefd6719a35457f374ffc33d7ec3ad244910781))
44
+ * **readme:** add FAQ entry for the VS Code remote-kernel workflow ([d4e0436](https://github.com/pehqge/jpsync/commit/d4e043644df21357d39156964954a164c68d3cfa))
45
+
46
+ ## [0.2.0](https://github.com/pehqge/jpsync/compare/v0.1.0...v0.2.0) (2026-05-31)
47
+
48
+
49
+ ### Features
50
+
51
+ * **config:** activate color setting and add dotfile "protect" policy ([38c9ac7](https://github.com/pehqge/jpsync/commit/38c9ac7ebe963156e0eb92015ec98c147d617a00))
52
+ * named credentials with interactive jp login ([ec290db](https://github.com/pehqge/jpsync/commit/ec290db9233c427a7150ecebb3cfad836d4b0e68))
53
+
54
+
55
+ ### Bug Fixes
56
+
57
+ * add top-level release-type to release-please config ([695d456](https://github.com/pehqge/jpsync/commit/695d456d42511a371a33bad1d3e327af61c45b88))
58
+ * tag releases as vX.Y.Z without component prefix ([c2e9794](https://github.com/pehqge/jpsync/commit/c2e9794065feb93f13afa24a947f5db26bae820c))
59
+ * **tui:** repair interactive settings render and add navigation hints ([0e6b673](https://github.com/pehqge/jpsync/commit/0e6b673ab1bab1ad4e396679b1874b1284b93cf7))
60
+
61
+
62
+ ### Documentation
63
+
64
+ * replace real JupyterHub URL with generic example placeholder ([54612b7](https://github.com/pehqge/jpsync/commit/54612b7e269d8e8f2c1e4757c6427aeaec625314))
65
+
66
+ ## [Unreleased]
67
+
68
+ ### Added
69
+
70
+ - Named credentials: `jp login` is now interactive — it explains how to get a
71
+ JupyterHub API token, reads it with hidden input, asks for a name, and saves it
72
+ **globally** (`~/.config/jp/`) or **locally** (a workspace's `.jp/`). Token
73
+ values are written to private `600` files and never leave the machine; only the
74
+ credential name is recorded in config. Scriptable via `--name`,
75
+ `--global`/`--local`, `--token-stdin`, `--token-path`, `--force`.
76
+ - `jp clone` / `jp init` select a saved credential automatically when only one
77
+ exists, or prompt to choose when several do (`--credential <name>` to skip the
78
+ prompt); the choice is recorded in the workspace config.
79
+ - New `credential` config key and a `credentials.json` registry per scope.
80
+ - Every `.jp/` workspace now gets a `.jp/.gitignore` (`*`) so a workspace that is
81
+ also a git repo can never commit its local state or a local token file.
82
+
83
+ ### Changed
84
+
85
+ - Token resolution order is now `$JP_TOKEN` → `$JP_TOKEN_FILE` → the workspace's
86
+ saved credential → legacy `token_path` / `~/.config/jp/token` (the legacy paths
87
+ remain supported).
88
+
89
+ ## [0.1.0] - 2026-05-30
90
+
91
+ ### Added
92
+
93
+ - Initial release of `jp` (PyPI package `jpsync`, command `jp`).
94
+ - Git-like commands: `clone`, `pull`, `push`, `status`, `diff`, `init`,
95
+ `config`, `auth`, `version`.
96
+ - Three-way sync model (local / remote / base) with conflict detection.
97
+ - Conflict-safe synchronization: conflicting files are never overwritten
98
+ silently (exit code `6`).
99
+ - Token-based authentication resolved as `--token-file` > `$JP_TOKEN_FILE` /
100
+ `$JUPYTER_TOKEN` > repo config > global config > `~/.jupyter_token`;
101
+ only the token path is stored in config, and the token file is expected to be
102
+ mode `0600`.
103
+ - Path-jail confinement: remote paths are resolved inside the clone root, with
104
+ `..` traversal, absolute paths, and boundary-crossing symlinks rejected.
105
+ - `.jpignore` support plus built-in defaults (`.jp/`, dotfiles, `.git/`,
106
+ `__pycache__/`, `*.pyc`); hidden files opt-in via `allow_hidden`.
107
+ - Pure standard-library HTTP client (`urllib`) for the Jupyter Contents API,
108
+ with retry/backoff on network errors.
109
+ - Distribution via PyPI, a single-file `jp.pyz` zipapp, standalone per-OS
110
+ PyInstaller binaries, and `install.sh` / `install.ps1` installers.
111
+
112
+ [Unreleased]: https://github.com/pehqge/jpsync/compare/v0.1.0...HEAD
113
+ [0.1.0]: https://github.com/pehqge/jpsync/releases/tag/v0.1.0
jpsync-1.0.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Pedro Gimenez
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
jpsync-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,406 @@
1
+ Metadata-Version: 2.4
2
+ Name: jpsync
3
+ Version: 1.0.0
4
+ Summary: A git-like CLI to sync local folders with a remote JupyterHub via the Contents API.
5
+ Project-URL: Homepage, https://github.com/pehqge/jpsync
6
+ Project-URL: Repository, https://github.com/pehqge/jpsync
7
+ Project-URL: Issues, https://github.com/pehqge/jpsync/issues
8
+ Project-URL: Changelog, https://github.com/pehqge/jpsync/blob/main/CHANGELOG.md
9
+ Author-email: Pedro Gimenez <pehqge@gmail.com>
10
+ Maintainer-email: Pedro Gimenez <pehqge@gmail.com>
11
+ License-Expression: MIT
12
+ License-File: LICENSE
13
+ Keywords: cli,contents-api,jupyter,jupyterhub,notebook,sync
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Environment :: Console
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: Intended Audience :: Science/Research
18
+ Classifier: License :: OSI Approved :: MIT License
19
+ Classifier: Operating System :: OS Independent
20
+ Classifier: Programming Language :: Python
21
+ Classifier: Programming Language :: Python :: 3
22
+ Classifier: Programming Language :: Python :: 3 :: Only
23
+ Classifier: Programming Language :: Python :: 3.9
24
+ Classifier: Programming Language :: Python :: 3.10
25
+ Classifier: Programming Language :: Python :: 3.11
26
+ Classifier: Programming Language :: Python :: 3.12
27
+ Classifier: Programming Language :: Python :: 3.13
28
+ Classifier: Topic :: Software Development :: Version Control
29
+ Classifier: Topic :: Utilities
30
+ Requires-Python: >=3.9
31
+ Provides-Extra: dev
32
+ Requires-Dist: mypy>=1.8; extra == 'dev'
33
+ Requires-Dist: pytest>=7; extra == 'dev'
34
+ Requires-Dist: ruff>=0.4; extra == 'dev'
35
+ Description-Content-Type: text/markdown
36
+
37
+ <h1 align="center">jp</h1>
38
+
39
+ <p align="center">
40
+ <em>A git-like CLI to safely sync local folders with a remote JupyterHub — zero dependencies, pure Python.</em>
41
+ </p>
42
+
43
+ <p align="center">
44
+ <a href="https://github.com/pehqge/jpsync/actions/workflows/ci.yml"><img src="https://github.com/pehqge/jpsync/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
45
+ <a href="https://github.com/pehqge/jpsync/releases/latest"><img src="https://img.shields.io/static/v1?label=release&message=v1.0.0&color=blue" alt="Release"></a> <!-- x-release-please-version -->
46
+ <img src="https://img.shields.io/badge/python-3.9%2B-blue" alt="Python 3.9+">
47
+ <a href="https://github.com/pehqge/jpsync/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-yellow" alt="License: MIT"></a>
48
+ <img src="https://img.shields.io/badge/dependencies-zero-brightgreen" alt="Zero dependencies">
49
+ </p>
50
+
51
+ ---
52
+
53
+ `jp` keeps a local folder in sync with a directory on a **JupyterHub** server —
54
+ the way `git` keeps you in sync with a remote. You edit notebooks and scripts on
55
+ your laptop, `jp push` to send them up, run your training on the server's GPUs,
56
+ and `jp pull` the results back down.
57
+
58
+ It talks to the JupyterHub REST API directly, has **zero third-party
59
+ dependencies** (pure Python standard library), and runs anywhere Python 3.9+
60
+ runs — macOS, Windows, Linux.
61
+
62
+ ```console
63
+ $ jp clone https://jupyter.example.com/user/you/lab/tree/your-folder
64
+ cloning your-folder -> ./your-folder
65
+ ✓ clone: 12 transferred
66
+
67
+ $ cd your-folder
68
+ $ # ...edit files locally...
69
+ $ jp push
70
+ push: train.py
71
+ push: data/config.yaml
72
+ ✓ push: 2 transferred, 10 up to date, 0 skipped, 0 conflict(s), 0 failed
73
+ ```
74
+
75
+ ## Why jp?
76
+
77
+ - **Git-like workflow** — `jp clone`, `jp status`, `jp push`, `jp pull`. Same muscle memory.
78
+ - **Safe by default** — on a *shared* research machine, jp never deletes remote files unless you explicitly turn that on, and even then it asks you file-by-file. Conflicts are never silently overwritten.
79
+ - **Zero dependencies** — one install, no dependency hell; ships as a wheel, a single `.pyz`, or a standalone binary.
80
+ - **Cross-platform** — macOS, Windows, Linux; Python 3.9 → 3.13.
81
+
82
+ ---
83
+
84
+ ## Installation
85
+
86
+ > Recommended: install in an isolated environment with **uv** or **pipx** so the
87
+ > `jp` command lands on your `PATH` without touching system Python.
88
+
89
+ **With uv** (fastest):
90
+ ```bash
91
+ uv tool install jpsync
92
+ ```
93
+
94
+ **With pipx**:
95
+ ```bash
96
+ pipx install jpsync
97
+ ```
98
+
99
+ **Straight from GitHub** (before the first PyPI release):
100
+ ```bash
101
+ pipx install "git+https://github.com/pehqge/jpsync"
102
+ # or: uv tool install "git+https://github.com/pehqge/jpsync"
103
+ ```
104
+
105
+ **Install script (macOS / Linux)** — downloads the standalone binary, no Python needed:
106
+ ```bash
107
+ curl -fsSL https://raw.githubusercontent.com/pehqge/jpsync/main/scripts/install.sh | sh
108
+ ```
109
+
110
+ **Install script (Windows, PowerShell)**:
111
+ ```powershell
112
+ powershell -ExecutionPolicy ByPass -c "irm https://raw.githubusercontent.com/pehqge/jpsync/main/scripts/install.ps1 | iex"
113
+ ```
114
+
115
+ **Single file, no install** — grab `jp.pyz` from the
116
+ [latest release](https://github.com/pehqge/jpsync/releases/latest) and run it
117
+ with any Python 3.9+:
118
+ ```bash
119
+ python jp.pyz --help
120
+ ```
121
+
122
+ Verify:
123
+ ```bash
124
+ jp --version
125
+ ```
126
+
127
+ > If `jp: command not found` after a pipx/uv install, run `pipx ensurepath` (or
128
+ > `uv tool update-shell`) and reopen your terminal.
129
+
130
+ ---
131
+
132
+ ## Getting started
133
+
134
+ ### 1. Get your JupyterHub API token
135
+
136
+ `jp` authenticates with a personal API token from your JupyterHub.
137
+
138
+ 1. Open your JupyterHub in a browser and log in (e.g. `https://jupyter.example.com`).
139
+ 2. Go to the **Token** page — usually the **Token** link in the top bar, or
140
+ visit `https://<your-hub>/hub/token` directly.
141
+ 3. Type a note (e.g. `jp`), leave the scopes blank (full access to what *you*
142
+ can already do), and click **Request new API token**.
143
+ 4. **Copy the token now** — JupyterHub shows it only once.
144
+
145
+ > Security: the token is like a password for your account. `jp` stores only the
146
+ > *path* to a token file, never the token value, and never logs or commits it.
147
+
148
+ ### 2. Log in with `jp login`
149
+
150
+ Run `jp login` and follow the prompts. It asks for a short name, walks you
151
+ through getting the token, then you paste it (your input stays **hidden**).
152
+ Credentials are saved **globally** by default (usable from anywhere); pass
153
+ `--local` to keep it only in the current workspace:
154
+
155
+ ```console
156
+ $ jp login
157
+ Name this server/credential (e.g. myserver): myserver
158
+ To get a JupyterHub API token:
159
+ 1. Open your JupyterHub in a browser and log in.
160
+ 2. Go to the Token page (the 'Token' link, or <your-hub>/hub/token).
161
+ 3. Click 'Request new API token' and copy it (it is shown only once).
162
+
163
+ Paste your API token (input hidden):
164
+ ✓ saved global credential 'myserver'
165
+ ```
166
+
167
+ **Everything stays on your machine.** `jp` writes the token to a private file
168
+ (permissions `600`) under `~/.config/jp/` (or the workspace's `.jp/` for a local
169
+ credential) and records only the credential's *name* in config — never the token
170
+ value. The token is never printed, logged, committed, or sent anywhere except as
171
+ the `Authorization` header to your own hub.
172
+
173
+ Run `jp login` again any time to add another server — keep as many credentials as
174
+ you like and pick one when you clone. See [Credentials](#credentials--jp-login).
175
+
176
+ ### 3. Make sure your server is running
177
+
178
+ `jp` talks to your *single-user* server, so it must be started: open JupyterHub
179
+ and, if needed, click **Start My Server**. (`jp doctor` will tell you if it's
180
+ stopped.)
181
+
182
+ ### 4. Clone your folder
183
+
184
+ Copy the URL of the folder from your browser's address bar — the `lab/tree/...`
185
+ URL works directly:
186
+
187
+ ```bash
188
+ jp clone https://jupyter.example.com/user/<you>/lab/tree/your-folder
189
+ cd your-folder
190
+ ```
191
+
192
+ That creates a `your-folder/` folder with a `.jp/` workspace inside (like `.git/`)
193
+ and downloads the remote tree. If you saved more than one credential, `jp` asks
194
+ which one to use; with a single one it just uses it. The choice is remembered in
195
+ the workspace (`jp clone … --credential <name>` to skip the prompt).
196
+
197
+ ### 5. Work like git
198
+
199
+ ```bash
200
+ jp status # what changed, locally vs the server
201
+ jp push # send local changes up
202
+ jp pull # bring remote changes (e.g. training output) down
203
+ ```
204
+
205
+ That's it. From any subdirectory of the workspace, `jp` finds its root
206
+ automatically (it walks up looking for `.jp/`, stopping at your home folder).
207
+
208
+ ---
209
+
210
+ ## Command reference
211
+
212
+ | Command | What it does |
213
+ |---|---|
214
+ | `jp clone <url> [dir]` | Clone a remote Jupyter folder into a new local directory. Accepts a `lab/tree` URL or `--base-url`/`--prefix`. |
215
+ | `jp init <url>` | Turn the current folder into a jp workspace (no download). |
216
+ | `jp login` | Save a named API-token credential (name the server, paste the token; defaults to global; use `--local` for workspace-only). |
217
+ | `jp status` | Show local vs. remote differences. Read-only. |
218
+ | `jp push` | Upload local changes. Additive by default. |
219
+ | `jp pull` | Download remote changes. Additive by default. |
220
+ | `jp diff [path]` | Show file-level differences. |
221
+ | `jp ls [remote-path]` | List a remote directory (no local writes). |
222
+ | `jp config` | Interactive settings editor (see below). Also `config get/set/list`. |
223
+ | `jp ignore [pattern]` | Manage `.jpignore` patterns. |
224
+ | `jp rm <path>` | Delete on the remote — gated, dry-run + typed confirmation. The only deleter. |
225
+ | `jp kernel` | Set up a VS Code remote kernel to run notebooks in the right directory ([guide](docs/vscode-remote-cwd.md)). |
226
+ | `jp doctor` | Diagnose token, connectivity, server status. |
227
+ | `jp update` | Update jp to the latest version. |
228
+ | `jp version` | Print the version (also `jp --version`). |
229
+
230
+ Global flags: `-q/--quiet`, `--no-color`. Every command has `--help`.
231
+
232
+ ### `jp config` — interactive settings
233
+
234
+ Run `jp config` with no arguments in a terminal for a settings screen:
235
+
236
+ ```
237
+ Mirror mode (allow deletes) false
238
+ > Dotfile policy skip
239
+ Colored output auto
240
+ Network timeout (s) 30.0
241
+
242
+ Up/Down move · Space change · i info · / search · Enter save · Esc cancel
243
+ ```
244
+
245
+ - **↑/↓** move · **Space** cycle the value · **i** show help for the selected
246
+ setting · **/** search · **Enter** save · **Esc** cancel.
247
+
248
+ For scripts, the classic forms still work: `jp config list`,
249
+ `jp config get <key>`, `jp config set <key> <value>`.
250
+
251
+ ### Mirror mode (deleting files to match the other side)
252
+
253
+ By default `jp push`/`jp pull` are **additive** — they never delete. If you want
254
+ true mirroring (delete on the remote when you delete locally, and vice-versa),
255
+ turn on **mirror mode**:
256
+
257
+ ```bash
258
+ jp config set mirror true # persist it, or use --mirror for one run
259
+ jp push --mirror # one-off
260
+ ```
261
+
262
+ With mirror on, after the normal sync jp finds files that exist on one side but
263
+ not the other and — **always, before deleting anything** — shows you the list
264
+ and lets you choose, with the arrow keys, which to **keep** and which to
265
+ **delete**:
266
+
267
+ ```
268
+ Mirror mode: 2 file(s) exist on remote but not on the other side.
269
+ Choose which to DELETE on remote. Default is KEEP.
270
+
271
+ > [keep] old_experiment.py
272
+ [keep] scratch.ipynb
273
+
274
+ Up/Down move · Space toggle · a delete-all · n keep-all · Enter confirm · Esc cancel
275
+ ```
276
+
277
+ Nothing is deleted unless you mark it. In a non-interactive shell, mirror
278
+ deletions are refused unless you pass `--yes`. Conflicts (both sides changed) are
279
+ *never* deleted or overwritten.
280
+
281
+ ### Credentials & `jp login`
282
+
283
+ `jp login` is how you give `jp` your JupyterHub API token. It is fully
284
+ interactive and **everything happens locally** — the token never leaves your
285
+ machine and is never printed:
286
+
287
+ - You give the credential a **name** (usually the server, e.g. `myserver`).
288
+ - It shows you how to get a token, then prompts you to paste it with the input
289
+ **hidden** (no echo).
290
+ - The **scope** defaults to **global** (stored in `~/.config/jp/`, usable from
291
+ any directory). Pass `--local` to store the credential only in the current
292
+ workspace's `.jp/`.
293
+ - The token value goes into a private `600` file; only its *name* is recorded in
294
+ the workspace config (`credential` key).
295
+ - A **local** credential lives in the workspace's `.jp/`, and `jp` drops a
296
+ `.jp/.gitignore` (`*`) so that — even if the workspace is also a git repo — git
297
+ ignores the whole `.jp/` directory and the token can never be committed.
298
+
299
+ Save as many as you like — run `jp login` once per server:
300
+
301
+ ```bash
302
+ jp login # interactive: name, paste token; saves globally
303
+ jp login --name myserver --global # scriptable form
304
+ jp login --token-stdin --name lab-gpu --local < token.txt
305
+ ```
306
+
307
+ When you `jp clone` / `jp init`, `jp` reads the credentials available **globally
308
+ and locally**: with one it's used automatically, with several you pick which
309
+ server to use (or pass `--credential <name>`). The choice is saved in the
310
+ workspace so later `push`/`pull` just work.
311
+
312
+ At sync time the token is resolved, in order: `$JP_TOKEN` (a value, for CI) →
313
+ `$JP_TOKEN_FILE` (a path) → the workspace's saved credential → legacy
314
+ `token_path` / `~/.config/jp/token`. `jp` warns if any token file is readable by
315
+ other users.
316
+
317
+ ### Keeping jp up to date
318
+
319
+ ```bash
320
+ jp update # detects pipx / uv / pip and upgrades in place
321
+ jp update --check # just check; don't install
322
+ ```
323
+
324
+ For a standalone binary install, `jp update` prints the one-line reinstall
325
+ command for your OS.
326
+
327
+ ---
328
+
329
+ ## Configuration
330
+
331
+ Each workspace stores its settings in `.jp/config.json` (JSON, never the token
332
+ value). Keys: `base_url`, `prefix`, `credential`, `token_path`, `mirror`,
333
+ `dotfiles`, `color`, `timeout`. See [docs/commands.md](docs/commands.md) and
334
+ [docs/architecture.md](docs/architecture.md).
335
+
336
+ ---
337
+
338
+ ## Security
339
+
340
+ `jp` is built for a **shared** machine where a mistake can destroy someone
341
+ else's research. The guarantees:
342
+
343
+ - **`push`/`pull` never delete** unless you opt into mirror mode — and even then
344
+ jp asks you, file by file, defaulting to keep.
345
+ - **Conflicts are never silently overwritten.** If both sides changed since the
346
+ last sync, jp aborts that file and tells you.
347
+ - **Path-jailing.** Every remote operation is confined to your workspace's
348
+ prefix. The server root and shared spaces (`compartilhado`, `lapix`,
349
+ `shared`, …) are refused outright.
350
+ - **Untrusted server on download.** File names from the server are sanitized
351
+ before anything is written locally (anti path-traversal / Zip-Slip), and
352
+ writes are atomic and never follow a symlink.
353
+ - **Your token never leaks** — stored by path only, sent in the `Authorization`
354
+ header (never a URL), redacted from all output, never committed.
355
+
356
+ Found a vulnerability? See [SECURITY.md](SECURITY.md) — please don't open a
357
+ public issue.
358
+
359
+ ---
360
+
361
+ ## FAQ
362
+
363
+ **Is `jp` related to git?** No — it borrows git's *workflow*, not its internals.
364
+ There's no remote version history on a JupyterHub.
365
+
366
+ **Does it need Jupyter installed locally?** No. Just Python 3.9+; it talks to the
367
+ Hub over HTTPS.
368
+
369
+ **Why won't my `.gitignore` (or any dotfile) upload?** Most JupyterHub servers
370
+ run with `allow_hidden=False`, which rejects creating hidden files (names
371
+ starting with `.`). `jp` detects this and *skips* dotfiles on push, reporting
372
+ them instead of failing — your `.git/`, `.gitignore`, `.env` etc. simply stay
373
+ local (which is usually what you want). A nice side effect: secrets in dotfiles
374
+ never get pushed by accident.
375
+
376
+ **Will it overwrite my work?** Never silently. A conflict aborts that file;
377
+ remote deletes are opt-in (mirror mode) and confirmed file-by-file.
378
+
379
+ **Can I edit notebooks locally in VS Code but run them on the remote GPUs?** Yes —
380
+ that's a core workflow. Sync with jp, then connect VS Code to your remote kernel.
381
+ One catch: a remote kernel starts in the server's home, not your notebook's
382
+ folder, so relative paths fail. Run `jp kernel` once to fix it. The full
383
+ walkthrough (connecting the kernel + the cwd fix) is in
384
+ [docs/vscode-remote-cwd.md](docs/vscode-remote-cwd.md).
385
+
386
+ ## Troubleshooting
387
+
388
+ - **`jp: command not found`** — run `pipx ensurepath` / `uv tool update-shell`, reopen the terminal.
389
+ - **`your JupyterHub server appears to be stopped`** — open the Hub UI and click *Start My Server*.
390
+ - **`authentication failed` / HTTP 403** — your token expired; create a new one and `jp login` again (or update the token file).
391
+ - **A big upload times out** — raise the timeout: `jp config set timeout 120`.
392
+ - **`FileNotFoundError` / relative paths fail in VS Code with a remote kernel** — the kernel starts in the server's home, not your notebook's folder. Run `jp kernel` (see the [VS Code remote-kernel guide](docs/vscode-remote-cwd.md)).
393
+
394
+ Run `jp doctor` for a guided check.
395
+
396
+ ---
397
+
398
+ ## Contributing
399
+
400
+ Contributions welcome — see [CONTRIBUTING.md](CONTRIBUTING.md) and the
401
+ [Code of Conduct](CODE_OF_CONDUCT.md). The project is standard-library only;
402
+ please keep it dependency-free.
403
+
404
+ ## License
405
+
406
+ [MIT](LICENSE) © Pedro Gimenez