@hasna/todos 0.15.52 → 0.17.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 +1993 -0
- package/README.md +356 -42
- package/dist/ai-tools.d.ts.map +1 -1
- package/dist/ai.d.ts.map +1 -1
- package/dist/cli/cloud-router.d.ts +162 -72
- 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.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 +22072 -15949
- 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 +25 -5
- 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/cli/template-remote.d.ts +90 -0
- package/dist/cli/template-remote.d.ts.map +1 -0
- package/dist/cli-mcp-parity.d.ts.map +1 -1
- package/dist/contracts.js +908 -291
- package/dist/db/agents.d.ts +8 -0
- package/dist/db/agents.d.ts.map +1 -1
- 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 +6 -0
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/handoffs.d.ts.map +1 -1
- package/dist/db/identity-mapping.d.ts +25 -0
- package/dist/db/identity-mapping.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-crud.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 +4341 -1756
- 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/feature-manifest.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 +104 -0
- package/dist/lib/local-opt-in.d.ts.map +1 -0
- package/dist/lib/paths.d.ts +20 -1
- 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 +53 -0
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +50124 -60838
- 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/agents.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-adv-tools.d.ts.map +1 -1
- package/dist/mcp/tools/task-auto-tools.d.ts.map +1 -1
- package/dist/mcp/tools/task-crud.d.ts.map +1 -1
- package/dist/mcp/tools/task-meta-tools.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/tools/templates.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 +3349 -645
- package/dist/registry.js +909 -292
- package/dist/release-provenance.json +6 -6
- package/dist/sdk/client.d.ts +53 -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 +1253 -1088
- package/dist/sdk/resolve.d.ts +101 -0
- package/dist/sdk/resolve.d.ts.map +1 -0
- package/dist/sdk/types.d.ts +17 -2
- package/dist/sdk/types.d.ts.map +1 -1
- package/dist/sdk/v1.generated.d.ts +334 -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 +24758 -16687
- package/dist/server/openapi.d.ts +2514 -469
- 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/rate-limit-config.d.ts +6 -0
- package/dist/server/rate-limit-config.d.ts.map +1 -0
- 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 +4 -2
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/interfaces.d.ts +135 -3
- package/dist/storage/interfaces.d.ts.map +1 -1
- package/dist/storage/local-sqlite.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 +3154 -350
- package/dist/task-manifest/postgres.d.ts +1 -1
- package/dist/task-manifest/sqlite.d.ts +1 -1
- package/dist/task-manifest.js +250 -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 +43 -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 +18 -15
- 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
- package/dist/mcp/tools/code-tools.d.ts +0 -15
- package/dist/mcp/tools/code-tools.d.ts.map +0 -1
|
@@ -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,74 @@ 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 { isAbsolute, 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();
|
|
395
|
+
}
|
|
396
|
+
function hasnaHomeRoot(env = process.env) {
|
|
397
|
+
const override = env["HASNA_HOME"]?.trim();
|
|
398
|
+
if (override && isAbsolute(override))
|
|
399
|
+
return override;
|
|
400
|
+
return join(effectiveHome(env), ".hasna");
|
|
400
401
|
}
|
|
401
402
|
function legacyHomeDir(env = process.env) {
|
|
402
|
-
return
|
|
403
|
+
return join(hasnaHomeRoot(env), "todos");
|
|
403
404
|
}
|
|
404
405
|
function resolverHome(env = process.env) {
|
|
405
406
|
return dataDir({ app: "todos", home: effectiveHome(env), env });
|
|
@@ -408,23 +409,30 @@ function adoptResolverHome(resolved, env = process.env) {
|
|
|
408
409
|
const dataOverride = env.HASNA_DATA_HOME;
|
|
409
410
|
if (typeof dataOverride === "string" && dataOverride.trim().length > 0)
|
|
410
411
|
return true;
|
|
411
|
-
return existsSync(
|
|
412
|
+
return existsSync(join(resolved, "todos.db")) || existsSync(join(resolved, "config.json"));
|
|
412
413
|
}
|
|
413
414
|
function getTodosDir(env = process.env) {
|
|
414
415
|
const resolved = resolverHome(env);
|
|
415
416
|
return resolve(adoptResolverHome(resolved, env) ? resolved : legacyHomeDir(env));
|
|
416
417
|
}
|
|
418
|
+
var PATHS_RESOLVER_KIND_ENV, PATHS_RESOLVER_APP_SLUG_RE;
|
|
417
419
|
var init_paths = __esm(() => {
|
|
418
|
-
|
|
420
|
+
PATHS_RESOLVER_KIND_ENV = {
|
|
421
|
+
config: "HASNA_CONFIG_HOME",
|
|
422
|
+
data: "HASNA_DATA_HOME",
|
|
423
|
+
state: "HASNA_STATE_HOME",
|
|
424
|
+
cache: "HASNA_CACHE_HOME"
|
|
425
|
+
};
|
|
426
|
+
PATHS_RESOLVER_APP_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
419
427
|
});
|
|
420
428
|
|
|
421
429
|
// src/lib/sync-utils.ts
|
|
422
430
|
import { existsSync as existsSync2, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "fs";
|
|
423
431
|
import { createHash as createHash2 } from "crypto";
|
|
424
|
-
import { homedir as
|
|
425
|
-
import { join as
|
|
432
|
+
import { homedir as homedir2 } from "os";
|
|
433
|
+
import { join as join2 } from "path";
|
|
426
434
|
function getHomeDir() {
|
|
427
|
-
return process.env["HOME"] || process.env["USERPROFILE"] ||
|
|
435
|
+
return process.env["HOME"] || process.env["USERPROFILE"] || homedir2();
|
|
428
436
|
}
|
|
429
437
|
function getTodosGlobalDir() {
|
|
430
438
|
return getTodosDir();
|
|
@@ -450,14 +458,14 @@ function writeJsonFile(path, data) {
|
|
|
450
458
|
`);
|
|
451
459
|
}
|
|
452
460
|
function readHighWaterMark(dir) {
|
|
453
|
-
const path =
|
|
461
|
+
const path = join2(dir, ".highwatermark");
|
|
454
462
|
if (!existsSync2(path))
|
|
455
463
|
return 1;
|
|
456
464
|
const val = parseInt(readFileSync(path, "utf-8").trim(), 10);
|
|
457
465
|
return isNaN(val) ? 1 : val;
|
|
458
466
|
}
|
|
459
467
|
function writeHighWaterMark(dir, value) {
|
|
460
|
-
writeFileSync(
|
|
468
|
+
writeFileSync(join2(dir, ".highwatermark"), String(value));
|
|
461
469
|
}
|
|
462
470
|
function getFileMtimeMs(path) {
|
|
463
471
|
try {
|
|
@@ -518,9 +526,9 @@ var init_sync_utils = __esm(() => {
|
|
|
518
526
|
|
|
519
527
|
// src/lib/config.ts
|
|
520
528
|
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
|
|
521
|
-
import { dirname, join as
|
|
529
|
+
import { dirname, join as join3 } from "path";
|
|
522
530
|
function getConfigPath() {
|
|
523
|
-
return
|
|
531
|
+
return join3(getTodosGlobalDir(), "config.json");
|
|
524
532
|
}
|
|
525
533
|
function normalizeAgent(agent) {
|
|
526
534
|
return agent.trim().toLowerCase();
|
|
@@ -571,21 +579,6 @@ function normalizeApiUrl(value) {
|
|
|
571
579
|
return null;
|
|
572
580
|
return trimmed.replace(/\/+$/, "");
|
|
573
581
|
}
|
|
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
582
|
function getSyncAgentsFromConfig() {
|
|
590
583
|
const config = loadConfig();
|
|
591
584
|
const agents = config.sync_agents;
|
|
@@ -6108,6 +6101,171 @@ class SqliteTodosTaskManifestBackend {
|
|
|
6108
6101
|
}
|
|
6109
6102
|
}
|
|
6110
6103
|
|
|
6104
|
+
// src/storage/machine-registry.ts
|
|
6105
|
+
import { randomUUID } from "crypto";
|
|
6106
|
+
import { isDeepStrictEqual } from "util";
|
|
6107
|
+
class MachineRegistryError extends Error {
|
|
6108
|
+
status;
|
|
6109
|
+
constructor(message, status = 409) {
|
|
6110
|
+
super(message);
|
|
6111
|
+
this.status = status;
|
|
6112
|
+
}
|
|
6113
|
+
}
|
|
6114
|
+
var fields = new Set(["id", "name", "hostname", "platform", "last_seen_at", "metadata", "created_at", "ssh_address", "is_primary", "archived_at"]);
|
|
6115
|
+
function validateMachines(value) {
|
|
6116
|
+
if (!Array.isArray(value) || value.length > 1e4)
|
|
6117
|
+
throw new MachineRegistryError("machines must be an array of at most 10000 complete records", 400);
|
|
6118
|
+
for (const row of value) {
|
|
6119
|
+
if (!row || typeof row !== "object" || Array.isArray(row) || Object.keys(row).some((key2) => !fields.has(key2)))
|
|
6120
|
+
throw new MachineRegistryError("Invalid machine record fields", 400);
|
|
6121
|
+
for (const key2 of ["id", "name", "last_seen_at", "created_at"])
|
|
6122
|
+
if (typeof row[key2] !== "string" || !row[key2].trim() || row[key2].length > 1024)
|
|
6123
|
+
throw new MachineRegistryError(`Invalid machine ${key2}`, 400);
|
|
6124
|
+
for (const key2 of ["hostname", "platform", "ssh_address", "archived_at"])
|
|
6125
|
+
if (row[key2] !== null && typeof row[key2] !== "string")
|
|
6126
|
+
throw new MachineRegistryError(`Invalid machine ${key2}`, 400);
|
|
6127
|
+
for (const key2 of ["last_seen_at", "created_at", "archived_at"])
|
|
6128
|
+
if (row[key2] !== null && !Number.isFinite(Date.parse(row[key2])))
|
|
6129
|
+
throw new MachineRegistryError(`Invalid machine ${key2}`, 400);
|
|
6130
|
+
if (typeof row.is_primary !== "boolean" || !row.metadata || typeof row.metadata !== "object" || Array.isArray(row.metadata))
|
|
6131
|
+
throw new MachineRegistryError("Invalid machine flags or metadata", 400);
|
|
6132
|
+
if (!isDeepStrictEqual(JSON.parse(JSON.stringify(row.metadata)), row.metadata))
|
|
6133
|
+
throw new MachineRegistryError("Machine metadata must preserve JSON values exactly", 400);
|
|
6134
|
+
if (JSON.stringify(row.metadata).length > 262144)
|
|
6135
|
+
throw new MachineRegistryError("Machine metadata is too large", 400);
|
|
6136
|
+
if (row.is_primary && row.archived_at !== null)
|
|
6137
|
+
throw new MachineRegistryError("An archived machine cannot be primary", 400);
|
|
6138
|
+
}
|
|
6139
|
+
return value;
|
|
6140
|
+
}
|
|
6141
|
+
function planMachineImport(existing, incoming) {
|
|
6142
|
+
const rows = validateMachines(incoming);
|
|
6143
|
+
const byId = new Map(existing.map((row) => [row.id, row]));
|
|
6144
|
+
const byName = new Map(existing.map((row) => [row.name, row.id]));
|
|
6145
|
+
const insert = [];
|
|
6146
|
+
let skipped = 0;
|
|
6147
|
+
for (const row of rows) {
|
|
6148
|
+
const prior = byId.get(row.id);
|
|
6149
|
+
if (prior && !isDeepStrictEqual(prior, row))
|
|
6150
|
+
throw new MachineRegistryError("Machine migration conflicts with an existing identity; reconcile before retrying");
|
|
6151
|
+
if (byName.has(row.name) && byName.get(row.name) !== row.id)
|
|
6152
|
+
throw new MachineRegistryError("Machine name belongs to a different identity; migration was not applied");
|
|
6153
|
+
if (prior)
|
|
6154
|
+
skipped++;
|
|
6155
|
+
else {
|
|
6156
|
+
insert.push(row);
|
|
6157
|
+
byId.set(row.id, row);
|
|
6158
|
+
byName.set(row.name, row.id);
|
|
6159
|
+
}
|
|
6160
|
+
}
|
|
6161
|
+
if ([...byId.values()].filter((row) => row.is_primary).length > 1)
|
|
6162
|
+
throw new MachineRegistryError("Machine migration would create multiple primary machines");
|
|
6163
|
+
return { rows: insert, skipped };
|
|
6164
|
+
}
|
|
6165
|
+
function createPostgresMachineRegistry(client, service, table, ensure) {
|
|
6166
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(table))
|
|
6167
|
+
throw new Error("Invalid machine registry table");
|
|
6168
|
+
async function list(connection = client) {
|
|
6169
|
+
await ensure();
|
|
6170
|
+
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]);
|
|
6171
|
+
return validateMachines(result.rows.map((row) => typeof row.payload === "string" ? JSON.parse(row.payload) : row.payload));
|
|
6172
|
+
}
|
|
6173
|
+
return { list, async execute(input) {
|
|
6174
|
+
if (!input || typeof input !== "object" || Array.isArray(input) || !["register", "heartbeat", "set-primary", "archive", "unarchive", "delete", "import"].includes(input.action))
|
|
6175
|
+
throw new MachineRegistryError("Invalid machine operation", 400);
|
|
6176
|
+
if (Object.keys(input).some((key2) => !["action", "name", "id", "options", "machines"].includes(key2)))
|
|
6177
|
+
throw new MachineRegistryError("Unknown machine operation fields", 400);
|
|
6178
|
+
if (input.options !== undefined && (!input.options || typeof input.options !== "object" || Array.isArray(input.options)))
|
|
6179
|
+
throw new MachineRegistryError("Invalid machine options", 400);
|
|
6180
|
+
await ensure();
|
|
6181
|
+
if (!client.transaction)
|
|
6182
|
+
throw new MachineRegistryError("Machine registry requires a transactional server backend", 501);
|
|
6183
|
+
return client.transaction(async (tx) => {
|
|
6184
|
+
await tx.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [`${service}:machine-registry`]);
|
|
6185
|
+
const current = await list(tx);
|
|
6186
|
+
let changed = [];
|
|
6187
|
+
let skipped = 0;
|
|
6188
|
+
let machine;
|
|
6189
|
+
let deleted;
|
|
6190
|
+
const stamp = new Date().toISOString();
|
|
6191
|
+
if (input.action === "import") {
|
|
6192
|
+
const plan = planMachineImport(current, input.machines);
|
|
6193
|
+
const ids = plan.rows.map((row) => row.id);
|
|
6194
|
+
if (ids.length) {
|
|
6195
|
+
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)]);
|
|
6196
|
+
if (retired.rows.length)
|
|
6197
|
+
throw new MachineRegistryError("Machine migration contains a retired identity");
|
|
6198
|
+
}
|
|
6199
|
+
changed = plan.rows;
|
|
6200
|
+
skipped = plan.skipped;
|
|
6201
|
+
} else {
|
|
6202
|
+
if (typeof input.name !== "string" || !input.name.trim() || input.name.length > 1024)
|
|
6203
|
+
throw new MachineRegistryError("Machine name or ID is required", 400);
|
|
6204
|
+
const matches = current.filter((row) => row.name === input.name || row.id === input.name);
|
|
6205
|
+
if (matches.length > 1)
|
|
6206
|
+
throw new MachineRegistryError("Machine selector is ambiguous between a name and another identity; use an unambiguous selector");
|
|
6207
|
+
const prior = matches[0];
|
|
6208
|
+
if (input.action === "register" || input.action === "heartbeat") {
|
|
6209
|
+
const opts = input.options ?? {};
|
|
6210
|
+
const allowed = new Set(["hostname", "platform", "ssh_address", "primary", "tailscale_name", "tailscale_ip", "lan_address", "workspace_path", "git_root", "arch"]);
|
|
6211
|
+
for (const [key2, value] of Object.entries(opts)) {
|
|
6212
|
+
if (!allowed.has(key2) || (key2 === "primary" ? typeof value !== "boolean" : typeof value !== "string" || value.length > 4096))
|
|
6213
|
+
throw new MachineRegistryError("Invalid machine registration options", 400);
|
|
6214
|
+
}
|
|
6215
|
+
if (input.id !== undefined && (typeof input.id !== "string" || !input.id.trim() || input.id.length > 1024))
|
|
6216
|
+
throw new MachineRegistryError("Invalid stable machine ID", 400);
|
|
6217
|
+
if (prior && input.id && input.id !== prior.id)
|
|
6218
|
+
throw new MachineRegistryError("Machine registration ID does not match existing identity");
|
|
6219
|
+
if (!prior && input.id && current.some((row) => row.id === input.id))
|
|
6220
|
+
throw new MachineRegistryError("Machine ID already belongs to another name");
|
|
6221
|
+
const metadata = { ...prior?.metadata };
|
|
6222
|
+
for (const key2 of ["tailscale_name", "tailscale_ip", "lan_address", "workspace_path", "git_root", "arch"])
|
|
6223
|
+
if (opts[key2] !== undefined)
|
|
6224
|
+
metadata[key2] = opts[key2];
|
|
6225
|
+
if (!prior && input.id) {
|
|
6226
|
+
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]);
|
|
6227
|
+
if (retired.rows.length)
|
|
6228
|
+
throw new MachineRegistryError("Machine identity has been retired");
|
|
6229
|
+
}
|
|
6230
|
+
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 };
|
|
6231
|
+
if (machine.archived_at && machine.is_primary)
|
|
6232
|
+
throw new MachineRegistryError("Cannot set archived machine as primary");
|
|
6233
|
+
changed = [machine];
|
|
6234
|
+
if (opts.primary === true)
|
|
6235
|
+
changed.push(...current.filter((row) => row.is_primary && row.id !== machine.id).map((row) => ({ ...row, is_primary: false })));
|
|
6236
|
+
} else {
|
|
6237
|
+
if (!prior)
|
|
6238
|
+
throw new MachineRegistryError("Machine not found", 404);
|
|
6239
|
+
machine = { ...prior };
|
|
6240
|
+
if (input.action === "set-primary") {
|
|
6241
|
+
if (prior.archived_at)
|
|
6242
|
+
throw new MachineRegistryError("Cannot set archived machine as primary");
|
|
6243
|
+
machine.is_primary = true;
|
|
6244
|
+
changed = current.filter((row) => row.is_primary && row.id !== prior.id).map((row) => ({ ...row, is_primary: false }));
|
|
6245
|
+
} else if (input.action === "archive" || input.action === "delete") {
|
|
6246
|
+
if (prior.is_primary)
|
|
6247
|
+
throw new MachineRegistryError("Cannot archive or delete the primary machine");
|
|
6248
|
+
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]);
|
|
6249
|
+
if (refs.rows.length)
|
|
6250
|
+
throw new MachineRegistryError("Machine still has referenced records or active tasks");
|
|
6251
|
+
machine.archived_at = stamp;
|
|
6252
|
+
deleted = input.action === "delete" ? true : undefined;
|
|
6253
|
+
} else if (input.action === "unarchive")
|
|
6254
|
+
machine.archived_at = null;
|
|
6255
|
+
else
|
|
6256
|
+
throw new MachineRegistryError("Unknown machine action", 400);
|
|
6257
|
+
changed.push(machine);
|
|
6258
|
+
}
|
|
6259
|
+
}
|
|
6260
|
+
for (const row of changed) {
|
|
6261
|
+
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]);
|
|
6262
|
+
}
|
|
6263
|
+
const machines = await list(tx);
|
|
6264
|
+
return { schema_version: 2, machines, ...machine ? { machine } : {}, inserted: changed.filter((row) => !current.some((old) => old.id === row.id)).length, skipped, ...deleted ? { deleted } : {} };
|
|
6265
|
+
});
|
|
6266
|
+
} };
|
|
6267
|
+
}
|
|
6268
|
+
|
|
6111
6269
|
// src/storage/postgres-sync.ts
|
|
6112
6270
|
init_types();
|
|
6113
6271
|
|
|
@@ -6330,6 +6488,10 @@ class PostgresTodosSyncStore {
|
|
|
6330
6488
|
}
|
|
6331
6489
|
}
|
|
6332
6490
|
async pushSnapshot(snapshot, context = {}) {
|
|
6491
|
+
if ((snapshot.tombstones ?? []).some((row) => row.object_type === "machines"))
|
|
6492
|
+
throw new Error("Machine tombstones require explicit registry lifecycle operations");
|
|
6493
|
+
if (snapshot.machines !== undefined)
|
|
6494
|
+
validateMachines(snapshot.machines);
|
|
6333
6495
|
const routingErrors = validateSnapshotRoutingRecords(snapshot.projects, snapshot.taskLists);
|
|
6334
6496
|
if (routingErrors.length > 0) {
|
|
6335
6497
|
throw new Error(`Invalid snapshot routing metadata: ${routingErrors.join("; ")}`);
|
|
@@ -6352,6 +6514,14 @@ class PostgresTodosSyncStore {
|
|
|
6352
6514
|
throw new ResourceConflictError("SNAPSHOT_DESTINATION_CONFLICT", `Snapshot routing conflicts with destination: ${destinationErrors.join("; ")}`);
|
|
6353
6515
|
}
|
|
6354
6516
|
const result = { records: 0, objectTypes: {} };
|
|
6517
|
+
if (snapshot.machines?.length) {
|
|
6518
|
+
if (!this.client.transaction)
|
|
6519
|
+
throw new Error("Machine snapshot migration requires transaction(callback)");
|
|
6520
|
+
const registry = createPostgresMachineRegistry({ query: client.query.bind(client), transaction: (fn) => fn(client) }, this.service, this.tableName, async () => {});
|
|
6521
|
+
const receipt = await registry.execute({ action: "import", machines: snapshot.machines });
|
|
6522
|
+
result.records += receipt.inserted + receipt.skipped;
|
|
6523
|
+
result.objectTypes.machines = receipt.inserted + receipt.skipped;
|
|
6524
|
+
}
|
|
6355
6525
|
const sourceMachineId = context.requestId ?? this.sourceMachineId ?? null;
|
|
6356
6526
|
for (const entry of snapshotEntries(snapshot)) {
|
|
6357
6527
|
if (entry.deletedAt === null)
|
|
@@ -6515,6 +6685,7 @@ function rowsToSnapshot(rows) {
|
|
|
6515
6685
|
tasks: [],
|
|
6516
6686
|
projects: [],
|
|
6517
6687
|
projectMachinePaths: [],
|
|
6688
|
+
machines: [],
|
|
6518
6689
|
plans: [],
|
|
6519
6690
|
agents: [],
|
|
6520
6691
|
taskLists: [],
|
|
@@ -6526,6 +6697,12 @@ function rowsToSnapshot(rows) {
|
|
|
6526
6697
|
for (const row of rows) {
|
|
6527
6698
|
const payload = payloadRecord(row.payload);
|
|
6528
6699
|
const deletedAt = stringValue(row.deleted_at);
|
|
6700
|
+
if (row.object_type === "machines") {
|
|
6701
|
+
if (deletedAt)
|
|
6702
|
+
throw new Error("Snapshot contains retired machine identities; use the authoritative registry instead of silently discarding tombstones");
|
|
6703
|
+
snapshot.machines.push(payload);
|
|
6704
|
+
continue;
|
|
6705
|
+
}
|
|
6529
6706
|
if (deletedAt) {
|
|
6530
6707
|
snapshot.tombstones ??= [];
|
|
6531
6708
|
snapshot.tombstones.push({
|