@nickmeriano/task 0.11.0 → 0.12.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.
Files changed (73) hide show
  1. package/README.md +175 -1
  2. package/dist/board/github.d.ts +68 -0
  3. package/dist/board/github.d.ts.map +1 -0
  4. package/dist/board/github.js +112 -0
  5. package/dist/board/github.js.map +1 -0
  6. package/dist/board/handler.d.ts +50 -0
  7. package/dist/board/handler.d.ts.map +1 -0
  8. package/dist/board/handler.js +183 -0
  9. package/dist/board/handler.js.map +1 -0
  10. package/dist/board/handler.test.d.ts +11 -0
  11. package/dist/board/handler.test.d.ts.map +1 -0
  12. package/dist/board/handler.test.js +229 -0
  13. package/dist/board/handler.test.js.map +1 -0
  14. package/dist/board/pages-function.d.ts +23 -0
  15. package/dist/board/pages-function.d.ts.map +1 -0
  16. package/dist/board/pages-function.js +34 -0
  17. package/dist/board/pages-function.js.map +1 -0
  18. package/dist/board/source.d.ts +118 -0
  19. package/dist/board/source.d.ts.map +1 -0
  20. package/dist/board/source.js +333 -0
  21. package/dist/board/source.js.map +1 -0
  22. package/dist/board/source.test.d.ts +12 -0
  23. package/dist/board/source.test.d.ts.map +1 -0
  24. package/dist/board/source.test.js +165 -0
  25. package/dist/board/source.test.js.map +1 -0
  26. package/dist/board/tar.d.ts +28 -0
  27. package/dist/board/tar.d.ts.map +1 -0
  28. package/dist/board/tar.js +188 -0
  29. package/dist/board/tar.js.map +1 -0
  30. package/dist/board/tar.test.d.ts +9 -0
  31. package/dist/board/tar.test.d.ts.map +1 -0
  32. package/dist/board/tar.test.js +110 -0
  33. package/dist/board/tar.test.js.map +1 -0
  34. package/dist/cli.js +37 -2
  35. package/dist/cli.js.map +1 -1
  36. package/dist/export.d.ts +31 -0
  37. package/dist/export.d.ts.map +1 -1
  38. package/dist/export.js +61 -1
  39. package/dist/export.js.map +1 -1
  40. package/dist/export.test.js +80 -1
  41. package/dist/export.test.js.map +1 -1
  42. package/dist/functions/board.js +709 -0
  43. package/dist/git-serve.d.ts +14 -1
  44. package/dist/git-serve.d.ts.map +1 -1
  45. package/dist/git-serve.js +30 -2
  46. package/dist/git-serve.js.map +1 -1
  47. package/dist/git-serve.test.d.ts +1 -0
  48. package/dist/git-serve.test.d.ts.map +1 -1
  49. package/dist/git-serve.test.js +36 -1
  50. package/dist/git-serve.test.js.map +1 -1
  51. package/dist/index.d.ts +2 -0
  52. package/dist/index.d.ts.map +1 -1
  53. package/dist/index.js +6 -0
  54. package/dist/index.js.map +1 -1
  55. package/package.json +3 -3
  56. package/skill/SKILL.md +10 -3
  57. package/src/board/github.ts +151 -0
  58. package/src/board/handler.test.ts +276 -0
  59. package/src/board/handler.ts +228 -0
  60. package/src/board/pages-function.ts +42 -0
  61. package/src/board/source.test.ts +203 -0
  62. package/src/board/source.ts +422 -0
  63. package/src/board/tar.test.ts +128 -0
  64. package/src/board/tar.ts +199 -0
  65. package/src/cli.ts +36 -2
  66. package/src/export.test.ts +108 -1
  67. package/src/export.ts +79 -1
  68. package/src/git-serve.test.ts +41 -1
  69. package/src/git-serve.ts +30 -2
  70. package/src/index.ts +10 -0
  71. package/ui/dist/assets/index-B8M_DaOt.js +229 -0
  72. package/ui/dist/index.html +1 -1
  73. package/ui/dist/assets/index-mJmm4sWq.js +0 -229
package/README.md CHANGED
@@ -376,7 +376,9 @@ jobs:
376
376
  `--force`). Hosted anywhere but a domain root, pass the mount point —
377
377
  `task export --base /board/` for `example.com/board/` — so the page knows
378
378
  where its assets and snapshot live. Point the host's SPA fallback at
379
- `index.html` so deep links resolve.
379
+ `index.html` so deep links resolve. It also writes a live upgrade for
380
+ Cloudflare Pages that stays dormant until you add a secret — see "Live data
381
+ for an exported board" below; `--no-functions` leaves it out.
380
382
  - `task serve` is the one command that also looks *down*: run it at a monorepo
