gent-cli 12.0.0 → 13.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/merge.js +3 -0
- package/src/commands/pet.js +98 -46
- package/src/commands/pull.js +3 -0
- package/src/commands/push.js +3 -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": "13.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/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,87 @@ 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 up = (n) => `\x1b[${n}A`;
|
|
382
383
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
384
|
+
/**
|
|
385
|
+
* Play a scene. Redraws IN PLACE (no full-screen clear, no scrollback spam).
|
|
386
|
+
* Resolves when finished.
|
|
387
|
+
*
|
|
388
|
+
* @param {string} sceneName
|
|
389
|
+
* @param {object} opts
|
|
390
|
+
* @param {boolean} opts.loop keep looping until Ctrl+C (default: play once)
|
|
391
|
+
* @param {boolean} opts.footer show the "Ctrl+C to leave / try …" hint line
|
|
392
|
+
* @param {boolean} opts.goodbye print a farewell line when done
|
|
393
|
+
* @returns {Promise<void>}
|
|
394
|
+
*/
|
|
395
|
+
function play(sceneName, { loop = false, footer = true, goodbye = false } = {}) {
|
|
396
|
+
return new Promise((resolve) => {
|
|
397
|
+
const scene = SCENES[sceneName] || SCENES.idle;
|
|
398
|
+
const cv = new Canvas(CV_W, CV_H);
|
|
399
|
+
const state = { count: 0, tip: TIPS[Math.floor(Math.random() * TIPS.length)] };
|
|
400
|
+
const totalTicks = loop ? Infinity : scene.cycle + 1; // one clean cycle
|
|
401
|
+
let t = 0;
|
|
402
|
+
let printed = false;
|
|
403
|
+
let done = false;
|
|
404
|
+
|
|
405
|
+
const footerLine = () => footer
|
|
406
|
+
? chalk.gray(' ') +
|
|
407
|
+
(loop ? chalk.gray('Ctrl+C to leave') : C.body('Genti')) +
|
|
408
|
+
chalk.gray(' · more scenes: ') + C.say('gent pet push|pull|merge')
|
|
409
|
+
: '';
|
|
410
|
+
|
|
411
|
+
// Draw one frame, moving the cursor back over the previous frame.
|
|
412
|
+
const paint = () => {
|
|
413
|
+
cv.clear();
|
|
414
|
+
scene.fn(cv, t, state);
|
|
415
|
+
if (sceneName === 'idle' && loop && t > 0 && t % scene.cycle === 0) {
|
|
416
|
+
state.tip = TIPS[Math.floor(Math.random() * TIPS.length)];
|
|
417
|
+
}
|
|
418
|
+
const lines = cv.render().split('\n');
|
|
419
|
+
lines.push(footerLine());
|
|
420
|
+
const block = lines.map(l => CLEAR_LINE + l).join('\n');
|
|
421
|
+
if (printed) process.stdout.write(up(lines.length));
|
|
422
|
+
process.stdout.write(block + '\n');
|
|
423
|
+
printed = true;
|
|
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
|
+
};
|
|
437
|
+
|
|
438
|
+
const onSig = () => finish();
|
|
439
|
+
|
|
440
|
+
process.stdout.write(HIDE);
|
|
441
|
+
paint();
|
|
442
|
+
const timer = setInterval(() => {
|
|
443
|
+
t++;
|
|
444
|
+
if (t >= totalTicks) { paint(); return finish(); }
|
|
445
|
+
paint();
|
|
446
|
+
}, FRAME_MS);
|
|
447
|
+
process.on('SIGINT', onSig);
|
|
448
|
+
});
|
|
414
449
|
}
|
|
415
450
|
|
|
416
451
|
// Static single frame for non-TTY (piped) output.
|
|
@@ -419,10 +454,9 @@ function still(sceneName) {
|
|
|
419
454
|
const state = { count: 1, tip: TIPS[Math.floor(Math.random() * TIPS.length)] };
|
|
420
455
|
(SCENES[sceneName] || SCENES.idle).fn(cv, 12, state);
|
|
421
456
|
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
457
|
}
|
|
424
458
|
|
|
425
|
-
// ──
|
|
459
|
+
// ── Public: standalone `gent pet` command ────────────────────────────────
|
|
426
460
|
async function petCommand(scene, options = {}) {
|
|
427
461
|
if (process.env.NO_COLOR || (options && options.color === false)) chalk.level = 0;
|
|
428
462
|
|
|
@@ -435,14 +469,32 @@ async function petCommand(scene, options = {}) {
|
|
|
435
469
|
return;
|
|
436
470
|
}
|
|
437
471
|
|
|
438
|
-
// Signed-out nudge:
|
|
439
|
-
if (name === 'idle'
|
|
472
|
+
// Signed-out nudge: bare `gent pet` greets you at the door.
|
|
473
|
+
if (name === 'idle') {
|
|
440
474
|
const authed = await authStorage.isAuthenticated().catch(() => false);
|
|
441
475
|
if (!authed) name = 'auth';
|
|
442
476
|
}
|
|
443
477
|
|
|
444
478
|
if (!process.stdout.isTTY) return still(name);
|
|
445
|
-
play
|
|
479
|
+
// Default: play once. `--loop` keeps it running until Ctrl+C.
|
|
480
|
+
await play(name, { loop: !!options.loop, footer: true, goodbye: true });
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Public: a one-shot celebration other commands fire after they succeed.
|
|
485
|
+
* Silent + safe in non-interactive contexts (CI, pipes, GENT_NO_PET=1).
|
|
486
|
+
* Never throws — a mascot must never break a real command.
|
|
487
|
+
*/
|
|
488
|
+
async function celebrate(scene) {
|
|
489
|
+
try {
|
|
490
|
+
if (!process.stdout.isTTY) return;
|
|
491
|
+
if (process.env.NO_COLOR) { /* still animate, just uncolored */ }
|
|
492
|
+
if (process.env.GENT_NO_PET || process.env.CI) return;
|
|
493
|
+
if (!SCENES[scene]) return;
|
|
494
|
+
console.log(); // one blank line between command output and Genti
|
|
495
|
+
await play(scene, { loop: false, footer: false, goodbye: false });
|
|
496
|
+
} catch (_) { /* ignore — decoration only */ }
|
|
446
497
|
}
|
|
447
498
|
|
|
448
499
|
module.exports = petCommand;
|
|
500
|
+
module.exports.celebrate = celebrate;
|
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/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
|