gent-cli 11.0.0 → 12.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/pet.js +448 -0
- package/src/index.js +11 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gent-cli",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "12.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": {
|
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pet Command — "Genti", the gent mascot.
|
|
3
|
+
*
|
|
4
|
+
* A chunky pixel-block creature that lives in your terminal and *acts out*
|
|
5
|
+
* gent workflows:
|
|
6
|
+
*
|
|
7
|
+
* gent pet → idle: Genti breathes, blinks, waves, drops a tip
|
|
8
|
+
* gent pet push → walks a file crate to the cloud, over and over
|
|
9
|
+
* gent pet pull → carries a crate back from the cloud
|
|
10
|
+
* gent pet merge → stands between two branches and "thinks" them together
|
|
11
|
+
* gent pet auth → little sign-in scene
|
|
12
|
+
*
|
|
13
|
+
* --once play a few cycles, then exit (good for scripts / shell startup)
|
|
14
|
+
* --no-color plain output (also honors NO_COLOR)
|
|
15
|
+
*
|
|
16
|
+
* Rendering: everything is drawn onto a colored character Canvas (one cell =
|
|
17
|
+
* one terminal column with its own color), then flushed as ANSI each frame.
|
|
18
|
+
* This keeps every sprite pixel-aligned regardless of color runs.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const chalk = require('chalk');
|
|
22
|
+
const authStorage = require('../utils/auth-storage');
|
|
23
|
+
|
|
24
|
+
const FRAME_MS = 90;
|
|
25
|
+
|
|
26
|
+
// ── Palette ──────────────────────────────────────────────────────────────
|
|
27
|
+
const C = {
|
|
28
|
+
body: chalk.hex('#c97b5a'), // Genti's orange skin
|
|
29
|
+
shade: chalk.hex('#9c5a3f'), // bottom shading
|
|
30
|
+
eye: chalk.hex('#15110f'), // dark eye holes
|
|
31
|
+
mouth: chalk.hex('#5a2f22'),
|
|
32
|
+
crate: chalk.hex('#e0b64d'), // file crate edges
|
|
33
|
+
crateIn:chalk.hex('#b98a1f'), // crate fill
|
|
34
|
+
cloud: chalk.hex('#d6def0'), // remote / cloud
|
|
35
|
+
cloudSh:chalk.hex('#8a93ad'),
|
|
36
|
+
spark: chalk.hex('#ffe08a'),
|
|
37
|
+
branchA:chalk.hex('#5ac8c9'),
|
|
38
|
+
branchB:chalk.hex('#c98ad6'),
|
|
39
|
+
node: chalk.hex('#8ae06a'),
|
|
40
|
+
ok: chalk.hex('#8ae06a'),
|
|
41
|
+
dim: chalk.gray,
|
|
42
|
+
say: chalk.hex('#e6e6e6'),
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// legend char → { color fn, glyph }
|
|
46
|
+
const INK = {
|
|
47
|
+
'#': [C.body, '█'],
|
|
48
|
+
'@': [C.shade, '█'],
|
|
49
|
+
'O': [C.eye, '█'],
|
|
50
|
+
'_': [C.mouth, '▄'],
|
|
51
|
+
'o': [C.mouth, '▄'],
|
|
52
|
+
'=': [C.crate, '█'],
|
|
53
|
+
':': [C.crateIn, '▒'],
|
|
54
|
+
'%': [C.cloud, '█'],
|
|
55
|
+
'&': [C.cloudSh, '█'],
|
|
56
|
+
'*': [C.spark, '✦'],
|
|
57
|
+
'|': [C.branchA, '│'],
|
|
58
|
+
'/': [C.branchA, '╱'],
|
|
59
|
+
'A': [C.branchA, '●'],
|
|
60
|
+
'\\':[C.branchB, '╲'],
|
|
61
|
+
'B': [C.branchB, '●'],
|
|
62
|
+
'M': [C.node, '●'],
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// ── Canvas ───────────────────────────────────────────────────────────────
|
|
66
|
+
class Canvas {
|
|
67
|
+
constructor(w, h) {
|
|
68
|
+
this.w = w; this.h = h;
|
|
69
|
+
this.clear();
|
|
70
|
+
}
|
|
71
|
+
clear() {
|
|
72
|
+
this.cells = Array.from({ length: this.h }, () =>
|
|
73
|
+
Array.from({ length: this.w }, () => ({ ch: ' ', fn: null })));
|
|
74
|
+
}
|
|
75
|
+
put(x, y, ch, fn) {
|
|
76
|
+
if (y < 0 || y >= this.h || x < 0 || x >= this.w) return;
|
|
77
|
+
this.cells[y][x] = { ch, fn };
|
|
78
|
+
}
|
|
79
|
+
// Draw a sprite (array of strings). ' ' and '.' are transparent.
|
|
80
|
+
sprite(rows, x, y) {
|
|
81
|
+
rows.forEach((row, dy) => {
|
|
82
|
+
for (let dx = 0; dx < row.length; dx++) {
|
|
83
|
+
const c = row[dx];
|
|
84
|
+
if (c === ' ' || c === '.') continue;
|
|
85
|
+
const ink = INK[c];
|
|
86
|
+
if (ink) this.put(x + dx, y + dy, ink[1], ink[0]);
|
|
87
|
+
else this.put(x + dx, y + dy, c, C.say);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
text(x, y, str, fn) {
|
|
92
|
+
for (let i = 0; i < str.length; i++) this.put(x + i, y, str[i], fn);
|
|
93
|
+
}
|
|
94
|
+
render() {
|
|
95
|
+
const out = [];
|
|
96
|
+
for (let y = 0; y < this.h; y++) {
|
|
97
|
+
let line = '';
|
|
98
|
+
let run = '';
|
|
99
|
+
let runFn = null;
|
|
100
|
+
const flush = () => {
|
|
101
|
+
if (!run) return;
|
|
102
|
+
line += runFn ? runFn(run) : run;
|
|
103
|
+
run = '';
|
|
104
|
+
};
|
|
105
|
+
for (let x = 0; x < this.w; x++) {
|
|
106
|
+
const cell = this.cells[y][x];
|
|
107
|
+
if (cell.fn !== runFn) { flush(); runFn = cell.fn; }
|
|
108
|
+
run += cell.ch;
|
|
109
|
+
}
|
|
110
|
+
flush();
|
|
111
|
+
out.push(line);
|
|
112
|
+
}
|
|
113
|
+
return out.join('\n');
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ── Mascot sprite ────────────────────────────────────────────────────────
|
|
118
|
+
// Body is a 12-wide block with a 1-col margin (transparent) each side.
|
|
119
|
+
function mascot({ blink = false, mouth = '_', armUp = false } = {}) {
|
|
120
|
+
const W = 12;
|
|
121
|
+
const rows = [];
|
|
122
|
+
rows[0] = ' ' + '#'.repeat(W) + ' ';
|
|
123
|
+
rows[1] = ' ' + '#'.repeat(W) + ' ';
|
|
124
|
+
rows[2] = ' ' + '#'.repeat(W) + ' '; // eyes row
|
|
125
|
+
rows[3] = ' ' + '#'.repeat(W) + ' ';
|
|
126
|
+
rows[4] = ' ' + '#'.repeat(W) + ' '; // mouth row
|
|
127
|
+
rows[5] = ' ' + '@'.repeat(W) + ' '; // shaded chin
|
|
128
|
+
|
|
129
|
+
const grid = rows.map(r => r.split(''));
|
|
130
|
+
const eye = blink ? '#' : 'O';
|
|
131
|
+
// eyes at body cols 3-4 and 8-9 → +1 for margin
|
|
132
|
+
[3, 4].forEach(c => grid[2][c + 1] = eye);
|
|
133
|
+
[8, 9].forEach(c => grid[2][c + 1] = eye);
|
|
134
|
+
// mouth at cols 5-8 (row4)
|
|
135
|
+
for (let c = 5; c <= 8; c++) grid[4][c + 1] = mouth;
|
|
136
|
+
// ears (stick out at row2)
|
|
137
|
+
grid[2][0] = '#';
|
|
138
|
+
grid[2][W + 1] = '#';
|
|
139
|
+
|
|
140
|
+
let body = grid.map(r => r.join(''));
|
|
141
|
+
|
|
142
|
+
// arm (raised wave) sits to the right of the head on row1
|
|
143
|
+
if (armUp) {
|
|
144
|
+
const r1 = body[1].split('');
|
|
145
|
+
r1[W + 1] = '#';
|
|
146
|
+
body[1] = r1.join('');
|
|
147
|
+
body.unshift(' #'); // tiny raised hand
|
|
148
|
+
} else {
|
|
149
|
+
body.unshift(' ');
|
|
150
|
+
}
|
|
151
|
+
return body; // 7 rows (incl. leading arm/space row), 14 wide
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Legs are separate so they can shuffle while the body glides.
|
|
155
|
+
function legs(step) {
|
|
156
|
+
// step: 'stand' | 'a' | 'b'
|
|
157
|
+
const map = {
|
|
158
|
+
stand: ' ## ## ',
|
|
159
|
+
a: ' ## # ',
|
|
160
|
+
b: ' # ## ',
|
|
161
|
+
};
|
|
162
|
+
return [map[step] || map.stand];
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ── Props ────────────────────────────────────────────────────────────────
|
|
166
|
+
const CRATE = [
|
|
167
|
+
'======',
|
|
168
|
+
'=::::=',
|
|
169
|
+
'=::::=',
|
|
170
|
+
'======',
|
|
171
|
+
];
|
|
172
|
+
|
|
173
|
+
const CLOUD = [
|
|
174
|
+
' %%%%%% ',
|
|
175
|
+
'%%%%%%%%%%',
|
|
176
|
+
'&&%%%%%%&&',
|
|
177
|
+
];
|
|
178
|
+
|
|
179
|
+
// ── Speech bubble (drawn straight to canvas as text) ─────────────────────
|
|
180
|
+
function drawBubble(cv, line1, line2, color) {
|
|
181
|
+
const w = Math.max(line1.length, (line2 || '').length);
|
|
182
|
+
cv.text(2, 0, '╭' + '─'.repeat(w + 2) + '╮', C.dim);
|
|
183
|
+
cv.text(2, 1, '│ ', C.dim);
|
|
184
|
+
cv.text(4, 1, line1.padEnd(w), color || C.say);
|
|
185
|
+
cv.text(4 + w, 1, ' │', C.dim);
|
|
186
|
+
if (line2) {
|
|
187
|
+
cv.text(2, 2, '│ ', C.dim);
|
|
188
|
+
cv.text(4, 2, line2.padEnd(w), chalk.bold.white);
|
|
189
|
+
cv.text(4 + w, 2, ' │', C.dim);
|
|
190
|
+
cv.text(2, 3, '╰┬' + '─'.repeat(w) + '─╯', C.dim);
|
|
191
|
+
cv.text(4, 4, '╲', C.dim);
|
|
192
|
+
} else {
|
|
193
|
+
cv.text(2, 2, '╰┬' + '─'.repeat(w) + '─╯', C.dim);
|
|
194
|
+
cv.text(4, 3, '╲', C.dim);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ── Scenes ───────────────────────────────────────────────────────────────
|
|
199
|
+
// Every scene fills a fresh canvas for tick `t`. Stage lives below the bubble.
|
|
200
|
+
const STAGE_TOP = 5; // first row used by the world floor
|
|
201
|
+
const CV_W = 52;
|
|
202
|
+
const CV_H = 15;
|
|
203
|
+
|
|
204
|
+
function walkStep(t) { return (Math.floor(t / 3) % 2) ? 'a' : 'b'; }
|
|
205
|
+
|
|
206
|
+
function sceneIdle(cv, t, tip) {
|
|
207
|
+
drawBubble(cv, tip.say, tip.cmd ? '$ ' + tip.cmd : null, tip.color);
|
|
208
|
+
const bob = (Math.floor(t / 8) % 2); // gentle breathing
|
|
209
|
+
const blink = (t % 40) < 2;
|
|
210
|
+
const wave = (t % 60) < 12; // occasional wave
|
|
211
|
+
const y = STAGE_TOP + bob;
|
|
212
|
+
cv.sprite(mascot({ blink, armUp: wave }), 6, y);
|
|
213
|
+
cv.sprite(legs('stand'), 6, y + 7);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function sceneAuth(cv, t) {
|
|
217
|
+
const stages = [
|
|
218
|
+
['Knock knock — let me in!', 'gent login'],
|
|
219
|
+
['New around here?', 'gent register'],
|
|
220
|
+
['Who am I again?', 'gent whoami'],
|
|
221
|
+
];
|
|
222
|
+
const idx = Math.floor(t / 34) % stages.length;
|
|
223
|
+
const [say, cmd] = stages[idx];
|
|
224
|
+
drawBubble(cv, say, '$ ' + cmd, C.branchA);
|
|
225
|
+
const blink = (t % 30) < 2;
|
|
226
|
+
const y = STAGE_TOP + (Math.floor(t / 8) % 2);
|
|
227
|
+
// a little door/key on the right
|
|
228
|
+
cv.sprite(['%%%%', '%::%', '%::%', '%%%%'], 34, STAGE_TOP + 1);
|
|
229
|
+
cv.text(34, STAGE_TOP, ' remote', C.cloudSh);
|
|
230
|
+
cv.sprite(mascot({ blink, armUp: (t % 40) < 14 }), 6, y);
|
|
231
|
+
cv.sprite(legs('stand'), 6, y + 7);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// PUSH: walk crate right → toss into cloud → walk back. Repeat.
|
|
235
|
+
function scenePush(cv, t, state) {
|
|
236
|
+
drawBubble(cv, 'Pushing your commits to the cloud…', '$ gent push', C.ok);
|
|
237
|
+
const cloudX = 40, cloudY = STAGE_TOP;
|
|
238
|
+
cv.sprite(CLOUD, cloudX, cloudY);
|
|
239
|
+
cv.text(cloudX + 2, cloudY + 3, 'remote', C.cloudSh);
|
|
240
|
+
|
|
241
|
+
const CYCLE = 66;
|
|
242
|
+
const p = t % CYCLE;
|
|
243
|
+
const my = STAGE_TOP + 1;
|
|
244
|
+
|
|
245
|
+
const startX = 4, turnX = 30;
|
|
246
|
+
if (p < 26) {
|
|
247
|
+
// walk out carrying crate
|
|
248
|
+
const mx = startX + Math.round((turnX - startX) * (p / 26));
|
|
249
|
+
cv.sprite(mascot({ blink: (t % 22) < 2 }), mx, my);
|
|
250
|
+
cv.sprite(legs(walkStep(t)), mx, my + 7);
|
|
251
|
+
cv.sprite(CRATE, mx + 14, my + 2);
|
|
252
|
+
} else if (p < 34) {
|
|
253
|
+
// toss: crate flies up-right into the cloud, sparkle
|
|
254
|
+
const k = (p - 26) / 8;
|
|
255
|
+
const bx = Math.round(turnX + 14 + (cloudX - (turnX + 14)) * k);
|
|
256
|
+
const by = Math.round((my + 2) - 3 * k);
|
|
257
|
+
cv.sprite(mascot({ mouth: 'o', armUp: true }), turnX, my);
|
|
258
|
+
cv.sprite(legs('stand'), turnX, my + 7);
|
|
259
|
+
cv.sprite(CRATE, bx, by);
|
|
260
|
+
if (k > 0.6) cv.text(cloudX + 4, cloudY + 1, '*', C.spark);
|
|
261
|
+
} else if (p < 58) {
|
|
262
|
+
// walk back empty-handed
|
|
263
|
+
const mx = turnX - Math.round((turnX - startX) * ((p - 34) / 24));
|
|
264
|
+
cv.sprite(mascot({ blink: (t % 22) < 2 }), mx, my);
|
|
265
|
+
cv.sprite(legs(walkStep(t)), mx, my + 7);
|
|
266
|
+
} else {
|
|
267
|
+
// brief cheer + count up
|
|
268
|
+
if (p === 58) state.count++;
|
|
269
|
+
cv.sprite(mascot({ mouth: 'o', armUp: (p % 4 < 2) }), startX, my);
|
|
270
|
+
cv.sprite(legs('stand'), startX, my + 7);
|
|
271
|
+
cv.text(cloudX + 1, cloudY + 1, '* *', C.spark);
|
|
272
|
+
}
|
|
273
|
+
cv.text(2, CV_H - 1, `pushed ${state.count} crate${state.count === 1 ? '' : 's'} ✓`, C.ok);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// PULL: crate drops from cloud → Genti catches → carries it home.
|
|
277
|
+
function scenePull(cv, t, state) {
|
|
278
|
+
drawBubble(cv, 'Pulling the latest from remote…', '$ gent pull', C.branchA);
|
|
279
|
+
const cloudX = 40, cloudY = STAGE_TOP;
|
|
280
|
+
cv.sprite(CLOUD, cloudX, cloudY);
|
|
281
|
+
cv.text(cloudX + 2, cloudY + 3, 'remote', C.cloudSh);
|
|
282
|
+
|
|
283
|
+
const CYCLE = 64;
|
|
284
|
+
const p = t % CYCLE;
|
|
285
|
+
const my = STAGE_TOP + 1;
|
|
286
|
+
const homeX = 4, catchX = 30;
|
|
287
|
+
|
|
288
|
+
if (p < 10) {
|
|
289
|
+
// Genti walks out to meet the delivery
|
|
290
|
+
const mx = homeX + Math.round((catchX - homeX) * (p / 10));
|
|
291
|
+
cv.sprite(mascot({ blink: (t % 20) < 2 }), mx, my);
|
|
292
|
+
cv.sprite(legs(walkStep(t)), mx, my + 7);
|
|
293
|
+
} else if (p < 20) {
|
|
294
|
+
// crate falls from cloud toward Genti's hands
|
|
295
|
+
const k = (p - 10) / 10;
|
|
296
|
+
const bx = Math.round(cloudX - (cloudX - (catchX + 14)) * k);
|
|
297
|
+
const by = Math.round((cloudY + 2) + ((my + 2) - (cloudY + 2)) * k);
|
|
298
|
+
cv.sprite(mascot({ mouth: 'o', armUp: true }), catchX, my);
|
|
299
|
+
cv.sprite(legs('stand'), catchX, my + 7);
|
|
300
|
+
cv.sprite(CRATE, bx, by);
|
|
301
|
+
} else if (p < 46) {
|
|
302
|
+
// carry it home
|
|
303
|
+
const mx = catchX - Math.round((catchX - homeX) * ((p - 20) / 26));
|
|
304
|
+
cv.sprite(mascot({ blink: (t % 22) < 2 }), mx, my);
|
|
305
|
+
cv.sprite(legs(walkStep(t)), mx, my + 7);
|
|
306
|
+
cv.sprite(CRATE, mx + 14, my + 2);
|
|
307
|
+
} else {
|
|
308
|
+
if (p === 46) state.count++;
|
|
309
|
+
cv.sprite(mascot({ mouth: 'o' }), homeX, my);
|
|
310
|
+
cv.sprite(legs('stand'), homeX, my + 7);
|
|
311
|
+
cv.sprite(CRATE, homeX + 14, my + 2);
|
|
312
|
+
cv.text(homeX + 6, my - 1, '*', C.spark);
|
|
313
|
+
}
|
|
314
|
+
cv.text(2, CV_H - 1, `pulled ${state.count} crate${state.count === 1 ? '' : 's'} ✓`, C.branchA);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// MERGE: two branches converge; Genti thinks, then a merge node lights up.
|
|
318
|
+
function sceneMerge(cv, t, state) {
|
|
319
|
+
const CYCLE = 70;
|
|
320
|
+
const p = t % CYCLE;
|
|
321
|
+
const thinking = p < 40;
|
|
322
|
+
const dots = '.'.repeat(1 + (Math.floor(t / 5) % 3));
|
|
323
|
+
drawBubble(cv,
|
|
324
|
+
thinking ? `Reconciling two histories${dots}` : 'Merged cleanly — no conflicts!',
|
|
325
|
+
'$ gent merge feature',
|
|
326
|
+
thinking ? C.branchB : C.ok);
|
|
327
|
+
|
|
328
|
+
const my = STAGE_TOP + 1;
|
|
329
|
+
const baseY = my + 3;
|
|
330
|
+
// branch A (top) flows down-right, branch B (bottom) flows up-right, meet at node
|
|
331
|
+
const nodeX = 40, nodeY = baseY;
|
|
332
|
+
for (let x = 24; x < nodeX; x++) {
|
|
333
|
+
const ay = baseY - 3 + Math.round((x - 24) / (nodeX - 24) * 3);
|
|
334
|
+
const by = baseY + 3 - Math.round((x - 24) / (nodeX - 24) * 3);
|
|
335
|
+
cv.put(x, ay, '╲'.length ? '╲' : '\\', C.branchA);
|
|
336
|
+
cv.put(x, by, '╱', C.branchB);
|
|
337
|
+
}
|
|
338
|
+
cv.put(23, baseY - 3, '●', C.branchA);
|
|
339
|
+
cv.put(23, baseY + 3, '●', C.branchB);
|
|
340
|
+
|
|
341
|
+
if (thinking) {
|
|
342
|
+
// thought bubble ". o O" above head
|
|
343
|
+
cv.text(18, my - 1, '. o O', C.dim);
|
|
344
|
+
cv.sprite(mascot({ mouth: 'o', blink: (t % 16) < 2 }), 6, my);
|
|
345
|
+
} else {
|
|
346
|
+
if (p === 40) state.count++;
|
|
347
|
+
cv.put(nodeX, nodeY, '●', C.node);
|
|
348
|
+
cv.text(nodeX - 1, nodeY - 1, '*', C.spark);
|
|
349
|
+
cv.text(nodeX - 1, nodeY + 1, '*', C.spark);
|
|
350
|
+
cv.sprite(mascot({ mouth: 'o', armUp: (p % 4 < 2) }), 6, my);
|
|
351
|
+
}
|
|
352
|
+
cv.sprite(legs('stand'), 6, my + 7);
|
|
353
|
+
cv.text(2, CV_H - 1, `merges resolved ${state.count} ✓`, C.node);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// ── Idle tips (used only by the idle scene) ─────────────────────────────
|
|
357
|
+
const TIPS = [
|
|
358
|
+
{ say: 'Ready when you are.', cmd: 'gent status', color: C.body },
|
|
359
|
+
{ say: 'Publish your work to the cloud.', cmd: 'gent push', color: C.ok },
|
|
360
|
+
{ say: 'Grab everyone else\'s changes.', cmd: 'gent pull', color: C.branchA },
|
|
361
|
+
{ say: 'New idea? Branch it.', cmd: 'gent checkout -b idea', color: C.spark },
|
|
362
|
+
{ say: 'Ask me about this repo.', cmd: 'gent ask "what is this?"', color: C.branchB },
|
|
363
|
+
{ say: 'Want a review of your diff?', cmd: 'gent review', color: C.node },
|
|
364
|
+
{ say: 'Commit small, commit often.', cmd: 'gent commit -m "wip"', color: C.body },
|
|
365
|
+
];
|
|
366
|
+
|
|
367
|
+
// ── Scene registry ───────────────────────────────────────────────────────
|
|
368
|
+
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) },
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
// ── ANSI helpers ─────────────────────────────────────────────────────────
|
|
378
|
+
const HOME = '[H';
|
|
379
|
+
const CLEAR = '[2J[H';
|
|
380
|
+
const HIDE = '[?25l';
|
|
381
|
+
const SHOW = '[?25h';
|
|
382
|
+
|
|
383
|
+
function play(sceneName, { once }) {
|
|
384
|
+
const scene = SCENES[sceneName] || SCENES.idle;
|
|
385
|
+
const cv = new Canvas(CV_W, CV_H);
|
|
386
|
+
const state = { count: 0, tip: TIPS[Math.floor(Math.random() * TIPS.length)] };
|
|
387
|
+
let t = 0;
|
|
388
|
+
|
|
389
|
+
process.stdout.write(HIDE + CLEAR);
|
|
390
|
+
|
|
391
|
+
const footer = () =>
|
|
392
|
+
chalk.gray(' scene: ') + C.body(sceneName) +
|
|
393
|
+
chalk.gray(' · try: ') + C.say('gent pet push|pull|merge') +
|
|
394
|
+
chalk.gray(' · Ctrl+C to leave');
|
|
395
|
+
|
|
396
|
+
const bye = () => {
|
|
397
|
+
process.stdout.write(SHOW + '\n');
|
|
398
|
+
console.log(C.body(' Genti waves. ') + chalk.gray('Come back with ') + C.say('gent pet') + chalk.gray('.'));
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
const timer = setInterval(() => {
|
|
402
|
+
cv.clear();
|
|
403
|
+
scene.fn(cv, t, state);
|
|
404
|
+
// rotate idle tip every ~6s
|
|
405
|
+
if (sceneName === 'idle' && t > 0 && t % 66 === 0) {
|
|
406
|
+
state.tip = TIPS[Math.floor(Math.random() * TIPS.length)];
|
|
407
|
+
}
|
|
408
|
+
process.stdout.write(HOME + cv.render() + '\n' + footer() + '\n');
|
|
409
|
+
t++;
|
|
410
|
+
if (once && t > 130) { clearInterval(timer); bye(); process.exit(0); }
|
|
411
|
+
}, FRAME_MS);
|
|
412
|
+
|
|
413
|
+
process.on('SIGINT', () => { clearInterval(timer); bye(); process.exit(0); });
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Static single frame for non-TTY (piped) output.
|
|
417
|
+
function still(sceneName) {
|
|
418
|
+
const cv = new Canvas(CV_W, CV_H);
|
|
419
|
+
const state = { count: 1, tip: TIPS[Math.floor(Math.random() * TIPS.length)] };
|
|
420
|
+
(SCENES[sceneName] || SCENES.idle).fn(cv, 12, state);
|
|
421
|
+
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
|
+
}
|
|
424
|
+
|
|
425
|
+
// ── Entry ────────────────────────────────────────────────────────────────
|
|
426
|
+
async function petCommand(scene, options = {}) {
|
|
427
|
+
if (process.env.NO_COLOR || (options && options.color === false)) chalk.level = 0;
|
|
428
|
+
|
|
429
|
+
let name = (scene || 'idle').toLowerCase();
|
|
430
|
+
if (!SCENES[name]) {
|
|
431
|
+
console.log(chalk.yellow(`Genti doesn't know the scene "${name}".`));
|
|
432
|
+
console.log(chalk.gray('Try: ') + C.say('gent pet') + chalk.gray(' · ') +
|
|
433
|
+
C.say('push') + chalk.gray(' · ') + C.say('pull') + chalk.gray(' · ') +
|
|
434
|
+
C.say('merge') + chalk.gray(' · ') + C.say('auth'));
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Signed-out nudge: default idle → auth scene the first time.
|
|
439
|
+
if (name === 'idle' && !options.stay) {
|
|
440
|
+
const authed = await authStorage.isAuthenticated().catch(() => false);
|
|
441
|
+
if (!authed) name = 'auth';
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (!process.stdout.isTTY) return still(name);
|
|
445
|
+
play(name, { once: !!options.once });
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
module.exports = petCommand;
|
package/src/index.js
CHANGED
|
@@ -78,6 +78,7 @@ const webCommand = require('./commands/web');
|
|
|
78
78
|
const shareCommand = require('./commands/share');
|
|
79
79
|
const searchCommand = require('./commands/search');
|
|
80
80
|
const templateCommand = require('./commands/template');
|
|
81
|
+
const petCommand = require('./commands/pet');
|
|
81
82
|
|
|
82
83
|
// Configure CLI
|
|
83
84
|
program
|
|
@@ -432,6 +433,14 @@ program
|
|
|
432
433
|
.option('-e, --email <email>', 'Account email (for reset)')
|
|
433
434
|
.action(passwordCommand);
|
|
434
435
|
|
|
436
|
+
// ─── Fun ────────────────────────────────────────────────
|
|
437
|
+
program
|
|
438
|
+
.command('pet [scene]')
|
|
439
|
+
.description('Meet Genti — an animated pixel mascot that acts out gent (push|pull|merge|auth)')
|
|
440
|
+
.option('--once', 'Play a few cycles, then exit (good for scripts / shell startup)')
|
|
441
|
+
.option('--stay', 'Stay on the idle scene even when signed out')
|
|
442
|
+
.action(petCommand);
|
|
443
|
+
|
|
435
444
|
// Help command
|
|
436
445
|
program
|
|
437
446
|
.command('help [command]')
|
|
@@ -456,6 +465,7 @@ const HELP_GROUPS = [
|
|
|
456
465
|
['Account', ['register', 'login', 'logout', 'whoami', 'password']],
|
|
457
466
|
['AI', ['ask', 'review', 'docs', 'changelog', 'ai']],
|
|
458
467
|
['Config & tools', ['config', 'doctor', 'template', 'help']],
|
|
468
|
+
['Fun', ['pet']],
|
|
459
469
|
];
|
|
460
470
|
|
|
461
471
|
function configureGroupedHelp(program) {
|
|
@@ -526,6 +536,7 @@ function showQuickstart() {
|
|
|
526
536
|
console.log(` ${chalk.cyan('gent changelog')} ${chalk.gray('grouped release notes')}`);
|
|
527
537
|
console.log();
|
|
528
538
|
console.log(chalk.gray('Full command list: ') + chalk.cyan('gent --help'));
|
|
539
|
+
console.log(chalk.gray('Say hi to your mascot: ') + chalk.cyan('gent pet'));
|
|
529
540
|
console.log();
|
|
530
541
|
}
|
|
531
542
|
|