gitxp 0.0.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/.output/nitro.json +17 -0
- package/.output/public/assets/index-BGHvBNtP.js +11 -0
- package/.output/public/assets/routes-DuE8S6pX.js +60 -0
- package/.output/public/assets/styles-D5xbQNYu.css +1 -0
- package/.output/server/_chunks/ssr-renderer.mjs +26 -0
- package/.output/server/_libs/@floating-ui/core+[...].mjs +671 -0
- package/.output/server/_libs/@floating-ui/dom+[...].mjs +649 -0
- package/.output/server/_libs/@floating-ui/react-dom+[...].mjs +856 -0
- package/.output/server/_libs/@radix-ui/react-arrow+[...].mjs +258 -0
- package/.output/server/_libs/@radix-ui/react-dialog+[...].mjs +1862 -0
- package/.output/server/_libs/@radix-ui/react-popper+[...].mjs +320 -0
- package/.output/server/_libs/@radix-ui/react-tooltip+[...].mjs +534 -0
- package/.output/server/_libs/@tanstack/db+[...].mjs +14680 -0
- package/.output/server/_libs/@tanstack/react-router+[...].mjs +14563 -0
- package/.output/server/_libs/@tanstack/react-router-ssr-query+[...].mjs +126 -0
- package/.output/server/_libs/@tanstack/router-core+[...].mjs +3636 -0
- package/.output/server/_libs/class-variance-authority+clsx.mjs +69 -0
- package/.output/server/_libs/cmdk.mjs +504 -0
- package/.output/server/_libs/h3+rou3+srvx.mjs +1361 -0
- package/.output/server/_libs/h3-v2.mjs +285 -0
- package/.output/server/_libs/lucide-react.mjs +158 -0
- package/.output/server/_libs/radix-ui__primitive.mjs +44 -0
- package/.output/server/_libs/radix-ui__react-context.mjs +108 -0
- package/.output/server/_libs/tailwind-merge.mjs +3380 -0
- package/.output/server/_libs/tanstack__history.mjs +384 -0
- package/.output/server/_libs/tanstack__query-core.mjs +2225 -0
- package/.output/server/_libs/tanstack__react-db.mjs +242 -0
- package/.output/server/_libs/tanstack__react-query.mjs +140 -0
- package/.output/server/_libs/ufo.mjs +64 -0
- package/.output/server/_runtime.mjs +35 -0
- package/.output/server/_ssr/empty-plugin-adapters-D9UWiqvJ.mjs +5 -0
- package/.output/server/_ssr/events-BNrmOvgU.mjs +13 -0
- package/.output/server/_ssr/notifications-DUoENv6E.mjs +514 -0
- package/.output/server/_ssr/router-DsUHD4bT.mjs +179 -0
- package/.output/server/_ssr/routes-DIFfr242.mjs +982 -0
- package/.output/server/_ssr/ssr.mjs +1854 -0
- package/.output/server/_ssr/start-5Z2QO8AU.mjs +4 -0
- package/.output/server/_tanstack-start-manifest_v-CrD0YpZr.mjs +20 -0
- package/.output/server/index.mjs +310 -0
- package/.output/server/node_modules/tslib/modules/index.js +70 -0
- package/.output/server/node_modules/tslib/modules/package.json +3 -0
- package/.output/server/node_modules/tslib/package.json +47 -0
- package/.output/server/node_modules/tslib/tslib.js +484 -0
- package/.output/server/package.json +9 -0
- package/LICENSE +21 -0
- package/README.md +325 -0
- package/bin/gitxp.js +85 -0
- package/package.json +92 -0
package/README.md
ADDED
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
# gitxp
|
|
2
|
+
|
|
3
|
+
An alternative GitHub UI that runs on your own machine. `npx gitxp` starts a
|
|
4
|
+
local server, authenticates through the GitHub CLI you already have set up, and
|
|
5
|
+
opens the app in your browser. There is no account to create and no OAuth app to
|
|
6
|
+
register.
|
|
7
|
+
|
|
8
|
+
The goal is an interface that feels like Linear rather than a website: the UI
|
|
9
|
+
reads from a local store and never blocks on a network round trip, and syncing
|
|
10
|
+
with GitHub happens in the background.
|
|
11
|
+
|
|
12
|
+
The long-term scope is the daily GitHub workflow: browsing repos, issues and
|
|
13
|
+
pull requests, reviewing code, discussions and comments, watching CI, and
|
|
14
|
+
merging. v0 is narrower, see Roadmap.
|
|
15
|
+
|
|
16
|
+
## Requirements
|
|
17
|
+
|
|
18
|
+
- Node 22.13 or newer, because the local store uses the built-in `node:sqlite`
|
|
19
|
+
module
|
|
20
|
+
- The [GitHub CLI](https://cli.github.com), authenticated with `gh auth login`
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npx gitxp
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Options:
|
|
29
|
+
|
|
30
|
+
- `--port <number>` use a specific port instead of a random free one
|
|
31
|
+
- `--no-open` start the server without opening a browser
|
|
32
|
+
|
|
33
|
+
## How it works
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
npx gitxp
|
|
37
|
+
|
|
|
38
|
+
+-- bin/gitxp.js checks `gh auth status`, picks a free port,
|
|
39
|
+
| sets HOST=127.0.0.1, boots the built server
|
|
40
|
+
|
|
|
41
|
+
+-- .output/server TanStack Start server functions talk to the
|
|
42
|
+
| GitHub API using the token from `gh auth token`
|
|
43
|
+
|
|
|
44
|
+
+-- browser React app that reads and writes a local
|
|
45
|
+
collection, synced against GitHub in the background
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The build output is a self-contained Node server produced by Nitro. It imports
|
|
49
|
+
nothing but `node:` builtins, so the published package has no runtime
|
|
50
|
+
dependencies.
|
|
51
|
+
|
|
52
|
+
## Roadmap
|
|
53
|
+
|
|
54
|
+
v0 is a complete review loop: a notifications inbox, the pull requests waiting
|
|
55
|
+
on you, the pull request itself with its diff and CI status, and submitting a
|
|
56
|
+
review with inline comments, approval or changes requested, and merging.
|
|
57
|
+
|
|
58
|
+
Build order, risky parts first, each step runnable on its own:
|
|
59
|
+
|
|
60
|
+
1. SQLite store, migration runner, the two zones
|
|
61
|
+
2. API client: token from `gh`, ETags, rate limit accounting
|
|
62
|
+
3. Notifications sync loop, one resource proven end to end
|
|
63
|
+
4. SSE and the client mirror, so a background write reaches the UI
|
|
64
|
+
5. Inbox UI: list, preview pane, command palette, row actions
|
|
65
|
+
6. Mutation queue, proven on mark-as-done
|
|
66
|
+
7. Review inbox as the second resource
|
|
67
|
+
8. Pull request detail: `@pierre/diffs`, existing review comments, CI status
|
|
68
|
+
9. Review submission: inline comments, approve, request changes, merge
|
|
69
|
+
|
|
70
|
+
After v0, in rough order: issues, repo and file browsing, discussions.
|
|
71
|
+
|
|
72
|
+
Out of v0 on purpose: search, projects, Actions logs, wikis, gists, org and team
|
|
73
|
+
views.
|
|
74
|
+
|
|
75
|
+
None of it is built yet. What is in `src/` today is the project scaffold.
|
|
76
|
+
|
|
77
|
+
## Interface
|
|
78
|
+
|
|
79
|
+
Three columns: a sidebar of views, the list, and a preview pane. Triage happens
|
|
80
|
+
without leaving the list. A pull request diff breaks out of that layout and
|
|
81
|
+
takes the full width.
|
|
82
|
+
|
|
83
|
+
Keyboard first. Cmd+K opens a command palette covering everything, and single
|
|
84
|
+
letters act on the focused row. The palette is also where an action lives before
|
|
85
|
+
it earns a shortcut, so nothing ends up reachable only by mouse.
|
|
86
|
+
|
|
87
|
+
The visual layer stays on stock shadcn for now. It is the one choice here that
|
|
88
|
+
is cheap to revisit, because retuning spacing and type tokens later does not
|
|
89
|
+
touch component structure or focus handling.
|
|
90
|
+
|
|
91
|
+
Cached rows render immediately, with a thin indicator while a background refresh
|
|
92
|
+
runs. Skeletons appear only where data is genuinely absent. Applied everywhere,
|
|
93
|
+
that rule is most of what separates this from a website.
|
|
94
|
+
|
|
95
|
+
## Local sync layer
|
|
96
|
+
|
|
97
|
+
The UI reads from a SQLite database at `~/.gitxp/`, written by the server
|
|
98
|
+
process. It uses the built-in `node:sqlite` module, so it adds no dependency and
|
|
99
|
+
the published package stays free of them. One server process means one writer,
|
|
100
|
+
sync keeps running while no tab is open, and the cache survives clearing browser
|
|
101
|
+
data.
|
|
102
|
+
|
|
103
|
+
### What gets synced
|
|
104
|
+
|
|
105
|
+
GitHub is too large to mirror, so the store has a boundary:
|
|
106
|
+
|
|
107
|
+
- Synced in the background: your notifications, pull requests waiting on your
|
|
108
|
+
review, and the repos you explicitly follow in gitxp.
|
|
109
|
+
- Fetched on demand and cached afterwards: file trees, blobs, older comment
|
|
110
|
+
threads, CI logs, anything you reach by navigating rather than by triage.
|
|
111
|
+
|
|
112
|
+
Anything outside the synced set is a cache miss on first view. The UI should
|
|
113
|
+
show it arriving rather than pretending it was already local.
|
|
114
|
+
|
|
115
|
+
### Schema
|
|
116
|
+
|
|
117
|
+
Each entity keeps GitHub's JSON payload as-is in a `raw` column, alongside
|
|
118
|
+
extracted columns for the fields queries filter and sort on. Scope grows toward
|
|
119
|
+
all of GitHub, and keeping the payload whole means adding a view does not need a
|
|
120
|
+
migration for every field that view happens to read.
|
|
121
|
+
|
|
122
|
+
Rows are keyed by GitHub's GraphQL `node_id`. REST and GraphQL return different
|
|
123
|
+
identifiers for the same object and REST payloads carry `node_id` too, so keying
|
|
124
|
+
on it means a row written by a bulk GraphQL pull and a row written by a REST
|
|
125
|
+
poll are the same row.
|
|
126
|
+
|
|
127
|
+
Notification threads are the exception. The REST notifications endpoint returns
|
|
128
|
+
no `node_id`, only a string thread `id`, so `notifications` is keyed on that.
|
|
129
|
+
|
|
130
|
+
The store has two zones. Everything cached from GitHub is disposable, can be
|
|
131
|
+
dropped and resynced, and therefore makes most schema changes free. The mutation
|
|
132
|
+
queue and your settings cannot be rederived from GitHub, so they live in tables
|
|
133
|
+
migrations have to preserve. That split is what makes a reset-cache command safe
|
|
134
|
+
to offer. Migrations run off SQLite's `user_version`.
|
|
135
|
+
|
|
136
|
+
### Scheduling
|
|
137
|
+
|
|
138
|
+
Each resource is its own job with its own interval, ETag and backoff, rather
|
|
139
|
+
than one loop polling everything. Checks on a pull request you are looking at
|
|
140
|
+
want seconds, followed repos want minutes, and whatever is on screen gets a
|
|
141
|
+
priority lane.
|
|
142
|
+
|
|
143
|
+
`node:sqlite` is synchronous, so a large write batch stalls HTTP responses in
|
|
144
|
+
the same process. Keep batches small and measure. If the UI stutters, the sync
|
|
145
|
+
loop moves to a worker thread.
|
|
146
|
+
|
|
147
|
+
### The client mirror
|
|
148
|
+
|
|
149
|
+
React runs in the browser and cannot read that SQLite file, so every row the UI
|
|
150
|
+
renders is copied into browser memory first. That copy is a TanStack DB
|
|
151
|
+
collection holding the slice the current view needs.
|
|
152
|
+
|
|
153
|
+
Holding a collection, rather than calling a server function per view, is what
|
|
154
|
+
allows filtering, sorting and joining to happen in the browser with no round
|
|
155
|
+
trip. It is why an inbox filter can feel instant. Only the bounded triage set is
|
|
156
|
+
mirrored this way; everything fetched on demand stays in the plain TanStack
|
|
157
|
+
Query cache.
|
|
158
|
+
|
|
159
|
+
The server pushes over Server-Sent Events at `/api/events`, as a plain
|
|
160
|
+
`ReadableStream` response. A sync that writes rows emits a change signal naming
|
|
161
|
+
the resource, and the client refetches that collection through its server
|
|
162
|
+
function, applying the result inside TanStack DB's `begin`, `truncate`, `write`
|
|
163
|
+
and `commit` calls. Mutations travel the other way as ordinary server function
|
|
164
|
+
calls, so the stream stays one-directional.
|
|
165
|
+
|
|
166
|
+
The stream carries a signal rather than the changed rows. On localhost the
|
|
167
|
+
refetch costs a sub-millisecond round trip and reuses the same query path as the
|
|
168
|
+
initial load, which is worth more than the saved bytes. Streaming rows, and
|
|
169
|
+
per-subset subscriptions through `loadSubset` and `unloadSubset`, are the
|
|
170
|
+
refinement if a collection ever grows past what a full refetch can carry.
|
|
171
|
+
|
|
172
|
+
```
|
|
173
|
+
GitHub --poll--> sync loop --write--> SQLite (~/.gitxp)
|
|
174
|
+
|
|
|
175
|
+
change signal
|
|
176
|
+
|
|
|
177
|
+
SSE stream at /api/events
|
|
178
|
+
|
|
|
179
|
+
client refetches that collection
|
|
180
|
+
|
|
|
181
|
+
SyncConfig: begin / truncate / write / commit
|
|
182
|
+
|
|
|
183
|
+
client collection --> React
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Writes
|
|
187
|
+
|
|
188
|
+
Mutations are queued in the same database and applied in order, so a write
|
|
189
|
+
survives a page reload. They fall into two classes:
|
|
190
|
+
|
|
191
|
+
- Optimistic: comments, labels, review submissions, marking a notification
|
|
192
|
+
done. The local store updates immediately and rolls back if the request fails.
|
|
193
|
+
- Confirmed: merging, closing, anything awkward to undo or that branch
|
|
194
|
+
protection and required checks can reject. The UI waits for GitHub and shows
|
|
195
|
+
the request in flight.
|
|
196
|
+
|
|
197
|
+
A queued write can fail long after you navigated away, so failed actions need a
|
|
198
|
+
persistent surface with retry rather than a toast that disappears.
|
|
199
|
+
|
|
200
|
+
A review in progress stays local until you submit it. Inline comments accumulate
|
|
201
|
+
in the queue and reach GitHub as one request that creates the review with its
|
|
202
|
+
comments, which keeps drafting instant and makes submission atomic.
|
|
203
|
+
|
|
204
|
+
### Staying inside the rate limit
|
|
205
|
+
|
|
206
|
+
The limit is 5000 requests an hour on REST and on GraphQL, and 30 a minute on
|
|
207
|
+
search, so search is not used for syncing.
|
|
208
|
+
|
|
209
|
+
- Background polling uses conditional requests. A 304 does not count against the
|
|
210
|
+
REST limit.
|
|
211
|
+
- The notifications endpoint returns an `X-Poll-Interval` header saying how
|
|
212
|
+
often GitHub wants to be polled. Respect it instead of picking an interval.
|
|
213
|
+
- GraphQL is for composite reads where one query replaces several REST calls,
|
|
214
|
+
such as a pull request with its reviews and checks.
|
|
215
|
+
- CI status is the hungriest part, because check runs change often. Poll it only
|
|
216
|
+
for a pull request currently on screen, never globally.
|
|
217
|
+
|
|
218
|
+
## Reviewing pull requests
|
|
219
|
+
|
|
220
|
+
Rendering is [`@pierre/diffs`](https://diffs.com) rather than a hand-built
|
|
221
|
+
viewer. It is Apache-2.0, built on Shiki, does stacked and split layouts, and
|
|
222
|
+
ships an annotation framework meant for review comments and CI annotations. File
|
|
223
|
+
trees come from [`@pierre/trees`](https://trees.software).
|
|
224
|
+
|
|
225
|
+
`@pierre/trees` is at `1.0.0-beta.6` and depends on a Preact 11 beta, so a
|
|
226
|
+
Preact beta ends up bundled next to React 19. Worth watching, and worth pinning
|
|
227
|
+
the version.
|
|
228
|
+
|
|
229
|
+
The data still comes from the REST pull request files endpoint, because GitHub
|
|
230
|
+
has already computed the patch hunks. That endpoint omits the patch for binary
|
|
231
|
+
files and for files over its size limit, so those need a fallback rather than an
|
|
232
|
+
empty view.
|
|
233
|
+
|
|
234
|
+
The preview pane is narrow, so it shows a stacked diff. Split view is available
|
|
235
|
+
at full width.
|
|
236
|
+
|
|
237
|
+
Review comments anchor to a path, a line and a side, mapped onto the annotation
|
|
238
|
+
framework. Comments whose line no longer exists in the current diff are
|
|
239
|
+
outdated, and the UI has to show them rather than drop them.
|
|
240
|
+
|
|
241
|
+
Long lists outside the diff still need virtualizing. `@tanstack/react-virtual`
|
|
242
|
+
is the fit, and is not installed yet because nothing imports it.
|
|
243
|
+
|
|
244
|
+
## Security
|
|
245
|
+
|
|
246
|
+
The server holds a real GitHub token, so it is only ever reachable from the
|
|
247
|
+
machine it runs on. `bin/gitxp.js` sets `HOST=127.0.0.1`, because the Nitro
|
|
248
|
+
default binds every interface.
|
|
249
|
+
|
|
250
|
+
The token comes from `gh auth token` and stays on the server side. Server
|
|
251
|
+
functions read it and the browser never receives it. It is re-read on a 401
|
|
252
|
+
rather than cached for the life of the process, and the host comes from
|
|
253
|
+
`gh auth status`, so an enterprise host works without extra configuration.
|
|
254
|
+
|
|
255
|
+
Server functions reject requests whose `Origin` header is not the app itself.
|
|
256
|
+
Any page open in your browser can reach `127.0.0.1`, and that is the realistic
|
|
257
|
+
attacker. This is not built yet, and needs to exist before the server starts
|
|
258
|
+
holding a token.
|
|
259
|
+
|
|
260
|
+
A local process can read the port and send no `Origin` at all. Defending against
|
|
261
|
+
that would need a secret generated at boot and carried in the URL. That is not
|
|
262
|
+
planned.
|
|
263
|
+
|
|
264
|
+
## Development
|
|
265
|
+
|
|
266
|
+
```bash
|
|
267
|
+
npm install
|
|
268
|
+
npm run dev # vite dev server on port 3000
|
|
269
|
+
npm run build # produces .output/
|
|
270
|
+
npm start # runs bin/gitxp.js against .output, same path as npx
|
|
271
|
+
npm run lint
|
|
272
|
+
npm run typecheck
|
|
273
|
+
npm run format
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
`npm run lint` does not typecheck. `npm run typecheck` catches what it misses,
|
|
277
|
+
including unsound casts on SQLite rows.
|
|
278
|
+
|
|
279
|
+
Modules under `src/server` import with explicit `.ts` extensions and stay inside
|
|
280
|
+
the TypeScript syntax Node can strip, so they run directly without the bundler:
|
|
281
|
+
|
|
282
|
+
```bash
|
|
283
|
+
GITXP_HOME=/tmp/gitxp-scratch node some-script.ts
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
## Publishing
|
|
287
|
+
|
|
288
|
+
`prepublishOnly` runs the build, and the `files` field ships `.output` and
|
|
289
|
+
`bin`. Everything else is a build-time concern and lives in `devDependencies`.
|
|
290
|
+
|
|
291
|
+
`dependencies` is empty on purpose. npm installs a package's dependencies when
|
|
292
|
+
you run it through npx, so anything listed there is downloaded by every user.
|
|
293
|
+
Nitro already inlines the framework into `.output`, which is about 1.4 MB
|
|
294
|
+
against a 278 MB development tree.
|
|
295
|
+
|
|
296
|
+
Check what would be published before releasing:
|
|
297
|
+
|
|
298
|
+
```bash
|
|
299
|
+
npm pack --dry-run
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
## Project layout
|
|
303
|
+
|
|
304
|
+
```
|
|
305
|
+
bin/gitxp.js CLI entry point, boots the built server
|
|
306
|
+
src/server/db/ SQLite store, migrations, per-resource sync state
|
|
307
|
+
src/server/github/ gh token, API client with ETags and rate limit accounting
|
|
308
|
+
src/server/sync/ per-resource sync jobs, scheduler, change emitter
|
|
309
|
+
src/functions/ server functions, the boundary the client calls
|
|
310
|
+
src/db-collections/ client collections mirroring the local database
|
|
311
|
+
src/routes/ file-based routes (TanStack Router)
|
|
312
|
+
src/components/ui/ shadcn components, all of them
|
|
313
|
+
src/integrations/ TanStack Query setup
|
|
314
|
+
.output/ build output, generated
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
## Built with
|
|
318
|
+
|
|
319
|
+
[TanStack Start](https://tanstack.com/start) with Nitro, TanStack Router,
|
|
320
|
+
Query, DB, Table and Form, Tailwind, and shadcn/ui. Diffs and file trees come
|
|
321
|
+
from [`@pierre/diffs`](https://diffs.com) and
|
|
322
|
+
[`@pierre/trees`](https://trees.software).
|
|
323
|
+
|
|
324
|
+
Every shadcn component is installed under `src/components/ui`. The full list is
|
|
325
|
+
in `AGENTS.md`.
|
package/bin/gitxp.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { execFileSync } from 'node:child_process'
|
|
3
|
+
import { existsSync } from 'node:fs'
|
|
4
|
+
import { createServer } from 'node:net'
|
|
5
|
+
import { fileURLToPath } from 'node:url'
|
|
6
|
+
|
|
7
|
+
const serverEntry = new URL('../.output/server/index.mjs', import.meta.url)
|
|
8
|
+
|
|
9
|
+
if (!existsSync(fileURLToPath(serverEntry))) {
|
|
10
|
+
console.error('Build output is missing. Run `npm run build` first.')
|
|
11
|
+
process.exit(1)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
execFileSync('gh', ['auth', 'status'], { stdio: 'ignore' })
|
|
16
|
+
} catch (error) {
|
|
17
|
+
console.error(
|
|
18
|
+
error.code === 'ENOENT'
|
|
19
|
+
? 'gitxp needs the GitHub CLI. Install it from https://cli.github.com, then run `gh auth login`.'
|
|
20
|
+
: 'gitxp needs an authenticated GitHub CLI. Run `gh auth login`.',
|
|
21
|
+
)
|
|
22
|
+
process.exit(1)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const args = process.argv.slice(2)
|
|
26
|
+
const portFlag = args.indexOf('--port')
|
|
27
|
+
const requestedPort = portFlag === -1 ? null : Number(args[portFlag + 1])
|
|
28
|
+
|
|
29
|
+
if (requestedPort !== null && !Number.isInteger(requestedPort)) {
|
|
30
|
+
console.error('--port needs a number, for example `gitxp --port 4000`.')
|
|
31
|
+
process.exit(1)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const port = requestedPort ?? (await findFreePort())
|
|
35
|
+
const url = `http://127.0.0.1:${port}`
|
|
36
|
+
|
|
37
|
+
process.env.HOST = '127.0.0.1'
|
|
38
|
+
process.env.PORT = String(port)
|
|
39
|
+
|
|
40
|
+
await import(serverEntry.href)
|
|
41
|
+
await waitUntilReady(url)
|
|
42
|
+
|
|
43
|
+
console.log(`gitxp is running at ${url}`)
|
|
44
|
+
|
|
45
|
+
if (!args.includes('--no-open')) {
|
|
46
|
+
openBrowser(url)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function findFreePort() {
|
|
50
|
+
return new Promise((resolve, reject) => {
|
|
51
|
+
const probe = createServer()
|
|
52
|
+
probe.on('error', reject)
|
|
53
|
+
probe.listen(0, '127.0.0.1', () => {
|
|
54
|
+
const { port } = probe.address()
|
|
55
|
+
probe.close(() => resolve(port))
|
|
56
|
+
})
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function waitUntilReady(target) {
|
|
61
|
+
for (let attempt = 0; attempt < 100; attempt++) {
|
|
62
|
+
try {
|
|
63
|
+
await fetch(target, { method: 'HEAD' })
|
|
64
|
+
return
|
|
65
|
+
} catch {
|
|
66
|
+
await new Promise((resolve) => setTimeout(resolve, 50))
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
throw new Error(`Server did not start at ${target}`)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function openBrowser(target) {
|
|
73
|
+
const [command, commandArgs] =
|
|
74
|
+
process.platform === 'darwin'
|
|
75
|
+
? ['open', [target]]
|
|
76
|
+
: process.platform === 'win32'
|
|
77
|
+
? ['cmd', ['/c', 'start', '', target]]
|
|
78
|
+
: ['xdg-open', [target]]
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
execFileSync(command, commandArgs, { stdio: 'ignore' })
|
|
82
|
+
} catch {
|
|
83
|
+
console.error(`Could not open a browser. Open ${target} manually.`)
|
|
84
|
+
}
|
|
85
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "gitxp",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "An alternative GitHub UI that runs locally on top of the GitHub CLI",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"gitxp": "bin/gitxp.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
".output",
|
|
12
|
+
"bin"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=22.13.0"
|
|
16
|
+
},
|
|
17
|
+
"imports": {
|
|
18
|
+
"#/*": "./src/*"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"dev": "vite dev --port 3000",
|
|
22
|
+
"generate-routes": "tsr generate",
|
|
23
|
+
"build": "vite build",
|
|
24
|
+
"start": "node bin/gitxp.js",
|
|
25
|
+
"preview": "vite preview",
|
|
26
|
+
"lint": "eslint",
|
|
27
|
+
"typecheck": "tsc --noEmit",
|
|
28
|
+
"format": "prettier --write . && eslint --fix",
|
|
29
|
+
"check": "prettier --check .",
|
|
30
|
+
"prepublishOnly": "vite build"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@base-ui/react": "^1.7.0",
|
|
34
|
+
"@hookform/resolvers": "^5.9.1",
|
|
35
|
+
"@pierre/diffs": "^1.3.6",
|
|
36
|
+
"@pierre/trees": "^1.0.0-beta.6",
|
|
37
|
+
"@shadcn/react": "^0.3.0",
|
|
38
|
+
"@tailwindcss/typography": "^0.5.16",
|
|
39
|
+
"@tailwindcss/vite": "^4.1.18",
|
|
40
|
+
"@tanstack/devtools-vite": "latest",
|
|
41
|
+
"@tanstack/eslint-config": "latest",
|
|
42
|
+
"@tanstack/match-sorter-utils": "latest",
|
|
43
|
+
"@tanstack/query-db-collection": "latest",
|
|
44
|
+
"@tanstack/react-db": "latest",
|
|
45
|
+
"@tanstack/react-devtools": "latest",
|
|
46
|
+
"@tanstack/react-form": "latest",
|
|
47
|
+
"@tanstack/react-query": "latest",
|
|
48
|
+
"@tanstack/react-query-devtools": "latest",
|
|
49
|
+
"@tanstack/react-router": "latest",
|
|
50
|
+
"@tanstack/react-router-devtools": "latest",
|
|
51
|
+
"@tanstack/react-router-ssr-query": "latest",
|
|
52
|
+
"@tanstack/react-start": "latest",
|
|
53
|
+
"@tanstack/react-table": "latest",
|
|
54
|
+
"@tanstack/router-cli": "^1.132.0",
|
|
55
|
+
"@types/node": "^22.10.2",
|
|
56
|
+
"@types/react": "^19.2.0",
|
|
57
|
+
"@types/react-dom": "^19.2.0",
|
|
58
|
+
"@vitejs/plugin-react": "^6.0.1",
|
|
59
|
+
"class-variance-authority": "^0.7.1",
|
|
60
|
+
"clsx": "^2.1.1",
|
|
61
|
+
"cmdk": "^1.1.1",
|
|
62
|
+
"date-fns": "^4.4.0",
|
|
63
|
+
"embla-carousel-react": "^8.6.0",
|
|
64
|
+
"eslint": "^9.20.0",
|
|
65
|
+
"input-otp": "^1.5.0",
|
|
66
|
+
"lucide-react": "^0.577.0",
|
|
67
|
+
"next-themes": "^0.4.6",
|
|
68
|
+
"nitro": "3.0.260610-beta",
|
|
69
|
+
"prettier": "^3.8.1",
|
|
70
|
+
"radix-ui": "^1.6.7",
|
|
71
|
+
"react": "^19.2.0",
|
|
72
|
+
"react-day-picker": "^10.0.1",
|
|
73
|
+
"react-dom": "^19.2.0",
|
|
74
|
+
"react-hook-form": "^7.87.0",
|
|
75
|
+
"react-resizable-panels": "^4.12.3",
|
|
76
|
+
"recharts": "^3.8.0",
|
|
77
|
+
"sonner": "^2.0.8",
|
|
78
|
+
"tailwind-merge": "^3.0.2",
|
|
79
|
+
"tailwindcss": "^4.1.18",
|
|
80
|
+
"tw-animate-css": "^1.3.6",
|
|
81
|
+
"typescript": "^6.0.2",
|
|
82
|
+
"vaul": "^1.1.2",
|
|
83
|
+
"vite": "^8.0.0",
|
|
84
|
+
"zod": "^4.3.6"
|
|
85
|
+
},
|
|
86
|
+
"pnpm": {
|
|
87
|
+
"onlyBuiltDependencies": [
|
|
88
|
+
"esbuild",
|
|
89
|
+
"lightningcss"
|
|
90
|
+
]
|
|
91
|
+
}
|
|
92
|
+
}
|