@agentrific/server 0.4.0 → 0.5.1
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/CHANGELOG.md +14 -0
- package/cli.js +222 -41
- package/cli.js.map +4 -4
- package/index.js +229 -59
- package/index.js.map +4 -4
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -1,27 +1,7 @@
|
|
|
1
|
-
// src/warnings.ts
|
|
2
|
-
var handlers = process.rawListeners("warning");
|
|
3
|
-
process.removeAllListeners("warning");
|
|
4
|
-
process.on("warning", (warning) => {
|
|
5
|
-
if (warning.name === "ExperimentalWarning" && warning.message === "SQLite is an experimental feature and might change at any time")
|
|
6
|
-
return;
|
|
7
|
-
for (const handler of handlers) handler.call(process, warning);
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
// src/push.ts
|
|
11
|
-
import {
|
|
12
|
-
generateKeyPairSync,
|
|
13
|
-
createPrivateKey,
|
|
14
|
-
createPublicKey,
|
|
15
|
-
sign,
|
|
16
|
-
randomUUID
|
|
17
|
-
} from "node:crypto";
|
|
18
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
19
|
-
import path from "node:path";
|
|
20
|
-
import { Effect } from "effect";
|
|
21
|
-
|
|
22
1
|
// ../../packages/contracts/src/push.ts
|
|
23
2
|
import { z } from "zod";
|
|
24
3
|
var PUSH_API_URL = "https://api.agentrific.com";
|
|
4
|
+
var PUSH_SANDBOX_API_URL = "https://sandbox-api.agentrific.com";
|
|
25
5
|
var PushKindSchema = z.enum([
|
|
26
6
|
"started",
|
|
27
7
|
"completed",
|
|
@@ -85,7 +65,26 @@ function pushSigningText(installationId, operation, request) {
|
|
|
85
65
|
].join("\n");
|
|
86
66
|
}
|
|
87
67
|
|
|
68
|
+
// src/warnings.ts
|
|
69
|
+
var handlers = process.rawListeners("warning");
|
|
70
|
+
process.removeAllListeners("warning");
|
|
71
|
+
process.on("warning", (warning) => {
|
|
72
|
+
if (warning.name === "ExperimentalWarning" && warning.message === "SQLite is an experimental feature and might change at any time")
|
|
73
|
+
return;
|
|
74
|
+
for (const handler of handlers) handler.call(process, warning);
|
|
75
|
+
});
|
|
76
|
+
|
|
88
77
|
// src/push.ts
|
|
78
|
+
import {
|
|
79
|
+
generateKeyPairSync,
|
|
80
|
+
createPrivateKey,
|
|
81
|
+
createPublicKey,
|
|
82
|
+
sign,
|
|
83
|
+
randomUUID
|
|
84
|
+
} from "node:crypto";
|
|
85
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
86
|
+
import path from "node:path";
|
|
87
|
+
import { Effect } from "effect";
|
|
89
88
|
function pushKind(session, previous) {
|
|
90
89
|
if (session.gitDelivery?.status === "pr_opened" && previous?.gitDelivery?.status !== "pr_opened")
|
|
91
90
|
return "pr_created";
|
|
@@ -586,11 +585,25 @@ var ConnectionsStatusSchema = z2.object({
|
|
|
586
585
|
githubCli: z2.object({ installed: z2.boolean(), authenticated: z2.boolean() })
|
|
587
586
|
});
|
|
588
587
|
var UpdateSettingsSchema = z2.object({ projectsRoot: z2.string().trim().min(1).max(4096) });
|
|
588
|
+
var RepositoryIdentitySchema = z2.object({
|
|
589
|
+
key: z2.string(),
|
|
590
|
+
display: z2.string()
|
|
591
|
+
});
|
|
592
|
+
var ProjectGitStatusSchema = z2.object({
|
|
593
|
+
observedAt: z2.string().datetime(),
|
|
594
|
+
state: z2.enum(["available", "unavailable"]),
|
|
595
|
+
repository: RepositoryIdentitySchema.nullable(),
|
|
596
|
+
head: z2.string().nullable(),
|
|
597
|
+
branch: z2.string().nullable(),
|
|
598
|
+
trackedChanges: z2.number().int().nonnegative(),
|
|
599
|
+
untrackedChanges: z2.number().int().nonnegative()
|
|
600
|
+
});
|
|
589
601
|
var ProjectSchema = z2.object({
|
|
590
602
|
id: z2.string(),
|
|
591
603
|
name: z2.string(),
|
|
592
604
|
path: z2.string(),
|
|
593
|
-
available: z2.boolean().default(true)
|
|
605
|
+
available: z2.boolean().default(true),
|
|
606
|
+
repository: RepositoryIdentitySchema.nullable().optional()
|
|
594
607
|
});
|
|
595
608
|
var TaskStatusSchema = z2.enum(["backlog", "needs_user_input", "running", "done"]);
|
|
596
609
|
var TaskSchema = z2.object({
|
|
@@ -609,6 +622,7 @@ var TaskSchema = z2.object({
|
|
|
609
622
|
deletedAt: z2.string().datetime().optional()
|
|
610
623
|
});
|
|
611
624
|
var CreateTaskSchema = z2.object({
|
|
625
|
+
clientTaskId: z2.string().min(1).max(128).regex(/^[a-zA-Z0-9_-]+$/).optional(),
|
|
612
626
|
projectId: z2.string().min(1),
|
|
613
627
|
title: z2.string().trim().min(1).max(120),
|
|
614
628
|
prompt: z2.string().trim().max(2e4).default(""),
|
|
@@ -871,8 +885,8 @@ var Attachments = class extends Context.Service()(
|
|
|
871
885
|
|
|
872
886
|
// src/index.ts
|
|
873
887
|
import { randomUUID as randomUUID9 } from "node:crypto";
|
|
874
|
-
import { execFile as
|
|
875
|
-
import { promisify as
|
|
888
|
+
import { execFile as execFile3 } from "node:child_process";
|
|
889
|
+
import { promisify as promisify3 } from "node:util";
|
|
876
890
|
import { realpath as realpath2 } from "node:fs/promises";
|
|
877
891
|
import { homedir as homedir2 } from "node:os";
|
|
878
892
|
import path9 from "node:path";
|
|
@@ -1097,7 +1111,7 @@ async function startControl(directory, security2, status, maintenance2) {
|
|
|
1097
1111
|
// package.json
|
|
1098
1112
|
var package_default = {
|
|
1099
1113
|
name: "@agentrific/server",
|
|
1100
|
-
version: "0.
|
|
1114
|
+
version: "0.5.1",
|
|
1101
1115
|
description: "Self-hosted control plane for long-running coding agents",
|
|
1102
1116
|
keywords: [
|
|
1103
1117
|
"agentrific",
|
|
@@ -1608,7 +1622,7 @@ function editTask(store2, id, input, resolveAttachments) {
|
|
|
1608
1622
|
});
|
|
1609
1623
|
}
|
|
1610
1624
|
function deleteTask(store2, id, cleanup = async () => {
|
|
1611
|
-
}) {
|
|
1625
|
+
}, unrunUpdatedAt) {
|
|
1612
1626
|
return withTaskLock(id, async () => {
|
|
1613
1627
|
const task = await findTask(store2, id);
|
|
1614
1628
|
const sessions = await Effect6.runPromise(store2.list());
|
|
@@ -1619,6 +1633,8 @@ function deleteTask(store2, id, cleanup = async () => {
|
|
|
1619
1633
|
const associated = sessions.filter(
|
|
1620
1634
|
(session) => session.taskId === task.id || session.id === task.sessionId
|
|
1621
1635
|
);
|
|
1636
|
+
if (unrunUpdatedAt !== void 0 && (task.updatedAt !== unrunUpdatedAt || task.sessionId || associated.length || task.status !== "backlog"))
|
|
1637
|
+
throw new TaskActionError("This task changed or has already run. It cannot be moved.", 409);
|
|
1622
1638
|
for (const session of associated) await cleanup(session);
|
|
1623
1639
|
await Effect6.runPromise(
|
|
1624
1640
|
store2.purgeTask(
|
|
@@ -2027,7 +2043,7 @@ var exec = promisify(execFile);
|
|
|
2027
2043
|
function workflowLog(text) {
|
|
2028
2044
|
return stripVTControlCharacters(text).slice(-32e3);
|
|
2029
2045
|
}
|
|
2030
|
-
|
|
2046
|
+
function workflowEnvironment() {
|
|
2031
2047
|
const env = { ...process.env };
|
|
2032
2048
|
for (const name of [
|
|
2033
2049
|
"GIT_DIR",
|
|
@@ -2047,6 +2063,10 @@ async function workflowCommand(cwd, command, args, signal, managedPreparation =
|
|
|
2047
2063
|
"GIT_REPLACE_REF_BASE"
|
|
2048
2064
|
])
|
|
2049
2065
|
delete env[name];
|
|
2066
|
+
return env;
|
|
2067
|
+
}
|
|
2068
|
+
async function workflowCommand(cwd, command, args, signal, managedPreparation = false) {
|
|
2069
|
+
const env = workflowEnvironment();
|
|
2050
2070
|
if (managedPreparation) env.AGENTRIFIC_MANAGED_PREPARATION = "1";
|
|
2051
2071
|
const result = await exec(command, args, {
|
|
2052
2072
|
cwd,
|
|
@@ -3211,6 +3231,106 @@ function reconnectProject(root, id, input, store2) {
|
|
|
3211
3231
|
}).pipe(projectLock(store2).withPermit);
|
|
3212
3232
|
}
|
|
3213
3233
|
|
|
3234
|
+
// src/project-git.ts
|
|
3235
|
+
import { execFile as execFile2 } from "node:child_process";
|
|
3236
|
+
import { promisify as promisify2 } from "node:util";
|
|
3237
|
+
var execute = promisify2(execFile2);
|
|
3238
|
+
function repositoryIdentity(remote) {
|
|
3239
|
+
const input = remote.trim();
|
|
3240
|
+
if (!input || input.startsWith("/") || input.startsWith(".")) return null;
|
|
3241
|
+
const scp = /^(?:[^@/\s]+@)?([^/:\s]+):([^\s]+)$/.exec(input);
|
|
3242
|
+
let url;
|
|
3243
|
+
try {
|
|
3244
|
+
url = new URL(scp && !input.includes("://") ? `ssh://${scp[1]}/${scp[2]}` : input);
|
|
3245
|
+
} catch {
|
|
3246
|
+
return null;
|
|
3247
|
+
}
|
|
3248
|
+
if (!["https:", "http:", "ssh:", "git:"].includes(url.protocol)) return null;
|
|
3249
|
+
if (url.search || url.hash || url.port) return null;
|
|
3250
|
+
const host2 = url.hostname.toLowerCase();
|
|
3251
|
+
let path10 = url.pathname.replace(/^\/+|\/+$/g, "").replace(/\.git$/, "");
|
|
3252
|
+
if (!host2 || !path10 || path10.includes("%") || path10.includes("//")) return null;
|
|
3253
|
+
if (["github.com", "bitbucket.org"].includes(host2)) path10 = path10.toLowerCase();
|
|
3254
|
+
const key = `${host2}/${path10}`;
|
|
3255
|
+
return { key, display: key };
|
|
3256
|
+
}
|
|
3257
|
+
async function git(directory, args) {
|
|
3258
|
+
const { stdout } = await execute("git", ["-C", directory, ...args], {
|
|
3259
|
+
timeout: 3e3,
|
|
3260
|
+
maxBuffer: 1024 * 1024,
|
|
3261
|
+
env: { ...workflowEnvironment(), GIT_OPTIONAL_LOCKS: "0", GIT_TERMINAL_PROMPT: "0" }
|
|
3262
|
+
});
|
|
3263
|
+
return stdout.trim();
|
|
3264
|
+
}
|
|
3265
|
+
async function readRepositoryIdentity(directory) {
|
|
3266
|
+
try {
|
|
3267
|
+
const remotes = (await git(directory, ["remote"])).split("\n").filter(Boolean);
|
|
3268
|
+
const remote = remotes.includes("origin") ? "origin" : remotes.length === 1 ? remotes[0] : null;
|
|
3269
|
+
if (!remote) return null;
|
|
3270
|
+
return repositoryIdentity(await git(directory, ["remote", "get-url", remote]));
|
|
3271
|
+
} catch {
|
|
3272
|
+
return null;
|
|
3273
|
+
}
|
|
3274
|
+
}
|
|
3275
|
+
async function inspect(directory) {
|
|
3276
|
+
const observedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3277
|
+
try {
|
|
3278
|
+
const [repository, head, branch, status] = await Promise.all([
|
|
3279
|
+
readRepositoryIdentity(directory),
|
|
3280
|
+
git(directory, ["rev-parse", "--verify", "HEAD"]),
|
|
3281
|
+
git(directory, ["symbolic-ref", "--quiet", "--short", "HEAD"]).catch(() => null),
|
|
3282
|
+
// NUL-separated records avoid ambiguous filenames/newlines. --no-renames
|
|
3283
|
+
// keeps each entry one record, so we only need its two status characters.
|
|
3284
|
+
execute(
|
|
3285
|
+
"git",
|
|
3286
|
+
[
|
|
3287
|
+
"-C",
|
|
3288
|
+
directory,
|
|
3289
|
+
"status",
|
|
3290
|
+
"--porcelain=v1",
|
|
3291
|
+
"-z",
|
|
3292
|
+
"--no-renames",
|
|
3293
|
+
"--untracked-files=normal"
|
|
3294
|
+
],
|
|
3295
|
+
{
|
|
3296
|
+
timeout: 3e3,
|
|
3297
|
+
maxBuffer: 1024 * 1024,
|
|
3298
|
+
env: { ...workflowEnvironment(), GIT_OPTIONAL_LOCKS: "0" }
|
|
3299
|
+
}
|
|
3300
|
+
)
|
|
3301
|
+
]);
|
|
3302
|
+
const records = status.stdout.split("\0").filter(Boolean);
|
|
3303
|
+
return {
|
|
3304
|
+
observedAt,
|
|
3305
|
+
state: "available",
|
|
3306
|
+
repository,
|
|
3307
|
+
head,
|
|
3308
|
+
branch,
|
|
3309
|
+
trackedChanges: records.filter((record) => !record.startsWith("??")).length,
|
|
3310
|
+
untrackedChanges: records.filter((record) => record.startsWith("??")).length
|
|
3311
|
+
};
|
|
3312
|
+
} catch {
|
|
3313
|
+
return {
|
|
3314
|
+
observedAt,
|
|
3315
|
+
state: "unavailable",
|
|
3316
|
+
repository: null,
|
|
3317
|
+
head: null,
|
|
3318
|
+
branch: null,
|
|
3319
|
+
trackedChanges: 0,
|
|
3320
|
+
untrackedChanges: 0
|
|
3321
|
+
};
|
|
3322
|
+
}
|
|
3323
|
+
}
|
|
3324
|
+
var observations = /* @__PURE__ */ new Map();
|
|
3325
|
+
function projectGitStatus(directory) {
|
|
3326
|
+
const cached = observations.get(directory);
|
|
3327
|
+
if (cached && cached.expires > Date.now()) return cached.value;
|
|
3328
|
+
if (observations.size >= 200) observations.delete(observations.keys().next().value);
|
|
3329
|
+
const value = inspect(directory);
|
|
3330
|
+
observations.set(directory, { expires: Date.now() + 15e3, value });
|
|
3331
|
+
return value;
|
|
3332
|
+
}
|
|
3333
|
+
|
|
3214
3334
|
// src/store.ts
|
|
3215
3335
|
import { chmodSync, existsSync as existsSync2, mkdirSync as mkdirSync2 } from "node:fs";
|
|
3216
3336
|
import path7 from "node:path";
|
|
@@ -3777,7 +3897,7 @@ var defaultProjectsRoot = path9.resolve(process.env.AGENTRIFIC_PROJECTS_DIR ?? "
|
|
|
3777
3897
|
var worktreesRoot = path9.resolve(
|
|
3778
3898
|
process.env.AGENTRIFIC_WORKTREES_DIR ?? path9.join(homedir2(), ".agentrific", "workspaces")
|
|
3779
3899
|
);
|
|
3780
|
-
var execFileAsync =
|
|
3900
|
+
var execFileAsync = promisify3(execFile3);
|
|
3781
3901
|
if (host !== "127.0.0.1" && host !== "::1")
|
|
3782
3902
|
throw new Error("Bind Agentrific to loopback and expose it with Tailscale Serve or cloudflared.");
|
|
3783
3903
|
var app = Fastify({
|
|
@@ -3803,10 +3923,19 @@ var releaseLock = await acquireServerLock(dataRoot);
|
|
|
3803
3923
|
var effects = ManagedRuntime.make(SessionStore.layer(path9.join(dataRoot, "sessions.sqlite")));
|
|
3804
3924
|
var store = await effects.runPromise(SessionStore);
|
|
3805
3925
|
var security = new ServerSecurity(store, await Effect17.runPromise(store.serverIdentity()));
|
|
3926
|
+
var sandboxPushUrl = process.env.AGENTRIFIC_PUSH_SANDBOX === "1" ? PUSH_SANDBOX_API_URL : void 0;
|
|
3927
|
+
if (sandboxPushUrl && process.env.AGENTRIFIC_TEST_MODE !== "1")
|
|
3928
|
+
throw new Error("Sandbox push requires explicit test mode.");
|
|
3806
3929
|
var testPushUrl = process.env.AGENTRIFIC_PUSH_TEST_URL;
|
|
3807
3930
|
if (testPushUrl && (process.env.AGENTRIFIC_TEST_MODE !== "1" || !new URL(testPushUrl).hostname.endsWith(".ts.net") || new URL(testPushUrl).protocol !== "https:"))
|
|
3808
3931
|
throw new Error("Test relay requires explicit test mode and Tailscale HTTPS.");
|
|
3809
|
-
var push = new ServerPush(
|
|
3932
|
+
var push = new ServerPush(
|
|
3933
|
+
store,
|
|
3934
|
+
security.serverId,
|
|
3935
|
+
dataRoot,
|
|
3936
|
+
fetch,
|
|
3937
|
+
testPushUrl ?? sandboxPushUrl
|
|
3938
|
+
);
|
|
3810
3939
|
var allowedOrigins = [
|
|
3811
3940
|
.../* @__PURE__ */ new Set([
|
|
3812
3941
|
"agentrific-desktop://app",
|
|
@@ -4054,7 +4183,24 @@ app.get("/api/projects", async (_request, reply) => {
|
|
|
4054
4183
|
const root = await Effect17.runPromise(store.projectsRoot(defaultProjectsRoot));
|
|
4055
4184
|
const result = await Effect17.runPromise(discoverProjects(root, store).pipe(Effect17.result));
|
|
4056
4185
|
if (result._tag === "Failure") return reply.code(400).send({ error: result.failure.message });
|
|
4057
|
-
return
|
|
4186
|
+
return Effect17.runPromise(
|
|
4187
|
+
Effect17.forEach(
|
|
4188
|
+
result.success,
|
|
4189
|
+
(project) => Effect17.promise(async () => ({
|
|
4190
|
+
...project,
|
|
4191
|
+
repository: project.available ? await readRepositoryIdentity(project.path) : null
|
|
4192
|
+
})),
|
|
4193
|
+
{ concurrency: 4 }
|
|
4194
|
+
)
|
|
4195
|
+
);
|
|
4196
|
+
});
|
|
4197
|
+
app.get("/api/projects/:id/git-status", async (request, reply) => {
|
|
4198
|
+
const root = await Effect17.runPromise(store.projectsRoot(defaultProjectsRoot));
|
|
4199
|
+
const projects = await Effect17.runPromise(discoverProjects(root, store));
|
|
4200
|
+
const project = projects.find((item) => item.id === request.params.id);
|
|
4201
|
+
if (!project?.available)
|
|
4202
|
+
return reply.code(404).send({ error: "Project folder unavailable on this server." });
|
|
4203
|
+
return projectGitStatus(project.path);
|
|
4058
4204
|
});
|
|
4059
4205
|
app.get("/api/projects/:id", async (request, reply) => {
|
|
4060
4206
|
const root = await Effect17.runPromise(store.projectsRoot(defaultProjectsRoot));
|
|
@@ -4078,39 +4224,51 @@ app.get(
|
|
|
4078
4224
|
"/api/tasks",
|
|
4079
4225
|
(request) => boardTasks(store, request.query.projectId)
|
|
4080
4226
|
);
|
|
4227
|
+
app.get("/api/task-moves", () => ({ supported: true }));
|
|
4081
4228
|
app.post("/api/tasks", async (request, reply) => {
|
|
4082
4229
|
const parsed = CreateTaskSchema.safeParse(request.body);
|
|
4083
4230
|
if (!parsed.success) return reply.code(400).send({ error: "Enter a task title." });
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
4231
|
+
const save = async () => {
|
|
4232
|
+
if (parsed.data.clientTaskId) {
|
|
4233
|
+
const existing = await Effect17.runPromise(store.getTask(parsed.data.clientTaskId));
|
|
4234
|
+
if (existing) {
|
|
4235
|
+
if (existing.deletedAt || existing.projectId !== parsed.data.projectId || existing.title !== parsed.data.title || existing.prompt !== parsed.data.prompt || JSON.stringify(existing.gitFlow) !== JSON.stringify(parsed.data.gitFlow) || JSON.stringify(existing.model) !== JSON.stringify(parsed.data.model))
|
|
4236
|
+
return reply.code(409).send({ error: "This task creation request has already been used." });
|
|
4237
|
+
return reply.code(201).send(existing);
|
|
4238
|
+
}
|
|
4239
|
+
}
|
|
4240
|
+
if (parsed.data.gitFlow?.delivery === "pull_request") {
|
|
4241
|
+
const github = await githubCliStatus();
|
|
4242
|
+
if (!github.githubCli.authenticated)
|
|
4243
|
+
return reply.code(400).send({ error: "GitHub CLI must be authenticated on the server to open a PR." });
|
|
4244
|
+
}
|
|
4245
|
+
const projectsRoot = await Effect17.runPromise(store.projectsRoot(defaultProjectsRoot));
|
|
4246
|
+
const projects = await Effect17.runPromise(
|
|
4247
|
+
discoverProjects(projectsRoot, store).pipe(Effect17.result)
|
|
4099
4248
|
);
|
|
4100
|
-
|
|
4101
|
-
|
|
4102
|
-
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
|
|
4111
|
-
|
|
4112
|
-
|
|
4113
|
-
|
|
4249
|
+
if (projects._tag === "Failure" || !projects.success.some((project) => project.id === parsed.data.projectId && project.available))
|
|
4250
|
+
return reply.code(404).send({ error: "Project not found in the server's projects folder." });
|
|
4251
|
+
let files;
|
|
4252
|
+
try {
|
|
4253
|
+
files = await Effect17.runPromise(
|
|
4254
|
+
attachments.resolve(parsed.data.projectId, parsed.data.attachmentIds ?? [])
|
|
4255
|
+
);
|
|
4256
|
+
} catch (error) {
|
|
4257
|
+
return reply.code(400).send({ error: String(error) });
|
|
4258
|
+
}
|
|
4259
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4260
|
+
const task = TaskSchema.parse({
|
|
4261
|
+
id: parsed.data.clientTaskId ?? randomUUID9(),
|
|
4262
|
+
...parsed.data,
|
|
4263
|
+
attachments: files,
|
|
4264
|
+
status: "backlog",
|
|
4265
|
+
sessionId: null,
|
|
4266
|
+
createdAt: now,
|
|
4267
|
+
updatedAt: now
|
|
4268
|
+
});
|
|
4269
|
+
return reply.code(201).send(await Effect17.runPromise(store.saveTask(task)));
|
|
4270
|
+
};
|
|
4271
|
+
return parsed.data.clientTaskId ? withTaskLock(`create-task:${parsed.data.clientTaskId}`, save) : save();
|
|
4114
4272
|
});
|
|
4115
4273
|
app.patch("/api/tasks/:id", async (request, reply) => {
|
|
4116
4274
|
const parsed = UpdateTaskSchema.safeParse(request.body);
|
|
@@ -4137,6 +4295,18 @@ app.patch("/api/tasks/:id", async (request, reply) => {
|
|
|
4137
4295
|
throw error;
|
|
4138
4296
|
}
|
|
4139
4297
|
});
|
|
4298
|
+
app.post("/api/tasks/:id/discard-unrun", async (request, reply) => {
|
|
4299
|
+
const parsed = z17.object({ updatedAt: z17.string().datetime() }).safeParse(request.body);
|
|
4300
|
+
if (!parsed.success) return reply.code(400).send({ error: "The task revision is required." });
|
|
4301
|
+
try {
|
|
4302
|
+
await deleteTask(store, request.params.id, void 0, parsed.data.updatedAt);
|
|
4303
|
+
return reply.code(204).send();
|
|
4304
|
+
} catch (error) {
|
|
4305
|
+
if (error instanceof TaskActionError)
|
|
4306
|
+
return reply.code(error.status).send({ error: error.message });
|
|
4307
|
+
throw error;
|
|
4308
|
+
}
|
|
4309
|
+
});
|
|
4140
4310
|
app.delete("/api/tasks/:id", async (request, reply) => {
|
|
4141
4311
|
try {
|
|
4142
4312
|
await deleteTask(store, request.params.id, async (session) => {
|