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/audit.js
DELETED
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
// gitroots audit · audit <ev-id> · audit follow
|
|
2
|
-
import { c, h, pad, fit, state as colorState } from '../ui/ansi.js';
|
|
3
|
-
import { line, write, motion, sleep } from '../ui/term.js';
|
|
4
|
-
import { dot, decision, json, csv, next } from '../ui/components.js';
|
|
5
|
-
import { FOLLOW_POOL } from '../data/sample.js';
|
|
6
|
-
import { apiAudit } from '../connected.js';
|
|
7
|
-
|
|
8
|
-
export async function run(_cmd, rest, flags, store) {
|
|
9
|
-
const [sub] = rest;
|
|
10
|
-
if (sub === 'follow' || flags.follow) return follow(flags, store);
|
|
11
|
-
if (sub && /^ev-\d+$/.test(sub)) return detail(sub, flags, store);
|
|
12
|
-
if (sub) throw new Error(`unknown: gitroots audit ${sub} · try gitroots audit, audit <ev-id>, audit follow`);
|
|
13
|
-
return list(flags, store);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function sinceMinutes(s) {
|
|
17
|
-
const m = /^(\d+)\s*(m|h|d)$/.exec(String(s));
|
|
18
|
-
if (!m) throw new Error(`--since takes 30m, 2h or 1d (got ${s})`);
|
|
19
|
-
return Number(m[1]) * { m: 1, h: 60, d: 1440 }[m[2]];
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
async function list(flags, store) {
|
|
23
|
-
let ev = store.api ? await apiAudit(store) : [...store.audit()];
|
|
24
|
-
if (flags.repo) ev = ev.filter((e) => e.repo === flags.repo);
|
|
25
|
-
if (flags.actor) ev = ev.filter((e) => e.actor === flags.actor);
|
|
26
|
-
if (flags.action) ev = ev.filter((e) => e.action === flags.action);
|
|
27
|
-
if (flags.decision) ev = ev.filter((e) => e.decision === flags.decision);
|
|
28
|
-
if (flags.pr) ev = ev.filter((e) => String(e.pr) === String(flags.pr).replace('#', ''));
|
|
29
|
-
if (flags.since) {
|
|
30
|
-
const mins = sinceMinutes(flags.since);
|
|
31
|
-
ev = ev.filter((e) => {
|
|
32
|
-
if (e.at) return (Date.now() - Date.parse(e.at)) / 60000 <= mins;
|
|
33
|
-
// sample events carry only a clock time, as seen from the sample's 14:21
|
|
34
|
-
const [hh, mm] = e.time.split(':').map(Number);
|
|
35
|
-
return 14 * 60 + 21 - (hh * 60 + mm) <= mins;
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
ev.sort((a, b) => (b.at || '').localeCompare(a.at || '') || b.time.localeCompare(a.time) || b.id.localeCompare(a.id));
|
|
39
|
-
const total = ev.length;
|
|
40
|
-
if (flags.json) { json(ev); return 0; }
|
|
41
|
-
if (flags.csv) { csv(ev, ['id', 'time', 'at', 'actor', 'action', 'target', 'decision', 'note', 'repo', 'pr']); return 0; }
|
|
42
|
-
const limit = flags.all ? ev.length : Number(flags.limit || 7);
|
|
43
|
-
const shown = ev.slice(0, limit);
|
|
44
|
-
line(` ${h(`${pad('TIME', 8)}${pad('ACTOR', 13)}${pad('ACTION', 16)}${pad('TARGET', 40)}${pad('DECISION', 35)}ID`)}`);
|
|
45
|
-
for (const e of shown) {
|
|
46
|
-
line(` ${c.m(pad(e.time, 8))}${fit(e.actor, 12)} ${fit(e.action, 15)} ${fit(e.target, 39)} ${pad(decision(e.decision), 12)}${c.m(pad(e.note || '', 23))}${c.m(e.id)}`);
|
|
47
|
-
}
|
|
48
|
-
if (!shown.length) line(c.m(' no events match'));
|
|
49
|
-
line(` ${c.m(`${shown.length} of ${total} events · --actor · --action · --decision blocked · --json · --csv for compliance exports`)}`);
|
|
50
|
-
const blocked = shown.find((e) => e.decision === 'blocked');
|
|
51
|
-
line();
|
|
52
|
-
line(next(blocked ? `gitroots audit ${blocked.id}` : 'gitroots audit follow'));
|
|
53
|
-
return 0;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
async function detail(id, flags, store) {
|
|
57
|
-
let e;
|
|
58
|
-
if (store.api) {
|
|
59
|
-
e = (await apiAudit(store)).find((x) => x.id === id);
|
|
60
|
-
if (!e) throw new Error(`no audit event ${id}`);
|
|
61
|
-
} else e = store.event(id);
|
|
62
|
-
if (flags.json) { json(e); return 0; }
|
|
63
|
-
line(` ${c.b(e.id)} ${decision(e.decision)} ${c.m(`${e.time}${e.at ? ` · ${e.at}` : ''}`)}`);
|
|
64
|
-
line();
|
|
65
|
-
const row = (k, v) => v && line(` ${c.s(pad(k, 10))}${v}`);
|
|
66
|
-
row('actor', e.actor);
|
|
67
|
-
row('action', e.action);
|
|
68
|
-
row('repo', e.repo);
|
|
69
|
-
row('target', e.target);
|
|
70
|
-
row('rule', e.rule);
|
|
71
|
-
if (e.note !== e.rule) row('note', e.note);
|
|
72
|
-
if (e.pr) row('pr', `#${e.pr}`);
|
|
73
|
-
if (e.metadata && Object.keys(e.metadata).length) row('metadata', c.m(JSON.stringify(e.metadata)));
|
|
74
|
-
line();
|
|
75
|
-
if (e.decision === 'blocked' && e.action === 'push') {
|
|
76
|
-
line(` ${c.m('nothing left the machine · the attempt is kept for review')}`);
|
|
77
|
-
if (!e.ack && !store.api) { e.ack = true; store.save(); }
|
|
78
|
-
line();
|
|
79
|
-
line(next(e.repo ? `gitroots branch rules ${e.repo} ${e.target.split(' · ')[0]}` : 'gitroots audit'));
|
|
80
|
-
} else if (e.pr) line(next(`gitroots pr view ${e.pr}`));
|
|
81
|
-
else line(next('gitroots audit'));
|
|
82
|
-
return 0;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function hms(d) { return d.toTimeString().slice(0, 8); }
|
|
86
|
-
|
|
87
|
-
// Signed in: poll the API and print events as they arrive.
|
|
88
|
-
async function followApi(flags, store) {
|
|
89
|
-
if (!flags.json) {
|
|
90
|
-
line(`${c.g('● live')} ${c.s(`${store.org.name} · ${store.api.server}`)} ${dot()} ${c.m('ctrl-c to stop')}`);
|
|
91
|
-
line();
|
|
92
|
-
}
|
|
93
|
-
let stop = false;
|
|
94
|
-
process.on('SIGINT', () => { stop = true; });
|
|
95
|
-
const seen = new Set((await apiAudit(store)).map((e) => e.id));
|
|
96
|
-
const every = Number(flags.interval || 3) * 1000;
|
|
97
|
-
let shown = 0;
|
|
98
|
-
while (!stop) {
|
|
99
|
-
const t0 = Date.now();
|
|
100
|
-
while (!stop && Date.now() - t0 < every) await sleep(100);
|
|
101
|
-
if (stop) break;
|
|
102
|
-
let events;
|
|
103
|
-
try { events = await apiAudit(store); } catch (e) { if (!flags.json) line(c.w(`◐ ${e.message} · retrying`)); continue; }
|
|
104
|
-
const fresh = events.filter((e) => !seen.has(e.id)).sort((a, b) => a.at.localeCompare(b.at));
|
|
105
|
-
for (const e of fresh) {
|
|
106
|
-
seen.add(e.id);
|
|
107
|
-
shown++;
|
|
108
|
-
if (flags.json) process.stdout.write(`${JSON.stringify(e)}\n`);
|
|
109
|
-
else line(`${c.m(new Date(e.at).toTimeString().slice(0, 8))} ${pad(e.actor, 13)}${pad(e.action, 16)}${pad(e.target, 40)}${pad(decision(e.decision), 11)} ${c.m(e.note || '')}`);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
if (!flags.json) { line(); line(c.m(` stopped · ${shown} new events`)); }
|
|
113
|
-
return 0;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
async function follow(flags, store) {
|
|
117
|
-
if (store.api) return followApi(flags, store);
|
|
118
|
-
const anim = motion();
|
|
119
|
-
if (!flags.json) {
|
|
120
|
-
line(`${c.g('● live')} ${c.s('all repos · all actors')} ${dot()} ${c.m('ctrl-c to stop')}`);
|
|
121
|
-
line();
|
|
122
|
-
}
|
|
123
|
-
let i = 0;
|
|
124
|
-
let stop = false;
|
|
125
|
-
process.on('SIGINT', () => { stop = true; });
|
|
126
|
-
const max = flags.count ? Number(flags.count) : anim ? Infinity : 10;
|
|
127
|
-
while (!stop && i < max) {
|
|
128
|
-
const e = FOLLOW_POOL[i % FOLLOW_POOL.length];
|
|
129
|
-
// sample stream: shown live, not written to your audit log
|
|
130
|
-
const ev = { id: `ev-${store.d.counters.ev + i}`, at: new Date().toISOString(), time: hms(new Date()), ...e, sample: true };
|
|
131
|
-
if (flags.json) process.stdout.write(`${JSON.stringify(ev)}\n`);
|
|
132
|
-
else {
|
|
133
|
-
const row = `${c.m(hms(new Date()))} ${pad(e.actor, 13)}${pad(e.action, 16)}${pad(e.target, 40)}${pad(decision(e.decision), 11)} ${c.m(e.note || '')}`;
|
|
134
|
-
if (anim && colorState.color) {
|
|
135
|
-
// new rows flash with a soft green background, then settle
|
|
136
|
-
write(`${c.bg('attach')(row)}\n`);
|
|
137
|
-
await sleep(450);
|
|
138
|
-
write(`\x1b[1A\r\x1b[2K${row}\n`);
|
|
139
|
-
} else line(row);
|
|
140
|
-
}
|
|
141
|
-
i++;
|
|
142
|
-
if (anim) {
|
|
143
|
-
const wait = 1400 + Math.random() * 1800;
|
|
144
|
-
const t0 = Date.now();
|
|
145
|
-
while (!stop && Date.now() - t0 < wait) await sleep(100);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
if (!flags.json) { line(); line(c.m(` stopped · ${i} events`)); }
|
|
149
|
-
return 0;
|
|
150
|
-
}
|
package/src/commands/auth.js
DELETED
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
// gitroots login · logout · whoami
|
|
2
|
-
import fs from 'node:fs';
|
|
3
|
-
import { c, pad } from '../ui/ansi.js';
|
|
4
|
-
import { line, ask, select, interactive, spinner } from '../ui/term.js';
|
|
5
|
-
import { brand, dot, json, next } from '../ui/components.js';
|
|
6
|
-
import { Api, DEFAULT_SERVER, readConfig, writeConfig, savePassword, forgetPassword } from '../api.js';
|
|
7
|
-
|
|
8
|
-
export async function run(cmd, rest, flags) {
|
|
9
|
-
if (cmd === 'logout') return logout(flags);
|
|
10
|
-
if (cmd === 'whoami') return whoami(flags);
|
|
11
|
-
return login(flags);
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
async function login(flags) {
|
|
15
|
-
const prev = readConfig();
|
|
16
|
-
const server = String(flags.server || prev.server || DEFAULT_SERVER).replace(/\/+$/, '');
|
|
17
|
-
const username = flags.username || flags.u || (interactive() ? await ask('Username or email', { def: prev.username }) : null);
|
|
18
|
-
if (!username) throw new Error('pass --username');
|
|
19
|
-
let password;
|
|
20
|
-
if (flags['password-stdin']) password = fs.readFileSync(0, 'utf8').replace(/\r?\n$/, '');
|
|
21
|
-
else if (process.env.GITROOTS_PASSWORD) password = process.env.GITROOTS_PASSWORD;
|
|
22
|
-
else password = await ask('Password', { hidden: true });
|
|
23
|
-
if (!password) throw new Error('no password given');
|
|
24
|
-
|
|
25
|
-
const api = new Api({ server, username, password });
|
|
26
|
-
const sp = spinner((f) => `${c.i(f)} ${c.s(`Signing in to ${server}…`)}`);
|
|
27
|
-
let me;
|
|
28
|
-
let orgs;
|
|
29
|
-
let memberships;
|
|
30
|
-
try {
|
|
31
|
-
me = await api.me();
|
|
32
|
-
[orgs, memberships] = await Promise.all([api.orgs(), api.memberships().catch(() => [])]);
|
|
33
|
-
} catch (e) {
|
|
34
|
-
sp.stop(`${c.r('✕')} ${e.message}`);
|
|
35
|
-
return 1;
|
|
36
|
-
}
|
|
37
|
-
sp.stop();
|
|
38
|
-
if (!me) { line(`${c.r('✕')} signed in, but the API did not return a user named ${username}`); return 1; }
|
|
39
|
-
|
|
40
|
-
const mine = memberships.filter((m) => m.user === me.id && m.is_active !== false);
|
|
41
|
-
const roleOf = (o) => (mine.find((m) => m.organization === o.id) || {}).role || (me.is_staff ? 'staff' : 'member');
|
|
42
|
-
let org = null;
|
|
43
|
-
if (flags.org) org = orgs.find((o) => o.slug === flags.org || o.name === flags.org || String(o.id) === String(flags.org));
|
|
44
|
-
else if (orgs.length === 1) org = orgs[0];
|
|
45
|
-
else if (orgs.length > 1) {
|
|
46
|
-
if (!interactive()) org = orgs[0];
|
|
47
|
-
else {
|
|
48
|
-
line(`${c.i(c.b('?'))} Which organisation?`);
|
|
49
|
-
const res = await select({
|
|
50
|
-
count: orgs.length,
|
|
51
|
-
render: (idx) => orgs.map((o, i) => `${i === idx ? c.g('❯') : ' '} ${pad(o.name, 30)}${c.m(`${o.slug} · ${roleOf(o)}`)}`).join('\n'),
|
|
52
|
-
});
|
|
53
|
-
if (res.action === 'cancel') return 1;
|
|
54
|
-
org = orgs[res.index];
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const store = savePassword(server, username, password);
|
|
59
|
-
writeConfig({
|
|
60
|
-
server, username, userId: me.id, email: me.email,
|
|
61
|
-
org: org ? { id: org.id, name: org.name, slug: org.slug, role: roleOf(org), plan: org.subscription_plan } : null,
|
|
62
|
-
signedInAt: new Date().toISOString(),
|
|
63
|
-
});
|
|
64
|
-
if (flags.json) { json({ server, user: me.username, org: org?.slug || null, credentials: store }); return 0; }
|
|
65
|
-
line(`${c.g('✓')} Signed in as ${c.b(me.username)} ${c.m(`${server}${org ? ` · org ${org.name} · ${roleOf(org)}` : ' · no organisation yet'}`)}`);
|
|
66
|
-
line(` ${c.m(`password kept in ${store === 'keychain' ? 'the macOS Keychain' : 'credentials.json (mode 600)'}`)}`);
|
|
67
|
-
line(next('gitroots status'));
|
|
68
|
-
return 0;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function logout(flags) {
|
|
72
|
-
const cfg = readConfig();
|
|
73
|
-
if (!cfg.username) { line(c.m('○ not signed in')); return 0; }
|
|
74
|
-
forgetPassword(cfg.server, cfg.username);
|
|
75
|
-
writeConfig({ server: cfg.server });
|
|
76
|
-
if (flags.json) { json({ signedOut: cfg.username }); return 0; }
|
|
77
|
-
line(`${c.g('✓')} Signed out ${c.b(cfg.username)} ${c.m(`· ${cfg.server}`)}`);
|
|
78
|
-
return 0;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
async function whoami(flags) {
|
|
82
|
-
const cfg = readConfig();
|
|
83
|
-
const api = Api.fromConfig();
|
|
84
|
-
if (!api) {
|
|
85
|
-
if (flags.json) { json({ signedIn: false }); return 1; }
|
|
86
|
-
line(`${c.m('○ not signed in')} ${dot()} sample org in use`);
|
|
87
|
-
line(next('gitroots login'));
|
|
88
|
-
return 1;
|
|
89
|
-
}
|
|
90
|
-
let me = null;
|
|
91
|
-
let err = null;
|
|
92
|
-
try { me = await api.me(); } catch (e) { err = e.message; }
|
|
93
|
-
if (flags.json) { json({ signedIn: Boolean(me), server: cfg.server, user: me, org: cfg.org, error: err }); return me ? 0 : 1; }
|
|
94
|
-
if (!me) { line(`${c.r('✕')} ${err || 'user not found'}`); return 1; }
|
|
95
|
-
line(`${brand()} ${dot()} ${c.b(me.username)} ${c.m(`<${me.email}>`)}${me.is_staff ? c.m(' · staff') : ''}`);
|
|
96
|
-
line(` ${c.s(pad('server', 8))}${cfg.server}`);
|
|
97
|
-
line(` ${c.s(pad('org', 8))}${cfg.org ? `${cfg.org.name} ${c.m(`(${cfg.org.slug} · ${cfg.org.role}${cfg.org.plan ? ` · ${cfg.org.plan}` : ''})`)}` : c.m('none')}`);
|
|
98
|
-
return 0;
|
|
99
|
-
}
|
package/src/commands/backup.js
DELETED
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
// gitroots backup <session> · backups · backup policy
|
|
2
|
-
import { c, h, pad } from '../ui/ansi.js';
|
|
3
|
-
import { line, runSteps } from '../ui/term.js';
|
|
4
|
-
import { dot, json, next } from '../ui/components.js';
|
|
5
|
-
|
|
6
|
-
export async function run(cmd, rest, flags, store) {
|
|
7
|
-
if (cmd === 'backups' || rest[0] === 'list' || rest[0] === 'ls') return list(flags, store);
|
|
8
|
-
if (rest[0] === 'policy' && rest[1] === 'set') return policySet(flags, store);
|
|
9
|
-
if (rest[0] === 'policy') return policy(flags, store);
|
|
10
|
-
const id = rest[0];
|
|
11
|
-
if (!id) throw new Error('which session? gitroots backup <session> [--label "…"]');
|
|
12
|
-
const s = store.session(id);
|
|
13
|
-
if (flags.json) { const b = createBackup(store, s, flags.label); store.save(); json(b); return 0; }
|
|
14
|
-
const b = await backupWithSteps(store, s, flags.label || 'manual');
|
|
15
|
-
line(` ${next(`gitroots restore ${b.id}`)}`);
|
|
16
|
-
return 0;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function createBackup(store, s, label = 'manual') {
|
|
20
|
-
const sizeMb = Math.round((0.3 + s.turns * 0.0135 + s.changed * 0.05) * 10) / 10;
|
|
21
|
-
const b = {
|
|
22
|
-
id: store.nextBackupId(), session: s.id, agent: s.agent, machine: s.machine, label, turn: s.turns, sizeMb,
|
|
23
|
-
created: 'just now', at: new Date().toISOString(), head: s.head, files: s.changed, prompt: s.lastPrompt || '', repo: s.repo,
|
|
24
|
-
};
|
|
25
|
-
store.d.backups.push(b);
|
|
26
|
-
s.backup = 'just now';
|
|
27
|
-
return b;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export async function backupWithSteps(store, s, label, { indent = ' ' } = {}) {
|
|
31
|
-
const convoMb = Math.round(s.turns * 0.0127 * 10) / 10;
|
|
32
|
-
let b;
|
|
33
|
-
await runSteps([
|
|
34
|
-
{ name: 'Conversation', live: `reading ${s.turns} turns…`, ms: 500, ok: `${s.turns} turns · ${convoMb} MB` },
|
|
35
|
-
{ name: 'Workspace', live: 'git stash · diff · untracked files…', ms: 700, ok: `HEAD ${s.head} · ${s.changed} file${s.changed === 1 ? '' : 's'} changed${s.stash ? ` · ${s.stash} stash` : ''}` },
|
|
36
|
-
{ name: 'Environment', live: 'sealing secrets…', ms: 300, ok: '12 vars · 2 secrets sealed' },
|
|
37
|
-
], { nameWidth: 15, indent });
|
|
38
|
-
b = createBackup(store, s, label);
|
|
39
|
-
store.record({ actor: store.org.user, action: 'backup', target: `${s.id} · ${b.id}`, decision: 'allowed', note: label, repo: s.repo });
|
|
40
|
-
store.save();
|
|
41
|
-
const pol = store.d.backupPolicy;
|
|
42
|
-
line(`${indent}${c.g('✓')} Backup ${c.b(b.id)} saved ${c.m(`${b.sizeMb} MB · encrypted · ${pol.storage}, ${pol.region}`)}`);
|
|
43
|
-
return b;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function list(flags, store) {
|
|
47
|
-
let rows = [...store.backups()];
|
|
48
|
-
if (flags.repo) rows = rows.filter((b) => b.repo === flags.repo);
|
|
49
|
-
if (flags.session) rows = rows.filter((b) => b.session === flags.session);
|
|
50
|
-
rows.sort((a, b) => b.id.localeCompare(a.id));
|
|
51
|
-
if (flags.json) { json(rows); return 0; }
|
|
52
|
-
const shown = flags.all ? rows : rows.slice(0, 7);
|
|
53
|
-
line(` ${h(`${pad('ID', 9)}${pad('SESSION', 10)}${pad('AGENT', 14)}${pad('MACHINE', 13)}${pad('LABEL', 22)}${pad('TURN', 8)}${pad('SIZE', 10)}CREATED`)}`);
|
|
54
|
-
for (const b of shown) {
|
|
55
|
-
const fresh = b.created === 'just now';
|
|
56
|
-
line(` ${fresh ? c.b(pad(b.id, 9)) : pad(b.id, 9)}${pad(b.session, 10)}${pad(b.agent, 14)}${pad(b.machine, 13)}${pad(b.label, 22)}${pad(String(b.turn), 8)}${pad(`${b.sizeMb} MB`, 10)}${fresh ? c.g(b.created) : c.m(b.created)}`);
|
|
57
|
-
}
|
|
58
|
-
line();
|
|
59
|
-
line(` ${c.m(`${shown.length} of ${rows.length} backups${rows.length > shown.length ? ' · gitroots backups --all for more' : ''}`)}`);
|
|
60
|
-
line();
|
|
61
|
-
policy({}, store, true);
|
|
62
|
-
return 0;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function policy(flags, store, embedded = false) {
|
|
66
|
-
const p = store.d.backupPolicy;
|
|
67
|
-
if (flags.json) { json(p); return 0; }
|
|
68
|
-
const on = (v) => (v ? c.g('on') : c.m('off'));
|
|
69
|
-
line(`${h('Policy')} ${c.m('(gitroots backup policy)')}`);
|
|
70
|
-
line(` ${pad('every', 13)}${pad(p.every, 34)}${pad('before risky commands', 24)}${on(p.beforeRisky)}`);
|
|
71
|
-
line(` ${pad('every turn', 13)}${pad(p.everyTurn ? c.g('on') : c.m('off'), 34)}${pad('before transfer', 24)}${on(p.beforeTransfer)}`);
|
|
72
|
-
line(` ${pad('keep', 13)}${pad(p.keep, 34)}${pad('storage', 24)}${p.storage}${p.storage === 'gitroots cloud' ? ` ${dot()} ${c.m('or --store s3://your-bucket')}` : ''}`);
|
|
73
|
-
if (!embedded) { line(); line(next('gitroots backups')); }
|
|
74
|
-
return 0;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// gitroots backup policy set --every 10m --every-turn on --before-risky on --before-transfer on --keep "30 days" --store s3://bucket
|
|
78
|
-
function policySet(flags, store) {
|
|
79
|
-
const p = store.d.backupPolicy;
|
|
80
|
-
const onoff = (v, name) => {
|
|
81
|
-
if (v === true || v === 'on' || v === 'true' || v === 'yes') return true;
|
|
82
|
-
if (v === false || v === 'off' || v === 'false' || v === 'no') return false;
|
|
83
|
-
throw new Error(`--${name} takes on or off`);
|
|
84
|
-
};
|
|
85
|
-
const changed = [];
|
|
86
|
-
if (flags.every !== undefined) {
|
|
87
|
-
const m = /^(\d+)\s*(m|min|h)$/.exec(String(flags.every));
|
|
88
|
-
if (!m) throw new Error('--every takes a time like 10m or 1h');
|
|
89
|
-
p.every = `${m[1]} ${m[2] === 'h' ? 'h' : 'min'} while running`; changed.push('every');
|
|
90
|
-
}
|
|
91
|
-
if (flags['every-turn'] !== undefined) { p.everyTurn = onoff(flags['every-turn'], 'every-turn'); changed.push('every turn'); }
|
|
92
|
-
if (flags['before-risky'] !== undefined) { p.beforeRisky = onoff(flags['before-risky'], 'before-risky'); changed.push('before risky commands'); }
|
|
93
|
-
if (flags['before-transfer'] !== undefined) { p.beforeTransfer = onoff(flags['before-transfer'], 'before-transfer'); changed.push('before transfer'); }
|
|
94
|
-
if (flags.keep !== undefined) { p.keep = String(flags.keep); changed.push('keep'); }
|
|
95
|
-
if (flags.store !== undefined) {
|
|
96
|
-
if (!/^(s3|gs|az):\/\/[^/]+/.test(flags.store) && flags.store !== 'gitroots') throw new Error('--store takes gitroots or s3://bucket, gs://bucket, az://container');
|
|
97
|
-
p.storage = flags.store === 'gitroots' ? 'gitroots cloud' : flags.store; changed.push('storage');
|
|
98
|
-
}
|
|
99
|
-
if (!changed.length) throw new Error('nothing to change · --every, --every-turn, --before-risky, --before-transfer, --keep, --store');
|
|
100
|
-
store.record({ actor: store.org.user, action: 'backup policy', target: changed.join(', '), decision: 'allowed' });
|
|
101
|
-
store.save();
|
|
102
|
-
if (flags.json) { json(p); return 0; }
|
|
103
|
-
line(` ${c.g('✓')} Backup policy updated ${c.m(`· ${changed.join(', ')}`)}`);
|
|
104
|
-
line();
|
|
105
|
-
return policy({}, store);
|
|
106
|
-
}
|
package/src/commands/demo.js
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
// gitroots demo reset
|
|
2
|
-
import { c } from '../ui/ansi.js';
|
|
3
|
-
import { line } from '../ui/term.js';
|
|
4
|
-
import { Store, home } from '../data/store.js';
|
|
5
|
-
|
|
6
|
-
export async function run(_cmd, rest) {
|
|
7
|
-
const [sub] = rest;
|
|
8
|
-
if (sub !== 'reset') throw new Error('usage: gitroots demo reset');
|
|
9
|
-
Store.reset();
|
|
10
|
-
line(` ${c.g('✓')} Demo org reset ${c.m(`· ${home()}/state.json removed`)}`);
|
|
11
|
-
return 0;
|
|
12
|
-
}
|
package/src/commands/doctor.js
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
// gitroots doctor
|
|
2
|
-
import fs from 'node:fs';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import { spawnSync } from 'node:child_process';
|
|
5
|
-
import { c, pad, state as colorState } from '../ui/ansi.js';
|
|
6
|
-
import { line, motion, imageProtocol } from '../ui/term.js';
|
|
7
|
-
import { json } from '../ui/components.js';
|
|
8
|
-
import { VERSION, DOWNLOAD_BASE, installMethod, nativePaths, platformKey, isBinary } from '../runtime.js';
|
|
9
|
-
import { latestVersion, newer, onPath } from '../installer.js';
|
|
10
|
-
import { home } from '../data/store.js';
|
|
11
|
-
import { readConfig } from '../api.js';
|
|
12
|
-
import { tryGit } from '../git/checks.js';
|
|
13
|
-
|
|
14
|
-
export async function run(_cmd, _rest, flags, store) {
|
|
15
|
-
const checks = [];
|
|
16
|
-
const add = (group, name, status, detail, fix) => checks.push({ group, name, status, detail, fix });
|
|
17
|
-
|
|
18
|
-
const method = installMethod();
|
|
19
|
-
add('install', 'gitroots', 'ok', `${VERSION} · ${method}${isBinary ? ` · ${process.execPath}` : ''}`);
|
|
20
|
-
add('install', 'platform', 'ok', platformKey());
|
|
21
|
-
if (!isBinary) {
|
|
22
|
-
const major = Number(process.versions.node.split('.')[0]);
|
|
23
|
-
add('install', 'node', major >= 18 ? 'ok' : 'fail', process.version, major >= 18 ? null : 'install Node 18 or newer, or the standalone build: curl -fsSL ' + DOWNLOAD_BASE + '/install.sh | bash');
|
|
24
|
-
}
|
|
25
|
-
if (method === 'native') {
|
|
26
|
-
const { bin } = nativePaths();
|
|
27
|
-
add('install', 'PATH', onPath(bin) ? 'ok' : 'warn', onPath(bin) ? `${bin} is on PATH` : `${bin} is not on PATH`, onPath(bin) ? null : `add export PATH="${bin}:$PATH" to your shell profile`);
|
|
28
|
-
}
|
|
29
|
-
const which = spawnSync(process.platform === 'win32' ? 'where' : 'sh', process.platform === 'win32' ? ['gitroots'] : ['-c', 'command -v gitroots'], { encoding: 'utf8' });
|
|
30
|
-
const found = (which.stdout || '').split('\n')[0].trim();
|
|
31
|
-
add('install', 'on PATH', found ? 'ok' : 'warn', found || 'gitroots is not on PATH in this shell', found ? null : method === 'npm' ? 'check npm prefix -g is on PATH' : null);
|
|
32
|
-
|
|
33
|
-
try {
|
|
34
|
-
const latest = await latestVersion(DOWNLOAD_BASE, 4000);
|
|
35
|
-
add('install', 'updates', newer(latest, VERSION) ? 'warn' : 'ok', newer(latest, VERSION) ? `${latest} is available` : 'up to date', newer(latest, VERSION) ? 'gitroots update' : null);
|
|
36
|
-
} catch (e) {
|
|
37
|
-
add('install', 'updates', 'warn', `couldn't check (${e.message})`);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const git = spawnSync('git', ['--version'], { encoding: 'utf8' });
|
|
41
|
-
add('git', 'git', git.status === 0 ? 'ok' : 'fail', git.status === 0 ? git.stdout.trim() : 'not found', git.status === 0 ? null : 'install git');
|
|
42
|
-
const top = tryGit(['rev-parse', '--show-toplevel']);
|
|
43
|
-
if (top) {
|
|
44
|
-
const hooks = path.resolve(tryGit(['rev-parse', '--git-path', 'hooks']) || '.git/hooks');
|
|
45
|
-
const hook = path.join(hooks, 'pre-push');
|
|
46
|
-
const ours = fs.existsSync(hook) && fs.readFileSync(hook, 'utf8').includes('# gitroots push checks');
|
|
47
|
-
add('git', 'push checks', ours ? 'ok' : 'warn', ours ? `installed in ${path.basename(top)}` : `not installed in ${path.basename(top)}`, ours ? null : 'gitroots hooks install');
|
|
48
|
-
const url = tryGit(['remote', 'get-url', 'origin']);
|
|
49
|
-
const repo = store.repoByRemote(url);
|
|
50
|
-
const cfg = fs.existsSync(path.join(top, '.gitroots.json'));
|
|
51
|
-
add('git', 'rules', repo || cfg ? 'ok' : 'warn', repo ? `gitroots repo ${repo.name}` : cfg ? '.gitroots.json' : 'no rules for this repo · only the secrets scan applies', repo || cfg ? null : 'add a .gitroots.json with "protected": ["main"]');
|
|
52
|
-
} else add('git', 'repository', 'ok', 'not inside a git repo (skipped hook checks)');
|
|
53
|
-
|
|
54
|
-
const cfg = readConfig();
|
|
55
|
-
if (store.api) {
|
|
56
|
-
try {
|
|
57
|
-
const me = await store.api.me();
|
|
58
|
-
add('account', 'signed in', me ? 'ok' : 'fail', me ? `${me.username} · ${cfg.server} · org ${cfg.org ? cfg.org.slug : 'none'}` : 'user not found', me ? null : 'gitroots login');
|
|
59
|
-
} catch (e) { add('account', 'signed in', 'fail', e.message, 'gitroots login'); }
|
|
60
|
-
} else add('account', 'signed in', 'warn', 'no · using the sample org', 'gitroots login');
|
|
61
|
-
|
|
62
|
-
try { fs.mkdirSync(home(), { recursive: true }); fs.accessSync(home(), fs.constants.W_OK); add('account', 'state', 'ok', home()); } catch { add('account', 'state', 'fail', `${home()} is not writable`, 'set GITROOTS_HOME to a writable folder'); }
|
|
63
|
-
|
|
64
|
-
const img = imageProtocol();
|
|
65
|
-
add('terminal', 'colors', colorState.color ? 'ok' : 'warn', colorState.color ? (colorState.truecolor ? 'truecolor' : '256 colors') : 'off (piped or NO_COLOR)');
|
|
66
|
-
add('terminal', 'motion', 'ok', motion() ? 'on' : 'off (piped, NO_COLOR, CI or reduced motion)');
|
|
67
|
-
add('terminal', 'logo image', 'ok', img ? `${img} graphics` : 'plain text logo (Kitty, Ghostty, WezTerm or iTerm2 show the image)');
|
|
68
|
-
|
|
69
|
-
if (flags.json) { json(checks); return checks.some((x) => x.status === 'fail') ? 1 : 0; }
|
|
70
|
-
let group = '';
|
|
71
|
-
for (const x of checks) {
|
|
72
|
-
if (x.group !== group) { if (group) line(); line(c.m(x.group.toUpperCase())); group = x.group; }
|
|
73
|
-
const mark = x.status === 'ok' ? c.g('✓') : x.status === 'warn' ? c.w('◐') : c.r('✕');
|
|
74
|
-
line(` ${mark} ${pad(x.name, 13)}${x.status === 'fail' ? c.r(x.detail) : x.status === 'warn' ? c.w(x.detail) : c.s(x.detail)}${x.fix ? ` ${c.g(`→ ${x.fix}`)}` : ''}`);
|
|
75
|
-
}
|
|
76
|
-
const fails = checks.filter((x) => x.status === 'fail').length;
|
|
77
|
-
const warns = checks.filter((x) => x.status === 'warn').length;
|
|
78
|
-
line();
|
|
79
|
-
line(fails ? c.r(`✕ ${fails} problem${fails > 1 ? 's' : ''}`) + (warns ? c.w(` · ◐ ${warns} to look at`) : '') : warns ? c.w(`◐ ${warns} thing${warns > 1 ? 's' : ''} to look at`) : c.g('✓ all good'));
|
|
80
|
-
return fails ? 1 : 0;
|
|
81
|
-
}
|