repoview 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/CHANGELOG.md +53 -0
- package/CONTRIBUTING.md +4 -3
- package/DEVELOPMENT.md +70 -16
- package/README.md +36 -5
- package/dist/api.js +58 -0
- package/dist/api.js.map +1 -0
- package/dist/cli.js +224 -0
- package/dist/cli.js.map +1 -0
- package/dist/csv.js +64 -0
- package/dist/csv.js.map +1 -0
- package/dist/format.js +25 -0
- package/dist/format.js.map +1 -0
- package/dist/git.js +67 -0
- package/dist/git.js.map +1 -0
- package/dist/gitignore.js +34 -0
- package/dist/gitignore.js.map +1 -0
- package/dist/linkcheck.js +310 -0
- package/dist/linkcheck.js.map +1 -0
- package/dist/markdown.js +493 -0
- package/dist/markdown.js.map +1 -0
- package/dist/net.js +10 -0
- package/dist/net.js.map +1 -0
- package/dist/paths.js +59 -0
- package/dist/paths.js.map +1 -0
- package/dist/reload.js +36 -0
- package/dist/reload.js.map +1 -0
- package/dist/repo-context.js +73 -0
- package/dist/repo-context.js.map +1 -0
- package/dist/repo-router.js +801 -0
- package/dist/repo-router.js.map +1 -0
- package/dist/review-cli.js +228 -0
- package/dist/review-cli.js.map +1 -0
- package/dist/server.js +116 -0
- package/dist/server.js.map +1 -0
- package/dist/session.js +86 -0
- package/dist/session.js.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/views.js +633 -0
- package/dist/views.js.map +1 -0
- package/package.json +22 -6
- package/public/app.css +842 -0
- package/public/app.js +35 -2
- package/public/review.js +587 -0
- package/public/session.js +61 -0
- package/src/cli.js +0 -73
- package/src/gitignore.js +0 -34
- package/src/linkcheck.js +0 -312
- package/src/markdown.js +0 -364
- package/src/server.js +0 -760
- package/src/views.js +0 -479
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. This project adheres to
|
|
4
|
+
[Semantic Versioning](https://semver.org/).
|
|
5
|
+
|
|
6
|
+
## 0.6.0 — 2026-06-04
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
- **Shared multi-repo sessions (tmux-style).** Multiple `repoview` invocations on
|
|
10
|
+
the same port now join one server instead of each starting its own. The first
|
|
11
|
+
run starts the daemon; later runs register their repo and exit immediately — no
|
|
12
|
+
more remembering a port per repo. Each repo is served at `/r/<id>/…`.
|
|
13
|
+
- **Repo switcher** in the topbar and a **`/session` management page** to open,
|
|
14
|
+
add, and remove repos from the browser.
|
|
15
|
+
- **CLI session commands:** `repoview ls`, `repoview rm <id|path>`, `repoview stop`.
|
|
16
|
+
- **Control API:** `GET /api/session`, `GET/POST /api/repos`,
|
|
17
|
+
`DELETE /api/repos/:id`, `POST /api/shutdown` (mutations restricted to loopback).
|
|
18
|
+
- HTTP integration test suite (`npm test`, `node:test`) — runs without a browser.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
- **Default port is now `7376`** ("REPO" on a phone keypad) instead of `3000`, to
|
|
22
|
+
avoid colliding with common dev servers. Override with `--port` or `$PORT`.
|
|
23
|
+
- **Migrated the codebase to TypeScript** (strict). Sources compile to `dist/`;
|
|
24
|
+
the published package ships `dist/` and `bin` points at `dist/cli.js`.
|
|
25
|
+
- `startServer` was refactored into a session + per-repo context/router design;
|
|
26
|
+
each repo owns its own git info, gitignore matcher, link scanner, reload hub,
|
|
27
|
+
and file watcher.
|
|
28
|
+
- Playwright moved to `npm run test:e2e`; `npm test` is the browser-free suite.
|
|
29
|
+
|
|
30
|
+
### Security
|
|
31
|
+
- The session binds `0.0.0.0` by default, exposing every added repo to the
|
|
32
|
+
network. The server now **warns at startup** when bound to a non-loopback host,
|
|
33
|
+
and the `/session` page is **read-only** for non-loopback viewers. Use
|
|
34
|
+
`--host 127.0.0.1` to keep a session fully local.
|
|
35
|
+
|
|
36
|
+
### Fixed
|
|
37
|
+
- Markdown content links are prefixed with the repo base exactly once (guards a
|
|
38
|
+
double-rewrite during sanitization).
|
|
39
|
+
- Repo switcher dropdown stays on-screen on narrow viewports (left-anchored).
|
|
40
|
+
|
|
41
|
+
### Upgrade notes (0.5.x → 0.6.0)
|
|
42
|
+
- **Default port changed** from `3000` to `7376`. If you relied on the old
|
|
43
|
+
default, pass `--port 3000` (or set `$PORT`).
|
|
44
|
+
- **URLs are now repo-prefixed** (`/r/<id>/…`). Old bookmarks to `/tree/…`,
|
|
45
|
+
`/blob/…`, etc. still work — they redirect to the default repo — but the
|
|
46
|
+
canonical URL now includes the repo id.
|
|
47
|
+
- No data migration is needed; review threads under `.repoview/` are unchanged.
|
|
48
|
+
|
|
49
|
+
## 0.5.1
|
|
50
|
+
|
|
51
|
+
- Render Markdown frontmatter as a styled header.
|
|
52
|
+
- Inline code review threads; Playwright end-to-end suite.
|
|
53
|
+
</content>
|
package/CONTRIBUTING.md
CHANGED
|
@@ -11,13 +11,14 @@ npm install
|
|
|
11
11
|
## Run
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
npm start -- --repo /path/to/repo --port
|
|
14
|
+
npm start -- --repo /path/to/repo --port 7376
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
## Lint
|
|
17
|
+
## Lint & test
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
|
-
npm run lint
|
|
20
|
+
npm run lint # tsc --noEmit (strict type-check)
|
|
21
|
+
npm test # build + node:test HTTP integration suite
|
|
21
22
|
```
|
|
22
23
|
|
|
23
24
|
## What to work on
|
package/DEVELOPMENT.md
CHANGED
|
@@ -4,43 +4,80 @@ This doc collects the “how it works” details so `README.md` can stay product
|
|
|
4
4
|
|
|
5
5
|
## Project layout
|
|
6
6
|
|
|
7
|
-
- `src/server.
|
|
8
|
-
- `src/
|
|
9
|
-
- `src/
|
|
10
|
-
- `src/
|
|
11
|
-
- `src/
|
|
12
|
-
- `
|
|
7
|
+
- `src/server.ts`: Express app wiring — vendor static mounts, the control API, the `/r/:repoId` repos router, the `/session` page, and legacy redirects
|
|
8
|
+
- `src/session.ts`: a session owning multiple repos (add/remove/list, slug ids, default repo)
|
|
9
|
+
- `src/api.ts`: control API router (`/api/session`, `/api/repos`, `/api/shutdown`); mutations loopback-guarded
|
|
10
|
+
- `src/repo-context.ts`: per-repo runtime state (git info, gitignore matcher, link scanner, reload hub, file watcher)
|
|
11
|
+
- `src/repo-router.ts`: the per-repo routes (`/tree`, `/blob`, `/raw`, `/diff`, `/review`, `/events`, …) as a router that resolves `/r/:repoId` → a per-repo child router
|
|
12
|
+
- `src/net.ts`: loopback helpers (control-endpoint guard, bind-host check)
|
|
13
|
+
- `src/types.ts`: shared interfaces (`RepoContext`, `Session`/`RepoSummary`, `GitInfo`, `MarkdownRenderer`, `LinkScanner`, …)
|
|
14
|
+
- `src/git.ts` / `src/paths.ts` / `src/format.ts` / `src/csv.ts` / `src/reload.ts`: extracted helpers (git CLI, path safety, byte/date formatting, CSV parsing, SSE reload hub)
|
|
15
|
+
- `src/markdown.ts`: Markdown rendering + link/image rewriting (repo-prefixed) + sanitization
|
|
16
|
+
- `src/linkcheck.ts`: broken-link scanner (Markdown → rendered HTML → internal link validation)
|
|
17
|
+
- `src/gitignore.ts`: `.gitignore` matcher (used for hiding + scanner noise reduction)
|
|
18
|
+
- `src/views.ts`: HTML templates (mobile-first top bar + repo switcher + GitHub-style Markdown shell)
|
|
19
|
+
- `public/`: CSS + client JS (live reload, KaTeX render, Mermaid render, diff collapse, query preservation, session management)
|
|
20
|
+
|
|
21
|
+
> See `docs/multi-repo-session.md` for the full multi-repo session design.
|
|
22
|
+
|
|
23
|
+
## TypeScript
|
|
24
|
+
|
|
25
|
+
The source is TypeScript (`src/*.ts`, `strict` mode). It compiles to `dist/` via
|
|
26
|
+
`tsc` and the published package ships `dist/` (the `bin` points at `dist/cli.js`).
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm run build # tsc → dist/
|
|
30
|
+
npm run lint # tsc --noEmit (type-check only)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
`dist/` is git-ignored and rebuilt on `prepack` (so `npm pack` / `npm publish`
|
|
34
|
+
always ship a fresh build).
|
|
13
35
|
|
|
14
36
|
## Running locally
|
|
15
37
|
|
|
16
38
|
```bash
|
|
17
39
|
npm install
|
|
18
|
-
npm start -- --repo /path/to/repo --port
|
|
40
|
+
npm start -- --repo /path/to/repo --port 7376 # runs src via tsx (no build needed)
|
|
41
|
+
# or, after a build:
|
|
42
|
+
node dist/cli.js --repo /path/to/repo --port 7376
|
|
19
43
|
```
|
|
20
44
|
|
|
45
|
+
`npm start` / `npm run dev` use `tsx` to run the TypeScript directly for fast
|
|
46
|
+
iteration; `npm run build` produces the runnable `dist/` for publishing.
|
|
47
|
+
|
|
21
48
|
Useful flags:
|
|
22
49
|
- `--watch` / `--no-watch` (watch is on by default)
|
|
23
50
|
- `--host 127.0.0.1` to bind locally only
|
|
24
51
|
|
|
25
52
|
## Routes
|
|
26
53
|
|
|
54
|
+
Every repo is served behind a `/r/<id>` prefix (the repo's slug id). Legacy
|
|
55
|
+
non-prefixed paths (`/`, `/tree`, `/blob`, `/raw`, `/diff`, `/review`,
|
|
56
|
+
`/broken-links`, `/events`, `/rev`) redirect to the default (first) repo.
|
|
57
|
+
|
|
58
|
+
Per-repo routes (under `/r/<id>`):
|
|
27
59
|
- `GET /tree/<path>`: directory listing (applies `.gitignore` by default; `?ignored=1` shows ignored)
|
|
28
60
|
- `GET /blob/<path>`: file view (Markdown rendered; non-Markdown shown as highlighted text)
|
|
29
61
|
- `GET /raw/<path>`: raw bytes (used for images and downloads)
|
|
30
|
-
- `GET /events`: Server-Sent Events stream for live reload
|
|
62
|
+
- `GET /events`: Server-Sent Events stream for live reload (`GET /rev` is the polling fallback)
|
|
31
63
|
- `GET /diff`: diff view — compare working tree against a base ref (`?base=HEAD` default; accepts branches, tags)
|
|
32
|
-
- `GET /
|
|
33
|
-
- `GET /broken-links.json`:
|
|
64
|
+
- `GET /review/…`: code review threads
|
|
65
|
+
- `GET /broken-links[.json]`: broken internal link report
|
|
66
|
+
|
|
67
|
+
Session-level routes:
|
|
68
|
+
- `GET /session`: manage repos (open / add / remove); read-only for non-loopback clients
|
|
69
|
+
- `GET /api/session`: session signature + repo list (also the join handshake)
|
|
70
|
+
- `GET /api/repos`, `POST /api/repos`, `DELETE /api/repos/:id`, `POST /api/shutdown`: control API (mutations are loopback-only)
|
|
34
71
|
|
|
35
72
|
## Link rewriting rules
|
|
36
73
|
|
|
37
|
-
`src/markdown.
|
|
74
|
+
`src/markdown.ts` rewrites relative Markdown links so they stay inside the repo UI:
|
|
38
75
|
|
|
39
|
-
- Links →
|
|
40
|
-
- Images →
|
|
76
|
+
- Links → `<repoBase>/blob/<path>` (or `<repoBase>/tree/<path>` when the link ends with `/`), where `repoBase` is `/r/<id>`
|
|
77
|
+
- Images → `<repoBase>/raw/<path>`
|
|
41
78
|
- Same rewriting is applied to HTML inside Markdown (`<a href>`, `<img src>`) after sanitization.
|
|
42
79
|
- Paths that would escape the repo root (leading `../`) are clamped to the repo root (GitHub-like).
|
|
43
|
-
- Already-internal links (`/blob/…`, `/tree/…`, `/raw/…`, `/static
|
|
80
|
+
- Already-internal links (`/blob/…`, `/tree/…`, `/raw/…`, `/static/…`, and anything already under `repoBase`) are not rewritten again.
|
|
44
81
|
|
|
45
82
|
## Markdown “GitHub-like” features
|
|
46
83
|
|
|
@@ -65,9 +102,26 @@ Notes:
|
|
|
65
102
|
## Lint
|
|
66
103
|
|
|
67
104
|
```bash
|
|
68
|
-
npm run lint
|
|
105
|
+
npm run lint # tsc --noEmit — full strict type-check
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Tests
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
npm test # build + node:test HTTP integration suite (no browser needed)
|
|
112
|
+
npm run test:e2e # Playwright browser suite (requires installed browsers)
|
|
69
113
|
```
|
|
70
114
|
|
|
115
|
+
- `tests/integration/*.test.mjs` boots the server in-process (from `dist/`) and
|
|
116
|
+
drives it over HTTP — covering the session API, `/r/<id>` routing, legacy
|
|
117
|
+
redirects, repo registration/idempotency/slug-disambiguation, the session
|
|
118
|
+
page, and the unknown-repo fallback. This is the default `npm test` and runs
|
|
119
|
+
anywhere.
|
|
120
|
+
- The Playwright suite (`tests/frontend.spec.js`) covers the rich browsing/review
|
|
121
|
+
UI in a real browser; it needs `npx playwright install` and is run as
|
|
122
|
+
`test:e2e`. New multi-repo flows are exercised via the Chrome DevTools MCP
|
|
123
|
+
(see `docs/multi-repo-session.md`).
|
|
124
|
+
|
|
71
125
|
## Release checklist
|
|
72
126
|
|
|
73
127
|
Before publishing to npm, run a quick smoke test from a clean install context (this catches issues where the server accidentally serves assets from the *repo* instead of the installed package):
|
|
@@ -83,5 +137,5 @@ npm init -y
|
|
|
83
137
|
npm install /path/to/repoview-*.tgz
|
|
84
138
|
|
|
85
139
|
# serve any repo and verify vendor assets load (no ENOENT)
|
|
86
|
-
node ./node_modules/.bin/repoview --repo /path/to/repo --port
|
|
140
|
+
node ./node_modules/.bin/repoview --repo /path/to/repo --port 7376
|
|
87
141
|
```
|
package/README.md
CHANGED
|
@@ -14,30 +14,61 @@ Not affiliated with GitHub.
|
|
|
14
14
|
- Live reload when files change (SSE with polling fallback)
|
|
15
15
|
- Broken internal link discovery for docs (`/broken-links`)
|
|
16
16
|
- Respects `.gitignore` by default (toggleable)
|
|
17
|
+
- Shared sessions (like tmux): run `repoview` in several repos on the same port and browse them all from one server, switching between them in the UI
|
|
17
18
|
|
|
18
19
|
## Quick start (from source)
|
|
19
20
|
|
|
20
21
|
```bash
|
|
21
22
|
npm install
|
|
22
|
-
npm start -- --repo /path/to/your/repo --port
|
|
23
|
+
npm start -- --repo /path/to/your/repo --port 7376
|
|
23
24
|
```
|
|
24
25
|
|
|
25
|
-
Then open `http://localhost:
|
|
26
|
+
Then open `http://localhost:7376`.
|
|
26
27
|
|
|
27
28
|
## Quick start (npx)
|
|
28
29
|
|
|
29
30
|
From anywhere:
|
|
30
31
|
|
|
31
32
|
```bash
|
|
32
|
-
npx repoview --repo /path/to/your/repo --port
|
|
33
|
+
npx repoview --repo /path/to/your/repo --port 7376
|
|
33
34
|
```
|
|
34
35
|
|
|
35
36
|
By default, `repoview` binds to `0.0.0.0` (LAN-accessible). For localhost-only:
|
|
36
37
|
|
|
37
38
|
```bash
|
|
38
|
-
npx repoview --repo /path/to/your/repo --host 127.0.0.1 --port
|
|
39
|
+
npx repoview --repo /path/to/your/repo --host 127.0.0.1 --port 7376
|
|
39
40
|
```
|
|
40
41
|
|
|
42
|
+
## Shared sessions (multi-repo)
|
|
43
|
+
|
|
44
|
+
The first `repoview` on a port starts a server; later runs on the **same port**
|
|
45
|
+
join it instead of failing — they register their repo and exit immediately
|
|
46
|
+
(no need to remember a port per repo):
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
cd ~/work/api && repoview # starts the session on :7376
|
|
50
|
+
cd ~/work/web && repoview # joins :7376, registers, exits
|
|
51
|
+
cd ~/work/docs && repoview # joins :7376, registers, exits
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Each repo is served at `/r/<id>/…`; switch between them from the dropdown in the
|
|
55
|
+
top bar, or open **Manage repos…** (the `/session` page) to add/remove repos
|
|
56
|
+
from the browser. Manage the session from the CLI too:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
repoview ls # list repos in the session
|
|
60
|
+
repoview rm <id|path> # unregister a repo
|
|
61
|
+
repoview stop # shut the session down
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Use `--port` to run independent sessions side by side.
|
|
65
|
+
|
|
66
|
+
> **Note:** by default the session binds `0.0.0.0`, so **every repo you add is
|
|
67
|
+
> browsable by anyone on the network** (you'll see a warning at startup). Session
|
|
68
|
+
> control endpoints (register / remove / stop) are restricted to localhost, and
|
|
69
|
+
> the `/session` page is read-only for remote viewers. Use `--host 127.0.0.1` to
|
|
70
|
+
> keep a session fully local.
|
|
71
|
+
|
|
41
72
|
## Why
|
|
42
73
|
|
|
43
74
|
- Keep GitHub as a remote, not your developer portal.
|
|
@@ -47,7 +78,7 @@ npx repoview --repo /path/to/your/repo --host 127.0.0.1 --port 3000
|
|
|
47
78
|
## Usage
|
|
48
79
|
|
|
49
80
|
```bash
|
|
50
|
-
npm start -- [--repo /path/to/repo] [--host 0.0.0.0] [--port
|
|
81
|
+
npm start -- [--repo /path/to/repo] [--host 0.0.0.0] [--port 7376] [--no-watch]
|
|
51
82
|
```
|
|
52
83
|
|
|
53
84
|
Common flags:
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import express from "express";
|
|
2
|
+
import { isLoopbackAddress } from "./net.js";
|
|
3
|
+
/** Restrict a mutating control endpoint to loopback clients. */
|
|
4
|
+
function requireLoopback(req, res, next) {
|
|
5
|
+
if (isLoopbackAddress(req.socket.remoteAddress))
|
|
6
|
+
return next();
|
|
7
|
+
res.status(403).json({ error: "Control endpoints are restricted to localhost" });
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* The session control API. `GET /api/session` doubles as the join-handshake
|
|
11
|
+
* signature; mutating routes (register/unregister/shutdown) are loopback-only.
|
|
12
|
+
*/
|
|
13
|
+
export function createApiRouter(session, { version, onShutdown }) {
|
|
14
|
+
const router = express.Router();
|
|
15
|
+
router.get("/session", (req, res) => {
|
|
16
|
+
res.json({ app: "repoview", version, repos: session.listRepos() });
|
|
17
|
+
});
|
|
18
|
+
router.get("/repos", (req, res) => {
|
|
19
|
+
res.json({ repos: session.listRepos() });
|
|
20
|
+
});
|
|
21
|
+
router.post("/repos", requireLoopback, express.json(), async (req, res) => {
|
|
22
|
+
try {
|
|
23
|
+
const repoRoot = req.body?.path;
|
|
24
|
+
if (!repoRoot || typeof repoRoot !== "string") {
|
|
25
|
+
return res.status(400).json({ error: "path is required" });
|
|
26
|
+
}
|
|
27
|
+
const watch = req.body?.watch !== false;
|
|
28
|
+
const ctx = await session.addRepo({ repoRoot, watch });
|
|
29
|
+
res.status(201).json({
|
|
30
|
+
id: ctx.id,
|
|
31
|
+
name: ctx.repoName,
|
|
32
|
+
path: ctx.repoRootReal,
|
|
33
|
+
url: `/r/${ctx.id}/tree/`,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
catch (e) {
|
|
37
|
+
res.status(500).json({ error: e.message });
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
router.delete("/repos/:id", requireLoopback, async (req, res) => {
|
|
41
|
+
try {
|
|
42
|
+
const removed = await session.removeRepo(req.params.id);
|
|
43
|
+
if (!removed)
|
|
44
|
+
return res.status(404).json({ error: "Repo not found" });
|
|
45
|
+
res.json({ ok: true, id: removed.id });
|
|
46
|
+
}
|
|
47
|
+
catch (e) {
|
|
48
|
+
res.status(500).json({ error: e.message });
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
router.post("/shutdown", requireLoopback, (req, res) => {
|
|
52
|
+
res.json({ ok: true });
|
|
53
|
+
// Defer so the response flushes before the process tears down.
|
|
54
|
+
setTimeout(onShutdown, 50);
|
|
55
|
+
});
|
|
56
|
+
return router;
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=api.js.map
|
package/dist/api.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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"}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import process from "node:process";
|
|
4
|
+
import { startServer } from "./server.js";
|
|
5
|
+
import { handleReviewCommand } from "./review-cli.js";
|
|
6
|
+
function printHelp() {
|
|
7
|
+
// Keep this in sync with README.md
|
|
8
|
+
process.stdout.write(`repoview
|
|
9
|
+
|
|
10
|
+
Serve local Git repositories as a GitHub-like website. Multiple invocations on
|
|
11
|
+
the same port join one shared session (like tmux) — the first run starts the
|
|
12
|
+
daemon, later runs register their repo and exit.
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
npx repoview [--repo /path/to/repo] [--host 0.0.0.0] [--port 7376] [--no-watch]
|
|
16
|
+
repoview [--repo /path/to/repo] [--host 0.0.0.0] [--port 7376] [--no-watch]
|
|
17
|
+
|
|
18
|
+
Options:
|
|
19
|
+
--repo <path> Repository root (default: REPO_ROOT or current dir)
|
|
20
|
+
--host <host> Bind address (default: 0.0.0.0)
|
|
21
|
+
--port <port> Bind/session port (default: 7376)
|
|
22
|
+
--watch Enable live reload (default)
|
|
23
|
+
--no-watch Disable live reload
|
|
24
|
+
-h, --help Show this help
|
|
25
|
+
|
|
26
|
+
Session subcommands (target the daemon on --port):
|
|
27
|
+
repoview ls List repos in the session
|
|
28
|
+
repoview rm <id|path> Unregister a repo from the session
|
|
29
|
+
repoview stop Shut the session daemon down
|
|
30
|
+
|
|
31
|
+
Review subcommands:
|
|
32
|
+
repoview review new --title "Title" Create a new review thread
|
|
33
|
+
repoview review post <id> --role agent --body "…" Post a message to a thread
|
|
34
|
+
repoview review post <id> --role agent --file f Post from file
|
|
35
|
+
repoview review read <id> Read thread messages + comments
|
|
36
|
+
repoview review list List all threads
|
|
37
|
+
|
|
38
|
+
Environment:
|
|
39
|
+
REPO_ROOT, HOST, PORT
|
|
40
|
+
`);
|
|
41
|
+
}
|
|
42
|
+
function parseArgs(argv) {
|
|
43
|
+
const args = { watch: true };
|
|
44
|
+
const rest = [];
|
|
45
|
+
for (let i = 0; i < argv.length; i++) {
|
|
46
|
+
const value = argv[i];
|
|
47
|
+
if (value === "-h" || value === "--help")
|
|
48
|
+
args.help = true;
|
|
49
|
+
if (value === "--watch")
|
|
50
|
+
args.watch = true;
|
|
51
|
+
else if (value === "--no-watch")
|
|
52
|
+
args.watch = false;
|
|
53
|
+
else if (value === "--repo")
|
|
54
|
+
args.repo = argv[++i];
|
|
55
|
+
else if (value === "--port")
|
|
56
|
+
args.port = Number(argv[++i]);
|
|
57
|
+
else if (value === "--host")
|
|
58
|
+
args.host = argv[++i];
|
|
59
|
+
else
|
|
60
|
+
rest.push(value);
|
|
61
|
+
}
|
|
62
|
+
return { ...args, rest };
|
|
63
|
+
}
|
|
64
|
+
/** Address to connect to when joining a local daemon (0.0.0.0 isn't routable). */
|
|
65
|
+
function connectHost(host) {
|
|
66
|
+
if (host === "0.0.0.0" || host === "::" || host === "")
|
|
67
|
+
return "127.0.0.1";
|
|
68
|
+
return host;
|
|
69
|
+
}
|
|
70
|
+
async function fetchJson(url, init) {
|
|
71
|
+
try {
|
|
72
|
+
const res = await fetch(url, init);
|
|
73
|
+
if (!res.ok) {
|
|
74
|
+
const body = (await res.json().catch(() => ({})));
|
|
75
|
+
throw new Error(body.error || `HTTP ${res.status}`);
|
|
76
|
+
}
|
|
77
|
+
return await res.json();
|
|
78
|
+
}
|
|
79
|
+
catch (e) {
|
|
80
|
+
if (e instanceof Error && /HTTP \d|error/i.test(e.message) && !/fetch failed|ECONNREFUSED/i.test(e.message)) {
|
|
81
|
+
throw e;
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
async function probeSession(base) {
|
|
87
|
+
return (await fetchJson(`${base}/api/session`));
|
|
88
|
+
}
|
|
89
|
+
async function registerRepo(base, repoRoot, watch) {
|
|
90
|
+
const result = await fetchJson(`${base}/api/repos`, {
|
|
91
|
+
method: "POST",
|
|
92
|
+
headers: { "Content-Type": "application/json" },
|
|
93
|
+
body: JSON.stringify({ path: repoRoot, watch }),
|
|
94
|
+
});
|
|
95
|
+
if (!result)
|
|
96
|
+
throw new Error("Failed to register repo with the session");
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
function printRepoTable(repos, port, host) {
|
|
100
|
+
if (!repos.length) {
|
|
101
|
+
process.stdout.write("(no repos registered)\n");
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const open = connectHost(host);
|
|
105
|
+
for (const r of repos) {
|
|
106
|
+
process.stdout.write(`${r.id.padEnd(20)} ${(r.branch || "no-git").padEnd(16)} ${r.path}\n` +
|
|
107
|
+
`${" ".repeat(20)} http://${open}:${port}/r/${r.id}/tree/\n`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
async function runSubcommand(sub, args, base, port, host) {
|
|
111
|
+
if (sub === "ls") {
|
|
112
|
+
const info = await probeSession(base);
|
|
113
|
+
if (!info) {
|
|
114
|
+
process.stderr.write(`No repoview session on ${base}\n`);
|
|
115
|
+
return 1;
|
|
116
|
+
}
|
|
117
|
+
printRepoTable(info.repos || [], port, host);
|
|
118
|
+
return 0;
|
|
119
|
+
}
|
|
120
|
+
if (sub === "stop") {
|
|
121
|
+
const info = await probeSession(base);
|
|
122
|
+
if (!info) {
|
|
123
|
+
process.stderr.write(`No repoview session on ${base}\n`);
|
|
124
|
+
return 1;
|
|
125
|
+
}
|
|
126
|
+
await fetchJson(`${base}/api/shutdown`, { method: "POST" });
|
|
127
|
+
process.stdout.write(`Stopped session on port ${port}\n`);
|
|
128
|
+
return 0;
|
|
129
|
+
}
|
|
130
|
+
if (sub === "rm") {
|
|
131
|
+
const target = args.rest[1];
|
|
132
|
+
if (!target) {
|
|
133
|
+
process.stderr.write("Usage: repoview rm <id|path>\n");
|
|
134
|
+
return 1;
|
|
135
|
+
}
|
|
136
|
+
const info = await probeSession(base);
|
|
137
|
+
if (!info) {
|
|
138
|
+
process.stderr.write(`No repoview session on ${base}\n`);
|
|
139
|
+
return 1;
|
|
140
|
+
}
|
|
141
|
+
const repos = info.repos || [];
|
|
142
|
+
let real = target;
|
|
143
|
+
try {
|
|
144
|
+
real = path.resolve(target);
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
// keep as-is
|
|
148
|
+
}
|
|
149
|
+
const match = repos.find((r) => r.id === target || r.path === real || r.path === target);
|
|
150
|
+
if (!match) {
|
|
151
|
+
process.stderr.write(`Repo not found in session: ${target}\n`);
|
|
152
|
+
return 1;
|
|
153
|
+
}
|
|
154
|
+
const res = await fetchJson(`${base}/api/repos/${encodeURIComponent(match.id)}`, {
|
|
155
|
+
method: "DELETE",
|
|
156
|
+
});
|
|
157
|
+
if (!res) {
|
|
158
|
+
process.stderr.write(`Failed to remove ${match.id}\n`);
|
|
159
|
+
return 1;
|
|
160
|
+
}
|
|
161
|
+
process.stdout.write(`Removed ${match.id}\n`);
|
|
162
|
+
return 0;
|
|
163
|
+
}
|
|
164
|
+
process.stderr.write(`Unknown command: ${sub}\n`);
|
|
165
|
+
printHelp();
|
|
166
|
+
return 1;
|
|
167
|
+
}
|
|
168
|
+
const parsed = parseArgs(process.argv.slice(2));
|
|
169
|
+
const { repo, port: portArg, host: hostArg, watch, help } = parsed;
|
|
170
|
+
if (help) {
|
|
171
|
+
printHelp();
|
|
172
|
+
process.exit(0);
|
|
173
|
+
}
|
|
174
|
+
// Review subcommand (pure filesystem, no daemon).
|
|
175
|
+
if (parsed.rest[0] === "review") {
|
|
176
|
+
const repoRootForReview = repo ?? process.env.REPO_ROOT ?? process.cwd();
|
|
177
|
+
await handleReviewCommand(parsed.rest.slice(1), repoRootForReview);
|
|
178
|
+
process.exit(0);
|
|
179
|
+
}
|
|
180
|
+
if (portArg != null && !Number.isFinite(portArg)) {
|
|
181
|
+
process.stderr.write("Invalid --port value\n");
|
|
182
|
+
process.exit(2);
|
|
183
|
+
}
|
|
184
|
+
const port = portArg || Number(process.env.PORT) || 7376;
|
|
185
|
+
const host = hostArg || process.env.HOST || "0.0.0.0";
|
|
186
|
+
const base = `http://${connectHost(host)}:${port}`;
|
|
187
|
+
// Session-management subcommands target the running daemon.
|
|
188
|
+
if (["ls", "rm", "stop"].includes(parsed.rest[0])) {
|
|
189
|
+
const code = await runSubcommand(parsed.rest[0], parsed, base, port, host);
|
|
190
|
+
process.exit(code);
|
|
191
|
+
}
|
|
192
|
+
const repoRoot = repo ?? process.env.REPO_ROOT ?? process.cwd();
|
|
193
|
+
const repoRootAbs = path.resolve(repoRoot);
|
|
194
|
+
async function joinExisting() {
|
|
195
|
+
const info = await probeSession(base);
|
|
196
|
+
if (!info)
|
|
197
|
+
return false;
|
|
198
|
+
if (info.app !== "repoview") {
|
|
199
|
+
process.stderr.write(`Port ${port} is in use by something that isn't repoview. Use --port to pick another.\n`);
|
|
200
|
+
process.exit(1);
|
|
201
|
+
}
|
|
202
|
+
const registered = await registerRepo(base, repoRootAbs, watch);
|
|
203
|
+
process.stdout.write(`Joined repoview session on port ${port}\n` +
|
|
204
|
+
`${registered.name} → http://${connectHost(host)}:${port}${registered.url}\n`);
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
// Try to join an existing session first; otherwise become the daemon.
|
|
208
|
+
if (await joinExisting()) {
|
|
209
|
+
process.exit(0);
|
|
210
|
+
}
|
|
211
|
+
try {
|
|
212
|
+
await startServer({ repoRoot: repoRootAbs, port, host, watch });
|
|
213
|
+
}
|
|
214
|
+
catch (e) {
|
|
215
|
+
if (e.code === "EADDRINUSE") {
|
|
216
|
+
// Lost a startup race — another daemon just bound the port. Join it.
|
|
217
|
+
if (await joinExisting())
|
|
218
|
+
process.exit(0);
|
|
219
|
+
process.stderr.write(`Port ${port} is already in use.\n`);
|
|
220
|
+
process.exit(1);
|
|
221
|
+
}
|
|
222
|
+
throw e;
|
|
223
|
+
}
|
|
224
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,OAAO,MAAM,cAAc,CAAC;AACnC,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAGtD,SAAS,SAAS;IAChB,mCAAmC;IACnC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgCtB,CAAC,CAAC;AACH,CAAC;AAWD,SAAS,SAAS,CAAC,IAAc;IAC/B,MAAM,IAAI,GAA6B,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;IACvD,MAAM,IAAI,GAAa,EAAE,CAAC;IAC1B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACrC,MAAM,KAAK,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;QACtB,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,QAAQ;YAAE,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QAC3D,IAAI,KAAK,KAAK,SAAS;YAAE,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;aACtC,IAAI,KAAK,KAAK,YAAY;YAAE,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;aAC/C,IAAI,KAAK,KAAK,QAAQ;YAAE,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC;aAC9C,IAAI,KAAK,KAAK,QAAQ;YAAE,IAAI,CAAC,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;aACtD,IAAI,KAAK,KAAK,QAAQ;YAAE,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC;;YAC9C,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACxB,CAAC;IACD,OAAO,EAAE,GAAG,IAAI,EAAE,IAAI,EAAE,CAAC;AAC3B,CAAC;AAED,kFAAkF;AAClF,SAAS,WAAW,CAAC,IAAY;IAC/B,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,KAAK,EAAE;QAAE,OAAO,WAAW,CAAC;IAC3E,OAAO,IAAI,CAAC;AACd,CAAC;AAQD,KAAK,UAAU,SAAS,CAAC,GAAW,EAAE,IAAkB;IACtD,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QACnC,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACZ,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAuB,CAAC;YACxE,MAAM,IAAI,KAAK,CAAC,IAAI,CAAC,KAAK,IAAI,QAAQ,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC;QACtD,CAAC;QACD,OAAO,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC;IAC1B,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,IAAI,CAAC,YAAY,KAAK,IAAI,gBAAgB,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,4BAA4B,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;YAC5G,MAAM,CAAC,CAAC;QACV,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,KAAK,UAAU,YAAY,CAAC,IAAY;IACtC,OAAO,CAAC,MAAM,SAAS,CAAC,GAAG,IAAI,cAAc,CAAC,CAAuB,CAAC;AACxE,CAAC;AAED,KAAK,UAAU,YAAY,CACzB,IAAY,EACZ,QAAgB,EAChB,KAAc;IAEd,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,GAAG,IAAI,YAAY,EAAE;QAClD,MAAM,EAAE,MAAM;QACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;QAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;KAChD,CAAC,CAAC;IACH,IAAI,CAAC,MAAM;QAAE,MAAM,IAAI,KAAK,CAAC,0CAA0C,CAAC,CAAC;IACzE,OAAO,MAAmD,CAAC;AAC7D,CAAC;AAED,SAAS,cAAc,CAAC,KAAoB,EAAE,IAAY,EAAE,IAAY;IACtE,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC;QAClB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,yBAAyB,CAAC,CAAC;QAChD,OAAO;IACT,CAAC;IACD,MAAM,IAAI,GAAG,WAAW,CAAC,IAAI,CAAC,CAAC;IAC/B,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;QACtB,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,GAAG,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,IAAI,QAAQ,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,IAAI,IAAI;YACnE,GAAG,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,WAAW,IAAI,IAAI,IAAI,MAAM,CAAC,CAAC,EAAE,UAAU,CAC/D,CAAC;IACJ,CAAC;AACH,CAAC;AAED,KAAK,UAAU,aAAa,CAC1B,GAAW,EACX,IAAgB,EAChB,IAAY,EACZ,IAAY,EACZ,IAAY;IAEZ,IAAI,GAAG,KAAK,IAAI,EAAE,CAAC;QACjB,MAAM,IAAI,GAAG,MAAM,YAAY,CAAC,IAAI,CAAC,CAAC;QACtC,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,0BAA0B,IAAI,IAAI,CAAC,CAAC;YACzD,OAAO,CAAC,CAAC;QACX,CAAC;QACD,cAAc,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;QAC7C,OAAO,CAAC,CAAC;IACX,CAAC;IAED,IAAI,GAAG,KAAK,MAAM,EAAE,CAAC;QACnB,MAAM,IAAI,GAAG,MAAM,YAAY,CAAC,IAAI,CAAC,CAAC;QACtC,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,0BAA0B,IAAI,IAAI,CAAC,CAAC;YACzD,OAAO,CAAC,CAAC;QACX,CAAC;QACD,MAAM,SAAS,CAAC,GAAG,IAAI,eAAe,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;QAC5D,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,2BAA2B,IAAI,IAAI,CAAC,CAAC;QAC1D,OAAO,CAAC,CAAC;IACX,CAAC;IAED,IAAI,GAAG,KAAK,IAAI,EAAE,CAAC;QACjB,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAC5B,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,gCAAgC,CAAC,CAAC;YACvD,OAAO,CAAC,CAAC;QACX,CAAC;QACD,MAAM,IAAI,GAAG,MAAM,YAAY,CAAC,IAAI,CAAC,CAAC;QACtC,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,0BAA0B,IAAI,IAAI,CAAC,CAAC;YACzD,OAAO,CAAC,CAAC;QACX,CAAC;QACD,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC;QAC/B,IAAI,IAAI,GAAG,MAAM,CAAC;QAClB,IAAI,CAAC;YACH,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QAC9B,CAAC;QAAC,MAAM,CAAC;YACP,aAAa;QACf,CAAC;QACD,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,MAAM,IAAI,CAAC,CAAC,IAAI,KAAK,IAAI,IAAI,CAAC,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC;QACzF,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,8BAA8B,MAAM,IAAI,CAAC,CAAC;YAC/D,OAAO,CAAC,CAAC;QACX,CAAC;QACD,MAAM,GAAG,GAAG,MAAM,SAAS,CAAC,GAAG,IAAI,cAAc,kBAAkB,CAAC,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE;YAC/E,MAAM,EAAE,QAAQ;SACjB,CAAC,CAAC;QACH,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,oBAAoB,KAAK,CAAC,EAAE,IAAI,CAAC,CAAC;YACvD,OAAO,CAAC,CAAC;QACX,CAAC;QACD,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,WAAW,KAAK,CAAC,EAAE,IAAI,CAAC,CAAC;QAC9C,OAAO,CAAC,CAAC;IACX,CAAC;IAED,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,oBAAoB,GAAG,IAAI,CAAC,CAAC;IAClD,SAAS,EAAE,CAAC;IACZ,OAAO,CAAC,CAAC;AACX,CAAC;AAED,MAAM,MAAM,GAAG,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;AAChD,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,MAAM,CAAC;AAEnE,IAAI,IAAI,EAAE,CAAC;IACT,SAAS,EAAE,CAAC;IACZ,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAED,kDAAkD;AAClD,IAAI,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,QAAQ,EAAE,CAAC;IAChC,MAAM,iBAAiB,GAAG,IAAI,IAAI,OAAO,CAAC,GAAG,CAAC,SAAS,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC;IACzE,MAAM,mBAAmB,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,iBAAiB,CAAC,CAAC;IACnE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAED,IAAI,OAAO,IAAI,IAAI,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;IACjD,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,wBAAwB,CAAC,CAAC;IAC/C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAED,MAAM,IAAI,GAAG,OAAO,IAAI,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC;AACzD,MAAM,IAAI,GAAG,OAAO,IAAI,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,SAAS,CAAC;AACtD,MAAM,IAAI,GAAG,UAAU,WAAW,CAAC,IAAI,CAAC,IAAI,IAAI,EAAE,CAAC;AAEnD,4DAA4D;AAC5D,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IAClD,MAAM,IAAI,GAAG,MAAM,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;IAC3E,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACrB,CAAC;AAED,MAAM,QAAQ,GAAG,IAAI,IAAI,OAAO,CAAC,GAAG,CAAC,SAAS,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC;AAChE,MAAM,WAAW,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;AAE3C,KAAK,UAAU,YAAY;IACzB,MAAM,IAAI,GAAG,MAAM,YAAY,CAAC,IAAI,CAAC,CAAC;IACtC,IAAI,CAAC,IAAI;QAAE,OAAO,KAAK,CAAC;IACxB,IAAI,IAAI,CAAC,GAAG,KAAK,UAAU,EAAE,CAAC;QAC5B,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,QAAQ,IAAI,4EAA4E,CACzF,CAAC;QACF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IACD,MAAM,UAAU,GAAG,MAAM,YAAY,CAAC,IAAI,EAAE,WAAW,EAAE,KAAK,CAAC,CAAC;IAChE,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,mCAAmC,IAAI,IAAI;QACzC,GAAG,UAAU,CAAC,IAAI,aAAa,WAAW,CAAC,IAAI,CAAC,IAAI,IAAI,GAAG,UAAU,CAAC,GAAG,IAAI,CAChF,CAAC;IACF,OAAO,IAAI,CAAC;AACd,CAAC;AAED,sEAAsE;AACtE,IAAI,MAAM,YAAY,EAAE,EAAE,CAAC;IACzB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAED,IAAI,CAAC;IACH,MAAM,WAAW,CAAC,EAAE,QAAQ,EAAE,WAAW,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;AAClE,CAAC;AAAC,OAAO,CAAC,EAAE,CAAC;IACX,IAAK,CAA2B,CAAC,IAAI,KAAK,YAAY,EAAE,CAAC;QACvD,qEAAqE;QACrE,IAAI,MAAM,YAAY,EAAE;YAAE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAC1C,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,IAAI,uBAAuB,CAAC,CAAC;QAC1D,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IACD,MAAM,CAAC,CAAC;AACV,CAAC"}
|
package/dist/csv.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export function parseCsv(text, delimiter = ",") {
|
|
2
|
+
const rows = [];
|
|
3
|
+
let current = [];
|
|
4
|
+
let cell = "";
|
|
5
|
+
let inQuotes = false;
|
|
6
|
+
for (let i = 0; i < text.length; i++) {
|
|
7
|
+
const ch = text[i];
|
|
8
|
+
if (inQuotes) {
|
|
9
|
+
if (ch === '"' && text[i + 1] === '"') {
|
|
10
|
+
cell += '"';
|
|
11
|
+
i++;
|
|
12
|
+
}
|
|
13
|
+
else if (ch === '"') {
|
|
14
|
+
inQuotes = false;
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
cell += ch;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
if (ch === '"') {
|
|
22
|
+
inQuotes = true;
|
|
23
|
+
}
|
|
24
|
+
else if (ch === delimiter) {
|
|
25
|
+
current.push(cell);
|
|
26
|
+
cell = "";
|
|
27
|
+
}
|
|
28
|
+
else if (ch === "\n" || (ch === "\r" && text[i + 1] === "\n")) {
|
|
29
|
+
if (ch === "\r")
|
|
30
|
+
i++;
|
|
31
|
+
current.push(cell);
|
|
32
|
+
rows.push(current);
|
|
33
|
+
current = [];
|
|
34
|
+
cell = "";
|
|
35
|
+
}
|
|
36
|
+
else if (ch === "\r") {
|
|
37
|
+
current.push(cell);
|
|
38
|
+
rows.push(current);
|
|
39
|
+
current = [];
|
|
40
|
+
cell = "";
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
cell += ch;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (cell || current.length) {
|
|
48
|
+
current.push(cell);
|
|
49
|
+
rows.push(current);
|
|
50
|
+
}
|
|
51
|
+
return rows;
|
|
52
|
+
}
|
|
53
|
+
export function renderCsvTable(rows, escFn) {
|
|
54
|
+
if (!rows.length)
|
|
55
|
+
return "<p>Empty file</p>";
|
|
56
|
+
const header = rows[0];
|
|
57
|
+
const body = rows.slice(1);
|
|
58
|
+
const ths = header.map((h) => `<th>${escFn(h)}</th>`).join("");
|
|
59
|
+
const trs = body
|
|
60
|
+
.map((row) => `<tr>${row.map((c) => `<td>${escFn(c)}</td>`).join("")}</tr>`)
|
|
61
|
+
.join("\n");
|
|
62
|
+
return `<div class="csv-table-wrap"><table class="csv-table"><thead><tr>${ths}</tr></thead><tbody>${trs}</tbody></table></div>`;
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=csv.js.map
|
package/dist/csv.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"csv.js","sourceRoot":"","sources":["../src/csv.ts"],"names":[],"mappings":"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"}
|