gent-cli 13.0.0 β 15.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/package.json +1 -1
- package/src/commands/login.js +4 -0
- package/src/commands/pet.js +136 -57
- package/src/commands/pull.js +3 -4
- package/src/commands/push.js +4 -5
- package/src/commands/register.js +4 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gent-cli",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "15.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/pet.js
CHANGED
|
@@ -379,73 +379,122 @@ const SCENES = {
|
|
|
379
379
|
const HIDE = '\x1b[?25l';
|
|
380
380
|
const SHOW = '\x1b[?25h';
|
|
381
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
|
|
382
385
|
const up = (n) => `\x1b[${n}A`;
|
|
383
386
|
|
|
384
387
|
/**
|
|
385
|
-
* Play a scene.
|
|
386
|
-
*
|
|
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.
|
|
387
396
|
*
|
|
388
397
|
* @param {string} sceneName
|
|
389
398
|
* @param {object} opts
|
|
390
|
-
* @param {boolean} opts.loop
|
|
391
|
-
* @param {boolean} opts.footer
|
|
392
|
-
* @param {boolean} opts.goodbye
|
|
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
|
|
393
403
|
* @returns {Promise<void>}
|
|
394
404
|
*/
|
|
395
|
-
function
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
405
|
+
function runLoop(sceneName, {
|
|
406
|
+
loop = false, footer = true, goodbye = false, altScreen = false,
|
|
407
|
+
maxTicks = Infinity, clearOnStop = false, exitOnSig = false,
|
|
408
|
+
} = {}) {
|
|
409
|
+
const scene = SCENES[sceneName] || SCENES.idle;
|
|
410
|
+
const cv = new Canvas(CV_W, CV_H);
|
|
411
|
+
const state = { count: 0, tip: TIPS[Math.floor(Math.random() * TIPS.length)] };
|
|
412
|
+
const totalTicks = loop ? Infinity : Math.min(scene.cycle + 1, maxTicks);
|
|
413
|
+
const blockH = CV_H + (footer ? 1 : 0);
|
|
414
|
+
let t = 0;
|
|
415
|
+
let printed = false;
|
|
416
|
+
let done = false;
|
|
417
|
+
let resolveFn;
|
|
418
|
+
const promise = new Promise((r) => { resolveFn = r; });
|
|
419
|
+
|
|
420
|
+
const footerLine = () => footer
|
|
421
|
+
? chalk.gray(' ') +
|
|
422
|
+
(loop ? chalk.gray('Ctrl+C to cancel') : C.body('Genti')) +
|
|
423
|
+
chalk.gray(' Β· more scenes: ') + C.say('gent pet push|pull|merge')
|
|
424
|
+
: '';
|
|
425
|
+
|
|
426
|
+
const paint = () => {
|
|
427
|
+
cv.clear();
|
|
428
|
+
scene.fn(cv, t, state);
|
|
429
|
+
if (sceneName === 'idle' && loop && t > 0 && t % scene.cycle === 0) {
|
|
430
|
+
state.tip = TIPS[Math.floor(Math.random() * TIPS.length)];
|
|
431
|
+
}
|
|
432
|
+
const lines = cv.render().split('\n');
|
|
433
|
+
lines.push(footerLine());
|
|
434
|
+
const block = lines.map(l => CLEAR_LINE + l).join('\n');
|
|
435
|
+
if (altScreen) {
|
|
436
|
+
process.stdout.write(HOME + block);
|
|
437
|
+
} else {
|
|
421
438
|
if (printed) process.stdout.write(up(lines.length));
|
|
422
439
|
process.stdout.write(block + '\n');
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
const finish = () => {
|
|
427
|
-
if (done) return;
|
|
428
|
-
done = true;
|
|
429
|
-
clearInterval(timer);
|
|
430
|
-
process.removeListener('SIGINT', onSig);
|
|
431
|
-
process.stdout.write(SHOW);
|
|
432
|
-
if (goodbye) {
|
|
433
|
-
console.log(C.body(' Genti waves.') + chalk.gray(' Come back with ') + C.say('gent pet') + chalk.gray('.'));
|
|
434
|
-
}
|
|
435
|
-
resolve();
|
|
436
|
-
};
|
|
440
|
+
}
|
|
441
|
+
printed = true;
|
|
442
|
+
};
|
|
437
443
|
|
|
438
|
-
|
|
444
|
+
// Erase the inline block so the caller can print clean output in its place.
|
|
445
|
+
const eraseBlock = () => {
|
|
446
|
+
if (!printed) return;
|
|
447
|
+
process.stdout.write(up(blockH));
|
|
448
|
+
for (let i = 0; i < blockH; i++) process.stdout.write(CLEAR_LINE + (i < blockH - 1 ? '\n' : ''));
|
|
449
|
+
process.stdout.write(up(blockH - 1));
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
const stop = () => {
|
|
453
|
+
if (done) return;
|
|
454
|
+
done = true;
|
|
455
|
+
clearInterval(timer);
|
|
456
|
+
process.removeListener('SIGINT', onSig);
|
|
457
|
+
if (clearOnStop && !altScreen) eraseBlock();
|
|
458
|
+
process.stdout.write(SHOW + (altScreen ? LEAVE_ALT : ''));
|
|
459
|
+
if (goodbye) {
|
|
460
|
+
console.log(C.body(' Genti waves.') + chalk.gray(' Come back with ') + C.say('gent pet') + chalk.gray('.'));
|
|
461
|
+
}
|
|
462
|
+
resolveFn();
|
|
463
|
+
};
|
|
439
464
|
|
|
440
|
-
|
|
465
|
+
const onSig = () => { stop(); if (exitOnSig) process.exit(130); };
|
|
466
|
+
|
|
467
|
+
process.stdout.write((altScreen ? ENTER_ALT + HOME : '') + HIDE);
|
|
468
|
+
paint();
|
|
469
|
+
const timer = setInterval(() => {
|
|
470
|
+
t++;
|
|
471
|
+
if (t >= totalTicks) { paint(); return stop(); }
|
|
441
472
|
paint();
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
473
|
+
}, FRAME_MS);
|
|
474
|
+
process.on('SIGINT', onSig);
|
|
475
|
+
|
|
476
|
+
return { promise, stop };
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Play a scene to completion (or until Ctrl+C when looping). Resolves when done.
|
|
480
|
+
function play(sceneName, opts = {}) {
|
|
481
|
+
return runLoop(sceneName, opts).promise;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* A compact STATIC Genti header β safe to print above an interactive prompt
|
|
486
|
+
* (inquirer, etc.) because it never animates or moves the cursor.
|
|
487
|
+
* @param {string} greeting bold line beside the mascot
|
|
488
|
+
* @param {string} [sub] dimmer line under the greeting
|
|
489
|
+
*/
|
|
490
|
+
function banner(greeting, sub) {
|
|
491
|
+
if (process.env.GENT_NO_PET || !process.stdout.isTTY) return;
|
|
492
|
+
const cv = new Canvas(52, 9);
|
|
493
|
+
cv.sprite(mascot({}), 1, 0);
|
|
494
|
+
cv.sprite(legs('stand'), 1, 7);
|
|
495
|
+
cv.text(18, 2, greeting, C.body);
|
|
496
|
+
if (sub) cv.text(18, 4, sub, C.say);
|
|
497
|
+
console.log('\n' + cv.render());
|
|
449
498
|
}
|
|
450
499
|
|
|
451
500
|
// Static single frame for non-TTY (piped) output.
|
|
@@ -476,8 +525,9 @@ async function petCommand(scene, options = {}) {
|
|
|
476
525
|
}
|
|
477
526
|
|
|
478
527
|
if (!process.stdout.isTTY) return still(name);
|
|
479
|
-
// Default: play once
|
|
480
|
-
|
|
528
|
+
// Default: play once, on the alternate screen so nothing pollutes scrollback.
|
|
529
|
+
// `--loop` keeps it running until Ctrl+C.
|
|
530
|
+
await play(name, { loop: !!options.loop, footer: true, goodbye: true, altScreen: true });
|
|
481
531
|
}
|
|
482
532
|
|
|
483
533
|
/**
|
|
@@ -492,9 +542,38 @@ async function celebrate(scene) {
|
|
|
492
542
|
if (process.env.GENT_NO_PET || process.env.CI) return;
|
|
493
543
|
if (!SCENES[scene]) return;
|
|
494
544
|
console.log(); // one blank line between command output and Genti
|
|
495
|
-
|
|
545
|
+
// Auth flows just need a quick friendly wave; action scenes tell a fuller story.
|
|
546
|
+
const maxTicks = (scene === 'auth' || scene === 'login') ? 34 : Infinity;
|
|
547
|
+
await play(scene, { loop: false, footer: false, goodbye: false, maxTicks });
|
|
496
548
|
} catch (_) { /* ignore β decoration only */ }
|
|
497
549
|
}
|
|
498
550
|
|
|
551
|
+
/**
|
|
552
|
+
* Public: run `task` while Genti animates as the live loader β carrying the
|
|
553
|
+
* crate to the cloud on push, home on pull, etc. The animation loops until the
|
|
554
|
+
* task settles, then erases itself so the command can print its own result.
|
|
555
|
+
*
|
|
556
|
+
* Safe + transparent: with no TTY / GENT_NO_PET / CI it simply awaits the task
|
|
557
|
+
* with no animation. Always returns (or throws) exactly what `task` does.
|
|
558
|
+
*
|
|
559
|
+
* @param {string} scene 'push' | 'pull' | 'merge' | β¦
|
|
560
|
+
* @param {() => Promise<any>} task the real async work (e.g. the network call)
|
|
561
|
+
* @returns {Promise<any>}
|
|
562
|
+
*/
|
|
563
|
+
async function during(scene, task) {
|
|
564
|
+
if (!process.stdout.isTTY || process.env.GENT_NO_PET || process.env.CI || !SCENES[scene]) {
|
|
565
|
+
return task();
|
|
566
|
+
}
|
|
567
|
+
const anim = runLoop(scene, { loop: true, footer: true, altScreen: false, clearOnStop: true, exitOnSig: true });
|
|
568
|
+
try {
|
|
569
|
+
return await task();
|
|
570
|
+
} finally {
|
|
571
|
+
anim.stop();
|
|
572
|
+
await anim.promise;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
499
576
|
module.exports = petCommand;
|
|
500
577
|
module.exports.celebrate = celebrate;
|
|
578
|
+
module.exports.banner = banner;
|
|
579
|
+
module.exports.during = during;
|
package/src/commands/pull.js
CHANGED
|
@@ -73,14 +73,15 @@ async function pull(remoteName, branchName, options) {
|
|
|
73
73
|
|
|
74
74
|
// 1. Fetch commits + objects for this branch in a single call. `since`
|
|
75
75
|
// lets the server send only what we don't have on a fast-forward.
|
|
76
|
-
|
|
76
|
+
// Genti walks a crate home from the cloud while we fetch.
|
|
77
|
+
spinner.stop();
|
|
77
78
|
let pullData;
|
|
78
79
|
try {
|
|
79
80
|
const pullUrl = buildRepoUrl(API_ENDPOINTS.REPO_PULL, repoInfo);
|
|
80
81
|
const query = localHead
|
|
81
82
|
? `?branch=${encodeURIComponent(branch)}&since=${encodeURIComponent(localHead)}`
|
|
82
83
|
: `?branch=${encodeURIComponent(branch)}`;
|
|
83
|
-
pullData = await apiClient.get(pullUrl + query);
|
|
84
|
+
pullData = await pet.during('pull', () => apiClient.get(pullUrl + query));
|
|
84
85
|
} catch (error) {
|
|
85
86
|
if (error.response?.status === 404) {
|
|
86
87
|
spinner.succeed(chalk.green('Remote branch not found β nothing to pull'));
|
|
@@ -138,7 +139,6 @@ async function pull(remoteName, branchName, options) {
|
|
|
138
139
|
|
|
139
140
|
spinner.succeed(chalk.green(`Fast-forward: ${newCount} new commit(s)`));
|
|
140
141
|
console.log(chalk.gray(` ${remote}/${branch} β ${remoteHead.substring(0, 7)}`));
|
|
141
|
-
await pet.celebrate('pull');
|
|
142
142
|
} else {
|
|
143
143
|
// Diverged β need 3-way merge
|
|
144
144
|
spinner.text = 'Branches diverged, merging...';
|
|
@@ -194,7 +194,6 @@ async function pull(remoteName, branchName, options) {
|
|
|
194
194
|
} else {
|
|
195
195
|
spinner.succeed(chalk.green(`Merged ${newCount} remote commit(s)`));
|
|
196
196
|
console.log(chalk.gray(` Merge commit: ${mergeCommit.hash.substring(0, 7)}`));
|
|
197
|
-
await pet.celebrate('pull');
|
|
198
197
|
}
|
|
199
198
|
}
|
|
200
199
|
} catch (error) {
|
package/src/commands/push.js
CHANGED
|
@@ -232,20 +232,19 @@ async function push(remoteName, branchName, options) {
|
|
|
232
232
|
tags: tagsToPush
|
|
233
233
|
};
|
|
234
234
|
|
|
235
|
-
// Send to backend
|
|
235
|
+
// Send to backend β Genti carries the crate to the cloud while we wait.
|
|
236
236
|
const pushUrl = buildRepoUrl(API_ENDPOINTS.REPO_PUSH, repoInfo);
|
|
237
|
-
|
|
237
|
+
spinner.stop();
|
|
238
|
+
const response = await pet.during('push', () => apiClient.post(pushUrl, payload));
|
|
238
239
|
|
|
239
240
|
// Update remote ref
|
|
240
241
|
config.remoteRefs[`${remote}/${branch}`] = localHead;
|
|
241
242
|
await writeJSON(configPath, config);
|
|
242
243
|
|
|
243
|
-
|
|
244
|
+
console.log(chalk.green(`β Pushed ${commitsToPush.length} commit(s) to ${remote}/${branch}`));
|
|
244
245
|
console.log(chalk.gray(` ${localHead.substring(0, 7)} β ${remote}/${branch}`));
|
|
245
246
|
console.log(chalk.gray(` ${packBlobs.length} blob(s), ${packTrees.length} tree(s) transferred`));
|
|
246
247
|
|
|
247
|
-
await pet.celebrate('push');
|
|
248
|
-
|
|
249
248
|
} catch (error) {
|
|
250
249
|
spinner.fail(chalk.red('Push failed'));
|
|
251
250
|
|
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`));
|