ework-web 0.10.96 → 0.10.98

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.96",
3
+ "version": "0.10.98",
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 {
@@ -6,6 +6,7 @@ import {
6
6
  getIssueByUpstreamNumber,
7
7
  getCommentByUpstreamId,
8
8
  createIssue,
9
+ ensureUser,
9
10
  postComment,
10
11
  editIssue,
11
12
  updateUpstreamSyncState,
@@ -23,6 +24,7 @@ interface GiteaIssue {
23
24
  user: { login: string } | null;
24
25
  created_at: string;
25
26
  updated_at: string;
27
+ pull_request?: unknown;
26
28
  }
27
29
 
28
30
  interface GiteaComment {
@@ -42,6 +44,12 @@ export interface UpstreamSyncPollResult {
42
44
 
43
45
  const GITEA_COMMENT_ISSUE_RE = /\/issues\/(\d+)$/;
44
46
 
47
+ // GitHub CI/actions identities end in [bot]; their comments must not wake
48
+ // the engine, so they are imported as bot-kind users.
49
+ function fromGithubBot(login: string | undefined): boolean {
50
+ return typeof login === "string" && /\[bot\]$/i.test(login.trim());
51
+ }
52
+
45
53
  function upstreamIssueNumberFromComment(gc: GiteaComment): number | null {
46
54
  const m = (gc.issue_url ?? "").match(GITEA_COMMENT_ISSUE_RE);
47
55
  return m ? Number(m[1]) : null;
@@ -71,7 +79,11 @@ export class UpstreamSync {
71
79
  }
72
80
 
73
81
  private api(path: string): string {
74
- return `${this.sync.base_url}/api/v1/repos/${this.sync.upstream_owner}/${this.sync.upstream_repo}${path}`;
82
+ const base = /github\.com$/i.test(new URL(this.sync.base_url).host)
83
+ ? "https://api.github.com"
84
+ : this.sync.base_url;
85
+ const prefix = base === "https://api.github.com" ? "" : "/api/v1";
86
+ return `${base}${prefix}/repos/${this.sync.upstream_owner}/${this.sync.upstream_repo}${path}`;
75
87
  }
76
88
 
77
89
  private async fetchJson<T>(path: string): Promise<T | null> {
@@ -85,14 +97,23 @@ export class UpstreamSync {
85
97
  // comments silently so the AI isn't woken by a flood of historical entries.
86
98
  // Live polls (cursor set) emit webhooks so new upstream activity reaches
87
99
  // the daemon exactly like locally-created content.
100
+ private get isGithub(): boolean {
101
+ try {
102
+ return /github\.com$/i.test(new URL(this.sync.base_url).host);
103
+ } catch {
104
+ return false;
105
+ }
106
+ }
107
+
88
108
  private get isBackfill(): boolean {
89
109
  return this.sync.issue_cursor === null;
90
110
  }
91
111
 
92
112
  private async importIssue(gi: GiteaIssue, emit: boolean): Promise<void> {
113
+ await ensureUser(gi.user?.login ?? "upstream", fromGithubBot(gi.user?.login) ? "bot" : "human");
93
114
  await createIssue(
94
115
  this.project.id,
95
- gi.title || `#${gi.number}`,
116
+ (gi.pull_request ? "[PR] " : "") + (gi.title || `#${gi.number}`),
96
117
  gi.body ?? "",
97
118
  gi.user?.login ?? "upstream",
98
119
  {
@@ -119,14 +140,21 @@ export class UpstreamSync {
119
140
  }
120
141
 
121
142
  private async importIssueComments(gi: GiteaIssue, emit: boolean): Promise<number> {
122
- const comments = await this.fetchJson<GiteaComment[]>(`/issues/${gi.number}/comments?limit=50&order=asc`);
143
+ const comments = await this.fetchJson<GiteaComment[]>(this.isGithub
144
+ ? `/issues/${gi.number}/comments?per_page=50`
145
+ : `/issues/${gi.number}/comments?limit=50&order=asc`);
123
146
  if (!comments) return 0;
124
147
  const local = await getIssueByUpstreamNumber(this.project.id, gi.number);
125
148
  if (!local) return 0;
126
149
  let n = 0;
127
150
  for (const gc of comments) {
128
151
  if (await getCommentByUpstreamId(gc.id)) continue;
129
- const row = await postComment(local.id, gc.body ?? "", gc.user?.login ?? "upstream", {
152
+ // write-back comments carry this marker; importing them would duplicate locally
153
+ if ((gc.body ?? "").includes("<!-- ework-mirror -->")) continue;
154
+ const gcAuthor = gc.user?.login ?? "upstream";
155
+ // ensure the kind before postComment's internal user creation runs
156
+ await ensureUser(gcAuthor, fromGithubBot(gcAuthor) ? "bot" : "human");
157
+ const row = await postComment(local.id, gc.body ?? "", gcAuthor, {
130
158
  createdAt: gc.created_at,
131
159
  updatedAt: gc.updated_at,
132
160
  upstreamCommentId: gc.id,
@@ -142,7 +170,9 @@ export class UpstreamSync {
142
170
  let cursor: string | null = null;
143
171
  for (let page = 1; page <= 20; page++) {
144
172
  const issues = await this.fetchJson<GiteaIssue[]>(
145
- `/issues?state=open&type=issues&limit=50&page=${page}&sort=created&order=asc`
173
+ this.isGithub
174
+ ? `/issues?state=open&per_page=50&page=${page}&sort=created&direction=asc`
175
+ : `/issues?state=open&type=issues&limit=50&page=${page}&sort=created&order=asc`
146
176
  );
147
177
  if (!issues || issues.length === 0) break;
148
178
  for (const gi of issues) {
@@ -166,7 +196,9 @@ export class UpstreamSync {
166
196
  private async liveOnce(): Promise<UpstreamSyncPollResult> {
167
197
  const result: UpstreamSyncPollResult = { issuesImported: 0, issuesUpdated: 0, commentsImported: 0 };
168
198
  const issues = await this.fetchJson<GiteaIssue[]>(
169
- `/issues?state=all&type=issues&limit=30&sort=updated&order=desc`
199
+ this.isGithub
200
+ ? `/issues?state=all&per_page=30&sort=updated&direction=desc`
201
+ : `/issues?state=all&type=issues&limit=30&sort=updated&order=desc`
170
202
  );
171
203
  let issueCursor = this.sync.issue_cursor;
172
204
  if (issues) {
@@ -186,18 +218,23 @@ export class UpstreamSync {
186
218
 
187
219
  let commentCursor = this.sync.comment_cursor;
188
220
  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
- }`
221
+ this.isGithub
222
+ ? `/issues/comments?per_page=100${this.sync.comment_cursor ? `&since=${encodeURIComponent(this.sync.comment_cursor)}` : ""}`
223
+ : `/issues/comments?limit=50&sort=updated&order=asc${
224
+ this.sync.comment_cursor ? `&since=${encodeURIComponent(this.sync.comment_cursor)}` : ""
225
+ }`
192
226
  );
193
227
  if (comments) {
194
228
  for (const gc of comments) {
195
229
  if (await getCommentByUpstreamId(gc.id)) continue;
230
+ if ((gc.body ?? "").includes("<!-- ework-mirror -->")) continue;
196
231
  const upstreamNumber = upstreamIssueNumberFromComment(gc);
197
232
  if (!upstreamNumber) continue;
198
233
  const local = await getIssueByUpstreamNumber(this.project.id, upstreamNumber);
199
234
  if (!local) continue;
200
- const row = await postComment(local.id, gc.body ?? "", gc.user?.login ?? "upstream", {
235
+ const gcAuthor = gc.user?.login ?? "upstream";
236
+ await ensureUser(gcAuthor, fromGithubBot(gcAuthor) ? "bot" : "human");
237
+ const row = await postComment(local.id, gc.body ?? "", gcAuthor, {
201
238
  createdAt: gc.created_at,
202
239
  updatedAt: gc.updated_at,
203
240
  upstreamCommentId: gc.id,
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,