381
383
  root and it serves every nested board from one server, with a board switcher
382
384
  in the header. Run it inside a package and you get just that board. Every
@@ -386,6 +388,178 @@ jobs:
386
388
  files (the database stays on disk as an ignored backup) — newer versions
387
389
  read only the text format.
388
390
 
391
+ ## Deploy the board
392
+
393
+ `task export` writes the board out as plain files, so putting it on the web is
394
+ a CI job and nothing else — no server, no database, no runtime. Cloudflare
395
+ Pages is the path this section walks (per-branch preview URLs come free, and
396
+ the later live upgrade has somewhere to land), but the output is an ordinary
397
+ directory: see "Any static host" below.
398
+
399
+ ```yaml
400
+ # .github/workflows/board.yml
401
+ name: Board
402
+ on: push
403
+ permissions:
404
+ contents: read
405
+ jobs:
406
+ deploy:
407
+ runs-on: ubuntu-latest
408
+ steps:
409
+ - uses: actions/checkout@v4
410
+ - uses: actions/setup-node@v4
411
+ with:
412
+ node-version: 22
413
+ - run: npx @nickmeriano/task export --out board
414
+ # The board's routes are real paths (/task/x7k4m), so the host has to
415
+ # answer them with index.html instead of a 404.
416
+ - run: echo '/* /index.html 200' > board/_redirects
417
+ - uses: cloudflare/wrangler-action@v4
418
+ with:
419
+ apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
420
+ accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
421
+ command: pages deploy board --project-name=<your-project> --branch=${{ github.ref_name }}
422
+ ```
423
+
424
+ Three things the workflow can't do for you, once:
425
+
426
+ 1. Create the Pages project (Direct Upload) and use its name for
427
+ `--project-name`.
428
+ 2. Add `CLOUDFLARE_API_TOKEN` as a repository secret — a token with
429
+ **Pages: Edit** on that account, nothing more.
430
+ 3. Add `CLOUDFLARE_ACCOUNT_ID` as a repository secret.
431
+
432
+ Notes on the shape:
433
+
434
+ - **`on: push`, every branch, is the point.** A claim is a push, so the
435
+ deployed board tracks claims at CI latency with no extra machinery — and
436
+ `--branch=${{ github.ref_name }}` hands each branch its own Pages preview
437
+ URL, so "the board as this PR would leave it" is a link rather than a
438
+ checkout.
439
+ - **The `_redirects` line is belt and braces here, and required elsewhere.**
440
+ `task export` writes `index.html`, `snapshot.json` and `assets/` — it does
441
+ not write an SPA fallback, and the board's routes are real paths
442
+ (`/task/<key>`, `/goal/<slug>`). Pages already serves `index.html` for
443
+ unmatched paths when the output has no `404.html`, so the line changes
444
+ nothing there; on a host without that default, leaving it out means every
445
+ deep link 404s while the home page looks fine.
446
+ - **`setup-node` is not decoration.** The package needs Node >= 22.13; without
447
+ the step you get the runner's default and a confusing failure.
448
+ - **No `--base`.** Pages serves at a domain root, where the default base `/` is
449
+ right.
450
+ - **The live upgrade is already in the output.** `task export` also writes a
451
+ `functions/` directory holding a Pages Function that stays dormant until
452
+ the project has a token — see "Live data for an exported board" below.
453
+ Turning it on is a secret in the Pages project, never a change to this
454
+ file.
455
+
456
+ ### Any static host
457
+
458
+ The output is a directory of static files, so anything that serves those
459
+ works. Two host-side requirements, and they're the whole list:
460
+
461
+ - **An SPA fallback to `index.html`**, however that host spells it
462
+ (`_redirects` on Pages and Netlify, a rewrite rule elsewhere). The routes are
463
+ real paths.
464
+ - **`task export --base /board/`** if you're not serving from a domain root, so
465
+ the page knows where its assets and snapshot live.
466
+
467
+ No platform matrix beyond that — there isn't one to write.
468
+
469
+ ### An exported board is as public as wherever you put it
470
+
471
+ Worth saying plainly, because the export itself has no opinion about it: a
472
+ Pages URL is public even when the repository is private. Anyone with the link
473
+ reads your board.
474
+
475
+ If that's not what you want, you have two answers. Put the deploy behind your
476
+ own perimeter (Cloudflare Access and the like) — the export doesn't care what's
477
+ in front of it. Or use `task publish`, whose entire point is the other trade:
478
+ GitHub-synced private access, where who can see the board is whoever can see
479
+ the repo.
480
+
481
+ Which is the shape of the whole picture — same board, three deployments, and
482
+ you pick by how much setup you want:
483
+
484
+ | | Where it runs | Who can see it | Setup |
485
+ | --- | --- | --- | --- |
486
+ | `task serve` | your machine | you | none |
487
+ | `task export` | any static host | whoever you let | a CI job and a host |
488
+ | `task export` + a token | Cloudflare Pages | whoever you let, live | the same, plus one secret |
489
+ | `task publish` | hosted | whoever can see the repo | one command |
490
+
491
+ ## Live data for an exported board
492
+
493
+ An export is a snapshot: as fresh as the last CI run, and only of the branch
494
+ it ran on. When that's not enough — you want the board to read the repository
495
+ *now*, or to browse a branch that nobody exported — the same deploy can
496
+ upgrade itself, and the export has already packed what that takes.
497
+
498
+ Every `task export` writes `functions/api/[[path]].js` beside the static
499
+ files: a Cloudflare Pages Function with the repository baked in (worked out
500
+ from your git remotes; `--repo owner/name` sets it when there isn't one, and
501
+ `--no-functions` leaves the file out). Pages deploys anything under
502
+ `functions/` with the rest of the directory, and other hosts ignore it. It
503
+ does nothing until the host has a token — so the workflow above has already
504
+ deployed it, dormant. To wake it, once:
505
+
506
+ 1. On GitHub, create a **fine-grained personal access token**: *Only select
507
+ repositories* → this one; *Repository permissions* → **Contents:
508
+ Read-only**, nothing else. That is the whole permission set the function
509
+ needs, and the only one you should give it.
510
+ 2. In the Pages project's settings, add it as a secret named
511
+ **`TASK_GITHUB_TOKEN`**.
512
+
513
+ That's the upgrade. The board's boot probe finds an API answering next to it
514
+ and reads from there instead of `snapshot.json`: the staleness banner is gone,
515
+ every view is the repository's current `.task/`, and the header grows the
516
+ branch switcher — `?ref=<branch>` reads any ref, so a PR's board is a link
517
+ again, the way it is on `task publish`. Until the secret is set (or if it
518
+ expires), the function answers 503, the probe reads that as "no API", and the
519
+ same files serve the snapshot as before. Nobody chooses an architecture up
520
+ front; the deploy is whichever tier it's configured for.
521
+
522
+ Still read-only, still by construction: the token can only read, and the
523
+ function refuses anything but a GET before it even looks for one.
524
+
525
+ Two things to know before you turn it on:
526
+
527
+ - **The token is a person's.** Fine-grained PATs expire (a year at most) and
528
+ belong to whoever minted them — when they leave, the board goes back to
529
+ being a snapshot until someone mints a new one. That's the trade for
530
+ needing no GitHub App of your own.
531
+ - **There is no viewer auth in this tier, and a token makes that matter
532
+ more.** An exported snapshot is as public as wherever you put it; a live
533
+ function is *the repository's current `.task/`*, served to anyone who can
534
+ reach the URL, for as long as the secret is set — including after you stop
535
+ deploying. `task export` says so in one line every time it writes the file,
536
+ because the reminder belongs next to the setup. Put the deploy behind your
537
+ own perimeter (Cloudflare Access and the like), or use `task publish`, whose
538
+ whole point is GitHub-synced access without a token of your own.
539
+
540
+ ### Any other host
541
+
542
+ The function is a three-line adapter around an exported handler. Anything
543
+ that can run a `(Request) => Promise<Response>` — a Netlify function, a Deno
544
+ or Bun server, a Worker of your own — gets the same live board:
545
+
546
+ ```ts
547
+ import { createBoardHandler } from "@nickmeriano/task"
548
+
549
+ const board = createBoardHandler({
550
+ token: process.env.TASK_GITHUB_TOKEN!, // or an async getter that mints one
551
+ repo: "owner/name",
552
+ basePath: "/api", // where you mount it: `<base>api`
553
+ })
554
+ export default { fetch: (request: Request) => board(request) }
555
+ ```
556
+
557
+ Mount it at `<base>api` — `/api` at a domain root, `/board/api` under
558
+ `task export --base /board/` — and the SPA finds it the same way. `token` may
559
+ be a function returning a promise, so an adapter that would rather mint an
560
+ installation token from its own GitHub App can, without this package knowing
561
+ anything about App auth.
562
+
389
563
  ## How it's built
