@hasna/todos 0.15.33 → 0.15.35
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 +33 -47
- 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 +344 -127
- package/dist/cli/stage-a.d.ts +3 -1
- package/dist/cli/stage-a.d.ts.map +1 -1
- package/dist/contracts.js +115 -27
- package/dist/db/migrations.d.ts.map +1 -1
- package/dist/db/projects.d.ts +23 -0
- package/dist/db/projects.d.ts.map +1 -1
- package/dist/db/schema.d.ts.map +1 -1
- package/dist/index.js +275 -116
- package/dist/json-contracts.d.ts.map +1 -1
- package/dist/lib/assignee-validation.d.ts.map +1 -1
- package/dist/lib/import-export-bridge.d.ts.map +1 -1
- package/dist/lib/onboarding-fixtures.d.ts.map +1 -1
- package/dist/mcp/index.js +285 -113
- package/dist/mcp/tools/task-project-tools.d.ts.map +1 -1
- package/dist/mcp.js +3 -2
- package/dist/project-registration/authority.d.ts.map +1 -1
- package/dist/project-registration/sqlite.d.ts.map +1 -1
- package/dist/project-registration.js +185 -40
- package/dist/registry.js +115 -27
- package/dist/release-provenance.json +5 -5
- package/dist/sdk/v1.generated.d.ts +3 -0
- package/dist/sdk/v1.generated.d.ts.map +1 -1
- package/dist/server/index.js +1268 -7051
- package/dist/server/openapi.d.ts +13 -0
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/v1.d.ts.map +1 -1
- package/dist/storage/config.d.ts +24 -4
- package/dist/storage/config.d.ts.map +1 -1
- package/dist/storage/hybrid.d.ts +5 -3
- package/dist/storage/hybrid.d.ts.map +1 -1
- package/dist/storage.js +158 -34
- package/dist/task-manifest.js +3 -3
- package/dist/testing.d.ts +16 -7
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +10 -6
- package/dist/types/index.d.ts +5 -2
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +3 -2
package/dist/server/openapi.d.ts
CHANGED
|
@@ -114,6 +114,10 @@ export declare function buildV1OpenApiDocument(version?: string): {
|
|
|
114
114
|
readonly task_counter: {
|
|
115
115
|
readonly type: "number";
|
|
116
116
|
};
|
|
117
|
+
readonly parent_id: {
|
|
118
|
+
readonly type: "string";
|
|
119
|
+
readonly nullable: true;
|
|
120
|
+
};
|
|
117
121
|
readonly created_at: {
|
|
118
122
|
readonly type: "string";
|
|
119
123
|
};
|
|
@@ -1972,6 +1976,10 @@ export declare function buildV1OpenApiDocument(version?: string): {
|
|
|
1972
1976
|
type: string;
|
|
1973
1977
|
minLength: number;
|
|
1974
1978
|
};
|
|
1979
|
+
parent_id: {
|
|
1980
|
+
type: string;
|
|
1981
|
+
minLength: number;
|
|
1982
|
+
};
|
|
1975
1983
|
};
|
|
1976
1984
|
};
|
|
1977
1985
|
UpdateProjectInput: {
|
|
@@ -1991,6 +1999,11 @@ export declare function buildV1OpenApiDocument(version?: string): {
|
|
|
1991
1999
|
type: string;
|
|
1992
2000
|
nullable: boolean;
|
|
1993
2001
|
};
|
|
2002
|
+
parent_id: {
|
|
2003
|
+
type: string;
|
|
2004
|
+
minLength: number;
|
|
2005
|
+
nullable: boolean;
|
|
2006
|
+
};
|
|
1994
2007
|
};
|
|
1995
2008
|
};
|
|
1996
2009
|
RenameProjectInput: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"openapi.d.ts","sourceRoot":"","sources":["../../src/server/openapi.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"openapi.d.ts","sourceRoot":"","sources":["../../src/server/openapi.ts"],"names":[],"mappings":"AAk3BA,wBAAgB,sBAAsB,CAAC,OAAO,SAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA+jEnE"}
|
package/dist/server/v1.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"v1.d.ts","sourceRoot":"","sources":["../../src/server/v1.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAuB,oBAAoB,EAAmD,MAAM,0BAA0B,CAAC;AAC3I,OAAO,EACL,iBAAiB,EACjB,qBAAqB,EACrB,oCAAoC,EACpC,sBAAsB,EACtB,6BAA6B,EAC7B,gBAAgB,EACjB,MAAM,YAAY,CAAC;AA0BpB,MAAM,WAAW,qBAAqB;IACpC,WAAW,CAAC,EAAE,OAAO,gBAAgB,CAAC;IACtC,YAAY,CAAC,EAAE,OAAO,iBAAiB,CAAC;IACxC,iBAAiB,CAAC,EAAE,OAAO,sBAAsB,CAAC;IAClD,gBAAgB,CAAC,EAAE,OAAO,qBAAqB,CAAC;IAChD,+BAA+B,CAAC,EAAE,OAAO,oCAAoC,CAAC;IAC9E,wBAAwB,CAAC,EAAE,OAAO,6BAA6B,CAAC;CACjE;
|
|
1
|
+
{"version":3,"file":"v1.d.ts","sourceRoot":"","sources":["../../src/server/v1.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAuB,oBAAoB,EAAmD,MAAM,0BAA0B,CAAC;AAC3I,OAAO,EACL,iBAAiB,EACjB,qBAAqB,EACrB,oCAAoC,EACpC,sBAAsB,EACtB,6BAA6B,EAC7B,gBAAgB,EACjB,MAAM,YAAY,CAAC;AA0BpB,MAAM,WAAW,qBAAqB;IACpC,WAAW,CAAC,EAAE,OAAO,gBAAgB,CAAC;IACtC,YAAY,CAAC,EAAE,OAAO,iBAAiB,CAAC;IACxC,iBAAiB,CAAC,EAAE,OAAO,sBAAsB,CAAC;IAClD,gBAAgB,CAAC,EAAE,OAAO,qBAAqB,CAAC;IAChD,+BAA+B,CAAC,EAAE,OAAO,oCAAoC,CAAC;IAC9E,wBAAwB,CAAC,EAAE,OAAO,6BAA6B,CAAC;CACjE;AAuYD;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,OAAO,GAAG,oBAAoB,CAiB1E;AAED,gFAAgF;AAChF,wBAAgB,oBAAoB,CAAC,CAAC,EAAE,oBAAoB,GAAG,MAAM,CAapE;AA4ED;;;GAGG;AACH,wBAAsB,eAAe,CACnC,GAAG,EAAE,OAAO,EACZ,GAAG,EAAE,GAAG,EACR,YAAY,GAAE,qBAA0B,GACvC,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAkuC1B"}
|
package/dist/storage/config.d.ts
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
* The single data-backend switch (owner directive 2026-07-29, knowledge
|
|
3
3
|
* k_ms3e6v41_zbe7m8): storage is either the local SQLite file or PostgreSQL.
|
|
4
4
|
* The former deployment-mode axis (local / remote / self_hosted / cloud /
|
|
5
|
-
* hybrid) is gone —
|
|
6
|
-
*
|
|
5
|
+
* hybrid) is gone — and since 2026-08-15 the storage-mode env variables are
|
|
6
|
+
* banned outright: their mere presence is a hard error. The backend is derived
|
|
7
|
+
* from HASNA_TODOS_DATABASE_URL: set selects postgresql, unset selects sqlite.
|
|
7
8
|
*/
|
|
8
9
|
export type TodosStorageBackend = "sqlite" | "postgres";
|
|
9
10
|
/** @deprecated Renamed to {@link TodosStorageBackend}; values collapsed to sqlite|postgres. */
|
|
@@ -39,7 +40,6 @@ export interface TodosStorageConfig {
|
|
|
39
40
|
sync: TodosSyncConfig;
|
|
40
41
|
}
|
|
41
42
|
export declare const TODOS_STORAGE_ENV: {
|
|
42
|
-
readonly mode: "HASNA_TODOS_STORAGE_MODE";
|
|
43
43
|
readonly shadow: "HASNA_TODOS_SHADOW";
|
|
44
44
|
readonly databaseUrl: "HASNA_TODOS_DATABASE_URL";
|
|
45
45
|
readonly databaseSsl: "HASNA_TODOS_DATABASE_SSL";
|
|
@@ -56,7 +56,6 @@ export declare const TODOS_STORAGE_ENV: {
|
|
|
56
56
|
readonly syncDryRun: "HASNA_TODOS_SYNC_DRY_RUN";
|
|
57
57
|
};
|
|
58
58
|
export declare const TODOS_STORAGE_FALLBACK_ENV: {
|
|
59
|
-
readonly mode: "TODOS_STORAGE_MODE";
|
|
60
59
|
readonly shadow: "TODOS_SHADOW";
|
|
61
60
|
readonly databaseUrl: "TODOS_DATABASE_URL";
|
|
62
61
|
readonly databaseSsl: "TODOS_DATABASE_SSL";
|
|
@@ -72,6 +71,13 @@ export declare const TODOS_STORAGE_FALLBACK_ENV: {
|
|
|
72
71
|
readonly syncBatchSize: "TODOS_SYNC_BATCH_SIZE";
|
|
73
72
|
readonly syncDryRun: "TODOS_SYNC_DRY_RUN";
|
|
74
73
|
};
|
|
74
|
+
/**
|
|
75
|
+
* The retired storage-mode variables (owner directive 2026-08-15). Their mere
|
|
76
|
+
* presence — even blank — is a hard error on the server path, exactly as on the
|
|
77
|
+
* client: the backend is derived from HASNA_TODOS_DATABASE_URL, never selected
|
|
78
|
+
* by a mode variable.
|
|
79
|
+
*/
|
|
80
|
+
export declare const REMOVED_STORAGE_MODE_ENV_KEYS: readonly ["HASNA_TODOS_STORAGE_MODE", "HASNA_TODOS_MODE", "TODOS_STORAGE_MODE", "TODOS_MODE"];
|
|
75
81
|
export declare const CANONICAL_TODOS_RDS_CLUSTER_ENV = "HASNA_TODOS_RDS_CLUSTER";
|
|
76
82
|
export declare const CANONICAL_TODOS_RDS_RUNTIME_PATH_ENV = "HASNA_TODOS_RDS_RUNTIME_PATH";
|
|
77
83
|
export declare const CANONICAL_TODOS_RDS_DATABASE = "todos";
|
|
@@ -90,9 +96,23 @@ export declare function isTodosPostgresBackend(config: TodosStorageConfig): bool
|
|
|
90
96
|
/** @deprecated Renamed to {@link isTodosPostgresBackend}. */
|
|
91
97
|
export declare function isTodosRemoteStorageEnabled(config: TodosStorageConfig): boolean;
|
|
92
98
|
export declare function assertTodosRemoteStorageConfig(config: TodosStorageConfig): void;
|
|
99
|
+
/**
|
|
100
|
+
* Parse a data-backend token. The deployment-mode vocabulary is gone and its
|
|
101
|
+
* variables are banned (owner directive 2026-08-15), so only the two canonical
|
|
102
|
+
* backend tokens — plus the `postgresql` engine-name spelling — are accepted;
|
|
103
|
+
* every deployment-mode token is rejected as removed.
|
|
104
|
+
*/
|
|
93
105
|
export declare function parseStorageBackend(value: string | undefined): TodosStorageBackend;
|
|
94
106
|
/** @deprecated Renamed to {@link parseStorageBackend}; values collapsed to sqlite|postgres. */
|
|
95
107
|
export declare function parseStorageMode(value: string | undefined): TodosStorageBackend;
|
|
108
|
+
/**
|
|
109
|
+
* Resolve the server data backend from the environment. The retired
|
|
110
|
+
* storage-mode variables are a hard error when present; otherwise the backend
|
|
111
|
+
* is derived from HASNA_TODOS_DATABASE_URL — set selects postgresql, unset
|
|
112
|
+
* selects the on-box SQLite file. One carve-out: a shadow mirror
|
|
113
|
+
* (HASNA_TODOS_SHADOW=1) keeps the SQLite file as the source of truth and
|
|
114
|
+
* treats the DSN as the mirror target, so the backend stays sqlite there.
|
|
115
|
+
*/
|
|
96
116
|
export declare function getTodosStorageBackend(env?: TodosStorageEnv): TodosStorageBackend;
|
|
97
117
|
/** @deprecated Renamed to {@link getTodosStorageBackend}. */
|
|
98
118
|
export declare function getTodosStorageMode(env?: TodosStorageEnv): TodosStorageBackend;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/storage/config.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/storage/config.ts"],"names":[],"mappings":"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"}
|
package/dist/storage/hybrid.d.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Explicit migration/sync machinery: a local-SQLite adapter that can push/pull
|
|
3
3
|
* snapshots against the Postgres sync tables. This is NOT an arm of the
|
|
4
|
-
* data-backend switch — no
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* data-backend switch — no environment value selects it; callers construct it
|
|
5
|
+
* directly when they need a cross-store sync (e.g. cutover tooling). The
|
|
6
|
+
* switch itself has exactly two arms, sqlite | postgres, derived from
|
|
7
|
+
* HASNA_TODOS_DATABASE_URL since the storage-mode variables were removed
|
|
8
|
+
* (owner directive 2026-08-15).
|
|
7
9
|
*/
|
|
8
10
|
import { type PostgresTodosSyncPushResult, type PostgresTodosSyncStore, type PullPostgresTodosSnapshotOptions, type TodosPostgresQueryClient } from "./postgres-sync.js";
|
|
9
11
|
import { type CreateLocalSqliteTodosStorageAdapterOptions } from "./local-sqlite.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hybrid.d.ts","sourceRoot":"","sources":["../../src/storage/hybrid.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"hybrid.d.ts","sourceRoot":"","sources":["../../src/storage/hybrid.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAEL,KAAK,2BAA2B,EAChC,KAAK,sBAAsB,EAC3B,KAAK,gCAAgC,EACrC,KAAK,wBAAwB,EAC9B,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAEL,KAAK,2CAA2C,EACjD,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EACV,mBAAmB,EACnB,mBAAmB,EACnB,wBAAwB,EAEzB,MAAM,iBAAiB,CAAC;AAEzB,MAAM,WAAW,4BAA4B;IAC3C,MAAM,EAAE,wBAAwB,CAAC;IACjC,MAAM,EAAE,2BAA2B,CAAC;CACrC;AAED,MAAM,WAAW,qBAAqB;IACpC,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9B,YAAY,CAAC,OAAO,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,2BAA2B,CAAC,CAAC;IAClF,YAAY,CAAC,OAAO,CAAC,EAAE,gCAAgC,EAAE,OAAO,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,wBAAwB,CAAC,CAAC;IAC3H,QAAQ,CAAC,OAAO,CAAC,EAAE,gCAAgC,EAAE,OAAO,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,4BAA4B,CAAC,CAAC;CAC5H;AAED,MAAM,WAAW,yBAA0B,SAAQ,mBAAmB;IACpE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,qBAAqB,CAAC;CACxC;AAED,MAAM,WAAW,sCAAsC;IACrD,YAAY,CAAC,EAAE,mBAAmB,CAAC;IACnC,KAAK,CAAC,EAAE,2CAA2C,CAAC;IACpD,SAAS,CAAC,EAAE,sBAAsB,CAAC;IACnC,cAAc,CAAC,EAAE,wBAAwB,CAAC;IAC1C,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,wBAAgB,+BAA+B,CAC7C,OAAO,EAAE,sCAAsC,GAC9C,yBAAyB,CAqD3B"}
|
package/dist/storage.js
CHANGED
|
@@ -65,6 +65,7 @@ __export(exports_config, {
|
|
|
65
65
|
TODOS_STORAGE_FALLBACK_ENV: () => TODOS_STORAGE_FALLBACK_ENV,
|
|
66
66
|
TODOS_STORAGE_ENV: () => TODOS_STORAGE_ENV,
|
|
67
67
|
STORAGE_TABLES: () => STORAGE_TABLES,
|
|
68
|
+
REMOVED_STORAGE_MODE_ENV_KEYS: () => REMOVED_STORAGE_MODE_ENV_KEYS,
|
|
68
69
|
CANONICAL_TODOS_RDS_RUNTIME_PATH_ENV: () => CANONICAL_TODOS_RDS_RUNTIME_PATH_ENV,
|
|
69
70
|
CANONICAL_TODOS_RDS_DATABASE: () => CANONICAL_TODOS_RDS_DATABASE,
|
|
70
71
|
CANONICAL_TODOS_RDS_CLUSTER_ENV: () => CANONICAL_TODOS_RDS_CLUSTER_ENV
|
|
@@ -126,25 +127,36 @@ function assertTodosRemoteStorageConfig(config) {
|
|
|
126
127
|
if (!isTodosPostgresBackend(config))
|
|
127
128
|
return;
|
|
128
129
|
if (!config.database?.url) {
|
|
129
|
-
throw new Error(`${TODOS_STORAGE_ENV.databaseUrl} is required when
|
|
130
|
+
throw new Error(`${TODOS_STORAGE_ENV.databaseUrl} is required when the postgresql backend is selected`);
|
|
130
131
|
}
|
|
131
132
|
}
|
|
132
133
|
function parseStorageBackend(value) {
|
|
133
134
|
const normalized = clean(value)?.toLowerCase();
|
|
134
135
|
if (!normalized)
|
|
135
136
|
return "sqlite";
|
|
136
|
-
if (normalized === "sqlite"
|
|
137
|
-
return
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
137
|
+
if (normalized === "sqlite")
|
|
138
|
+
return "sqlite";
|
|
139
|
+
if (normalized === "postgres" || normalized === "postgresql")
|
|
140
|
+
return "postgres";
|
|
141
|
+
if (["local", "remote", "cloud", "hybrid", "self_hosted"].includes(normalized)) {
|
|
142
|
+
throw new Error(`${TODOS_STORAGE_ENV.databaseUrl} selects the backend. Deployment modes no longer exist: ` + `delete the storage-mode variable and set ${TODOS_STORAGE_ENV.databaseUrl} to select the ` + `postgresql backend, or leave it unset for sqlite.`);
|
|
143
|
+
}
|
|
144
|
+
throw new Error(`Storage backend must be sqlite or postgres`);
|
|
142
145
|
}
|
|
143
146
|
function parseStorageMode(value) {
|
|
144
147
|
return parseStorageBackend(value);
|
|
145
148
|
}
|
|
146
149
|
function getTodosStorageBackend(env = process.env) {
|
|
147
|
-
|
|
150
|
+
for (const key of REMOVED_STORAGE_MODE_ENV_KEYS) {
|
|
151
|
+
if (Object.hasOwn(env, key) && env[key] !== undefined) {
|
|
152
|
+
throw new Error(`${key} was removed. Deployment modes no longer exist: delete the storage-mode variable. ` + `Set ${TODOS_STORAGE_ENV.databaseUrl} to select the postgresql backend, ` + `or leave it unset for sqlite.`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (!getTodosStorageDatabaseUrl(env))
|
|
156
|
+
return "sqlite";
|
|
157
|
+
if (isTodosShadowEnabled(env))
|
|
158
|
+
return "sqlite";
|
|
159
|
+
return "postgres";
|
|
148
160
|
}
|
|
149
161
|
function getTodosStorageMode(env = process.env) {
|
|
150
162
|
return getTodosStorageBackend(env);
|
|
@@ -218,7 +230,7 @@ function parsePositiveInteger(value, fallback) {
|
|
|
218
230
|
}
|
|
219
231
|
return parsed;
|
|
220
232
|
}
|
|
221
|
-
var TODOS_STORAGE_TABLES, STORAGE_TABLES, TODOS_STORAGE_ENV, TODOS_STORAGE_FALLBACK_ENV, CANONICAL_TODOS_RDS_CLUSTER_ENV = "HASNA_TODOS_RDS_CLUSTER", CANONICAL_TODOS_RDS_RUNTIME_PATH_ENV = "HASNA_TODOS_RDS_RUNTIME_PATH", CANONICAL_TODOS_RDS_DATABASE = "todos"
|
|
233
|
+
var TODOS_STORAGE_TABLES, STORAGE_TABLES, TODOS_STORAGE_ENV, TODOS_STORAGE_FALLBACK_ENV, REMOVED_STORAGE_MODE_ENV_KEYS, CANONICAL_TODOS_RDS_CLUSTER_ENV = "HASNA_TODOS_RDS_CLUSTER", CANONICAL_TODOS_RDS_RUNTIME_PATH_ENV = "HASNA_TODOS_RDS_RUNTIME_PATH", CANONICAL_TODOS_RDS_DATABASE = "todos";
|
|
222
234
|
var init_config = __esm(() => {
|
|
223
235
|
TODOS_STORAGE_TABLES = [
|
|
224
236
|
"todos_sync_records",
|
|
@@ -226,7 +238,6 @@ var init_config = __esm(() => {
|
|
|
226
238
|
];
|
|
227
239
|
STORAGE_TABLES = TODOS_STORAGE_TABLES;
|
|
228
240
|
TODOS_STORAGE_ENV = {
|
|
229
|
-
mode: "HASNA_TODOS_STORAGE_MODE",
|
|
230
241
|
shadow: "HASNA_TODOS_SHADOW",
|
|
231
242
|
databaseUrl: "HASNA_TODOS_DATABASE_URL",
|
|
232
243
|
databaseSsl: "HASNA_TODOS_DATABASE_SSL",
|
|
@@ -243,7 +254,6 @@ var init_config = __esm(() => {
|
|
|
243
254
|
syncDryRun: "HASNA_TODOS_SYNC_DRY_RUN"
|
|
244
255
|
};
|
|
245
256
|
TODOS_STORAGE_FALLBACK_ENV = {
|
|
246
|
-
mode: "TODOS_STORAGE_MODE",
|
|
247
257
|
shadow: "TODOS_SHADOW",
|
|
248
258
|
databaseUrl: "TODOS_DATABASE_URL",
|
|
249
259
|
databaseSsl: "TODOS_DATABASE_SSL",
|
|
@@ -259,14 +269,12 @@ var init_config = __esm(() => {
|
|
|
259
269
|
syncBatchSize: "TODOS_SYNC_BATCH_SIZE",
|
|
260
270
|
syncDryRun: "TODOS_SYNC_DRY_RUN"
|
|
261
271
|
};
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
cloud: "postgres"
|
|
269
|
-
};
|
|
272
|
+
REMOVED_STORAGE_MODE_ENV_KEYS = [
|
|
273
|
+
"HASNA_TODOS_STORAGE_MODE",
|
|
274
|
+
"HASNA_TODOS_MODE",
|
|
275
|
+
"TODOS_STORAGE_MODE",
|
|
276
|
+
"TODOS_MODE"
|
|
277
|
+
];
|
|
270
278
|
});
|
|
271
279
|
|
|
272
280
|
// src/types/index.ts
|
|
@@ -2327,6 +2335,10 @@ var init_migrations = __esm(() => {
|
|
|
2327
2335
|
);
|
|
2328
2336
|
INSERT OR IGNORE INTO _migrations (id) VALUES (70);
|
|
2329
2337
|
COMMIT;
|
|
2338
|
+
`,
|
|
2339
|
+
`BEGIN;
|
|
2340
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (71);
|
|
2341
|
+
COMMIT;
|
|
2330
2342
|
`
|
|
2331
2343
|
];
|
|
2332
2344
|
});
|
|
@@ -3000,6 +3012,8 @@ function ensureSchema(db) {
|
|
|
3000
3012
|
ensureColumn("projects", "task_list_id", "TEXT");
|
|
3001
3013
|
ensureColumn("projects", "task_prefix", "TEXT");
|
|
3002
3014
|
ensureColumn("projects", "task_counter", "INTEGER NOT NULL DEFAULT 0");
|
|
3015
|
+
ensureColumn("projects", "parent_id", "TEXT REFERENCES projects(id) ON DELETE SET NULL");
|
|
3016
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_projects_parent_id ON projects(parent_id)");
|
|
3003
3017
|
ensureColumn("tasks", "plan_id", "TEXT REFERENCES plans(id) ON DELETE SET NULL");
|
|
3004
3018
|
ensureColumn("tasks", "task_list_id", "TEXT REFERENCES task_lists(id) ON DELETE SET NULL");
|
|
3005
3019
|
ensureColumn("tasks", "short_id", "TEXT");
|
|
@@ -5037,17 +5051,24 @@ function createProject(input, db) {
|
|
|
5037
5051
|
const id = uuid();
|
|
5038
5052
|
const timestamp2 = now();
|
|
5039
5053
|
const derivedSlug = slugify(input.name);
|
|
5040
|
-
const taskListId = input.task_list_id === undefined ?
|
|
5054
|
+
const taskListId = input.task_list_id === undefined ? derivedSlug : slugify(input.task_list_id);
|
|
5041
5055
|
if (!derivedSlug || !taskListId)
|
|
5042
5056
|
throw new Error("Project name and task-list slug must be non-empty");
|
|
5043
5057
|
const slugConflict = d.query("SELECT id FROM projects WHERE task_list_id = ? LIMIT 1").get(taskListId);
|
|
5044
5058
|
if (slugConflict || !claimCanonicalSlug("project", "global", taskListId, id, d)) {
|
|
5045
5059
|
throw new ResourceConflictError("PROJECT_SLUG_CONFLICT", `Project slug "${taskListId}" already exists`);
|
|
5046
5060
|
}
|
|
5061
|
+
const parentId = input.parent_id ?? null;
|
|
5062
|
+
if (parentId !== null) {
|
|
5063
|
+
const parent = getProject(parentId, d);
|
|
5064
|
+
if (!parent)
|
|
5065
|
+
throw new ProjectNotFoundError(parentId);
|
|
5066
|
+
assertNotProjectAncestor(id, parentId, d);
|
|
5067
|
+
}
|
|
5047
5068
|
const taskPrefix = input.task_prefix || generatePrefix(input.name, d);
|
|
5048
5069
|
const machineId = currentStorageMachineId(d);
|
|
5049
|
-
d.run(`INSERT INTO projects (id, name, path, description, task_list_id, task_prefix, task_counter, created_at, updated_at, machine_id)
|
|
5050
|
-
VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?)`, [id, input.name, input.path, input.description || null, taskListId, taskPrefix, timestamp2, timestamp2, machineId]);
|
|
5070
|
+
d.run(`INSERT INTO projects (id, name, path, description, task_list_id, task_prefix, task_counter, parent_id, created_at, updated_at, machine_id)
|
|
5071
|
+
VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?)`, [id, input.name, input.path, input.description || null, taskListId, taskPrefix, parentId, timestamp2, timestamp2, machineId]);
|
|
5051
5072
|
return getProject(id, d);
|
|
5052
5073
|
})();
|
|
5053
5074
|
}
|
|
@@ -5072,6 +5093,60 @@ function listProjects(db) {
|
|
|
5072
5093
|
const d = db || getDatabase();
|
|
5073
5094
|
return d.query("SELECT * FROM projects ORDER BY name").all();
|
|
5074
5095
|
}
|
|
5096
|
+
function orderProjectsParentFirst(projects) {
|
|
5097
|
+
const projectId = (project) => {
|
|
5098
|
+
const id = project.id;
|
|
5099
|
+
return typeof id === "string" && id.length > 0 ? id : null;
|
|
5100
|
+
};
|
|
5101
|
+
const parentId = (project) => {
|
|
5102
|
+
const parent = project.parent_id;
|
|
5103
|
+
return parent == null ? null : String(parent);
|
|
5104
|
+
};
|
|
5105
|
+
const byId = new Set;
|
|
5106
|
+
for (const project of projects) {
|
|
5107
|
+
const id = projectId(project);
|
|
5108
|
+
if (id !== null)
|
|
5109
|
+
byId.add(id);
|
|
5110
|
+
}
|
|
5111
|
+
const ordered = [];
|
|
5112
|
+
const emitted = new Set;
|
|
5113
|
+
let remaining = [...projects];
|
|
5114
|
+
let progress = true;
|
|
5115
|
+
while (progress && remaining.length > 0) {
|
|
5116
|
+
progress = false;
|
|
5117
|
+
const deferred = [];
|
|
5118
|
+
for (const project of remaining) {
|
|
5119
|
+
const parent = parentId(project);
|
|
5120
|
+
if (parent === null || emitted.has(parent) || !byId.has(parent)) {
|
|
5121
|
+
ordered.push(project);
|
|
5122
|
+
const id = projectId(project);
|
|
5123
|
+
if (id !== null)
|
|
5124
|
+
emitted.add(id);
|
|
5125
|
+
progress = true;
|
|
5126
|
+
} else {
|
|
5127
|
+
deferred.push(project);
|
|
5128
|
+
}
|
|
5129
|
+
}
|
|
5130
|
+
remaining = deferred;
|
|
5131
|
+
}
|
|
5132
|
+
ordered.push(...remaining);
|
|
5133
|
+
return ordered;
|
|
5134
|
+
}
|
|
5135
|
+
function assertNotProjectAncestor(projectId, ancestorId, db) {
|
|
5136
|
+
const d = db || getDatabase();
|
|
5137
|
+
let cursor = ancestorId;
|
|
5138
|
+
const seen = new Set;
|
|
5139
|
+
while (cursor !== null) {
|
|
5140
|
+
if (cursor === projectId) {
|
|
5141
|
+
throw new ResourceConflictError("PROJECT_PARENT_CYCLE", `Project "${projectId}" cannot be placed under its own descendant`);
|
|
5142
|
+
}
|
|
5143
|
+
if (seen.has(cursor))
|
|
5144
|
+
break;
|
|
5145
|
+
seen.add(cursor);
|
|
5146
|
+
const row = d.query("SELECT parent_id FROM projects WHERE id = ?").get(cursor);
|
|
5147
|
+
cursor = row?.parent_id ?? null;
|
|
5148
|
+
}
|
|
5149
|
+
}
|
|
5075
5150
|
function updateProject(id, input, db) {
|
|
5076
5151
|
const d = db || getDatabase();
|
|
5077
5152
|
const project = getProject(id, d);
|
|
@@ -5094,6 +5169,16 @@ function updateProject(id, input, db) {
|
|
|
5094
5169
|
sets.push("path = ?");
|
|
5095
5170
|
params.push(input.path);
|
|
5096
5171
|
}
|
|
5172
|
+
if (input.parent_id !== undefined) {
|
|
5173
|
+
if (input.parent_id !== null) {
|
|
5174
|
+
const parent = getProject(input.parent_id, d);
|
|
5175
|
+
if (!parent)
|
|
5176
|
+
throw new ProjectNotFoundError(input.parent_id);
|
|
5177
|
+
assertNotProjectAncestor(id, input.parent_id, d);
|
|
5178
|
+
}
|
|
5179
|
+
sets.push("parent_id = ?");
|
|
5180
|
+
params.push(input.parent_id);
|
|
5181
|
+
}
|
|
5097
5182
|
params.push(id);
|
|
5098
5183
|
d.run(`UPDATE projects SET ${sets.join(", ")} WHERE id = ?`, params);
|
|
5099
5184
|
return getProject(id, d);
|
|
@@ -7227,8 +7312,8 @@ var init_secret_redaction = __esm(() => {
|
|
|
7227
7312
|
SECRET_REDACTION_SCHEMA = ["todos", "secret_redaction", "v1"].join(".");
|
|
7228
7313
|
DEFAULT_PATTERNS = [
|
|
7229
7314
|
{ name: "openai_sk", pattern: /\bsk-[a-zA-Z0-9]{10,}\b/g },
|
|
7230
|
-
{ name: "github_pat", pattern: /\
|
|
7231
|
-
{ name: "github_oauth", pattern: /\
|
|
7315
|
+
{ name: "github_pat", pattern: /\bgh[p]_[a-zA-Z0-9]{20,}\b/g },
|
|
7316
|
+
{ name: "github_oauth", pattern: /\bgh[o]_[a-zA-Z0-9]{20,}\b/g },
|
|
7232
7317
|
{ name: "aws_access_key", pattern: /\bAKIA[0-9A-Z]{16}\b/g },
|
|
7233
7318
|
{ name: "bearer_token", pattern: /\bBearer\s+[a-zA-Z0-9\-._~+/]+=*\b/gi },
|
|
7234
7319
|
{ name: "jwt", pattern: /\beyJ[a-zA-Z0-9_-]+\.eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\b/g },
|
|
@@ -7240,7 +7325,7 @@ var init_secret_redaction = __esm(() => {
|
|
|
7240
7325
|
/example\.com/i,
|
|
7241
7326
|
/your-api-key-here/i,
|
|
7242
7327
|
/sk-test/i,
|
|
7243
|
-
/
|
|
7328
|
+
/gh[p]_xxx/i
|
|
7244
7329
|
];
|
|
7245
7330
|
customRedactors = [];
|
|
7246
7331
|
});
|
|
@@ -14083,6 +14168,7 @@ var PROJECT_COLUMNS = [
|
|
|
14083
14168
|
"task_list_id",
|
|
14084
14169
|
"task_prefix",
|
|
14085
14170
|
"task_counter",
|
|
14171
|
+
"parent_id",
|
|
14086
14172
|
"created_at",
|
|
14087
14173
|
"updated_at",
|
|
14088
14174
|
"machine_id",
|
|
@@ -14302,7 +14388,7 @@ function importSqliteTodosStorageSnapshot(snapshot, db) {
|
|
|
14302
14388
|
}
|
|
14303
14389
|
}
|
|
14304
14390
|
};
|
|
14305
|
-
applyRows("projects", "projects", PROJECT_COLUMNS, snapshot.projects, "updated_at");
|
|
14391
|
+
applyRows("projects", "projects", PROJECT_COLUMNS, orderProjectsParentFirst(snapshot.projects), "updated_at");
|
|
14306
14392
|
applyRows("project_machine_paths", "project_machine_paths", PROJECT_MACHINE_PATH_COLUMNS, snapshot.projectMachinePaths ?? [], "updated_at");
|
|
14307
14393
|
applyRows("agents", "agents", AGENT_COLUMNS, snapshot.agents, "last_seen_at");
|
|
14308
14394
|
applyRows("task_lists", "task_lists", TASK_LIST_COLUMNS, snapshot.taskLists, "updated_at");
|
|
@@ -15757,8 +15843,12 @@ class PostgresJsonRecordStore {
|
|
|
15757
15843
|
OR ($11::text <> $2
|
|
15758
15844
|
AND NOT EXISTS (SELECT 1 FROM parent_chain WHERE object_id = $2)
|
|
15759
15845
|
AND NOT EXISTS (SELECT 1 FROM parent_chain WHERE cycle))) AS parent_acyclic,
|
|
15760
|
-
(SELECT
|
|
15761
|
-
|
|
15846
|
+
(COALESCE((SELECT payload->>'plan_id' FROM locked_task), '')
|
|
15847
|
+
IS DISTINCT FROM COALESCE($3::jsonb->>'plan_id', '')) AS membership_changed,
|
|
15848
|
+
(NOT (COALESCE((SELECT payload->>'plan_id' FROM locked_task), '')
|
|
15849
|
+
IS DISTINCT FROM COALESCE($3::jsonb->>'plan_id', ''))
|
|
15850
|
+
OR $8::text IS NULL
|
|
15851
|
+
OR EXISTS (SELECT 1 FROM locked_plans WHERE object_id = $8)) AS target_plan_found,
|
|
15762
15852
|
(SELECT payload->>'project_id' FROM locked_plans WHERE object_id = $8) AS target_project_id
|
|
15763
15853
|
), guarded AS (
|
|
15764
15854
|
SELECT
|
|
@@ -15781,7 +15871,6 @@ class PostgresJsonRecordStore {
|
|
|
15781
15871
|
AND guarded.version_matches
|
|
15782
15872
|
AND guarded.parent_found
|
|
15783
15873
|
AND guarded.parent_acyclic
|
|
15784
|
-
AND guarded.all_plans_found
|
|
15785
15874
|
AND guarded.target_plan_found
|
|
15786
15875
|
AND NOT guarded.project_conflict
|
|
15787
15876
|
ON CONFLICT (service, object_type, object_id) DO UPDATE SET
|
|
@@ -15797,7 +15886,7 @@ class PostgresJsonRecordStore {
|
|
|
15797
15886
|
RETURNING payload
|
|
15798
15887
|
)
|
|
15799
15888
|
SELECT guarded.task_found, guarded.version_matches, guarded.parent_found, guarded.parent_acyclic,
|
|
15800
|
-
guarded.
|
|
15889
|
+
guarded.membership_changed, guarded.target_plan_found, guarded.project_conflict,
|
|
15801
15890
|
(SELECT payload FROM stored) AS payload,
|
|
15802
15891
|
(SELECT payload FROM locked_task) AS current_payload
|
|
15803
15892
|
FROM guarded`, [
|
|
@@ -15829,7 +15918,7 @@ class PostgresJsonRecordStore {
|
|
|
15829
15918
|
if (parentGuard && !row?.parent_acyclic && parentGuard.parentId) {
|
|
15830
15919
|
throw new ResourceConflictError("TASK_PARENT_CYCLE", `TASK_PARENT_CYCLE: assigning parent ${parentGuard.parentId} to task ${value.id} would create or retain a parent cycle`);
|
|
15831
15920
|
}
|
|
15832
|
-
if (!row?.
|
|
15921
|
+
if (!row?.target_plan_found) {
|
|
15833
15922
|
throw new PlanProjectLinkError("PLAN_PROJECT_LINK_PLAN_NOT_FOUND", `Plan membership changed through a missing plan: ${targetPlanId ?? planIds.join(", ")}`, { plan_ids: planIds, target_plan_id: targetPlanId });
|
|
15834
15923
|
}
|
|
15835
15924
|
if (row.project_conflict) {
|
|
@@ -17041,17 +17130,26 @@ async function getChangedSince(since, filters, store) {
|
|
|
17041
17130
|
async function createProject2(input, store, context) {
|
|
17042
17131
|
const timestamp2 = new Date().toISOString();
|
|
17043
17132
|
const derivedSlug = slugifyRaw(input.name);
|
|
17044
|
-
const taskListId = input.task_list_id === undefined ?
|
|
17133
|
+
const taskListId = input.task_list_id === undefined ? derivedSlug : slugifyRaw(input.task_list_id);
|
|
17045
17134
|
if (!derivedSlug || !taskListId)
|
|
17046
17135
|
throw new Error("Project name and task-list slug must be non-empty");
|
|
17136
|
+
const parentId = input.parent_id ?? null;
|
|
17137
|
+
const id = randomUUID3();
|
|
17138
|
+
if (parentId !== null) {
|
|
17139
|
+
const parent = await store.get("projects", parentId);
|
|
17140
|
+
if (!parent)
|
|
17141
|
+
throw new ProjectNotFoundError(parentId);
|
|
17142
|
+
await assertNotProjectAncestorPostgres(id, parentId, store);
|
|
17143
|
+
}
|
|
17047
17144
|
const project = {
|
|
17048
|
-
id
|
|
17145
|
+
id,
|
|
17049
17146
|
name: input.name,
|
|
17050
17147
|
path: input.path,
|
|
17051
17148
|
description: input.description ?? null,
|
|
17052
17149
|
task_list_id: taskListId,
|
|
17053
17150
|
task_prefix: input.task_prefix ?? await generateProjectPrefix(input.name, store),
|
|
17054
17151
|
task_counter: 0,
|
|
17152
|
+
parent_id: parentId,
|
|
17055
17153
|
created_at: timestamp2,
|
|
17056
17154
|
updated_at: timestamp2,
|
|
17057
17155
|
machine_id: store.machineId(context),
|
|
@@ -17059,12 +17157,38 @@ async function createProject2(input, store, context) {
|
|
|
17059
17157
|
};
|
|
17060
17158
|
return store.upsert("projects", project, context);
|
|
17061
17159
|
}
|
|
17160
|
+
async function assertNotProjectAncestorPostgres(projectId, candidateParentId, store) {
|
|
17161
|
+
const all = await store.list("projects");
|
|
17162
|
+
const byId = new Map(all.map((project) => [project.id, project.parent_id ?? null]));
|
|
17163
|
+
let cursor = candidateParentId;
|
|
17164
|
+
const seen = new Set;
|
|
17165
|
+
while (cursor !== null) {
|
|
17166
|
+
if (cursor === projectId) {
|
|
17167
|
+
throw new ResourceConflictError("PROJECT_PARENT_CYCLE", `Project "${projectId}" cannot be placed under its own descendant`);
|
|
17168
|
+
}
|
|
17169
|
+
if (seen.has(cursor))
|
|
17170
|
+
break;
|
|
17171
|
+
seen.add(cursor);
|
|
17172
|
+
cursor = byId.get(cursor) ?? null;
|
|
17173
|
+
}
|
|
17174
|
+
}
|
|
17062
17175
|
async function updateProject2(id, input, store) {
|
|
17063
17176
|
if ("task_list_id" in input) {
|
|
17064
17177
|
throw new Error("task_list_id cannot be changed by updateProject; use renameProject for an atomic canonical rename");
|
|
17065
17178
|
}
|
|
17066
17179
|
const project = await requireRecord("projects", id, store);
|
|
17067
|
-
|
|
17180
|
+
if (input.parent_id !== undefined && input.parent_id !== null) {
|
|
17181
|
+
const parent = await store.get("projects", input.parent_id);
|
|
17182
|
+
if (!parent)
|
|
17183
|
+
throw new ProjectNotFoundError(input.parent_id);
|
|
17184
|
+
await assertNotProjectAncestorPostgres(id, input.parent_id, store);
|
|
17185
|
+
}
|
|
17186
|
+
const updated = {
|
|
17187
|
+
...project,
|
|
17188
|
+
...definedPatch(input),
|
|
17189
|
+
...input.parent_id !== undefined ? { parent_id: input.parent_id } : {},
|
|
17190
|
+
updated_at: new Date().toISOString()
|
|
17191
|
+
};
|
|
17068
17192
|
return store.upsert("projects", updated);
|
|
17069
17193
|
}
|
|
17070
17194
|
async function createPlan2(input, store, context) {
|
package/dist/task-manifest.js
CHANGED
|
@@ -749,8 +749,8 @@ var init_secret_redaction = __esm(() => {
|
|
|
749
749
|
SECRET_REDACTION_SCHEMA = ["todos", "secret_redaction", "v1"].join(".");
|
|
750
750
|
DEFAULT_PATTERNS = [
|
|
751
751
|
{ name: "openai_sk", pattern: /\bsk-[a-zA-Z0-9]{10,}\b/g },
|
|
752
|
-
{ name: "github_pat", pattern: /\
|
|
753
|
-
{ name: "github_oauth", pattern: /\
|
|
752
|
+
{ name: "github_pat", pattern: /\bgh[p]_[a-zA-Z0-9]{20,}\b/g },
|
|
753
|
+
{ name: "github_oauth", pattern: /\bgh[o]_[a-zA-Z0-9]{20,}\b/g },
|
|
754
754
|
{ name: "aws_access_key", pattern: /\bAKIA[0-9A-Z]{16}\b/g },
|
|
755
755
|
{ name: "bearer_token", pattern: /\bBearer\s+[a-zA-Z0-9\-._~+/]+=*\b/gi },
|
|
756
756
|
{ name: "jwt", pattern: /\beyJ[a-zA-Z0-9_-]+\.eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\b/g },
|
|
@@ -762,7 +762,7 @@ var init_secret_redaction = __esm(() => {
|
|
|
762
762
|
/example\.com/i,
|
|
763
763
|
/your-api-key-here/i,
|
|
764
764
|
/sk-test/i,
|
|
765
|
-
/
|
|
765
|
+
/gh[p]_xxx/i
|
|
766
766
|
];
|
|
767
767
|
customRedactors = [];
|
|
768
768
|
});
|
package/dist/testing.d.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* so any test that spawns the CLI with `{ ...process.env }`, or loads the SDK in-process,
|
|
8
8
|
* writes into the **shared hosted store**. That is not hypothetical:
|
|
9
9
|
*
|
|
10
|
-
* -
|
|
10
|
+
* - a consumer test suite leaked **1,151** rows (`seed-task-<epoch>`,
|
|
11
11
|
* `Short ID resolution test`, …) between 2026-04-06 and 2026-06-21.
|
|
12
12
|
* - `hasna/open-loops` `drain.test.ts` leaked **943** `Merge the release PR` rows
|
|
13
13
|
* between 2026-07-05 and 2026-07-15.
|
|
@@ -48,17 +48,26 @@ export declare const SHARED_TODOS_STORE_ENV_KEYS: readonly ["HASNA_TODOS_API_URL
|
|
|
48
48
|
* destination. They are pinned rather than blanked, so they are exempt from the scrub
|
|
49
49
|
* coverage assertion.
|
|
50
50
|
*/
|
|
51
|
-
export declare const LOCAL_ONLY_TODOS_ENV_KEYS: readonly ["
|
|
51
|
+
export declare const LOCAL_ONLY_TODOS_ENV_KEYS: readonly ["HASNA_TODOS_DB_PATH", "TODOS_DB_PATH"];
|
|
52
|
+
/**
|
|
53
|
+
* The retired storage-mode variables (owner directive 2026-08-15). They are
|
|
54
|
+
* banned: if any is present the resolver THROWS, so a test env must DELETE them,
|
|
55
|
+
* never blank them — a blank leftover is still a stale fragment. They are listed
|
|
56
|
+
* here so the scrub-coverage assertion treats them as handled.
|
|
57
|
+
*/
|
|
58
|
+
export declare const REMOVED_TODOS_ENV_KEYS: readonly ["HASNA_TODOS_STORAGE_MODE", "HASNA_TODOS_MODE", "TODOS_STORAGE_MODE", "TODOS_MODE"];
|
|
52
59
|
export type SharedTodosStoreEnvKey = (typeof SHARED_TODOS_STORE_ENV_KEYS)[number];
|
|
53
60
|
export type TodosTestEnv = Record<string, string | undefined>;
|
|
54
61
|
/**
|
|
55
|
-
* `process.env` with every shared-store pointer blanked and
|
|
56
|
-
*
|
|
62
|
+
* `process.env` with every shared-store pointer blanked and the retired
|
|
63
|
+
* storage-mode variables deleted, so the transport resolves to a local SQLite
|
|
64
|
+
* file. Overrides are applied last, so a test that deliberately exercises the
|
|
57
65
|
* hosted transport against a throwaway server can still opt back in explicitly.
|
|
58
66
|
*
|
|
59
|
-
* Blank string, not `delete
|
|
60
|
-
*
|
|
61
|
-
*
|
|
67
|
+
* Blank string, not `delete`, for the shared-store pointers: a blanked URL/key
|
|
68
|
+
* is treated as absent, and there is no partial-config fallback. The retired
|
|
69
|
+
* `*_STORAGE_MODE` variables are DELETED, not blanked, because their mere
|
|
70
|
+
* presence is a hard error.
|
|
62
71
|
*/
|
|
63
72
|
export declare function localTodosTestEnv(overrides?: TodosTestEnv): TodosTestEnv;
|
|
64
73
|
/**
|
package/dist/testing.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH;;;;;;;;;GASG;AACH,eAAO,MAAM,2BAA2B,qNAa9B,CAAC;AAEX;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,
|
|
1
|
+
{"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH;;;;;;;;;GASG;AACH,eAAO,MAAM,2BAA2B,qNAa9B,CAAC;AAEX;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,mDAG5B,CAAC;AAEX;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,+FAKzB,CAAC;AAEX,MAAM,MAAM,sBAAsB,GAAG,CAAC,OAAO,2BAA2B,CAAC,CAAC,MAAM,CAAC,CAAC;AAElF,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;AAE9D;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,GAAE,YAAiB,GAAG,YAAY,CAK5E;AAED;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CAAC,SAAS,GAAE,YAAiB,GAAG,MAAM,IAAI,CAoB/E;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,GAAG,GAAE,YAA0C,GAAG,IAAI,CAQ7F"}
|