gitroots 0.5.0 → 0.6.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 +76 -76
- package/bin/gitroots.js +8 -0
- package/package.json +2 -1
- package/src/agents/local.js +219 -0
- package/src/api.js +61 -98
- package/src/cli.js +36 -110
- package/src/commands/completion.js +14 -37
- package/src/commands/push.js +65 -131
- package/src/commands/update.js +1 -1
- package/src/config.js +66 -0
- package/src/rules.js +88 -0
- package/src/runtime.js +1 -1
- package/src/session/account.js +133 -0
- package/src/session/agents.js +416 -0
- package/src/session/helpers.js +71 -0
- package/src/session/logo.js +40 -0
- package/src/session/registry.js +61 -0
- package/src/session/repl.js +332 -0
- package/src/session/setup.js +101 -0
- package/src/session/workspace.js +270 -0
- package/src/ui/term.js +2 -0
- package/src/commands/attach.js +0 -262
- package/src/commands/audit.js +0 -150
- package/src/commands/auth.js +0 -99
- package/src/commands/backup.js +0 -106
- package/src/commands/demo.js +0 -12
- package/src/commands/doctor.js +0 -81
- package/src/commands/pr.js +0 -322
- package/src/commands/repos.js +0 -122
- package/src/commands/restore.js +0 -125
- package/src/commands/splash.js +0 -95
- package/src/commands/status.js +0 -84
- package/src/commands/transfer.js +0 -79
- package/src/commands/tunnel.js +0 -116
- package/src/commands/watch.js +0 -347
- package/src/connected.js +0 -82
- package/src/data/live.js +0 -49
- package/src/data/sample.js +0 -163
- package/src/data/store.js +0 -145
package/src/commands/watch.js
DELETED
|
@@ -1,347 +0,0 @@
|
|
|
1
|
-
// gitroots watch · live agent dashboard (full screen)
|
|
2
|
-
import { c, h, pad, fit, width } from '../ui/ansi.js';
|
|
3
|
-
import { line, interactive, enterFullscreen, leaveFullscreen, paintFrame, size, parseKeys, keySession, FRAMES } from '../ui/term.js';
|
|
4
|
-
import { json, sessionState, dot } from '../ui/components.js';
|
|
5
|
-
import { liveLog } from '../data/live.js';
|
|
6
|
-
import { createBackup } from './backup.js';
|
|
7
|
-
|
|
8
|
-
export async function run(cmd, rest, flags, store) {
|
|
9
|
-
const machineFilter = flags.machine || null;
|
|
10
|
-
if (flags.json) {
|
|
11
|
-
json({ machines: store.machines(), sessions: store.sessions().filter((s) => (!machineFilter || s.machine === machineFilter) && matches(s, flags)) });
|
|
12
|
-
return 0;
|
|
13
|
-
}
|
|
14
|
-
if (cmd === 'sessions' || !interactive()) return snapshot(store, machineFilter, flags);
|
|
15
|
-
return new Watch(store, machineFilter, { agent: flags.agent, repo: flags.repo, state: flags.state }).start();
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const COLS = [['ID', 9], ['AGENT', 14], ['REPO · BRANCH', 31], ['STATE', 18], ['TURNS', 8], ['CPU', 7], ['UPDATED', 10]];
|
|
19
|
-
|
|
20
|
-
function sessionRow(s, cpu) {
|
|
21
|
-
return `${pad(s.id, 9)}${pad(s.agent, 14)}${pad(`${s.repo} · ${s.branch}`, 31)}${pad(sessionState(s), 18)}${pad(String(s.turns), 8)}${pad(s.state === 'running' ? `${cpu ?? s.cpu}%` : '0%', 7)}${pad(s.updated, 10)}`;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const matches = (s, f) => (!f.agent || s.agent.toLowerCase().includes(String(f.agent).toLowerCase()))
|
|
25
|
-
&& (!f.repo || s.repo === f.repo) && (!f.state || s.state === f.state || (f.state === 'waiting' && s.state === 'waiting'));
|
|
26
|
-
|
|
27
|
-
function snapshot(store, machine, flags = {}) {
|
|
28
|
-
const sessions = store.sessions().filter((s) => (!machine || s.machine === machine) && matches(s, flags));
|
|
29
|
-
line(` ${h(COLS.map(([l, w]) => pad(l, w)).join(''))}${h('MACHINE')}`);
|
|
30
|
-
for (const s of sessions) line(` ${sessionRow(s)} ${c.m(s.machine)}`);
|
|
31
|
-
line();
|
|
32
|
-
line(c.g('→ gitroots attach <session>'));
|
|
33
|
-
return 0;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function logLine(e, spin) {
|
|
37
|
-
const verb = pad(e.verb, 8);
|
|
38
|
-
const vcol = e.verb === 'think' ? c.m(verb) : e.verb === 'run' ? c.i(verb) : e.verb === 'edit' ? c.w(verb) : e.verb === 'backup' || e.verb === 'git' ? c.g(verb) : c.s(verb);
|
|
39
|
-
let s = `${c.m(e.time)} ${vcol}${e.verb === 'think' ? c.m(e.body) : e.body}`;
|
|
40
|
-
if (e.add) s += c.g(e.add);
|
|
41
|
-
if (e.del) s += c.r(e.del);
|
|
42
|
-
if (e.fail) s += c.r(e.fail);
|
|
43
|
-
if (e.last && e.running) s += ` ${c.i(spin)} ${c.s('running')}`;
|
|
44
|
-
return s;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
class Watch {
|
|
48
|
-
constructor(store, machineFilter, fixed = {}) {
|
|
49
|
-
this.store = store;
|
|
50
|
-
this.fixed = fixed;
|
|
51
|
-
this.tick = 0;
|
|
52
|
-
this.focus = 'sessions';
|
|
53
|
-
const ms = this.machineList();
|
|
54
|
-
this.mIdx = machineFilter ? Math.max(0, ms.findIndex((m) => m.name === machineFilter)) : Math.max(0, ms.findIndex((m) => m.name === (store.sessions()[0] || {}).machine));
|
|
55
|
-
this.sIdx = 0;
|
|
56
|
-
this.mode = null;
|
|
57
|
-
this.flash = null;
|
|
58
|
-
this.filter = '';
|
|
59
|
-
this.help = false;
|
|
60
|
-
this.cpu = {};
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
machineList() { return [{ name: '*', all: true }, ...this.store.machines()]; }
|
|
64
|
-
machine() { return this.machineList()[this.mIdx]; }
|
|
65
|
-
visible() {
|
|
66
|
-
const m = this.machine();
|
|
67
|
-
const f = this.filter.toLowerCase();
|
|
68
|
-
return this.store.sessions().filter((s) => (m.all || s.machine === m.name) && matches(s, this.fixed) && (!f || `${s.id} ${s.agent} ${s.repo} ${s.branch} ${s.state}`.toLowerCase().includes(f)));
|
|
69
|
-
}
|
|
70
|
-
selected() { const v = this.visible(); return v[Math.min(this.sIdx, v.length - 1)]; }
|
|
71
|
-
|
|
72
|
-
say(msg, tone = 'g') { this.flash = { msg, tone, until: Date.now() + 3500 }; }
|
|
73
|
-
|
|
74
|
-
async start() {
|
|
75
|
-
enterFullscreen();
|
|
76
|
-
this.listen();
|
|
77
|
-
this.timer = setInterval(() => { this.tick++; this.draw(); }, 100);
|
|
78
|
-
this.onResize = () => this.draw();
|
|
79
|
-
process.stdout.on('resize', this.onResize);
|
|
80
|
-
this.draw();
|
|
81
|
-
return new Promise((resolve) => { this.done = resolve; });
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
listen() {
|
|
85
|
-
process.stdin.setRawMode(true);
|
|
86
|
-
process.stdin.resume();
|
|
87
|
-
this.onData = (buf) => { for (const k of parseKeys(buf)) this.key(k); };
|
|
88
|
-
process.stdin.on('data', this.onData);
|
|
89
|
-
}
|
|
90
|
-
unlisten() {
|
|
91
|
-
process.stdin.off('data', this.onData);
|
|
92
|
-
process.stdin.setRawMode(false);
|
|
93
|
-
process.stdin.pause();
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
quit() {
|
|
97
|
-
clearInterval(this.timer);
|
|
98
|
-
process.stdout.off('resize', this.onResize);
|
|
99
|
-
this.unlisten();
|
|
100
|
-
leaveFullscreen();
|
|
101
|
-
this.done(0);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// Leave the dashboard to run a full command, then come back.
|
|
105
|
-
async outside(fn, { waitKey = true } = {}) {
|
|
106
|
-
clearInterval(this.timer);
|
|
107
|
-
this.unlisten();
|
|
108
|
-
leaveFullscreen();
|
|
109
|
-
try { await fn(); } catch (e) { line(`${c.r('✕')} ${e.message}`); }
|
|
110
|
-
if (waitKey) {
|
|
111
|
-
line();
|
|
112
|
-
line(c.m('press any key to return to watch'));
|
|
113
|
-
await keySession(() => true);
|
|
114
|
-
}
|
|
115
|
-
this.store.save();
|
|
116
|
-
enterFullscreen();
|
|
117
|
-
this.listen();
|
|
118
|
-
this.timer = setInterval(() => { this.tick++; this.draw(); }, 100);
|
|
119
|
-
this.draw();
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
key(k) {
|
|
123
|
-
if (this.mode) return this.modeKey(k);
|
|
124
|
-
if (this.help && k.name !== '?') { this.help = false; this.draw(); if (k.name === 'escape') return; }
|
|
125
|
-
const s = this.selected();
|
|
126
|
-
switch (k.ch || k.name) {
|
|
127
|
-
case 'q': case 'ctrl-c': return this.quit();
|
|
128
|
-
case 'up': case 'k': this.move(-1); break;
|
|
129
|
-
case 'down': case 'j': this.move(1); break;
|
|
130
|
-
case 'tab': case 'left': case 'right': case 'shift-tab': this.focus = this.focus === 'sessions' ? 'machines' : 'sessions'; break;
|
|
131
|
-
case '?': this.help = !this.help; break;
|
|
132
|
-
case '/': this.mode = { type: 'filter', text: this.filter }; break;
|
|
133
|
-
case 'enter': case 'a':
|
|
134
|
-
if (s) return this.outside(async () => { const { attach } = await import('./attach.js'); await attach(this.store, s.id); }, { waitKey: false });
|
|
135
|
-
break;
|
|
136
|
-
case 't':
|
|
137
|
-
if (s) {
|
|
138
|
-
const targets = this.store.machines().filter((m) => m.online && m.name !== s.machine);
|
|
139
|
-
if (!targets.length) this.say('no other machine is online', 'w');
|
|
140
|
-
else this.mode = { type: 'transfer', idx: 0, targets };
|
|
141
|
-
}
|
|
142
|
-
break;
|
|
143
|
-
case 'b':
|
|
144
|
-
if (s) {
|
|
145
|
-
const b = createBackup(this.store, s, 'manual');
|
|
146
|
-
this.store.record({ actor: this.store.org.user, action: 'backup', target: `${s.id} · ${b.id}`, decision: 'allowed', note: 'from watch', repo: s.repo });
|
|
147
|
-
this.store.save();
|
|
148
|
-
this.say(`✓ backup ${b.id} saved · ${b.sizeMb} MB · gitroots restore ${b.id}`);
|
|
149
|
-
}
|
|
150
|
-
break;
|
|
151
|
-
case 'r':
|
|
152
|
-
if (s) return this.outside(async () => { const { restore } = await import('./restore.js'); await restore(this.store, s.id, { pick: true }); });
|
|
153
|
-
break;
|
|
154
|
-
case 'f':
|
|
155
|
-
if (s) {
|
|
156
|
-
const id = this.store.nextSessionId();
|
|
157
|
-
this.store.d.sessions.push({ ...s, id, state: 'idle', updated: 'just now', ask: undefined, forkedFrom: s.id });
|
|
158
|
-
this.store.record({ actor: this.store.org.user, action: 'fork', target: `${s.id} → ${id}`, decision: 'allowed', repo: s.repo });
|
|
159
|
-
this.store.save();
|
|
160
|
-
this.say(`✓ forked ${s.id} → ${id} · same conversation, separate workspace`);
|
|
161
|
-
}
|
|
162
|
-
break;
|
|
163
|
-
case 'u':
|
|
164
|
-
if (s) this.mode = { type: 'port', text: '' };
|
|
165
|
-
break;
|
|
166
|
-
case 'p':
|
|
167
|
-
if (s) {
|
|
168
|
-
if (s.state === 'running' || s.state === 'waiting') { s.prevState = s.state; s.state = 'paused'; this.say(`⏸ paused ${s.id} at a safe point`, 'w'); }
|
|
169
|
-
else if (s.state === 'paused' || s.state === 'idle' || s.state === 'suspended') { s.state = s.prevState || 'running'; delete s.prevState; this.say(`● resumed ${s.id}`); }
|
|
170
|
-
this.store.save();
|
|
171
|
-
}
|
|
172
|
-
break;
|
|
173
|
-
case 's':
|
|
174
|
-
if (s && s.state !== 'stopped') this.mode = { type: 'stop' };
|
|
175
|
-
break;
|
|
176
|
-
default: break;
|
|
177
|
-
}
|
|
178
|
-
this.draw();
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
async modeKey(k) {
|
|
182
|
-
const m = this.mode;
|
|
183
|
-
if (k.name === 'escape' || k.name === 'ctrl-c') {
|
|
184
|
-
if (m.type === 'filter') this.filter = '';
|
|
185
|
-
this.mode = null; this.draw(); return;
|
|
186
|
-
}
|
|
187
|
-
const s = this.selected();
|
|
188
|
-
if (m.type === 'filter' || m.type === 'port') {
|
|
189
|
-
if (k.name === 'enter') {
|
|
190
|
-
this.mode = null;
|
|
191
|
-
if (m.type === 'filter') { this.filter = m.text; this.sIdx = 0; } else if (s) {
|
|
192
|
-
const port = Number(m.text);
|
|
193
|
-
if (!(port > 0 && port < 65536)) this.say('✕ not a port number', 'r');
|
|
194
|
-
else {
|
|
195
|
-
const t = { id: this.store.nextTunnelId(), session: s.id, machine: s.machine, target: `:${port}`, access: 'only you', viewers: 0, traffic: '0 B', openFor: 'just now', at: new Date().toISOString(), url: `https://${s.id}-${port}.${process.env.GITROOTS_RELAY_DOMAIN || `${this.store.org.relay.name}.relay.gitroots.com`}` };
|
|
196
|
-
this.store.d.tunnels.push(t);
|
|
197
|
-
this.store.record({ actor: this.store.org.user, action: 'tunnel open', target: `${s.id} :${port}`, decision: 'allowed', note: s.machine, repo: s.repo });
|
|
198
|
-
this.store.save();
|
|
199
|
-
this.say(`✓ tunnel ${t.id} open · ${t.url}`);
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
} else if (k.name === 'backspace') m.text = m.text.slice(0, -1);
|
|
203
|
-
else if (k.ch && (m.type === 'filter' || /\d/.test(k.ch))) { m.text += k.ch; if (m.type === 'filter') { this.filter = m.text; this.sIdx = 0; } }
|
|
204
|
-
} else if (m.type === 'stop') {
|
|
205
|
-
this.mode = null;
|
|
206
|
-
if (k.ch === 'y' || k.ch === 'Y') {
|
|
207
|
-
const b = createBackup(this.store, s, 'before stop');
|
|
208
|
-
s.state = 'stopped';
|
|
209
|
-
this.store.record({ actor: this.store.org.user, action: 'stop', target: s.id, decision: 'allowed', note: `backup ${b.id}`, repo: s.repo });
|
|
210
|
-
this.store.save();
|
|
211
|
-
this.say(`■ stopped ${s.id} · backup ${b.id} kept · gitroots restore ${b.id}`, 'w');
|
|
212
|
-
}
|
|
213
|
-
} else if (m.type === 'transfer') {
|
|
214
|
-
if (k.name === 'up' || k.ch === 'k') m.idx = (m.idx - 1 + m.targets.length) % m.targets.length;
|
|
215
|
-
else if (k.name === 'down' || k.ch === 'j') m.idx = (m.idx + 1) % m.targets.length;
|
|
216
|
-
else if (k.name === 'enter') {
|
|
217
|
-
const to = m.targets[m.idx].name;
|
|
218
|
-
this.mode = null;
|
|
219
|
-
return this.outside(async () => { const { transfer } = await import('./transfer.js'); await transfer(this.store, s.id, to); });
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
this.draw();
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
move(d) {
|
|
226
|
-
if (this.focus === 'machines') {
|
|
227
|
-
const n = this.machineList().length;
|
|
228
|
-
this.mIdx = (this.mIdx + d + n) % n;
|
|
229
|
-
this.sIdx = 0;
|
|
230
|
-
} else {
|
|
231
|
-
const n = this.visible().length;
|
|
232
|
-
if (n) this.sIdx = (this.sIdx + d + n) % n;
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
cpuOf(s) {
|
|
237
|
-
if (s.state !== 'running') return 0;
|
|
238
|
-
if (this.tick % 20 === 0 || this.cpu[s.id] === undefined) this.cpu[s.id] = Math.max(1, Math.min(99, s.cpu + Math.round((Math.random() - 0.5) * 10)));
|
|
239
|
-
return this.cpu[s.id];
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
draw() {
|
|
243
|
-
const { cols, rows } = size();
|
|
244
|
-
const store = this.store;
|
|
245
|
-
const spin = FRAMES[this.tick % FRAMES.length];
|
|
246
|
-
const all = store.sessions();
|
|
247
|
-
const n = (st) => all.filter((s) => s.state === st).length;
|
|
248
|
-
const clock = new Date().toTimeString().slice(0, 8);
|
|
249
|
-
const stats = [n('running') && `${n('running')} running`, n('waiting') && `${n('waiting')} waiting`, n('idle') && `${n('idle')} idle`, n('paused') && `${n('paused')} paused`, n('suspended') && `${n('suspended')} suspended`, n('stopped') && `${n('stopped')} stopped`].filter(Boolean).join(' · ');
|
|
250
|
-
const left = ` ${c.g(c.b('gitroots'))} ${c.b('watch')} ${c.s(`org ${store.org.name}`)} ${c.g('●')} ${c.s(`relay ${store.org.relay.name} ${store.org.relay.latencyMs} ms`)} ${c.s(stats)}`;
|
|
251
|
-
const right = `${c.m(`${clock} · live`)} `;
|
|
252
|
-
const top = c.bg('bar')(fit(left, cols - width(right)) + right);
|
|
253
|
-
|
|
254
|
-
const SW = Math.min(26, Math.floor(cols * 0.25));
|
|
255
|
-
const MW = cols - SW - 1;
|
|
256
|
-
const BH = rows - 2;
|
|
257
|
-
|
|
258
|
-
// sidebar
|
|
259
|
-
const side = [''];
|
|
260
|
-
side.push(` ${this.focus === 'machines' ? c.g('MACHINES') : h('Machines')}`);
|
|
261
|
-
this.machineList().forEach((m, i) => {
|
|
262
|
-
const sel = i === this.mIdx;
|
|
263
|
-
const count = m.all ? all.length : all.filter((s) => s.machine === m.name).length;
|
|
264
|
-
const a = m.all ? `${pad('All machines', SW - 6)}${count}` : `${m.online ? c.g('●') : c.m('○')} ${pad(m.name, SW - 8)}${count}`;
|
|
265
|
-
const b = m.all ? null : ` ${c.m(m.online ? `${m.os}${m.self ? ' · this machine' : ` · cpu ${m.cpu}%`}` : `${m.os} · offline ${m.offlineFor}`)}`;
|
|
266
|
-
const wrap = (txt) => (sel ? `${c.g('▌')}${c.bg('sel')(fit(txt, SW - 1))}` : ` ${fit(txt, SW - 1)}`);
|
|
267
|
-
side.push(wrap(` ${a}`));
|
|
268
|
-
if (b) side.push(wrap(b));
|
|
269
|
-
});
|
|
270
|
-
side.push('');
|
|
271
|
-
side.push(` ${h('Filters')}`);
|
|
272
|
-
side.push(` ${c.m(pad('search', 8))}${this.filter ? c.w(this.filter) : c.m('none · /')}`);
|
|
273
|
-
side.push(` ${c.m(pad('agent', 8))}${this.fixed.agent ? c.w(this.fixed.agent) : c.m('any')}`);
|
|
274
|
-
side.push(` ${c.m(pad('repo', 8))}${this.fixed.repo ? c.w(this.fixed.repo) : c.m('any')}`);
|
|
275
|
-
side.push(` ${c.m(pad('state', 8))}${this.fixed.state ? c.w(this.fixed.state) : c.m('any')}`);
|
|
276
|
-
|
|
277
|
-
// main
|
|
278
|
-
const main = [''];
|
|
279
|
-
const m = this.machine();
|
|
280
|
-
const vis = this.visible();
|
|
281
|
-
if (this.sIdx >= vis.length) this.sIdx = Math.max(0, vis.length - 1);
|
|
282
|
-
const title = m.all ? 'Sessions on all machines' : `Sessions on ${m.name}`;
|
|
283
|
-
main.push(` ${this.focus === 'sessions' ? c.g(title.toUpperCase()) : h(title)}`);
|
|
284
|
-
main.push(` ${h(COLS.map(([l, w]) => pad(l, w)).join(''))}${m.all ? h('MACHINE') : ''}`);
|
|
285
|
-
const maxRows = Math.max(3, Math.min(vis.length, Math.floor(BH / 3)));
|
|
286
|
-
const start = Math.max(0, Math.min(this.sIdx - maxRows + 1, vis.length - maxRows));
|
|
287
|
-
vis.slice(start, start + maxRows).forEach((s, j) => {
|
|
288
|
-
const i = start + j;
|
|
289
|
-
const row = `${sessionRow(s, this.cpuOf(s))}${m.all ? ` ${c.m(s.machine)}` : ''}`;
|
|
290
|
-
main.push(i === this.sIdx ? `${c.g(' ❯ ')}${c.bg('sel')(fit(row, MW - 4))}` : ` ${row}`);
|
|
291
|
-
});
|
|
292
|
-
if (!vis.length) main.push(` ${c.m(this.filter ? `no sessions match "${this.filter}"` : 'no sessions on this machine')}`);
|
|
293
|
-
main.push('');
|
|
294
|
-
|
|
295
|
-
const s = this.selected();
|
|
296
|
-
const boxH = BH - main.length - 2;
|
|
297
|
-
if (s && boxH > 3) {
|
|
298
|
-
main.push(` ${c.b(`LIVE · ${s.id.toUpperCase()}`)} ${c.m(`${s.agent} · ${s.machine} · ${s.repo}/${s.branch} · HEAD ${s.head} · ${s.changed} files changed · backup ${s.backup}`)}`);
|
|
299
|
-
const inner = boxH - 2;
|
|
300
|
-
let content;
|
|
301
|
-
if (this.help) {
|
|
302
|
-
content = [
|
|
303
|
-
c.b('keys'),
|
|
304
|
-
`${c.g('↑↓')} move ${c.g('tab')} machines / sessions ${c.g('/')} filter ${c.g('enter')} or ${c.g('a')} attach`,
|
|
305
|
-
`${c.g('t')} transfer to another machine ${c.g('b')} backup now ${c.g('r')} restore to an earlier point`,
|
|
306
|
-
`${c.g('f')} fork (same conversation, separate workspace) ${c.g('u')} tunnel a port ${c.g('p')} pause / resume`,
|
|
307
|
-
`${c.g('s')} stop (a backup is kept) ${c.g('?')} close help ${c.g('q')} quit`,
|
|
308
|
-
];
|
|
309
|
-
} else if (s.state === 'waiting' && s.ask) {
|
|
310
|
-
content = [...liveLog({ ...s, state: 'idle' }, 0, Math.max(0, inner - 4)).map((e) => logLine(e, spin)), '',
|
|
311
|
-
`${c.w(`◐ ${s.agent} asks:`)} ${s.ask.question}`, `${c.g(`→ press a to attach and answer`)}`];
|
|
312
|
-
} else if (s.state === 'stopped') {
|
|
313
|
-
content = [c.m(`■ ${s.id} is stopped · press r to restore it`)];
|
|
314
|
-
} else {
|
|
315
|
-
content = liveLog(s, Math.floor(this.tick / 16), inner).map((e) => logLine(e, spin));
|
|
316
|
-
}
|
|
317
|
-
while (content.length < inner) content.unshift('');
|
|
318
|
-
content = content.slice(-inner);
|
|
319
|
-
main.push(` ${c.m(`╭${'─'.repeat(MW - 4)}╮`)}`);
|
|
320
|
-
for (const l of content) main.push(` ${c.m('│')} ${fit(l, MW - 6)} ${c.m('│')}`);
|
|
321
|
-
main.push(` ${c.m(`╰${'─'.repeat(MW - 4)}╯`)}`);
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
// bottom bar
|
|
325
|
-
let bottom;
|
|
326
|
-
const md = this.mode;
|
|
327
|
-
if (md && md.type === 'filter') bottom = ` ${c.g('/')} ${md.text}${c.inv(' ')} ${c.m('enter apply · esc clear')}`;
|
|
328
|
-
else if (md && md.type === 'port') bottom = ` ${c.s(`tunnel ${s?.id} · port:`)} ${md.text}${c.inv(' ')} ${c.m('enter open · esc cancel')}`;
|
|
329
|
-
else if (md && md.type === 'stop') bottom = ` ${c.w(`Stop ${s?.id}? A backup is taken first.`)} ${c.m('(y/N)')}`;
|
|
330
|
-
else if (md && md.type === 'transfer') bottom = ` ${c.s(`transfer ${s?.id} to`)} ${md.targets.map((t, i) => (i === md.idx ? c.bg('sel')(c.b(` ❯ ${t.name} `)) : c.m(` ${t.name} `))).join(' ')} ${c.m('↑↓ choose · enter go · esc cancel')}`;
|
|
331
|
-
else {
|
|
332
|
-
const key = (k, label) => `${c.bg('key')(c.b(` ${k} `))} ${c.s(label)}`;
|
|
333
|
-
bottom = ` ${[key('a', 'attach'), key('t', 'transfer'), key('b', 'backup'), key('r', 'restore'), key('f', 'fork'), key('u', 'tunnel'), key('p', 'pause'), key('s', 'stop'), key('/', 'filter'), key('?', 'help'), key('q', 'quit')].join(' ')}`;
|
|
334
|
-
}
|
|
335
|
-
if (this.flash && Date.now() < this.flash.until) {
|
|
336
|
-
const msg = c[this.flash.tone](this.flash.msg);
|
|
337
|
-
bottom = `${fit(bottom, Math.max(0, cols - width(msg) - 4))} ${msg} `;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
const frame = [top];
|
|
341
|
-
for (let i = 0; i < BH; i++) frame.push(`${fit(side[i] || '', SW)}${c.m('│')}${fit(main[i] || '', MW)}`);
|
|
342
|
-
frame.push(c.bg('bar')(fit(bottom, cols)));
|
|
343
|
-
paintFrame(frame.slice(0, rows));
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
export { dot };
|
package/src/connected.js
DELETED
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
// Data that comes from the gitroots API when signed in (gitroots login).
|
|
2
|
-
// The API covers organisations, users, memberships, GitHub repositories/branches and audit events.
|
|
3
|
-
// PRs, agent sessions, backups and tunnels are not in the API yet and stay on sample data.
|
|
4
|
-
import { Api, readConfig } from './api.js';
|
|
5
|
-
import { c } from './ui/ansi.js';
|
|
6
|
-
import { line } from './ui/term.js';
|
|
7
|
-
|
|
8
|
-
export function connect(store) {
|
|
9
|
-
const api = Api.fromConfig();
|
|
10
|
-
if (!api) return store;
|
|
11
|
-
const cfg = readConfig();
|
|
12
|
-
store.api = api;
|
|
13
|
-
store.mode = 'connected';
|
|
14
|
-
store.orgId = cfg.org?.id ?? null;
|
|
15
|
-
store.orgOverlay = {
|
|
16
|
-
...store.d.org,
|
|
17
|
-
name: cfg.org?.slug || cfg.org?.name || 'no organisation',
|
|
18
|
-
user: cfg.username,
|
|
19
|
-
role: cfg.org?.role || 'member',
|
|
20
|
-
server: cfg.server,
|
|
21
|
-
};
|
|
22
|
-
return store;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function needOrg(store) {
|
|
26
|
-
if (store.orgId === null || store.orgId === undefined) throw new Error('signed in without an organisation · gitroots login --org <slug>');
|
|
27
|
-
return store.orgId;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export async function apiRepos(store) {
|
|
31
|
-
const rows = await store.api.repos(needOrg(store));
|
|
32
|
-
return rows.map((r) => {
|
|
33
|
-
const local = store.d.rules[r.name];
|
|
34
|
-
const protectedBranches = local ? Object.entries(local).filter(([k, v]) => v && v.protected).map(([k]) => k) : [];
|
|
35
|
-
return { name: r.name, fullName: r.full_name, provider: 'GitHub', default: r.default_branch || 'main', branches: r.branch_count ?? 0, protected: protectedBranches, openPrs: null, role: store.org.role, lastPush: '' };
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export async function apiBranches(store, repoName) {
|
|
40
|
-
const repos = await apiRepos(store);
|
|
41
|
-
const repo = repos.find((r) => r.name === repoName || r.fullName === repoName);
|
|
42
|
-
if (!repo) throw new Error(`no repository "${repoName}" in ${store.org.name} · gitroots repo list`);
|
|
43
|
-
const names = await store.api.branches(needOrg(store), repo.fullName);
|
|
44
|
-
return names.map((name) => {
|
|
45
|
-
const rule = store.d.rules[repo.name] ? store.ruleFor(repo.name, name) : null;
|
|
46
|
-
const prot = rule && rule.protected;
|
|
47
|
-
return { name, rule: prot ? 'protected' : 'open', ruleText: prot ? (rule.label.split(': ')[1] || 'protected') : 'developers push', commit: '', by: '', age: '', aheadBehind: '' };
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function hhmm(d) { return d.toTimeString().slice(0, 5); }
|
|
52
|
-
|
|
53
|
-
function summarize(meta) {
|
|
54
|
-
const keys = Object.keys(meta);
|
|
55
|
-
if (!keys.length) return '';
|
|
56
|
-
return keys.slice(0, 3).map((k) => `${k}=${typeof meta[k] === 'object' ? JSON.stringify(meta[k]) : meta[k]}`).join(' ');
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export async function apiAudit(store) {
|
|
60
|
-
const [events, users] = await Promise.all([store.api.auditEvents(), store.api.list('users/').catch(() => [])]);
|
|
61
|
-
const names = Object.fromEntries(users.map((u) => [u.id, u.username]));
|
|
62
|
-
return events.map((e) => {
|
|
63
|
-
let meta = {};
|
|
64
|
-
try { meta = e.metadata ? JSON.parse(e.metadata) : {}; } catch { meta = { detail: e.metadata }; }
|
|
65
|
-
if (typeof meta !== 'object' || meta === null) meta = { detail: String(meta) };
|
|
66
|
-
const d = new Date(e.created_at);
|
|
67
|
-
const decision = meta.decision || (/(block|den(y|ied)|reject|fail)/i.test(e.action) ? 'blocked' : /(held|pending)/i.test(e.action) ? 'held' : 'allowed');
|
|
68
|
-
return {
|
|
69
|
-
id: `ev-${e.id}`, apiId: e.id, time: hhmm(d), at: e.created_at,
|
|
70
|
-
actor: names[e.actor] || (e.actor ? `user#${e.actor}` : 'system'),
|
|
71
|
-
action: e.action,
|
|
72
|
-
target: meta.target || (meta.repo && meta.branch ? `${meta.repo} · ${meta.branch}` : '') || meta.full_name || meta.repo || meta.branch || meta.path || summarize(meta),
|
|
73
|
-
decision, note: meta.reason || meta.note || meta.message || '',
|
|
74
|
-
repo: meta.repo || meta.full_name || '', pr: meta.pr, metadata: meta,
|
|
75
|
-
};
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// One muted line where a screen mixes real data with sample data.
|
|
80
|
-
export function sampleNote(store, what) {
|
|
81
|
-
if (store.mode === 'connected') line(c.m(` sample data · ${what} not in the gitroots API yet`));
|
|
82
|
-
}
|
package/src/data/live.js
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
// Live agent activity in demo mode: a deterministic stream per session.
|
|
2
|
-
import { LIVE_POOL } from './sample.js';
|
|
3
|
-
|
|
4
|
-
const ML_POOL = [
|
|
5
|
-
{ verb: 'read', body: 'forecast/models/lstm.py' },
|
|
6
|
-
{ verb: 'edit', body: 'forecast/models/lstm.py ', add: '+12', del: ' −4' },
|
|
7
|
-
{ verb: 'run', body: 'python -m pytest tests/test_lstm.py -q', running: true },
|
|
8
|
-
{ verb: 'output', body: '14 passed in 21.3s · ', add: 'OK' },
|
|
9
|
-
{ verb: 'run', body: 'python train.py --epochs 40 --horizon 28', running: true },
|
|
10
|
-
{ verb: 'output', body: 'epoch 12/40 · val_mape 6.81%' },
|
|
11
|
-
{ verb: 'think', body: 'Validation error plateaus after epoch 10; try a lower learning rate.' },
|
|
12
|
-
{ verb: 'edit', body: 'forecast/config.yaml ', add: '+1', del: ' −1' },
|
|
13
|
-
{ verb: 'git', body: 'commit -m "tune: lr 1e-3 → 3e-4" ', add: 'e10fa34' },
|
|
14
|
-
];
|
|
15
|
-
|
|
16
|
-
const PORTAL_POOL = [
|
|
17
|
-
{ verb: 'read', body: 'portal/auth/sso.py' },
|
|
18
|
-
{ verb: 'think', body: 'The redirect drops the next= parameter after the IdP callback.' },
|
|
19
|
-
{ verb: 'edit', body: 'portal/auth/sso.py ', add: '+9', del: ' −3' },
|
|
20
|
-
{ verb: 'run', body: 'npm test -- sso', running: true },
|
|
21
|
-
{ verb: 'output', body: 'Tests: 22 passed · ', add: 'OK' },
|
|
22
|
-
{ verb: 'git', body: 'commit -m "fix: keep next= through SSO callback" ', add: '7aa0c1a' },
|
|
23
|
-
];
|
|
24
|
-
|
|
25
|
-
export function poolFor(s) {
|
|
26
|
-
if (s.repo === 'ml-forecast') return ML_POOL;
|
|
27
|
-
if (s.repo === 'portal') return PORTAL_POOL;
|
|
28
|
-
const file = s.branch.includes('payroll') ? 'addons/hr_payroll_ext/models/bank_export.py'
|
|
29
|
-
: s.branch.includes('pos') ? 'addons/pos_refund/models/pos_order.py'
|
|
30
|
-
: s.branch.includes('mrp') ? 'addons/mrp_ext/models/routing.py' : 'addons/stock_ext/models/stock_move.py';
|
|
31
|
-
return LIVE_POOL(file);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function seed(id) { let n = 0; for (const ch of id) n = (n * 31 + ch.charCodeAt(0)) % 997; return n; }
|
|
35
|
-
|
|
36
|
-
/** Entries for a session: last `count` lines ending at tick k. */
|
|
37
|
-
export function liveLog(s, tick, count) {
|
|
38
|
-
const pool = poolFor(s);
|
|
39
|
-
const active = s.state === 'running';
|
|
40
|
-
const k = seed(s.id) + (active ? tick : 0) + 7;
|
|
41
|
-
const now = Date.now();
|
|
42
|
-
const out = [];
|
|
43
|
-
for (let i = Math.max(0, k - count + 1); i <= k; i++) {
|
|
44
|
-
const e = pool[i % pool.length];
|
|
45
|
-
const t = new Date(now - (k - i) * 3000);
|
|
46
|
-
out.push({ ...e, time: t.toTimeString().slice(0, 8), last: i === k && active });
|
|
47
|
-
}
|
|
48
|
-
return out;
|
|
49
|
-
}
|