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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitroots",
3
- "version": "0.6.1",
3
+ "version": "0.6.3",
4
4
  "description": "gitroots CLI: code governance and agent sessions.",
5
5
  "type": "module",
6
6
  "bin": {
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.1';
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';
@@ -1,107 +1,35 @@
1
- // The gitroots logo as terminal pixel art: the 4×4 mark and the "gitroots" wordmark.
2
- // One pixel is one column wide and half a row tall (▀ ▄ █ with fg/bg colors), so pixels are about square.
3
- import { state as colorState, c, width, fit } from '../ui/ansi.js';
4
-
5
- // Colors sampled from assets/logo.png.
6
- const CELLS = [
7
- ['#B3DA44', '#97D039', '#59B238', '#20944D'],
8
- ['#7AC135', '#64B532', '#238B32', '#07753E'],
9
- ['#1D872F', '#12743D', '#096736', '#09512E'],
10
- ['#075833', '#0A5229', '#0A3E28', '#0A3929'],
11
- ];
12
- const WORD = '#E3EDFC';
13
-
14
- // 8 px tall glyphs: ascenders in rows 0-1, x-height 2-5, descender 6-7.
15
- const GLYPHS = {
16
- g: ['....', '....', '.###', '#..#', '#..#', '.###', '...#', '.##.'],
17
- i: ['#', '.', '#', '#', '#', '#', '.', '.'],
18
- t: ['.#.', '.#.', '###', '.#.', '.#.', '.##', '...', '...'],
19
- r: ['...', '...', '#.#', '##.', '#..', '#..', '...', '...'],
20
- o: ['....', '....', '.##.', '#..#', '#..#', '.##.', '....', '....'],
21
- s: ['....', '....', '.###', '#...', '..##', '###.', '....', '....'],
22
- };
23
-
24
- const CELL = 2;
25
- const GAP = 1;
26
- const MARK = 4 * CELL + 3 * GAP; // 11 px
27
- const SPACE = 4;
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
- const oy = Math.floor((H - 8) / 2);
41
- word.forEach((row, y) => [...row].forEach((ch, x) => { if (ch === '#') px[oy + y][MARK + SPACE + x] = WORD; }));
42
- return { px, W, H };
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
- out.push(row(''), b(`╰${'─'.repeat(inner)}╯`));
106
- return out;
34
+ return rows;
107
35
  }
@@ -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 { welcomeBox } from './logo.js';
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
- const who = ctx.api.signedIn
43
- ? `${c.b(cfg.user?.name || cfg.username)} ${c.m('·')} ${cfg.org ? `${cfg.org.name} ${c.m(`(${cfg.org.role})`)}` : c.w('no workspace · /workspace')} ${c.m(`· ${ctx.api.host}`)}`
44
- : `${c.w('Not signed in')} ${c.m('· /login to connect your gitroots account')}`;
45
- return welcomeBox([
46
- `${c.s(`v${VERSION}`)} ${c.m('· code governance and agent sessions')}`,
47
- '',
48
- who,
49
- c.m(`${shortPath(process.cwd())}${repo ? ` · ${repo}${branch ? ` @ ${branch}` : ''}` : ''}`),
50
- ], cols);
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; write(`${header(this.ctx).join('\n')}\n\n`); break;
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'); write(`${header(this.ctx).join('\n')}\n\n`); this.draw(); return; }
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
- write(`\n${header(this.ctx).join('\n')}\n`);
297
- write(` ${c.m('Type')} ${c.g('/')} ${c.m('to pick a command, or')} ${c.g('/help')} ${c.m('for all of them.')}${this.ctx.api.signedIn ? '' : ` ${c.m('Start with')} ${c.g('/login')}${c.m('.')}`}\n\n`);
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
  }