gent-cli 5.0.4 → 6.0.1

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.
package/README.md CHANGED
@@ -1,306 +1,641 @@
1
1
  # Gent CLI
2
2
 
3
- ![npm](https://img.shields.io/npm/v/gent-cli)
4
- ![downloads](https://img.shields.io/npm/dw/gent-cli)
5
-
6
- > A modern, Git-like version control CLI with built-in cloud authentication and global user identity management.
7
-
8
- Gent is a lightweight version control system that feels like Git but handles user identity automatically through the cloud. No more configuring `user.name` and `user.email` for every repository.
9
-
10
- ## Highlights
11
-
12
- - **Cloud Authentication** — Login once, work everywhere. JWT-based auth with automatic token refresh.
13
- - **Git-like Experience** — Familiar commands: `init`, `add`, `commit`, `status`, `log`, `branch`, `checkout`, `merge`, `push`, `pull`, `clone`, and more.
14
- - **Zero Configuration** — `gent init` auto-detects your authenticated user profile.
15
- - **Global Identity** — Commits are automatically authored with your cloud profile.
16
- - **Content-Addressable Storage** — SHA-256 object store with zlib compression and deduplication.
17
- - **Smart Diff & Merge** — Line-level LCS diff, three-way merge with conflict detection.
18
- - **Remote Sync** — Push, pull, and clone repositories from the cloud backend.
19
- - **Secure** — Tokens stored with AES encryption in `~/.gent/auth.json`.
20
-
21
- ## Table of Contents
22
-
23
- - [Requirements](#requirements)
24
- - [Installation](#installation)
25
- - [Quickstart](#quickstart)
26
- - [Authentication](#authentication)
27
- - [Commands](#commands)
28
- - [Repository Setup](#repository-setup)
29
- - [Staging & Working Tree](#staging--working-tree)
30
- - [History](#history)
31
- - [Branching & Merging](#branching--merging)
32
- - [Remote & Sync](#remote--sync)
33
- - [Authentication Commands](#authentication-commands)
34
- - [Usage Examples](#usage-examples)
35
- - [Repository Structure](#repository-structure)
36
- - [Configuration](#configuration)
37
- - [Docs](#docs)
38
- - [Troubleshooting](#troubleshooting)
39
- - [Contributing](#contributing)
40
- - [License](#license)
3
+ Gent is a Git-like version control CLI with cloud authentication and remote sync.
4
+
5
+ Global API URL:
6
+
7
+ ```text
8
+ https://gent-api.onrender.com
9
+ ```
10
+
11
+ The CLI is configured in `src/utils/constants.js` to use that deployed API. Do not use a local API URL for normal CLI work.
41
12
 
42
13
  ## Requirements
43
14
 
44
- - Node.js >= 14
15
+ - Node.js 14 or newer
16
+ - Internet access to `https://gent-api.onrender.com`
17
+ - A Gent account, created with `gent register`
45
18
 
46
- ## Installation
19
+ ## Install
20
+
21
+ From npm:
47
22
 
48
23
  ```bash
49
24
  npm install -g gent-cli
25
+ gent --help
50
26
  ```
51
27
 
52
- Run locally without installing:
28
+ From this repository:
53
29
 
54
30
  ```bash
55
31
  cd apps/Cli
56
32
  npm install
33
+ npm link
34
+ gent --help
35
+ ```
36
+
37
+ Run without linking:
38
+
39
+ ```bash
57
40
  node src/index.js --help
58
41
  ```
59
42
 
60
- ## Quickstart
43
+ ## Full Step-by-Step Workflow
44
+
45
+ ### 1. Create an account
46
+
47
+ Interactive:
61
48
 
62
49
  ```bash
63
- # 1) Authenticate
64
- gent login
50
+ gent register
51
+ ```
65
52
 
66
- # 2) Initialize a repo
67
- gent init
53
+ Non-interactive:
68
54
 
69
- # 3) Make a first commit
70
- gent add .
71
- gent commit -m "Initial commit"
55
+ ```bash
56
+ gent register \
57
+ -e user@example.com \
58
+ -p StrongPass123! \
59
+ --password-confirm StrongPass123! \
60
+ --first-name YourFirstName \
61
+ --last-name YourLastName
72
62
  ```
73
63
 
74
- ## Authentication
64
+ After registration, the CLI stores your encrypted auth tokens in:
65
+
66
+ ```text
67
+ ~/.gent/auth.json
68
+ ```
75
69
 
76
- Gent uses a global authentication system. You only need to log in once.
70
+ ### 2. Log in
77
71
 
78
- ### Register a New Account
72
+ Interactive:
79
73
 
80
74
  ```bash
81
- gent register
75
+ gent login
82
76
  ```
83
77
 
84
- ### Login
78
+ Non-interactive:
85
79
 
86
80
  ```bash
87
- gent login
88
- # or with flags
89
- gent login -e user@example.com -p YourPassword
81
+ gent login -e user@example.com -p StrongPass123!
90
82
  ```
91
83
 
92
- ### Check Current User
84
+ ### 3. Confirm the logged-in user
93
85
 
94
86
  ```bash
95
87
  gent whoami
96
88
  ```
97
89
 
98
- ### Logout
90
+ Expected result: your email, name, account ID, joined date, and active status.
91
+
92
+ ### 4. Create a project folder
99
93
 
100
94
  ```bash
101
- gent logout
95
+ mkdir my-project
96
+ cd my-project
102
97
  ```
103
98
 
104
- ## Commands
99
+ ### 5. Initialize a Gent repository
105
100
 
106
- ### Repository Setup
101
+ ```bash
102
+ gent init
103
+ ```
104
+
105
+ This creates:
106
+
107
+ ```text
108
+ .gent/
109
+ .gentignore
110
+ ```
111
+
112
+ The `.gent` directory stores local commits, objects, branches, tags, staging data, and config.
113
+
114
+ ### 6. Create a remote repository
107
115
 
108
- | Command | Description |
109
- |---|---|
110
- | `gent init [-y]` | Initialize a new Gent repository in the current directory. Use `-y` to skip prompts. |
111
- | `gent clone <url> [directory]` | Clone a remote repository from the cloud backend. |
116
+ ```bash
117
+ gent repos --create my-project --description "My first Gent repository"
118
+ ```
112
119
 
113
- ### Staging & Working Tree
120
+ Expected output includes a remote path like:
114
121
 
115
- | Command | Description |
116
- |---|---|
117
- | `gent status [-s]` | Show the working tree status. Use `-s` for short format. |
118
- | `gent add <files...> [-A]` | Add files to the staging area. Use `-A` or `--all` to add all files. |
119
- | `gent rm <files...> [--cached]` | Remove files from the working tree and staging area. Use `--cached` to keep the file on disk. |
120
- | `gent reset [files...] [--hard <hash> \| --soft <hash>]` | Unstage files or reset HEAD to a specific commit. |
121
- | `gent diff [files...] [--staged] [--stat]` | Show changes between working tree, staging area, and commits. |
122
+ ```text
123
+ /api/repos/2/my-project
124
+ ```
122
125
 
123
- ### History
126
+ Keep this path. It is not a local URL. The CLI combines it with the global API URL:
124
127
 
125
- | Command | Description |
126
- |---|---|
127
- | `gent commit [-m <message>] [-a]` | Record changes to the repository. Use `-a` to auto-stage all modified files. |
128
- | `gent log [-n <count>] [--oneline] [--stat]` | Show commit history. Default limit is 10 commits. |
129
- | `gent show [ref] [--no-patch]` | Show commit details and diff. |
130
- | `gent tag [name] [-m <message>] [-d <name>]` | Create, list, or delete tags. |
128
+ ```text
129
+ https://gent-api.onrender.com/api/repos/2/my-project
130
+ ```
131
+
132
+ ### 7. Link the local repo to the remote repo
131
133
 
132
- ### Branching & Merging
134
+ Use the `/api/repos/<owner_id>/<repo_name>` path from the previous command:
133
135
 
134
- | Command | Description |
135
- |---|---|
136
- | `gent branch [name] [-d <name>] [-a]` | List, create, or delete branches. |
137
- | `gent checkout <branch> [-b]` | Switch branches. Use `-b` to create and switch to a new branch. |
138
- | `gent merge <branch> [-m <message>]` | Merge a branch into the current branch using a three-way smart merge. |
139
- | `gent stash [pop \| list \| drop \| apply] [-m <message>] [-i <index>]` | Stash working tree changes. |
136
+ ```bash
137
+ gent remote add origin /api/repos/2/my-project
138
+ ```
140
139
 
141
- ### Remote & Sync
140
+ If the current folder is not initialized yet, `gent remote add` initializes `.gent` first, then adds the remote. You can still run `gent init` yourself before this step if you prefer the explicit flow.
142
141
 
143
- | Command | Description |
144
- |---|---|
145
- | `gent remote [add \| remove \| set-url] [args...] [-v]` | Manage remote connections. |
146
- | `gent push [remote] [branch] [-f]` | Push local commits to the remote. Use `-f` to force push. |
147
- | `gent pull [remote] [branch]` | Pull and merge remote commits into the current branch. |
142
+ Check it:
148
143
 
149
- ### Authentication Commands
144
+ ```bash
145
+ gent remote -v
146
+ ```
150
147
 
151
- | Command | Description |
152
- |---|---|
153
- | `gent register` | Create a new user account interactively. |
154
- | `gent login [-e <email>] [-p <password>]` | Log in to your account. |
155
- | `gent logout` | Log out and clear stored tokens. |
156
- | `gent whoami` | Display the currently logged-in user. |
148
+ Expected output:
157
149
 
158
- ## Usage Examples
150
+ ```text
151
+ origin -> /api/repos/2/my-project
152
+ ```
159
153
 
160
- ### Initialize a Repository
154
+ ### 8. Create files
161
155
 
162
156
  ```bash
163
- gent init
164
- # Output: Initialized empty Gent repository in /path/to/project
157
+ echo "Hello Gent" > README.md
158
+ mkdir src
159
+ echo "console.log('hello')" > src/index.js
165
160
  ```
166
161
 
167
- ### Stage and Commit
162
+ ### 9. Check status
163
+
164
+ ```bash
165
+ gent status
166
+ ```
167
+
168
+ Expected result: untracked files.
169
+
170
+ ### 10. Stage files
171
+
172
+ Stage specific files:
173
+
174
+ ```bash
175
+ gent add README.md src/index.js
176
+ ```
177
+
178
+ Or stage everything:
168
179
 
169
180
  ```bash
170
181
  gent add .
182
+ ```
183
+
184
+ ### 11. Review staged changes
185
+
186
+ ```bash
187
+ gent diff --staged
188
+ ```
189
+
190
+ Short summary:
191
+
192
+ ```bash
193
+ gent diff --staged --stat
194
+ ```
195
+
196
+ ### 12. Commit
197
+
198
+ ```bash
171
199
  gent commit -m "Initial commit"
172
200
  ```
173
201
 
174
- ### Work with Branches
202
+ Expected result: a commit hash, author, date, tree hash, and file stats.
203
+
204
+ ### 13. View history
205
+
206
+ ```bash
207
+ gent log
208
+ gent log --oneline
209
+ gent show --no-patch
210
+ ```
211
+
212
+ ### 14. Push to the remote API
213
+
214
+ ```bash
215
+ gent push
216
+ ```
217
+
218
+ Expected result:
219
+
220
+ ```text
221
+ Pushed 1 commit(s) to origin/main
222
+ ```
223
+
224
+ Run it again to confirm nothing else needs syncing:
225
+
226
+ ```bash
227
+ gent push
228
+ ```
229
+
230
+ Expected result:
231
+
232
+ ```text
233
+ Everything up-to-date
234
+ ```
235
+
236
+ ### 15. Clone from the remote API
237
+
238
+ Go outside your current project:
239
+
240
+ ```bash
241
+ cd ..
242
+ gent clone /api/repos/2/my-project my-project-clone
243
+ cd my-project-clone
244
+ ```
245
+
246
+ Check the cloned files:
175
247
 
176
248
  ```bash
177
- # Create and switch to a new branch
178
- gent checkout -b feature-login
249
+ cat README.md
250
+ gent status
251
+ gent log --oneline
252
+ ```
253
+
254
+ ### 16. Make another change in the original repo
255
+
256
+ ```bash
257
+ cd ../my-project
258
+ echo "Second line" >> README.md
259
+ gent add README.md
260
+ gent commit -m "Update README"
261
+ gent push
262
+ ```
263
+
264
+ ### 17. Pull the change into the clone
265
+
266
+ ```bash
267
+ cd ../my-project-clone
268
+ gent pull
269
+ cat README.md
270
+ gent status
271
+ ```
272
+
273
+ Expected result: the clone fast-forwards, `README.md` includes the new line, and status shows no staged changes.
274
+
275
+ ### 18. Create and sync a branch
179
276
 
180
- # List branches
277
+ From a repository with at least one commit and an `origin` remote:
278
+
279
+ ```bash
280
+ gent branch feature-login
181
281
  gent branch
282
+ ```
283
+
284
+ Switch to the branch:
182
285
 
183
- # Switch back to main
286
+ ```bash
287
+ gent checkout feature-login
288
+ ```
289
+
290
+ Make a change:
291
+
292
+ ```bash
293
+ echo "feature work" > feature.txt
294
+ gent add feature.txt
295
+ gent commit -m "Add feature work"
296
+ gent push origin feature-login
297
+ ```
298
+
299
+ Switch back to main:
300
+
301
+ ```bash
184
302
  gent checkout main
303
+ ```
185
304
 
186
- # Delete a branch
187
- gent branch -d feature-login
305
+ ### 19. Merge a branch
306
+
307
+ ```bash
308
+ gent merge feature-login
309
+ gent push
188
310
  ```
189
311
 
190
- ### View History
312
+ If there are conflicts, resolve the files, then:
191
313
 
192
314
  ```bash
315
+ gent add .
316
+ gent commit -m "Resolve merge"
317
+ gent push
318
+ ```
319
+
320
+ ### 20. Create and sync a tag
321
+
322
+ Create a lightweight tag:
323
+
324
+ ```bash
325
+ gent tag v1.0.0
326
+ ```
327
+
328
+ Create an annotated tag:
329
+
330
+ ```bash
331
+ gent tag v1.0.1 -m "Release v1.0.1"
332
+ ```
333
+
334
+ List tags:
335
+
336
+ ```bash
337
+ gent tag
338
+ ```
339
+
340
+ Delete a tag:
341
+
342
+ ```bash
343
+ gent tag -d v1.0.0
344
+ ```
345
+
346
+ ### 21. Use stash when needed
347
+
348
+ Save local work:
349
+
350
+ ```bash
351
+ gent stash
352
+ ```
353
+
354
+ List stashes:
355
+
356
+ ```bash
357
+ gent stash list
358
+ ```
359
+
360
+ Apply latest stash:
361
+
362
+ ```bash
363
+ gent stash pop
364
+ ```
365
+
366
+ ### 22. Log out
367
+
368
+ ```bash
369
+ gent logout
370
+ ```
371
+
372
+ Confirm:
373
+
374
+ ```bash
375
+ gent whoami
376
+ ```
377
+
378
+ Expected result: not logged in.
379
+
380
+ ## One-Command Remote Repo Setup
381
+
382
+ You can initialize a local repo and create the remote repo in one command:
383
+
384
+ ```bash
385
+ mkdir another-project
386
+ cd another-project
387
+ gent init --remote another-project
388
+ gent remote -v
389
+ ```
390
+
391
+ This creates the remote repository on `https://gent-api.onrender.com` and configures `origin` automatically.
392
+
393
+ ## Command Reference
394
+
395
+ ### Authentication
396
+
397
+ ```bash
398
+ gent register
399
+ gent register -e user@example.com -p StrongPass123! --password-confirm StrongPass123!
400
+ gent login
401
+ gent login -e user@example.com -p StrongPass123!
402
+ gent whoami
403
+ gent logout
404
+ ```
405
+
406
+ ### Repository Setup
407
+
408
+ ```bash
409
+ gent init
410
+ gent init --remote my-repo
411
+ gent clone /api/repos/<owner_id>/<repo_name> [directory]
412
+ ```
413
+
414
+ ### Staging and Working Tree
415
+
416
+ ```bash
417
+ gent status
418
+ gent status -s
419
+ gent add <files...>
420
+ gent add .
421
+ gent rm <files...>
422
+ gent rm <files...> --cached
423
+ gent reset [files...]
424
+ gent reset --soft <commit_hash>
425
+ gent reset --hard <commit_hash>
426
+ gent diff
427
+ gent diff --staged
428
+ gent diff --stat
429
+ ```
430
+
431
+ ### History
432
+
433
+ ```bash
434
+ gent commit -m "Message"
193
435
  gent log
194
436
  gent log --oneline
195
437
  gent log -n 5
438
+ gent show
439
+ gent show <commit_hash>
440
+ gent show --no-patch
196
441
  ```
197
442
 
198
- ### Remote Workflow
443
+ ### Branching and Merging
199
444
 
200
445
  ```bash
201
- # Add a remote
202
- gent remote add origin https://gent-api.onrender.com/api/repos/123/
446
+ gent branch
447
+ gent branch <name>
448
+ gent branch -d <name>
449
+ gent checkout <branch>
450
+ gent checkout -b <branch>
451
+ gent merge <branch>
452
+ gent merge <branch> -m "Merge message"
453
+ ```
203
454
 
204
- # Push to remote
205
- gent push origin main
455
+ ### Tags
206
456
 
207
- # Pull from remote
457
+ ```bash
458
+ gent tag
459
+ gent tag <name>
460
+ gent tag <name> -m "Message"
461
+ gent tag -d <name>
462
+ ```
463
+
464
+ ### Remotes and Sync
465
+
466
+ ```bash
467
+ gent repos
468
+ gent repos --create <name>
469
+ gent repos --create <name> --description "Description"
470
+ gent repos --create <name> --private
471
+ gent remote
472
+ gent remote -v
473
+ gent remote add origin /api/repos/<owner_id>/<repo_name>
474
+ gent remote set-url origin /api/repos/<owner_id>/<repo_name>
475
+ gent remote remove origin
476
+ gent push
477
+ gent push origin main
478
+ gent pull
208
479
  gent pull origin main
480
+ ```
481
+
482
+ ## Remote URL Rules
209
483
 
210
- # Clone a repository
211
- gent clone https://gent-api.onrender.com/api/repos/123/ my-project
484
+ The global API base is fixed:
485
+
486
+ ```text
487
+ https://gent-api.onrender.com
488
+ ```
489
+
490
+ Remote repository paths should be stored like this:
491
+
492
+ ```text
493
+ /api/repos/<owner_id>/<repo_name>
212
494
  ```
213
495
 
214
- ## Repository Structure
496
+ Example:
215
497
 
216
- Gent creates a `.gent` directory in your project root:
498
+ ```bash
499
+ gent remote add origin /api/repos/2/my-project
500
+ ```
217
501
 
502
+ Do not use:
503
+
504
+ ```text
505
+ http://localhost:8000
506
+ http://127.0.0.1:8000
218
507
  ```
508
+
509
+ ## Files Created by Gent
510
+
511
+ Inside each repo:
512
+
513
+ ```text
219
514
  .gent/
220
- ├── config.json # Project configuration, remotes, user info
221
- ├── commits.json # Full commit history, branches, and tags
222
- ├── staging.json # Current staging area
223
- ├── stash.json # Stashed changes (created on first stash)
224
- ├── HEAD # Current branch reference
225
- ├── objects/ # Content-addressable blob and tree store
226
- │ ├── ab/ # First 2 characters of SHA-256 hash
227
- │ │ └── cdef... # zlib-compressed object
228
- │ └── ...
515
+ ├── config.json
516
+ ├── commits.json
517
+ ├── staging.json
518
+ ├── HEAD
519
+ ├── objects/
229
520
  └── refs/
230
- ├── heads/ # Branch references (reserved for future use)
231
- └── tags/ # Tag references (reserved for future use)
521
+
522
+ .gentignore
232
523
  ```
233
524
 
234
- Your authentication tokens are stored globally in `~/.gent/auth.json`.
525
+ Global auth:
235
526
 
236
- ## Configuration
527
+ ```text
528
+ ~/.gent/auth.json
529
+ ```
237
530
 
238
- ### Ignore Files
531
+ ## Ignore Rules
239
532
 
240
- Create a `.gentignore` file in your repository root to exclude files from tracking:
533
+ Gent creates a `.gentignore` file by default:
241
534
 
242
- ```
535
+ ```text
243
536
  node_modules/
244
- dist/
245
- .env
537
+ .DS_Store
246
538
  *.log
539
+ .env
540
+ .gent/
247
541
  ```
248
542
 
249
- Default ignored patterns include: `.gent`, `node_modules`, `.git`, `.DS_Store`, `.env`, `dist`, `build`, `coverage`, `.vscode`, `.idea`, and `*.log`.
543
+ Add project-specific ignored files there.
250
544
 
251
- ### Remotes
545
+ ## Test the Full Remote Flow
252
546
 
253
- Remotes are stored in `.gent/config.json`:
547
+ This repository includes a remote-only E2E test. It uses only:
254
548
 
255
- ```json
256
- {
257
- "remotes": {
258
- "origin": {
259
- "url": "https://gent-api.onrender.com/api/repos/123/"
260
- }
261
- }
262
- }
549
+ ```text
550
+ https://gent-api.onrender.com
263
551
  ```
264
552
 
553
+ Run syntax checks:
554
+
555
+ ```bash
556
+ npm test
557
+ ```
558
+
559
+ Run the full remote scenario:
560
+
561
+ ```bash
562
+ npm run test:remote:e2e
563
+ ```
564
+
565
+ The test covers:
566
+
567
+ - API health check
568
+ - Register, login, whoami, logout
569
+ - Create and list remote repositories
570
+ - Init, remote add, status, add, diff, commit
571
+ - Push and up-to-date push
572
+ - Branch sync
573
+ - Tag sync
574
+ - Clone from remote
575
+ - Second commit and push
576
+ - Pull into clone and verify working tree content
577
+ - `init --remote`
578
+ - Unauthenticated guard
579
+ - Version flag
580
+
265
581
  ## Troubleshooting
266
582
 
267
- **Command not found?**
583
+ ### Command not found
584
+
585
+ Install or link the CLI:
586
+
587
+ ```bash
588
+ npm install -g gent-cli
589
+ ```
590
+
591
+ or:
268
592
 
269
- Make sure the CLI is linked globally:
270
593
  ```bash
594
+ cd apps/Cli
271
595
  npm link
272
596
  ```
273
597
 
274
- Or run it directly:
598
+ ### Not authenticated
599
+
600
+ Log in again:
601
+
275
602
  ```bash
276
- node src/index.js <command>
603
+ gent login
277
604
  ```
278
605
 
279
- **Not a Gent repository?**
606
+ ### Not a Gent repository
280
607
 
281
- Run `gent init` in your project directory first.
608
+ Run commands inside a folder initialized with:
282
609
 
283
- **No changes to commit?**
610
+ ```bash
611
+ gent init
612
+ ```
284
613
 
285
- Stage files with `gent add <files>` before committing.
614
+ ### Remote not found
286
615
 
287
- **Authentication errors?**
616
+ Add an origin remote:
288
617
 
289
- Run `gent login` to refresh your session. Tokens expire automatically and should refresh; if not, log in again.
618
+ ```bash
619
+ gent remote add origin /api/repos/<owner_id>/<repo_name>
620
+ ```
290
621
 
291
- ## Docs
622
+ ### Push says everything up-to-date
292
623
 
293
- - [QUICKSTART.md](QUICKSTART.md)
294
- - [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)
624
+ That means the local branch has no new commits compared to the last pushed remote ref.
295
625
 
296
- ## Contributing
626
+ ### Render cold start
297
627
 
298
- Contributions are welcome! Please fork the repository and submit a Pull Request.
628
+ The deployed API may take several seconds to respond after inactivity. Retry the command if the first request times out.
299
629
 
300
- ## License
630
+ ## Version
301
631
 
302
- ISC
632
+ Show the CLI version:
303
633
 
304
- ---
634
+ ```bash
635
+ gent -V
636
+ gent --version
637
+ ```
305
638
 
306
- Built with love by [Abdalrahman Kanawati](https://github.com/abdo-ka)
639
+ ## License
640
+
641
+ ISC
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gent-cli",
3
- "version": "5.0.4",
3
+ "version": "6.0.1",
4
4
  "description": "A modern, Git-like version control CLI with built-in cloud authentication and global user identity management.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -8,7 +8,8 @@
8
8
  },
9
9
  "scripts": {
10
10
  "start": "node src/index.js",
11
- "test": "echo \"Error: no test specified\" && exit 1",
11
+ "test": "node --check src/index.js && node --check tests/remote-e2e.js",
12
+ "test:remote:e2e": "node tests/remote-e2e.js",
12
13
  "demo": "bash demo.sh",
13
14
  "link": "npm link",
14
15
  "unlink": "npm unlink",
@@ -32,7 +32,7 @@ const { ensureDir, writeJSON, pathExists } = require('../utils/fileSystem');
32
32
  const { GENT_DIR, CONFIG_FILE, STAGING_FILE, COMMITS_FILE, API_ENDPOINTS, buildRepoUrl, parseRemoteUrl } = require('../utils/constants');
33
33
  const apiClient = require('../utils/api-client');
34
34
  const authStorage = require('../utils/auth-storage');
35
- const { storeBlob, readBlobAsString } = require('../utils/hash-engine');
35
+ const { storeBlob, readBlobAsString, decodeRemoteBlobContent } = require('../utils/hash-engine');
36
36
 
37
37
  /**
38
38
  * Clone remote repository
@@ -153,7 +153,7 @@ async function clone(url, directory, options) {
153
153
  buildRepoUrl(API_ENDPOINTS.REPO_BLOB_DETAIL, { ...repoInfo, sha: entry.sha })
154
154
  );
155
155
  if (blob.content) {
156
- const buf = Buffer.from(blob.content, 'base64');
156
+ const buf = decodeRemoteBlobContent(blob.content, entry.sha);
157
157
  await storeBlob(gentPath, buf);
158
158
  objectCount++;
159
159
  }
@@ -144,15 +144,16 @@ async function createRemoteRepo(cwd, gentPath, config, options) {
144
144
  };
145
145
 
146
146
  const data = await apiClient.post(API_ENDPOINTS.REPOS_CREATE, payload);
147
+ const repo = data.repository || data;
147
148
 
148
149
  // Update local config with remote
149
150
  const configPath = path.join(gentPath, CONFIG_FILE);
150
151
  const localConfig = await require('../utils/fileSystem').readJSON(configPath);
151
152
  localConfig.remotes = localConfig.remotes || {};
152
- localConfig.remotes.origin = { url: `/api/repos/${data.owner_id}/${data.name}` };
153
+ localConfig.remotes.origin = { url: `/api/repos/${repo.owner_id}/${repo.name}` };
153
154
  await writeJSON(configPath, localConfig);
154
155
 
155
- console.log(chalk.green(`✓ Remote repository created: /api/repos/${data.owner_id}/${data.name}`));
156
+ console.log(chalk.green(`✓ Remote repository created: /api/repos/${repo.owner_id}/${repo.name}`));
156
157
  console.log(chalk.gray(` Remote 'origin' configured automatically`));
157
158
 
158
159
  } catch (error) {
@@ -30,7 +30,7 @@ const { getGentPath, readJSON, writeJSON } = require('../utils/fileSystem');
30
30
  const { COMMITS_FILE, CONFIG_FILE, API_ENDPOINTS, buildRepoUrl, parseRemoteUrl } = require('../utils/constants');
31
31
  const apiClient = require('../utils/api-client');
32
32
  const authStorage = require('../utils/auth-storage');
33
- const { storeBlob, objectExists } = require('../utils/hash-engine');
33
+ const { storeBlob, objectExists, readBlobAsString, decodeRemoteBlobContent } = require('../utils/hash-engine');
34
34
  const { findMergeBase, mergeTreeEntries } = require('../utils/merge-engine');
35
35
  const { generateCommitHash } = require('../utils/helpers');
36
36
 
@@ -140,7 +140,7 @@ async function pull(remoteName, branchName, options) {
140
140
  buildRepoUrl(API_ENDPOINTS.REPO_BLOB_DETAIL, { ...repoInfo, sha: entry.sha })
141
141
  );
142
142
  if (blob.content) {
143
- const buf = Buffer.from(blob.content, 'base64');
143
+ const buf = decodeRemoteBlobContent(blob.content, entry.sha);
144
144
  await storeBlob(gentPath, buf);
145
145
  }
146
146
  } catch {
@@ -187,8 +187,11 @@ async function pull(remoteName, branchName, options) {
187
187
 
188
188
  if (!localHead || isAncestor(repository.commits, localHead, remoteHead)) {
189
189
  // Fast-forward
190
+ const previousTree = localHead ? getCommitTree(repository.commits, localHead) : [];
191
+ const nextTree = getCommitTree(repository.commits, remoteHead);
190
192
  repository.branches[branch] = remoteHead;
191
193
  await writeJSON(path.join(gentPath, COMMITS_FILE), repository);
194
+ await checkoutTree(gentPath, process.cwd(), previousTree, nextTree);
192
195
 
193
196
  config.remoteRefs[`${remote}/${branch}`] = remoteHead;
194
197
  await writeJSON(path.join(gentPath, CONFIG_FILE), config);
@@ -234,6 +237,9 @@ async function pull(remoteName, branchName, options) {
234
237
  repository.commits.push(mergeCommit);
235
238
  repository.branches[branch] = mergeCommit.hash;
236
239
  await writeJSON(path.join(gentPath, COMMITS_FILE), repository);
240
+ if (!mergeResult.hasConflicts) {
241
+ await checkoutTree(gentPath, process.cwd(), oursTree, mergeResult.mergedEntries);
242
+ }
237
243
 
238
244
  config.remoteRefs[`${remote}/${branch}`] = remoteHead;
239
245
  await writeJSON(path.join(gentPath, CONFIG_FILE), config);
@@ -276,4 +282,40 @@ function isAncestor(commits, hashA, hashB) {
276
282
  return false;
277
283
  }
278
284
 
285
+ function getCommitTree(commits, hash) {
286
+ const commit = commits.find(c => c.hash === hash);
287
+ if (!commit) return [];
288
+ return commit.tree || (commit.files || []).map(f => ({
289
+ mode: '100644',
290
+ name: f.path || f.name,
291
+ hash: f.hash,
292
+ type: 'blob'
293
+ }));
294
+ }
295
+
296
+ async function checkoutTree(gentPath, cwd, previousTree, nextTree) {
297
+ const nextPaths = new Set(nextTree.map(e => e.name || e.path));
298
+
299
+ for (const entry of previousTree) {
300
+ const relPath = entry.name || entry.path;
301
+ if (!relPath || nextPaths.has(relPath)) continue;
302
+ try {
303
+ await fs.unlink(path.join(cwd, relPath));
304
+ } catch {
305
+ // File already absent.
306
+ }
307
+ }
308
+
309
+ for (const entry of nextTree) {
310
+ if (entry.type && entry.type !== 'blob') continue;
311
+ const relPath = entry.name || entry.path;
312
+ if (!relPath || !entry.hash) continue;
313
+
314
+ const content = await readBlobAsString(gentPath, entry.hash);
315
+ const fullPath = path.join(cwd, relPath);
316
+ await fs.mkdir(path.dirname(fullPath), { recursive: true });
317
+ await fs.writeFile(fullPath, content, 'utf-8');
318
+ }
319
+ }
320
+
279
321
  module.exports = pull;
@@ -17,12 +17,19 @@ async function register(options) {
17
17
  console.log(chalk.cyan('\n🚀 Create your Gent account\n'));
18
18
 
19
19
  try {
20
+ let email = options.email;
21
+ let password = options.password;
22
+ let passwordConfirm = options.passwordConfirm;
23
+ let firstName = options.firstName;
24
+ let lastName = options.lastName;
25
+
20
26
  // Prompt for user information
21
27
  const answers = await inquirer.prompt([
22
28
  {
23
29
  type: 'input',
24
30
  name: 'email',
25
31
  message: 'Email address:',
32
+ when: !email,
26
33
  validate: (input) => {
27
34
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
28
35
  return emailRegex.test(input) || 'Please enter a valid email address';
@@ -33,6 +40,7 @@ async function register(options) {
33
40
  name: 'password',
34
41
  message: 'Password:',
35
42
  mask: '*',
43
+ when: !password,
36
44
  validate: (input) => {
37
45
  if (input.length < 8) {
38
46
  return 'Password must be at least 8 characters long';
@@ -45,33 +53,46 @@ async function register(options) {
45
53
  name: 'passwordConfirm',
46
54
  message: 'Confirm password:',
47
55
  mask: '*',
56
+ when: !passwordConfirm,
48
57
  validate: (input, answers) => {
49
- return input === answers.password || 'Passwords do not match';
58
+ return input === (password || answers.password) || 'Passwords do not match';
50
59
  }
51
60
  },
52
61
  {
53
62
  type: 'input',
54
63
  name: 'firstName',
55
64
  message: 'First name:',
65
+ when: firstName === undefined,
56
66
  default: ''
57
67
  },
58
68
  {
59
69
  type: 'input',
60
70
  name: 'lastName',
61
71
  message: 'Last name:',
72
+ when: lastName === undefined,
62
73
  default: ''
63
74
  }
64
75
  ]);
65
76
 
77
+ email = email || answers.email;
78
+ password = password || answers.password;
79
+ passwordConfirm = passwordConfirm || answers.passwordConfirm;
80
+ firstName = firstName !== undefined ? firstName : answers.firstName;
81
+ lastName = lastName !== undefined ? lastName : answers.lastName;
82
+
83
+ if (!email || !password || !passwordConfirm) {
84
+ throw new Error('Email, password, and password confirmation are required');
85
+ }
86
+
66
87
  const spinner = ora('Creating your account...').start();
67
88
 
68
89
  // Register user
69
90
  const user = await authService.register(
70
- answers.email,
71
- answers.password,
72
- answers.passwordConfirm,
73
- answers.firstName,
74
- answers.lastName
91
+ email,
92
+ password,
93
+ passwordConfirm,
94
+ firstName || '',
95
+ lastName || ''
75
96
  );
76
97
 
77
98
  spinner.succeed(chalk.green('✓ Account created successfully!'));
@@ -30,6 +30,7 @@ const path = require('path');
30
30
  const chalk = require('chalk');
31
31
  const { getGentPath, readJSON, writeJSON } = require('../utils/fileSystem');
32
32
  const { CONFIG_FILE, parseRemoteUrl } = require('../utils/constants');
33
+ const initCommand = require('./init');
33
34
 
34
35
  /**
35
36
  * Manage remotes
@@ -39,7 +40,19 @@ const { CONFIG_FILE, parseRemoteUrl } = require('../utils/constants');
39
40
  */
40
41
  async function remote(subcommand, args, options) {
41
42
  try {
42
- const gentPath = await getGentPath();
43
+ let gentPath;
44
+ try {
45
+ gentPath = await getGentPath();
46
+ } catch (error) {
47
+ if (subcommand !== 'add' || error.code !== 'ENOENT') {
48
+ throw error;
49
+ }
50
+
51
+ console.log(chalk.yellow('Not a Gent repository yet. Initializing first...'));
52
+ await initCommand({});
53
+ gentPath = await getGentPath();
54
+ }
55
+
43
56
  const configPath = path.join(gentPath, CONFIG_FILE);
44
57
  const config = await readJSON(configPath);
45
58
  config.remotes = config.remotes || {};
@@ -120,6 +133,7 @@ async function remote(subcommand, args, options) {
120
133
  } catch (error) {
121
134
  if (error.code === 'ENOENT' && error.message.includes('.gent')) {
122
135
  console.error(chalk.red('Error: Not a gent repository'));
136
+ console.log(chalk.yellow('Run "gent init" first, then retry this command'));
123
137
  } else {
124
138
  console.error(chalk.red('Error:'), error.message);
125
139
  }
@@ -93,10 +93,11 @@ async function createRepo(options) {
93
93
  }
94
94
 
95
95
  const data = await apiClient.post(API_ENDPOINTS.REPOS_CREATE, payload);
96
+ const repo = data.repository || data;
96
97
 
97
- spinner.succeed(chalk.green(`Created repository '${data.name}'`));
98
- console.log(chalk.gray(` URL: /api/repos/${data.owner_id}/${data.name}`));
99
- console.log(chalk.gray(` Use "gent remote add origin /api/repos/${data.owner_id}/${data.name}" to link`));
98
+ spinner.succeed(chalk.green(`Created repository '${repo.name}'`));
99
+ console.log(chalk.gray(` URL: /api/repos/${repo.owner_id}/${repo.name}`));
100
+ console.log(chalk.gray(` Use "gent remote add origin /api/repos/${repo.owner_id}/${repo.name}" to link`));
100
101
  }
101
102
 
102
103
  module.exports = repos;
package/src/index.js CHANGED
@@ -53,7 +53,7 @@ const whoamiCommand = require('./commands/whoami');
53
53
  program
54
54
  .name('gent')
55
55
  .description(chalk.cyan('Gent - A Git-like version control CLI with cloud backend'))
56
- .version(packageJson.version, '-v, --version', 'Output the current version');
56
+ .version(packageJson.version, '-V, --version', 'Output the current version');
57
57
 
58
58
  // ─── Repository Setup ───────────────────────────────────
59
59
 
@@ -195,6 +195,11 @@ program
195
195
  program
196
196
  .command('register')
197
197
  .description('Create a new user account')
198
+ .option('-e, --email <email>', 'Email address')
199
+ .option('-p, --password <password>', 'Password')
200
+ .option('--password-confirm <password>', 'Password confirmation')
201
+ .option('--first-name <name>', 'First name')
202
+ .option('--last-name <name>', 'Last name')
198
203
  .action(registerCommand);
199
204
 
200
205
  program
@@ -237,7 +242,7 @@ try {
237
242
  program.outputHelp();
238
243
  }
239
244
  } catch (err) {
240
- if (err.code !== 'commander.help' && err.code !== 'commander.helpDisplayed') {
245
+ if (err.code !== 'commander.help' && err.code !== 'commander.helpDisplayed' && err.code !== 'commander.version') {
241
246
  console.error(chalk.red('Error:'), err.message);
242
247
  process.exit(1);
243
248
  }
@@ -137,6 +137,29 @@ function hashBlob(content) {
137
137
  return hashObject('blob', content);
138
138
  }
139
139
 
140
+ /**
141
+ * Decode blob content returned by the backend.
142
+ * Current Django endpoints return raw UTF-8 content, while older/planned pull
143
+ * responses may return base64. Prefer the representation whose blob hash
144
+ * matches the expected object SHA.
145
+ * @param {String} content
146
+ * @param {String} expectedHash
147
+ * @returns {Buffer}
148
+ */
149
+ function decodeRemoteBlobContent(content, expectedHash) {
150
+ const raw = Buffer.from(content, 'utf-8');
151
+ if (!expectedHash || hashBlob(raw) === expectedHash) {
152
+ return raw;
153
+ }
154
+
155
+ const decoded = Buffer.from(content, 'base64');
156
+ if (hashBlob(decoded) === expectedHash) {
157
+ return decoded;
158
+ }
159
+
160
+ return raw;
161
+ }
162
+
140
163
  /**
141
164
  * Hash a tree structure.
142
165
  * @param {Array<{mode: String, name: String, hash: String, type: String}>} entries
@@ -324,6 +347,7 @@ module.exports = {
324
347
  hashObject,
325
348
  hashBlob,
326
349
  hashTree,
350
+ decodeRemoteBlobContent,
327
351
  objectExists,
328
352
  storeBlob,
329
353
  readBlob,