390
564
 
391
565
  - **Zero dependencies.** Storage is markdown files with a tiny frontmatter
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Reading a repository from GitHub, with a bearer token and nothing else.
3
+ *
4
+ * This is the read half of the hosted board's `worker/github.ts`, moved here
5
+ * so an exported board's live proxy (`handler.ts`) and the hosted worker read
6
+ * a repository through the same four calls. What deliberately did *not* move
7
+ * with it: everything that mints or exchanges a credential — App JWTs,
8
+ * installation tokens, the viewer's OAuth flow. Those are the hosted
9
+ * product's auth surface and stay in the worker; here a token is a string
10
+ * somebody already holds (a fine-grained PAT, an installation token the
11
+ * worker minted), and this module only ever spends it on reads.
12
+ *
13
+ * Web-standard only — `fetch`, `Response`, `DecompressionStream` — so the same
14
+ * source runs under Node 22, a Cloudflare Worker, and a Pages Function.
15
+ */
16
+ import { type TarContents } from "./tar.ts";
17
+ export declare class GitHubError extends Error {
18
+ readonly status: number;
19
+ constructor(status: number, message: string);
20
+ }
21
+ export interface RepoInfo {
22
+ defaultBranch: string;
23
+ private: boolean;
24
+ }
25
+ export declare function repoInfo(token: string, owner: string, repo: string): Promise<RepoInfo>;
26
+ /**
27
+ * Branch names, for the board's branch switcher. One page of 100: a selector
28
+ * over more branches than that stops being a selector, and the switcher's
29
+ * search box only filters what's listed. Sorting is the caller's job — it
30
+ * knows the default branch; this endpoint doesn't.
31
+ */
32
+ export declare function listBranches(token: string, owner: string, repo: string): Promise<string[]>;
33
+ /** A commit's identity, so the board can say how fresh it is. */
34
+ export interface CommitInfo {
35
+ sha: string;
36
+ committedAt: string;
37
+ message: string;
38
+ }
39
+ export declare function headCommit(token: string, owner: string, repo: string, ref: string): Promise<CommitInfo>;
40
+ /**
41
+ * Did GitHub just say "that ref doesn't exist"?
42
+ *
43
+ * It says it two different ways, and the obvious one is the rarer one. Asking
44
+ * `/repos/{o}/{r}/commits/{ref}` for a branch that isn't there answers **422**
45
+ * ("No commit found for SHA: …"), not 404 — 404 is what you get when the
46
+ * repository itself is out of reach. Checking only for 404 let every dead
47
+ * `?ref=` link fall through to a generic "GitHub is unhappy — try again"
48
+ * screen: a retry prompt for something no amount of retrying will fix.
49
+ */
50
+ export declare function isMissingRef(error: unknown): boolean;
51
+ /**
52
+ * The whole repository at `sha`, in one request: every file path, plus the
53
+ * bytes of the paths `keep` selects.
54
+ *
55
+ * This replaced two earlier shapes, each broken by arithmetic. Per-file
56
+ * contents reads were one subrequest per ticket, against the 50-subrequest
57
+ * ceiling Workers get on the free plan — every cold board view of this repo
58
+ * 500'd. Batched GraphQL blob reads fixed that, but still cost one tree
59
+ * listing plus one request per 80 files, scaling with board size. A tarball
60
+ * is a single request whatever the repo holds, and it carries the path
61
+ * listing too. Streamed through gzip and a tar walk, keeping bytes only for
62
+ * `keep`-selected paths, so memory is bounded by the boards rather than the
63
+ * repository.
64
+ *
65
+ * The tarball endpoint answers with a redirect to codeload; fetch follows it.
66
+ */
67
+ export declare function fetchRepoArchive(token: string, owner: string, repo: string, sha: string, keep: (path: string) => boolean): Promise<TarContents>;
68
+ //# sourceMappingURL=github.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"github.d.ts","sourceRoot":"","sources":["../../src/board/github.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAqB,KAAK,WAAW,EAAE,MAAM,UAAU,CAAA;AAO9D,qBAAa,WAAY,SAAQ,KAAK;IACpC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;gBACX,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM;CAK5C;AAcD,MAAM,WAAW,QAAQ;IACvB,aAAa,EAAE,MAAM,CAAA;IACrB,OAAO,EAAE,OAAO,CAAA;CACjB;AAED,wBAAsB,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,CAM5F;AAED;;;;;GAKG;AACH,wBAAsB,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAMhG;AAED,iEAAiE;AACjE,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,MAAM,CAAA;IACX,WAAW,EAAE,MAAM,CAAA;IACnB,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,wBAAsB,UAAU,CAC9B,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,UAAU,CAAC,CAUrB;AAED;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAEpD;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,gBAAgB,CACpC,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,GAC9B,OAAO,CAAC,WAAW,CAAC,CAmBtB"}
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Reading a repository from GitHub, with a bearer token and nothing else.
3
+ *
4
+ * This is the read half of the hosted board's `worker/github.ts`, moved here
5
+ * so an exported board's live proxy (`handler.ts`) and the hosted worker read
6
+ * a repository through the same four calls. What deliberately did *not* move
7
+ * with it: everything that mints or exchanges a credential — App JWTs,
8
+ * installation tokens, the viewer's OAuth flow. Those are the hosted
9
+ * product's auth surface and stay in the worker; here a token is a string
10
+ * somebody already holds (a fine-grained PAT, an installation token the
11
+ * worker minted), and this module only ever spends it on reads.
12
+ *
13
+ * Web-standard only — `fetch`, `Response`, `DecompressionStream` — so the same
14
+ * source runs under Node 22, a Cloudflare Worker, and a Pages Function.
15
+ */
16
+ import { readTar, TarError } from "./tar.js";
17
+ const API = "https://api.github.com";
18
+ /** Sent on every call; GitHub rejects requests without a User-Agent. */
19
+ const UA = "@nickmeriano/task";
20
+ export class GitHubError extends Error {
21
+ status;
22
+ constructor(status, message) {
23
+ super(message);
24
+ this.name = "GitHubError";
25
+ this.status = status;
26
+ }
27
+ }
28
+ async function apiGet(token, path) {
29
+ const res = await fetch(`${API}${path}`, {
30
+ headers: {
31
+ Authorization: `Bearer ${token}`,
32
+ Accept: "application/vnd.github+json",
33
+ "User-Agent": UA,
34
+ },
35
+ });
36
+ if (!res.ok)
37
+ throw new GitHubError(res.status, `GET ${path} failed (${res.status})`);
38
+ return (await res.json());
39
+ }
40
+ export async function repoInfo(token, owner, repo) {
41
+ const data = await apiGet(token, `/repos/${owner}/${repo}`);
42
+ return { defaultBranch: data.default_branch, private: data.private };
43
+ }
44
+ /**
45
+ * Branch names, for the board's branch switcher. One page of 100: a selector
46
+ * over more branches than that stops being a selector, and the switcher's
47
+ * search box only filters what's listed. Sorting is the caller's job — it
48
+ * knows the default branch; this endpoint doesn't.
49
+ */
50
+ export async function listBranches(token, owner, repo) {
51
+ const data = await apiGet(token, `/repos/${owner}/${repo}/branches?per_page=100`);
52
+ return data.map((branch) => branch.name);
53
+ }
54
+ export async function headCommit(token, owner, repo, ref) {
55
+ const data = await apiGet(token, `/repos/${owner}/${repo}/commits/${encodeURIComponent(ref)}`);
56
+ return {
57
+ sha: data.sha,
58
+ committedAt: data.commit.committer.date,
59
+ message: data.commit.message.split("\n")[0],
60
+ };
61
+ }
62
+ /**
63
+ * Did GitHub just say "that ref doesn't exist"?
64
+ *
65
+ * It says it two different ways, and the obvious one is the rarer one. Asking
66
+ * `/repos/{o}/{r}/commits/{ref}` for a branch that isn't there answers **422**
67
+ * ("No commit found for SHA: …"), not 404 — 404 is what you get when the
68
+ * repository itself is out of reach. Checking only for 404 let every dead
69
+ * `?ref=` link fall through to a generic "GitHub is unhappy — try again"
70
+ * screen: a retry prompt for something no amount of retrying will fix.
71
+ */
72
+ export function isMissingRef(error) {
73
+ return error instanceof GitHubError && (error.status === 404 || error.status === 422);
74
+ }
75
+ /**
76
+ * The whole repository at `sha`, in one request: every file path, plus the
77
+ * bytes of the paths `keep` selects.
78
+ *
79
+ * This replaced two earlier shapes, each broken by arithmetic. Per-file
80
+ * contents reads were one subrequest per ticket, against the 50-subrequest
81
+ * ceiling Workers get on the free plan — every cold board view of this repo
82
+ * 500'd. Batched GraphQL blob reads fixed that, but still cost one tree
83
+ * listing plus one request per 80 files, scaling with board size. A tarball
84
+ * is a single request whatever the repo holds, and it carries the path
85
+ * listing too. Streamed through gzip and a tar walk, keeping bytes only for
86
+ * `keep`-selected paths, so memory is bounded by the boards rather than the
87
+ * repository.
88
+ *
89
+ * The tarball endpoint answers with a redirect to codeload; fetch follows it.
90
+ */
91
+ export async function fetchRepoArchive(token, owner, repo, sha, keep) {
92
+ const res = await fetch(`${API}/repos/${owner}/${repo}/tarball/${encodeURIComponent(sha)}`, {
93
+ headers: {
94
+ Authorization: `Bearer ${token}`,
95
+ Accept: "application/vnd.github+json",
96
+ "User-Agent": UA,
97
+ },
98
+ });
99
+ if (!res.ok || !res.body) {
100
+ throw new GitHubError(res.status, `could not read the repository archive (${res.status})`);
101
+ }
102
+ try {
103
+ return await readTar(res.body.pipeThrough(new DecompressionStream("gzip")), keep);
104
+ }
105
+ catch (error) {
106
+ if (error instanceof TarError) {
107
+ throw new GitHubError(502, `could not read the repository archive (${error.message})`);
108
+ }
109
+ throw error;
110
+ }
111
+ }
112
+ //# sourceMappingURL=github.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"github.js","sourceRoot":"","sources":["../../src/board/github.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * The live proxy: an exported board's API, as one fetch handler.
3
+ *
4
+ * `task export` ships the board as static files plus a baked `snapshot.json`.
5
+ * Drop this next to them — as a Cloudflare Pages Function, or behind any host
6
+ * that can run a `(Request) => Response` — hand it a token, and the same
7
+ * files become a live board: the SPA probes for an API before it mounts,
8
+ * finds this one answering, and never reads the snapshot at all. No token,
9
+ * no function, and the probe fails and the snapshot is what you get. The
10
+ * upgrade is the deploy, not a rebuild.
11
+ *
12
+ * The routes are `task serve`'s read routes — the same ones the snapshot
13
+ * answers (`ui/src/snapshot.ts`), plus the two things a live proxy is *for*:
14
+ * `?ref=` to read any branch, and `/branches` so the switcher has something to
15
+ * list. Payloads come from `source.ts`, which is what the hosted board serves,
16
+ * so a board rendered from this differs from the hosted one in one thing:
17
+ * who may look. This tier has no viewer auth — the perimeter is wherever you
18
+ * deploy it — and it says so in `/project` the only way the SPA reads:
19
+ * `readOnly: true`, no `git` capability.
20
+ *
21
+ * Read-only by construction: anything but GET is a 405 before a token is
22
+ * even resolved.
23
+ */
24
+ export interface BoardHandlerOptions {
25
+ /**
26
+ * A bearer token GitHub will accept for reading the repository — a
27
+ * fine-grained PAT with `Contents: Read-only` on this one repo is the
28
+ * shape the README walks through — or a function that produces one, for
29
+ * an adapter that mints its own (an installation token, say). Called per
30
+ * request; cache in the getter if minting is expensive.
31
+ */
32
+ token: string | (() => string | Promise<string>);
33
+ /** `owner/name`. */
34
+ repo: string;
35
+ /** What a request with no `?ref=` reads. Default `HEAD`: the default branch. */
36
+ ref?: string;
37
+ /**
38
+ * Where the handler is mounted, so it can strip the prefix off incoming
39
+ * paths. `task export` writes `<base>api` here — `/api` at a domain root,
40
+ * `/board/api` under `--base /board/` — matching where the SPA looks.
41
+ */
42
+ basePath?: string;
43
+ /** Snapshots kept in memory, by commit. Default 24. */
44
+ maxCachedSnapshots?: number;
45
+ }
46
+ export type BoardHandler = (request: Request) => Promise<Response>;
47
+ /** `owner/name`, both halves as GitHub spells them. */
48
+ export declare const REPO_PATTERN: RegExp;
49
+ export declare function createBoardHandler(options: BoardHandlerOptions): BoardHandler;
50
+ //# sourceMappingURL=handler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../src/board/handler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAYH,MAAM,WAAW,mBAAmB;IAClC;;;;;;OAMG;IACH,KAAK,EAAE,MAAM,GAAG,CAAC,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAA;IAChD,oBAAoB;IACpB,IAAI,EAAE,MAAM,CAAA;IACZ,gFAAgF;IAChF,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,uDAAuD;IACvD,kBAAkB,CAAC,EAAE,MAAM,CAAA;CAC5B;AAED,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAA;AAIlE,uDAAuD;AACvD,eAAO,MAAM,YAAY,QAAuB,CAAA;AAoBhD,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,mBAAmB,GAAG,YAAY,CAiJ7E"}
@@ -0,0 +1,183 @@
1
+ /**
2
+ * The live proxy: an exported board's API, as one fetch handler.
3
+ *
4
+ * `task export` ships the board as static files plus a baked `snapshot.json`.
5
+ * Drop this next to them — as a Cloudflare Pages Function, or behind any host
6
+ * that can run a `(Request) => Response` — hand it a token, and the same
7
+ * files become a live board: the SPA probes for an API before it mounts,
8
+ * finds this one answering, and never reads the snapshot at all. No token,
9
+ * no function, and the probe fails and the snapshot is what you get. The
10
+ * upgrade is the deploy, not a rebuild.
11
+ *
12
+ * The routes are `task serve`'s read routes — the same ones the snapshot
13
+ * answers (`ui/src/snapshot.ts`), plus the two things a live proxy is *for*:
14
+ * `?ref=` to read any branch, and `/branches` so the switcher has something to
15
+ * list. Payloads come from `source.ts`, which is what the hosted board serves,
16
+ * so a board rendered from this differs from the hosted one in one thing:
17
+ * who may look. This tier has no viewer auth — the perimeter is wherever you
18
+ * deploy it — and it says so in `/project` the only way the SPA reads:
19
+ * `readOnly: true`, no `git` capability.
20
+ *
21
+ * Read-only by construction: anything but GET is a 405 before a token is
22
+ * even resolved.
23
+ */
24
+ import { GitHubError, headCommit, isMissingRef, listBranches, repoInfo } from "./github.js";
25
+ import { BoardSourceError, SnapshotMemory, STATUSES, snapshotAt, } from "./source.js";
26
+ const REF_PATTERN = /^[\w.\-/]{1,255}$/;
27
+ /** `owner/name`, both halves as GitHub spells them. */
28
+ export const REPO_PATTERN = /^[\w.-]+\/[\w.-]+$/;
29
+ function json(body, status = 200) {
30
+ return new Response(JSON.stringify(body), {
31
+ status,
32
+ headers: {
33
+ "Content-Type": "application/json; charset=utf-8",
34
+ // Every answer is "the repository right now"; nothing between this
35
+ // function and the browser should hold on to one.
36
+ "Cache-Control": "no-store",
37
+ },
38
+ });
39
+ }
40
+ /** The board the request asked for, or the repo's default — `pickBoard`'s rule. */
41
+ function pickBoard(snapshot, requested) {
42
+ const id = requested && snapshot.data.has(requested) ? requested : snapshot.boards[0]?.id;
43
+ return id === undefined ? null : (snapshot.data.get(id) ?? null);
44
+ }
45
+ export function createBoardHandler(options) {
46
+ const { repo: slug, ref: defaultRef = "HEAD" } = options;
47
+ if (!REPO_PATTERN.test(slug))
48
+ throw new Error(`repo must be owner/name — got ${JSON.stringify(slug)}`);
49
+ const [owner, repo] = slug.split("/");
50
+ const basePath = (options.basePath ?? "/api").replace(/\/+$/, "");
51
+ const memory = new SnapshotMemory(options.maxCachedSnapshots);
52
+ // A cold start sees the SPA's four boot requests at once, all for the same
53
+ // commit. One tarball, not four: the first caller's load is shared with
54
+ // everyone who asks for that sha while it is still in flight.
55
+ const inFlight = new Map();
56
+ async function resolveToken() {
57
+ const token = typeof options.token === "function" ? await options.token() : options.token;
58
+ if (!token) {
59
+ // Unconfigured is not an error the board can act on; it is "no API",
60
+ // and a 5xx is exactly what makes the SPA's probe fall back to the
61
+ // snapshot sitting next to this function.
62
+ throw new BoardSourceError(503, "no GitHub token configured for the live board", "no-token");
63
+ }
64
+ return token;
65
+ }
66
+ async function load(token, ref, explicit) {
67
+ let commit;
68
+ try {
69
+ commit = await headCommit(token, owner, repo, ref);
70
+ }
71
+ catch (error) {
72
+ // With an explicit ref, a miss almost always means the branch — a
73
+ // deleted preview, a typo — not the repository.
74
+ if (explicit && isMissingRef(error)) {
75
+ throw new BoardSourceError(404, `no such branch or ref: ${ref}`, "no-such-ref");
76
+ }
77
+ throw error;
78
+ }
79
+ const key = `${owner}/${repo}@${commit.sha}`;
80
+ const cached = memory.get(key);
81
+ if (cached)
82
+ return cached;
83
+ let pending = inFlight.get(key);
84
+ if (!pending) {
85
+ pending = snapshotAt(token, owner, repo, commit, ref)
86
+ .then((snapshot) => memory.remember(key, snapshot))
87
+ .finally(() => inFlight.delete(key));
88
+ inFlight.set(key, pending);
89
+ }
90
+ return pending;
91
+ }
92
+ async function handle(request) {
93
+ if (request.method !== "GET" && request.method !== "HEAD") {
94
+ // Every editing affordance is already hidden by `readOnly`; this is the
95
+ // backstop, in the shape `ApiError` reads.
96
+ return json({ error: "this board is read-only — commit and push to change it", reason: "read-only" }, 405);
97
+ }
98
+ const url = new URL(request.url);
99
+ if (url.pathname !== basePath && !url.pathname.startsWith(`${basePath}/`)) {
100
+ return json({ error: `not an API path: ${url.pathname}` }, 404);
101
+ }
102
+ const route = url.pathname.slice(basePath.length) || "/";
103
+ const token = await resolveToken();
104
+ if (route === "/branches") {
105
+ // Default branch first (it's the way back), then alphabetical — the
106
+ // hosted board's order, which is the order the switcher expects.
107
+ const [names, info] = await Promise.all([
108
+ listBranches(token, owner, repo),
109
+ repoInfo(token, owner, repo),
110
+ ]);
111
+ const branches = names
112
+ .map((name) => ({ name, isDefault: name === info.defaultBranch }))
113
+ .sort((a, b) => Number(b.isDefault) - Number(a.isDefault) || a.name.localeCompare(b.name));
114
+ return json({ branches });
115
+ }
116
+ // A ref is a viewer-supplied string that lands in a URL path; anything
117
+ // outside git's own naming rules is ignored rather than escaped.
118
+ const requestedRef = url.searchParams.get("ref");
119
+ const ref = requestedRef && REF_PATTERN.test(requestedRef) && !requestedRef.includes("..")
120
+ ? requestedRef
121
+ : null;
122
+ const snapshot = await load(token, ref ?? defaultRef, ref !== null);
123
+ if (route === "/boards")
124
+ return json({ boards: snapshot.boards });
125
+ const board = pickBoard(snapshot, url.searchParams.get("board"));
126
+ if (!board)
127
+ return json({ error: "no such board" }, 404);
128
+ if (route === "/project") {
129
+ return json({
130
+ name: board.info.name,
131
+ prefix: board.info.prefix,
132
+ statuses: STATUSES,
133
+ // The payload the SPA reads to decide whether to offer editing at all.
134
+ // The data is a commit; the only way to change one is to push.
135
+ readOnly: true,
136
+ // Locally this is who would author a comment. Nothing here can
137
+ // comment, and nothing here knows who is looking.
138
+ author: "",
139
+ });
140
+ }
141
+ if (route === "/tasks")
142
+ return json({ tasks: board.tasks });
143
+ if (route === "/goals")
144
+ return json({ goals: board.goals });
145
+ const detail = /^\/tasks\/([^/]+)$/.exec(route);
146
+ if (detail) {
147
+ // The CLI accepts "PREFIX-x7k4m" or "x7k4m"; so does this.
148
+ const match = /^(?:[A-Za-z0-9]+-)?([A-Za-z0-9]+)$/.exec(decodeURIComponent(detail[1]));
149
+ if (!match)
150
+ return json({ error: "invalid task id" }, 400);
151
+ const key = match[1].toLowerCase();
152
+ const task = board.tasks.find((candidate) => candidate.key === key);
153
+ if (!task)
154
+ return json({ error: `no such task: ${key}` }, 404);
155
+ return json({ task, comments: board.comments.get(key) ?? [] });
156
+ }
157
+ // `/inbox` and the git surface are deliberately absent: the SPA degrades
158
+ // to no inbox and no git chrome when they 404, same as the hosted board.
159
+ return json({ error: `not here: ${route}` }, 404);
160
+ }
161
+ return async (request) => {
162
+ try {
163
+ return await handle(request);
164
+ }
165
+ catch (error) {
166
+ if (error instanceof BoardSourceError) {
167
+ return json({ error: error.message, reason: error.reason }, error.status);
168
+ }
169
+ if (error instanceof GitHubError) {
170
+ // A bad or expired token, a repo the token can't see, GitHub itself
171
+ // down: all of them are "the live board can't answer right now". A
172
+ // 5xx is deliberate — on the boot probe it is what sends the SPA to
173
+ // the snapshot instead of an error screen.
174
+ return json({
175
+ error: `GitHub answered ${error.status} — check the token and the repository name`,
176
+ reason: "github",
177
+ }, 502);
178
+ }
179
+ return json({ error: error instanceof Error ? error.message : "internal error" }, 500);
180
+ }
181
+ };
182
+ }
183
+ //# sourceMappingURL=handler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handler.js","sourceRoot":"","sources":["../../src/board/handler.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The live proxy, against a fake GitHub.
3
+ *
4
+ * `globalThis.fetch` is replaced with a tiny router that answers the four
5
+ * calls the handler makes — head commit, tarball, branches, repo info — from
6
+ * an in-memory repository, so what's under test is the handler's routing,
7
+ * caching and error shapes, not the network. The payloads themselves are
8
+ * pinned elsewhere (`source.test.ts` diffs them against the CLI's store).
9
+ */
10
+ export {};
11
+ //# sourceMappingURL=handler.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handler.test.d.ts","sourceRoot":"","sources":["../../src/board/handler.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG"}