@volter/twin-github 0.1.2 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +88 -36
- package/client/github-mirror.css +277 -319
- package/client/github-mirror.d.ts +418 -0
- package/client/github-mirror.js +485 -0
- package/client/github-mirror.tsx +153 -357
- package/client/pulls-rest.ts +159 -0
- package/client/pulls-workspace.tsx +841 -0
- package/dist/client/github-mirror.bundle.js +239 -0
- package/dist/client/github-mirror.css +916 -0
- package/dist/client/github-mirror.d.ts +418 -0
- package/dist/client/github-mirror.js +485 -0
- package/dist/client/github-mirror.tsx +1315 -0
- package/dist/client/pulls-rest.d.ts +42 -0
- package/dist/client/pulls-rest.js +140 -0
- package/dist/client/pulls-rest.ts +159 -0
- package/dist/client/pulls-workspace.bundle.js +22 -0
- package/dist/client/pulls-workspace.d.ts +114 -0
- package/dist/client/pulls-workspace.js +418 -0
- package/dist/client/pulls-workspace.tsx +841 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +40 -0
- package/dist/src/generated/graphql-sdl.gen.json +1 -0
- package/dist/src/generated/graphql.gen.json +1 -0
- package/dist/src/generated/surface.gen.json +1 -0
- package/dist/src/generated/ui.gen.json +1 -0
- package/dist/src/github-budget.d.ts +69 -0
- package/dist/src/github-budget.js +172 -0
- package/dist/src/github-capabilities.d.ts +5 -0
- package/dist/src/github-capabilities.js +4468 -0
- package/dist/src/github-conformance.d.ts +43 -0
- package/dist/src/github-conformance.js +76 -0
- package/dist/src/github-connector.d.ts +307 -0
- package/dist/src/github-connector.js +1398 -0
- package/dist/src/github-events.d.ts +41 -0
- package/dist/src/github-events.js +230 -0
- package/dist/src/github-git-http.d.ts +49 -0
- package/dist/src/github-git-http.js +185 -0
- package/dist/src/github-git-plane.d.ts +114 -0
- package/dist/src/github-git-plane.js +407 -0
- package/dist/src/github-mirror-state.d.ts +2 -0
- package/dist/src/github-mirror-state.js +335 -0
- package/dist/src/github-mirror-ui.d.ts +20 -0
- package/dist/src/github-mirror-ui.js +101 -0
- package/dist/src/github-server.d.ts +14 -0
- package/dist/src/github-server.js +240 -0
- package/dist/src/github-shared.d.ts +12 -0
- package/dist/src/github-shared.js +21 -0
- package/dist/src/github-twin.d.ts +1411 -0
- package/dist/src/github-twin.js +4084 -0
- package/dist/src/github-ui-conformance.d.ts +4 -0
- package/dist/src/github-ui-conformance.js +105 -0
- package/dist/src/github-ui-structure.d.ts +18 -0
- package/dist/src/github-ui-structure.js +251 -0
- package/dist/src/graphql-wire.d.ts +22 -0
- package/dist/src/graphql-wire.js +89 -0
- package/dist/src/index.d.ts +17 -0
- package/dist/src/index.js +101 -0
- package/dist/src/manifest.d.ts +8 -0
- package/dist/src/manifest.js +597 -0
- package/dist/src/npm-registry.d.ts +7 -0
- package/dist/src/npm-registry.js +47 -0
- package/dist/src/screens/app-installation.d.ts +3 -0
- package/dist/src/screens/app-installation.js +166 -0
- package/dist/src/screens/app-manifest.d.ts +3 -0
- package/dist/src/screens/app-manifest.js +81 -0
- package/dist/src/screens/oauth.d.ts +15 -0
- package/dist/src/screens/oauth.js +257 -0
- package/dist/src/screens/session.d.ts +8 -0
- package/dist/src/screens/session.js +159 -0
- package/dist/src/semantics/actions.d.ts +2 -0
- package/dist/src/semantics/actions.js +413 -0
- package/dist/src/semantics/activity.d.ts +4 -0
- package/dist/src/semantics/activity.js +161 -0
- package/dist/src/semantics/apps.d.ts +2 -0
- package/dist/src/semantics/apps.js +144 -0
- package/dist/src/semantics/branches.d.ts +2 -0
- package/dist/src/semantics/branches.js +136 -0
- package/dist/src/semantics/checks.d.ts +2 -0
- package/dist/src/semantics/checks.js +176 -0
- package/dist/src/semantics/code-scanning-upload.d.ts +2 -0
- package/dist/src/semantics/code-scanning-upload.js +97 -0
- package/dist/src/semantics/codespaces.d.ts +2 -0
- package/dist/src/semantics/codespaces.js +58 -0
- package/dist/src/semantics/commits.d.ts +2 -0
- package/dist/src/semantics/commits.js +109 -0
- package/dist/src/semantics/contents.d.ts +2 -0
- package/dist/src/semantics/contents.js +131 -0
- package/dist/src/semantics/deployments.d.ts +2 -0
- package/dist/src/semantics/deployments.js +127 -0
- package/dist/src/semantics/gists.d.ts +2 -0
- package/dist/src/semantics/gists.js +86 -0
- package/dist/src/semantics/git.d.ts +2 -0
- package/dist/src/semantics/git.js +235 -0
- package/dist/src/semantics/graphql.d.ts +7 -0
- package/dist/src/semantics/graphql.js +512 -0
- package/dist/src/semantics/index.d.ts +5 -0
- package/dist/src/semantics/index.js +62 -0
- package/dist/src/semantics/issues.d.ts +2 -0
- package/dist/src/semantics/issues.js +456 -0
- package/dist/src/semantics/keys.d.ts +2 -0
- package/dist/src/semantics/keys.js +66 -0
- package/dist/src/semantics/labels.d.ts +2 -0
- package/dist/src/semantics/labels.js +100 -0
- package/dist/src/semantics/meta.d.ts +12 -0
- package/dist/src/semantics/meta.js +144 -0
- package/dist/src/semantics/notifications.d.ts +4 -0
- package/dist/src/semantics/notifications.js +62 -0
- package/dist/src/semantics/orgs.d.ts +2 -0
- package/dist/src/semantics/orgs.js +420 -0
- package/dist/src/semantics/packages.d.ts +2 -0
- package/dist/src/semantics/packages.js +55 -0
- package/dist/src/semantics/pages.d.ts +2 -0
- package/dist/src/semantics/pages.js +176 -0
- package/dist/src/semantics/projects.d.ts +2 -0
- package/dist/src/semantics/projects.js +239 -0
- package/dist/src/semantics/pulls.d.ts +2 -0
- package/dist/src/semantics/pulls.js +462 -0
- package/dist/src/semantics/push-reactions.d.ts +101 -0
- package/dist/src/semantics/push-reactions.js +513 -0
- package/dist/src/semantics/releases.d.ts +19 -0
- package/dist/src/semantics/releases.js +231 -0
- package/dist/src/semantics/repo-invitations.d.ts +2 -0
- package/dist/src/semantics/repo-invitations.js +98 -0
- package/dist/src/semantics/repos.d.ts +17 -0
- package/dist/src/semantics/repos.js +390 -0
- package/dist/src/semantics/rulesets.d.ts +2 -0
- package/dist/src/semantics/rulesets.js +111 -0
- package/dist/src/semantics/search.d.ts +2 -0
- package/dist/src/semantics/search.js +84 -0
- package/dist/src/semantics/security.d.ts +2 -0
- package/dist/src/semantics/security.js +177 -0
- package/dist/src/semantics/shared.d.ts +61 -0
- package/dist/src/semantics/shared.js +151 -0
- package/dist/src/semantics/users.d.ts +2 -0
- package/dist/src/semantics/users.js +255 -0
- package/dist/src/semantics/webhooks.d.ts +2 -0
- package/dist/src/semantics/webhooks.js +107 -0
- package/dist/test-fixtures/github-a11y-reference.pr-list.SOURCE.md +56 -0
- package/dist/test-fixtures/github-a11y-reference.pr-list.json +1447 -0
- package/dist/test-fixtures/github-comment-schema.SOURCE.md +11 -0
- package/dist/test-fixtures/github-comment-schema.json +702 -0
- package/dist/test-fixtures/github-known-deviations.json +94 -0
- package/dist/test-fixtures/github-openapi-operations.SOURCE.md +76 -0
- package/dist/test-fixtures/github-openapi-operations.json +362 -0
- package/dist/test-fixtures/github-pull-schema.SOURCE.md +37 -0
- package/dist/test-fixtures/github-pull-schema.json +3601 -0
- package/dist/test-fixtures/github-review-schema.SOURCE.md +12 -0
- package/dist/test-fixtures/github-review-schema.json +236 -0
- package/package.json +20 -11
- package/src/cli.ts +7 -6
- package/src/generated/graphql-sdl.gen.json +1 -0
- package/src/generated/graphql.gen.json +1 -0
- package/src/generated/surface.gen.json +1 -0
- package/src/generated/ui.gen.json +1 -0
- package/src/github-a11y-snapshot.uitest.ts +5 -5
- package/src/github-budget.ts +4 -4
- package/src/github-capabilities.ts +1993 -556
- package/src/github-conformance.ts +12 -7
- package/src/github-connector.ts +108 -96
- package/src/github-events.ts +223 -95
- package/src/github-git-http.ts +47 -83
- package/src/github-git-plane.ts +247 -385
- package/src/github-journey.uitest.ts +28 -44
- package/src/github-mirror-state.ts +23 -61
- package/src/github-mirror-ui.ts +16 -10
- package/src/github-server.ts +158 -60
- package/src/github-shared.ts +1 -1
- package/src/github-twin.ts +1525 -4494
- package/src/github-ui-conformance.ts +7 -8
- package/src/github-ui-structure.ts +19 -18
- package/src/graphql-wire.ts +115 -0
- package/src/index.ts +47 -7
- package/src/manifest.ts +605 -0
- package/src/npm-registry.ts +43 -0
- package/src/screens/app-installation.tsx +217 -0
- package/src/screens/app-manifest.tsx +97 -0
- package/src/screens/oauth.tsx +258 -0
- package/src/screens/session.tsx +186 -0
- package/src/semantics/actions.ts +395 -0
- package/src/semantics/activity.ts +171 -0
- package/src/semantics/apps.ts +133 -0
- package/src/semantics/branches.ts +133 -0
- package/src/semantics/checks.ts +163 -0
- package/src/semantics/code-scanning-upload.ts +92 -0
- package/src/semantics/codespaces.ts +58 -0
- package/src/semantics/commits.ts +109 -0
- package/src/semantics/contents.ts +112 -0
- package/src/semantics/deployments.ts +116 -0
- package/src/semantics/gists.ts +85 -0
- package/src/semantics/git.ts +226 -0
- package/src/semantics/graphql.ts +505 -0
- package/src/semantics/index.ts +68 -0
- package/src/semantics/issues.ts +434 -0
- package/src/semantics/keys.ts +66 -0
- package/src/semantics/labels.ts +91 -0
- package/src/semantics/meta.ts +139 -0
- package/src/semantics/notifications.ts +58 -0
- package/src/semantics/orgs.ts +383 -0
- package/src/semantics/packages.ts +59 -0
- package/src/semantics/pages.ts +154 -0
- package/src/semantics/projects.ts +246 -0
- package/src/semantics/pulls.ts +421 -0
- package/src/semantics/push-reactions.ts +471 -0
- package/src/semantics/releases.ts +213 -0
- package/src/semantics/repo-invitations.ts +112 -0
- package/src/semantics/repos.ts +373 -0
- package/src/semantics/rulesets.ts +107 -0
- package/src/semantics/search.ts +83 -0
- package/src/semantics/security.ts +153 -0
- package/src/semantics/shared.ts +181 -0
- package/src/semantics/users.ts +252 -0
- package/src/semantics/webhooks.ts +101 -0
- package/test-fixtures/github-known-deviations.json +7 -8
- package/test-fixtures/github-openapi-operations.json +46 -227
- package/src/github-graphql.ts +0 -398
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
// GitHub MIRROR STATE — the ONE named store the mirror console renders (runtime contract
|
|
2
|
+
// R5, the store door; R3, mirror purity): `GET /twin/store/mirror`, declared on the pack's
|
|
3
|
+
// own fetch adapter (`github-server.ts`) and served from it. A deterministic projection of
|
|
4
|
+
// the stored state — pull requests grouped by repo (the per-repo selector), issues,
|
|
5
|
+
// reviews/comments, Actions, releases, discussions, contents, notifications, deployments,
|
|
6
|
+
// projects, insights and settings — built from the twin's own evidence + local-write state.
|
|
7
|
+
// No wall-clock, no randomness: the same stored state projects the same bytes. Server-side
|
|
8
|
+
// only — the mirror module (`github-mirror-ui.ts`) never imports this; it reads the door
|
|
9
|
+
// over the wire. The git plane (bare repos, smart-HTTP) is never read here: the projection
|
|
10
|
+
// is over the folded REST state alone.
|
|
11
|
+
//
|
|
12
|
+
// Honesty: the github world is an EVIDENCE mirror — observed data carries PR metadata +
|
|
13
|
+
// review/comment COUNTS but not their content; local simulator/fork writes carry
|
|
14
|
+
// titles/bodies. So review/comment timeline entries are surfaced from the observed counts
|
|
15
|
+
// and labelled as such; PR titles/bodies appear when present.
|
|
16
|
+
import { discussionCategoriesFor, githubState } from "./github-twin.js";
|
|
17
|
+
import { GITHUB_WEB_ORIGIN, githubIssuePath, githubPullPath } from "./github-shared.js";
|
|
18
|
+
const OBSERVED = 'content not mirrored by the evidence twin';
|
|
19
|
+
/** The `mirror` store: the whole console payload, projected from the twin's github world. */
|
|
20
|
+
export function githubMirrorState(root) {
|
|
21
|
+
const { prs: foldedPrs, issues, reviews: foldedReviews, comments: foldedComments, statuses, checkRuns, milestones, workflows: foldedWorkflows, workflowRuns, jobs, releases: foldedReleases, releaseAssets, tags, discussions: foldedDiscussions, discussionComments: foldedDiscussionComments, contents: foldedContents, notifications: foldedNotifications, deployments: foldedDeployments, deploymentStatuses: foldedDeploymentStatuses, environments: foldedEnvironments, projects: foldedProjects, projectFields: foldedProjectFields, projectItems: foldedProjectItems, projectViews: foldedProjectViews, repos: foldedRepos, collaborators: foldedCollaborators, webhooks: foldedWebhooks, branches: foldedBranches } = githubState(root);
|
|
22
|
+
// Only PULL REQUESTS are pull requests. The fold's `prs` map also holds the shells it
|
|
23
|
+
// materializes for the review/comment counters, which hang off issue numbers too — the
|
|
24
|
+
// mirror is a door onto the same world the REST list serves, and the two must agree about
|
|
25
|
+
// what a pull request is.
|
|
26
|
+
const prs = foldedPrs.filter((p) => p.is_pull_request);
|
|
27
|
+
// CI rollup per PR head_sha from commit statuses + check runs (a LOCAL CI construct;
|
|
28
|
+
// observed-only PRs carry no head_sha CI, so this stays null for them — honesty).
|
|
29
|
+
// Precedence mirrors GitHub's rollup: any failure/error → 'failure'; any pending/queued/
|
|
30
|
+
// in_progress → 'pending'; all success/neutral → 'success'.
|
|
31
|
+
const ciStatusFor = (repository, headSha) => {
|
|
32
|
+
if (!headSha)
|
|
33
|
+
return undefined;
|
|
34
|
+
const relStatuses = statuses.filter((s) => s.repository === repository && s.sha === headSha);
|
|
35
|
+
const relChecks = checkRuns.filter((c) => c.repository === repository && c.head_sha === headSha);
|
|
36
|
+
if (!relStatuses.length && !relChecks.length)
|
|
37
|
+
return undefined;
|
|
38
|
+
const states = [
|
|
39
|
+
...relStatuses.map((s) => s.state),
|
|
40
|
+
...relChecks.map((c) => (c.status === 'completed' ? (c.conclusion ?? 'neutral') : 'pending')),
|
|
41
|
+
];
|
|
42
|
+
if (states.some((s) => s === 'failure' || s === 'error'))
|
|
43
|
+
return 'failure';
|
|
44
|
+
if (states.some((s) => s === 'pending'))
|
|
45
|
+
return 'pending';
|
|
46
|
+
return 'success';
|
|
47
|
+
};
|
|
48
|
+
const pullRequests = prs.map((pr) => {
|
|
49
|
+
// A PR's merge state: an explicit merged write wins; otherwise the recorded state,
|
|
50
|
+
// defaulting to open. We never fabricate a title for observed-only PRs.
|
|
51
|
+
const state = pr.merged ? 'merged' : (pr.state ?? 'open');
|
|
52
|
+
// milestone: a local PATCH stores the NUMBER; surface its title from state.milestones.
|
|
53
|
+
const ms = pr.milestone !== undefined ? milestones.find((m) => m.repository === pr.repository && m.number === pr.milestone) : undefined;
|
|
54
|
+
return {
|
|
55
|
+
repo: pr.repository,
|
|
56
|
+
number: pr.number,
|
|
57
|
+
title: pr.title,
|
|
58
|
+
body: pr.body,
|
|
59
|
+
state,
|
|
60
|
+
merged: pr.merged ?? false,
|
|
61
|
+
mergedAt: pr.merged_at,
|
|
62
|
+
mergeCommit: pr.merge_commit_sha,
|
|
63
|
+
url: `${GITHUB_WEB_ORIGIN}${githubPullPath(pr.repository, pr.number)}`,
|
|
64
|
+
baseBranch: pr.base_ref,
|
|
65
|
+
headSha: pr.head_sha,
|
|
66
|
+
changedFiles: pr.changed_files,
|
|
67
|
+
commitsCount: pr.commits,
|
|
68
|
+
commentsCount: pr.comment_count,
|
|
69
|
+
reviewCount: pr.review_count,
|
|
70
|
+
reviewCommentsCount: foldedComments.filter((c) => c.repository === pr.repository && c.number === pr.number && c.kind === 'review').length,
|
|
71
|
+
labels: (pr.labels ?? []).map((name) => ({ name })),
|
|
72
|
+
assignees: pr.assignees ?? [],
|
|
73
|
+
// requested reviewers (LOCAL writes): bare logins distinct from submitted reviews.
|
|
74
|
+
requestedReviewers: pr.requested_reviewers ?? [],
|
|
75
|
+
// milestone title (LOCAL write); undefined for observed-only PRs.
|
|
76
|
+
milestone: ms?.title,
|
|
77
|
+
// additions/deletions line counts: roll up from a carried file list on LOCAL writes;
|
|
78
|
+
// undefined for observed-only PRs (the evidence fold never carries line counts).
|
|
79
|
+
additions: pr.additions,
|
|
80
|
+
deletions: pr.deletions,
|
|
81
|
+
// CI rollup over head_sha statuses + check-runs; undefined when no CI evidence.
|
|
82
|
+
ciStatus: ciStatusFor(pr.repository, pr.head_sha),
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
// Per-commit detail and per-file diffs live in the git plane, which this projection does not read: the console
|
|
86
|
+
// reads them over REST (client/pulls-rest.ts).
|
|
87
|
+
const commits = [];
|
|
88
|
+
const files = [];
|
|
89
|
+
// First-class issues (a LOCAL construct) grouped per repo via the `repo` field, like PRs.
|
|
90
|
+
const issuesOut = issues.map((iss) => {
|
|
91
|
+
const ms = iss.milestone !== undefined ? milestones.find((m) => m.repository === iss.repository && m.number === iss.milestone) : undefined;
|
|
92
|
+
return {
|
|
93
|
+
repo: iss.repository,
|
|
94
|
+
number: iss.number,
|
|
95
|
+
title: iss.title,
|
|
96
|
+
body: iss.body,
|
|
97
|
+
state: iss.state ?? 'open',
|
|
98
|
+
url: `${GITHUB_WEB_ORIGIN}${githubIssuePath(iss.repository, iss.number)}`,
|
|
99
|
+
createdAt: iss.created_at,
|
|
100
|
+
updatedAt: iss.updated_at,
|
|
101
|
+
labels: (iss.labels ?? []).map((name) => ({ name })),
|
|
102
|
+
assignees: iss.assignees ?? [],
|
|
103
|
+
milestone: ms?.title,
|
|
104
|
+
stateReason: iss.state_reason ?? undefined,
|
|
105
|
+
locked: iss.locked ?? false,
|
|
106
|
+
// issue comments (kind:'issue' on the shared comment space) — the detail page renders
|
|
107
|
+
// these in its timeline; observed comments carry no body (labelled, never fabricated).
|
|
108
|
+
comments: foldedComments
|
|
109
|
+
.filter((c) => c.kind === 'issue' && c.repository === iss.repository && c.number === iss.number)
|
|
110
|
+
.map((c) => ({ id: c.id, author: c.body === undefined ? '(observed)' : 'simulator', body: c.body ?? `Comment observed — ${OBSERVED}.`, createdAt: c.created_at })),
|
|
111
|
+
// a derived timeline of state-change events (labeled/assigned/milestoned/locked/closed)
|
|
112
|
+
// mirroring the issue's current modeled state — the detail page renders it as a feed.
|
|
113
|
+
timeline: [
|
|
114
|
+
...(iss.labels ?? []).map((name) => ({ event: 'labeled', detail: name })),
|
|
115
|
+
...(iss.assignees ?? []).map((login) => ({ event: 'assigned', detail: `@${login}` })),
|
|
116
|
+
...(iss.locked ? [{ event: 'locked', detail: iss.active_lock_reason ?? '' }] : []),
|
|
117
|
+
...(iss.state === 'closed' ? [{ event: 'closed', detail: iss.state_reason ?? '' }] : []),
|
|
118
|
+
],
|
|
119
|
+
};
|
|
120
|
+
});
|
|
121
|
+
// Reviews carry state/body for LOCAL writes; observed reviews carry neither, so we
|
|
122
|
+
// label them honestly as observed-only rather than fabricating review content.
|
|
123
|
+
const reviews = foldedReviews.map((r) => {
|
|
124
|
+
const observed = r.state === undefined && r.body === undefined;
|
|
125
|
+
return {
|
|
126
|
+
repo: r.repository,
|
|
127
|
+
number: r.number,
|
|
128
|
+
reviewer: observed ? '(observed)' : 'simulator',
|
|
129
|
+
state: r.state ?? 'reviewed',
|
|
130
|
+
body: r.body ?? (observed ? `Review observed — ${OBSERVED}.` : ''),
|
|
131
|
+
submittedAt: r.submitted_at,
|
|
132
|
+
observed,
|
|
133
|
+
};
|
|
134
|
+
});
|
|
135
|
+
// Issue comments and review (diff-thread) comments are split by kind so the client can
|
|
136
|
+
// render them in their right place. Observed comments carry no body — label them.
|
|
137
|
+
const toComment = (c) => {
|
|
138
|
+
const observed = c.body === undefined;
|
|
139
|
+
return {
|
|
140
|
+
repo: c.repository,
|
|
141
|
+
number: c.number,
|
|
142
|
+
author: observed ? '(observed)' : 'simulator',
|
|
143
|
+
body: c.body ?? (observed ? `Comment observed — ${OBSERVED}.` : ''),
|
|
144
|
+
createdAt: c.created_at,
|
|
145
|
+
observed,
|
|
146
|
+
};
|
|
147
|
+
};
|
|
148
|
+
const comments = foldedComments.filter((c) => c.kind === 'issue').map(toComment);
|
|
149
|
+
const reviewComments = foldedComments.filter((c) => c.kind === 'review').map(toComment);
|
|
150
|
+
// Actions (workflows + runs + jobs) — a LOCAL construct (running real CI is the only Actions
|
|
151
|
+
// non-goal; the run/job OBJECTS are modeled). Surfaced per-repo like PRs/issues. Runs carry
|
|
152
|
+
// status/conclusion the client renders as badges; jobs are scoped to a run by run_id.
|
|
153
|
+
const workflows = foldedWorkflows.map((w) => ({
|
|
154
|
+
repo: w.repository,
|
|
155
|
+
id: w.id,
|
|
156
|
+
name: w.name,
|
|
157
|
+
path: w.path,
|
|
158
|
+
state: w.state ?? 'active',
|
|
159
|
+
}));
|
|
160
|
+
const runs = workflowRuns.map((r) => {
|
|
161
|
+
const wf = foldedWorkflows.find((w) => w.repository === r.repository && w.id === r.workflow_id);
|
|
162
|
+
return {
|
|
163
|
+
repo: r.repository,
|
|
164
|
+
id: r.id,
|
|
165
|
+
workflowId: r.workflow_id,
|
|
166
|
+
workflowName: wf?.name ?? r.name,
|
|
167
|
+
name: r.name,
|
|
168
|
+
runNumber: r.run_number,
|
|
169
|
+
event: r.event,
|
|
170
|
+
status: r.status,
|
|
171
|
+
conclusion: r.conclusion,
|
|
172
|
+
headSha: r.head_sha,
|
|
173
|
+
headBranch: r.head_branch,
|
|
174
|
+
createdAt: r.created_at,
|
|
175
|
+
updatedAt: r.updated_at,
|
|
176
|
+
url: `${GITHUB_WEB_ORIGIN}/${r.repository}/actions/runs/${r.id}`,
|
|
177
|
+
jobCount: jobs.filter((j) => j.repository === r.repository && j.run_id === r.id).length,
|
|
178
|
+
};
|
|
179
|
+
});
|
|
180
|
+
const runJobs = jobs.map((j) => ({
|
|
181
|
+
repo: j.repository,
|
|
182
|
+
id: j.id,
|
|
183
|
+
runId: j.run_id,
|
|
184
|
+
name: j.name,
|
|
185
|
+
status: j.status,
|
|
186
|
+
conclusion: j.conclusion,
|
|
187
|
+
steps: j.steps.map((s) => ({ name: s.name, status: s.status, conclusion: s.conclusion, number: s.number })),
|
|
188
|
+
}));
|
|
189
|
+
// Releases (a LOCAL construct — there is no observed release evidence; release ASSETS are
|
|
190
|
+
// metadata only, the binary bytes are the declared Non-goal). Surfaced per-repo like PRs.
|
|
191
|
+
// Each release carries its tag/name/draft/prerelease flags + its asset catalog; tags are
|
|
192
|
+
// the published-release tag list (derived, no git blob bytes).
|
|
193
|
+
const releases = foldedReleases.map((r) => ({
|
|
194
|
+
repo: r.repository,
|
|
195
|
+
id: r.id,
|
|
196
|
+
tagName: r.tag_name,
|
|
197
|
+
name: r.name,
|
|
198
|
+
body: r.body,
|
|
199
|
+
draft: r.draft ?? false,
|
|
200
|
+
prerelease: r.prerelease ?? false,
|
|
201
|
+
createdAt: r.created_at,
|
|
202
|
+
publishedAt: r.published_at,
|
|
203
|
+
targetCommitish: r.target_commitish,
|
|
204
|
+
url: `${GITHUB_WEB_ORIGIN}/${r.repository}/releases/tag/${encodeURIComponent(r.tag_name)}`,
|
|
205
|
+
assets: releaseAssets
|
|
206
|
+
.filter((a) => a.repository === r.repository && a.release_id === r.id)
|
|
207
|
+
.map((a) => ({ id: a.id, name: a.name, label: a.label, contentType: a.content_type, size: a.size ?? 0, downloadCount: a.download_count ?? 0 })),
|
|
208
|
+
}));
|
|
209
|
+
const tagList = tags.map((t) => ({ repo: t.repository, name: t.name, commitSha: t.commit_sha }));
|
|
210
|
+
// Discussions (a LOCAL construct — no observed discussion evidence; modeled over REST,
|
|
211
|
+
// honestly noted as a deviation since real GitHub Discussions are GraphQL). Each discussion
|
|
212
|
+
// carries its category (resolved from the per-repo category list), state/lock, the chosen
|
|
213
|
+
// answer pointer, and its comments/replies (with the is_answer flag). Surfaced per-repo.
|
|
214
|
+
const discussionRepos = new Set(foldedDiscussions.map((d) => d.repository));
|
|
215
|
+
const categoryByRepoSlug = new Map();
|
|
216
|
+
for (const r of discussionRepos)
|
|
217
|
+
for (const c of discussionCategoriesFor(r))
|
|
218
|
+
categoryByRepoSlug.set(`${r}#${c.slug}`, { name: c.name, emoji: c.emoji, isAnswerable: c.is_answerable ?? false });
|
|
219
|
+
const discussions = foldedDiscussions.map((d) => {
|
|
220
|
+
const cat = d.category_slug ? categoryByRepoSlug.get(`${d.repository}#${d.category_slug}`) : undefined;
|
|
221
|
+
const threadComments = foldedDiscussionComments.filter((c) => c.repository === d.repository && c.discussion_number === d.number);
|
|
222
|
+
return {
|
|
223
|
+
repo: d.repository,
|
|
224
|
+
number: d.number,
|
|
225
|
+
title: d.title,
|
|
226
|
+
body: d.body,
|
|
227
|
+
state: d.locked ? 'locked' : (d.state ?? 'open'),
|
|
228
|
+
locked: d.locked ?? false,
|
|
229
|
+
categorySlug: d.category_slug,
|
|
230
|
+
categoryName: cat?.name,
|
|
231
|
+
categoryEmoji: cat?.emoji,
|
|
232
|
+
isAnswerable: cat?.isAnswerable ?? false,
|
|
233
|
+
answerCommentId: d.answer_comment_id ?? null,
|
|
234
|
+
isAnswered: d.answer_comment_id != null,
|
|
235
|
+
commentsCount: threadComments.length,
|
|
236
|
+
createdAt: d.created_at,
|
|
237
|
+
updatedAt: d.updated_at,
|
|
238
|
+
url: `${GITHUB_WEB_ORIGIN}/${d.repository}/discussions/${d.number}`,
|
|
239
|
+
comments: threadComments.map((c) => ({
|
|
240
|
+
id: c.id,
|
|
241
|
+
parentId: c.parent_id ?? null,
|
|
242
|
+
body: c.body,
|
|
243
|
+
isAnswer: c.is_answer ?? false,
|
|
244
|
+
createdAt: c.created_at,
|
|
245
|
+
})),
|
|
246
|
+
};
|
|
247
|
+
});
|
|
248
|
+
// Repo contents (a LOCAL construct) — the Code/file browser tree. Each entry carries its
|
|
249
|
+
// path + size; the browser renders the directory tree per repo (no blob bytes rendered).
|
|
250
|
+
const contents = foldedContents.map((c) => ({ repo: c.repository, path: c.path, size: c.size, sha: c.sha, branch: c.branch ?? 'main' }));
|
|
251
|
+
// Notifications (a LOCAL construct) — the inbox screen. unread flag + reason + subject.
|
|
252
|
+
const notifications = foldedNotifications.map((n) => ({ id: n.id, repo: n.repository, title: n.subject_title, type: n.subject_type, reason: n.reason, unread: n.unread, updatedAt: n.updated_at }));
|
|
253
|
+
// Deployments (a LOCAL construct) — the repo's Deployments/Environments screen. Each
|
|
254
|
+
// deployment carries its environment + ref + the LATEST status (the deployment's state),
|
|
255
|
+
// mirroring GitHub's Environments view (most-recent deployment per environment, with state).
|
|
256
|
+
const deployments = foldedDeployments.map((d) => {
|
|
257
|
+
const statusesForDep = foldedDeploymentStatuses.filter((s) => s.deployment_id === d.id).sort((a, b) => b.id - a.id);
|
|
258
|
+
const latest = statusesForDep[0];
|
|
259
|
+
return {
|
|
260
|
+
repo: d.repository,
|
|
261
|
+
id: d.id,
|
|
262
|
+
ref: d.ref,
|
|
263
|
+
sha: d.sha,
|
|
264
|
+
environment: d.environment,
|
|
265
|
+
task: d.task,
|
|
266
|
+
description: d.description ?? undefined,
|
|
267
|
+
production: d.production_environment ?? false,
|
|
268
|
+
transient: d.transient_environment ?? false,
|
|
269
|
+
createdAt: d.created_at,
|
|
270
|
+
state: latest?.state ?? 'pending',
|
|
271
|
+
environmentUrl: latest?.environment_url,
|
|
272
|
+
url: `${GITHUB_WEB_ORIGIN}/${d.repository}/deployments`,
|
|
273
|
+
statuses: statusesForDep.map((s) => ({ id: s.id, state: s.state, description: s.description ?? undefined, createdAt: s.created_at })),
|
|
274
|
+
};
|
|
275
|
+
});
|
|
276
|
+
// Environments (a LOCAL construct) — protection-rule summaries per environment.
|
|
277
|
+
const environments = foldedEnvironments.map((e) => ({
|
|
278
|
+
repo: e.repository,
|
|
279
|
+
name: e.name,
|
|
280
|
+
waitTimer: e.wait_timer,
|
|
281
|
+
reviewers: e.reviewers ?? [],
|
|
282
|
+
protected: Boolean(e.wait_timer) || Boolean(e.reviewers?.length),
|
|
283
|
+
}));
|
|
284
|
+
// ── Projects v2 boards — the Project board screen. Each project carries its views and its first single-select
|
|
285
|
+
// field's options as COLUMNS, with its items grouped into them by that field's value; an item with none lands
|
|
286
|
+
// in "No Status" (GitHub's board). Data-coupled: a field, an item or a value moves what the board renders.
|
|
287
|
+
const projectBoards = foldedProjects.map((p) => {
|
|
288
|
+
const fields = foldedProjectFields.filter((f) => f.project_id === p.id);
|
|
289
|
+
const statusField = fields.find((f) => f.data_type === 'single_select');
|
|
290
|
+
const items = foldedProjectItems.filter((i) => i.project_id === p.id);
|
|
291
|
+
const card = (i) => ({ id: i.id, title: i.title ?? `#${i.content_number ?? i.id}`, contentType: i.content_type });
|
|
292
|
+
const optionOf = (i) => statusField?.options?.find((o) => o.id === i.field_values?.[statusField.id]);
|
|
293
|
+
return {
|
|
294
|
+
id: p.id, number: p.number, title: p.title, owner: p.owner, closed: p.closed ?? false,
|
|
295
|
+
views: foldedProjectViews.filter((v) => v.project_id === p.id).map((v) => ({ number: v.number, name: v.name, layout: v.layout })),
|
|
296
|
+
fields: fields.map((f) => ({ name: f.name, dataType: f.data_type })),
|
|
297
|
+
itemCount: items.length,
|
|
298
|
+
columns: (statusField?.options ?? []).map((o) => ({ name: o.name, cards: items.filter((i) => optionOf(i)?.id === o.id).map(card) })),
|
|
299
|
+
noStatus: { name: 'No Status', cards: items.filter((i) => !optionOf(i)).map(card) },
|
|
300
|
+
url: `${GITHUB_WEB_ORIGIN}/${p.owner_type === 'organization' ? 'orgs/' : 'users/'}${p.owner}/projects/${p.number}`,
|
|
301
|
+
};
|
|
302
|
+
});
|
|
303
|
+
// ── Insights / Pulse / Contributors (DERIVED per repo from the modeled PRs/issues/commits) —
|
|
304
|
+
// the Pulse screen's counts (merged/opened PRs, opened/closed issues). Data-coupled: each new
|
|
305
|
+
// PR/issue moves a Pulse count.
|
|
306
|
+
const repoNames = new Set([...prs.map((p) => p.repository), ...issues.map((i) => i.repository), ...foldedRepos.map((r) => r.full_name)]);
|
|
307
|
+
const insights = [...repoNames].sort().map((repo) => {
|
|
308
|
+
const repoPrs = prs.filter((p) => p.repository === repo);
|
|
309
|
+
const repoIssues = issues.filter((i) => i.repository === repo);
|
|
310
|
+
return {
|
|
311
|
+
repo,
|
|
312
|
+
pulse: {
|
|
313
|
+
mergedPrs: repoPrs.filter((p) => p.merged).length,
|
|
314
|
+
openPrs: repoPrs.filter((p) => !p.merged && (p.state ?? 'open') === 'open').length,
|
|
315
|
+
openedIssues: repoIssues.filter((i) => (i.state ?? 'open') === 'open').length,
|
|
316
|
+
closedIssues: repoIssues.filter((i) => i.state === 'closed').length,
|
|
317
|
+
},
|
|
318
|
+
// commit authors live in the git plane, which this projection does not read
|
|
319
|
+
contributors: [],
|
|
320
|
+
};
|
|
321
|
+
});
|
|
322
|
+
// ── Settings (DERIVED per repo from the modeled repo config + collaborators/webhooks/branches)
|
|
323
|
+
// the repo Settings screens (General / Collaborators / Webhooks / Branches). Data-coupled:
|
|
324
|
+
// adding a collaborator or webhook moves the counts/rows the settings screen renders.
|
|
325
|
+
const settings = foldedRepos.map((r) => ({
|
|
326
|
+
repo: r.full_name,
|
|
327
|
+
general: { defaultBranch: r.default_branch, private: r.private ?? false, hasIssues: r.has_issues ?? true, hasProjects: r.has_projects ?? true, hasWiki: r.has_wiki ?? true, hasDiscussions: r.has_discussions ?? false, description: r.description ?? null },
|
|
328
|
+
collaborators: foldedCollaborators.filter((c) => c.repository === r.full_name).map((c) => ({ login: c.login, permission: c.permission })),
|
|
329
|
+
webhooks: foldedWebhooks.filter((h) => h.repository === r.full_name).map((h) => ({ id: h.id, url: h.url, active: h.active, events: h.events })),
|
|
330
|
+
branches: foldedBranches.filter((b) => b.repository === r.full_name).map((b) => ({ name: b.name, protected: b.protected ?? false })),
|
|
331
|
+
}));
|
|
332
|
+
return {
|
|
333
|
+
github: { pullRequests, issues: issuesOut, reviews, commits, comments, reviewComments, files, workflows, runs, jobs: runJobs, releases, tags: tagList, discussions, contents, notifications, deployments, environments, projectBoards, insights, settings },
|
|
334
|
+
};
|
|
335
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export declare function buildGithubMirrorClient(): Promise<string>;
|
|
2
|
+
/**
|
|
3
|
+
* Serve the GitHub UI mirror (React app) with the twin's own fetch adapter mounted beside it.
|
|
4
|
+
*
|
|
5
|
+
* Every data path on this host IS the twin: reads fold from stored state, writes go through
|
|
6
|
+
* the twin's real write path (webhooks emitted like every write), the doors answer — the
|
|
7
|
+
* exact closure `createGithubTwinServer` serves, so the mirror host and the API host cannot
|
|
8
|
+
* drift. There is no mirror-private route: `/api/state` (the old fold) answers with the
|
|
9
|
+
* vendor's own `Not Found` envelope, because GitHub has no such endpoint.
|
|
10
|
+
*/
|
|
11
|
+
export declare function createGithubMirrorServer(options: {
|
|
12
|
+
root?: string;
|
|
13
|
+
port?: number;
|
|
14
|
+
}): Promise<{
|
|
15
|
+
port: number;
|
|
16
|
+
stop: () => void;
|
|
17
|
+
}>;
|
|
18
|
+
export declare function githubMirrorHtml(): string;
|
|
19
|
+
/** The mirror's stylesheet, for a host serving the shell's `assets/styles.css` itself (the hosted mirror mount). */
|
|
20
|
+
export declare function githubMirrorStyles(): Promise<string>;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// GitHub UI mirror (scorecard R13) — a GitHub-style React app (Bun-bundled) over the
|
|
2
|
+
// twin's github world. A PURE FRONTEND (runtime contract R3): this module is the shell +
|
|
3
|
+
// its assets and one listener that mounts the pack's OWN fetch adapter beside them. It
|
|
4
|
+
// imports no handler and no twin-internal module, and holds no mirror-private data path:
|
|
5
|
+
// the client reads the world through the twin's store door (`GET /twin/store/mirror`, the
|
|
6
|
+
// ONE named projection `github-mirror-state.ts` builds server-side), and any write a
|
|
7
|
+
// console makes goes through the vendor's own REST API on the same origin — the wire a
|
|
8
|
+
// real GitHub client speaks, so the mirror renders a twin or a real account unchanged,
|
|
9
|
+
// pointed at any origin by configuration. The client is the shared world UI-mirror design.
|
|
10
|
+
import { readFile } from 'node:fs/promises';
|
|
11
|
+
import { bundleClient, fileResponse } from '@volter/world-core';
|
|
12
|
+
import { serveHttp } from '@volter/world-core';
|
|
13
|
+
import { createGithubTwinFetch } from "./github-server.js";
|
|
14
|
+
const CLIENT_ENTRY = () => new URL('../client/github-mirror.tsx', import.meta.url).pathname; // lazy: workerd rejects top-level relative URL from import.meta.url (bundled via pack index)
|
|
15
|
+
const CLIENT_CSS = () => new URL('../client/github-mirror.css', import.meta.url).pathname; // lazy: workerd rejects top-level relative URL from import.meta.url (bundled via pack index)
|
|
16
|
+
const APP_SHELL = `<!doctype html>
|
|
17
|
+
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
18
|
+
<base href="/"><title>GitHub UI mirror (twin)</title><link rel="stylesheet" href="assets/styles.css"></head>
|
|
19
|
+
<body><div id="root"></div><script type="module" src="assets/app.js"></script></body></html>`;
|
|
20
|
+
// cache: the built client bundle, memoized per process — a build artifact, never twin state (the tree
|
|
21
|
+
// holds what this UI renders); cleared on a failed build so the next request rebuilds.
|
|
22
|
+
let clientBundle = null;
|
|
23
|
+
export function buildGithubMirrorClient() {
|
|
24
|
+
if (!clientBundle) {
|
|
25
|
+
clientBundle = bundleClient(CLIENT_ENTRY())
|
|
26
|
+
.catch((error) => { clientBundle = null; throw error; });
|
|
27
|
+
}
|
|
28
|
+
return clientBundle;
|
|
29
|
+
}
|
|
30
|
+
// GitHub's REST surface and the console's own routes share ONE path grammar (real GitHub
|
|
31
|
+
// splits them across its API host and its web host): `/pulls`, `/notifications`,
|
|
32
|
+
// `/projects/…` and `/{owner}/{repo}/…` are both an API path and a console deep link. On
|
|
33
|
+
// this one host a request is the console's when it is a DOCUMENT NAVIGATION — the browser
|
|
34
|
+
// asks for `text/html` — and the twin's otherwise (an API client and the console's own
|
|
35
|
+
// store read ask for JSON), the ordinary history-fallback rule every SPA host applies.
|
|
36
|
+
function wantsDocument(request) {
|
|
37
|
+
return request.method === 'GET' && (request.headers.get('accept') ?? '').includes('text/html');
|
|
38
|
+
}
|
|
39
|
+
// The git smart-HTTP grammar (`/{owner}/{repo}/info/refs`, `…/git-upload-pack`,
|
|
40
|
+
// `…/git-receive-pack`): the git plane stays on the twin's API host — the console host
|
|
41
|
+
// carries no clone/push lane, so a git client here gets the plain refusal, never the shell.
|
|
42
|
+
function isGitSmartHttpPath(pathname) {
|
|
43
|
+
return /^\/[^/]+\/[^/]+\/(?:info\/refs|git-upload-pack|git-receive-pack)$/.test(pathname);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Serve the GitHub UI mirror (React app) with the twin's own fetch adapter mounted beside it.
|
|
47
|
+
*
|
|
48
|
+
* Every data path on this host IS the twin: reads fold from stored state, writes go through
|
|
49
|
+
* the twin's real write path (webhooks emitted like every write), the doors answer — the
|
|
50
|
+
* exact closure `createGithubTwinServer` serves, so the mirror host and the API host cannot
|
|
51
|
+
* drift. There is no mirror-private route: `/api/state` (the old fold) answers with the
|
|
52
|
+
* vendor's own `Not Found` envelope, because GitHub has no such endpoint.
|
|
53
|
+
*/
|
|
54
|
+
export async function createGithubMirrorServer(options) {
|
|
55
|
+
const twin = createGithubTwinFetch({ ...(options.root !== undefined ? { root: options.root } : {}) });
|
|
56
|
+
const server = await serveHttp({
|
|
57
|
+
// LOOPBACK-SPECIFIC bind (2026-08-20, the roving ui-verify flake): with the default
|
|
58
|
+
// wildcard hostname, `port: 0` can be handed a port some long-running app already LISTENS
|
|
59
|
+
// on at 127.0.0.1 (SO_REUSEADDR allows the overlapping non-identical bind), and the more
|
|
60
|
+
// specific loopback listener then shadows this server for every 127.0.0.1 fetch — the
|
|
61
|
+
// verify talks to a STRANGER (captured: a desktop app's asset server answering 404s on the
|
|
62
|
+
// mirror's port). Binding 127.0.0.1 makes the kernel allocate a port that is actually free
|
|
63
|
+
// on loopback, so the verify's fetches deterministically reach THIS server.
|
|
64
|
+
hostname: '127.0.0.1',
|
|
65
|
+
port: options.port ?? 0,
|
|
66
|
+
idleTimeout: 60,
|
|
67
|
+
async fetch(request) {
|
|
68
|
+
const url = new URL(request.url);
|
|
69
|
+
if (request.method === 'GET' && url.pathname === '/assets/app.js') {
|
|
70
|
+
try {
|
|
71
|
+
return new Response(await buildGithubMirrorClient(), { headers: { 'content-type': 'text/javascript; charset=utf-8' } });
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
return new Response(String(error), { status: 500 });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (request.method === 'GET' && url.pathname === '/assets/styles.css') {
|
|
78
|
+
return fileResponse(CLIENT_CSS(), { headers: { 'content-type': 'text/css; charset=utf-8' } });
|
|
79
|
+
}
|
|
80
|
+
// The uniform doors (`/twin`, `/twin/store/<name>`) are the twin's, whoever asks.
|
|
81
|
+
if (url.pathname === '/twin' || url.pathname.startsWith('/twin/'))
|
|
82
|
+
return twin(request);
|
|
83
|
+
if (isGitSmartHttpPath(url.pathname))
|
|
84
|
+
return new Response('not found', { status: 404 });
|
|
85
|
+
// The console: its root and every document navigation (deep links included).
|
|
86
|
+
if (request.method === 'GET' && (url.pathname === '/' || url.pathname === '' || wantsDocument(request))) {
|
|
87
|
+
return new Response(APP_SHELL, { headers: { 'content-type': 'text/html; charset=utf-8' } });
|
|
88
|
+
}
|
|
89
|
+
// Everything else is the vendor's wire: the mounted adapter answers as the API host would.
|
|
90
|
+
return twin(request);
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
return { port: server.port ?? options.port ?? 0, stop: () => server.stop(true) };
|
|
94
|
+
}
|
|
95
|
+
export function githubMirrorHtml() {
|
|
96
|
+
return APP_SHELL;
|
|
97
|
+
}
|
|
98
|
+
/** The mirror's stylesheet, for a host serving the shell's `assets/styles.css` itself (the hosted mirror mount). */
|
|
99
|
+
export function githubMirrorStyles() {
|
|
100
|
+
return readFile(CLIENT_CSS(), 'utf8');
|
|
101
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Options shared by the fetch handler and the Bun.serve wrapper around it. `port` is a BIND
|
|
2
|
+
* concern the fetch ignores; it stays in one shape so a caller configures the twin once
|
|
3
|
+
* whichever way it is mounted. */
|
|
4
|
+
export type GithubTwinOptions = {
|
|
5
|
+
root?: string;
|
|
6
|
+
port?: number;
|
|
7
|
+
readOnly?: boolean;
|
|
8
|
+
clock?: () => string;
|
|
9
|
+
};
|
|
10
|
+
export declare function createGithubTwinFetch(options: GithubTwinOptions): (request: Request) => Promise<Response>;
|
|
11
|
+
export declare function createGithubTwinServer(options: GithubTwinOptions): Promise<{
|
|
12
|
+
port: number;
|
|
13
|
+
stop: () => void;
|
|
14
|
+
}>;
|