gitroots 0.5.0 → 0.6.1
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 +107 -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/data/sample.js
DELETED
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
// Sample organisation used in demo mode. Mirrors the gitroots CLI Terminal design canvas.
|
|
2
|
-
export function sampleData() {
|
|
3
|
-
return {
|
|
4
|
-
org: {
|
|
5
|
-
name: 'acme-erp',
|
|
6
|
-
user: 'shawki',
|
|
7
|
-
role: 'tech lead',
|
|
8
|
-
relay: { name: 'fra-1', latencyMs: 18, connected: true },
|
|
9
|
-
},
|
|
10
|
-
repos: [
|
|
11
|
-
{ name: 'erp', provider: 'GitHub', remote: 'github.com:acme/erp.git', default: 'main', protected: ['main', 'staging', '17.0'], branches: 22, openPrs: 5, role: 'tech lead', lastPush: '2m ago', lastBranch: 'feature/inventory' },
|
|
12
|
-
{ name: 'portal', provider: 'GitLab', remote: 'gitlab.com:acme/portal.git', default: 'main', protected: ['main'], branches: 11, openPrs: 2, role: 'tech lead', lastPush: '40m ago', lastBranch: 'fix/login-redirect' },
|
|
13
|
-
{ name: 'ml-forecast', provider: 'GitHub', remote: 'github.com:acme/ml-forecast.git', default: 'main', protected: ['main'], branches: 8, openPrs: 0, role: 'reviewer', lastPush: '3h ago', lastBranch: 'exp/lstm' },
|
|
14
|
-
],
|
|
15
|
-
branches: {
|
|
16
|
-
erp: [
|
|
17
|
-
{ name: 'main', rule: 'protected', ruleText: 'no direct push · PR only', approvals: 3, commit: 'a77d1e0', by: 'lead', age: '1d', aheadBehind: '' },
|
|
18
|
-
{ name: 'staging', rule: 'protected', ruleText: 'PR only · checks required', approvals: 2, commit: 'c12f9a4', by: 'lead', age: '32m', aheadBehind: '4 ↑ 0 ↓ vs main' },
|
|
19
|
-
{ name: '17.0', rule: 'protected', ruleText: 'release · read only', approvals: 3, commit: '9e0b311', by: 'lead', age: '9d', aheadBehind: '' },
|
|
20
|
-
{ name: 'feature/inventory', rule: 'open', ruleText: 'developers push', commit: 'a91c2e5', by: 's-7f3a', age: '2m', aheadBehind: '6 ↑ 2 ↓ vs staging' },
|
|
21
|
-
{ name: 'feature/pos-refund', rule: 'open', ruleText: 'developers push', commit: '5d20b7c', by: 'dev-01', age: '1h', aheadBehind: '3 ↑ 0 ↓ · PR #412' },
|
|
22
|
-
{ name: 'feature/hr-payroll', rule: 'open', ruleText: 'developers push', commit: '0f61a2d', by: 's-a017', age: '4s', aheadBehind: '1 ↑ 0 ↓' },
|
|
23
|
-
...['fix/stock-picking-batch', 'fix/invoice-rounding', 'feature/mrp-routing', 'feature/crm-scoring', 'feature/website-b2b', 'fix/payroll-timezone',
|
|
24
|
-
'feature/purchase-approval', 'fix/pos-session-close', 'feature/helpdesk-sla', 'chore/upgrade-deps', 'feature/fleet-costs', 'fix/report-margins',
|
|
25
|
-
'feature/l10n-sa-vat', 'exp/forecast-hook', 'fix/multicompany-rules', 'feature/subscription-renewal']
|
|
26
|
-
.map((name, i) => ({ name, rule: 'open', ruleText: 'developers push', commit: (0x1a2b3c0 + i * 7919).toString(16).slice(0, 7), by: ['dev-01', 'dev-02', 'dev-03', 'ahmed', 'nora'][i % 5], age: `${i + 2}d`, aheadBehind: `${(i % 4) + 1} ↑ ${i % 3} ↓` })),
|
|
27
|
-
],
|
|
28
|
-
portal: [
|
|
29
|
-
{ name: 'main', rule: 'protected', ruleText: 'no direct push · PR only', approvals: 2, commit: 'b3e1f70', by: 'lead', age: '2d', aheadBehind: '' },
|
|
30
|
-
{ name: 'fix/login-redirect', rule: 'open', ruleText: 'developers push', commit: '7aa0c19', by: 'dev-03', age: '40m', aheadBehind: '2 ↑ 0 ↓ · PR #405' },
|
|
31
|
-
],
|
|
32
|
-
'ml-forecast': [
|
|
33
|
-
{ name: 'main', rule: 'protected', ruleText: 'no direct push · PR only', approvals: 1, commit: '44c09de', by: 'lead', age: '5d', aheadBehind: '' },
|
|
34
|
-
{ name: 'exp/lstm', rule: 'open', ruleText: 'developers push', commit: 'e10fa33', by: 's-c3f8', age: '3h', aheadBehind: '9 ↑ 1 ↓' },
|
|
35
|
-
],
|
|
36
|
-
},
|
|
37
|
-
// Branch rules used by `branch rules` and by the real pre-push checks.
|
|
38
|
-
rules: {
|
|
39
|
-
erp: {
|
|
40
|
-
main: { protected: true, directPush: false, approvals: 3, checks: true, push: 'direct pushes blocked for everyone', mergeText: '3 approvals · checks green · release manager', label: 'main: no direct push, PR only', suggest: 'push to a feature branch, then open a PR into staging' },
|
|
41
|
-
staging: { protected: true, directPush: false, approvals: 2, checks: true, push: 'direct pushes blocked for everyone', mergeText: '2 approvals · checks green · no unresolved comments', label: 'staging: PR only', suggest: 'push to a feature branch, then open a PR into staging' },
|
|
42
|
-
'17.0': { protected: true, directPush: false, approvals: 3, checks: true, push: 'release branch · read only', mergeText: 'release managers only', label: '17.0: release, read only', suggest: 'cherry-pick fixes through a PR into staging' },
|
|
43
|
-
'feature/*': { protected: false, directPush: true, label: 'feature/* · developers can push' },
|
|
44
|
-
'fix/*': { protected: false, directPush: true, label: 'fix/* · developers can push' },
|
|
45
|
-
'*': { protected: false, directPush: true, label: 'developers can push' },
|
|
46
|
-
paths: [{ pattern: 'addons/*/security/**', needs: 'a tech lead approval' }],
|
|
47
|
-
agents: 'agent commits allowed on feature/* only, tagged in audit',
|
|
48
|
-
},
|
|
49
|
-
portal: {
|
|
50
|
-
main: { protected: true, directPush: false, approvals: 2, checks: true, push: 'direct pushes blocked for everyone', mergeText: '2 approvals · checks green', label: 'main: no direct push, PR only', suggest: 'push to a fix/ or feature/ branch, then open a PR into main' },
|
|
51
|
-
'*': { protected: false, directPush: true, label: 'developers can push' },
|
|
52
|
-
paths: [],
|
|
53
|
-
agents: 'agent commits allowed on feature/* only, tagged in audit',
|
|
54
|
-
},
|
|
55
|
-
'ml-forecast': {
|
|
56
|
-
main: { protected: true, directPush: false, approvals: 1, checks: true, push: 'direct pushes blocked for everyone', mergeText: '1 approval · checks green', label: 'main: no direct push, PR only', suggest: 'push to an exp/ branch, then open a PR into main' },
|
|
57
|
-
'*': { protected: false, directPush: true, label: 'developers can push' },
|
|
58
|
-
paths: [],
|
|
59
|
-
agents: 'agent commits allowed on exp/* and feature/*',
|
|
60
|
-
},
|
|
61
|
-
},
|
|
62
|
-
prs: [
|
|
63
|
-
{
|
|
64
|
-
number: 412, repo: 'erp', title: 'Partial refunds in POS', from: 'feature/pos-refund', into: 'staging', author: 'dev-01', opened: '54m ago',
|
|
65
|
-
required: 2, approvals: [{ by: 'nora', ago: '20m ago' }], checks: { state: 'pass', tests: '24 / 24' }, state: 'open', commits: 3,
|
|
66
|
-
needs: { who: 'shawki', why: 'tech lead for migrations' },
|
|
67
|
-
files: [
|
|
68
|
-
{ path: 'addons/pos_refund/models/pos_order.py', add: 168, del: 22 },
|
|
69
|
-
{ path: 'addons/pos_refund/wizard/refund_wizard.py', add: 96, del: 0 },
|
|
70
|
-
{ path: 'migrations/17.0.1.3/post-migrate.py', add: 31, del: 0, flag: 'changes data' },
|
|
71
|
-
{ path: 'addons/pos_refund/views/refund_wizard_views.xml', add: 4, del: 20 },
|
|
72
|
-
{ path: 'addons/pos_refund/tests/test_partial_refund.py', add: 6, del: 3 },
|
|
73
|
-
{ path: 'addons/pos_refund/__manifest__.py', add: 2, del: 1 },
|
|
74
|
-
{ path: 'addons/pos_refund/static/src/js/refund_button.js', add: 5, del: 2 },
|
|
75
|
-
],
|
|
76
|
-
notes: ['migration backfills about 18,000 orders on staging · review the SQL first'],
|
|
77
|
-
},
|
|
78
|
-
{ number: 410, repo: 'erp', title: 'Inventory valuation rounding', from: 'feature/inventory', into: 'staging', author: 's-7f3a', agent: 'Claude Code', opened: '25m ago', required: 2, approvals: [], checks: { state: 'running' }, state: 'open', commits: 6, files: [], notes: [] },
|
|
79
|
-
{ number: 409, repo: 'erp', title: 'Payroll export to bank format', from: 'feature/hr-payroll', into: 'staging', author: 'dev-02', opened: '3h ago', required: 2, approvals: [{ by: 'nora', ago: '1h ago' }, { by: 'lead', ago: '12m ago' }], checks: { state: 'pass', tests: '31 / 31' }, state: 'merged', mergedAt: '14:12', commits: 4, files: [], notes: [] },
|
|
80
|
-
{ number: 407, repo: 'erp', title: 'Release 17.0.1.3', from: 'staging', into: 'main', author: 'lead', opened: '1d ago', required: 3, approvals: [{ by: 'nora', ago: '3h ago' }], checks: { state: 'pass', tests: '212 / 212' }, state: 'open', commits: 14, files: [], notes: [] },
|
|
81
|
-
{ number: 405, repo: 'portal', title: 'Login redirect after SSO', from: 'fix/login-redirect', into: 'staging', author: 'dev-03', opened: '2h ago', required: 2, approvals: [], checks: { state: 'fail', failed: 'secrets scan · AWS key in config.py' }, state: 'open', commits: 2, files: [], notes: [] },
|
|
82
|
-
],
|
|
83
|
-
audit: [
|
|
84
|
-
{ id: 'ev-8790', time: '13:22', actor: 's-a017', action: 'push', target: 'addons/hr/security/ir.model.access.csv', decision: 'held', note: 'needs tech lead', repo: 'erp' },
|
|
85
|
-
{ id: 'ev-8797', time: '13:40', actor: 'dev-03', action: 'push', target: 'fix/login-redirect · secrets scan', decision: 'blocked', note: 'AWS key in config.py', repo: 'portal' },
|
|
86
|
-
{ id: 'ev-8801', time: '13:51', actor: 'dev-01', action: 'open pr', target: 'PR #412 feature/pos-refund → staging', decision: 'allowed', note: '', repo: 'erp', pr: 412 },
|
|
87
|
-
{ id: 'ev-8806', time: '13:58', actor: 'nora', action: 'approve', target: 'PR #412', decision: 'recorded', note: '', repo: 'erp', pr: 412 },
|
|
88
|
-
{ id: 'ev-8809', time: '14:05', actor: 's-7f3a', action: 'push', target: 'feature/inventory · 3 commits', decision: 'allowed', note: 'Claude Code on vps-1', repo: 'erp' },
|
|
89
|
-
{ id: 'ev-8812', time: '14:03', actor: 'ahmed', action: 'push', target: 'main · 1 commit', decision: 'blocked', note: 'main: no direct push', repo: 'erp', rule: 'main: no direct push' },
|
|
90
|
-
{ id: 'ev-8811', time: '14:12', actor: 'lead', action: 'merge', target: 'PR #409 → staging', decision: 'allowed', note: '2 approvals', repo: 'erp', pr: 409 },
|
|
91
|
-
],
|
|
92
|
-
machines: [
|
|
93
|
-
{ name: 'mbp-shawki', os: 'macOS', online: true, cpu: 22, self: true },
|
|
94
|
-
{ name: 'vps-1', os: 'Linux', online: true, cpu: 61 },
|
|
95
|
-
{ name: 'gpu-a100', os: 'Linux', online: true, cpu: 12 },
|
|
96
|
-
{ name: 'win-qa', os: 'Windows', online: false, offlineFor: '2h' },
|
|
97
|
-
],
|
|
98
|
-
sessions: [
|
|
99
|
-
{ id: 's-7f3a', agent: 'Claude Code', resume: 'claude --resume', machine: 'vps-1', repo: 'erp', branch: 'feature/inventory', state: 'running', turns: 142, cpu: 38, updated: '12s ago', head: 'a91c2e4', changed: 3, stash: 1, backup: '4m ago' },
|
|
100
|
-
{ id: 's-2c91', agent: 'Codex', resume: 'codex resume', machine: 'vps-1', repo: 'erp', branch: 'feature/pos-refund', state: 'waiting', turns: 37, cpu: 0, updated: '1m ago', head: '5d20b7c', changed: 7, stash: 0, backup: '2m ago',
|
|
101
|
-
ask: { question: 'Apply the migration to the staging database?', short: 'apply the migration to staging?', options: ['Yes, run it on staging', 'Run it on erp_test first', 'No, show me the SQL'] } },
|
|
102
|
-
{ id: 's-a017', agent: 'Codex', resume: 'codex resume', machine: 'vps-1', repo: 'erp', branch: 'feature/hr-payroll', state: 'running', turns: 9, cpu: 21, updated: '4s ago', head: '0f61a2d', changed: 1, stash: 0, backup: '12m ago' },
|
|
103
|
-
{ id: 's-91be', agent: 'Claude Code', resume: 'claude --resume', machine: 'mbp-shawki', repo: 'portal', branch: 'fix/login-redirect', state: 'running', turns: 23, cpu: 14, updated: '30s ago', head: '7aa0c19', changed: 2, stash: 0, backup: '8m ago' },
|
|
104
|
-
{ id: 's-d402', agent: 'Claude Code', resume: 'claude --resume', machine: 'mbp-shawki', repo: 'erp', branch: 'feature/mrp-routing', state: 'idle', turns: 58, cpu: 0, updated: '25m ago', head: '3c8e2a1', changed: 0, stash: 0, backup: '25m ago' },
|
|
105
|
-
{ id: 's-c3f8', agent: 'Claude Code', resume: 'claude --resume', machine: 'gpu-a100', repo: 'ml-forecast', branch: 'exp/lstm', state: 'running', turns: 77, cpu: 12, updated: '9s ago', head: 'e10fa33', changed: 4, stash: 0, backup: '6m ago' },
|
|
106
|
-
{ id: 's-e55b', agent: 'OpenCode', resume: 'opencode --continue', machine: 'win-qa', repo: 'portal', branch: 'main', state: 'suspended', turns: 44, cpu: 0, updated: '2h ago', head: 'b3e1f70', changed: 0, stash: 0, backup: '2h ago' },
|
|
107
|
-
],
|
|
108
|
-
backups: [
|
|
109
|
-
{ id: 'b-0098', session: 's-7f3a', agent: 'Claude Code', machine: 'mbp-shawki', label: 'moved to vps-1', turn: 61, sizeMb: 0.8, created: 'yesterday', head: '1be03c7', files: 1, prompt: 'first pass on inventory valuation', repo: 'erp' },
|
|
110
|
-
{ id: 'b-0120', session: 's-e55b', agent: 'OpenCode', machine: 'win-qa', label: 'before offline', turn: 44, sizeMb: 1.1, created: '2h ago', head: 'b3e1f70', files: 0, prompt: 'check SSO callback on Edge', repo: 'portal' },
|
|
111
|
-
{ id: 'b-0133', session: 's-7f3a', agent: 'Claude Code', machine: 'vps-1', label: 'auto', turn: 114, sizeMb: 1.7, created: '20m ago', head: '52f9e80', files: 4, prompt: 'move quant logic into stock_ext', repo: 'erp' },
|
|
112
|
-
{ id: 'b-0137', session: 's-a017', agent: 'Codex', machine: 'vps-1', label: 'auto', turn: 6, sizeMb: 0.4, created: '12m ago', head: '0f61a2d', files: 1, prompt: 'add bank export format for payroll', repo: 'erp' },
|
|
113
|
-
{ id: 'b-0139', session: 's-7f3a', agent: 'Claude Code', machine: 'vps-1', label: 'auto', turn: 128, sizeMb: 1.9, created: '10m ago', head: '7c04b19', files: 2, prompt: 'refactor _compute_qty_available', repo: 'erp',
|
|
114
|
-
diff: [{ path: 'models/stock_move.py', add: 48, del: 12 }, { path: 'models/stock_quant.py', add: 9, del: 3 }] },
|
|
115
|
-
{ id: 'b-0141', session: 's-2c91', agent: 'Codex', machine: 'vps-1', label: 'auto · before risky', turn: 36, sizeMb: 0.9, created: '2m ago', head: '5d20b7c', files: 7, prompt: 'write the migration for refunded_amount', repo: 'erp' },
|
|
116
|
-
{ id: 'b-0142', session: 's-7f3a', agent: 'Claude Code', machine: 'vps-1', label: 'before refactor', turn: 142, sizeMb: 2.1, created: 'just now', head: 'a91c2e4', files: 3, prompt: 'add tests for stock.move.line rounding', repo: 'erp' },
|
|
117
|
-
],
|
|
118
|
-
backupPolicy: { every: '10 min while running', everyTurn: false, beforeRisky: true, beforeTransfer: true, keep: '30 days · last 20 per session', storage: 'gitroots cloud', region: 'eu-central' },
|
|
119
|
-
tunnels: [
|
|
120
|
-
{ id: 't-27', session: 's-c3f8', machine: 'gpu-a100', target: ':8888 Jupyter', access: 'only you', viewers: 0, traffic: '41 MB', openFor: '1h 12m' },
|
|
121
|
-
],
|
|
122
|
-
// What an attached agent said recently (attach + watch live view).
|
|
123
|
-
transcripts: {
|
|
124
|
-
's-2c91': [
|
|
125
|
-
{ who: 'you', text: 'Add partial refunds to the POS order flow, then write the migration.' },
|
|
126
|
-
{ who: 'agent', text: 'Added refund_line_ids to pos.order and a wizard for partial amounts.\nTests pass locally (24 / 24).\n\nNext step needs a database change:\n§migrations/17.0.1.3/post-migrate.py§ adds column pos_order.refunded_amount\nand backfills 18,402 orders on *staging*.' },
|
|
127
|
-
],
|
|
128
|
-
},
|
|
129
|
-
hiddenTurns: { 's-2c91': 34 },
|
|
130
|
-
counters: { ev: 8813, backup: 143, tunnel: 31, session: 0 },
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// Live log pool for the watch screen (cycled in demo mode).
|
|
135
|
-
export const LIVE_POOL = (file = 'addons/stock_ext/models/stock_move.py') => [
|
|
136
|
-
{ verb: 'read', body: file },
|
|
137
|
-
{ verb: 'edit', body: `${file} `, add: '+24', del: ' −6' },
|
|
138
|
-
{ verb: 'run', body: 'odoo-bin -d erp_test --test-tags /stock_ext -u stock_ext --stop-after-init', running: true },
|
|
139
|
-
{ verb: 'output', body: 'Ran 18 tests in 7.42s' },
|
|
140
|
-
{ verb: 'output', body: '', fail: 'FAIL: test_move_line_qty_rounding expected 2.0, got 1.999' },
|
|
141
|
-
{ verb: 'think', body: 'Rounding should use the product UoM precision, not the company one.' },
|
|
142
|
-
{ verb: 'edit', body: `${file} `, add: '+3', del: ' −1' },
|
|
143
|
-
{ verb: 'run', body: 'odoo-bin -d erp_test --test-tags /stock_ext --stop-after-init', running: true },
|
|
144
|
-
{ verb: 'output', body: 'Ran 18 tests in 7.38s · ', add: 'OK' },
|
|
145
|
-
{ verb: 'git', body: 'commit -m "fix: round move line qty with product UoM" ', add: 'a91c2e5' },
|
|
146
|
-
{ verb: 'backup', body: 'auto · b-0144 · 2.1 MB', add: ' ✓' },
|
|
147
|
-
{ verb: 'read', body: 'addons/stock_ext/models/stock_quant.py' },
|
|
148
|
-
{ verb: 'think', body: 'Quant reservation has the same rounding issue; checking callers.' },
|
|
149
|
-
{ verb: 'edit', body: 'addons/stock_ext/models/stock_quant.py ', add: '+7', del: ' −2' },
|
|
150
|
-
];
|
|
151
|
-
|
|
152
|
-
export const FOLLOW_POOL = [
|
|
153
|
-
{ actor: 'shawki', action: 'approve', target: 'PR #412', decision: 'recorded' },
|
|
154
|
-
{ actor: 'shawki', action: 'merge', target: 'PR #412 → staging', decision: 'allowed', note: '2 approvals · checks green' },
|
|
155
|
-
{ actor: 's-7f3a', action: 'push', target: 'feature/inventory · 1 commit', decision: 'allowed', note: 'Claude Code on gpu-a100' },
|
|
156
|
-
{ actor: 'dev-02', action: 'push', target: 'staging · 1 commit', decision: 'blocked', note: 'staging: PR only' },
|
|
157
|
-
{ actor: 'portal', action: 'rule change', target: 'main · approvals 1 → 2', decision: 'held', note: 'needs a second admin' },
|
|
158
|
-
{ actor: 'dev-03', action: 'push', target: 'fix/login-redirect · 2 commits', decision: 'allowed', note: 'secrets scan clean' },
|
|
159
|
-
{ actor: 's-c3f8', action: 'push', target: 'ml-forecast exp/lstm', decision: 'allowed', note: 'Claude Code on gpu-a100' },
|
|
160
|
-
{ actor: 'nora', action: 'approve', target: 'PR #410', decision: 'recorded' },
|
|
161
|
-
{ actor: 'ahmed', action: 'clone', target: 'erp', decision: 'allowed', note: 'mbp-ahmed · macOS' },
|
|
162
|
-
{ actor: 's-2c91', action: 'push', target: 'migrations/17.0.1.3', decision: 'held', note: 'data migration · needs review' },
|
|
163
|
-
];
|
package/src/data/store.js
DELETED
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
// Local store. In demo mode the org starts from the sample data and every change
|
|
2
|
-
// (approvals, merges, backups, transfers, tunnels, audit events) is kept in
|
|
3
|
-
// $GITROOTS_HOME/state.json (default ~/.gitroots) so commands build on each other.
|
|
4
|
-
//
|
|
5
|
-
// A connected backend would implement the same methods against the gitroots API.
|
|
6
|
-
import fs from 'node:fs';
|
|
7
|
-
import os from 'node:os';
|
|
8
|
-
import path from 'node:path';
|
|
9
|
-
import { sampleData } from './sample.js';
|
|
10
|
-
|
|
11
|
-
export function home() {
|
|
12
|
-
return process.env.GITROOTS_HOME || path.join(os.homedir(), '.gitroots');
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function stateFile() { return path.join(home(), 'state.json'); }
|
|
16
|
-
|
|
17
|
-
function globMatch(pattern, value) {
|
|
18
|
-
const re = new RegExp(`^${pattern
|
|
19
|
-
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
20
|
-
.replace(/\*\*/g, '\u0000')
|
|
21
|
-
.replace(/\*/g, '[^/]*')
|
|
22
|
-
.replace(/\u0000/g, '.*')
|
|
23
|
-
.replace(/\?/g, '.')}$`);
|
|
24
|
-
return re.test(value);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export class Store {
|
|
28
|
-
constructor(data, { persist = true } = {}) {
|
|
29
|
-
this.d = data;
|
|
30
|
-
this.persist = persist;
|
|
31
|
-
this.mode = 'demo';
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
static open() {
|
|
35
|
-
let data;
|
|
36
|
-
try { data = JSON.parse(fs.readFileSync(stateFile(), 'utf8')); } catch { data = null; }
|
|
37
|
-
if (!data || data.version !== 1) data = { version: 1, ...sampleData() };
|
|
38
|
-
return new Store(data);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
static reset() {
|
|
42
|
-
try { fs.rmSync(stateFile()); } catch { /* none */ }
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
save() {
|
|
46
|
-
if (!this.persist) return;
|
|
47
|
-
try {
|
|
48
|
-
fs.mkdirSync(home(), { recursive: true });
|
|
49
|
-
fs.writeFileSync(stateFile(), JSON.stringify(this.d, null, 1));
|
|
50
|
-
} catch { /* read-only home: keep going in memory */ }
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
get org() { return this.orgOverlay || this.d.org; }
|
|
54
|
-
|
|
55
|
-
// ---- repos & rules
|
|
56
|
-
repos() { return this.d.repos; }
|
|
57
|
-
repo(name) {
|
|
58
|
-
const r = this.d.repos.find((x) => x.name === name);
|
|
59
|
-
if (!r) throw new Error(`no repository "${name}" · see gitroots repo list`);
|
|
60
|
-
return r;
|
|
61
|
-
}
|
|
62
|
-
repoByRemote(url) {
|
|
63
|
-
if (!url) return null;
|
|
64
|
-
const base = url.replace(/\/+$/, '').split(/[/:]/).pop().replace(/\.git$/, '');
|
|
65
|
-
return this.d.repos.find((r) => r.name === base || (r.remote && url.includes(r.remote.replace(/^.*?:/, '').replace(/\.git$/, '')))) || null;
|
|
66
|
-
}
|
|
67
|
-
branches(repo) { this.repo(repo); return this.d.branches[repo] || []; }
|
|
68
|
-
rules(repo) { return this.d.rules[repo] || { '*': { protected: false, directPush: true, label: 'developers can push' }, paths: [] }; }
|
|
69
|
-
ruleFor(repo, branch) {
|
|
70
|
-
const rules = this.rules(repo);
|
|
71
|
-
if (rules[branch]) return { name: branch, ...rules[branch] };
|
|
72
|
-
const keys = Object.keys(rules).filter((k) => k.includes('*') && k !== '*' && globMatch(k, branch));
|
|
73
|
-
if (keys.length) return { name: keys[0], ...rules[keys[0]] };
|
|
74
|
-
return { name: '*', ...(rules['*'] || { protected: false, directPush: true, label: 'developers can push' }) };
|
|
75
|
-
}
|
|
76
|
-
protectedPaths(repo) { return (this.rules(repo).paths || []); }
|
|
77
|
-
matchPath(pattern, file) { return globMatch(pattern, file); }
|
|
78
|
-
|
|
79
|
-
// ---- PRs
|
|
80
|
-
prs(repo) { return this.d.prs.filter((p) => !repo || p.repo === repo); }
|
|
81
|
-
pr(n) {
|
|
82
|
-
const num = Number(String(n).replace(/^#/, ''));
|
|
83
|
-
const p = this.d.prs.find((x) => x.number === num);
|
|
84
|
-
if (!p) throw new Error(`no pull request #${n} · see gitroots pr list`);
|
|
85
|
-
return p;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// ---- audit
|
|
89
|
-
audit() { return this.d.audit; }
|
|
90
|
-
event(id) {
|
|
91
|
-
const e = this.d.audit.find((x) => x.id === id);
|
|
92
|
-
if (!e) throw new Error(`no audit event ${id}`);
|
|
93
|
-
return e;
|
|
94
|
-
}
|
|
95
|
-
nextEventId() { return `ev-${this.d.counters.ev++}`; }
|
|
96
|
-
record(ev) {
|
|
97
|
-
const now = new Date();
|
|
98
|
-
const e = { id: this.nextEventId(), time: `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`, at: now.toISOString(), note: '', ...ev };
|
|
99
|
-
this.d.audit.push(e);
|
|
100
|
-
return e;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// ---- agents
|
|
104
|
-
machines() { return this.d.machines; }
|
|
105
|
-
machine(name) {
|
|
106
|
-
const m = this.d.machines.find((x) => x.name === name);
|
|
107
|
-
if (!m) throw new Error(`no machine "${name}" · machines: ${this.d.machines.map((x) => x.name).join(', ')}`);
|
|
108
|
-
return m;
|
|
109
|
-
}
|
|
110
|
-
sessions() { return this.d.sessions; }
|
|
111
|
-
session(id) {
|
|
112
|
-
const s = this.d.sessions.find((x) => x.id === id || x.id === `s-${id}`);
|
|
113
|
-
if (!s) throw new Error(`no agent session "${id}" · see gitroots watch`);
|
|
114
|
-
return s;
|
|
115
|
-
}
|
|
116
|
-
transcript(id) { return this.d.transcripts[id] || []; }
|
|
117
|
-
|
|
118
|
-
backups() { return this.d.backups; }
|
|
119
|
-
backup(id) {
|
|
120
|
-
const b = this.d.backups.find((x) => x.id === id);
|
|
121
|
-
if (!b) throw new Error(`no backup ${id} · see gitroots backups`);
|
|
122
|
-
return b;
|
|
123
|
-
}
|
|
124
|
-
nextBackupId() { return `b-${String(this.d.counters.backup++).padStart(4, '0')}`; }
|
|
125
|
-
|
|
126
|
-
tunnels() { return this.d.tunnels; }
|
|
127
|
-
nextTunnelId() { return `t-${this.d.counters.tunnel++}`; }
|
|
128
|
-
nextSessionId() {
|
|
129
|
-
this.d.counters.session++;
|
|
130
|
-
return `s-${Math.floor(Math.random() * 0xffff).toString(16).padStart(4, '0')}`;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// What needs the signed-in user.
|
|
134
|
-
needsYou() {
|
|
135
|
-
const me = this.org.user;
|
|
136
|
-
const items = [];
|
|
137
|
-
for (const p of this.d.prs) {
|
|
138
|
-
if (p.state === 'open' && p.needs && p.needs.who === me && !p.approvals.some((a) => a.by === me)) items.push({ kind: 'review', pr: p });
|
|
139
|
-
}
|
|
140
|
-
for (const s of this.d.sessions) if (s.state === 'waiting' && s.ask) items.push({ kind: 'agent', session: s });
|
|
141
|
-
// signed in, blocked pushes come from the real audit log instead
|
|
142
|
-
if (!this.api) for (const e of this.d.audit.filter((x) => x.decision === 'blocked' && x.action === 'push' && x.rule && !x.ack)) items.push({ kind: 'blocked', event: e });
|
|
143
|
-
return items;
|
|
144
|
-
}
|
|
145
|
-
}
|