ework-web 0.10.95 → 0.10.97

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ework-web",
3
- "version": "0.10.95",
3
+ "version": "0.10.97",
4
4
  "type": "module",
5
5
  "description": "ework-web — standalone multi-project issue tracker. Local SQLite-backed, no external API dependency. Bun + TypeScript + SSR HTML.",
6
6
  "license": "MIT",
package/src/store.ts CHANGED
@@ -76,6 +76,7 @@ export interface IssueRow {
76
76
  ai_status: string;
77
77
  model: string;
78
78
  runtime: string;
79
+ upstream_issue_number: number | null;
79
80
  }
80
81
 
81
82
  export interface IssueWithMeta extends IssueRow {
@@ -23,6 +23,7 @@ interface GiteaIssue {
23
23
  user: { login: string } | null;
24
24
  created_at: string;
25
25
  updated_at: string;
26
+ pull_request?: unknown;
26
27
  }
27
28
 
28
29
  interface GiteaComment {
@@ -71,7 +72,11 @@ export class UpstreamSync {
71
72
  }
72
73
 
73
74
  private api(path: string): string {
74
- return `${this.sync.base_url}/api/v1/repos/${this.sync.upstream_owner}/${this.sync.upstream_repo}${path}`;
75
+ const base = /github\.com$/i.test(new URL(this.sync.base_url).host)
76
+ ? "https://api.github.com"
77
+ : this.sync.base_url;
78
+ const prefix = base === "https://api.github.com" ? "" : "/api/v1";
79
+ return `${base}${prefix}/repos/${this.sync.upstream_owner}/${this.sync.upstream_repo}${path}`;
75
80
  }
76
81
 
77
82
  private async fetchJson<T>(path: string): Promise<T | null> {
@@ -85,6 +90,14 @@ export class UpstreamSync {
85
90
  // comments silently so the AI isn't woken by a flood of historical entries.
86
91
  // Live polls (cursor set) emit webhooks so new upstream activity reaches
87
92
  // the daemon exactly like locally-created content.
93
+ private get isGithub(): boolean {
94
+ try {
95
+ return /github\.com$/i.test(new URL(this.sync.base_url).host);
96
+ } catch {
97
+ return false;
98
+ }
99
+ }
100
+
88
101
  private get isBackfill(): boolean {
89
102
  return this.sync.issue_cursor === null;
90
103
  }
@@ -92,7 +105,7 @@ export class UpstreamSync {
92
105
  private async importIssue(gi: GiteaIssue, emit: boolean): Promise<void> {
93
106
  await createIssue(
94
107
  this.project.id,
95
- gi.title || `#${gi.number}`,
108
+ (gi.pull_request ? "[PR] " : "") + (gi.title || `#${gi.number}`),
96
109
  gi.body ?? "",
97
110
  gi.user?.login ?? "upstream",
98
111
  {
@@ -119,13 +132,17 @@ export class UpstreamSync {
119
132
  }
120
133
 
121
134
  private async importIssueComments(gi: GiteaIssue, emit: boolean): Promise<number> {
122
- const comments = await this.fetchJson<GiteaComment[]>(`/issues/${gi.number}/comments?limit=50&order=asc`);
135
+ const comments = await this.fetchJson<GiteaComment[]>(this.isGithub
136
+ ? `/issues/${gi.number}/comments?per_page=50`
137
+ : `/issues/${gi.number}/comments?limit=50&order=asc`);
123
138
  if (!comments) return 0;
124
139
  const local = await getIssueByUpstreamNumber(this.project.id, gi.number);
125
140
  if (!local) return 0;
126
141
  let n = 0;
127
142
  for (const gc of comments) {
128
143
  if (await getCommentByUpstreamId(gc.id)) continue;
144
+ // write-back comments carry this marker; importing them would duplicate locally
145
+ if ((gc.body ?? "").includes("<!-- ework-mirror -->")) continue;
129
146
  const row = await postComment(local.id, gc.body ?? "", gc.user?.login ?? "upstream", {
130
147
  createdAt: gc.created_at,
131
148
  updatedAt: gc.updated_at,
@@ -142,7 +159,9 @@ export class UpstreamSync {
142
159
  let cursor: string | null = null;
143
160
  for (let page = 1; page <= 20; page++) {
144
161
  const issues = await this.fetchJson<GiteaIssue[]>(
145
- `/issues?state=open&type=issues&limit=50&page=${page}&sort=created&order=asc`
162
+ this.isGithub
163
+ ? `/issues?state=open&per_page=50&page=${page}&sort=created&direction=asc`
164
+ : `/issues?state=open&type=issues&limit=50&page=${page}&sort=created&order=asc`
146
165
  );
147
166
  if (!issues || issues.length === 0) break;
148
167
  for (const gi of issues) {
@@ -166,7 +185,9 @@ export class UpstreamSync {
166
185
  private async liveOnce(): Promise<UpstreamSyncPollResult> {
167
186
  const result: UpstreamSyncPollResult = { issuesImported: 0, issuesUpdated: 0, commentsImported: 0 };
168
187
  const issues = await this.fetchJson<GiteaIssue[]>(
169
- `/issues?state=all&type=issues&limit=30&sort=updated&order=desc`
188
+ this.isGithub
189
+ ? `/issues?state=all&per_page=30&sort=updated&direction=desc`
190
+ : `/issues?state=all&type=issues&limit=30&sort=updated&order=desc`
170
191
  );
171
192
  let issueCursor = this.sync.issue_cursor;
172
193
  if (issues) {
@@ -186,13 +207,16 @@ export class UpstreamSync {
186
207
 
187
208
  let commentCursor = this.sync.comment_cursor;
188
209
  const comments = await this.fetchJson<GiteaComment[]>(
189
- `/issues/comments?limit=50&sort=updated&order=asc${
190
- this.sync.comment_cursor ? `&since=${encodeURIComponent(this.sync.comment_cursor)}` : ""
191
- }`
210
+ this.isGithub
211
+ ? `/issues/comments?per_page=100${this.sync.comment_cursor ? `&since=${encodeURIComponent(this.sync.comment_cursor)}` : ""}`
212
+ : `/issues/comments?limit=50&sort=updated&order=asc${
213
+ this.sync.comment_cursor ? `&since=${encodeURIComponent(this.sync.comment_cursor)}` : ""
214
+ }`
192
215
  );
193
216
  if (comments) {
194
217
  for (const gc of comments) {
195
218
  if (await getCommentByUpstreamId(gc.id)) continue;
219
+ if ((gc.body ?? "").includes("<!-- ework-mirror -->")) continue;
196
220
  const upstreamNumber = upstreamIssueNumberFromComment(gc);
197
221
  if (!upstreamNumber) continue;
198
222
  const local = await getIssueByUpstreamNumber(this.project.id, upstreamNumber);
@@ -17,6 +17,7 @@ import {
17
17
  listCachedModels,
18
18
  type CommentRow,
19
19
  type IssueWithMeta,
20
+ type ProjectRow,
20
21
  } from "../store";
21
22
  import { webUrlFromClone } from "./projectUpstreams";
22
23
 
@@ -37,6 +38,22 @@ export interface IssueThreadPayload {
37
38
  comments: CommentView[];
38
39
  }
39
40
 
41
+ // Issues in a project cloned from an upstream are that upstream's issues:
42
+ // #204 in a github-mirrored repo means github.com/o/r/issues/204. Normalize
43
+ // the configured remote (https/git@/ssh/git forms) to its web base; projects
44
+ // without an upstream keep resolving refs against this ework install.
45
+ export function upstreamRefBase(project: Pick<ProjectRow, "upstream_urls">): string | null {
46
+ const url = getDefaultUpstreamUrl(project);
47
+ if (!url) return null;
48
+ let m = url.match(/^git@([^:]+):(.+)$/);
49
+ if (m?.[1] && m[2]) return `https://${m[1]}/${m[2].replace(/\.git$/, "")}`;
50
+ m = url.match(/^(?:ssh|git):\/\/(?:git@)?([^\/]+)\/(.+)$/);
51
+ if (m?.[1] && m[2]) return `https://${m[1]}/${m[2].replace(/\.git$/, "")}`;
52
+ m = url.match(/^https?:\/\/([^\/]+)\/(.+)$/);
53
+ if (m?.[1] && m[2]) return `https://${m[1]}/${m[2].replace(/\.git$/, "")}`;
54
+ return null;
55
+ }
56
+
40
57
  function toView(c: CommentRow, issuePath = ""): CommentView {
41
58
  return {
42
59
  id: c.id,
@@ -105,7 +122,7 @@ export async function buildIssueThread(
105
122
  const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE));
106
123
  const currentPage = totalPages;
107
124
  const { rows } = await listCommentsPage(issue.id, currentPage, PAGE_SIZE);
108
- const issuePath = `/${owner}/${repo}/issues/${issue.number}`;
125
+ const issuePath = `${upstreamRefBase(project) ?? `/${owner}/${repo}`}/issues/${issue.number}`;
109
126
  const views = await viewsFromComments(rows, issuePath);
110
127
  const hasOlder = currentPage > 1;
111
128
  const displayViews = orderForDisplay(views, cfg.commentSort);
@@ -188,7 +205,7 @@ export async function fetchIssuePage(
188
205
  const issue = await getIssueWithMeta(project.id, number);
189
206
  if (!issue) throw new StoreError(404, `#${number} 不存在`);
190
207
  const { rows, page: clamped } = await listCommentsPage(issue.id, page, PAGE_SIZE);
191
- const views = await viewsFromComments(rows, `/${owner}/${repo}/issues/${issue.number}`);
208
+ const views = await viewsFromComments(rows, `${upstreamRefBase(project) ?? `/${owner}/${repo}`}/issues/${issue.number}`);
192
209
  return { issue, views, currentPage: clamped, hasOlder: clamped > 1 };
193
210
  }
194
211
 
@@ -203,7 +220,7 @@ export async function fetchIssueSince(
203
220
  const issue = await getIssueWithMeta(project.id, number);
204
221
  if (!issue) throw new StoreError(404, `#${number} 不存在`);
205
222
  const rows = await listCommentsSince(issue.id, sinceISO);
206
- return await viewsFromComments(rows, `/${owner}/${repo}/issues/${issue.number}`);
223
+ return await viewsFromComments(rows, `${upstreamRefBase(project) ?? `/${owner}/${repo}`}/issues/${issue.number}`);
207
224
  }
208
225
 
209
226
  export function safeJsonEmbed(v: unknown): string {
package/src/webhooks.ts CHANGED
@@ -277,6 +277,7 @@ interface PayloadIssue {
277
277
  url: string;
278
278
  html_url: string;
279
279
  number: number;
280
+ upstream_issue_number?: number | null;
280
281
  title: string;
281
282
  body: string;
282
283
  labels: PayloadLabel[];
@@ -429,6 +430,7 @@ function buildIssue(
429
430
  url: `${origin}/api/v1/repos/${encodeURIComponent(project.owner)}/${encodeURIComponent(project.name)}/issues/${issue.number}`,
430
431
  html_url: issueUrl,
431
432
  number: issue.number,
433
+ upstream_issue_number: issue.upstream_issue_number ?? null,
432
434
  title: issue.title,
433
435
  body: issue.body ?? "",
434
436
  labels,