@hasna/todos 0.15.17 → 0.15.19
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/cli/cloud-router.d.ts +7 -1
- package/dist/cli/cloud-router.d.ts.map +1 -1
- package/dist/cli/commands/project-commands.d.ts.map +1 -1
- package/dist/cli/commands/task-commands.d.ts.map +1 -1
- package/dist/cli/index.js +478 -88
- package/dist/contracts.js +57 -2
- package/dist/db/plans.d.ts +4 -0
- package/dist/db/plans.d.ts.map +1 -1
- package/dist/index.js +122 -8
- package/dist/mcp/index.js +268 -21
- package/dist/mcp.js +1 -1
- package/dist/project-registration.js +113 -8
- package/dist/registry.js +57 -2
- package/dist/release-provenance.json +5 -5
- package/dist/sdk/v1.generated.d.ts +13 -1
- package/dist/sdk/v1.generated.d.ts.map +1 -1
- package/dist/server/index.js +268 -21
- package/dist/server/openapi.d.ts +50 -0
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/v1.d.ts.map +1 -1
- package/dist/storage/interfaces.d.ts +10 -0
- package/dist/storage/interfaces.d.ts.map +1 -1
- package/dist/storage/local-sqlite.d.ts.map +1 -1
- package/dist/storage/postgres-adapter.d.ts.map +1 -1
- package/dist/storage/shadow.d.ts.map +1 -1
- package/dist/storage/sqlite-snapshot.d.ts.map +1 -1
- package/dist/storage.js +121 -7
- package/dist/task-manifest.js +14 -1
- package/dist/types/index.d.ts +7 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/contracts.js
CHANGED
|
@@ -3662,7 +3662,7 @@ function isBlockingDependencyStatus(status) {
|
|
|
3662
3662
|
function isTerminalStatus(status) {
|
|
3663
3663
|
return status === "completed" || status === "failed" || status === "cancelled";
|
|
3664
3664
|
}
|
|
3665
|
-
var TASK_STATUSES, TASK_PRIORITIES, PLAN_STATUSES, VersionConflictError, TaskNotFoundError, TaskNotStartableError, TaskReferenceAmbiguousError, ProjectNotFoundError, ResourceConflictError, PlanNotFoundError, LockError, AgentNotFoundError, IdentityAliasAmbiguousError, IdentityIdImmutableError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DISPATCH_STATUSES, DispatchNotFoundError;
|
|
3665
|
+
var TASK_STATUSES, TASK_PRIORITIES, PLAN_STATUSES, VersionConflictError, TaskNotFoundError, TaskNotStartableError, TaskReferenceAmbiguousError, ProjectNotFoundError, ResourceConflictError, PlanRevisionConflictError, PlanNotFoundError, LockError, AgentNotFoundError, IdentityAliasAmbiguousError, IdentityIdImmutableError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DISPATCH_STATUSES, DispatchNotFoundError;
|
|
3666
3666
|
var init_types = __esm(() => {
|
|
3667
3667
|
TASK_STATUSES = [
|
|
3668
3668
|
"pending",
|
|
@@ -3750,6 +3750,19 @@ var init_types = __esm(() => {
|
|
|
3750
3750
|
this.name = "ResourceConflictError";
|
|
3751
3751
|
}
|
|
3752
3752
|
};
|
|
3753
|
+
PlanRevisionConflictError = class PlanRevisionConflictError extends Error {
|
|
3754
|
+
planId;
|
|
3755
|
+
expectedUpdatedAt;
|
|
3756
|
+
currentUpdatedAt;
|
|
3757
|
+
static code = "PLAN_REVISION_CONFLICT";
|
|
3758
|
+
constructor(planId, expectedUpdatedAt, currentUpdatedAt) {
|
|
3759
|
+
super(`Plan revision conflict for ${planId}: expected ${expectedUpdatedAt}, current ${currentUpdatedAt}`);
|
|
3760
|
+
this.planId = planId;
|
|
3761
|
+
this.expectedUpdatedAt = expectedUpdatedAt;
|
|
3762
|
+
this.currentUpdatedAt = currentUpdatedAt;
|
|
3763
|
+
this.name = "PlanRevisionConflictError";
|
|
3764
|
+
}
|
|
3765
|
+
};
|
|
3753
3766
|
PlanNotFoundError = class PlanNotFoundError extends Error {
|
|
3754
3767
|
planId;
|
|
3755
3768
|
static code = "PLAN_NOT_FOUND";
|
|
@@ -10521,6 +10534,48 @@ function updatePlan(id, input, db) {
|
|
|
10521
10534
|
return updatePlanStored(id, input, d);
|
|
10522
10535
|
})();
|
|
10523
10536
|
}
|
|
10537
|
+
function nextPlanCompletionTimestamp(expectedUpdatedAt) {
|
|
10538
|
+
const expected = Date.parse(expectedUpdatedAt);
|
|
10539
|
+
const minimum = Number.isNaN(expected) ? Date.now() : expected + 2;
|
|
10540
|
+
return new Date(Math.max(Date.now(), minimum)).toISOString();
|
|
10541
|
+
}
|
|
10542
|
+
function completePlanAtRevision(id, expectedUpdatedAt, db) {
|
|
10543
|
+
const d = db || getDatabase();
|
|
10544
|
+
return d.transaction(() => {
|
|
10545
|
+
guardPlanRowsSqlite([id], d);
|
|
10546
|
+
const plan = getPlan(id, d);
|
|
10547
|
+
if (!plan)
|
|
10548
|
+
throw new PlanNotFoundError(id);
|
|
10549
|
+
if (plan.updated_at !== expectedUpdatedAt) {
|
|
10550
|
+
throw new PlanRevisionConflictError(id, expectedUpdatedAt, plan.updated_at);
|
|
10551
|
+
}
|
|
10552
|
+
if (plan.status === "completed")
|
|
10553
|
+
return { plan, applied: false };
|
|
10554
|
+
const updatedAt = nextPlanCompletionTimestamp(expectedUpdatedAt);
|
|
10555
|
+
const result = d.run(`UPDATE plans
|
|
10556
|
+
SET status = 'completed', updated_at = ?
|
|
10557
|
+
WHERE id = ? AND updated_at = ? AND status <> 'completed'`, [updatedAt, id, expectedUpdatedAt]);
|
|
10558
|
+
if (result.changes !== 1) {
|
|
10559
|
+
const current = getPlan(id, d);
|
|
10560
|
+
if (!current)
|
|
10561
|
+
throw new PlanNotFoundError(id);
|
|
10562
|
+
throw new PlanRevisionConflictError(id, expectedUpdatedAt, current.updated_at);
|
|
10563
|
+
}
|
|
10564
|
+
const completed = getPlan(id, d);
|
|
10565
|
+
emitLocalEventHooksQuiet({
|
|
10566
|
+
type: "plan.updated",
|
|
10567
|
+
payload: {
|
|
10568
|
+
id,
|
|
10569
|
+
old_status: plan.status,
|
|
10570
|
+
new_status: completed.status,
|
|
10571
|
+
name: completed.name,
|
|
10572
|
+
project_id: completed.project_id
|
|
10573
|
+
},
|
|
10574
|
+
databasePath: databasePathFromDatabase(d)
|
|
10575
|
+
});
|
|
10576
|
+
return { plan: completed, applied: true };
|
|
10577
|
+
})();
|
|
10578
|
+
}
|
|
10524
10579
|
function deletePlan(id, db) {
|
|
10525
10580
|
const d = db || getDatabase();
|
|
10526
10581
|
const plan = getPlan(id, d);
|
|
@@ -12380,7 +12435,7 @@ var init_tasks = __esm(() => {
|
|
|
12380
12435
|
// package.json
|
|
12381
12436
|
var package_default = {
|
|
12382
12437
|
name: "@hasna/todos",
|
|
12383
|
-
version: "0.15.
|
|
12438
|
+
version: "0.15.19",
|
|
12384
12439
|
description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
|
|
12385
12440
|
type: "module",
|
|
12386
12441
|
main: "dist/index.js",
|
package/dist/db/plans.d.ts
CHANGED
|
@@ -12,5 +12,9 @@ export declare function createPlan(input: CreatePlanInput, db?: Database): Plan;
|
|
|
12
12
|
export declare function getPlan(id: string, db?: Database): Plan | null;
|
|
13
13
|
export declare function listPlans(projectId?: string, db?: Database): Plan[];
|
|
14
14
|
export declare function updatePlan(id: string, input: UpdatePlanInput, db?: Database): Plan;
|
|
15
|
+
export declare function completePlanAtRevision(id: string, expectedUpdatedAt: string, db?: Database): {
|
|
16
|
+
plan: Plan;
|
|
17
|
+
applied: boolean;
|
|
18
|
+
};
|
|
15
19
|
export declare function deletePlan(id: string, db?: Database): boolean;
|
|
16
20
|
//# sourceMappingURL=plans.d.ts.map
|
package/dist/db/plans.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plans.d.ts","sourceRoot":"","sources":["../../src/db/plans.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAoB,MAAM,YAAY,CAAC;AAC7D,OAAO,KAAK,EAAE,eAAe,EAAE,IAAI,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAShF,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAClB,MAAM,EAAE,IAAI,GAAG,MAAM,GAAG,WAAW,GAAG,WAAW,CAAC;IAClD,OAAO,EAAE,IAAI,EAAE,CAAC;CACjB;AAMD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAIvD;AAsCD,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,oBAAoB,CAalH;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAEnG;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,eAAe,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,IAAI,CA2BtE;AAED,wBAAgB,OAAO,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,IAAI,GAAG,IAAI,CAI9D;AAED,wBAAgB,SAAS,CAAC,SAAS,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,IAAI,EAAE,CAUnE;AAuDD,wBAAgB,UAAU,CACxB,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,eAAe,EACtB,EAAE,CAAC,EAAE,QAAQ,GACZ,IAAI,CAMN;AAED,wBAAgB,UAAU,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,OAAO,CAW7D"}
|
|
1
|
+
{"version":3,"file":"plans.d.ts","sourceRoot":"","sources":["../../src/db/plans.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAoB,MAAM,YAAY,CAAC;AAC7D,OAAO,KAAK,EAAE,eAAe,EAAE,IAAI,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAShF,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAClB,MAAM,EAAE,IAAI,GAAG,MAAM,GAAG,WAAW,GAAG,WAAW,CAAC;IAClD,OAAO,EAAE,IAAI,EAAE,CAAC;CACjB;AAMD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAIvD;AAsCD,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,oBAAoB,CAalH;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAEnG;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,eAAe,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,IAAI,CA2BtE;AAED,wBAAgB,OAAO,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,IAAI,GAAG,IAAI,CAI9D;AAED,wBAAgB,SAAS,CAAC,SAAS,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,IAAI,EAAE,CAUnE;AAuDD,wBAAgB,UAAU,CACxB,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,eAAe,EACtB,EAAE,CAAC,EAAE,QAAQ,GACZ,IAAI,CAMN;AAQD,wBAAgB,sBAAsB,CACpC,EAAE,EAAE,MAAM,EACV,iBAAiB,EAAE,MAAM,EACzB,EAAE,CAAC,EAAE,QAAQ,GACZ;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAqClC;AAED,wBAAgB,UAAU,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,OAAO,CAW7D"}
|
package/dist/index.js
CHANGED
|
@@ -3662,7 +3662,7 @@ function isBlockingDependencyStatus(status) {
|
|
|
3662
3662
|
function isTerminalStatus(status) {
|
|
3663
3663
|
return status === "completed" || status === "failed" || status === "cancelled";
|
|
3664
3664
|
}
|
|
3665
|
-
var TASK_STATUSES, TASK_PRIORITIES, PLAN_STATUSES, VersionConflictError, TaskNotFoundError, TaskNotStartableError, TaskReferenceAmbiguousError, ProjectNotFoundError, ResourceConflictError, PlanNotFoundError, LockError, AgentNotFoundError, IdentityAliasAmbiguousError, IdentityIdImmutableError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DISPATCH_STATUSES, DispatchNotFoundError;
|
|
3665
|
+
var TASK_STATUSES, TASK_PRIORITIES, PLAN_STATUSES, VersionConflictError, TaskNotFoundError, TaskNotStartableError, TaskReferenceAmbiguousError, ProjectNotFoundError, ResourceConflictError, PlanRevisionConflictError, PlanNotFoundError, LockError, AgentNotFoundError, IdentityAliasAmbiguousError, IdentityIdImmutableError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DISPATCH_STATUSES, DispatchNotFoundError;
|
|
3666
3666
|
var init_types = __esm(() => {
|
|
3667
3667
|
TASK_STATUSES = [
|
|
3668
3668
|
"pending",
|
|
@@ -3750,6 +3750,19 @@ var init_types = __esm(() => {
|
|
|
3750
3750
|
this.name = "ResourceConflictError";
|
|
3751
3751
|
}
|
|
3752
3752
|
};
|
|
3753
|
+
PlanRevisionConflictError = class PlanRevisionConflictError extends Error {
|
|
3754
|
+
planId;
|
|
3755
|
+
expectedUpdatedAt;
|
|
3756
|
+
currentUpdatedAt;
|
|
3757
|
+
static code = "PLAN_REVISION_CONFLICT";
|
|
3758
|
+
constructor(planId, expectedUpdatedAt, currentUpdatedAt) {
|
|
3759
|
+
super(`Plan revision conflict for ${planId}: expected ${expectedUpdatedAt}, current ${currentUpdatedAt}`);
|
|
3760
|
+
this.planId = planId;
|
|
3761
|
+
this.expectedUpdatedAt = expectedUpdatedAt;
|
|
3762
|
+
this.currentUpdatedAt = currentUpdatedAt;
|
|
3763
|
+
this.name = "PlanRevisionConflictError";
|
|
3764
|
+
}
|
|
3765
|
+
};
|
|
3753
3766
|
PlanNotFoundError = class PlanNotFoundError extends Error {
|
|
3754
3767
|
planId;
|
|
3755
3768
|
static code = "PLAN_NOT_FOUND";
|
|
@@ -10521,6 +10534,48 @@ function updatePlan(id, input, db) {
|
|
|
10521
10534
|
return updatePlanStored(id, input, d);
|
|
10522
10535
|
})();
|
|
10523
10536
|
}
|
|
10537
|
+
function nextPlanCompletionTimestamp(expectedUpdatedAt) {
|
|
10538
|
+
const expected = Date.parse(expectedUpdatedAt);
|
|
10539
|
+
const minimum = Number.isNaN(expected) ? Date.now() : expected + 2;
|
|
10540
|
+
return new Date(Math.max(Date.now(), minimum)).toISOString();
|
|
10541
|
+
}
|
|
10542
|
+
function completePlanAtRevision(id, expectedUpdatedAt, db) {
|
|
10543
|
+
const d = db || getDatabase();
|
|
10544
|
+
return d.transaction(() => {
|
|
10545
|
+
guardPlanRowsSqlite([id], d);
|
|
10546
|
+
const plan = getPlan(id, d);
|
|
10547
|
+
if (!plan)
|
|
10548
|
+
throw new PlanNotFoundError(id);
|
|
10549
|
+
if (plan.updated_at !== expectedUpdatedAt) {
|
|
10550
|
+
throw new PlanRevisionConflictError(id, expectedUpdatedAt, plan.updated_at);
|
|
10551
|
+
}
|
|
10552
|
+
if (plan.status === "completed")
|
|
10553
|
+
return { plan, applied: false };
|
|
10554
|
+
const updatedAt = nextPlanCompletionTimestamp(expectedUpdatedAt);
|
|
10555
|
+
const result = d.run(`UPDATE plans
|
|
10556
|
+
SET status = 'completed', updated_at = ?
|
|
10557
|
+
WHERE id = ? AND updated_at = ? AND status <> 'completed'`, [updatedAt, id, expectedUpdatedAt]);
|
|
10558
|
+
if (result.changes !== 1) {
|
|
10559
|
+
const current = getPlan(id, d);
|
|
10560
|
+
if (!current)
|
|
10561
|
+
throw new PlanNotFoundError(id);
|
|
10562
|
+
throw new PlanRevisionConflictError(id, expectedUpdatedAt, current.updated_at);
|
|
10563
|
+
}
|
|
10564
|
+
const completed = getPlan(id, d);
|
|
10565
|
+
emitLocalEventHooksQuiet({
|
|
10566
|
+
type: "plan.updated",
|
|
10567
|
+
payload: {
|
|
10568
|
+
id,
|
|
10569
|
+
old_status: plan.status,
|
|
10570
|
+
new_status: completed.status,
|
|
10571
|
+
name: completed.name,
|
|
10572
|
+
project_id: completed.project_id
|
|
10573
|
+
},
|
|
10574
|
+
databasePath: databasePathFromDatabase(d)
|
|
10575
|
+
});
|
|
10576
|
+
return { plan: completed, applied: true };
|
|
10577
|
+
})();
|
|
10578
|
+
}
|
|
10524
10579
|
function deletePlan(id, db) {
|
|
10525
10580
|
const d = db || getDatabase();
|
|
10526
10581
|
const plan = getPlan(id, d);
|
|
@@ -12493,7 +12548,7 @@ var init_dispatches = __esm(() => {
|
|
|
12493
12548
|
// package.json
|
|
12494
12549
|
var package_default = {
|
|
12495
12550
|
name: "@hasna/todos",
|
|
12496
|
-
version: "0.15.
|
|
12551
|
+
version: "0.15.19",
|
|
12497
12552
|
description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
|
|
12498
12553
|
type: "module",
|
|
12499
12554
|
main: "dist/index.js",
|
|
@@ -25766,7 +25821,7 @@ function importSqliteTodosStorageSnapshot(snapshot, db) {
|
|
|
25766
25821
|
}
|
|
25767
25822
|
if (result.errors.length > 0)
|
|
25768
25823
|
return result;
|
|
25769
|
-
const applyRows = (objectType, table, columns, rows, updateClockColumn, afterUpsert) => {
|
|
25824
|
+
const applyRows = (objectType, table, columns, rows, updateClockColumn, acceptEqualClock = true, afterUpsert) => {
|
|
25770
25825
|
for (const row of rows) {
|
|
25771
25826
|
try {
|
|
25772
25827
|
const record = asRecord2(row);
|
|
@@ -25775,7 +25830,7 @@ function importSqliteTodosStorageSnapshot(snapshot, db) {
|
|
|
25775
25830
|
result.skipped += 1;
|
|
25776
25831
|
continue;
|
|
25777
25832
|
}
|
|
25778
|
-
const state = upsertById(d, table, columns, record, updateClockColumn);
|
|
25833
|
+
const state = upsertById(d, table, columns, record, updateClockColumn, acceptEqualClock);
|
|
25779
25834
|
if (state === "inserted")
|
|
25780
25835
|
result.inserted += 1;
|
|
25781
25836
|
else if (state === "updated")
|
|
@@ -25792,10 +25847,10 @@ function importSqliteTodosStorageSnapshot(snapshot, db) {
|
|
|
25792
25847
|
applyRows("project_machine_paths", "project_machine_paths", PROJECT_MACHINE_PATH_COLUMNS, snapshot.projectMachinePaths ?? [], "updated_at");
|
|
25793
25848
|
applyRows("agents", "agents", AGENT_COLUMNS, snapshot.agents, "last_seen_at");
|
|
25794
25849
|
applyRows("task_lists", "task_lists", TASK_LIST_COLUMNS, snapshot.taskLists, "updated_at");
|
|
25795
|
-
applyRows("plans", "plans", PLAN_COLUMNS, snapshot.plans, "updated_at");
|
|
25850
|
+
applyRows("plans", "plans", PLAN_COLUMNS, snapshot.plans, "updated_at", false);
|
|
25796
25851
|
applyRows("templates", "task_templates", TEMPLATE_COLUMNS, snapshot.templates);
|
|
25797
25852
|
applyRows("template_tasks", "template_tasks", TEMPLATE_TASK_COLUMNS, snapshot.templateTasks ?? []);
|
|
25798
|
-
applyRows("tasks", "tasks", TASK_COLUMNS, sortedTasks2(snapshot.tasks), "updated_at", (row, changed) => {
|
|
25853
|
+
applyRows("tasks", "tasks", TASK_COLUMNS, sortedTasks2(snapshot.tasks), "updated_at", true, (row, changed) => {
|
|
25799
25854
|
if (changed && Array.isArray(row["tags"]) && typeof row["id"] === "string") {
|
|
25800
25855
|
replaceTaskTags(row["id"], row["tags"].filter((tag) => typeof tag === "string"), d);
|
|
25801
25856
|
}
|
|
@@ -25804,7 +25859,7 @@ function importSqliteTodosStorageSnapshot(snapshot, db) {
|
|
|
25804
25859
|
applyTombstones(d, snapshot.tombstones ?? [], result);
|
|
25805
25860
|
return result;
|
|
25806
25861
|
}
|
|
25807
|
-
function upsertById(db, table, columns, row, updateClockColumn) {
|
|
25862
|
+
function upsertById(db, table, columns, row, updateClockColumn, acceptEqualClock = true) {
|
|
25808
25863
|
const id = row["id"];
|
|
25809
25864
|
if (typeof id !== "string" || !id)
|
|
25810
25865
|
throw new Error(`${table} row is missing id`);
|
|
@@ -25816,7 +25871,7 @@ function upsertById(db, table, columns, row, updateClockColumn) {
|
|
|
25816
25871
|
const values = presentColumns.map((column) => valueForColumn(column, row[column]));
|
|
25817
25872
|
const updateColumns = presentColumns.filter((column) => column !== "id");
|
|
25818
25873
|
const updateSet = updateColumns.map((column) => column === "version" ? `version = MAX(COALESCE(${table}.version, 0), excluded.version)` : `${column} = excluded.${column}`).join(", ");
|
|
25819
|
-
const clockGuard = updateClockColumn && presentColumns.includes(updateClockColumn) ? ` WHERE ${table}.${updateClockColumn} IS NULL OR ${table}.${updateClockColumn} <= excluded.${updateClockColumn}` : "";
|
|
25874
|
+
const clockGuard = updateClockColumn && presentColumns.includes(updateClockColumn) ? ` WHERE ${table}.${updateClockColumn} IS NULL OR ${table}.${updateClockColumn} ${acceptEqualClock ? "<=" : "<"} excluded.${updateClockColumn}` : "";
|
|
25820
25875
|
const sql = updateSet ? `INSERT INTO ${table} (${presentColumns.join(", ")}) VALUES (${placeholders})
|
|
25821
25876
|
ON CONFLICT(id) DO UPDATE SET ${updateSet}${clockGuard}` : `INSERT OR IGNORE INTO ${table} (${presentColumns.join(", ")}) VALUES (${placeholders})`;
|
|
25822
25877
|
const changes = db.run(sql, values).changes;
|
|
@@ -26056,6 +26111,7 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
|
|
|
26056
26111
|
get: (id) => getPlan(id, database()),
|
|
26057
26112
|
list: (projectId) => listPlans(projectId, database()),
|
|
26058
26113
|
update: (id, input) => updatePlan(id, input, database()),
|
|
26114
|
+
completeAtRevision: (id, expectedUpdatedAt) => completePlanAtRevision(id, expectedUpdatedAt, database()),
|
|
26059
26115
|
delete: (id) => deletePlan(id, database())
|
|
26060
26116
|
},
|
|
26061
26117
|
planProjectLinks: {
|
|
@@ -26600,6 +26656,7 @@ function createPostgresTodosStorageAdapter(options) {
|
|
|
26600
26656
|
get: (id) => store.get("plans", id),
|
|
26601
26657
|
list: async (projectId) => (await store.list("plans")).filter((plan) => projectId === undefined || plan.project_id === projectId).sort((a, b) => a.name.localeCompare(b.name)),
|
|
26602
26658
|
update: (id, input) => updatePlan2(id, input, store),
|
|
26659
|
+
completeAtRevision: (id, expectedUpdatedAt, context) => store.completePlanAtRevision(id, expectedUpdatedAt, context),
|
|
26603
26660
|
delete: (id, context) => store.deletePlan(id, context)
|
|
26604
26661
|
},
|
|
26605
26662
|
planProjectLinks: {
|
|
@@ -27087,6 +27144,54 @@ class PostgresJsonRecordStore {
|
|
|
27087
27144
|
throw new PlanNotFoundError(value.id);
|
|
27088
27145
|
return payloadRecord2(row.payload);
|
|
27089
27146
|
}
|
|
27147
|
+
async completePlanAtRevision(id, expectedUpdatedAt, context = {}) {
|
|
27148
|
+
await this.ensureSchema();
|
|
27149
|
+
const result = await this.options.client.query(`/* todos:complete-plan-revision-cas */ WITH next_clock AS (
|
|
27150
|
+
SELECT date_trunc(
|
|
27151
|
+
'milliseconds',
|
|
27152
|
+
GREATEST(clock_timestamp(), ($3::text)::timestamptz + interval '2 milliseconds')
|
|
27153
|
+
) AS completed_at
|
|
27154
|
+
), stored AS (
|
|
27155
|
+
UPDATE ${this.tableName} AS record SET
|
|
27156
|
+
payload = record.payload || jsonb_build_object(
|
|
27157
|
+
'status', 'completed',
|
|
27158
|
+
'updated_at', to_char(
|
|
27159
|
+
next_clock.completed_at AT TIME ZONE 'UTC',
|
|
27160
|
+
'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'
|
|
27161
|
+
)
|
|
27162
|
+
),
|
|
27163
|
+
updated_at = next_clock.completed_at,
|
|
27164
|
+
deleted_at = NULL,
|
|
27165
|
+
source_machine_id = COALESCE($4, record.source_machine_id),
|
|
27166
|
+
version = COALESCE(record.version, 0) + 1
|
|
27167
|
+
FROM next_clock
|
|
27168
|
+
WHERE record.service = $1
|
|
27169
|
+
AND record.object_type = 'plans'
|
|
27170
|
+
AND record.object_id = $2
|
|
27171
|
+
AND record.deleted_at IS NULL
|
|
27172
|
+
AND record.payload->>'updated_at' = $3::text
|
|
27173
|
+
AND record.payload->>'status' IS DISTINCT FROM 'completed'
|
|
27174
|
+
RETURNING record.payload
|
|
27175
|
+
)
|
|
27176
|
+
SELECT payload FROM stored`, [
|
|
27177
|
+
this.service,
|
|
27178
|
+
id,
|
|
27179
|
+
expectedUpdatedAt,
|
|
27180
|
+
context.requestId ?? this.sourceMachineId ?? null
|
|
27181
|
+
]);
|
|
27182
|
+
const payload = result.rows[0]?.payload;
|
|
27183
|
+
if (payload)
|
|
27184
|
+
return { plan: payloadRecord2(payload), applied: true };
|
|
27185
|
+
const current = await this.get("plans", id);
|
|
27186
|
+
if (!current)
|
|
27187
|
+
throw new PlanNotFoundError(id);
|
|
27188
|
+
if (current.updated_at !== expectedUpdatedAt) {
|
|
27189
|
+
throw new PlanRevisionConflictError(id, expectedUpdatedAt, current.updated_at);
|
|
27190
|
+
}
|
|
27191
|
+
if (current.status === "completed")
|
|
27192
|
+
return { plan: current, applied: false };
|
|
27193
|
+
throw new PlanRevisionConflictError(id, expectedUpdatedAt, current.updated_at);
|
|
27194
|
+
}
|
|
27090
27195
|
async createTemplateWithTasks(template, tasks, context = {}) {
|
|
27091
27196
|
await this.ensureSchema();
|
|
27092
27197
|
const records = [
|
|
@@ -28865,6 +28970,15 @@ function createShadowTodosStorageAdapter(options) {
|
|
|
28865
28970
|
mirror.enqueueUpsert("plans", plan, context);
|
|
28866
28971
|
return plan;
|
|
28867
28972
|
},
|
|
28973
|
+
async completeAtRevision(id, expectedUpdatedAt, context) {
|
|
28974
|
+
if (typeof local.plans.completeAtRevision !== "function") {
|
|
28975
|
+
throw new Error("Atomic plan completion is not supported by the local shadow adapter");
|
|
28976
|
+
}
|
|
28977
|
+
const completed = await local.plans.completeAtRevision(id, expectedUpdatedAt, context);
|
|
28978
|
+
if (completed.applied)
|
|
28979
|
+
mirror.enqueueUpsert("plans", completed.plan, context);
|
|
28980
|
+
return completed;
|
|
28981
|
+
},
|
|
28868
28982
|
async delete(id, context) {
|
|
28869
28983
|
const deleted = await local.plans.delete(id, context);
|
|
28870
28984
|
if (deleted)
|