offsite 0.0.23 → 0.0.25
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 +10 -0
- package/apps/server/dist/main.mjs +673 -583
- package/apps/server/dist/main.mjs.map +1 -1
- package/apps/web/dist/assets/HugeiconsIcon-eRbI5N8r.js +1 -0
- package/apps/web/dist/assets/audio-player-DPpDuqFI.js +1 -0
- package/apps/web/dist/assets/{dist-BYj5XneA.js → dist-BxKL_-7_.js} +1 -1
- package/apps/web/dist/assets/{image-viewer-DnE3hPpr.js → image-viewer-CXSBWHCX.js} +1 -1
- package/apps/web/dist/assets/index-CjjE4vH0.css +2 -0
- package/apps/web/dist/assets/index-_B37DCJA.js +781 -0
- package/apps/web/dist/assets/{preview-dialog-CApdSumg.js → preview-dialog-Cgeg3W6o.js} +12 -12
- package/apps/web/dist/assets/react-BlUBLzv4.js +1416 -0
- package/apps/web/dist/assets/{tooltip-DZsSQhD8.js → tooltip-D08x1wZY.js} +1 -1
- package/apps/web/dist/assets/utils-PtDt7m8t.js +1 -0
- package/apps/web/dist/assets/video-player-DDu2cDQE.js +1 -0
- package/apps/web/dist/assets/video-player-DlVZx3V3.js +1 -0
- package/apps/web/dist/assets/{video-viewer-B404MUMw.js → video-viewer-Cr1-BYtu.js} +1 -1
- package/apps/web/dist/assets/{zoomable-image-CjNpa0bY.js → zoomable-image-CrpD_Zwi.js} +1 -1
- package/apps/web/dist/assets/zoomable-image-Fkv4fx_U.js +1 -0
- package/apps/web/dist/index.html +6 -5
- package/apps/web/dist/sw.js +1 -1
- package/package.json +1 -1
- package/apps/web/dist/assets/HugeiconsIcon-B46TDTdz.js +0 -1
- package/apps/web/dist/assets/index-DMsagQXD.css +0 -2
- package/apps/web/dist/assets/index-W2nwhfc9.js +0 -783
- package/apps/web/dist/assets/video-player-05lO0mz3.js +0 -1416
- package/apps/web/dist/assets/video-player-DGtpeSpK.js +0 -1
- package/apps/web/dist/assets/zoomable-image-CzRbnhdc.js +0 -1
|
@@ -21,8 +21,8 @@ import { DatabaseSync } from "node:sqlite";
|
|
|
21
21
|
import { drizzle } from "drizzle-orm/node-sqlite";
|
|
22
22
|
import { migrate } from "drizzle-orm/node-sqlite/migrator";
|
|
23
23
|
import { integer, snakeCase, text } from "drizzle-orm/sqlite-core";
|
|
24
|
-
import webPush from "web-push";
|
|
25
24
|
import { promisify } from "node:util";
|
|
25
|
+
import webPush from "web-push";
|
|
26
26
|
import { spawn as spawn$1 } from "@lydell/node-pty";
|
|
27
27
|
//#region src/constants.ts
|
|
28
28
|
const deprecatedModels = {
|
|
@@ -818,6 +818,9 @@ const platformDirs = z.strictObject({
|
|
|
818
818
|
linux: posixDir,
|
|
819
819
|
windows: z.string().refine((value) => path.win32.isAbsolute(value), "must be an absolute path").nullable().default(null)
|
|
820
820
|
}).prefault({});
|
|
821
|
+
function workingDir(isAbsolute) {
|
|
822
|
+
return z.string().refine((value) => value === "~" || value.startsWith("~/") || isAbsolute(value), "must be an absolute path or start with ~").nullable().default(null);
|
|
823
|
+
}
|
|
821
824
|
function accountSchema(provider) {
|
|
822
825
|
return z.strictObject({
|
|
823
826
|
id: z.string(),
|
|
@@ -830,6 +833,11 @@ function accountSchema(provider) {
|
|
|
830
833
|
}
|
|
831
834
|
const configSchema = z.strictObject({
|
|
832
835
|
claudeBaseDir: platformDirs,
|
|
836
|
+
workingDir: z.strictObject({
|
|
837
|
+
macos: workingDir(path.posix.isAbsolute),
|
|
838
|
+
linux: workingDir(path.posix.isAbsolute),
|
|
839
|
+
windows: workingDir(path.win32.isAbsolute)
|
|
840
|
+
}).prefault({}),
|
|
833
841
|
claudeAccounts: z.array(accountSchema("claude")).default([]),
|
|
834
842
|
codexAccounts: z.array(accountSchema("codex")).default([]),
|
|
835
843
|
enabledProviders: z.strictObject({
|
|
@@ -930,6 +938,7 @@ function activity() {
|
|
|
930
938
|
running: run.running,
|
|
931
939
|
waiting: run.pending.size > 0,
|
|
932
940
|
prompted: run.prompted,
|
|
941
|
+
compacting: run.compacting,
|
|
933
942
|
tasks: run.tasks
|
|
934
943
|
};
|
|
935
944
|
return result;
|
|
@@ -964,6 +973,48 @@ function scheduleAutoResolve(sessionId, request) {
|
|
|
964
973
|
};
|
|
965
974
|
}
|
|
966
975
|
//#endregion
|
|
976
|
+
//#region src/lib/codex/presets.ts
|
|
977
|
+
const presets = /* @__PURE__ */ new Map([
|
|
978
|
+
["read-only", {
|
|
979
|
+
approvalPolicy: "on-request",
|
|
980
|
+
sandbox: "read-only",
|
|
981
|
+
sandboxPolicy: {
|
|
982
|
+
type: "readOnly",
|
|
983
|
+
networkAccess: false
|
|
984
|
+
}
|
|
985
|
+
}],
|
|
986
|
+
["auto", {
|
|
987
|
+
approvalPolicy: "on-request",
|
|
988
|
+
sandbox: "workspace-write",
|
|
989
|
+
sandboxPolicy: {
|
|
990
|
+
type: "workspaceWrite",
|
|
991
|
+
writableRoots: [],
|
|
992
|
+
networkAccess: false,
|
|
993
|
+
excludeTmpdirEnvVar: false,
|
|
994
|
+
excludeSlashTmp: false
|
|
995
|
+
}
|
|
996
|
+
}],
|
|
997
|
+
["full-access", {
|
|
998
|
+
approvalPolicy: "never",
|
|
999
|
+
sandbox: "danger-full-access",
|
|
1000
|
+
sandboxPolicy: { type: "dangerFullAccess" }
|
|
1001
|
+
}]
|
|
1002
|
+
]);
|
|
1003
|
+
function threadPreset(settings) {
|
|
1004
|
+
const preset = modePreset(settings.mode);
|
|
1005
|
+
if (preset === null) return {};
|
|
1006
|
+
return {
|
|
1007
|
+
approvalPolicy: preset.approvalPolicy,
|
|
1008
|
+
sandbox: preset.sandbox
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
function modePreset(mode) {
|
|
1012
|
+
if (mode === null) return null;
|
|
1013
|
+
const preset = presets.get(mode);
|
|
1014
|
+
if (preset === void 0) throw new Error(`Codex has no ${mode} mode`);
|
|
1015
|
+
return preset;
|
|
1016
|
+
}
|
|
1017
|
+
//#endregion
|
|
967
1018
|
//#region src/lib/codex/requests.ts
|
|
968
1019
|
function toRequest$1(requestId, method, params, items) {
|
|
969
1020
|
const turnId = params.turnId;
|
|
@@ -1055,32 +1106,6 @@ function reply(method, params, decision, answers) {
|
|
|
1055
1106
|
//#endregion
|
|
1056
1107
|
//#region src/lib/codex/run.ts
|
|
1057
1108
|
const IDLE_LIMIT$2 = 18e5;
|
|
1058
|
-
const presets = /* @__PURE__ */ new Map([
|
|
1059
|
-
["read-only", {
|
|
1060
|
-
approvalPolicy: "on-request",
|
|
1061
|
-
sandbox: "read-only",
|
|
1062
|
-
sandboxPolicy: {
|
|
1063
|
-
type: "readOnly",
|
|
1064
|
-
networkAccess: false
|
|
1065
|
-
}
|
|
1066
|
-
}],
|
|
1067
|
-
["auto", {
|
|
1068
|
-
approvalPolicy: "on-request",
|
|
1069
|
-
sandbox: "workspace-write",
|
|
1070
|
-
sandboxPolicy: {
|
|
1071
|
-
type: "workspaceWrite",
|
|
1072
|
-
writableRoots: [],
|
|
1073
|
-
networkAccess: false,
|
|
1074
|
-
excludeTmpdirEnvVar: false,
|
|
1075
|
-
excludeSlashTmp: false
|
|
1076
|
-
}
|
|
1077
|
-
}],
|
|
1078
|
-
["full-access", {
|
|
1079
|
-
approvalPolicy: "never",
|
|
1080
|
-
sandbox: "danger-full-access",
|
|
1081
|
-
sandboxPolicy: { type: "dangerFullAccess" }
|
|
1082
|
-
}]
|
|
1083
|
-
]);
|
|
1084
1109
|
function codexServer(settings) {
|
|
1085
1110
|
return appServer(storedAccount("codex", settings.accountId).configDir ?? codexBaseDir);
|
|
1086
1111
|
}
|
|
@@ -1098,6 +1123,7 @@ var CodexRun = class CodexRun {
|
|
|
1098
1123
|
partial = "";
|
|
1099
1124
|
running = false;
|
|
1100
1125
|
prompted = false;
|
|
1126
|
+
compacting = false;
|
|
1101
1127
|
backgroundTasks = 0;
|
|
1102
1128
|
tasks = {};
|
|
1103
1129
|
activeAt = Date.now();
|
|
@@ -1228,9 +1254,16 @@ var CodexRun = class CodexRun {
|
|
|
1228
1254
|
return;
|
|
1229
1255
|
}
|
|
1230
1256
|
case "item/started":
|
|
1231
|
-
case "item/completed":
|
|
1232
|
-
|
|
1257
|
+
case "item/completed": {
|
|
1258
|
+
const thread = params.item;
|
|
1259
|
+
if (thread.type === "contextCompaction") {
|
|
1260
|
+
this.compacting = method === "item/started";
|
|
1261
|
+
broadcastActivity();
|
|
1262
|
+
if (this.compacting) return;
|
|
1263
|
+
}
|
|
1264
|
+
for (const item of toItems$1(params.turnId, thread, null, Date.now())) this.put(item);
|
|
1233
1265
|
return;
|
|
1266
|
+
}
|
|
1234
1267
|
case "item/agentMessage/delta":
|
|
1235
1268
|
this.partial += params.delta;
|
|
1236
1269
|
publish$2(this.sessionId, {
|
|
@@ -1370,6 +1403,7 @@ var CodexRun = class CodexRun {
|
|
|
1370
1403
|
}
|
|
1371
1404
|
finishTurn() {
|
|
1372
1405
|
this.partial = "";
|
|
1406
|
+
this.compacting = false;
|
|
1373
1407
|
this.turnId = null;
|
|
1374
1408
|
const [next, ...rest] = this.queue;
|
|
1375
1409
|
if (next !== void 0) {
|
|
@@ -1424,20 +1458,6 @@ var CodexRun = class CodexRun {
|
|
|
1424
1458
|
});
|
|
1425
1459
|
}
|
|
1426
1460
|
};
|
|
1427
|
-
function threadPreset(settings) {
|
|
1428
|
-
const preset = modePreset(settings.mode);
|
|
1429
|
-
if (preset === null) return {};
|
|
1430
|
-
return {
|
|
1431
|
-
approvalPolicy: preset.approvalPolicy,
|
|
1432
|
-
sandbox: preset.sandbox
|
|
1433
|
-
};
|
|
1434
|
-
}
|
|
1435
|
-
function modePreset(mode) {
|
|
1436
|
-
if (mode === null) return null;
|
|
1437
|
-
const preset = presets.get(mode);
|
|
1438
|
-
if (preset === void 0) throw new Error(`Codex has no ${mode} mode`);
|
|
1439
|
-
return preset;
|
|
1440
|
-
}
|
|
1441
1461
|
//#endregion
|
|
1442
1462
|
//#region src/lib/codex/sessions.ts
|
|
1443
1463
|
function codexInstalled() {
|
|
@@ -1610,13 +1630,19 @@ function messageItems(message, parent) {
|
|
|
1610
1630
|
background: null
|
|
1611
1631
|
}];
|
|
1612
1632
|
case "assistant": return message.content.flatMap((part, index) => partItems(message, part, index, parent));
|
|
1613
|
-
case "compaction":
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1633
|
+
case "compaction":
|
|
1634
|
+
switch (message.status) {
|
|
1635
|
+
case "running": return [];
|
|
1636
|
+
case "completed": return [{
|
|
1637
|
+
...base,
|
|
1638
|
+
kind: "compaction"
|
|
1639
|
+
}];
|
|
1640
|
+
case "failed": return [{
|
|
1641
|
+
...errorItem(message.id, message.error.message),
|
|
1642
|
+
parent
|
|
1643
|
+
}];
|
|
1644
|
+
}
|
|
1645
|
+
throw new Error("Unknown compaction status");
|
|
1620
1646
|
case "idle": return message.outcome === "failed" && message.error !== void 0 ? [{
|
|
1621
1647
|
...errorItem(message.id, message.error.message),
|
|
1622
1648
|
parent
|
|
@@ -1828,6 +1854,47 @@ function record(value) {
|
|
|
1828
1854
|
return isRecord(value) ? value : {};
|
|
1829
1855
|
}
|
|
1830
1856
|
//#endregion
|
|
1857
|
+
//#region src/lib/opencode/prompts.ts
|
|
1858
|
+
async function sendPrompt(sessionId, cwd, prompt) {
|
|
1859
|
+
const client = await openCode();
|
|
1860
|
+
const sessionID = sessionId;
|
|
1861
|
+
const location = { directory: cwd };
|
|
1862
|
+
const match = /^\/(\S+)\s*([\s\S]*)$/.exec(prompt.text);
|
|
1863
|
+
if (match !== null) {
|
|
1864
|
+
if (match[1] === "compact" && prompt.images.length === 0) {
|
|
1865
|
+
await client.session.compact({ sessionID });
|
|
1866
|
+
return;
|
|
1867
|
+
}
|
|
1868
|
+
if ((await client.command.list({ location })).data.some((command) => command.name === match[1])) {
|
|
1869
|
+
await client.session.command({
|
|
1870
|
+
sessionID,
|
|
1871
|
+
name: match[1],
|
|
1872
|
+
text: match[2],
|
|
1873
|
+
files: await files(sessionID, prompt)
|
|
1874
|
+
});
|
|
1875
|
+
return;
|
|
1876
|
+
}
|
|
1877
|
+
const skill = (await client.skill.list({ location })).data.find((item) => item.name === match[1]);
|
|
1878
|
+
if (skill !== void 0) {
|
|
1879
|
+
await client.session.prompt({
|
|
1880
|
+
sessionID,
|
|
1881
|
+
text: match[2],
|
|
1882
|
+
files: await files(sessionID, prompt),
|
|
1883
|
+
skills: [{ id: skill.id }]
|
|
1884
|
+
});
|
|
1885
|
+
return;
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
await client.session.prompt({
|
|
1889
|
+
sessionID,
|
|
1890
|
+
text: prompt.text,
|
|
1891
|
+
files: await files(sessionID, prompt)
|
|
1892
|
+
});
|
|
1893
|
+
}
|
|
1894
|
+
function files(sessionId, prompt) {
|
|
1895
|
+
return Promise.all(prompt.images.map(async (image) => ({ uri: pathToFileURL(await saveImage(sessionId, image)).href })));
|
|
1896
|
+
}
|
|
1897
|
+
//#endregion
|
|
1831
1898
|
//#region src/lib/opencode/requests.ts
|
|
1832
1899
|
function permissionRequest(permission, item) {
|
|
1833
1900
|
return {
|
|
@@ -2061,6 +2128,7 @@ var OpenCodeRun = class OpenCodeRun {
|
|
|
2061
2128
|
partial = "";
|
|
2062
2129
|
running = false;
|
|
2063
2130
|
prompted = false;
|
|
2131
|
+
compacting = false;
|
|
2064
2132
|
backgroundTasks = 0;
|
|
2065
2133
|
tasks = {};
|
|
2066
2134
|
close;
|
|
@@ -2182,6 +2250,16 @@ var OpenCodeRun = class OpenCodeRun {
|
|
|
2182
2250
|
case "session.reasoning.delta":
|
|
2183
2251
|
case "session.tool.input.delta":
|
|
2184
2252
|
case "session.compaction.delta": return;
|
|
2253
|
+
case "session.compaction.started":
|
|
2254
|
+
this.compacting = true;
|
|
2255
|
+
broadcastActivity();
|
|
2256
|
+
return;
|
|
2257
|
+
case "session.compaction.ended":
|
|
2258
|
+
case "session.compaction.failed":
|
|
2259
|
+
this.compacting = false;
|
|
2260
|
+
broadcastActivity();
|
|
2261
|
+
this.refresh();
|
|
2262
|
+
return;
|
|
2185
2263
|
case "session.execution.started":
|
|
2186
2264
|
clearTimeout(this.idleTimer);
|
|
2187
2265
|
if (!this.running) this.prompted = false;
|
|
@@ -2248,7 +2326,7 @@ var OpenCodeRun = class OpenCodeRun {
|
|
|
2248
2326
|
broadcastActivity();
|
|
2249
2327
|
try {
|
|
2250
2328
|
await this.apply();
|
|
2251
|
-
await this.
|
|
2329
|
+
await sendPrompt(this.sessionId, this.cwd, prompt);
|
|
2252
2330
|
} catch (error) {
|
|
2253
2331
|
this.running = false;
|
|
2254
2332
|
broadcastActivity();
|
|
@@ -2256,42 +2334,6 @@ var OpenCodeRun = class OpenCodeRun {
|
|
|
2256
2334
|
throw error;
|
|
2257
2335
|
}
|
|
2258
2336
|
}
|
|
2259
|
-
async send(prompt) {
|
|
2260
|
-
const client = await openCode();
|
|
2261
|
-
const sessionID = this.sessionId;
|
|
2262
|
-
const location = { directory: this.cwd };
|
|
2263
|
-
const match = /^\/(\S+)\s*([\s\S]*)$/.exec(prompt.text);
|
|
2264
|
-
if (match !== null) {
|
|
2265
|
-
if (match[1] === "compact" && prompt.images.length === 0) {
|
|
2266
|
-
await client.session.compact({ sessionID });
|
|
2267
|
-
return;
|
|
2268
|
-
}
|
|
2269
|
-
if ((await client.command.list({ location })).data.some((command) => command.name === match[1])) {
|
|
2270
|
-
await client.session.command({
|
|
2271
|
-
sessionID,
|
|
2272
|
-
name: match[1],
|
|
2273
|
-
text: match[2],
|
|
2274
|
-
files: await files(sessionID, prompt)
|
|
2275
|
-
});
|
|
2276
|
-
return;
|
|
2277
|
-
}
|
|
2278
|
-
const skill = (await client.skill.list({ location })).data.find((item) => item.name === match[1]);
|
|
2279
|
-
if (skill !== void 0) {
|
|
2280
|
-
await client.session.prompt({
|
|
2281
|
-
sessionID,
|
|
2282
|
-
text: match[2],
|
|
2283
|
-
files: await files(sessionID, prompt),
|
|
2284
|
-
skills: [{ id: skill.id }]
|
|
2285
|
-
});
|
|
2286
|
-
return;
|
|
2287
|
-
}
|
|
2288
|
-
}
|
|
2289
|
-
await client.session.prompt({
|
|
2290
|
-
sessionID,
|
|
2291
|
-
text: prompt.text,
|
|
2292
|
-
files: await files(sessionID, prompt)
|
|
2293
|
-
});
|
|
2294
|
-
}
|
|
2295
2337
|
async apply() {
|
|
2296
2338
|
const client = await openCode();
|
|
2297
2339
|
const settings = this.settings;
|
|
@@ -2344,6 +2386,7 @@ var OpenCodeRun = class OpenCodeRun {
|
|
|
2344
2386
|
}
|
|
2345
2387
|
finishTurn() {
|
|
2346
2388
|
this.partial = "";
|
|
2389
|
+
this.compacting = false;
|
|
2347
2390
|
const [next, ...rest] = this.queue;
|
|
2348
2391
|
if (next !== void 0) {
|
|
2349
2392
|
this.queue = rest;
|
|
@@ -2400,9 +2443,6 @@ var OpenCodeRun = class OpenCodeRun {
|
|
|
2400
2443
|
});
|
|
2401
2444
|
}
|
|
2402
2445
|
};
|
|
2403
|
-
function files(sessionId, prompt) {
|
|
2404
|
-
return Promise.all(prompt.images.map(async (image) => ({ uri: pathToFileURL(await saveImage(sessionId, image)).href })));
|
|
2405
|
-
}
|
|
2406
2446
|
//#endregion
|
|
2407
2447
|
//#region src/storage/db.ts
|
|
2408
2448
|
const file = path.join(home, "offsite.db");
|
|
@@ -2456,63 +2496,482 @@ const pushSubscriptions = snakeCase.table("push_subscriptions", {
|
|
|
2456
2496
|
server: text().notNull()
|
|
2457
2497
|
});
|
|
2458
2498
|
//#endregion
|
|
2459
|
-
//#region src/services/
|
|
2460
|
-
const
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2499
|
+
//#region src/services/git.ts
|
|
2500
|
+
const DIFF_LIMIT = 8e4;
|
|
2501
|
+
const UNTRACKED_LIMIT = 100;
|
|
2502
|
+
const NEW_FILE_LIMIT = 262144;
|
|
2503
|
+
const REPO_DEPTH = 3;
|
|
2504
|
+
const FETCH_TIMEOUT = 6e4;
|
|
2505
|
+
const run$1 = promisify(execFile);
|
|
2506
|
+
const PULL_REQUEST_INSTRUCTIONS = `You write GitHub pull requests as Markdown.
|
|
2507
|
+
|
|
2508
|
+
## TITLE RULES
|
|
2509
|
+
|
|
2510
|
+
- Use the conventional commits format: type(scope): subject
|
|
2511
|
+
- Types: feat, fix, docs, style, refactor, perf, test, chore, ci
|
|
2512
|
+
- Scope is optional; include it when the change targets a clear area
|
|
2513
|
+
- Subject: imperative mood, lowercase, no trailing period
|
|
2514
|
+
- Maximum 72 characters in total
|
|
2515
|
+
- Describe the overall change, not a single commit
|
|
2516
|
+
|
|
2517
|
+
## BODY RULES
|
|
2518
|
+
|
|
2519
|
+
- Use short Markdown sections with level-2 headings. Choose only sections that fit the change: \`New features\`, \`Improvements\`, \`Fixes\`, \`Before\` / \`After\`, or \`Breaking changes\`. Do not use all of them as a template.
|
|
2520
|
+
- Use bullet points under every section, never paragraphs. Each bullet describes one feature or change in one short sentence.
|
|
2521
|
+
- For a small change, use one section with one bullet. For larger changes, use only the sections needed, with at most three short bullets across the body.
|
|
2522
|
+
- Aim for 20-50 words; fewer is better when enough. Never exceed 80 words.
|
|
2523
|
+
- Describe the outcome, not the implementation walkthrough. Do not enumerate changed files, functions, commits, or incidental cleanup; reviewers can read the diff.
|
|
2524
|
+
- Add a \`Breaking changes\` section only for an actual compatibility break or required migration.
|
|
2525
|
+
- No generic \`Summary\` section, bold labels, checklists, boilerplate openings like "This pull request", or closing summaries.
|
|
2526
|
+
|
|
2527
|
+
## RULES
|
|
2528
|
+
|
|
2529
|
+
Base every statement on the provided commits and diff; do NOT invent details.
|
|
2530
|
+
|
|
2531
|
+
## OUTPUT
|
|
2532
|
+
|
|
2533
|
+
Return one Markdown document. The title is the level-1 heading (\`# <title>\`), and everything after it is the body.`;
|
|
2534
|
+
async function gitRepos(cwd) {
|
|
2535
|
+
return Promise.all((await projectRepos(cwd)).map(async (repo) => ({
|
|
2536
|
+
...await gitStatus(path.join(cwd, repo)),
|
|
2537
|
+
path: repo
|
|
2538
|
+
})));
|
|
2491
2539
|
}
|
|
2492
|
-
function
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2540
|
+
async function commit(cwd, message, andPush) {
|
|
2541
|
+
const text = message.trim() === "" ? await commitMessage(cwd) : message;
|
|
2542
|
+
await git(cwd, ["add", "--all"]);
|
|
2543
|
+
await git(cwd, [
|
|
2544
|
+
"commit",
|
|
2545
|
+
"--message",
|
|
2546
|
+
text
|
|
2547
|
+
]);
|
|
2548
|
+
if (andPush) try {
|
|
2549
|
+
await push$1(cwd);
|
|
2550
|
+
} catch (error) {
|
|
2551
|
+
throw new Error(`Committed, but push failed: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
2501
2552
|
}
|
|
2502
|
-
|
|
2503
|
-
return null;
|
|
2553
|
+
return { message: text };
|
|
2504
2554
|
}
|
|
2505
|
-
function
|
|
2506
|
-
|
|
2507
|
-
|
|
2555
|
+
async function push$1(cwd) {
|
|
2556
|
+
const status = await gitStatus(cwd);
|
|
2557
|
+
if (status.branch === null) throw new Error("Cannot push a detached HEAD");
|
|
2558
|
+
try {
|
|
2559
|
+
if (status.upstream === null) await git(cwd, [
|
|
2560
|
+
"push",
|
|
2561
|
+
"--set-upstream",
|
|
2562
|
+
"origin",
|
|
2563
|
+
"HEAD"
|
|
2564
|
+
]);
|
|
2565
|
+
else await git(cwd, ["push"]);
|
|
2566
|
+
} catch (error) {
|
|
2567
|
+
throw gitError(error);
|
|
2568
|
+
}
|
|
2508
2569
|
}
|
|
2509
|
-
async function
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2570
|
+
async function sync(cwd) {
|
|
2571
|
+
if ((await gitStatus(cwd)).upstream === null) {
|
|
2572
|
+
await push$1(cwd);
|
|
2573
|
+
return;
|
|
2574
|
+
}
|
|
2575
|
+
try {
|
|
2576
|
+
await git(cwd, ["fetch"]);
|
|
2577
|
+
const status = await gitStatus(cwd);
|
|
2578
|
+
if (status.behind > 0 && status.ahead === 0) await git(cwd, [
|
|
2579
|
+
"merge",
|
|
2580
|
+
"--ff-only",
|
|
2581
|
+
"@{upstream}"
|
|
2582
|
+
]);
|
|
2583
|
+
if (status.behind > 0 && status.ahead > 0) await rebaseOnUpstream(cwd);
|
|
2584
|
+
} catch (error) {
|
|
2585
|
+
throw gitError(error);
|
|
2586
|
+
}
|
|
2587
|
+
if ((await gitStatus(cwd)).ahead > 0) await push$1(cwd);
|
|
2588
|
+
}
|
|
2589
|
+
async function fetchProject(cwd) {
|
|
2590
|
+
await Promise.all((await projectRepos(cwd)).map(async (repo) => {
|
|
2591
|
+
try {
|
|
2592
|
+
await run$1("git", ["fetch"], {
|
|
2593
|
+
cwd: path.join(cwd, repo),
|
|
2594
|
+
env: {
|
|
2595
|
+
...process.env,
|
|
2596
|
+
GIT_TERMINAL_PROMPT: "0"
|
|
2597
|
+
},
|
|
2598
|
+
timeout: FETCH_TIMEOUT
|
|
2599
|
+
});
|
|
2600
|
+
} catch (error) {
|
|
2601
|
+
throw gitError(error);
|
|
2602
|
+
}
|
|
2603
|
+
}));
|
|
2604
|
+
}
|
|
2605
|
+
async function localBranches(cwd) {
|
|
2606
|
+
return (await git(cwd, [
|
|
2607
|
+
"branch",
|
|
2608
|
+
"--sort=-committerdate",
|
|
2609
|
+
"--format=%(refname:short)"
|
|
2610
|
+
])).stdout.split("\n").filter(Boolean);
|
|
2611
|
+
}
|
|
2612
|
+
async function switchBranch(cwd, branch, create) {
|
|
2613
|
+
await git(cwd, create ? [
|
|
2614
|
+
"switch",
|
|
2615
|
+
"--create",
|
|
2616
|
+
branch
|
|
2617
|
+
] : ["switch", branch]);
|
|
2618
|
+
}
|
|
2619
|
+
async function pullRequestBranches(cwd) {
|
|
2620
|
+
const head = (await gitStatus(cwd)).branch;
|
|
2621
|
+
if (head === null) throw new Error("Not on a branch");
|
|
2622
|
+
const base = (await run$1("gh", [
|
|
2623
|
+
"repo",
|
|
2624
|
+
"view",
|
|
2625
|
+
"--json",
|
|
2626
|
+
"defaultBranchRef",
|
|
2627
|
+
"--jq",
|
|
2628
|
+
".defaultBranchRef.name"
|
|
2629
|
+
], { cwd })).stdout.trim();
|
|
2630
|
+
if (base === head) throw new Error(`${head} is the default branch. Create a branch first.`);
|
|
2631
|
+
return {
|
|
2632
|
+
head,
|
|
2633
|
+
base,
|
|
2634
|
+
branches: (await git(cwd, [
|
|
2635
|
+
"ls-remote",
|
|
2636
|
+
"--heads",
|
|
2637
|
+
"origin"
|
|
2638
|
+
])).stdout.split("\n").filter(Boolean).map((line) => line.split("refs/heads/")[1]).filter((branch) => branch !== head)
|
|
2639
|
+
};
|
|
2640
|
+
}
|
|
2641
|
+
async function preparePullRequest(cwd, base, commitChanges) {
|
|
2642
|
+
const head = (await gitStatus(cwd)).branch;
|
|
2643
|
+
if (head === null) throw new Error("Not on a branch");
|
|
2644
|
+
if (commitChanges) await commit(cwd, "", true);
|
|
2645
|
+
const status = await gitStatus(cwd);
|
|
2646
|
+
if (status.upstream === null || status.ahead > 0) await push$1(cwd);
|
|
2647
|
+
const open = (await run$1("gh", [
|
|
2648
|
+
"pr",
|
|
2649
|
+
"list",
|
|
2650
|
+
"--head",
|
|
2651
|
+
head,
|
|
2652
|
+
"--base",
|
|
2653
|
+
base,
|
|
2654
|
+
"--state",
|
|
2655
|
+
"open",
|
|
2656
|
+
"--json",
|
|
2657
|
+
"url",
|
|
2658
|
+
"--jq",
|
|
2659
|
+
".[0].url // \"\""
|
|
2660
|
+
], { cwd })).stdout.trim();
|
|
2661
|
+
if (open !== "") return {
|
|
2662
|
+
kind: "open",
|
|
2663
|
+
url: open
|
|
2664
|
+
};
|
|
2665
|
+
await git(cwd, [
|
|
2666
|
+
"fetch",
|
|
2667
|
+
"origin",
|
|
2668
|
+
base
|
|
2669
|
+
]);
|
|
2670
|
+
const commits = (await git(cwd, [
|
|
2671
|
+
"log",
|
|
2672
|
+
"--format=- %B",
|
|
2673
|
+
`origin/${base}..HEAD`
|
|
2674
|
+
])).stdout.trim();
|
|
2675
|
+
if (commits === "") throw new Error(`No commits between ${base} and ${head}`);
|
|
2676
|
+
const diff = (await git(cwd, [
|
|
2677
|
+
"diff",
|
|
2678
|
+
"--no-color",
|
|
2679
|
+
"--no-ext-diff",
|
|
2680
|
+
`origin/${base}...HEAD`
|
|
2681
|
+
])).stdout.slice(0, DIFF_LIMIT);
|
|
2682
|
+
const lines = (await generateText(cwd, PULL_REQUEST_INSTRUCTIONS, `Commits between base and head:\n${commits}\n\n${diff}\n\nGenerate the pull request Markdown document from the above.`)).split("\n");
|
|
2683
|
+
const title = lines[0].trim().replace(/^#{1,6}\s*/, "").replace(/^(\*\*|__|\*|_|`)+/, "").replace(/(\*\*|__|\*|_|`)+$/, "").trim();
|
|
2684
|
+
if (title === "") throw new Error("The model returned an empty title");
|
|
2685
|
+
return {
|
|
2686
|
+
kind: "draft",
|
|
2687
|
+
base,
|
|
2688
|
+
head,
|
|
2689
|
+
title,
|
|
2690
|
+
body: lines.slice(1).join("\n").trim()
|
|
2691
|
+
};
|
|
2692
|
+
}
|
|
2693
|
+
async function createPullRequest(cwd, request) {
|
|
2694
|
+
const url = (await run$1("gh", [
|
|
2695
|
+
"pr",
|
|
2696
|
+
"create",
|
|
2697
|
+
"--title",
|
|
2698
|
+
request.title,
|
|
2699
|
+
"--body",
|
|
2700
|
+
request.body,
|
|
2701
|
+
"--base",
|
|
2702
|
+
request.base,
|
|
2703
|
+
"--head",
|
|
2704
|
+
request.head,
|
|
2705
|
+
"--assignee",
|
|
2706
|
+
"@me"
|
|
2707
|
+
], { cwd })).stdout.trim().split("\n").at(-1);
|
|
2708
|
+
if (url === void 0) throw new Error("gh did not return a pull request URL");
|
|
2709
|
+
return { url };
|
|
2710
|
+
}
|
|
2711
|
+
async function commitMessage(cwd) {
|
|
2712
|
+
const diff = (await repoDiff(cwd, "")).slice(0, DIFF_LIMIT);
|
|
2713
|
+
if (diff === "") throw new Error("There is nothing to commit");
|
|
2714
|
+
return generateText(cwd, "You write concise git commit messages.", `Write a git commit message for this diff. Reply with only the message: a short summary line, optionally followed by a blank line and a brief body.\n\n${diff}`);
|
|
2715
|
+
}
|
|
2716
|
+
async function generateText(cwd, instructions, prompt) {
|
|
2717
|
+
const settings = readConfig().commitMessage;
|
|
2718
|
+
const provider = settings.provider;
|
|
2719
|
+
const accountId = settings.accountId;
|
|
2720
|
+
const model = settings.model;
|
|
2721
|
+
if (provider === null || model === null) throw new Error("Choose a model in Settings → Git");
|
|
2722
|
+
assertEnabled(provider, accountId);
|
|
2723
|
+
if (provider === "codex") return codexText(accountId, cwd, model, instructions, prompt);
|
|
2724
|
+
if (provider === "opencode") return openCodeText(cwd, model, `${instructions}\n\n${prompt}`);
|
|
2725
|
+
const conversation = query({
|
|
2726
|
+
prompt,
|
|
2727
|
+
options: {
|
|
2728
|
+
...claudeOptions(accountId, cwd),
|
|
2729
|
+
systemPrompt: instructions,
|
|
2730
|
+
tools: [],
|
|
2731
|
+
model,
|
|
2732
|
+
maxTurns: 1,
|
|
2733
|
+
persistSession: false
|
|
2734
|
+
}
|
|
2735
|
+
});
|
|
2736
|
+
for await (const message of conversation) {
|
|
2737
|
+
if (message.type !== "result") continue;
|
|
2738
|
+
if (message.subtype !== "success") throw new Error(message.errors.join("\n"));
|
|
2739
|
+
if (message.is_error) throw new Error(message.result);
|
|
2740
|
+
return message.result.trim();
|
|
2741
|
+
}
|
|
2742
|
+
throw new Error("Claude did not reply");
|
|
2743
|
+
}
|
|
2744
|
+
async function projectDiff(cwd) {
|
|
2745
|
+
const repos = await projectRepos(cwd);
|
|
2746
|
+
if (repos.length === 0) throw new Error(`${cwd} is not a git repository and has none inside it`);
|
|
2747
|
+
return (await Promise.all(repos.map((repo) => repoDiff(path.join(cwd, repo), repo === "" ? "" : `${repo}/`)))).join("");
|
|
2748
|
+
}
|
|
2749
|
+
function git(cwd, args) {
|
|
2750
|
+
return run$1("git", args, {
|
|
2751
|
+
cwd,
|
|
2752
|
+
maxBuffer: 67108864
|
|
2753
|
+
});
|
|
2754
|
+
}
|
|
2755
|
+
async function projectRepos(cwd) {
|
|
2756
|
+
return await isRepo(cwd) ? [""] : childRepos(cwd, "", REPO_DEPTH);
|
|
2757
|
+
}
|
|
2758
|
+
async function gitStatus(cwd) {
|
|
2759
|
+
const output = await git(cwd, [
|
|
2760
|
+
"--no-optional-locks",
|
|
2761
|
+
"status",
|
|
2762
|
+
"--porcelain=v2",
|
|
2763
|
+
"--branch"
|
|
2764
|
+
]);
|
|
2765
|
+
const status = {
|
|
2766
|
+
branch: null,
|
|
2767
|
+
upstream: null,
|
|
2768
|
+
remote: (await git(cwd, ["remote"])).stdout.trim() !== "",
|
|
2769
|
+
ahead: 0,
|
|
2770
|
+
behind: 0,
|
|
2771
|
+
changed: 0
|
|
2772
|
+
};
|
|
2773
|
+
for (const line of output.stdout.split("\n").filter(Boolean)) {
|
|
2774
|
+
const [marker, key, ...rest] = line.split(" ");
|
|
2775
|
+
if (marker !== "#") status.changed++;
|
|
2776
|
+
else if (key === "branch.head") status.branch = rest[0] === "(detached)" ? null : rest[0];
|
|
2777
|
+
else if (key === "branch.upstream") status.upstream = rest[0];
|
|
2778
|
+
else if (key === "branch.ab") {
|
|
2779
|
+
status.ahead = Number(rest[0]);
|
|
2780
|
+
status.behind = -Number(rest[1]);
|
|
2781
|
+
}
|
|
2782
|
+
}
|
|
2783
|
+
return status;
|
|
2784
|
+
}
|
|
2785
|
+
async function rebaseOnUpstream(cwd) {
|
|
2786
|
+
try {
|
|
2787
|
+
await git(cwd, ["rebase", "@{upstream}"]);
|
|
2788
|
+
} catch (error) {
|
|
2789
|
+
const rebasing = await git(cwd, [
|
|
2790
|
+
"rev-parse",
|
|
2791
|
+
"--git-path",
|
|
2792
|
+
"rebase-merge"
|
|
2793
|
+
]);
|
|
2794
|
+
if (existsSync(path.resolve(cwd, rebasing.stdout.trim()))) {
|
|
2795
|
+
await git(cwd, ["rebase", "--abort"]);
|
|
2796
|
+
throw new Error("Your commits conflict with the remote branch, so nothing was pulled or pushed. Resolve it in a terminal.", { cause: error });
|
|
2797
|
+
}
|
|
2798
|
+
throw error;
|
|
2799
|
+
}
|
|
2800
|
+
}
|
|
2801
|
+
function gitError(error) {
|
|
2802
|
+
return new Error((error instanceof Error ? error.message : String(error)).split("\n").filter((line) => line.trim() !== "" && !line.startsWith("hint:") && !line.startsWith("Command failed:")).join("\n"), { cause: error });
|
|
2803
|
+
}
|
|
2804
|
+
async function isRepo(cwd) {
|
|
2805
|
+
try {
|
|
2806
|
+
await git(cwd, ["rev-parse", "--is-inside-work-tree"]);
|
|
2807
|
+
return true;
|
|
2808
|
+
} catch {
|
|
2809
|
+
return false;
|
|
2810
|
+
}
|
|
2811
|
+
}
|
|
2812
|
+
async function repoDiff(repo, prefix) {
|
|
2813
|
+
const tracked = await git(repo, [
|
|
2814
|
+
"--no-optional-locks",
|
|
2815
|
+
"diff",
|
|
2816
|
+
await diffBase(repo),
|
|
2817
|
+
"--no-color",
|
|
2818
|
+
"--no-ext-diff",
|
|
2819
|
+
`--src-prefix=a/${prefix}`,
|
|
2820
|
+
`--dst-prefix=b/${prefix}`
|
|
2821
|
+
]);
|
|
2822
|
+
const added = (await git(repo, [
|
|
2823
|
+
"ls-files",
|
|
2824
|
+
"--others",
|
|
2825
|
+
"--exclude-standard"
|
|
2826
|
+
])).stdout.split("\n").filter(Boolean).slice(0, UNTRACKED_LIMIT).map((file) => newFilePatch(repo, file, prefix));
|
|
2827
|
+
return [tracked.stdout, ...added].join("");
|
|
2828
|
+
}
|
|
2829
|
+
async function diffBase(repo) {
|
|
2830
|
+
try {
|
|
2831
|
+
await git(repo, [
|
|
2832
|
+
"rev-parse",
|
|
2833
|
+
"--verify",
|
|
2834
|
+
"--quiet",
|
|
2835
|
+
"HEAD"
|
|
2836
|
+
]);
|
|
2837
|
+
return "HEAD";
|
|
2838
|
+
} catch (error) {
|
|
2839
|
+
if (!(error instanceof Error && "code" in error && error.code === 1)) throw error;
|
|
2840
|
+
}
|
|
2841
|
+
return (await git(repo, [
|
|
2842
|
+
"hash-object",
|
|
2843
|
+
"-t",
|
|
2844
|
+
"tree",
|
|
2845
|
+
"/dev/null"
|
|
2846
|
+
])).stdout.trim();
|
|
2847
|
+
}
|
|
2848
|
+
function childRepos(root, dir, depth) {
|
|
2849
|
+
if (depth === 0) return [];
|
|
2850
|
+
let entries;
|
|
2851
|
+
try {
|
|
2852
|
+
entries = readdirSync(path.join(root, dir), { withFileTypes: true });
|
|
2853
|
+
} catch {
|
|
2854
|
+
return [];
|
|
2855
|
+
}
|
|
2856
|
+
return entries.filter((entry) => entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules").flatMap((entry) => {
|
|
2857
|
+
const relative = dir === "" ? entry.name : `${dir}/${entry.name}`;
|
|
2858
|
+
return existsSync(path.join(root, relative, ".git")) ? [relative] : childRepos(root, relative, depth - 1);
|
|
2859
|
+
});
|
|
2860
|
+
}
|
|
2861
|
+
function newFilePatch(cwd, file, prefix) {
|
|
2862
|
+
const name = `${prefix}${file}`;
|
|
2863
|
+
const header = `diff --git a/${name} b/${name}\nnew file\n--- /dev/null\n+++ b/${name}\n`;
|
|
2864
|
+
const full = path.join(cwd, file);
|
|
2865
|
+
const stats = lstatSync(full);
|
|
2866
|
+
if (stats.isSymbolicLink()) return `${header}@@ -0,0 +1 @@\n+${readlinkSync(full)}\n`;
|
|
2867
|
+
if (stats.isDirectory()) return `${header}Nested repository\n`;
|
|
2868
|
+
if (stats.size > NEW_FILE_LIMIT) return `${header}Binary or large file\n`;
|
|
2869
|
+
const content = readFileSync(full, "utf8");
|
|
2870
|
+
if (content.includes("\0")) return `${header}Binary or large file\n`;
|
|
2871
|
+
const lines = content.split("\n");
|
|
2872
|
+
if (lines.at(-1) === "") lines.pop();
|
|
2873
|
+
return `${header}@@ -0,0 +1,${lines.length} @@\n${lines.map((line) => `+${line}\n`).join("")}`;
|
|
2874
|
+
}
|
|
2875
|
+
const LOGO_FILES = /* @__PURE__ */ new Map([
|
|
2876
|
+
[".svg", "image/svg+xml"],
|
|
2877
|
+
[".png", "image/png"],
|
|
2878
|
+
[".webp", "image/webp"],
|
|
2879
|
+
[".jpg", "image/jpeg"],
|
|
2880
|
+
[".jpeg", "image/jpeg"],
|
|
2881
|
+
[".gif", "image/gif"],
|
|
2882
|
+
[".ico", "image/x-icon"]
|
|
2883
|
+
]);
|
|
2884
|
+
const PREFERRED = [
|
|
2885
|
+
"logo.svg",
|
|
2886
|
+
"logo.png",
|
|
2887
|
+
"logo.webp",
|
|
2888
|
+
"favicon.ico",
|
|
2889
|
+
"icon.svg",
|
|
2890
|
+
"icon.png",
|
|
2891
|
+
"icon.webp"
|
|
2892
|
+
];
|
|
2893
|
+
async function findLogo(folder) {
|
|
2894
|
+
const repos = await projectRepos(folder);
|
|
2895
|
+
const candidates = (await Promise.all(repos.map(async (repo) => {
|
|
2896
|
+
return (await git(path.join(folder, repo), ["ls-files", "-z"])).stdout.split("\0").map((file) => repo === "" ? file : `${repo}/${file}`);
|
|
2897
|
+
}))).flat().map((file) => ({
|
|
2898
|
+
file,
|
|
2899
|
+
rank: logoRank(file)
|
|
2900
|
+
})).filter((candidate) => candidate.rank !== -1).sort((a, b) => a.rank - b.rank || a.file.length - b.file.length);
|
|
2901
|
+
for (const { file } of candidates) {
|
|
2902
|
+
const full = path.join(folder, file);
|
|
2903
|
+
if (!existsSync(full)) continue;
|
|
2904
|
+
const logo = `data:${LOGO_FILES.get(path.extname(file).toLowerCase())};base64,${(await readFile(full)).toString("base64")}`;
|
|
2905
|
+
if (logo.length <= 524288) return logo;
|
|
2906
|
+
}
|
|
2907
|
+
return null;
|
|
2908
|
+
}
|
|
2909
|
+
function logoRank(file) {
|
|
2910
|
+
const preferred = PREFERRED.indexOf(path.basename(file).toLowerCase());
|
|
2911
|
+
if (preferred !== -1) return preferred;
|
|
2912
|
+
const extension = path.extname(file).toLowerCase();
|
|
2913
|
+
if (!LOGO_FILES.has(extension)) return -1;
|
|
2914
|
+
const words = path.basename(file, extension).replaceAll(/([a-z])([A-Z])/g, "$1 $2").toLowerCase().split(/[^a-z]+/);
|
|
2915
|
+
if (words.includes("logo") || words.includes("icon") || words.includes("favicon")) return PREFERRED.length;
|
|
2916
|
+
return -1;
|
|
2917
|
+
}
|
|
2918
|
+
//#endregion
|
|
2919
|
+
//#region src/services/projects.ts
|
|
2920
|
+
const LOGO_TYPES = [
|
|
2921
|
+
"image/png",
|
|
2922
|
+
"image/jpeg",
|
|
2923
|
+
"image/gif",
|
|
2924
|
+
"image/webp",
|
|
2925
|
+
"image/svg+xml",
|
|
2926
|
+
"image/x-icon",
|
|
2927
|
+
"image/vnd.microsoft.icon"
|
|
2928
|
+
];
|
|
2929
|
+
function projects$1() {
|
|
2930
|
+
return db.select().from(projects$2).all();
|
|
2931
|
+
}
|
|
2932
|
+
function projectPath(id) {
|
|
2933
|
+
const project = db.select().from(projects$2).where(eq(projects$2.id, id)).get();
|
|
2934
|
+
if (project === void 0) throw new Error("That project was removed");
|
|
2935
|
+
return project.path;
|
|
2936
|
+
}
|
|
2937
|
+
async function addProject(input) {
|
|
2938
|
+
const folder = path.resolve(expandHome(input.trim()));
|
|
2939
|
+
if (!(await stat(folder)).isDirectory()) throw new Error(`${folder} is not a folder`);
|
|
2940
|
+
db.insert(projects$2).values({
|
|
2941
|
+
id: randomUUID(),
|
|
2942
|
+
path: folder,
|
|
2943
|
+
name: path.basename(folder) || folder,
|
|
2944
|
+
logo: await findLogo(folder)
|
|
2945
|
+
}).onConflictDoNothing({ target: projects$2.path }).run();
|
|
2946
|
+
publish$1();
|
|
2947
|
+
const project = projects$1().find((item) => item.path === folder);
|
|
2948
|
+
if (project === void 0) throw new Error(`Could not add ${folder}`);
|
|
2949
|
+
return project;
|
|
2950
|
+
}
|
|
2951
|
+
function updateProject(id, patch) {
|
|
2952
|
+
if (patch.name !== void 0) {
|
|
2953
|
+
const name = patch.name.trim();
|
|
2954
|
+
if (name === "") throw new Error("Project name cannot be empty");
|
|
2955
|
+
db.update(projects$2).set({ name }).where(eq(projects$2.id, id)).run();
|
|
2956
|
+
}
|
|
2957
|
+
if (patch.logo !== void 0) {
|
|
2958
|
+
if (patch.logo !== null) checkLogo(patch.logo);
|
|
2959
|
+
db.update(projects$2).set({ logo: patch.logo }).where(eq(projects$2.id, id)).run();
|
|
2960
|
+
}
|
|
2961
|
+
publish$1();
|
|
2962
|
+
return null;
|
|
2963
|
+
}
|
|
2964
|
+
function removeProject(id) {
|
|
2965
|
+
db.delete(projects$2).where(eq(projects$2.id, id)).run();
|
|
2966
|
+
publish$1();
|
|
2967
|
+
}
|
|
2968
|
+
async function directories(input) {
|
|
2969
|
+
const folder = path.resolve(expandHome(input.trim()));
|
|
2970
|
+
const entries = await readdir(folder, { withFileTypes: true });
|
|
2971
|
+
const parent = path.dirname(folder);
|
|
2972
|
+
return {
|
|
2973
|
+
path: folder,
|
|
2974
|
+
parent: parent === folder ? null : parent,
|
|
2516
2975
|
directories: entries.filter((entry) => entry.isDirectory()).map((entry) => ({
|
|
2517
2976
|
name: entry.name,
|
|
2518
2977
|
path: path.join(folder, entry.name)
|
|
@@ -2528,7 +2987,7 @@ function publish$1() {
|
|
|
2528
2987
|
function checkLogo(logo) {
|
|
2529
2988
|
const type = /^data:([^;,]+);base64,/.exec(logo)?.[1];
|
|
2530
2989
|
if (type === void 0 || !LOGO_TYPES.includes(type)) throw new Error("Use a PNG, JPEG, GIF, WebP, SVG or ICO image");
|
|
2531
|
-
if (logo.length >
|
|
2990
|
+
if (logo.length > 524288) throw new Error("Use an image smaller than 384 KB");
|
|
2532
2991
|
}
|
|
2533
2992
|
function expandHome(input) {
|
|
2534
2993
|
if (input === "~") return os.homedir();
|
|
@@ -4072,6 +4531,7 @@ var Run = class {
|
|
|
4072
4531
|
model = null;
|
|
4073
4532
|
running = false;
|
|
4074
4533
|
prompted = false;
|
|
4534
|
+
compacting = false;
|
|
4075
4535
|
backgroundTasks = 0;
|
|
4076
4536
|
tasks = {};
|
|
4077
4537
|
inbox = [];
|
|
@@ -4303,6 +4763,10 @@ var Run = class {
|
|
|
4303
4763
|
broadcastActivity();
|
|
4304
4764
|
return;
|
|
4305
4765
|
case "status":
|
|
4766
|
+
if (this.compacting !== (message.status === "compacting")) {
|
|
4767
|
+
this.compacting = message.status === "compacting";
|
|
4768
|
+
broadcastActivity();
|
|
4769
|
+
}
|
|
4306
4770
|
if (message.permissionMode !== void 0) this.updateSettings({
|
|
4307
4771
|
...this.settings,
|
|
4308
4772
|
mode: message.permissionMode
|
|
@@ -4334,6 +4798,7 @@ var Run = class {
|
|
|
4334
4798
|
}
|
|
4335
4799
|
finishTurn() {
|
|
4336
4800
|
this.partial = "";
|
|
4801
|
+
this.compacting = false;
|
|
4337
4802
|
const [next, ...rest] = this.queue;
|
|
4338
4803
|
if (next !== void 0) {
|
|
4339
4804
|
this.queue = rest;
|
|
@@ -4497,462 +4962,87 @@ function readProjectFile(cwd, file) {
|
|
|
4497
4962
|
const mediaType = mediaTypes.get(path.extname(full).toLowerCase());
|
|
4498
4963
|
if (mediaType?.startsWith("image/") === true) return {
|
|
4499
4964
|
kind: "image",
|
|
4500
|
-
size: stats.size,
|
|
4501
|
-
mediaType,
|
|
4502
|
-
data: data.toString("base64")
|
|
4503
|
-
};
|
|
4504
|
-
if (data.includes(0)) return {
|
|
4505
|
-
kind: "binary",
|
|
4506
|
-
size: stats.size
|
|
4507
|
-
};
|
|
4508
|
-
return {
|
|
4509
|
-
kind: "text",
|
|
4510
|
-
size: stats.size,
|
|
4511
|
-
content: data.toString("utf8")
|
|
4512
|
-
};
|
|
4513
|
-
}
|
|
4514
|
-
function linkProjectMedia(cwd, file) {
|
|
4515
|
-
const full = resolveProjectFile(cwd, file);
|
|
4516
|
-
const mediaType = mediaTypes.get(path.extname(full).toLowerCase());
|
|
4517
|
-
if (mediaType === void 0) throw new Error(`${file} is not an image, video or audio file`);
|
|
4518
|
-
return addLink(full, mediaType, null);
|
|
4519
|
-
}
|
|
4520
|
-
function linkProjectDownload(cwd, file) {
|
|
4521
|
-
return addLink(resolveProjectFile(cwd, file), "application/octet-stream", path.basename(file));
|
|
4522
|
-
}
|
|
4523
|
-
function addLink(file, mediaType, download) {
|
|
4524
|
-
for (const [token, link] of mediaLinks) if (link.expiresAt <= Date.now()) mediaLinks.delete(token);
|
|
4525
|
-
const token = randomBytes(32).toString("base64url");
|
|
4526
|
-
mediaLinks.set(token, {
|
|
4527
|
-
file,
|
|
4528
|
-
mediaType,
|
|
4529
|
-
download,
|
|
4530
|
-
expiresAt: Date.now() + MEDIA_LINK_LIFETIME
|
|
4531
|
-
});
|
|
4532
|
-
return token;
|
|
4533
|
-
}
|
|
4534
|
-
function findMediaLink(token) {
|
|
4535
|
-
if (token === null) return null;
|
|
4536
|
-
const link = mediaLinks.get(token);
|
|
4537
|
-
if (link === void 0 || link.expiresAt <= Date.now()) return null;
|
|
4538
|
-
return link;
|
|
4539
|
-
}
|
|
4540
|
-
function saveUpload(name, data) {
|
|
4541
|
-
const dir = path.join(home, "uploads", randomUUID());
|
|
4542
|
-
mkdirSync(dir, { recursive: true });
|
|
4543
|
-
const file = path.join(dir, path.basename(name));
|
|
4544
|
-
writeFileSync(file, data);
|
|
4545
|
-
return file;
|
|
4546
|
-
}
|
|
4547
|
-
async function taskOutput(sessionId, file) {
|
|
4548
|
-
const tasks = path.dirname(file);
|
|
4549
|
-
if (!/^[\w-]+$/.test(sessionId) || !path.isAbsolute(file) || path.normalize(file) !== file || !/^\w+\.output$/.test(path.basename(file)) || path.basename(tasks) !== "tasks" || path.basename(path.dirname(tasks)) !== sessionId) throw new Error("Not a background task output file of this thread");
|
|
4550
|
-
let stats;
|
|
4551
|
-
try {
|
|
4552
|
-
stats = await lstat(file);
|
|
4553
|
-
} catch (error) {
|
|
4554
|
-
if (hasCode(error, "ENOENT")) return null;
|
|
4555
|
-
throw error;
|
|
4556
|
-
}
|
|
4557
|
-
if (!stats.isFile()) throw new Error("Background task output is not a regular file");
|
|
4558
|
-
const start = Math.max(0, stats.size - OUTPUT_LIMIT$1);
|
|
4559
|
-
const handle = await open(file, "r");
|
|
4560
|
-
try {
|
|
4561
|
-
const buffer = Buffer.alloc(stats.size - start);
|
|
4562
|
-
const read = await handle.read(buffer, 0, buffer.length, start);
|
|
4563
|
-
return {
|
|
4564
|
-
text: buffer.subarray(0, read.bytesRead).toString("utf8"),
|
|
4565
|
-
truncated: start > 0
|
|
4566
|
-
};
|
|
4567
|
-
} finally {
|
|
4568
|
-
await handle.close();
|
|
4569
|
-
}
|
|
4570
|
-
}
|
|
4571
|
-
function resolveProjectFile(cwd, file) {
|
|
4572
|
-
const root = realpathSync(cwd);
|
|
4573
|
-
const resolved = path.resolve(root, file);
|
|
4574
|
-
if (!existsSync(resolved)) throw new Error(`${file} does not exist`);
|
|
4575
|
-
const full = realpathSync(resolved);
|
|
4576
|
-
const relative = path.relative(root, full);
|
|
4577
|
-
if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) throw new Error(`${file} is outside the project`);
|
|
4578
|
-
return full;
|
|
4579
|
-
}
|
|
4580
|
-
//#endregion
|
|
4581
|
-
//#region src/services/git.ts
|
|
4582
|
-
const DIFF_LIMIT = 8e4;
|
|
4583
|
-
const UNTRACKED_LIMIT = 100;
|
|
4584
|
-
const NEW_FILE_LIMIT = 262144;
|
|
4585
|
-
const REPO_DEPTH = 3;
|
|
4586
|
-
const FETCH_TIMEOUT = 6e4;
|
|
4587
|
-
const run$1 = promisify(execFile);
|
|
4588
|
-
const PULL_REQUEST_INSTRUCTIONS = `You write GitHub pull requests as Markdown.
|
|
4589
|
-
|
|
4590
|
-
## TITLE RULES
|
|
4591
|
-
|
|
4592
|
-
- Use the conventional commits format: type(scope): subject
|
|
4593
|
-
- Types: feat, fix, docs, style, refactor, perf, test, chore, ci
|
|
4594
|
-
- Scope is optional; include it when the change targets a clear area
|
|
4595
|
-
- Subject: imperative mood, lowercase, no trailing period
|
|
4596
|
-
- Maximum 72 characters in total
|
|
4597
|
-
- Describe the overall change, not a single commit
|
|
4598
|
-
|
|
4599
|
-
## BODY RULES
|
|
4600
|
-
|
|
4601
|
-
- Use short Markdown sections with level-2 headings. Choose only sections that fit the change: \`New features\`, \`Improvements\`, \`Fixes\`, \`Before\` / \`After\`, or \`Breaking changes\`. Do not use all of them as a template.
|
|
4602
|
-
- Use bullet points under every section, never paragraphs. Each bullet describes one feature or change in one short sentence.
|
|
4603
|
-
- For a small change, use one section with one bullet. For larger changes, use only the sections needed, with at most three short bullets across the body.
|
|
4604
|
-
- Aim for 20-50 words; fewer is better when enough. Never exceed 80 words.
|
|
4605
|
-
- Describe the outcome, not the implementation walkthrough. Do not enumerate changed files, functions, commits, or incidental cleanup; reviewers can read the diff.
|
|
4606
|
-
- Add a \`Breaking changes\` section only for an actual compatibility break or required migration.
|
|
4607
|
-
- No generic \`Summary\` section, bold labels, checklists, boilerplate openings like "This pull request", or closing summaries.
|
|
4608
|
-
|
|
4609
|
-
## RULES
|
|
4610
|
-
|
|
4611
|
-
Base every statement on the provided commits and diff; do NOT invent details.
|
|
4612
|
-
|
|
4613
|
-
## OUTPUT
|
|
4614
|
-
|
|
4615
|
-
Return one Markdown document. The title is the level-1 heading (\`# <title>\`), and everything after it is the body.`;
|
|
4616
|
-
async function gitRepos(cwd) {
|
|
4617
|
-
return Promise.all((await projectRepos(cwd)).map(async (repo) => ({
|
|
4618
|
-
...await gitStatus(path.join(cwd, repo)),
|
|
4619
|
-
path: repo
|
|
4620
|
-
})));
|
|
4621
|
-
}
|
|
4622
|
-
async function commit(cwd, message, andPush) {
|
|
4623
|
-
const text = message.trim() === "" ? await commitMessage(cwd) : message;
|
|
4624
|
-
await git(cwd, ["add", "--all"]);
|
|
4625
|
-
await git(cwd, [
|
|
4626
|
-
"commit",
|
|
4627
|
-
"--message",
|
|
4628
|
-
text
|
|
4629
|
-
]);
|
|
4630
|
-
if (andPush) try {
|
|
4631
|
-
await push$1(cwd);
|
|
4632
|
-
} catch (error) {
|
|
4633
|
-
throw new Error(`Committed, but push failed: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
4634
|
-
}
|
|
4635
|
-
return { message: text };
|
|
4636
|
-
}
|
|
4637
|
-
async function push$1(cwd) {
|
|
4638
|
-
const status = await gitStatus(cwd);
|
|
4639
|
-
if (status.branch === null) throw new Error("Cannot push a detached HEAD");
|
|
4640
|
-
try {
|
|
4641
|
-
if (status.upstream === null) await git(cwd, [
|
|
4642
|
-
"push",
|
|
4643
|
-
"--set-upstream",
|
|
4644
|
-
"origin",
|
|
4645
|
-
"HEAD"
|
|
4646
|
-
]);
|
|
4647
|
-
else await git(cwd, ["push"]);
|
|
4648
|
-
} catch (error) {
|
|
4649
|
-
throw gitError(error);
|
|
4650
|
-
}
|
|
4651
|
-
}
|
|
4652
|
-
async function sync(cwd) {
|
|
4653
|
-
if ((await gitStatus(cwd)).upstream === null) {
|
|
4654
|
-
await push$1(cwd);
|
|
4655
|
-
return;
|
|
4656
|
-
}
|
|
4657
|
-
try {
|
|
4658
|
-
await git(cwd, ["fetch"]);
|
|
4659
|
-
const status = await gitStatus(cwd);
|
|
4660
|
-
if (status.behind > 0 && status.ahead === 0) await git(cwd, [
|
|
4661
|
-
"merge",
|
|
4662
|
-
"--ff-only",
|
|
4663
|
-
"@{upstream}"
|
|
4664
|
-
]);
|
|
4665
|
-
if (status.behind > 0 && status.ahead > 0) await rebaseOnUpstream(cwd);
|
|
4666
|
-
} catch (error) {
|
|
4667
|
-
throw gitError(error);
|
|
4668
|
-
}
|
|
4669
|
-
if ((await gitStatus(cwd)).ahead > 0) await push$1(cwd);
|
|
4670
|
-
}
|
|
4671
|
-
async function fetchProject(cwd) {
|
|
4672
|
-
await Promise.all((await projectRepos(cwd)).map(async (repo) => {
|
|
4673
|
-
try {
|
|
4674
|
-
await run$1("git", ["fetch"], {
|
|
4675
|
-
cwd: path.join(cwd, repo),
|
|
4676
|
-
env: {
|
|
4677
|
-
...process.env,
|
|
4678
|
-
GIT_TERMINAL_PROMPT: "0"
|
|
4679
|
-
},
|
|
4680
|
-
timeout: FETCH_TIMEOUT
|
|
4681
|
-
});
|
|
4682
|
-
} catch (error) {
|
|
4683
|
-
throw gitError(error);
|
|
4684
|
-
}
|
|
4685
|
-
}));
|
|
4686
|
-
}
|
|
4687
|
-
async function localBranches(cwd) {
|
|
4688
|
-
return (await git(cwd, [
|
|
4689
|
-
"branch",
|
|
4690
|
-
"--sort=-committerdate",
|
|
4691
|
-
"--format=%(refname:short)"
|
|
4692
|
-
])).stdout.split("\n").filter(Boolean);
|
|
4693
|
-
}
|
|
4694
|
-
async function switchBranch(cwd, branch, create) {
|
|
4695
|
-
await git(cwd, create ? [
|
|
4696
|
-
"switch",
|
|
4697
|
-
"--create",
|
|
4698
|
-
branch
|
|
4699
|
-
] : ["switch", branch]);
|
|
4700
|
-
}
|
|
4701
|
-
async function pullRequestBranches(cwd) {
|
|
4702
|
-
const head = (await gitStatus(cwd)).branch;
|
|
4703
|
-
if (head === null) throw new Error("Not on a branch");
|
|
4704
|
-
const base = (await run$1("gh", [
|
|
4705
|
-
"repo",
|
|
4706
|
-
"view",
|
|
4707
|
-
"--json",
|
|
4708
|
-
"defaultBranchRef",
|
|
4709
|
-
"--jq",
|
|
4710
|
-
".defaultBranchRef.name"
|
|
4711
|
-
], { cwd })).stdout.trim();
|
|
4712
|
-
if (base === head) throw new Error(`${head} is the default branch. Create a branch first.`);
|
|
4713
|
-
return {
|
|
4714
|
-
head,
|
|
4715
|
-
base,
|
|
4716
|
-
branches: (await git(cwd, [
|
|
4717
|
-
"ls-remote",
|
|
4718
|
-
"--heads",
|
|
4719
|
-
"origin"
|
|
4720
|
-
])).stdout.split("\n").filter(Boolean).map((line) => line.split("refs/heads/")[1]).filter((branch) => branch !== head)
|
|
4721
|
-
};
|
|
4722
|
-
}
|
|
4723
|
-
async function preparePullRequest(cwd, base, commitChanges) {
|
|
4724
|
-
const head = (await gitStatus(cwd)).branch;
|
|
4725
|
-
if (head === null) throw new Error("Not on a branch");
|
|
4726
|
-
if (commitChanges) await commit(cwd, "", true);
|
|
4727
|
-
const status = await gitStatus(cwd);
|
|
4728
|
-
if (status.upstream === null || status.ahead > 0) await push$1(cwd);
|
|
4729
|
-
const open = (await run$1("gh", [
|
|
4730
|
-
"pr",
|
|
4731
|
-
"list",
|
|
4732
|
-
"--head",
|
|
4733
|
-
head,
|
|
4734
|
-
"--base",
|
|
4735
|
-
base,
|
|
4736
|
-
"--state",
|
|
4737
|
-
"open",
|
|
4738
|
-
"--json",
|
|
4739
|
-
"url",
|
|
4740
|
-
"--jq",
|
|
4741
|
-
".[0].url // \"\""
|
|
4742
|
-
], { cwd })).stdout.trim();
|
|
4743
|
-
if (open !== "") return {
|
|
4744
|
-
kind: "open",
|
|
4745
|
-
url: open
|
|
4965
|
+
size: stats.size,
|
|
4966
|
+
mediaType,
|
|
4967
|
+
data: data.toString("base64")
|
|
4968
|
+
};
|
|
4969
|
+
if (data.includes(0)) return {
|
|
4970
|
+
kind: "binary",
|
|
4971
|
+
size: stats.size
|
|
4746
4972
|
};
|
|
4747
|
-
await git(cwd, [
|
|
4748
|
-
"fetch",
|
|
4749
|
-
"origin",
|
|
4750
|
-
base
|
|
4751
|
-
]);
|
|
4752
|
-
const commits = (await git(cwd, [
|
|
4753
|
-
"log",
|
|
4754
|
-
"--format=- %B",
|
|
4755
|
-
`origin/${base}..HEAD`
|
|
4756
|
-
])).stdout.trim();
|
|
4757
|
-
if (commits === "") throw new Error(`No commits between ${base} and ${head}`);
|
|
4758
|
-
const diff = (await git(cwd, [
|
|
4759
|
-
"diff",
|
|
4760
|
-
"--no-color",
|
|
4761
|
-
"--no-ext-diff",
|
|
4762
|
-
`origin/${base}...HEAD`
|
|
4763
|
-
])).stdout.slice(0, DIFF_LIMIT);
|
|
4764
|
-
const lines = (await generateText(cwd, PULL_REQUEST_INSTRUCTIONS, `Commits between base and head:\n${commits}\n\n${diff}\n\nGenerate the pull request Markdown document from the above.`)).split("\n");
|
|
4765
|
-
const title = lines[0].trim().replace(/^#{1,6}\s*/, "").replace(/^(\*\*|__|\*|_|`)+/, "").replace(/(\*\*|__|\*|_|`)+$/, "").trim();
|
|
4766
|
-
if (title === "") throw new Error("The model returned an empty title");
|
|
4767
4973
|
return {
|
|
4768
|
-
kind: "
|
|
4769
|
-
|
|
4770
|
-
|
|
4771
|
-
title,
|
|
4772
|
-
body: lines.slice(1).join("\n").trim()
|
|
4974
|
+
kind: "text",
|
|
4975
|
+
size: stats.size,
|
|
4976
|
+
content: data.toString("utf8")
|
|
4773
4977
|
};
|
|
4774
4978
|
}
|
|
4775
|
-
|
|
4776
|
-
const
|
|
4777
|
-
|
|
4778
|
-
|
|
4779
|
-
|
|
4780
|
-
|
|
4781
|
-
|
|
4782
|
-
request.body,
|
|
4783
|
-
"--base",
|
|
4784
|
-
request.base,
|
|
4785
|
-
"--head",
|
|
4786
|
-
request.head,
|
|
4787
|
-
"--assignee",
|
|
4788
|
-
"@me"
|
|
4789
|
-
], { cwd })).stdout.trim().split("\n").at(-1);
|
|
4790
|
-
if (url === void 0) throw new Error("gh did not return a pull request URL");
|
|
4791
|
-
return { url };
|
|
4792
|
-
}
|
|
4793
|
-
async function commitMessage(cwd) {
|
|
4794
|
-
const diff = (await repoDiff(cwd, "")).slice(0, DIFF_LIMIT);
|
|
4795
|
-
if (diff === "") throw new Error("There is nothing to commit");
|
|
4796
|
-
return generateText(cwd, "You write concise git commit messages.", `Write a git commit message for this diff. Reply with only the message: a short summary line, optionally followed by a blank line and a brief body.\n\n${diff}`);
|
|
4797
|
-
}
|
|
4798
|
-
async function generateText(cwd, instructions, prompt) {
|
|
4799
|
-
const settings = readConfig().commitMessage;
|
|
4800
|
-
const provider = settings.provider;
|
|
4801
|
-
const accountId = settings.accountId;
|
|
4802
|
-
const model = settings.model;
|
|
4803
|
-
if (provider === null || model === null) throw new Error("Choose a model in Settings → Git");
|
|
4804
|
-
assertEnabled(provider, accountId);
|
|
4805
|
-
if (provider === "codex") return codexText(accountId, cwd, model, instructions, prompt);
|
|
4806
|
-
if (provider === "opencode") return openCodeText(cwd, model, `${instructions}\n\n${prompt}`);
|
|
4807
|
-
const conversation = query({
|
|
4808
|
-
prompt,
|
|
4809
|
-
options: {
|
|
4810
|
-
...claudeOptions(accountId, cwd),
|
|
4811
|
-
systemPrompt: instructions,
|
|
4812
|
-
tools: [],
|
|
4813
|
-
model,
|
|
4814
|
-
maxTurns: 1,
|
|
4815
|
-
persistSession: false
|
|
4816
|
-
}
|
|
4817
|
-
});
|
|
4818
|
-
for await (const message of conversation) {
|
|
4819
|
-
if (message.type !== "result") continue;
|
|
4820
|
-
if (message.subtype !== "success") throw new Error(message.errors.join("\n"));
|
|
4821
|
-
if (message.is_error) throw new Error(message.result);
|
|
4822
|
-
return message.result.trim();
|
|
4823
|
-
}
|
|
4824
|
-
throw new Error("Claude did not reply");
|
|
4979
|
+
function linkProjectMedia(cwd, file) {
|
|
4980
|
+
const resolved = path.resolve(realpathSync(cwd), file);
|
|
4981
|
+
if (!existsSync(resolved)) throw new Error(`${file} does not exist`);
|
|
4982
|
+
const full = realpathSync(resolved);
|
|
4983
|
+
const mediaType = mediaTypes.get(path.extname(full).toLowerCase());
|
|
4984
|
+
if (mediaType === void 0) throw new Error(`${file} is not an image, video or audio file`);
|
|
4985
|
+
return addLink(full, mediaType, null);
|
|
4825
4986
|
}
|
|
4826
|
-
|
|
4827
|
-
|
|
4828
|
-
if (repos.length === 0) throw new Error(`${cwd} is not a git repository and has none inside it`);
|
|
4829
|
-
return (await Promise.all(repos.map((repo) => repoDiff(path.join(cwd, repo), repo === "" ? "" : `${repo}/`)))).join("");
|
|
4987
|
+
function linkProjectDownload(cwd, file) {
|
|
4988
|
+
return addLink(resolveProjectFile(cwd, file), "application/octet-stream", path.basename(file));
|
|
4830
4989
|
}
|
|
4831
|
-
function
|
|
4832
|
-
|
|
4833
|
-
|
|
4834
|
-
|
|
4990
|
+
function addLink(file, mediaType, download) {
|
|
4991
|
+
for (const [token, link] of mediaLinks) if (link.expiresAt <= Date.now()) mediaLinks.delete(token);
|
|
4992
|
+
const token = randomBytes(32).toString("base64url");
|
|
4993
|
+
mediaLinks.set(token, {
|
|
4994
|
+
file,
|
|
4995
|
+
mediaType,
|
|
4996
|
+
download,
|
|
4997
|
+
expiresAt: Date.now() + MEDIA_LINK_LIFETIME
|
|
4835
4998
|
});
|
|
4999
|
+
return token;
|
|
4836
5000
|
}
|
|
4837
|
-
|
|
4838
|
-
|
|
5001
|
+
function findMediaLink(token) {
|
|
5002
|
+
if (token === null) return null;
|
|
5003
|
+
const link = mediaLinks.get(token);
|
|
5004
|
+
if (link === void 0 || link.expiresAt <= Date.now()) return null;
|
|
5005
|
+
return link;
|
|
4839
5006
|
}
|
|
4840
|
-
|
|
4841
|
-
const
|
|
4842
|
-
|
|
4843
|
-
|
|
4844
|
-
|
|
4845
|
-
|
|
4846
|
-
]);
|
|
4847
|
-
const status = {
|
|
4848
|
-
branch: null,
|
|
4849
|
-
upstream: null,
|
|
4850
|
-
remote: (await git(cwd, ["remote"])).stdout.trim() !== "",
|
|
4851
|
-
ahead: 0,
|
|
4852
|
-
behind: 0,
|
|
4853
|
-
changed: 0
|
|
4854
|
-
};
|
|
4855
|
-
for (const line of output.stdout.split("\n").filter(Boolean)) {
|
|
4856
|
-
const [marker, key, ...rest] = line.split(" ");
|
|
4857
|
-
if (marker !== "#") status.changed++;
|
|
4858
|
-
else if (key === "branch.head") status.branch = rest[0] === "(detached)" ? null : rest[0];
|
|
4859
|
-
else if (key === "branch.upstream") status.upstream = rest[0];
|
|
4860
|
-
else if (key === "branch.ab") {
|
|
4861
|
-
status.ahead = Number(rest[0]);
|
|
4862
|
-
status.behind = -Number(rest[1]);
|
|
4863
|
-
}
|
|
4864
|
-
}
|
|
4865
|
-
return status;
|
|
5007
|
+
function saveUpload(name, data) {
|
|
5008
|
+
const dir = path.join(home, "uploads", randomUUID());
|
|
5009
|
+
mkdirSync(dir, { recursive: true });
|
|
5010
|
+
const file = path.join(dir, path.basename(name));
|
|
5011
|
+
writeFileSync(file, data);
|
|
5012
|
+
return file;
|
|
4866
5013
|
}
|
|
4867
|
-
async function
|
|
5014
|
+
async function taskOutput(sessionId, file) {
|
|
5015
|
+
const tasks = path.dirname(file);
|
|
5016
|
+
if (!/^[\w-]+$/.test(sessionId) || !path.isAbsolute(file) || path.normalize(file) !== file || !/^\w+\.output$/.test(path.basename(file)) || path.basename(tasks) !== "tasks" || path.basename(path.dirname(tasks)) !== sessionId) throw new Error("Not a background task output file of this thread");
|
|
5017
|
+
let stats;
|
|
4868
5018
|
try {
|
|
4869
|
-
await
|
|
5019
|
+
stats = await lstat(file);
|
|
4870
5020
|
} catch (error) {
|
|
4871
|
-
|
|
4872
|
-
"rev-parse",
|
|
4873
|
-
"--git-path",
|
|
4874
|
-
"rebase-merge"
|
|
4875
|
-
]);
|
|
4876
|
-
if (existsSync(path.resolve(cwd, rebasing.stdout.trim()))) {
|
|
4877
|
-
await git(cwd, ["rebase", "--abort"]);
|
|
4878
|
-
throw new Error("Your commits conflict with the remote branch, so nothing was pulled or pushed. Resolve it in a terminal.", { cause: error });
|
|
4879
|
-
}
|
|
5021
|
+
if (hasCode(error, "ENOENT")) return null;
|
|
4880
5022
|
throw error;
|
|
4881
5023
|
}
|
|
4882
|
-
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
}
|
|
4886
|
-
async function isRepo(cwd) {
|
|
4887
|
-
try {
|
|
4888
|
-
await git(cwd, ["rev-parse", "--is-inside-work-tree"]);
|
|
4889
|
-
return true;
|
|
4890
|
-
} catch {
|
|
4891
|
-
return false;
|
|
4892
|
-
}
|
|
4893
|
-
}
|
|
4894
|
-
async function repoDiff(repo, prefix) {
|
|
4895
|
-
const tracked = await git(repo, [
|
|
4896
|
-
"--no-optional-locks",
|
|
4897
|
-
"diff",
|
|
4898
|
-
await diffBase(repo),
|
|
4899
|
-
"--no-color",
|
|
4900
|
-
"--no-ext-diff",
|
|
4901
|
-
`--src-prefix=a/${prefix}`,
|
|
4902
|
-
`--dst-prefix=b/${prefix}`
|
|
4903
|
-
]);
|
|
4904
|
-
const added = (await git(repo, [
|
|
4905
|
-
"ls-files",
|
|
4906
|
-
"--others",
|
|
4907
|
-
"--exclude-standard"
|
|
4908
|
-
])).stdout.split("\n").filter(Boolean).slice(0, UNTRACKED_LIMIT).map((file) => newFilePatch(repo, file, prefix));
|
|
4909
|
-
return [tracked.stdout, ...added].join("");
|
|
4910
|
-
}
|
|
4911
|
-
async function diffBase(repo) {
|
|
4912
|
-
try {
|
|
4913
|
-
await git(repo, [
|
|
4914
|
-
"rev-parse",
|
|
4915
|
-
"--verify",
|
|
4916
|
-
"--quiet",
|
|
4917
|
-
"HEAD"
|
|
4918
|
-
]);
|
|
4919
|
-
return "HEAD";
|
|
4920
|
-
} catch (error) {
|
|
4921
|
-
if (!(error instanceof Error && "code" in error && error.code === 1)) throw error;
|
|
4922
|
-
}
|
|
4923
|
-
return (await git(repo, [
|
|
4924
|
-
"hash-object",
|
|
4925
|
-
"-t",
|
|
4926
|
-
"tree",
|
|
4927
|
-
"/dev/null"
|
|
4928
|
-
])).stdout.trim();
|
|
4929
|
-
}
|
|
4930
|
-
function childRepos(root, dir, depth) {
|
|
4931
|
-
if (depth === 0) return [];
|
|
4932
|
-
let entries;
|
|
5024
|
+
if (!stats.isFile()) throw new Error("Background task output is not a regular file");
|
|
5025
|
+
const start = Math.max(0, stats.size - OUTPUT_LIMIT$1);
|
|
5026
|
+
const handle = await open(file, "r");
|
|
4933
5027
|
try {
|
|
4934
|
-
|
|
4935
|
-
|
|
4936
|
-
return
|
|
5028
|
+
const buffer = Buffer.alloc(stats.size - start);
|
|
5029
|
+
const read = await handle.read(buffer, 0, buffer.length, start);
|
|
5030
|
+
return {
|
|
5031
|
+
text: buffer.subarray(0, read.bytesRead).toString("utf8"),
|
|
5032
|
+
truncated: start > 0
|
|
5033
|
+
};
|
|
5034
|
+
} finally {
|
|
5035
|
+
await handle.close();
|
|
4937
5036
|
}
|
|
4938
|
-
return entries.filter((entry) => entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules").flatMap((entry) => {
|
|
4939
|
-
const relative = dir === "" ? entry.name : `${dir}/${entry.name}`;
|
|
4940
|
-
return existsSync(path.join(root, relative, ".git")) ? [relative] : childRepos(root, relative, depth - 1);
|
|
4941
|
-
});
|
|
4942
5037
|
}
|
|
4943
|
-
function
|
|
4944
|
-
const
|
|
4945
|
-
const
|
|
4946
|
-
|
|
4947
|
-
const
|
|
4948
|
-
|
|
4949
|
-
if (
|
|
4950
|
-
|
|
4951
|
-
const content = readFileSync(full, "utf8");
|
|
4952
|
-
if (content.includes("\0")) return `${header}Binary or large file\n`;
|
|
4953
|
-
const lines = content.split("\n");
|
|
4954
|
-
if (lines.at(-1) === "") lines.pop();
|
|
4955
|
-
return `${header}@@ -0,0 +1,${lines.length} @@\n${lines.map((line) => `+${line}\n`).join("")}`;
|
|
5038
|
+
function resolveProjectFile(cwd, file) {
|
|
5039
|
+
const root = realpathSync(cwd);
|
|
5040
|
+
const resolved = path.resolve(root, file);
|
|
5041
|
+
if (!existsSync(resolved)) throw new Error(`${file} does not exist`);
|
|
5042
|
+
const full = realpathSync(resolved);
|
|
5043
|
+
const relative = path.relative(root, full);
|
|
5044
|
+
if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) throw new Error(`${file} is outside the project`);
|
|
5045
|
+
return full;
|
|
4956
5046
|
}
|
|
4957
5047
|
//#endregion
|
|
4958
5048
|
//#region src/services/runs.ts
|