jowork 0.2.4 → 0.3.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/dist/{chunk-ROIINI33.js → chunk-4PIT2GZ4.js} +13 -1
- package/dist/{chunk-XLYRHKG6.js → chunk-54SD5GBF.js} +1 -1
- package/dist/chunk-63AMINQC.js +156 -0
- package/dist/{chunk-XAEGXSEO.js → chunk-74AHY7X6.js} +4 -0
- package/dist/{chunk-7U3SXINY.js → chunk-ATAUWJYD.js} +320 -50
- package/dist/chunk-DQW74UCN.js +671 -0
- package/dist/chunk-EYP6WMFF.js +153 -0
- package/dist/{chunk-JSTXMDXI.js → chunk-FCFZCZHR.js} +1 -1
- package/dist/chunk-FX6Z3QHV.js +34 -0
- package/dist/chunk-HENAABEL.js +419 -0
- package/dist/chunk-OXWWOKC7.js +201 -0
- package/dist/chunk-QGHJ45PL.js +661 -0
- package/dist/chunk-RO3KK5RC.js +132 -0
- package/dist/{chunk-JE6TOU7W.js → chunk-TFMF3EXE.js} +2 -7
- package/dist/{chunk-TN327MDF.js → chunk-VX662YLA.js} +3 -3
- package/dist/cli.js +338 -149
- package/dist/{config-AI6UIJJN.js → config-FH2XLN7A.js} +2 -2
- package/dist/content-reader-VPGTR2SF.js +10 -0
- package/dist/context-ZNI3WOB7.js +10 -0
- package/dist/{credential-store-ZRZCSRPC.js → credential-store-OS5ZY4OW.js} +2 -2
- package/dist/{feishu-A6YVFKEN.js → feishu-XW5T6ER2.js} +8 -3
- package/dist/{git-manager-N35XSG4Y.js → git-manager-RVWV2GSV.js} +2 -1
- package/dist/github-PQKAYTLO.js +11 -0
- package/dist/{paths-JXOMBYIT.js → paths-FFRET6F7.js} +7 -3
- package/dist/{server-5GVWN2NB.js → server-WEADPUST.js} +59 -66
- package/dist/{setup-IDQDPCEJ.js → setup-S2S2CHB2.js} +91 -32
- package/dist/sync-SRLFR5NA.js +21 -0
- package/dist/transport.js +6 -4
- package/package.json +1 -1
- package/src/dashboard/public/app.js +34 -8
- package/src/dashboard/public/style.css +14 -0
- package/dist/chunk-AIXKXEYS.js +0 -547
- package/dist/chunk-L5ZR7TSK.js +0 -82
- package/dist/chunk-LS2AJM5A.js +0 -163
- package/dist/chunk-QMOFQX7X.js +0 -612
- package/dist/chunk-YJWTKFWX.js +0 -451
- package/dist/github-SHWUFNYB.js +0 -10
- package/dist/sync-7V54N62M.js +0 -18
|
@@ -0,0 +1,661 @@
|
|
|
1
|
+
import {
|
|
2
|
+
linkAllUnprocessed,
|
|
3
|
+
syncGitLab,
|
|
4
|
+
syncLinear
|
|
5
|
+
} from "./chunk-HENAABEL.js";
|
|
6
|
+
import {
|
|
7
|
+
syncGitHub
|
|
8
|
+
} from "./chunk-63AMINQC.js";
|
|
9
|
+
import {
|
|
10
|
+
GitManager
|
|
11
|
+
} from "./chunk-EYP6WMFF.js";
|
|
12
|
+
import {
|
|
13
|
+
DbManager
|
|
14
|
+
} from "./chunk-74AHY7X6.js";
|
|
15
|
+
import {
|
|
16
|
+
listCredentials,
|
|
17
|
+
loadCredential
|
|
18
|
+
} from "./chunk-54SD5GBF.js";
|
|
19
|
+
import {
|
|
20
|
+
dbPath,
|
|
21
|
+
fileRepoDir
|
|
22
|
+
} from "./chunk-4PIT2GZ4.js";
|
|
23
|
+
import {
|
|
24
|
+
syncFeishu,
|
|
25
|
+
syncFeishuApprovals,
|
|
26
|
+
syncFeishuDocs,
|
|
27
|
+
syncFeishuLinks,
|
|
28
|
+
syncFeishuMeetings
|
|
29
|
+
} from "./chunk-DQW74UCN.js";
|
|
30
|
+
import {
|
|
31
|
+
formatAnalytics,
|
|
32
|
+
formatMessages
|
|
33
|
+
} from "./chunk-RO3KK5RC.js";
|
|
34
|
+
import {
|
|
35
|
+
logError,
|
|
36
|
+
logInfo
|
|
37
|
+
} from "./chunk-MYDK7MWB.js";
|
|
38
|
+
import {
|
|
39
|
+
SyncContext
|
|
40
|
+
} from "./chunk-OXWWOKC7.js";
|
|
41
|
+
|
|
42
|
+
// src/commands/sync.ts
|
|
43
|
+
import { existsSync as existsSync2 } from "fs";
|
|
44
|
+
|
|
45
|
+
// src/sync/posthog.ts
|
|
46
|
+
var defaultLogger = {
|
|
47
|
+
info: (msg, ctx) => logInfo("sync", msg, ctx),
|
|
48
|
+
warn: (msg, ctx) => logError("sync", msg, ctx),
|
|
49
|
+
error: (msg, ctx) => logError("sync", msg, ctx)
|
|
50
|
+
};
|
|
51
|
+
async function syncPostHog(ctx, data, logger = defaultLogger) {
|
|
52
|
+
const { apiKey, host, projectId: rawProjectId } = data;
|
|
53
|
+
if (!apiKey) throw new Error("Missing PostHog API key");
|
|
54
|
+
const baseUrl = host || "https://app.posthog.com";
|
|
55
|
+
const projectId = rawProjectId || "1";
|
|
56
|
+
const headers = {
|
|
57
|
+
Authorization: `Bearer ${apiKey}`,
|
|
58
|
+
"Content-Type": "application/json"
|
|
59
|
+
};
|
|
60
|
+
let events = 0, insights = 0, newObjects = 0, updatedObjects = 0;
|
|
61
|
+
try {
|
|
62
|
+
let insightsUrl = `${baseUrl}/api/projects/${projectId}/insights/?limit=100`;
|
|
63
|
+
const allInsights = [];
|
|
64
|
+
while (insightsUrl) {
|
|
65
|
+
const insightsRes = await fetch(insightsUrl, { headers });
|
|
66
|
+
if (!insightsRes.ok) {
|
|
67
|
+
logger.warn(`Failed to fetch insights: ${insightsRes.status}`);
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
const page = await insightsRes.json();
|
|
71
|
+
allInsights.push(...page.results ?? []);
|
|
72
|
+
insightsUrl = page.next;
|
|
73
|
+
if (insightsUrl) await new Promise((r) => setTimeout(r, 200));
|
|
74
|
+
}
|
|
75
|
+
if (allInsights.length > 0) {
|
|
76
|
+
const items = [];
|
|
77
|
+
for (const insight of allInsights) {
|
|
78
|
+
const uri = `posthog://insight/${insight.id}`;
|
|
79
|
+
const summary = insight.description || `Insight: ${insight.name}`;
|
|
80
|
+
const body = JSON.stringify({
|
|
81
|
+
name: insight.name,
|
|
82
|
+
description: insight.description,
|
|
83
|
+
filters: insight.filters,
|
|
84
|
+
lastRefresh: insight.last_refresh
|
|
85
|
+
}, null, 2);
|
|
86
|
+
const fileContent = formatAnalytics({
|
|
87
|
+
name: insight.name,
|
|
88
|
+
description: insight.description,
|
|
89
|
+
filters: insight.filters,
|
|
90
|
+
lastRefresh: insight.last_refresh
|
|
91
|
+
});
|
|
92
|
+
items.push({
|
|
93
|
+
source: "posthog",
|
|
94
|
+
sourceType: "insight",
|
|
95
|
+
uri,
|
|
96
|
+
title: insight.name,
|
|
97
|
+
summary,
|
|
98
|
+
tags: ["posthog", "insight", ...Object.keys(insight.filters).slice(0, 3)],
|
|
99
|
+
content: body,
|
|
100
|
+
contentType: "application/json",
|
|
101
|
+
fileContent,
|
|
102
|
+
fileMeta: { title: insight.name }
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
const result = ctx.batchUpsert(items);
|
|
106
|
+
newObjects += result.inserted;
|
|
107
|
+
updatedObjects += result.updated;
|
|
108
|
+
insights += items.length;
|
|
109
|
+
logger.info(`Synced ${insights} insights`);
|
|
110
|
+
}
|
|
111
|
+
} catch (err) {
|
|
112
|
+
logger.error(`Insights sync error: ${err}`);
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
let eventsUrl = `${baseUrl}/api/projects/${projectId}/event_definitions/?limit=100`;
|
|
116
|
+
const allEvents = [];
|
|
117
|
+
while (eventsUrl) {
|
|
118
|
+
const eventsRes = await fetch(eventsUrl, { headers });
|
|
119
|
+
if (!eventsRes.ok) break;
|
|
120
|
+
const page = await eventsRes.json();
|
|
121
|
+
allEvents.push(...page.results ?? []);
|
|
122
|
+
eventsUrl = page.next;
|
|
123
|
+
if (eventsUrl) await new Promise((r) => setTimeout(r, 200));
|
|
124
|
+
}
|
|
125
|
+
if (allEvents.length > 0) {
|
|
126
|
+
const items = [];
|
|
127
|
+
for (const event of allEvents) {
|
|
128
|
+
const uri = `posthog://event/${event.name}`;
|
|
129
|
+
const summary = `${event.name}: ${event.description ?? "no description"} (30d volume: ${event.volume_30_day ?? "N/A"})`;
|
|
130
|
+
const body = JSON.stringify(event, null, 2);
|
|
131
|
+
const fileContent = formatAnalytics(event);
|
|
132
|
+
items.push({
|
|
133
|
+
source: "posthog",
|
|
134
|
+
sourceType: "event_definition",
|
|
135
|
+
uri,
|
|
136
|
+
title: event.name,
|
|
137
|
+
summary,
|
|
138
|
+
tags: ["posthog", "event_definition"],
|
|
139
|
+
content: body,
|
|
140
|
+
contentType: "application/json",
|
|
141
|
+
fileContent,
|
|
142
|
+
fileMeta: { title: event.name }
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
const result = ctx.batchUpsert(items);
|
|
146
|
+
newObjects += result.inserted;
|
|
147
|
+
updatedObjects += result.updated;
|
|
148
|
+
events += items.length;
|
|
149
|
+
logger.info(`Synced ${events} event definitions`);
|
|
150
|
+
}
|
|
151
|
+
} catch (err) {
|
|
152
|
+
logger.error(`Events sync error: ${err}`);
|
|
153
|
+
}
|
|
154
|
+
try {
|
|
155
|
+
let actionsUrl = `${baseUrl}/api/projects/${projectId}/actions/?limit=100`;
|
|
156
|
+
while (actionsUrl) {
|
|
157
|
+
const actionsRes = await fetch(actionsUrl, { headers });
|
|
158
|
+
if (!actionsRes.ok) break;
|
|
159
|
+
const page = await actionsRes.json();
|
|
160
|
+
const items = [];
|
|
161
|
+
for (const action of page.results ?? []) {
|
|
162
|
+
const uri = `posthog://action/${action.id}`;
|
|
163
|
+
const summary = action.description || `Action: ${action.name}`;
|
|
164
|
+
const body = JSON.stringify(action, null, 2);
|
|
165
|
+
const fileContent = formatAnalytics(action);
|
|
166
|
+
items.push({
|
|
167
|
+
source: "posthog",
|
|
168
|
+
sourceType: "action",
|
|
169
|
+
uri,
|
|
170
|
+
title: action.name,
|
|
171
|
+
summary,
|
|
172
|
+
tags: ["posthog", "action"],
|
|
173
|
+
content: body,
|
|
174
|
+
contentType: "application/json",
|
|
175
|
+
createdAt: new Date(action.created_at).getTime(),
|
|
176
|
+
fileContent,
|
|
177
|
+
fileMeta: { title: action.name }
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
const result = ctx.batchUpsert(items);
|
|
181
|
+
newObjects += result.inserted;
|
|
182
|
+
updatedObjects += result.updated;
|
|
183
|
+
actionsUrl = page.next;
|
|
184
|
+
if (actionsUrl) await new Promise((r) => setTimeout(r, 200));
|
|
185
|
+
}
|
|
186
|
+
} catch (err) {
|
|
187
|
+
logger.warn(`Actions sync error (non-fatal): ${err}`);
|
|
188
|
+
}
|
|
189
|
+
ctx.saveTimestampCursor("posthog:insights");
|
|
190
|
+
ctx.saveTimestampCursor("posthog:events");
|
|
191
|
+
logger.info("PostHog sync complete", { events, insights, newObjects, updatedObjects });
|
|
192
|
+
return { events, insights, newObjects, updatedObjects };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// src/sync/firebase.ts
|
|
196
|
+
var defaultLogger2 = {
|
|
197
|
+
info: (msg, ctx) => logInfo("sync", msg, ctx),
|
|
198
|
+
warn: (msg, ctx) => logError("sync", msg, ctx),
|
|
199
|
+
error: (msg, ctx) => logError("sync", msg, ctx)
|
|
200
|
+
};
|
|
201
|
+
async function syncFirebase(ctx, data, logger = defaultLogger2) {
|
|
202
|
+
const { projectId, apiKey } = data;
|
|
203
|
+
if (!projectId) throw new Error("Missing Firebase project ID");
|
|
204
|
+
let events = 0, newObjects = 0, updatedObjects = 0;
|
|
205
|
+
if (apiKey) {
|
|
206
|
+
try {
|
|
207
|
+
const propertyId = data.propertyId ?? projectId;
|
|
208
|
+
const res = await fetch(
|
|
209
|
+
`https://analyticsdata.googleapis.com/v1beta/properties/${propertyId}:runReport?key=${apiKey}`,
|
|
210
|
+
{
|
|
211
|
+
method: "POST",
|
|
212
|
+
headers: { "Content-Type": "application/json" },
|
|
213
|
+
body: JSON.stringify({
|
|
214
|
+
dateRanges: [{ startDate: "30daysAgo", endDate: "today" }],
|
|
215
|
+
dimensions: [{ name: "eventName" }],
|
|
216
|
+
metrics: [{ name: "eventCount" }, { name: "totalUsers" }],
|
|
217
|
+
limit: 500
|
|
218
|
+
})
|
|
219
|
+
}
|
|
220
|
+
);
|
|
221
|
+
if (res.ok) {
|
|
222
|
+
const report = await res.json();
|
|
223
|
+
const items = [];
|
|
224
|
+
for (const row of report.rows ?? []) {
|
|
225
|
+
const eventName = row.dimensionValues[0]?.value ?? "unknown";
|
|
226
|
+
const eventCount = parseInt(row.metricValues[0]?.value ?? "0");
|
|
227
|
+
const totalUsers = parseInt(row.metricValues[1]?.value ?? "0");
|
|
228
|
+
const uri = `firebase://${projectId}/event/${eventName}`;
|
|
229
|
+
const summary = `${eventName}: ${eventCount} events, ${totalUsers} users (last 30 days)`;
|
|
230
|
+
const body = JSON.stringify({ eventName, eventCount, totalUsers, period: "30daysAgo..today" }, null, 2);
|
|
231
|
+
const fileContent = formatAnalytics({ eventName, eventCount, totalUsers, period: "30daysAgo..today" });
|
|
232
|
+
items.push({
|
|
233
|
+
source: "firebase",
|
|
234
|
+
sourceType: "analytics_event",
|
|
235
|
+
uri,
|
|
236
|
+
title: eventName,
|
|
237
|
+
summary,
|
|
238
|
+
tags: ["firebase", "analytics", "event"],
|
|
239
|
+
content: body,
|
|
240
|
+
contentType: "application/json",
|
|
241
|
+
fileContent,
|
|
242
|
+
fileMeta: { title: eventName }
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
const result = ctx.batchUpsert(items);
|
|
246
|
+
newObjects += result.inserted;
|
|
247
|
+
updatedObjects += result.updated;
|
|
248
|
+
events += items.length;
|
|
249
|
+
} else {
|
|
250
|
+
logger.warn(`Firebase Analytics API: ${res.status}`);
|
|
251
|
+
}
|
|
252
|
+
} catch (err) {
|
|
253
|
+
logger.error(`Firebase sync error: ${err}`);
|
|
254
|
+
}
|
|
255
|
+
} else {
|
|
256
|
+
logger.warn("Firebase sync requires apiKey. Provide via jowork connect firebase --api-key <key>");
|
|
257
|
+
}
|
|
258
|
+
ctx.saveTimestampCursor("firebase:analytics");
|
|
259
|
+
logger.info("Firebase sync complete", { events, newObjects, updatedObjects });
|
|
260
|
+
return { events, newObjects, updatedObjects };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// src/sync/file-writer.ts
|
|
264
|
+
import { writeFileSync, readFileSync, appendFileSync, existsSync, mkdirSync, chmodSync } from "fs";
|
|
265
|
+
import { join, dirname } from "path";
|
|
266
|
+
|
|
267
|
+
// src/utils/slugify.ts
|
|
268
|
+
function slugify(name) {
|
|
269
|
+
return name.replace(/[\/\\:*?"<>|]/g, "-").replace(/\s+/g, "-").replace(/--+/g, "-").replace(/^-|-$/g, "").slice(0, 100) || "untitled";
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// src/sync/sanitizer.ts
|
|
273
|
+
var SENSITIVE_PATTERNS = [
|
|
274
|
+
/Bearer [A-Za-z0-9\-._~+/]+=*/g,
|
|
275
|
+
/[A-Za-z0-9+/]{40,}/g,
|
|
276
|
+
/sk-[a-zA-Z0-9]{20,}/g,
|
|
277
|
+
/ghp_[a-zA-Z0-9]{36}/g,
|
|
278
|
+
/xoxb-[0-9]{10,}-[a-zA-Z0-9]{20,}/g,
|
|
279
|
+
/glpat-[a-zA-Z0-9\-]{20,}/g,
|
|
280
|
+
/-----BEGIN (RSA |EC )?PRIVATE KEY-----[\s\S]*?-----END/g
|
|
281
|
+
];
|
|
282
|
+
function sanitizeContent(content) {
|
|
283
|
+
let result = content;
|
|
284
|
+
for (const pattern of SENSITIVE_PATTERNS) {
|
|
285
|
+
result = result.replace(pattern, "[REDACTED]");
|
|
286
|
+
}
|
|
287
|
+
return result;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// src/sync/file-writer.ts
|
|
291
|
+
var FileWriter = class {
|
|
292
|
+
repoDir;
|
|
293
|
+
constructor(repoDir) {
|
|
294
|
+
this.repoDir = repoDir ?? fileRepoDir();
|
|
295
|
+
try {
|
|
296
|
+
chmodSync(this.repoDir, 448);
|
|
297
|
+
} catch {
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
/** Write an object to a file. Returns the relative path from repoDir. */
|
|
301
|
+
writeObject(source, sourceType, meta, content) {
|
|
302
|
+
const filePath = this.getFilePath(source, sourceType, meta);
|
|
303
|
+
const absPath = join(this.repoDir, filePath);
|
|
304
|
+
mkdirSync(dirname(absPath), { recursive: true });
|
|
305
|
+
writeFileSync(absPath, sanitizeContent(content));
|
|
306
|
+
return filePath;
|
|
307
|
+
}
|
|
308
|
+
/** Append messages to a day file (for message-type sources). */
|
|
309
|
+
appendMessages(source, chatName, chatId, date, messages) {
|
|
310
|
+
const dir = join(source, "messages", slugify(chatName));
|
|
311
|
+
const filePath = join(dir, `${date}.md`);
|
|
312
|
+
const absPath = join(this.repoDir, filePath);
|
|
313
|
+
mkdirSync(dirname(absPath), { recursive: true });
|
|
314
|
+
let existingHeaders = [];
|
|
315
|
+
if (existsSync(absPath)) {
|
|
316
|
+
const existing = readFileSync(absPath, "utf-8");
|
|
317
|
+
existingHeaders = [...existing.matchAll(/^## .+/gm)].map((m) => m[0]);
|
|
318
|
+
}
|
|
319
|
+
const newMessages = messages.filter((m) => {
|
|
320
|
+
const header2 = `## ${m.time} \u2014 ${m.sender}`;
|
|
321
|
+
return !existingHeaders.includes(header2);
|
|
322
|
+
});
|
|
323
|
+
if (newMessages.length === 0) return filePath;
|
|
324
|
+
if (!existsSync(absPath)) {
|
|
325
|
+
writeFileSync(absPath, formatMessages(chatName, chatId, date, newMessages));
|
|
326
|
+
} else {
|
|
327
|
+
const appendText = newMessages.map((m) => `
|
|
328
|
+
## ${m.time} \u2014 ${m.sender}
|
|
329
|
+
${sanitizeContent(m.content)}`).join("\n");
|
|
330
|
+
appendFileSync(absPath, appendText + "\n");
|
|
331
|
+
}
|
|
332
|
+
return filePath;
|
|
333
|
+
}
|
|
334
|
+
/** Calculate file path for an object based on source + type. */
|
|
335
|
+
getFilePath(source, sourceType, meta) {
|
|
336
|
+
switch (source) {
|
|
337
|
+
case "github":
|
|
338
|
+
case "gitlab": {
|
|
339
|
+
const repo = slugify(meta.repo ?? "unknown");
|
|
340
|
+
const typeDir = sourceType === "pull_request" ? "pulls" : sourceType === "merge_request" ? "merge-requests" : "issues";
|
|
341
|
+
return join(source, repo, typeDir, `${meta.number ?? meta.id}.md`);
|
|
342
|
+
}
|
|
343
|
+
case "linear":
|
|
344
|
+
return join("linear", "issues", `${meta.identifier ?? meta.id}.md`);
|
|
345
|
+
case "feishu": {
|
|
346
|
+
if (sourceType === "calendar_event")
|
|
347
|
+
return join("feishu", "meetings", `${meta.date}-${slugify(meta.title ?? "event")}.md`);
|
|
348
|
+
if (sourceType === "approval")
|
|
349
|
+
return join("feishu", "approvals", `${slugify(meta.title ?? "approval")}-${meta.id}.md`);
|
|
350
|
+
if (sourceType === "document")
|
|
351
|
+
return join("feishu", "docs", `${slugify(meta.title ?? "doc")}.md`);
|
|
352
|
+
if (sourceType === "link")
|
|
353
|
+
return join("feishu", "links", `${slugify(meta.title ?? meta.id)}.md`);
|
|
354
|
+
return join("feishu", "other", `${meta.id}.md`);
|
|
355
|
+
}
|
|
356
|
+
case "posthog":
|
|
357
|
+
return join(
|
|
358
|
+
"posthog",
|
|
359
|
+
sourceType === "insight" ? "insights" : "events",
|
|
360
|
+
`${slugify(meta.title ?? meta.id)}.json`
|
|
361
|
+
);
|
|
362
|
+
case "firebase":
|
|
363
|
+
return join(
|
|
364
|
+
"firebase",
|
|
365
|
+
"analytics",
|
|
366
|
+
`${slugify(meta.title ?? meta.id)}.json`
|
|
367
|
+
);
|
|
368
|
+
case "notion":
|
|
369
|
+
return join("notion", "pages", `${slugify(meta.title ?? meta.id)}.md`);
|
|
370
|
+
case "jira":
|
|
371
|
+
return join("jira", slugify(meta.project ?? "unknown"), `${meta.identifier ?? meta.id}.md`);
|
|
372
|
+
case "sentry":
|
|
373
|
+
return join("sentry", "issues", `${meta.id}.json`);
|
|
374
|
+
default:
|
|
375
|
+
return join(source, `${meta.id}.md`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
get rootDir() {
|
|
379
|
+
return this.repoDir;
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
// src/commands/sync.ts
|
|
384
|
+
var isTTY = process.stdout.isTTY;
|
|
385
|
+
var c = {
|
|
386
|
+
reset: isTTY ? "\x1B[0m" : "",
|
|
387
|
+
bold: isTTY ? "\x1B[1m" : "",
|
|
388
|
+
dim: isTTY ? "\x1B[2m" : "",
|
|
389
|
+
green: isTTY ? "\x1B[32m" : "",
|
|
390
|
+
yellow: isTTY ? "\x1B[33m" : "",
|
|
391
|
+
red: isTTY ? "\x1B[31m" : "",
|
|
392
|
+
cyan: isTTY ? "\x1B[36m" : "",
|
|
393
|
+
gray: isTTY ? "\x1B[90m" : "",
|
|
394
|
+
white: isTTY ? "\x1B[37m" : "",
|
|
395
|
+
bgGreen: isTTY ? "\x1B[42m" : "",
|
|
396
|
+
bgRed: isTTY ? "\x1B[41m" : "",
|
|
397
|
+
bgYellow: isTTY ? "\x1B[43m" : ""
|
|
398
|
+
};
|
|
399
|
+
var icon = {
|
|
400
|
+
ok: `${c.green}\u2713${c.reset}`,
|
|
401
|
+
warn: `${c.yellow}\u26A0${c.reset}`,
|
|
402
|
+
fail: `${c.red}\u2717${c.reset}`,
|
|
403
|
+
skip: `${c.gray}\u25CB${c.reset}`,
|
|
404
|
+
sync: `${c.cyan}\u21BB${c.reset}`,
|
|
405
|
+
link: `${c.cyan}\u27E1${c.reset}`,
|
|
406
|
+
git: `${c.gray}\u2387${c.reset}`
|
|
407
|
+
};
|
|
408
|
+
function header(text) {
|
|
409
|
+
console.log("");
|
|
410
|
+
console.log(` ${c.bold}${text}${c.reset}`);
|
|
411
|
+
console.log(` ${c.dim}${"\u2500".repeat(Math.min(text.length + 4, 50))}${c.reset}`);
|
|
412
|
+
}
|
|
413
|
+
function progressBar(current, total, width = 20) {
|
|
414
|
+
const pct = total > 0 ? current / total : 0;
|
|
415
|
+
const filled = Math.round(pct * width);
|
|
416
|
+
const empty = width - filled;
|
|
417
|
+
const bar = `${c.green}${"\u2588".repeat(filled)}${c.gray}${"\u2591".repeat(empty)}${c.reset}`;
|
|
418
|
+
return `${bar} ${c.dim}${current}/${total}${c.reset}`;
|
|
419
|
+
}
|
|
420
|
+
function sourceLabel(name) {
|
|
421
|
+
const colors = {
|
|
422
|
+
feishu: c.cyan,
|
|
423
|
+
github: c.white,
|
|
424
|
+
gitlab: c.yellow,
|
|
425
|
+
linear: c.cyan,
|
|
426
|
+
posthog: c.red,
|
|
427
|
+
firebase: c.yellow
|
|
428
|
+
};
|
|
429
|
+
return `${colors[name] ?? c.white}${c.bold}${name}${c.reset}`;
|
|
430
|
+
}
|
|
431
|
+
function resultLine(ok, msg) {
|
|
432
|
+
console.log(` ${ok ? icon.ok : icon.warn} ${msg}`);
|
|
433
|
+
}
|
|
434
|
+
function elapsed(start) {
|
|
435
|
+
const ms = Date.now() - start;
|
|
436
|
+
return ms < 1e3 ? `${ms}ms` : `${(ms / 1e3).toFixed(1)}s`;
|
|
437
|
+
}
|
|
438
|
+
function getSyncErrorHint(source, error) {
|
|
439
|
+
const is401 = /401|unauthorized|auth.*fail/i.test(error);
|
|
440
|
+
const is403 = /403|forbidden/i.test(error);
|
|
441
|
+
if (is401 || is403) {
|
|
442
|
+
switch (source) {
|
|
443
|
+
case "github":
|
|
444
|
+
return "Token invalid or expired. Create a new one at https://github.com/settings/tokens";
|
|
445
|
+
case "gitlab":
|
|
446
|
+
return "Token invalid. Check at https://gitlab.com/-/profile/personal_access_tokens";
|
|
447
|
+
case "feishu":
|
|
448
|
+
return "App ID/Secret invalid. Check at https://open.feishu.cn/app";
|
|
449
|
+
case "linear":
|
|
450
|
+
return "API key invalid. Get one at https://linear.app/settings/api";
|
|
451
|
+
case "posthog":
|
|
452
|
+
return "API key invalid. Get one at https://app.posthog.com/project/settings";
|
|
453
|
+
case "firebase":
|
|
454
|
+
return "API key invalid. Check at https://console.cloud.google.com \u2192 APIs & Services \u2192 Credentials";
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
if (/ENOTFOUND|ECONNREFUSED|network/i.test(error)) {
|
|
458
|
+
return "Network error. Check your internet connection and try again.";
|
|
459
|
+
}
|
|
460
|
+
return null;
|
|
461
|
+
}
|
|
462
|
+
async function runSync(sources) {
|
|
463
|
+
const db = new DbManager(dbPath());
|
|
464
|
+
db.ensureTables();
|
|
465
|
+
const fileWriter = new FileWriter();
|
|
466
|
+
const syncResults = [];
|
|
467
|
+
const t0 = Date.now();
|
|
468
|
+
let totalNew = 0;
|
|
469
|
+
let gitManager = null;
|
|
470
|
+
try {
|
|
471
|
+
gitManager = new GitManager(fileRepoDir());
|
|
472
|
+
await gitManager.init();
|
|
473
|
+
} catch {
|
|
474
|
+
}
|
|
475
|
+
header(`Syncing ${sources.length} source${sources.length > 1 ? "s" : ""}`);
|
|
476
|
+
for (let i = 0; i < sources.length; i++) {
|
|
477
|
+
const source = sources[i];
|
|
478
|
+
const cred = loadCredential(source);
|
|
479
|
+
if (!cred) {
|
|
480
|
+
console.log(` ${icon.skip} ${sourceLabel(source)} ${c.dim}no credentials, skipping${c.reset}`);
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
const sourceStart = Date.now();
|
|
484
|
+
console.log(` ${icon.sync} ${sourceLabel(source)} ${c.dim}syncing...${c.reset}`);
|
|
485
|
+
const logger = {
|
|
486
|
+
info: (_msg) => {
|
|
487
|
+
},
|
|
488
|
+
warn: (msg) => resultLine(false, `${c.dim}${msg}${c.reset}`),
|
|
489
|
+
error: (msg) => console.error(` ${icon.fail} ${c.red}${msg}${c.reset}`)
|
|
490
|
+
};
|
|
491
|
+
try {
|
|
492
|
+
switch (source) {
|
|
493
|
+
case "feishu": {
|
|
494
|
+
const feishuCtx = new SyncContext(db.getSqlite(), logger, fileWriter);
|
|
495
|
+
const result = await syncFeishu(feishuCtx, cred.data, logger);
|
|
496
|
+
resultLine(true, `${result.newMessages} new messages from ${result.chats} chats`);
|
|
497
|
+
totalNew += result.newMessages;
|
|
498
|
+
syncResults.push({ source: "feishu", newObjects: result.newMessages, label: "messages" });
|
|
499
|
+
try {
|
|
500
|
+
const mr = await syncFeishuMeetings(feishuCtx, cred.data, logger);
|
|
501
|
+
if (mr.newObjects > 0) resultLine(true, `${mr.newObjects} calendar events`);
|
|
502
|
+
syncResults.push({ source: "feishu/meetings", newObjects: mr.newObjects, label: "events" });
|
|
503
|
+
} catch {
|
|
504
|
+
}
|
|
505
|
+
try {
|
|
506
|
+
const dr = await syncFeishuDocs(feishuCtx, cred.data, logger);
|
|
507
|
+
if (dr.newObjects > 0) resultLine(true, `${dr.newObjects} documents`);
|
|
508
|
+
syncResults.push({ source: "feishu/docs", newObjects: dr.newObjects, label: "docs" });
|
|
509
|
+
} catch {
|
|
510
|
+
}
|
|
511
|
+
try {
|
|
512
|
+
const ar = await syncFeishuApprovals(feishuCtx, cred.data, logger);
|
|
513
|
+
if (ar.newObjects > 0) resultLine(true, `${ar.newObjects} approvals`);
|
|
514
|
+
syncResults.push({ source: "feishu/approvals", newObjects: ar.newObjects, label: "approvals" });
|
|
515
|
+
} catch {
|
|
516
|
+
}
|
|
517
|
+
try {
|
|
518
|
+
const lr = await syncFeishuLinks(feishuCtx, cred.data, logger);
|
|
519
|
+
if (lr.fetched > 0) resultLine(true, `${lr.fetched} link contents fetched (${lr.extracted} URLs found)`);
|
|
520
|
+
else if (lr.extracted > 0) resultLine(false, `${lr.extracted} URLs found, ${lr.failed} failed to fetch`);
|
|
521
|
+
syncResults.push({ source: "feishu/links", newObjects: lr.fetched, label: "links" });
|
|
522
|
+
} catch {
|
|
523
|
+
}
|
|
524
|
+
break;
|
|
525
|
+
}
|
|
526
|
+
case "github": {
|
|
527
|
+
const ghCtx = new SyncContext(db.getSqlite(), logger, fileWriter);
|
|
528
|
+
const r = await syncGitHub(ghCtx, cred.data, logger);
|
|
529
|
+
const changeInfo = r.updatedObjects > 0 ? `, ${r.updatedObjects} updated` : "";
|
|
530
|
+
const cloneInfo = r.clonedRepos > 0 ? ` | ${r.clonedRepos} repos cloned` : "";
|
|
531
|
+
resultLine(true, `${r.repos} repos, ${r.prs} PRs, ${r.issues} issues ${c.dim}(${r.newObjects} new${changeInfo})${cloneInfo}${c.reset}`);
|
|
532
|
+
totalNew += r.newObjects;
|
|
533
|
+
syncResults.push({ source: "github", newObjects: r.newObjects + r.updatedObjects });
|
|
534
|
+
break;
|
|
535
|
+
}
|
|
536
|
+
case "gitlab": {
|
|
537
|
+
const glCtx = new SyncContext(db.getSqlite(), logger, fileWriter);
|
|
538
|
+
const r = await syncGitLab(glCtx, cred.data, logger);
|
|
539
|
+
const glChangeInfo = r.updatedObjects > 0 ? `, ${r.updatedObjects} updated` : "";
|
|
540
|
+
resultLine(true, `${r.projects} projects, ${r.mrs} MRs, ${r.issues} issues ${c.dim}(${r.newObjects} new${glChangeInfo})${c.reset}`);
|
|
541
|
+
totalNew += r.newObjects;
|
|
542
|
+
syncResults.push({ source: "gitlab", newObjects: r.newObjects + r.updatedObjects });
|
|
543
|
+
break;
|
|
544
|
+
}
|
|
545
|
+
case "linear": {
|
|
546
|
+
const lnCtx = new SyncContext(db.getSqlite(), logger, fileWriter);
|
|
547
|
+
const r = await syncLinear(lnCtx, cred.data, logger);
|
|
548
|
+
const lnChangeInfo = r.updatedObjects > 0 ? `, ${r.updatedObjects} updated` : "";
|
|
549
|
+
resultLine(true, `${r.issues} issues ${c.dim}(${r.newObjects} new${lnChangeInfo})${c.reset}`);
|
|
550
|
+
totalNew += r.newObjects;
|
|
551
|
+
syncResults.push({ source: "linear", newObjects: r.newObjects + r.updatedObjects, label: "issues" });
|
|
552
|
+
break;
|
|
553
|
+
}
|
|
554
|
+
case "posthog": {
|
|
555
|
+
const phCtx = new SyncContext(db.getSqlite(), logger, fileWriter);
|
|
556
|
+
const r = await syncPostHog(phCtx, cred.data, logger);
|
|
557
|
+
const phChangeInfo = r.updatedObjects > 0 ? `, ${r.updatedObjects} updated` : "";
|
|
558
|
+
resultLine(true, `${r.insights} insights, ${r.events} events ${c.dim}(${r.newObjects} new${phChangeInfo})${c.reset}`);
|
|
559
|
+
totalNew += r.newObjects;
|
|
560
|
+
syncResults.push({ source: "posthog", newObjects: r.newObjects + r.updatedObjects });
|
|
561
|
+
break;
|
|
562
|
+
}
|
|
563
|
+
case "firebase": {
|
|
564
|
+
const fbCtx = new SyncContext(db.getSqlite(), logger, fileWriter);
|
|
565
|
+
const r = await syncFirebase(fbCtx, cred.data, logger);
|
|
566
|
+
const fbChangeInfo = r.updatedObjects > 0 ? `, ${r.updatedObjects} updated` : "";
|
|
567
|
+
resultLine(true, `${r.events} events ${c.dim}(${r.newObjects} new${fbChangeInfo})${c.reset}`);
|
|
568
|
+
totalNew += r.newObjects;
|
|
569
|
+
syncResults.push({ source: "firebase", newObjects: r.newObjects + r.updatedObjects, label: "events" });
|
|
570
|
+
break;
|
|
571
|
+
}
|
|
572
|
+
default:
|
|
573
|
+
console.log(` ${icon.skip} ${c.dim}unknown source${c.reset}`);
|
|
574
|
+
}
|
|
575
|
+
console.log(` ${c.dim}${elapsed(sourceStart)}${c.reset}`);
|
|
576
|
+
} catch (err) {
|
|
577
|
+
logError("sync", `Failed to sync ${source}`, { error: String(err) });
|
|
578
|
+
const errStr = String(err);
|
|
579
|
+
console.log(` ${icon.fail} ${c.red}sync failed${c.reset} ${c.dim}${errStr.slice(0, 60)}${c.reset}`);
|
|
580
|
+
const hint = getSyncErrorHint(source, errStr);
|
|
581
|
+
if (hint) {
|
|
582
|
+
console.log(` ${c.yellow}Hint: ${hint}${c.reset}`);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
if (sources.length > 1) {
|
|
586
|
+
console.log(` ${progressBar(i + 1, sources.length)}`);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
console.log("");
|
|
590
|
+
console.log(` ${icon.link} ${c.dim}extracting links...${c.reset}`);
|
|
591
|
+
const { processed, linksCreated } = linkAllUnprocessed(db.getSqlite());
|
|
592
|
+
if (processed > 0) {
|
|
593
|
+
resultLine(true, `${linksCreated} links from ${processed} objects`);
|
|
594
|
+
}
|
|
595
|
+
try {
|
|
596
|
+
const sqlite = db.getSqlite();
|
|
597
|
+
const { readObjectContent } = await import("./content-reader-VPGTR2SF.js");
|
|
598
|
+
const noFilePath = sqlite.prepare(`
|
|
599
|
+
SELECT o.id, o.source, o.source_type, o.uri, o.title
|
|
600
|
+
FROM objects o
|
|
601
|
+
WHERE o.file_path IS NULL
|
|
602
|
+
LIMIT 500
|
|
603
|
+
`).all();
|
|
604
|
+
if (noFilePath.length > 0) {
|
|
605
|
+
console.log(` ${icon.sync} ${c.dim}backfilling ${noFilePath.length} objects without files...${c.reset}`);
|
|
606
|
+
let backfilled = 0;
|
|
607
|
+
for (const obj of noFilePath) {
|
|
608
|
+
try {
|
|
609
|
+
const content = readObjectContent(sqlite, obj.id, null);
|
|
610
|
+
if (!content) continue;
|
|
611
|
+
const filePath = fileWriter.writeObject(obj.source, obj.source_type, {
|
|
612
|
+
id: obj.id,
|
|
613
|
+
title: obj.title,
|
|
614
|
+
uri: obj.uri
|
|
615
|
+
}, content);
|
|
616
|
+
sqlite.prepare("UPDATE objects SET file_path = ? WHERE id = ?").run(filePath, obj.id);
|
|
617
|
+
backfilled++;
|
|
618
|
+
} catch {
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
if (backfilled > 0) {
|
|
622
|
+
resultLine(true, `${backfilled} files backfilled`);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
} catch {
|
|
626
|
+
}
|
|
627
|
+
db.close();
|
|
628
|
+
if (gitManager) {
|
|
629
|
+
try {
|
|
630
|
+
const sha = await gitManager.commitSync({ timestamp: (/* @__PURE__ */ new Date()).toISOString(), sources: syncResults });
|
|
631
|
+
if (sha) console.log(` ${icon.git} ${c.dim}committed ${sha.slice(0, 7)}${c.reset}`);
|
|
632
|
+
} catch {
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
console.log("");
|
|
636
|
+
console.log(` ${c.bold}${c.green}Done${c.reset} ${c.dim}in ${elapsed(t0)}${c.reset}`);
|
|
637
|
+
console.log(` ${c.bold}${totalNew}${c.reset} new objects synced from ${c.bold}${syncResults.length}${c.reset} sources`);
|
|
638
|
+
console.log("");
|
|
639
|
+
}
|
|
640
|
+
function syncCommand(program) {
|
|
641
|
+
program.command("sync").description("Sync data from connected sources").option("--source <source>", "Sync specific source only").action(async (opts) => {
|
|
642
|
+
if (!existsSync2(dbPath())) {
|
|
643
|
+
console.error("Error: JoWork not initialized. Run `jowork init` first.");
|
|
644
|
+
process.exit(1);
|
|
645
|
+
}
|
|
646
|
+
const sources = opts.source ? [opts.source] : listCredentials();
|
|
647
|
+
if (sources.length === 0) {
|
|
648
|
+
console.log("No data sources connected. Run `jowork connect <source>` first.");
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
await runSync(sources);
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
export {
|
|
656
|
+
syncPostHog,
|
|
657
|
+
syncFirebase,
|
|
658
|
+
FileWriter,
|
|
659
|
+
runSync,
|
|
660
|
+
syncCommand
|
|
661
|
+
};
|