gitroots 0.0.0-stage → 0.5.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 +137 -2
- package/assets/logo.png +0 -0
- package/bin/gitroots.js +11 -0
- package/package.json +35 -4
- package/src/api.js +121 -0
- package/src/cli.js +150 -0
- package/src/commands/attach.js +262 -0
- package/src/commands/audit.js +150 -0
- package/src/commands/auth.js +99 -0
- package/src/commands/backup.js +106 -0
- package/src/commands/completion.js +69 -0
- package/src/commands/demo.js +12 -0
- package/src/commands/doctor.js +81 -0
- package/src/commands/pr.js +322 -0
- package/src/commands/push.js +273 -0
- package/src/commands/repos.js +122 -0
- package/src/commands/restore.js +125 -0
- package/src/commands/splash.js +95 -0
- package/src/commands/status.js +84 -0
- package/src/commands/transfer.js +79 -0
- package/src/commands/tunnel.js +116 -0
- package/src/commands/update.js +69 -0
- package/src/commands/watch.js +347 -0
- package/src/connected.js +82 -0
- package/src/data/live.js +49 -0
- package/src/data/sample.js +163 -0
- package/src/data/store.js +145 -0
- package/src/git/checks.js +96 -0
- package/src/installer.js +96 -0
- package/src/runtime.js +63 -0
- package/src/ui/ansi.js +138 -0
- package/src/ui/components.js +85 -0
- package/src/ui/term.js +296 -0
package/README.md
CHANGED
|
@@ -1,3 +1,138 @@
|
|
|
1
|
-
#
|
|
1
|
+
# gitroots CLI
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Code governance and agent sessions in the terminal. Built from the **gitroots CLI Terminal** design canvas. Each artboard maps to one command, using the same colors, symbols and layout.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
**macOS / Linux**: standalone binary, no Node needed:
|
|
8
|
+
```bash
|
|
9
|
+
curl -fsSL https://gitroots.com/cli/install.sh | bash
|
|
10
|
+
```
|
|
11
|
+
**Windows** (PowerShell):
|
|
12
|
+
```powershell
|
|
13
|
+
irm https://gitroots.com/cli/install.ps1 | iex
|
|
14
|
+
```
|
|
15
|
+
**npm** (any OS with Node 18+):
|
|
16
|
+
```bash
|
|
17
|
+
npm install -g gitroots
|
|
18
|
+
```
|
|
19
|
+
**Homebrew** (after publishing `dist/Formula/gitroots.rb` to a tap):
|
|
20
|
+
```bash
|
|
21
|
+
brew install <you>/tap/gitroots
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Then run `gitroots`, `gitroots login` and `gitroots doctor`. `gitroots update` updates the same way it was installed (native binary or npm), and `gitroots uninstall` removes a native install.
|
|
25
|
+
|
|
26
|
+
The native installer works like Claude Code's:
|
|
27
|
+
- binaries go to `~/.local/share/gitroots/versions/<version>/`
|
|
28
|
+
- `~/.local/bin/gitroots` links to the current one
|
|
29
|
+
- every download is checked against `SHA256SUMS`
|
|
30
|
+
- the current and previous versions are kept
|
|
31
|
+
- `~/.local/bin` is added to your shell profile (skip with `GITROOTS_NO_MODIFY_PATH=1`)
|
|
32
|
+
|
|
33
|
+
Pin a version with `… | bash -s -- 0.5.0`. On a platform without a binary (for example Alpine/musl), the installer falls back to the npm package if Node 18+ is present.
|
|
34
|
+
|
|
35
|
+
From a checkout: `npm link`.
|
|
36
|
+
|
|
37
|
+
## Releasing
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npm test
|
|
41
|
+
node scripts/build.mjs # all platforms → dist/
|
|
42
|
+
```
|
|
43
|
+
`dist/` is the whole release. Upload it as-is to the download location: `https://gitroots.com/cli` by default, or any static host/bucket. To use another host, set `GITROOTS_DOWNLOAD_BASE` at build time (it goes into the Homebrew formula) and in `src/runtime.js` (`DOWNLOAD_BASE`, used by `update` and `doctor`).
|
|
44
|
+
```
|
|
45
|
+
dist/install.sh dist/install.ps1 dist/latest
|
|
46
|
+
dist/0.5.0/gitroots-0.5.0-{darwin-arm64,darwin-x64,linux-x64,linux-arm64}.tar.gz
|
|
47
|
+
dist/0.5.0/gitroots-0.5.0-win32-x64.zip
|
|
48
|
+
dist/0.5.0/gitroots-0.5.0.tgz dist/0.5.0/SHA256SUMS dist/0.5.0/manifest.json
|
|
49
|
+
dist/Formula/gitroots.rb
|
|
50
|
+
```
|
|
51
|
+
- The binaries are Node 22 LTS single-executable apps, about 35–42 MB compressed. macOS builds are ad-hoc signed. For Gatekeeper-friendly distribution outside the curl installer, sign and notarize them with your Developer ID.
|
|
52
|
+
- For npm, run `npm publish` from this folder (the name `gitroots` was free on npm when this was written).
|
|
53
|
+
- For a new version, bump `version` in `package.json` and `VERSION` in `src/runtime.js`. The build refuses to run if they differ.
|
|
54
|
+
|
|
55
|
+
## Commands
|
|
56
|
+
|
|
57
|
+
| Design screen | Command |
|
|
58
|
+
|---|---|
|
|
59
|
+
| Welcome and boot | `gitroots` |
|
|
60
|
+
| Status | `gitroots status` |
|
|
61
|
+
| Repo · branch · clone | `gitroots repo list` · `branch list <repo> [--all]` · `branch rules <repo> <branch>` · `branch restore <repo> <branch>` · `clone <repo\|url> [--url <git-url>]` |
|
|
62
|
+
| git push · gitroots checks | `gitroots hooks install` then a normal `git push` · `gitroots check` (dry run) |
|
|
63
|
+
| PR list · view | `gitroots pr list [repo]` · `pr view <n>` · `pr diff <n>` |
|
|
64
|
+
| PR create | `gitroots pr create [--base staging] [--head branch] [--title "…"] [--draft]` |
|
|
65
|
+
| PR approve · merge | `gitroots pr approve <n> -m "…"` · `pr review <n> --request-changes -m "…"` · `pr merge <n> [--yes]` |
|
|
66
|
+
| Audit · follow | `gitroots audit [--repo --since 2h --actor --action --decision --pr --csv]` · `audit <ev-id>` · `audit follow` |
|
|
67
|
+
| Watch (live dashboard) | `gitroots watch [--machine m --agent codex --repo erp --state running]` (full screen) · `gitroots sessions` (plain list) |
|
|
68
|
+
| Attach | `gitroots attach <session>` · `attach <session> --shell` (prefix `ctrl-g`: d detach · b backup · t transfer · f fork · s share · h history) |
|
|
69
|
+
| Transfer | `gitroots transfer <session> --to <machine>` |
|
|
70
|
+
| Backup · backups | `gitroots backup <session> --label "…"` · `backups [--repo --session --all]` · `backup policy` · `backup policy set --every 10m --store s3://bucket` |
|
|
71
|
+
| Restore | `gitroots restore <session> --pick` · `restore <b-id>` · `--point <turn\|b-id>` · `--machine <m>` · `--fork` |
|
|
72
|
+
| Tunnel · share | `gitroots tunnel <session> --port 8069` · `tunnel ls` · `tunnel close <t-id>\|--all` · `share <session> --read-only --expires 1h` |
|
|
73
|
+
| (account) | `gitroots login [--server URL] [--org slug]` · `whoami` · `logout` |
|
|
74
|
+
| (setup) | `gitroots doctor` · `update` · `uninstall [--purge]` · `completion bash\|zsh\|fish` |
|
|
75
|
+
|
|
76
|
+
`gitroots help <command>` shows the details for one command.
|
|
77
|
+
|
|
78
|
+
## Terminal design rules (from the canvas)
|
|
79
|
+
|
|
80
|
+
- Every state pairs a symbol with a word: `● running`, `◐ waiting`, `○ queued`, `✓ done`, `✕ blocked`.
|
|
81
|
+
- Green is used only for success and the one next action. Amber means it needs you. Red means a real block or failure.
|
|
82
|
+
- Every screen ends with the next command (`→ gitroots pr view 412`).
|
|
83
|
+
- `--json` works on every command. Colors drop when output is piped or `NO_COLOR` is set.
|
|
84
|
+
- Motion (spinners, progress bars, live rows) stops when output is piped, with `NO_COLOR`, in `CI`, or when `GITROOTS_REDUCED_MOTION=1`.
|
|
85
|
+
- The welcome screen shows the real logo image (`assets/logo.png`) in Kitty, Ghostty, WezTerm and iTerm2. Other terminals, tmux and screen get a plain `gitroots` line. Set `GITROOTS_NO_IMAGES=1` to turn the image off.
|
|
86
|
+
|
|
87
|
+
## What is real and what is sample data
|
|
88
|
+
|
|
89
|
+
- **Push checks (real):** `gitroots hooks install` writes a `pre-push` hook. An existing hook is kept and run first with `--force`. On every `git push` the hook checks your actual commits:
|
|
90
|
+
- the branch rule
|
|
91
|
+
- protected paths
|
|
92
|
+
- a secrets scan of added lines (AWS, GitHub, GitLab, Slack, Stripe, Google, Anthropic and OpenAI keys, private keys, hard-coded passwords)
|
|
93
|
+
- commit authors and signatures
|
|
94
|
+
- migration files
|
|
95
|
+
|
|
96
|
+
A blocked push exits non-zero, so nothing leaves the machine. If gitroots itself can't be found, the hook blocks the push rather than skipping the checks.
|
|
97
|
+
- **Rules:** rules come from the gitroots org. For any other repo, put a `.gitroots.json` at its root:
|
|
98
|
+
```json
|
|
99
|
+
{ "protected": ["main", "release/*"], "paths": [{ "pattern": "infra/**", "needs": "an ops review" }] }
|
|
100
|
+
```
|
|
101
|
+
- **Signed in (real):** `gitroots login` signs in to your gitroots server (gitroots_core) with its Basic auth. The password is stored in the macOS Keychain, or in `~/.gitroots/credentials.json` (mode 600) elsewhere. After that, these use the real API:
|
|
102
|
+
- `whoami` and the `status` header
|
|
103
|
+
- `repo list` and `branch list` (GitHub repos synced in the dashboard)
|
|
104
|
+
- `audit`, `audit <ev-id>` and `audit follow` (which polls the API)
|
|
105
|
+
- the welcome screen's sign-in and repository checks
|
|
106
|
+
- **Sample data:** PRs, agent sessions, machines, backups and tunnels aren't in the gitroots API yet. They run on the design's sample org, and screens say so when you're signed in. Your changes to them are saved in `~/.gitroots/state.json`, so commands build on each other. `gitroots demo reset` starts over. When the API grows those endpoints, add them in `src/connected.js` the same way repos and audit work.
|
|
107
|
+
- **Shells:** `attach --shell` opens a real shell for sessions on this machine. A remote shell would need an agentd on that machine, which doesn't exist yet.
|
|
108
|
+
|
|
109
|
+
## Environment
|
|
110
|
+
|
|
111
|
+
| Variable | Effect |
|
|
112
|
+
|---|---|
|
|
113
|
+
| `GITROOTS_HOME` | where state, config and credentials live (default `~/.gitroots`) |
|
|
114
|
+
| `GITROOTS_SERVER` | default server for `gitroots login` (default `https://gitroots.com`) |
|
|
115
|
+
| `GITROOTS_DOWNLOAD_BASE` | where `update` and the installers fetch releases |
|
|
116
|
+
| `GITROOTS_NO_KEYCHAIN` | store the password in credentials.json even on macOS |
|
|
117
|
+
| `GITROOTS_RELAY_DOMAIN` | domain used for tunnel and share links (default `<relay>.relay.gitroots.com`) |
|
|
118
|
+
| `NO_COLOR` / `FORCE_COLOR` | turn colors off or force them on |
|
|
119
|
+
| `GITROOTS_REDUCED_MOTION` | no spinners or animation |
|
|
120
|
+
| `GITROOTS_NO_IMAGES` | no inline logo image |
|
|
121
|
+
| `GITROOTS_DEBUG` | print stack traces |
|
|
122
|
+
|
|
123
|
+
## Layout
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
bin/gitroots.js entry
|
|
127
|
+
src/cli.js argument parsing, dispatch, help
|
|
128
|
+
src/runtime.js version, npm vs standalone binary, install paths
|
|
129
|
+
src/api.js gitroots API client, credentials
|
|
130
|
+
src/connected.js real data when signed in
|
|
131
|
+
src/installer.js download, verify, install (gitroots update)
|
|
132
|
+
scripts/ build.mjs (release), install.sh, install.ps1
|
|
133
|
+
src/ui/ colors (design tokens), steps/spinners/prompts, full-screen helpers, inline images
|
|
134
|
+
src/commands/ one file per design screen
|
|
135
|
+
src/git/checks.js the real push checks
|
|
136
|
+
src/data/ sample org, persisted store, live activity streams
|
|
137
|
+
test/ node --test (includes a real git push against a scratch repo)
|
|
138
|
+
```
|
package/assets/logo.png
ADDED
|
Binary file
|
package/bin/gitroots.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { main } from '../src/cli.js';
|
|
3
|
+
|
|
4
|
+
main(process.argv.slice(2)).then(
|
|
5
|
+
(code) => { process.exitCode = code ?? 0; },
|
|
6
|
+
(err) => {
|
|
7
|
+
process.stderr.write(`gitroots: ${err && err.message ? err.message : err}\n`);
|
|
8
|
+
if (process.env.GITROOTS_DEBUG) process.stderr.write(`${err.stack}\n`);
|
|
9
|
+
process.exitCode = 1;
|
|
10
|
+
}
|
|
11
|
+
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,37 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gitroots",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "gitroots CLI: code governance and agent sessions.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"gitroots": "bin/gitroots.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"src",
|
|
12
|
+
"assets",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"test": "node --test test/",
|
|
17
|
+
"build": "node scripts/build.mjs",
|
|
18
|
+
"build:npm": "node scripts/build.mjs --no-binaries",
|
|
19
|
+
"prepublishOnly": "node --test test/"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"gitroots",
|
|
23
|
+
"git",
|
|
24
|
+
"governance",
|
|
25
|
+
"code-review",
|
|
26
|
+
"agents",
|
|
27
|
+
"cli"
|
|
28
|
+
],
|
|
29
|
+
"homepage": "https://gitroots.com",
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=18"
|
|
32
|
+
},
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
},
|
|
36
|
+
"license": "UNLICENSED"
|
|
37
|
+
}
|
package/src/api.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// gitroots API client (gitroots_core, Django REST framework).
|
|
2
|
+
// Sign-in uses the API's Basic auth; the password is kept in the macOS Keychain,
|
|
3
|
+
// or in $GITROOTS_HOME/credentials.json (mode 600) elsewhere.
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { spawnSync } from 'node:child_process';
|
|
7
|
+
import { home } from './data/store.js';
|
|
8
|
+
|
|
9
|
+
export const DEFAULT_SERVER = process.env.GITROOTS_SERVER || 'https://gitroots.com';
|
|
10
|
+
|
|
11
|
+
const configFile = () => path.join(home(), 'config.json');
|
|
12
|
+
const credFile = () => path.join(home(), 'credentials.json');
|
|
13
|
+
|
|
14
|
+
export function readConfig() {
|
|
15
|
+
try { return JSON.parse(fs.readFileSync(configFile(), 'utf8')); } catch { return {}; }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function writeConfig(cfg) {
|
|
19
|
+
fs.mkdirSync(home(), { recursive: true });
|
|
20
|
+
fs.writeFileSync(configFile(), JSON.stringify(cfg, null, 2), { mode: 0o600 });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const service = (server) => `gitroots:${server}`;
|
|
24
|
+
const useKeychain = () => process.platform === 'darwin' && !process.env.GITROOTS_NO_KEYCHAIN;
|
|
25
|
+
|
|
26
|
+
export function savePassword(server, username, password) {
|
|
27
|
+
if (useKeychain()) {
|
|
28
|
+
const r = spawnSync('security', ['add-generic-password', '-U', '-a', username, '-s', service(server), '-w', password], { stdio: 'ignore' });
|
|
29
|
+
if (r.status === 0) return 'keychain';
|
|
30
|
+
}
|
|
31
|
+
fs.mkdirSync(home(), { recursive: true });
|
|
32
|
+
let all = {};
|
|
33
|
+
try { all = JSON.parse(fs.readFileSync(credFile(), 'utf8')); } catch { /* none */ }
|
|
34
|
+
all[`${username}@${server}`] = password;
|
|
35
|
+
fs.writeFileSync(credFile(), JSON.stringify(all, null, 2), { mode: 0o600 });
|
|
36
|
+
fs.chmodSync(credFile(), 0o600);
|
|
37
|
+
return 'file';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function loadPassword(server, username) {
|
|
41
|
+
if (process.env.GITROOTS_PASSWORD) return process.env.GITROOTS_PASSWORD;
|
|
42
|
+
if (useKeychain()) {
|
|
43
|
+
const r = spawnSync('security', ['find-generic-password', '-a', username, '-s', service(server), '-w'], { encoding: 'utf8' });
|
|
44
|
+
if (r.status === 0) return r.stdout.replace(/\n$/, '');
|
|
45
|
+
}
|
|
46
|
+
try { return JSON.parse(fs.readFileSync(credFile(), 'utf8'))[`${username}@${server}`] || null; } catch { return null; }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function forgetPassword(server, username) {
|
|
50
|
+
if (process.platform === 'darwin') spawnSync('security', ['delete-generic-password', '-a', username, '-s', service(server)], { stdio: 'ignore' });
|
|
51
|
+
try {
|
|
52
|
+
const all = JSON.parse(fs.readFileSync(credFile(), 'utf8'));
|
|
53
|
+
delete all[`${username}@${server}`];
|
|
54
|
+
fs.writeFileSync(credFile(), JSON.stringify(all, null, 2), { mode: 0o600 });
|
|
55
|
+
} catch { /* none */ }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export class ApiError extends Error {
|
|
59
|
+
constructor(message, status) { super(message); this.status = status; }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export class Api {
|
|
63
|
+
constructor({ server, username, password }) {
|
|
64
|
+
this.server = server.replace(/\/+$/, '');
|
|
65
|
+
this.username = username;
|
|
66
|
+
this.auth = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
static fromConfig() {
|
|
70
|
+
const cfg = readConfig();
|
|
71
|
+
if (!cfg.server || !cfg.username) return null;
|
|
72
|
+
const password = loadPassword(cfg.server, cfg.username);
|
|
73
|
+
if (!password) return null;
|
|
74
|
+
return new Api({ server: cfg.server, username: cfg.username, password });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async get(p, query = {}) {
|
|
78
|
+
const url = new URL(`${this.server}/api/${p.replace(/^\/+/, '')}`);
|
|
79
|
+
for (const [k, v] of Object.entries(query)) if (v !== undefined && v !== null) url.searchParams.set(k, v);
|
|
80
|
+
let res;
|
|
81
|
+
try {
|
|
82
|
+
res = await fetch(url, { headers: { Authorization: this.auth, Accept: 'application/json', 'User-Agent': 'gitroots-cli' }, signal: AbortSignal.timeout(15000) });
|
|
83
|
+
} catch (e) {
|
|
84
|
+
throw new ApiError(`can't reach ${this.server} (${e.cause?.code || e.name})`, 0);
|
|
85
|
+
}
|
|
86
|
+
if (res.status === 401 || res.status === 403) {
|
|
87
|
+
let detail = '';
|
|
88
|
+
try { detail = (await res.json()).detail || ''; } catch { /* not json */ }
|
|
89
|
+
throw new ApiError(res.status === 401 ? 'not signed in, or the password changed · gitroots login' : `not allowed${detail ? `: ${detail}` : ''}`, res.status);
|
|
90
|
+
}
|
|
91
|
+
if (!res.ok) {
|
|
92
|
+
let detail = '';
|
|
93
|
+
try { detail = (await res.json()).detail || ''; } catch { /* not json */ }
|
|
94
|
+
throw new ApiError(`${res.status} from ${url.pathname}${detail ? `: ${detail}` : ''}`, res.status);
|
|
95
|
+
}
|
|
96
|
+
const ct = res.headers.get('content-type') || '';
|
|
97
|
+
if (!ct.includes('json')) throw new ApiError(`${url.pathname} did not return JSON · is ${this.server} a gitroots server?`, res.status);
|
|
98
|
+
return res.json();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// DRF list: a plain array, or paginated { results, next }.
|
|
102
|
+
async list(p, query = {}) {
|
|
103
|
+
let data = await this.get(p, query);
|
|
104
|
+
if (Array.isArray(data)) return data;
|
|
105
|
+
const out = [...(data.results || [])];
|
|
106
|
+
let pages = 0;
|
|
107
|
+
while (data.next && pages++ < 50) {
|
|
108
|
+
const next = new URL(data.next);
|
|
109
|
+
data = await this.get(next.pathname.replace(/^\/api\//, ''), Object.fromEntries(next.searchParams));
|
|
110
|
+
out.push(...(data.results || []));
|
|
111
|
+
}
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
me() { return this.list('users/').then((us) => us.find((u) => u.username === this.username || u.email === this.username) || null); }
|
|
116
|
+
orgs() { return this.list('orgs/'); }
|
|
117
|
+
memberships() { return this.list('memberships/'); }
|
|
118
|
+
auditEvents() { return this.list('audit/events/'); }
|
|
119
|
+
repos(orgId) { return this.get('integrations/github/org-repos', { org_id: orgId, limit: 200 }).then((d) => d.repos || []); }
|
|
120
|
+
branches(orgId, fullName) { return this.get('integrations/github/org-branches', { org_id: orgId, full_name: fullName }).then((d) => d.branches || []); }
|
|
121
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { setColor, c } from './ui/ansi.js';
|
|
2
|
+
import { line } from './ui/term.js';
|
|
3
|
+
import { brand, dot } from './ui/components.js';
|
|
4
|
+
import { Store } from './data/store.js';
|
|
5
|
+
import { VERSION } from './runtime.js';
|
|
6
|
+
import { connect } from './connected.js';
|
|
7
|
+
|
|
8
|
+
export { VERSION } from './runtime.js';
|
|
9
|
+
|
|
10
|
+
// Flags that never take a value.
|
|
11
|
+
const BOOL = new Set(['password-stdin', 'purge', 'draft', 'json', 'csv', 'all', 'pick', 'read-only', 'shell', 'request-changes', 'approve', 'comment', 'yes', 'y',
|
|
12
|
+
'help', 'h', 'version', 'v', 'no-color', 'fork', 'follow', 'squash', 'rebase', 'merge-commit', 'keep-branch', 'no-hooks', 'force', 'dry-run']);
|
|
13
|
+
|
|
14
|
+
export function parseArgs(argv) {
|
|
15
|
+
const args = { _: [], flags: {} };
|
|
16
|
+
for (let i = 0; i < argv.length; i++) {
|
|
17
|
+
const a = argv[i];
|
|
18
|
+
if (a === '--') { args._.push(...argv.slice(i + 1)); break; }
|
|
19
|
+
if (a.startsWith('--')) {
|
|
20
|
+
const eq = a.indexOf('=');
|
|
21
|
+
if (eq > -1) { args.flags[a.slice(2, eq)] = a.slice(eq + 1); continue; }
|
|
22
|
+
const key = a.slice(2);
|
|
23
|
+
if (key.startsWith('no-') && !BOOL.has(key)) { args.flags[key.slice(3)] = false; continue; }
|
|
24
|
+
const nxt = argv[i + 1];
|
|
25
|
+
if (!BOOL.has(key) && nxt !== undefined && !nxt.startsWith('-')) { args.flags[key] = nxt; i++; } else args.flags[key] = true;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (a.startsWith('-') && a.length > 1 && !/^-\d/.test(a)) {
|
|
29
|
+
const key = a.slice(1);
|
|
30
|
+
const nxt = argv[i + 1];
|
|
31
|
+
if (!BOOL.has(key) && key.length === 1 && nxt !== undefined && !nxt.startsWith('-')) { args.flags[key] = nxt; i++; } else args.flags[key] = true;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
args._.push(a);
|
|
35
|
+
}
|
|
36
|
+
return args;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const COMMANDS = {
|
|
40
|
+
status: () => import('./commands/status.js'),
|
|
41
|
+
repo: () => import('./commands/repos.js'),
|
|
42
|
+
repos: () => import('./commands/repos.js'),
|
|
43
|
+
branch: () => import('./commands/repos.js'),
|
|
44
|
+
clone: () => import('./commands/repos.js'),
|
|
45
|
+
hooks: () => import('./commands/push.js'),
|
|
46
|
+
hook: () => import('./commands/push.js'),
|
|
47
|
+
check: () => import('./commands/push.js'),
|
|
48
|
+
pr: () => import('./commands/pr.js'),
|
|
49
|
+
audit: () => import('./commands/audit.js'),
|
|
50
|
+
watch: () => import('./commands/watch.js'),
|
|
51
|
+
attach: () => import('./commands/attach.js'),
|
|
52
|
+
transfer: () => import('./commands/transfer.js'),
|
|
53
|
+
backup: () => import('./commands/backup.js'),
|
|
54
|
+
backups: () => import('./commands/backup.js'),
|
|
55
|
+
restore: () => import('./commands/restore.js'),
|
|
56
|
+
tunnel: () => import('./commands/tunnel.js'),
|
|
57
|
+
share: () => import('./commands/tunnel.js'),
|
|
58
|
+
sessions: () => import('./commands/watch.js'),
|
|
59
|
+
demo: () => import('./commands/demo.js'),
|
|
60
|
+
login: () => import('./commands/auth.js'),
|
|
61
|
+
logout: () => import('./commands/auth.js'),
|
|
62
|
+
whoami: () => import('./commands/auth.js'),
|
|
63
|
+
doctor: () => import('./commands/doctor.js'),
|
|
64
|
+
completion: () => import('./commands/completion.js'),
|
|
65
|
+
__complete: () => import('./commands/completion.js'),
|
|
66
|
+
update: () => import('./commands/update.js'),
|
|
67
|
+
upgrade: () => import('./commands/update.js'),
|
|
68
|
+
uninstall: () => import('./commands/update.js'),
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export async function main(argv) {
|
|
72
|
+
const args = parseArgs(argv);
|
|
73
|
+
if (args.flags.color === false || args.flags['no-color']) setColor(false);
|
|
74
|
+
if (args.flags.json || args.flags.csv) setColor(false);
|
|
75
|
+
if (args.flags.version || args.flags.v) { line(`gitroots ${VERSION}`); return 0; }
|
|
76
|
+
|
|
77
|
+
const [cmd, ...rest] = args._;
|
|
78
|
+
if (!cmd) {
|
|
79
|
+
if (args.flags.help || args.flags.h) { help(); return 0; }
|
|
80
|
+
const { default: splash } = await import('./commands/splash.js');
|
|
81
|
+
return splash(connect(Store.open()), args);
|
|
82
|
+
}
|
|
83
|
+
if (cmd === 'help' || args.flags.help || args.flags.h) { help(cmd === 'help' ? rest[0] : cmd); return 0; }
|
|
84
|
+
if (cmd === 'version') { line(`gitroots ${VERSION}`); return 0; }
|
|
85
|
+
|
|
86
|
+
const load = COMMANDS[cmd];
|
|
87
|
+
if (!load) {
|
|
88
|
+
line(`${c.r('✕')} unknown command ${c.b(cmd)}`);
|
|
89
|
+
line(` ${c.g('→ gitroots help')}`);
|
|
90
|
+
return 2;
|
|
91
|
+
}
|
|
92
|
+
const mod = await load();
|
|
93
|
+
const store = connect(Store.open());
|
|
94
|
+
const code = await mod.run(cmd, rest, args.flags, store);
|
|
95
|
+
return code ?? 0;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const HELP = [
|
|
99
|
+
['repos', 'gitroots repo list · branch list <repo> · branch rules <repo> <branch> · clone <repo>', 'repositories and branch rules'],
|
|
100
|
+
['push', 'git push (after gitroots hooks install) · gitroots check', 'gitroots checks every push before it leaves'],
|
|
101
|
+
['review', 'gitroots pr list · pr view <n> · pr diff <n> · pr approve <n> · pr merge <n>', 'reviews and merges'],
|
|
102
|
+
['audit', 'gitroots audit · audit <ev-id> · audit follow', 'who did what, when, and what was blocked'],
|
|
103
|
+
['agents', 'gitroots watch · attach · transfer · backup · backups · restore · tunnel · share', 'every agent session, one place'],
|
|
104
|
+
['account', 'gitroots login · whoami · logout', 'sign in to your gitroots server'],
|
|
105
|
+
['setup', 'gitroots doctor · update · completion <shell> · uninstall', 'keep the CLI healthy and current'],
|
|
106
|
+
];
|
|
107
|
+
|
|
108
|
+
const DETAIL = {
|
|
109
|
+
status: ['gitroots status', 'Everything at a glance: what needs you, repositories, agent sessions, the last hour.'],
|
|
110
|
+
repo: ['gitroots repo list', 'List repositories you can see, with protected branches and your role.'],
|
|
111
|
+
branch: ['gitroots branch list <repo> [--all]\ngitroots branch rules <repo> <branch>', 'Branches and the rules gitroots enforces on them.'],
|
|
112
|
+
clone: ['gitroots clone <repo|git-url> [dir] [--url <git-url>]', 'Clone a repository and install the gitroots push checks.'],
|
|
113
|
+
hooks: ['gitroots hooks install [--force]\ngitroots hooks uninstall', 'Install the pre-push hook so git push runs gitroots checks first.'],
|
|
114
|
+
check: ['gitroots check [<base>..<head>] [--branch <name>]', 'Run the push checks on local commits without pushing.'],
|
|
115
|
+
pr: ['gitroots pr list [repo] [--state open|merged|all]\ngitroots pr view <n>\ngitroots pr diff <n>\ngitroots pr create [--base staging] [--head branch] [--title "…"] [--draft]\ngitroots pr approve <n> [-m "note"]\ngitroots pr review <n> --request-changes -m "why"\ngitroots pr merge <n> [--yes] [--squash|--rebase|--merge-commit] [--keep-branch]', 'Pull requests: review, approve and merge under the branch rules.'],
|
|
116
|
+
audit: ['gitroots audit [--repo r] [--since 2h] [--actor a] [--action a] [--decision blocked] [--pr n] [--csv]\ngitroots audit <ev-id>\ngitroots audit follow', 'History of every push, approval, merge and rule change, plus a live stream.'],
|
|
117
|
+
watch: ['gitroots watch [--machine m]', 'Live dashboard of every agent session. Keys: ↑↓ move · tab machines/sessions · a attach · t transfer · b backup · r restore · f fork · u tunnel · p pause · s stop · / filter · ? help · q quit'],
|
|
118
|
+
attach: ['gitroots attach <session>', 'Attach to an agent session. Prefix ctrl-g: d detach · b backup · t transfer · f fork · s share read-only. Everything else goes to the agent.'],
|
|
119
|
+
transfer: ['gitroots transfer <session> --to <machine>', 'Move a running agent session to another machine with its conversation and workspace.'],
|
|
120
|
+
backup: ['gitroots backup <session> [--label "…"]\ngitroots backups [--repo r] [--session s] [--all]\ngitroots backup policy', 'Snapshot an agent session: conversation, workspace and environment.'],
|
|
121
|
+
restore: ['gitroots restore <session|backup> [--machine m] [--pick] [--point turn|backup] [--fork]', 'Bring a session back to an earlier point, on any machine.'],
|
|
122
|
+
tunnel: ['gitroots tunnel <session> --port <n> [--shell]\ngitroots tunnel ls\ngitroots tunnel close <t-id> | --all\ngitroots share <session> --read-only [--expires 1h]', 'Reach a port on the agent\'s machine, or share a read-only live view.'],
|
|
123
|
+
demo: ['gitroots demo reset', 'Reset the demo organisation to its starting state.'],
|
|
124
|
+
login: ['gitroots login [--server https://gitroots.com] [--username u] [--org slug] [--password-stdin]', 'Sign in to a gitroots server. Repositories, branches and audit then come from the API.'],
|
|
125
|
+
whoami: ['gitroots whoami', 'Show who you are signed in as.'],
|
|
126
|
+
logout: ['gitroots logout', 'Sign out and forget the stored password.'],
|
|
127
|
+
doctor: ['gitroots doctor', 'Check the install, git, hooks in this repo, sign-in and terminal support.'],
|
|
128
|
+
update: ['gitroots update [--version x.y.z]', 'Update to the latest release, the same way gitroots was installed.'],
|
|
129
|
+
completion: ['gitroots completion bash|zsh|fish', 'Print a shell completion script. e.g. gitroots completion zsh > "${fpath[1]}/_gitroots"'],
|
|
130
|
+
uninstall: ['gitroots uninstall [--purge]', 'Remove the native install (--purge also removes ~/.gitroots).'],
|
|
131
|
+
};
|
|
132
|
+
DETAIL.upgrade = DETAIL.update; DETAIL.repos = DETAIL.repo; DETAIL.backups = DETAIL.backup; DETAIL.share = DETAIL.tunnel; DETAIL.hook = DETAIL.hooks;
|
|
133
|
+
|
|
134
|
+
export function help(topic) {
|
|
135
|
+
if (topic && DETAIL[topic]) {
|
|
136
|
+
const [usage, desc] = DETAIL[topic];
|
|
137
|
+
line(desc);
|
|
138
|
+
line();
|
|
139
|
+
for (const u of usage.split('\n')) line(` ${c.g(u)}`);
|
|
140
|
+
line();
|
|
141
|
+
line(c.m(' --json on every command · colors and motion drop when piped or with NO_COLOR'));
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
line(`${brand()} ${c.s(`v${VERSION}`)} ${dot()} ${c.s('code governance and agent sessions')}`);
|
|
145
|
+
line();
|
|
146
|
+
line(c.m('QUICK START'));
|
|
147
|
+
for (const [k, cmds, what] of HELP) line(` ${c.s(k.padEnd(8))} ${c.g(cmds)}`), line(` ${' '.repeat(8)} ${c.m(what)}`);
|
|
148
|
+
line();
|
|
149
|
+
line(c.m(' gitroots help <command> for details · --json on every command'));
|
|
150
|
+
}
|