gent-cli 12.0.0 → 14.0.0
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 +1 -0
- package/package.json +1 -1
- package/src/commands/login.js +4 -0
- package/src/commands/merge.js +3 -0
- package/src/commands/pet.js +130 -45
- package/src/commands/pull.js +3 -0
- package/src/commands/push.js +3 -0
- package/src/commands/register.js +4 -0
- package/src/index.js +1 -2
package/README.md
CHANGED
|
@@ -25,6 +25,7 @@ Beyond a faithful git-like workflow, Gent adds:
|
|
|
25
25
|
- **`gent resolve`** — an interactive conflict resolver (ours / theirs / both / edit / AI).
|
|
26
26
|
- **`gent summary`** — a repository health dashboard, plus **`gent log --graph`**.
|
|
27
27
|
- **Optional AI** (`gent commit --ai`, `gent explain`, `gent summary --ai`, AI option in `gent resolve`) — off by default, enabled with `ANTHROPIC_API_KEY`.
|
|
28
|
+
- **Genti, your terminal mascot** — a chunky pixel bot that *acts out* your workflow: it carries a file crate to the cloud on `gent push`, walks one home on `gent pull`, and reconciles two branches on `gent merge`. It plays once (in place, no scrollback spam) after a successful command. Meet it directly with `gent pet` (add `--loop` to keep it running; try `gent pet push|pull|merge|auth`). Set `GENT_NO_PET=1` (or run in CI / a non-interactive shell) to turn the celebrations off.
|
|
28
29
|
|
|
29
30
|
See [docs/COMMANDS.md](docs/COMMANDS.md) for the full reference and
|
|
30
31
|
[docs/ALGORITHMS.md](docs/ALGORITHMS.md) for how the engines work.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gent-cli",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "14.0.0",
|
|
4
4
|
"description": "A modern, Git-like version control CLI with cloud sync, AI-powered superpowers (ask/review/docs/changelog), and zero-friction setup (gent setup/doctor/config).",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
package/src/commands/login.js
CHANGED
|
@@ -8,12 +8,14 @@ const inquirer = require('inquirer');
|
|
|
8
8
|
const ora = require('ora');
|
|
9
9
|
const boxen = require('boxen');
|
|
10
10
|
const authService = require('../services/auth-service');
|
|
11
|
+
const pet = require('./pet');
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Login user
|
|
14
15
|
* @param {Object} options - Command options
|
|
15
16
|
*/
|
|
16
17
|
async function login(options) {
|
|
18
|
+
pet.banner('Knock knock — let\'s sign you in.', 'Enter your details below.');
|
|
17
19
|
console.log(chalk.cyan('\n🔐 Login to your Gent account\n'));
|
|
18
20
|
|
|
19
21
|
try {
|
|
@@ -75,6 +77,8 @@ ${chalk.bold('Commands:')}
|
|
|
75
77
|
borderColor: 'cyan'
|
|
76
78
|
}));
|
|
77
79
|
|
|
80
|
+
await pet.celebrate('auth');
|
|
81
|
+
|
|
78
82
|
} catch (error) {
|
|
79
83
|
console.error(chalk.red('\n✗ Login failed'));
|
|
80
84
|
console.error(chalk.red(`Error: ${error.message}\n`));
|
package/src/commands/merge.js
CHANGED
|
@@ -13,6 +13,7 @@ const { generateCommitHash } = require('../utils/helpers');
|
|
|
13
13
|
const authStorage = require('../utils/auth-storage');
|
|
14
14
|
const { findMergeBase, mergeTreeEntries, autoMerge } = require('../utils/merge-engine');
|
|
15
15
|
const { storeTree, readBlobAsString, storeBlob } = require('../utils/hash-engine');
|
|
16
|
+
const pet = require('./pet');
|
|
16
17
|
const journal = require('../utils/journal');
|
|
17
18
|
|
|
18
19
|
/**
|
|
@@ -78,6 +79,7 @@ async function merge(sourceBranch, options) {
|
|
|
78
79
|
}
|
|
79
80
|
|
|
80
81
|
spinner.succeed(chalk.green(`Fast-forward merge: ${currentBranch} → ${theirsHash.substring(0, 7)}`));
|
|
82
|
+
await pet.celebrate('merge');
|
|
81
83
|
return;
|
|
82
84
|
}
|
|
83
85
|
|
|
@@ -185,6 +187,7 @@ async function merge(sourceBranch, options) {
|
|
|
185
187
|
console.log(chalk.gray(`\n Base: ${baseHash ? baseHash.substring(0, 7) : 'none'}`));
|
|
186
188
|
console.log(chalk.gray(` Ours: ${oursHash.substring(0, 7)} Theirs: ${theirsHash.substring(0, 7)}`));
|
|
187
189
|
console.log(chalk.green(` ${autoResolved} file(s) merged automatically`));
|
|
190
|
+
await pet.celebrate('merge');
|
|
188
191
|
} else {
|
|
189
192
|
// Stage the merge state for manual resolution
|
|
190
193
|
const staging = await readJSON(path.join(gentPath, STAGING_FILE));
|
package/src/commands/pet.js
CHANGED
|
@@ -365,52 +365,116 @@ const TIPS = [
|
|
|
365
365
|
];
|
|
366
366
|
|
|
367
367
|
// ── Scene registry ───────────────────────────────────────────────────────
|
|
368
|
+
// `cycle` = ticks in one full loop; playing "once" runs exactly one cycle.
|
|
368
369
|
const SCENES = {
|
|
369
|
-
idle: { fn: (cv, t, st) => sceneIdle(cv, t, st.tip) },
|
|
370
|
-
push: { fn: scenePush },
|
|
371
|
-
pull: { fn: scenePull },
|
|
372
|
-
merge: { fn: sceneMerge },
|
|
373
|
-
auth: { fn: (cv, t) => sceneAuth(cv, t) },
|
|
374
|
-
login: { fn: (cv, t) => sceneAuth(cv, t) },
|
|
370
|
+
idle: { fn: (cv, t, st) => sceneIdle(cv, t, st.tip), cycle: 66 },
|
|
371
|
+
push: { fn: scenePush, cycle: 66 },
|
|
372
|
+
pull: { fn: scenePull, cycle: 64 },
|
|
373
|
+
merge: { fn: sceneMerge, cycle: 70 },
|
|
374
|
+
auth: { fn: (cv, t) => sceneAuth(cv, t), cycle: 102 },
|
|
375
|
+
login: { fn: (cv, t) => sceneAuth(cv, t), cycle: 102 },
|
|
375
376
|
};
|
|
376
377
|
|
|
377
378
|
// ── ANSI helpers ─────────────────────────────────────────────────────────
|
|
378
|
-
const
|
|
379
|
-
const
|
|
380
|
-
const
|
|
381
|
-
const
|
|
379
|
+
const HIDE = '\x1b[?25l';
|
|
380
|
+
const SHOW = '\x1b[?25h';
|
|
381
|
+
const CLEAR_LINE = '\x1b[2K';
|
|
382
|
+
const HOME = '\x1b[H';
|
|
383
|
+
const ENTER_ALT = '\x1b[?1049h'; // switch to the alternate screen buffer
|
|
384
|
+
const LEAVE_ALT = '\x1b[?1049l'; // …and restore the user's scrollback on exit
|
|
385
|
+
const up = (n) => `\x1b[${n}A`;
|
|
382
386
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
scene
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
387
|
+
/**
|
|
388
|
+
* Play a scene. Resolves when finished.
|
|
389
|
+
*
|
|
390
|
+
* Two redraw strategies:
|
|
391
|
+
* - altScreen:true → take over the whole screen (alternate buffer), redraw
|
|
392
|
+
* from HOME each frame, restore on exit. Zero scrollback drift. Used by the
|
|
393
|
+
* standalone `gent pet` command.
|
|
394
|
+
* - altScreen:false → inline redraw below existing output via cursor-up, so a
|
|
395
|
+
* command's own text stays visible above. Used by post-command celebrations.
|
|
396
|
+
*
|
|
397
|
+
* @param {string} sceneName
|
|
398
|
+
* @param {object} opts
|
|
399
|
+
* @param {boolean} opts.loop keep looping until Ctrl+C (default: play once)
|
|
400
|
+
* @param {boolean} opts.footer show the hint line under the stage
|
|
401
|
+
* @param {boolean} opts.goodbye print a farewell line when done
|
|
402
|
+
* @param {boolean} opts.altScreen use the alternate screen buffer
|
|
403
|
+
* @returns {Promise<void>}
|
|
404
|
+
*/
|
|
405
|
+
function play(sceneName, { loop = false, footer = true, goodbye = false, altScreen = false, maxTicks = Infinity } = {}) {
|
|
406
|
+
return new Promise((resolve) => {
|
|
407
|
+
const scene = SCENES[sceneName] || SCENES.idle;
|
|
408
|
+
const cv = new Canvas(CV_W, CV_H);
|
|
409
|
+
const state = { count: 0, tip: TIPS[Math.floor(Math.random() * TIPS.length)] };
|
|
410
|
+
const totalTicks = loop ? Infinity : Math.min(scene.cycle + 1, maxTicks); // one clean cycle, capped
|
|
411
|
+
let t = 0;
|
|
412
|
+
let printed = false;
|
|
413
|
+
let done = false;
|
|
414
|
+
|
|
415
|
+
const footerLine = () => footer
|
|
416
|
+
? chalk.gray(' ') +
|
|
417
|
+
(loop ? chalk.gray('Ctrl+C to leave') : C.body('Genti')) +
|
|
418
|
+
chalk.gray(' · more scenes: ') + C.say('gent pet push|pull|merge')
|
|
419
|
+
: '';
|
|
420
|
+
|
|
421
|
+
const paint = () => {
|
|
422
|
+
cv.clear();
|
|
423
|
+
scene.fn(cv, t, state);
|
|
424
|
+
if (sceneName === 'idle' && loop && t > 0 && t % scene.cycle === 0) {
|
|
425
|
+
state.tip = TIPS[Math.floor(Math.random() * TIPS.length)];
|
|
426
|
+
}
|
|
427
|
+
const lines = cv.render().split('\n');
|
|
428
|
+
lines.push(footerLine());
|
|
429
|
+
const block = lines.map(l => CLEAR_LINE + l).join('\n');
|
|
430
|
+
if (altScreen) {
|
|
431
|
+
process.stdout.write(HOME + block);
|
|
432
|
+
} else {
|
|
433
|
+
if (printed) process.stdout.write(up(lines.length));
|
|
434
|
+
process.stdout.write(block + '\n');
|
|
435
|
+
}
|
|
436
|
+
printed = true;
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
const finish = () => {
|
|
440
|
+
if (done) return;
|
|
441
|
+
done = true;
|
|
442
|
+
clearInterval(timer);
|
|
443
|
+
process.removeListener('SIGINT', onSig);
|
|
444
|
+
process.stdout.write(SHOW + (altScreen ? LEAVE_ALT : ''));
|
|
445
|
+
if (goodbye) {
|
|
446
|
+
console.log(C.body(' Genti waves.') + chalk.gray(' Come back with ') + C.say('gent pet') + chalk.gray('.'));
|
|
447
|
+
}
|
|
448
|
+
resolve();
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
const onSig = () => finish();
|
|
452
|
+
|
|
453
|
+
process.stdout.write((altScreen ? ENTER_ALT + HOME : '') + HIDE);
|
|
454
|
+
paint();
|
|
455
|
+
const timer = setInterval(() => {
|
|
456
|
+
t++;
|
|
457
|
+
if (t >= totalTicks) { paint(); return finish(); }
|
|
458
|
+
paint();
|
|
459
|
+
}, FRAME_MS);
|
|
460
|
+
process.on('SIGINT', onSig);
|
|
461
|
+
});
|
|
462
|
+
}
|
|
412
463
|
|
|
413
|
-
|
|
464
|
+
/**
|
|
465
|
+
* A compact STATIC Genti header — safe to print above an interactive prompt
|
|
466
|
+
* (inquirer, etc.) because it never animates or moves the cursor.
|
|
467
|
+
* @param {string} greeting bold line beside the mascot
|
|
468
|
+
* @param {string} [sub] dimmer line under the greeting
|
|
469
|
+
*/
|
|
470
|
+
function banner(greeting, sub) {
|
|
471
|
+
if (process.env.GENT_NO_PET || !process.stdout.isTTY) return;
|
|
472
|
+
const cv = new Canvas(52, 9);
|
|
473
|
+
cv.sprite(mascot({}), 1, 0);
|
|
474
|
+
cv.sprite(legs('stand'), 1, 7);
|
|
475
|
+
cv.text(18, 2, greeting, C.body);
|
|
476
|
+
if (sub) cv.text(18, 4, sub, C.say);
|
|
477
|
+
console.log('\n' + cv.render());
|
|
414
478
|
}
|
|
415
479
|
|
|
416
480
|
// Static single frame for non-TTY (piped) output.
|
|
@@ -419,10 +483,9 @@ function still(sceneName) {
|
|
|
419
483
|
const state = { count: 1, tip: TIPS[Math.floor(Math.random() * TIPS.length)] };
|
|
420
484
|
(SCENES[sceneName] || SCENES.idle).fn(cv, 12, state);
|
|
421
485
|
console.log(cv.render());
|
|
422
|
-
console.log(chalk.gray(' (animated in a real terminal — run ') + C.say(`gent pet ${sceneName === 'idle' ? '' : sceneName}`.trim()) + chalk.gray(')'));
|
|
423
486
|
}
|
|
424
487
|
|
|
425
|
-
// ──
|
|
488
|
+
// ── Public: standalone `gent pet` command ────────────────────────────────
|
|
426
489
|
async function petCommand(scene, options = {}) {
|
|
427
490
|
if (process.env.NO_COLOR || (options && options.color === false)) chalk.level = 0;
|
|
428
491
|
|
|
@@ -435,14 +498,36 @@ async function petCommand(scene, options = {}) {
|
|
|
435
498
|
return;
|
|
436
499
|
}
|
|
437
500
|
|
|
438
|
-
// Signed-out nudge:
|
|
439
|
-
if (name === 'idle'
|
|
501
|
+
// Signed-out nudge: bare `gent pet` greets you at the door.
|
|
502
|
+
if (name === 'idle') {
|
|
440
503
|
const authed = await authStorage.isAuthenticated().catch(() => false);
|
|
441
504
|
if (!authed) name = 'auth';
|
|
442
505
|
}
|
|
443
506
|
|
|
444
507
|
if (!process.stdout.isTTY) return still(name);
|
|
445
|
-
play
|
|
508
|
+
// Default: play once, on the alternate screen so nothing pollutes scrollback.
|
|
509
|
+
// `--loop` keeps it running until Ctrl+C.
|
|
510
|
+
await play(name, { loop: !!options.loop, footer: true, goodbye: true, altScreen: true });
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Public: a one-shot celebration other commands fire after they succeed.
|
|
515
|
+
* Silent + safe in non-interactive contexts (CI, pipes, GENT_NO_PET=1).
|
|
516
|
+
* Never throws — a mascot must never break a real command.
|
|
517
|
+
*/
|
|
518
|
+
async function celebrate(scene) {
|
|
519
|
+
try {
|
|
520
|
+
if (!process.stdout.isTTY) return;
|
|
521
|
+
if (process.env.NO_COLOR) { /* still animate, just uncolored */ }
|
|
522
|
+
if (process.env.GENT_NO_PET || process.env.CI) return;
|
|
523
|
+
if (!SCENES[scene]) return;
|
|
524
|
+
console.log(); // one blank line between command output and Genti
|
|
525
|
+
// Auth flows just need a quick friendly wave; action scenes tell a fuller story.
|
|
526
|
+
const maxTicks = (scene === 'auth' || scene === 'login') ? 34 : Infinity;
|
|
527
|
+
await play(scene, { loop: false, footer: false, goodbye: false, maxTicks });
|
|
528
|
+
} catch (_) { /* ignore — decoration only */ }
|
|
446
529
|
}
|
|
447
530
|
|
|
448
531
|
module.exports = petCommand;
|
|
532
|
+
module.exports.celebrate = celebrate;
|
|
533
|
+
module.exports.banner = banner;
|
package/src/commands/pull.js
CHANGED
|
@@ -29,6 +29,7 @@ const apiClient = require('../utils/api-client');
|
|
|
29
29
|
const authStorage = require('../utils/auth-storage');
|
|
30
30
|
const { storeBlob, readBlob } = require('../utils/hash-engine');
|
|
31
31
|
const { findMergeBase, mergeTreeEntries } = require('../utils/merge-engine');
|
|
32
|
+
const pet = require('./pet');
|
|
32
33
|
const { generateCommitHash } = require('../utils/helpers');
|
|
33
34
|
|
|
34
35
|
/**
|
|
@@ -137,6 +138,7 @@ async function pull(remoteName, branchName, options) {
|
|
|
137
138
|
|
|
138
139
|
spinner.succeed(chalk.green(`Fast-forward: ${newCount} new commit(s)`));
|
|
139
140
|
console.log(chalk.gray(` ${remote}/${branch} → ${remoteHead.substring(0, 7)}`));
|
|
141
|
+
await pet.celebrate('pull');
|
|
140
142
|
} else {
|
|
141
143
|
// Diverged — need 3-way merge
|
|
142
144
|
spinner.text = 'Branches diverged, merging...';
|
|
@@ -192,6 +194,7 @@ async function pull(remoteName, branchName, options) {
|
|
|
192
194
|
} else {
|
|
193
195
|
spinner.succeed(chalk.green(`Merged ${newCount} remote commit(s)`));
|
|
194
196
|
console.log(chalk.gray(` Merge commit: ${mergeCommit.hash.substring(0, 7)}`));
|
|
197
|
+
await pet.celebrate('pull');
|
|
195
198
|
}
|
|
196
199
|
}
|
|
197
200
|
} catch (error) {
|
package/src/commands/push.js
CHANGED
|
@@ -42,6 +42,7 @@ const { COMMITS_FILE, CONFIG_FILE, API_ENDPOINTS, buildRepoUrl, parseRemoteUrl }
|
|
|
42
42
|
const apiClient = require('../utils/api-client');
|
|
43
43
|
const authStorage = require('../utils/auth-storage');
|
|
44
44
|
const { readBlob, readTree, objectExists, readBlobAsString } = require('../utils/hash-engine');
|
|
45
|
+
const pet = require('./pet');
|
|
45
46
|
|
|
46
47
|
/**
|
|
47
48
|
* Push commits to remote
|
|
@@ -243,6 +244,8 @@ async function push(remoteName, branchName, options) {
|
|
|
243
244
|
console.log(chalk.gray(` ${localHead.substring(0, 7)} → ${remote}/${branch}`));
|
|
244
245
|
console.log(chalk.gray(` ${packBlobs.length} blob(s), ${packTrees.length} tree(s) transferred`));
|
|
245
246
|
|
|
247
|
+
await pet.celebrate('push');
|
|
248
|
+
|
|
246
249
|
} catch (error) {
|
|
247
250
|
spinner.fail(chalk.red('Push failed'));
|
|
248
251
|
|
package/src/commands/register.js
CHANGED
|
@@ -8,12 +8,14 @@ const inquirer = require('inquirer');
|
|
|
8
8
|
const ora = require('ora');
|
|
9
9
|
const boxen = require('boxen');
|
|
10
10
|
const authService = require('../services/auth-service');
|
|
11
|
+
const pet = require('./pet');
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Register a new user
|
|
14
15
|
* @param {Object} options - Command options
|
|
15
16
|
*/
|
|
16
17
|
async function register(options) {
|
|
18
|
+
pet.banner('New here? Let\'s get you set up.', 'Create your account below.');
|
|
17
19
|
console.log(chalk.cyan('\n🚀 Create your Gent account\n'));
|
|
18
20
|
|
|
19
21
|
try {
|
|
@@ -120,6 +122,8 @@ ${chalk.bold('Next steps:')}
|
|
|
120
122
|
borderColor: 'green'
|
|
121
123
|
}));
|
|
122
124
|
|
|
125
|
+
await pet.celebrate('auth');
|
|
126
|
+
|
|
123
127
|
} catch (error) {
|
|
124
128
|
console.error(chalk.red('\n✗ Registration failed'));
|
|
125
129
|
console.error(chalk.red(`Error: ${error.message}\n`));
|
package/src/index.js
CHANGED
|
@@ -437,8 +437,7 @@ program
|
|
|
437
437
|
program
|
|
438
438
|
.command('pet [scene]')
|
|
439
439
|
.description('Meet Genti — an animated pixel mascot that acts out gent (push|pull|merge|auth)')
|
|
440
|
-
.option('--
|
|
441
|
-
.option('--stay', 'Stay on the idle scene even when signed out')
|
|
440
|
+
.option('--loop', 'Keep looping until Ctrl+C (default: play once)')
|
|
442
441
|
.action(petCommand);
|
|
443
442
|
|
|
444
443
|
// Help command
|