gitroots 0.6.1 → 0.6.3
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/runtime.js +1 -1
- package/src/session/logo.js +32 -104
- package/src/session/repl.js +18 -15
- package/src/ui/ansi.js +2 -0
package/package.json
CHANGED
package/src/runtime.js
CHANGED
|
@@ -5,7 +5,7 @@ import os from 'node:os';
|
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
|
|
8
|
-
export const VERSION = '0.6.
|
|
8
|
+
export const VERSION = '0.6.3';
|
|
9
9
|
|
|
10
10
|
// Default download location for install.sh / gitroots update. Override with GITROOTS_DOWNLOAD_BASE.
|
|
11
11
|
export const DOWNLOAD_BASE = process.env.GITROOTS_DOWNLOAD_BASE || 'https://gitroots.com/cli';
|
package/src/session/logo.js
CHANGED
|
@@ -1,107 +1,35 @@
|
|
|
1
|
-
// The
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
//
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
function canvas() {
|
|
30
|
-
const word = [...'gitroots'].reduce((rows, ch, i) => rows.map((r, y) => r + (i ? '.' : '') + GLYPHS[ch][y]), Array(8).fill(''));
|
|
31
|
-
let H = Math.max(MARK, 8);
|
|
32
|
-
if (H % 2) H += 1;
|
|
33
|
-
const W = MARK + SPACE + word[0].length;
|
|
34
|
-
const px = Array.from({ length: H }, () => Array(W).fill(null));
|
|
35
|
-
for (let r = 0; r < 4; r++) {
|
|
36
|
-
for (let col = 0; col < 4; col++) {
|
|
37
|
-
for (let y = 0; y < CELL; y++) for (let x = 0; x < CELL; x++) px[r * (CELL + GAP) + y][col * (CELL + GAP) + x] = CELLS[r][col];
|
|
38
|
-
}
|
|
1
|
+
// The welcome header from the gitroots CLI design:
|
|
2
|
+
//
|
|
3
|
+
// [logo] │ v0.6.2 · code governance and agent sessions
|
|
4
|
+
// │ Secure it to the root, scale with confidence.
|
|
5
|
+
//
|
|
6
|
+
// Terminals that can show images (iTerm2, Ghostty, WezTerm, Kitty) get the real assets/logo.png.
|
|
7
|
+
// Others (Terminal.app, …) print a plain bold "gitroots" in its place, as the design specifies.
|
|
8
|
+
import { c, width, state as colorState } from '../ui/ansi.js';
|
|
9
|
+
import { imageProtocol, logoImage } from '../ui/term.js';
|
|
10
|
+
|
|
11
|
+
const IMG_ROWS = 3;
|
|
12
|
+
const IMG_COLS = 22; // logo.png is 3.38:1; terminal cells are about 1:2
|
|
13
|
+
|
|
14
|
+
const word = (s) => (colorState.color ? `\x1b[1m${colorState.truecolor ? '\x1b[38;2;227;237;252m' : '\x1b[38;5;255m'}${s}\x1b[39;22m` : s);
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Print the header. `lines` are the text lines beside the divider (two in the design).
|
|
18
|
+
* Returns the number of rows printed.
|
|
19
|
+
*/
|
|
20
|
+
export function printWelcome(write, lines) {
|
|
21
|
+
const proto = colorState.color ? imageProtocol() : null;
|
|
22
|
+
const rows = proto ? IMG_ROWS : Math.max(2, lines.length);
|
|
23
|
+
const logoCol = proto ? IMG_COLS : width('gitroots');
|
|
24
|
+
const out = [];
|
|
25
|
+
for (let i = 0; i < rows; i++) {
|
|
26
|
+
const logo = proto ? ' '.repeat(logoCol) : i === 0 ? word('gitroots') : ' '.repeat(logoCol);
|
|
27
|
+
out.push(` ${logo} ${c.m('│')} ${lines[i] ?? ''}`);
|
|
39
28
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const rgb = (hex) => [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16));
|
|
46
|
-
|
|
47
|
-
// Nearest xterm-256 color for terminals without truecolor.
|
|
48
|
-
function x256([r, g, b]) {
|
|
49
|
-
const q = (v) => (v < 48 ? 0 : v < 115 ? 1 : Math.floor((v - 35) / 40));
|
|
50
|
-
const lv = [0, 95, 135, 175, 215, 255];
|
|
51
|
-
const [qr, qg, qb] = [q(r), q(g), q(b)];
|
|
52
|
-
const cube = 16 + 36 * qr + 6 * qg + qb;
|
|
53
|
-
const dc = (lv[qr] - r) ** 2 + (lv[qg] - g) ** 2 + (lv[qb] - b) ** 2;
|
|
54
|
-
const gray = Math.max(0, Math.min(23, Math.round(((r + g + b) / 3 - 8) / 10)));
|
|
55
|
-
const gv = 8 + gray * 10;
|
|
56
|
-
const dg = (gv - r) ** 2 + (gv - g) ** 2 + (gv - b) ** 2;
|
|
57
|
-
return dg < dc ? 232 + gray : cube;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const fg = (hex) => (colorState.truecolor ? `\x1b[38;2;${rgb(hex).join(';')}m` : `\x1b[38;5;${x256(rgb(hex))}m`);
|
|
61
|
-
const bg = (hex) => (colorState.truecolor ? `\x1b[48;2;${rgb(hex).join(';')}m` : `\x1b[48;5;${x256(rgb(hex))}m`);
|
|
62
|
-
|
|
63
|
-
let cached = null;
|
|
64
|
-
/** The logo as terminal lines (6 rows × 48 columns). */
|
|
65
|
-
export function logoLines() {
|
|
66
|
-
if (cached && cached.color === colorState.color && cached.tc === colorState.truecolor) return cached.lines;
|
|
67
|
-
const { px, W, H } = canvas();
|
|
68
|
-
const lines = [];
|
|
69
|
-
for (let y = 0; y < H; y += 2) {
|
|
70
|
-
let s = '';
|
|
71
|
-
for (let x = 0; x < W; x++) {
|
|
72
|
-
const top = px[y][x];
|
|
73
|
-
const bot = px[y + 1][x];
|
|
74
|
-
if (!colorState.color) { s += top && bot ? '█' : top ? '▀' : bot ? '▄' : ' '; continue; }
|
|
75
|
-
if (!top && !bot) s += ' ';
|
|
76
|
-
else if (top && bot && top === bot) s += `${fg(top)}█\x1b[39m`;
|
|
77
|
-
else if (top && bot) s += `${fg(top)}${bg(bot)}▀\x1b[39;49m`;
|
|
78
|
-
else if (top) s += `${fg(top)}▀\x1b[39m`;
|
|
79
|
-
else s += `${fg(bot)}▄\x1b[39m`;
|
|
80
|
-
}
|
|
81
|
-
lines.push(s);
|
|
82
|
-
}
|
|
83
|
-
cached = { color: colorState.color, tc: colorState.truecolor, lines };
|
|
84
|
-
return lines;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/** Welcome box: the logo with the info lines beside it (or under it in narrow terminals). */
|
|
88
|
-
export function welcomeBox(info, cols) {
|
|
89
|
-
const logo = logoLines();
|
|
90
|
-
const logoW = width(logo[0]);
|
|
91
|
-
const infoW = Math.max(...info.map((l) => width(l)));
|
|
92
|
-
const side = cols >= logoW + infoW + 12;
|
|
93
|
-
const inner = Math.max(logoW + 4, Math.min(cols - 2, side ? logoW + infoW + 9 : Math.max(logoW, infoW) + 6));
|
|
94
|
-
const b = (s) => c.g(s);
|
|
95
|
-
const row = (s) => `${b('│')} ${fit(s, inner - 4)}${' '.repeat(Math.max(0, inner - 4 - width(fit(s, inner - 4))))} ${b('│')}`;
|
|
96
|
-
const out = [b(`╭${'─'.repeat(inner)}╮`), row('')];
|
|
97
|
-
if (side) {
|
|
98
|
-
const off = Math.max(0, Math.floor((logo.length - info.length) / 2));
|
|
99
|
-
for (let i = 0; i < logo.length; i++) out.push(row(`${logo[i]} ${info[i - off] || ''}`));
|
|
100
|
-
} else {
|
|
101
|
-
for (const l of logo) out.push(row(l));
|
|
102
|
-
out.push(row(''));
|
|
103
|
-
for (const l of info) out.push(row(l));
|
|
29
|
+
write(`${out.join('\n')}\n`);
|
|
30
|
+
if (proto) {
|
|
31
|
+
const esc = logoImage(IMG_COLS, IMG_ROWS);
|
|
32
|
+
if (esc) write(`\x1b7\x1b[${rows}A\r\x1b[2C${esc}\x1b8`);
|
|
104
33
|
}
|
|
105
|
-
|
|
106
|
-
return out;
|
|
34
|
+
return rows;
|
|
107
35
|
}
|
package/src/session/repl.js
CHANGED
|
@@ -8,7 +8,7 @@ import { VERSION } from '../runtime.js';
|
|
|
8
8
|
import { tryGit } from '../git/checks.js';
|
|
9
9
|
import { localSessions, shortPath } from '../agents/local.js';
|
|
10
10
|
import { remoteFullName } from '../rules.js';
|
|
11
|
-
import {
|
|
11
|
+
import { printWelcome } from './logo.js';
|
|
12
12
|
import { COMMANDS, find, match, suggest } from './registry.js';
|
|
13
13
|
|
|
14
14
|
const MENU_ROWS = 8;
|
|
@@ -33,21 +33,24 @@ export function words(text) {
|
|
|
33
33
|
return outw;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
// The design's welcome: logo │ version + tagline, then who you are and where you are.
|
|
36
37
|
function header(ctx) {
|
|
37
38
|
const cfg = readConfig();
|
|
38
|
-
const { cols } = size();
|
|
39
39
|
const top = tryGit(['rev-parse', '--show-toplevel']);
|
|
40
40
|
const branch = top ? tryGit(['rev-parse', '--abbrev-ref', 'HEAD']) : '';
|
|
41
41
|
const repo = top ? remoteFullName(tryGit(['remote', 'get-url', 'origin'])) || top.split('/').pop() : '';
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
42
|
+
write('\n');
|
|
43
|
+
printWelcome(write, [
|
|
44
|
+
`${c.s(`v${VERSION}`)} ${c.m('·')} ${c.s('code governance and agent sessions')}`,
|
|
45
|
+
c.g('Secure it to the root, scale with confidence.'),
|
|
46
|
+
]);
|
|
47
|
+
write('\n');
|
|
48
|
+
if (ctx.api.signedIn) {
|
|
49
|
+
write(` ${c.g('✓')} Signed in as ${c.b(cfg.user?.name || cfg.username)} ${c.m(`${cfg.org ? `${cfg.org.name} · ${cfg.org.role}` : 'no workspace · /workspace'} · ${ctx.api.host}`)}\n`);
|
|
50
|
+
} else {
|
|
51
|
+
write(` ${c.w('◐')} Not signed in ${c.m('·')} ${c.g('/login')} ${c.m('connects your gitroots account')}\n`);
|
|
52
|
+
}
|
|
53
|
+
write(` ${c.m(`${shortPath(process.cwd())}${repo ? ` · ${repo}${branch ? ` @ ${branch}` : ''}` : ''}`)}\n`);
|
|
51
54
|
}
|
|
52
55
|
|
|
53
56
|
class Repl {
|
|
@@ -169,7 +172,7 @@ class Repl {
|
|
|
169
172
|
this.notice = c.w(' press ctrl-c again to exit');
|
|
170
173
|
break;
|
|
171
174
|
case 'ctrl-d': if (!this.input) return this.exit(); this.input = this.input.slice(0, this.pos) + this.input.slice(this.pos + 1); break;
|
|
172
|
-
case 'ctrl-l': write('\x1b[2J\x1b[H'); this.drawn = 0;
|
|
175
|
+
case 'ctrl-l': write('\x1b[2J\x1b[H'); this.drawn = 0; header(this.ctx); write('\n'); break;
|
|
173
176
|
case 'left': this.pos = Math.max(0, this.pos - 1); break;
|
|
174
177
|
case 'right': this.pos = Math.min(this.input.length, this.pos + 1); break;
|
|
175
178
|
case 'home': case 'ctrl-a': this.pos = 0; break;
|
|
@@ -229,7 +232,7 @@ class Repl {
|
|
|
229
232
|
return;
|
|
230
233
|
}
|
|
231
234
|
if (cmd.name === 'exit') return this.exit();
|
|
232
|
-
if (cmd.name === 'clear') { write('\x1b[2J\x1b[3J\x1b[H');
|
|
235
|
+
if (cmd.name === 'clear') { write('\x1b[2J\x1b[3J\x1b[H'); header(this.ctx); write('\n'); this.draw(); return; }
|
|
233
236
|
if (cmd.name === 'help') { printHelp(true); write('\n'); this.draw(); return; }
|
|
234
237
|
await this.run(cmd, rest);
|
|
235
238
|
}
|
|
@@ -293,8 +296,8 @@ class Repl {
|
|
|
293
296
|
}
|
|
294
297
|
|
|
295
298
|
start() {
|
|
296
|
-
|
|
297
|
-
write(
|
|
299
|
+
header(this.ctx);
|
|
300
|
+
write(`\n ${c.m('Type')} ${c.g('/')} ${c.m('to pick a command, or')} ${c.g('/help')} ${c.m('for all of them.')}\n\n`);
|
|
298
301
|
process.stdin.setRawMode(true);
|
|
299
302
|
process.stdin.resume();
|
|
300
303
|
process.stdin.on('data', this.onData);
|
package/src/ui/ansi.js
CHANGED
|
@@ -23,6 +23,8 @@ function detectTruecolor() {
|
|
|
23
23
|
const tp = env.TERM_PROGRAM || '';
|
|
24
24
|
if (['iTerm.app', 'WezTerm', 'ghostty', 'vscode', 'Hyper'].includes(tp)) return true;
|
|
25
25
|
if (env.WT_SESSION) return true;
|
|
26
|
+
// Terminal.app gained 24-bit color in macOS 26 (Terminal 460+).
|
|
27
|
+
if (tp === 'Apple_Terminal' && Number(String(env.TERM_PROGRAM_VERSION || '').split('.')[0]) >= 460) return true;
|
|
26
28
|
const term = env.TERM || '';
|
|
27
29
|
return /kitty|ghostty|direct|truecolor/.test(term);
|
|
28
30
|
}
|