@hasna/todos 0.15.51 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1884 -0
- package/README.md +314 -37
- package/dist/ai-tools.d.ts.map +1 -1
- package/dist/ai.d.ts.map +1 -1
- package/dist/cli/cloud-router.d.ts +99 -51
- package/dist/cli/cloud-router.d.ts.map +1 -1
- package/dist/cli/commands/agent-commands.d.ts.map +1 -1
- package/dist/cli/commands/config-serve-commands.d.ts.map +1 -1
- package/dist/cli/commands/delegate.d.ts.map +1 -1
- package/dist/cli/commands/environment-snapshots.d.ts.map +1 -1
- package/dist/cli/commands/machines-api.d.ts +4 -0
- package/dist/cli/commands/machines-api.d.ts.map +1 -0
- package/dist/cli/commands/machines.d.ts.map +1 -1
- package/dist/cli/commands/mcp-hooks-commands.d.ts.map +1 -1
- package/dist/cli/commands/plan-commands.d.ts +3 -0
- package/dist/cli/commands/plan-commands.d.ts.map +1 -0
- package/dist/cli/commands/plan-template-commands.d.ts.map +1 -1
- package/dist/cli/commands/pr-group-commands.d.ts.map +1 -1
- package/dist/cli/commands/project-commands.d.ts +15 -0
- package/dist/cli/commands/project-commands.d.ts.map +1 -1
- package/dist/cli/commands/query-commands.d.ts.map +1 -1
- package/dist/cli/commands/storage-commands.d.ts.map +1 -1
- package/dist/cli/commands/task-list-commands.d.ts +3 -0
- package/dist/cli/commands/task-list-commands.d.ts.map +1 -0
- package/dist/cli/commands/template-commands.d.ts +3 -0
- package/dist/cli/commands/template-commands.d.ts.map +1 -0
- package/dist/cli/helpers.d.ts.map +1 -1
- package/dist/cli/index.js +15156 -11088
- package/dist/cli/machine-api.d.ts +7 -0
- package/dist/cli/machine-api.d.ts.map +1 -0
- package/dist/cli/plan-read-api.d.ts +6 -0
- package/dist/cli/plan-read-api.d.ts.map +1 -0
- package/dist/cli/project-api.d.ts +119 -0
- package/dist/cli/project-api.d.ts.map +1 -0
- package/dist/cli/stage-a.d.ts +34 -4
- package/dist/cli/stage-a.d.ts.map +1 -1
- package/dist/cli/task-query-api.d.ts +21 -0
- package/dist/cli/task-query-api.d.ts.map +1 -0
- package/dist/cli/template-api.d.ts +7 -0
- package/dist/cli/template-api.d.ts.map +1 -0
- package/dist/contracts.js +836 -266
- package/dist/db/builtin-templates.d.ts +2 -33
- package/dist/db/builtin-templates.d.ts.map +1 -1
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/handoffs.d.ts.map +1 -1
- package/dist/db/machines.d.ts.map +1 -1
- package/dist/db/migrations.d.ts.map +1 -1
- package/dist/db/plans.d.ts.map +1 -1
- package/dist/db/retrospectives.d.ts.map +1 -1
- package/dist/db/schema.d.ts.map +1 -1
- package/dist/db/task-lists.d.ts.map +1 -1
- package/dist/db/task-runs.d.ts +2 -0
- package/dist/db/task-runs.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3863 -1708
- package/dist/lib/assignee-validation.d.ts.map +1 -1
- package/dist/lib/builtin-template-library.d.ts +32 -0
- package/dist/lib/builtin-template-library.d.ts.map +1 -0
- package/dist/lib/cli-reference.d.ts.map +1 -1
- package/dist/lib/config.d.ts +10 -13
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/db-backup.d.ts +1 -1
- package/dist/lib/db-backup.d.ts.map +1 -1
- package/dist/lib/env.d.ts +18 -0
- package/dist/lib/env.d.ts.map +1 -0
- package/dist/lib/environment-snapshots.d.ts.map +1 -1
- package/dist/lib/gatherer.d.ts.map +1 -1
- package/dist/lib/headless-boundaries.d.ts +4 -3
- package/dist/lib/headless-boundaries.d.ts.map +1 -1
- package/dist/lib/local-encryption.d.ts +12 -0
- package/dist/lib/local-encryption.d.ts.map +1 -1
- package/dist/lib/local-opt-in.d.ts +90 -0
- package/dist/lib/local-opt-in.d.ts.map +1 -0
- package/dist/lib/paths.d.ts +9 -0
- package/dist/lib/paths.d.ts.map +1 -1
- package/dist/lib/plan-artifacts.d.ts +1 -0
- package/dist/lib/plan-artifacts.d.ts.map +1 -1
- package/dist/lib/plan-client-boundary.d.ts +2 -0
- package/dist/lib/plan-client-boundary.d.ts.map +1 -0
- package/dist/lib/plan-schedule.d.ts +6 -0
- package/dist/lib/plan-schedule.d.ts.map +1 -0
- package/dist/lib/project-panel.d.ts +6 -0
- package/dist/lib/project-panel.d.ts.map +1 -1
- package/dist/lib/project-receipt.d.ts +3 -0
- package/dist/lib/project-receipt.d.ts.map +1 -0
- package/dist/lib/sandbox-profiles.d.ts.map +1 -1
- package/dist/lib/shared-plan-artifacts.d.ts +14 -0
- package/dist/lib/shared-plan-artifacts.d.ts.map +1 -0
- package/dist/lib/sync.d.ts.map +1 -1
- package/dist/lib/task-list-client-boundary.d.ts +3 -0
- package/dist/lib/task-list-client-boundary.d.ts.map +1 -0
- package/dist/lib/task-routing.d.ts.map +1 -1
- package/dist/lib/template-client-boundary.d.ts +2 -0
- package/dist/lib/template-client-boundary.d.ts.map +1 -0
- package/dist/mcp/index.d.ts +1 -0
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +45195 -40368
- package/dist/mcp/plan-api.d.ts +6 -0
- package/dist/mcp/plan-api.d.ts.map +1 -0
- package/dist/mcp/remote-authority.d.ts +49 -0
- package/dist/mcp/remote-authority.d.ts.map +1 -0
- package/dist/mcp/task-coordination-api.d.ts +15 -0
- package/dist/mcp/task-coordination-api.d.ts.map +1 -0
- package/dist/mcp/task-list-api.d.ts +8 -0
- package/dist/mcp/task-list-api.d.ts.map +1 -0
- package/dist/mcp/token-utils.d.ts.map +1 -1
- package/dist/mcp/tools/environment-snapshots.d.ts.map +1 -1
- package/dist/mcp/tools/machines.d.ts.map +1 -1
- package/dist/mcp/tools/task-project-tools.d.ts.map +1 -1
- package/dist/mcp/tools/task-resources.d.ts.map +1 -1
- package/dist/mcp.d.ts +2 -2
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +45 -19
- package/dist/project-registration.js +2830 -557
- package/dist/registry.js +837 -267
- package/dist/release-provenance.json +6 -6
- package/dist/sdk/client.d.ts +51 -4
- package/dist/sdk/client.d.ts.map +1 -1
- package/dist/sdk/index.d.ts +2 -0
- package/dist/sdk/index.d.ts.map +1 -1
- package/dist/sdk/index.js +1209 -1080
- package/dist/sdk/resolve.d.ts +91 -0
- package/dist/sdk/resolve.d.ts.map +1 -0
- package/dist/sdk/v1.generated.d.ts +219 -3
- package/dist/sdk/v1.generated.d.ts.map +1 -1
- package/dist/server/auth-posture.d.ts +66 -7
- package/dist/server/auth-posture.d.ts.map +1 -1
- package/dist/server/client-ip.d.ts +39 -0
- package/dist/server/client-ip.d.ts.map +1 -0
- package/dist/server/cloud.d.ts +2 -1
- package/dist/server/cloud.d.ts.map +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +20556 -15533
- package/dist/server/openapi.d.ts +1341 -83
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/port.d.ts +2 -2
- package/dist/server/port.d.ts.map +1 -1
- package/dist/server/routes.d.ts +3 -6
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/serve.d.ts +2 -6
- package/dist/server/serve.d.ts.map +1 -1
- package/dist/server/v1.d.ts +3 -1
- package/dist/server/v1.d.ts.map +1 -1
- package/dist/storage/atomic-project-migration.d.ts +53 -0
- package/dist/storage/atomic-project-migration.d.ts.map +1 -0
- package/dist/storage/index.d.ts +3 -1
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/interfaces.d.ts +61 -0
- package/dist/storage/interfaces.d.ts.map +1 -1
- package/dist/storage/machine-registry.d.ts +36 -0
- package/dist/storage/machine-registry.d.ts.map +1 -0
- package/dist/storage/postgres-adapter.d.ts.map +1 -1
- package/dist/storage/postgres-sync.d.ts +2 -2
- package/dist/storage/postgres-sync.d.ts.map +1 -1
- package/dist/storage/run-artifact-creation-upload.d.ts +50 -0
- package/dist/storage/run-artifact-creation-upload.d.ts.map +1 -0
- package/dist/storage/s3-artifact-sync.d.ts +18 -0
- package/dist/storage/s3-artifact-sync.d.ts.map +1 -1
- package/dist/storage/sqlite-snapshot.d.ts +2 -0
- package/dist/storage/sqlite-snapshot.d.ts.map +1 -1
- package/dist/storage.d.ts +2 -2
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +2695 -322
- package/dist/task-manifest/postgres.d.ts +1 -1
- package/dist/task-manifest/sqlite.d.ts +1 -1
- package/dist/task-manifest.js +244 -73
- package/dist/task-subtree-transfer.js +198 -4
- package/dist/testing.d.ts +90 -29
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +34 -10
- package/dist/types/index.d.ts +40 -4
- package/dist/types/index.d.ts.map +1 -1
- package/docs/PLAN_API.md +93 -0
- package/docs/TASK_LIST_API.md +58 -0
- package/docs/TASK_QUERY_API.md +14 -0
- package/docs/TEMPLATE_API.md +18 -0
- package/docs/native-storage.md +405 -0
- package/package.json +19 -16
- package/postinstall.js +52 -1
- package/dashboard/dist/assets/index-BNQ4gJua.js +0 -342
- package/dashboard/dist/assets/index-DjzvHUWt.css +0 -1
- package/dashboard/dist/index.html +0 -13
- package/dashboard/dist/logo.jpg +0 -0
|
@@ -13,7 +13,7 @@ export declare class PostgresTodosTaskManifestBackend implements TodosTaskManife
|
|
|
13
13
|
private persistTerminal;
|
|
14
14
|
apply(input: NormalizedTaskManifest, faults: PreparedTaskManifestFaults): Promise<TodosTaskManifestApplyResult>;
|
|
15
15
|
readExact(receiptId: string): Promise<TodosTaskManifestApplyResult>;
|
|
16
|
-
lookupBindingByPlanId(planId: string): Promise<Omit<import("./types.js").TodosTaskManifestBindingLookupResult, "
|
|
16
|
+
lookupBindingByPlanId(planId: string): Promise<Omit<import("./types.js").TodosTaskManifestBindingLookupResult, "authority" | "route" | "tenant_id" | "schema_version">>;
|
|
17
17
|
markOutboxDelivered(outboxId: string, deliveredAt: string): Promise<void>;
|
|
18
18
|
compensate(input: TodosTaskManifestCompensateRequest, receipt: TodosTaskManifestReceipt, compensationReceiptId: string, requestDigest: string, now: string): Promise<TodosTaskManifestCompensationResult>;
|
|
19
19
|
private readback;
|
|
@@ -10,7 +10,7 @@ export declare class SqliteTodosTaskManifestBackend implements TodosTaskManifest
|
|
|
10
10
|
apply(input: NormalizedTaskManifest, faults: PreparedTaskManifestFaults): Promise<TodosTaskManifestApplyResult>;
|
|
11
11
|
private persistTerminal;
|
|
12
12
|
readExact(receiptId: string): Promise<TodosTaskManifestApplyResult>;
|
|
13
|
-
lookupBindingByPlanId(planId: string): Promise<Omit<import("./types.js").TodosTaskManifestBindingLookupResult, "
|
|
13
|
+
lookupBindingByPlanId(planId: string): Promise<Omit<import("./types.js").TodosTaskManifestBindingLookupResult, "authority" | "route" | "tenant_id" | "schema_version">>;
|
|
14
14
|
markOutboxDelivered(outboxId: string, deliveredAt: string): Promise<void>;
|
|
15
15
|
compensate(input: TodosTaskManifestCompensateRequest, receipt: TodosTaskManifestReceipt, compensationReceiptId: string, requestDigest: string, now: string): Promise<TodosTaskManifestCompensationResult>;
|
|
16
16
|
private readback;
|
package/dist/task-manifest.js
CHANGED
|
@@ -46,7 +46,7 @@ function isBlockingDependencyStatus(status) {
|
|
|
46
46
|
function isTerminalStatus(status) {
|
|
47
47
|
return status === "completed" || status === "failed" || status === "cancelled";
|
|
48
48
|
}
|
|
49
|
-
var TASK_STATUSES, TASK_PRIORITIES, PLAN_STATUSES, VersionConflictError, TaskNotFoundError, TaskNotStartableError, TaskReferenceAmbiguousError, ProjectNotFoundError, ResourceConflictError, PlanRevisionConflictError, PlanNotFoundError, LockError, StaleLockHandoffError, AgentNotFoundError, IdentityAliasAmbiguousError, IdentityIdImmutableError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DISPATCH_STATUSES, DispatchNotFoundError;
|
|
49
|
+
var TASK_STATUSES, TASK_PRIORITIES, PLAN_STATUSES, VersionConflictError, TaskNotFoundError, TaskNotStartableError, TaskReferenceAmbiguousError, ProjectNotFoundError, ResourceConflictError, PlanRevisionConflictError, PlanNotFoundError, LockError, StaleLockHandoffError, AgentNotFoundError, IdentityAliasAmbiguousError, IdentityIdImmutableError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DISPATCH_STATUSES, DispatchNotFoundError, InputValidationError;
|
|
50
50
|
var init_types = __esm(() => {
|
|
51
51
|
TASK_STATUSES = [
|
|
52
52
|
"pending",
|
|
@@ -61,7 +61,7 @@ var init_types = __esm(() => {
|
|
|
61
61
|
"high",
|
|
62
62
|
"critical"
|
|
63
63
|
];
|
|
64
|
-
PLAN_STATUSES = ["active", "completed", "archived"];
|
|
64
|
+
PLAN_STATUSES = ["active", "completed", "archived", "planning", "cancelled"];
|
|
65
65
|
VersionConflictError = class VersionConflictError extends Error {
|
|
66
66
|
taskId;
|
|
67
67
|
expectedVersion;
|
|
@@ -250,6 +250,16 @@ var init_types = __esm(() => {
|
|
|
250
250
|
this.name = "DispatchNotFoundError";
|
|
251
251
|
}
|
|
252
252
|
};
|
|
253
|
+
InputValidationError = class InputValidationError extends Error {
|
|
254
|
+
static code = "INVALID_INPUT";
|
|
255
|
+
code = InputValidationError.code;
|
|
256
|
+
suggestion;
|
|
257
|
+
constructor(message, suggestion) {
|
|
258
|
+
super(message);
|
|
259
|
+
this.name = "InputValidationError";
|
|
260
|
+
this.suggestion = suggestion;
|
|
261
|
+
}
|
|
262
|
+
};
|
|
253
263
|
});
|
|
254
264
|
|
|
255
265
|
// src/lib/slugs.ts
|
|
@@ -323,83 +333,68 @@ function validateSnapshotRoutingDestinationConflicts(projects, taskLists, existi
|
|
|
323
333
|
return errors2;
|
|
324
334
|
}
|
|
325
335
|
|
|
326
|
-
//
|
|
336
|
+
// src/lib/paths.ts
|
|
337
|
+
import { existsSync } from "fs";
|
|
327
338
|
import { homedir } from "os";
|
|
328
|
-
import { join } from "path";
|
|
329
|
-
|
|
339
|
+
import { join, resolve } from "path";
|
|
340
|
+
import { homedir as pathsResolverHomedir } from "os";
|
|
341
|
+
import { join as pathsResolverJoin } from "path";
|
|
342
|
+
function pathsResolverAssertApp(app) {
|
|
330
343
|
if (typeof app !== "string" || app.length === 0) {
|
|
331
344
|
throw new TypeError("paths: app must be a non-empty string");
|
|
332
345
|
}
|
|
333
|
-
if (!
|
|
346
|
+
if (!PATHS_RESOLVER_APP_SLUG_RE.test(app)) {
|
|
334
347
|
throw new TypeError(`paths: invalid app slug "${app}" \u2014 expected lowercase kebab-case ([a-z0-9]+(-[a-z0-9]+)*)`);
|
|
335
348
|
}
|
|
336
349
|
}
|
|
337
|
-
function
|
|
338
|
-
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
const value = envOf(options)[KIND_ENV[kind]];
|
|
342
|
-
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
343
|
-
}
|
|
344
|
-
function isMacOS(platform) {
|
|
345
|
-
return platform === "darwin";
|
|
350
|
+
function pathsResolverAssertKind(kind) {
|
|
351
|
+
if (!Object.keys(PATHS_RESOLVER_KIND_ENV).includes(kind)) {
|
|
352
|
+
throw new TypeError(`paths: invalid path kind "${kind}" \u2014 expected one of ${Object.keys(PATHS_RESOLVER_KIND_ENV).join(", ")}`);
|
|
353
|
+
}
|
|
346
354
|
}
|
|
347
|
-
function
|
|
348
|
-
|
|
349
|
-
|
|
355
|
+
function pathsResolverBaseDir(kind, options) {
|
|
356
|
+
pathsResolverAssertKind(kind);
|
|
357
|
+
const env = options.env ?? process.env;
|
|
358
|
+
const override = env[PATHS_RESOLVER_KIND_ENV[kind]];
|
|
359
|
+
if (typeof override === "string" && override.length > 0)
|
|
350
360
|
return override;
|
|
351
|
-
const home = options.home ??
|
|
361
|
+
const home = options.home ?? pathsResolverHomedir();
|
|
352
362
|
const platform = options.platform ?? process.platform;
|
|
353
|
-
if (
|
|
363
|
+
if (platform === "darwin") {
|
|
354
364
|
switch (kind) {
|
|
355
365
|
case "config":
|
|
356
366
|
case "data":
|
|
357
|
-
return
|
|
367
|
+
return pathsResolverJoin(home, "Library", "Application Support", "Hasna");
|
|
358
368
|
case "cache":
|
|
359
|
-
return
|
|
369
|
+
return pathsResolverJoin(home, "Library", "Caches", "Hasna");
|
|
360
370
|
case "state":
|
|
361
|
-
return
|
|
371
|
+
return pathsResolverJoin(home, "Library", "Logs", "Hasna");
|
|
362
372
|
}
|
|
363
373
|
}
|
|
364
374
|
switch (kind) {
|
|
365
375
|
case "config":
|
|
366
|
-
return
|
|
376
|
+
return pathsResolverJoin(home, ".config", "hasna");
|
|
367
377
|
case "data":
|
|
368
|
-
return
|
|
378
|
+
return pathsResolverJoin(home, ".local", "share", "hasna");
|
|
369
379
|
case "state":
|
|
370
|
-
return
|
|
380
|
+
return pathsResolverJoin(home, ".local", "state", "hasna");
|
|
371
381
|
case "cache":
|
|
372
|
-
return
|
|
382
|
+
return pathsResolverJoin(home, ".cache", "hasna");
|
|
373
383
|
}
|
|
374
384
|
}
|
|
375
|
-
function
|
|
376
|
-
|
|
377
|
-
const appSegment = options.internal === true ?
|
|
378
|
-
return
|
|
385
|
+
function pathsResolverResolve(kind, options) {
|
|
386
|
+
pathsResolverAssertApp(options.app);
|
|
387
|
+
const appSegment = options.internal === true ? pathsResolverJoin("internal", options.app) : options.app;
|
|
388
|
+
return pathsResolverJoin(pathsResolverBaseDir(kind, options), appSegment);
|
|
379
389
|
}
|
|
380
390
|
function dataDir(options) {
|
|
381
|
-
return
|
|
391
|
+
return pathsResolverResolve("data", options);
|
|
382
392
|
}
|
|
383
|
-
var KIND_ENV, APP_SLUG_RE;
|
|
384
|
-
var init_dist = __esm(() => {
|
|
385
|
-
KIND_ENV = {
|
|
386
|
-
config: "HASNA_CONFIG_HOME",
|
|
387
|
-
data: "HASNA_DATA_HOME",
|
|
388
|
-
state: "HASNA_STATE_HOME",
|
|
389
|
-
cache: "HASNA_CACHE_HOME"
|
|
390
|
-
};
|
|
391
|
-
APP_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
392
|
-
});
|
|
393
|
-
|
|
394
|
-
// src/lib/paths.ts
|
|
395
|
-
import { existsSync } from "fs";
|
|
396
|
-
import { homedir as homedir2 } from "os";
|
|
397
|
-
import { join as join2, resolve } from "path";
|
|
398
393
|
function effectiveHome(env = process.env) {
|
|
399
|
-
return env.HOME || env.USERPROFILE ||
|
|
394
|
+
return env.HOME || env.USERPROFILE || homedir();
|
|
400
395
|
}
|
|
401
396
|
function legacyHomeDir(env = process.env) {
|
|
402
|
-
return
|
|
397
|
+
return join(effectiveHome(env), ".hasna", "todos");
|
|
403
398
|
}
|
|
404
399
|
function resolverHome(env = process.env) {
|
|
405
400
|
return dataDir({ app: "todos", home: effectiveHome(env), env });
|
|
@@ -408,23 +403,30 @@ function adoptResolverHome(resolved, env = process.env) {
|
|
|
408
403
|
const dataOverride = env.HASNA_DATA_HOME;
|
|
409
404
|
if (typeof dataOverride === "string" && dataOverride.trim().length > 0)
|
|
410
405
|
return true;
|
|
411
|
-
return existsSync(
|
|
406
|
+
return existsSync(join(resolved, "todos.db")) || existsSync(join(resolved, "config.json"));
|
|
412
407
|
}
|
|
413
408
|
function getTodosDir(env = process.env) {
|
|
414
409
|
const resolved = resolverHome(env);
|
|
415
410
|
return resolve(adoptResolverHome(resolved, env) ? resolved : legacyHomeDir(env));
|
|
416
411
|
}
|
|
412
|
+
var PATHS_RESOLVER_KIND_ENV, PATHS_RESOLVER_APP_SLUG_RE;
|
|
417
413
|
var init_paths = __esm(() => {
|
|
418
|
-
|
|
414
|
+
PATHS_RESOLVER_KIND_ENV = {
|
|
415
|
+
config: "HASNA_CONFIG_HOME",
|
|
416
|
+
data: "HASNA_DATA_HOME",
|
|
417
|
+
state: "HASNA_STATE_HOME",
|
|
418
|
+
cache: "HASNA_CACHE_HOME"
|
|
419
|
+
};
|
|
420
|
+
PATHS_RESOLVER_APP_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
419
421
|
});
|
|
420
422
|
|
|
421
423
|
// src/lib/sync-utils.ts
|
|
422
424
|
import { existsSync as existsSync2, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "fs";
|
|
423
425
|
import { createHash as createHash2 } from "crypto";
|
|
424
|
-
import { homedir as
|
|
425
|
-
import { join as
|
|
426
|
+
import { homedir as homedir2 } from "os";
|
|
427
|
+
import { join as join2 } from "path";
|
|
426
428
|
function getHomeDir() {
|
|
427
|
-
return process.env["HOME"] || process.env["USERPROFILE"] ||
|
|
429
|
+
return process.env["HOME"] || process.env["USERPROFILE"] || homedir2();
|
|
428
430
|
}
|
|
429
431
|
function getTodosGlobalDir() {
|
|
430
432
|
return getTodosDir();
|
|
@@ -450,14 +452,14 @@ function writeJsonFile(path, data) {
|
|
|
450
452
|
`);
|
|
451
453
|
}
|
|
452
454
|
function readHighWaterMark(dir) {
|
|
453
|
-
const path =
|
|
455
|
+
const path = join2(dir, ".highwatermark");
|
|
454
456
|
if (!existsSync2(path))
|
|
455
457
|
return 1;
|
|
456
458
|
const val = parseInt(readFileSync(path, "utf-8").trim(), 10);
|
|
457
459
|
return isNaN(val) ? 1 : val;
|
|
458
460
|
}
|
|
459
461
|
function writeHighWaterMark(dir, value) {
|
|
460
|
-
writeFileSync(
|
|
462
|
+
writeFileSync(join2(dir, ".highwatermark"), String(value));
|
|
461
463
|
}
|
|
462
464
|
function getFileMtimeMs(path) {
|
|
463
465
|
try {
|
|
@@ -518,9 +520,9 @@ var init_sync_utils = __esm(() => {
|
|
|
518
520
|
|
|
519
521
|
// src/lib/config.ts
|
|
520
522
|
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
|
|
521
|
-
import { dirname, join as
|
|
523
|
+
import { dirname, join as join3 } from "path";
|
|
522
524
|
function getConfigPath() {
|
|
523
|
-
return
|
|
525
|
+
return join3(getTodosGlobalDir(), "config.json");
|
|
524
526
|
}
|
|
525
527
|
function normalizeAgent(agent) {
|
|
526
528
|
return agent.trim().toLowerCase();
|
|
@@ -571,21 +573,6 @@ function normalizeApiUrl(value) {
|
|
|
571
573
|
return null;
|
|
572
574
|
return trimmed.replace(/\/+$/, "");
|
|
573
575
|
}
|
|
574
|
-
function getLocalApiConfig(env = process.env) {
|
|
575
|
-
const config = loadConfig();
|
|
576
|
-
const envApiUrl = normalizeApiUrl(env["TODOS_URL"]);
|
|
577
|
-
const configApiUrl = normalizeApiUrl(config.apiUrl);
|
|
578
|
-
const apiUrl = envApiUrl ?? configApiUrl;
|
|
579
|
-
const apiKey = env["TODOS_API_KEY"] || config.apiKey || null;
|
|
580
|
-
return {
|
|
581
|
-
apiUrl,
|
|
582
|
-
apiKey,
|
|
583
|
-
source: {
|
|
584
|
-
apiUrl: envApiUrl ? "TODOS_URL" : configApiUrl ? "config" : "none",
|
|
585
|
-
apiKey: env["TODOS_API_KEY"] ? "TODOS_API_KEY" : config.apiKey ? "config" : "none"
|
|
586
|
-
}
|
|
587
|
-
};
|
|
588
|
-
}
|
|
589
576
|
function getSyncAgentsFromConfig() {
|
|
590
577
|
const config = loadConfig();
|
|
591
578
|
const agents = config.sync_agents;
|
|
@@ -6108,6 +6095,171 @@ class SqliteTodosTaskManifestBackend {
|
|
|
6108
6095
|
}
|
|
6109
6096
|
}
|
|
6110
6097
|
|
|
6098
|
+
// src/storage/machine-registry.ts
|
|
6099
|
+
import { randomUUID } from "crypto";
|
|
6100
|
+
import { isDeepStrictEqual } from "util";
|
|
6101
|
+
class MachineRegistryError extends Error {
|
|
6102
|
+
status;
|
|
6103
|
+
constructor(message, status = 409) {
|
|
6104
|
+
super(message);
|
|
6105
|
+
this.status = status;
|
|
6106
|
+
}
|
|
6107
|
+
}
|
|
6108
|
+
var fields = new Set(["id", "name", "hostname", "platform", "last_seen_at", "metadata", "created_at", "ssh_address", "is_primary", "archived_at"]);
|
|
6109
|
+
function validateMachines(value) {
|
|
6110
|
+
if (!Array.isArray(value) || value.length > 1e4)
|
|
6111
|
+
throw new MachineRegistryError("machines must be an array of at most 10000 complete records", 400);
|
|
6112
|
+
for (const row of value) {
|
|
6113
|
+
if (!row || typeof row !== "object" || Array.isArray(row) || Object.keys(row).some((key2) => !fields.has(key2)))
|
|
6114
|
+
throw new MachineRegistryError("Invalid machine record fields", 400);
|
|
6115
|
+
for (const key2 of ["id", "name", "last_seen_at", "created_at"])
|
|
6116
|
+
if (typeof row[key2] !== "string" || !row[key2].trim() || row[key2].length > 1024)
|
|
6117
|
+
throw new MachineRegistryError(`Invalid machine ${key2}`, 400);
|
|
6118
|
+
for (const key2 of ["hostname", "platform", "ssh_address", "archived_at"])
|
|
6119
|
+
if (row[key2] !== null && typeof row[key2] !== "string")
|
|
6120
|
+
throw new MachineRegistryError(`Invalid machine ${key2}`, 400);
|
|
6121
|
+
for (const key2 of ["last_seen_at", "created_at", "archived_at"])
|
|
6122
|
+
if (row[key2] !== null && !Number.isFinite(Date.parse(row[key2])))
|
|
6123
|
+
throw new MachineRegistryError(`Invalid machine ${key2}`, 400);
|
|
6124
|
+
if (typeof row.is_primary !== "boolean" || !row.metadata || typeof row.metadata !== "object" || Array.isArray(row.metadata))
|
|
6125
|
+
throw new MachineRegistryError("Invalid machine flags or metadata", 400);
|
|
6126
|
+
if (!isDeepStrictEqual(JSON.parse(JSON.stringify(row.metadata)), row.metadata))
|
|
6127
|
+
throw new MachineRegistryError("Machine metadata must preserve JSON values exactly", 400);
|
|
6128
|
+
if (JSON.stringify(row.metadata).length > 262144)
|
|
6129
|
+
throw new MachineRegistryError("Machine metadata is too large", 400);
|
|
6130
|
+
if (row.is_primary && row.archived_at !== null)
|
|
6131
|
+
throw new MachineRegistryError("An archived machine cannot be primary", 400);
|
|
6132
|
+
}
|
|
6133
|
+
return value;
|
|
6134
|
+
}
|
|
6135
|
+
function planMachineImport(existing, incoming) {
|
|
6136
|
+
const rows = validateMachines(incoming);
|
|
6137
|
+
const byId = new Map(existing.map((row) => [row.id, row]));
|
|
6138
|
+
const byName = new Map(existing.map((row) => [row.name, row.id]));
|
|
6139
|
+
const insert = [];
|
|
6140
|
+
let skipped = 0;
|
|
6141
|
+
for (const row of rows) {
|
|
6142
|
+
const prior = byId.get(row.id);
|
|
6143
|
+
if (prior && !isDeepStrictEqual(prior, row))
|
|
6144
|
+
throw new MachineRegistryError("Machine migration conflicts with an existing identity; reconcile before retrying");
|
|
6145
|
+
if (byName.has(row.name) && byName.get(row.name) !== row.id)
|
|
6146
|
+
throw new MachineRegistryError("Machine name belongs to a different identity; migration was not applied");
|
|
6147
|
+
if (prior)
|
|
6148
|
+
skipped++;
|
|
6149
|
+
else {
|
|
6150
|
+
insert.push(row);
|
|
6151
|
+
byId.set(row.id, row);
|
|
6152
|
+
byName.set(row.name, row.id);
|
|
6153
|
+
}
|
|
6154
|
+
}
|
|
6155
|
+
if ([...byId.values()].filter((row) => row.is_primary).length > 1)
|
|
6156
|
+
throw new MachineRegistryError("Machine migration would create multiple primary machines");
|
|
6157
|
+
return { rows: insert, skipped };
|
|
6158
|
+
}
|
|
6159
|
+
function createPostgresMachineRegistry(client, service, table, ensure) {
|
|
6160
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(table))
|
|
6161
|
+
throw new Error("Invalid machine registry table");
|
|
6162
|
+
async function list(connection = client) {
|
|
6163
|
+
await ensure();
|
|
6164
|
+
const result = await connection.query(`SELECT payload FROM ${table} WHERE service=$1 AND object_type='machines' AND deleted_at IS NULL ORDER BY object_id`, [service]);
|
|
6165
|
+
return validateMachines(result.rows.map((row) => typeof row.payload === "string" ? JSON.parse(row.payload) : row.payload));
|
|
6166
|
+
}
|
|
6167
|
+
return { list, async execute(input) {
|
|
6168
|
+
if (!input || typeof input !== "object" || Array.isArray(input) || !["register", "heartbeat", "set-primary", "archive", "unarchive", "delete", "import"].includes(input.action))
|
|
6169
|
+
throw new MachineRegistryError("Invalid machine operation", 400);
|
|
6170
|
+
if (Object.keys(input).some((key2) => !["action", "name", "id", "options", "machines"].includes(key2)))
|
|
6171
|
+
throw new MachineRegistryError("Unknown machine operation fields", 400);
|
|
6172
|
+
if (input.options !== undefined && (!input.options || typeof input.options !== "object" || Array.isArray(input.options)))
|
|
6173
|
+
throw new MachineRegistryError("Invalid machine options", 400);
|
|
6174
|
+
await ensure();
|
|
6175
|
+
if (!client.transaction)
|
|
6176
|
+
throw new MachineRegistryError("Machine registry requires a transactional server backend", 501);
|
|
6177
|
+
return client.transaction(async (tx) => {
|
|
6178
|
+
await tx.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [`${service}:machine-registry`]);
|
|
6179
|
+
const current = await list(tx);
|
|
6180
|
+
let changed = [];
|
|
6181
|
+
let skipped = 0;
|
|
6182
|
+
let machine;
|
|
6183
|
+
let deleted;
|
|
6184
|
+
const stamp = new Date().toISOString();
|
|
6185
|
+
if (input.action === "import") {
|
|
6186
|
+
const plan = planMachineImport(current, input.machines);
|
|
6187
|
+
const ids = plan.rows.map((row) => row.id);
|
|
6188
|
+
if (ids.length) {
|
|
6189
|
+
const retired = await tx.query(`SELECT object_id FROM ${table} WHERE service=$1 AND object_type='machines' AND object_id IN (SELECT jsonb_array_elements_text($2::text::jsonb)) AND deleted_at IS NOT NULL`, [service, JSON.stringify(ids)]);
|
|
6190
|
+
if (retired.rows.length)
|
|
6191
|
+
throw new MachineRegistryError("Machine migration contains a retired identity");
|
|
6192
|
+
}
|
|
6193
|
+
changed = plan.rows;
|
|
6194
|
+
skipped = plan.skipped;
|
|
6195
|
+
} else {
|
|
6196
|
+
if (typeof input.name !== "string" || !input.name.trim() || input.name.length > 1024)
|
|
6197
|
+
throw new MachineRegistryError("Machine name or ID is required", 400);
|
|
6198
|
+
const matches = current.filter((row) => row.name === input.name || row.id === input.name);
|
|
6199
|
+
if (matches.length > 1)
|
|
6200
|
+
throw new MachineRegistryError("Machine selector is ambiguous between a name and another identity; use an unambiguous selector");
|
|
6201
|
+
const prior = matches[0];
|
|
6202
|
+
if (input.action === "register" || input.action === "heartbeat") {
|
|
6203
|
+
const opts = input.options ?? {};
|
|
6204
|
+
const allowed = new Set(["hostname", "platform", "ssh_address", "primary", "tailscale_name", "tailscale_ip", "lan_address", "workspace_path", "git_root", "arch"]);
|
|
6205
|
+
for (const [key2, value] of Object.entries(opts)) {
|
|
6206
|
+
if (!allowed.has(key2) || (key2 === "primary" ? typeof value !== "boolean" : typeof value !== "string" || value.length > 4096))
|
|
6207
|
+
throw new MachineRegistryError("Invalid machine registration options", 400);
|
|
6208
|
+
}
|
|
6209
|
+
if (input.id !== undefined && (typeof input.id !== "string" || !input.id.trim() || input.id.length > 1024))
|
|
6210
|
+
throw new MachineRegistryError("Invalid stable machine ID", 400);
|
|
6211
|
+
if (prior && input.id && input.id !== prior.id)
|
|
6212
|
+
throw new MachineRegistryError("Machine registration ID does not match existing identity");
|
|
6213
|
+
if (!prior && input.id && current.some((row) => row.id === input.id))
|
|
6214
|
+
throw new MachineRegistryError("Machine ID already belongs to another name");
|
|
6215
|
+
const metadata = { ...prior?.metadata };
|
|
6216
|
+
for (const key2 of ["tailscale_name", "tailscale_ip", "lan_address", "workspace_path", "git_root", "arch"])
|
|
6217
|
+
if (opts[key2] !== undefined)
|
|
6218
|
+
metadata[key2] = opts[key2];
|
|
6219
|
+
if (!prior && input.id) {
|
|
6220
|
+
const retired = await tx.query(`SELECT object_id FROM ${table} WHERE service=$1 AND object_type='machines' AND object_id=$2 AND deleted_at IS NOT NULL`, [service, input.id]);
|
|
6221
|
+
if (retired.rows.length)
|
|
6222
|
+
throw new MachineRegistryError("Machine identity has been retired");
|
|
6223
|
+
}
|
|
6224
|
+
machine = { id: prior?.id ?? input.id ?? randomUUID(), name: prior?.name ?? input.name, hostname: opts.hostname ?? prior?.hostname ?? null, platform: opts.platform ?? prior?.platform ?? null, ssh_address: opts.ssh_address ?? prior?.ssh_address ?? null, created_at: prior?.created_at ?? stamp, last_seen_at: stamp, archived_at: prior?.archived_at ?? null, is_primary: opts.primary === true || (prior?.is_primary ?? false), metadata };
|
|
6225
|
+
if (machine.archived_at && machine.is_primary)
|
|
6226
|
+
throw new MachineRegistryError("Cannot set archived machine as primary");
|
|
6227
|
+
changed = [machine];
|
|
6228
|
+
if (opts.primary === true)
|
|
6229
|
+
changed.push(...current.filter((row) => row.is_primary && row.id !== machine.id).map((row) => ({ ...row, is_primary: false })));
|
|
6230
|
+
} else {
|
|
6231
|
+
if (!prior)
|
|
6232
|
+
throw new MachineRegistryError("Machine not found", 404);
|
|
6233
|
+
machine = { ...prior };
|
|
6234
|
+
if (input.action === "set-primary") {
|
|
6235
|
+
if (prior.archived_at)
|
|
6236
|
+
throw new MachineRegistryError("Cannot set archived machine as primary");
|
|
6237
|
+
machine.is_primary = true;
|
|
6238
|
+
changed = current.filter((row) => row.is_primary && row.id !== prior.id).map((row) => ({ ...row, is_primary: false }));
|
|
6239
|
+
} else if (input.action === "archive" || input.action === "delete") {
|
|
6240
|
+
if (prior.is_primary)
|
|
6241
|
+
throw new MachineRegistryError("Cannot archive or delete the primary machine");
|
|
6242
|
+
const refs = await tx.query(`SELECT object_id FROM ${table} WHERE service=$1 AND object_type<>'machines' AND deleted_at IS NULL AND payload->>'machine_id'=$2 AND ($3='delete' OR (object_type='tasks' AND payload->>'status' IN ('pending','in_progress'))) LIMIT 1`, [service, prior.id, input.action]);
|
|
6243
|
+
if (refs.rows.length)
|
|
6244
|
+
throw new MachineRegistryError("Machine still has referenced records or active tasks");
|
|
6245
|
+
machine.archived_at = stamp;
|
|
6246
|
+
deleted = input.action === "delete" ? true : undefined;
|
|
6247
|
+
} else if (input.action === "unarchive")
|
|
6248
|
+
machine.archived_at = null;
|
|
6249
|
+
else
|
|
6250
|
+
throw new MachineRegistryError("Unknown machine action", 400);
|
|
6251
|
+
changed.push(machine);
|
|
6252
|
+
}
|
|
6253
|
+
}
|
|
6254
|
+
for (const row of changed) {
|
|
6255
|
+
await tx.query(`INSERT INTO ${table} (service,object_type,object_id,payload,updated_at,deleted_at,version) VALUES ($1,'machines',$2,$3::text::jsonb,$4,$5,1) ON CONFLICT (service,object_type,object_id) DO UPDATE SET payload=EXCLUDED.payload,updated_at=EXCLUDED.updated_at,deleted_at=EXCLUDED.deleted_at,version=COALESCE(${table}.version,0)+1`, [service, row.id, JSON.stringify(row), stamp, deleted && row.id === machine?.id ? stamp : null]);
|
|
6256
|
+
}
|
|
6257
|
+
const machines = await list(tx);
|
|
6258
|
+
return { schema_version: 2, machines, ...machine ? { machine } : {}, inserted: changed.filter((row) => !current.some((old) => old.id === row.id)).length, skipped, ...deleted ? { deleted } : {} };
|
|
6259
|
+
});
|
|
6260
|
+
} };
|
|
6261
|
+
}
|
|
6262
|
+
|
|
6111
6263
|
// src/storage/postgres-sync.ts
|
|
6112
6264
|
init_types();
|
|
6113
6265
|
|
|
@@ -6330,6 +6482,10 @@ class PostgresTodosSyncStore {
|
|
|
6330
6482
|
}
|
|
6331
6483
|
}
|
|
6332
6484
|
async pushSnapshot(snapshot, context = {}) {
|
|
6485
|
+
if ((snapshot.tombstones ?? []).some((row) => row.object_type === "machines"))
|
|
6486
|
+
throw new Error("Machine tombstones require explicit registry lifecycle operations");
|
|
6487
|
+
if (snapshot.machines !== undefined)
|
|
6488
|
+
validateMachines(snapshot.machines);
|
|
6333
6489
|
const routingErrors = validateSnapshotRoutingRecords(snapshot.projects, snapshot.taskLists);
|
|
6334
6490
|
if (routingErrors.length > 0) {
|
|
6335
6491
|
throw new Error(`Invalid snapshot routing metadata: ${routingErrors.join("; ")}`);
|
|
@@ -6352,6 +6508,14 @@ class PostgresTodosSyncStore {
|
|
|
6352
6508
|
throw new ResourceConflictError("SNAPSHOT_DESTINATION_CONFLICT", `Snapshot routing conflicts with destination: ${destinationErrors.join("; ")}`);
|
|
6353
6509
|
}
|
|
6354
6510
|
const result = { records: 0, objectTypes: {} };
|
|
6511
|
+
if (snapshot.machines?.length) {
|
|
6512
|
+
if (!this.client.transaction)
|
|
6513
|
+
throw new Error("Machine snapshot migration requires transaction(callback)");
|
|
6514
|
+
const registry = createPostgresMachineRegistry({ query: client.query.bind(client), transaction: (fn) => fn(client) }, this.service, this.tableName, async () => {});
|
|
6515
|
+
const receipt = await registry.execute({ action: "import", machines: snapshot.machines });
|
|
6516
|
+
result.records += receipt.inserted + receipt.skipped;
|
|
6517
|
+
result.objectTypes.machines = receipt.inserted + receipt.skipped;
|
|
6518
|
+
}
|
|
6355
6519
|
const sourceMachineId = context.requestId ?? this.sourceMachineId ?? null;
|
|
6356
6520
|
for (const entry of snapshotEntries(snapshot)) {
|
|
6357
6521
|
if (entry.deletedAt === null)
|
|
@@ -6515,6 +6679,7 @@ function rowsToSnapshot(rows) {
|
|
|
6515
6679
|
tasks: [],
|
|
6516
6680
|
projects: [],
|
|
6517
6681
|
projectMachinePaths: [],
|
|
6682
|
+
machines: [],
|
|
6518
6683
|
plans: [],
|
|
6519
6684
|
agents: [],
|
|
6520
6685
|
taskLists: [],
|
|
@@ -6526,6 +6691,12 @@ function rowsToSnapshot(rows) {
|
|
|
6526
6691
|
for (const row of rows) {
|
|
6527
6692
|
const payload = payloadRecord(row.payload);
|
|
6528
6693
|
const deletedAt = stringValue(row.deleted_at);
|
|
6694
|
+
if (row.object_type === "machines") {
|
|
6695
|
+
if (deletedAt)
|
|
6696
|
+
throw new Error("Snapshot contains retired machine identities; use the authoritative registry instead of silently discarding tombstones");
|
|
6697
|
+
snapshot.machines.push(payload);
|
|
6698
|
+
continue;
|
|
6699
|
+
}
|
|
6529
6700
|
if (deletedAt) {
|
|
6530
6701
|
snapshot.tombstones ??= [];
|
|
6531
6702
|
snapshot.tombstones.push({
|