@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/mcp/index.js
CHANGED
|
@@ -2096,6 +2096,10 @@ var init_migrations = __esm(() => {
|
|
|
2096
2096
|
);
|
|
2097
2097
|
INSERT OR IGNORE INTO _migrations (id) VALUES (70);
|
|
2098
2098
|
COMMIT;
|
|
2099
|
+
`,
|
|
2100
|
+
`BEGIN;
|
|
2101
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (71);
|
|
2102
|
+
COMMIT;
|
|
2099
2103
|
`
|
|
2100
2104
|
];
|
|
2101
2105
|
});
|
|
@@ -2769,6 +2773,8 @@ function ensureSchema(db) {
|
|
|
2769
2773
|
ensureColumn("projects", "task_list_id", "TEXT");
|
|
2770
2774
|
ensureColumn("projects", "task_prefix", "TEXT");
|
|
2771
2775
|
ensureColumn("projects", "task_counter", "INTEGER NOT NULL DEFAULT 0");
|
|
2776
|
+
ensureColumn("projects", "parent_id", "TEXT REFERENCES projects(id) ON DELETE SET NULL");
|
|
2777
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_projects_parent_id ON projects(parent_id)");
|
|
2772
2778
|
ensureColumn("tasks", "plan_id", "TEXT REFERENCES plans(id) ON DELETE SET NULL");
|
|
2773
2779
|
ensureColumn("tasks", "task_list_id", "TEXT REFERENCES task_lists(id) ON DELETE SET NULL");
|
|
2774
2780
|
ensureColumn("tasks", "short_id", "TEXT");
|
|
@@ -4129,6 +4135,7 @@ __export(exports_config, {
|
|
|
4129
4135
|
TODOS_STORAGE_FALLBACK_ENV: () => TODOS_STORAGE_FALLBACK_ENV,
|
|
4130
4136
|
TODOS_STORAGE_ENV: () => TODOS_STORAGE_ENV,
|
|
4131
4137
|
STORAGE_TABLES: () => STORAGE_TABLES,
|
|
4138
|
+
REMOVED_STORAGE_MODE_ENV_KEYS: () => REMOVED_STORAGE_MODE_ENV_KEYS,
|
|
4132
4139
|
CANONICAL_TODOS_RDS_RUNTIME_PATH_ENV: () => CANONICAL_TODOS_RDS_RUNTIME_PATH_ENV,
|
|
4133
4140
|
CANONICAL_TODOS_RDS_DATABASE: () => CANONICAL_TODOS_RDS_DATABASE,
|
|
4134
4141
|
CANONICAL_TODOS_RDS_CLUSTER_ENV: () => CANONICAL_TODOS_RDS_CLUSTER_ENV
|
|
@@ -4190,25 +4197,36 @@ function assertTodosRemoteStorageConfig(config) {
|
|
|
4190
4197
|
if (!isTodosPostgresBackend(config))
|
|
4191
4198
|
return;
|
|
4192
4199
|
if (!config.database?.url) {
|
|
4193
|
-
throw new Error(`${TODOS_STORAGE_ENV.databaseUrl} is required when
|
|
4200
|
+
throw new Error(`${TODOS_STORAGE_ENV.databaseUrl} is required when the postgresql backend is selected`);
|
|
4194
4201
|
}
|
|
4195
4202
|
}
|
|
4196
4203
|
function parseStorageBackend(value) {
|
|
4197
4204
|
const normalized = clean(value)?.toLowerCase();
|
|
4198
4205
|
if (!normalized)
|
|
4199
4206
|
return "sqlite";
|
|
4200
|
-
if (normalized === "sqlite"
|
|
4201
|
-
return
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
4207
|
+
if (normalized === "sqlite")
|
|
4208
|
+
return "sqlite";
|
|
4209
|
+
if (normalized === "postgres" || normalized === "postgresql")
|
|
4210
|
+
return "postgres";
|
|
4211
|
+
if (["local", "remote", "cloud", "hybrid", "self_hosted"].includes(normalized)) {
|
|
4212
|
+
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.`);
|
|
4213
|
+
}
|
|
4214
|
+
throw new Error(`Storage backend must be sqlite or postgres`);
|
|
4206
4215
|
}
|
|
4207
4216
|
function parseStorageMode(value) {
|
|
4208
4217
|
return parseStorageBackend(value);
|
|
4209
4218
|
}
|
|
4210
4219
|
function getTodosStorageBackend(env = process.env) {
|
|
4211
|
-
|
|
4220
|
+
for (const key of REMOVED_STORAGE_MODE_ENV_KEYS) {
|
|
4221
|
+
if (Object.hasOwn(env, key) && env[key] !== undefined) {
|
|
4222
|
+
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.`);
|
|
4223
|
+
}
|
|
4224
|
+
}
|
|
4225
|
+
if (!getTodosStorageDatabaseUrl(env))
|
|
4226
|
+
return "sqlite";
|
|
4227
|
+
if (isTodosShadowEnabled(env))
|
|
4228
|
+
return "sqlite";
|
|
4229
|
+
return "postgres";
|
|
4212
4230
|
}
|
|
4213
4231
|
function getTodosStorageMode(env = process.env) {
|
|
4214
4232
|
return getTodosStorageBackend(env);
|
|
@@ -4282,7 +4300,7 @@ function parsePositiveInteger(value, fallback) {
|
|
|
4282
4300
|
}
|
|
4283
4301
|
return parsed;
|
|
4284
4302
|
}
|
|
4285
|
-
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"
|
|
4303
|
+
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";
|
|
4286
4304
|
var init_config = __esm(() => {
|
|
4287
4305
|
TODOS_STORAGE_TABLES = [
|
|
4288
4306
|
"todos_sync_records",
|
|
@@ -4290,7 +4308,6 @@ var init_config = __esm(() => {
|
|
|
4290
4308
|
];
|
|
4291
4309
|
STORAGE_TABLES = TODOS_STORAGE_TABLES;
|
|
4292
4310
|
TODOS_STORAGE_ENV = {
|
|
4293
|
-
mode: "HASNA_TODOS_STORAGE_MODE",
|
|
4294
4311
|
shadow: "HASNA_TODOS_SHADOW",
|
|
4295
4312
|
databaseUrl: "HASNA_TODOS_DATABASE_URL",
|
|
4296
4313
|
databaseSsl: "HASNA_TODOS_DATABASE_SSL",
|
|
@@ -4307,7 +4324,6 @@ var init_config = __esm(() => {
|
|
|
4307
4324
|
syncDryRun: "HASNA_TODOS_SYNC_DRY_RUN"
|
|
4308
4325
|
};
|
|
4309
4326
|
TODOS_STORAGE_FALLBACK_ENV = {
|
|
4310
|
-
mode: "TODOS_STORAGE_MODE",
|
|
4311
4327
|
shadow: "TODOS_SHADOW",
|
|
4312
4328
|
databaseUrl: "TODOS_DATABASE_URL",
|
|
4313
4329
|
databaseSsl: "TODOS_DATABASE_SSL",
|
|
@@ -4323,14 +4339,12 @@ var init_config = __esm(() => {
|
|
|
4323
4339
|
syncBatchSize: "TODOS_SYNC_BATCH_SIZE",
|
|
4324
4340
|
syncDryRun: "TODOS_SYNC_DRY_RUN"
|
|
4325
4341
|
};
|
|
4326
|
-
|
|
4327
|
-
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
cloud: "postgres"
|
|
4333
|
-
};
|
|
4342
|
+
REMOVED_STORAGE_MODE_ENV_KEYS = [
|
|
4343
|
+
"HASNA_TODOS_STORAGE_MODE",
|
|
4344
|
+
"HASNA_TODOS_MODE",
|
|
4345
|
+
"TODOS_STORAGE_MODE",
|
|
4346
|
+
"TODOS_MODE"
|
|
4347
|
+
];
|
|
4334
4348
|
});
|
|
4335
4349
|
|
|
4336
4350
|
// src/storage/shadow-outbox-schema.ts
|
|
@@ -9677,8 +9691,8 @@ var init_secret_redaction = __esm(() => {
|
|
|
9677
9691
|
SECRET_REDACTION_SCHEMA = ["todos", "secret_redaction", "v1"].join(".");
|
|
9678
9692
|
DEFAULT_PATTERNS = [
|
|
9679
9693
|
{ name: "openai_sk", pattern: /\bsk-[a-zA-Z0-9]{10,}\b/g },
|
|
9680
|
-
{ name: "github_pat", pattern: /\
|
|
9681
|
-
{ name: "github_oauth", pattern: /\
|
|
9694
|
+
{ name: "github_pat", pattern: /\bgh[p]_[a-zA-Z0-9]{20,}\b/g },
|
|
9695
|
+
{ name: "github_oauth", pattern: /\bgh[o]_[a-zA-Z0-9]{20,}\b/g },
|
|
9682
9696
|
{ name: "aws_access_key", pattern: /\bAKIA[0-9A-Z]{16}\b/g },
|
|
9683
9697
|
{ name: "bearer_token", pattern: /\bBearer\s+[a-zA-Z0-9\-._~+/]+=*\b/gi },
|
|
9684
9698
|
{ name: "jwt", pattern: /\beyJ[a-zA-Z0-9_-]+\.eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\b/g },
|
|
@@ -9690,7 +9704,7 @@ var init_secret_redaction = __esm(() => {
|
|
|
9690
9704
|
/example\.com/i,
|
|
9691
9705
|
/your-api-key-here/i,
|
|
9692
9706
|
/sk-test/i,
|
|
9693
|
-
/
|
|
9707
|
+
/gh[p]_xxx/i
|
|
9694
9708
|
];
|
|
9695
9709
|
customRedactors = [];
|
|
9696
9710
|
});
|
|
@@ -9997,10 +10011,12 @@ __export(exports_projects, {
|
|
|
9997
10011
|
renameProject: () => renameProject,
|
|
9998
10012
|
removeProjectSource: () => removeProjectSource,
|
|
9999
10013
|
removeMachineLocalPath: () => removeMachineLocalPath,
|
|
10014
|
+
orderProjectsParentFirst: () => orderProjectsParentFirst,
|
|
10000
10015
|
nextTaskShortId: () => nextTaskShortId,
|
|
10001
10016
|
listProjects: () => listProjects,
|
|
10002
10017
|
listProjectSources: () => listProjectSources,
|
|
10003
10018
|
listMachineLocalPaths: () => listMachineLocalPaths,
|
|
10019
|
+
listChildProjects: () => listChildProjects,
|
|
10004
10020
|
getProjectWithSources: () => getProjectWithSources,
|
|
10005
10021
|
getProjectByPath: () => getProjectByPath,
|
|
10006
10022
|
getProject: () => getProject,
|
|
@@ -10008,6 +10024,7 @@ __export(exports_projects, {
|
|
|
10008
10024
|
ensureProject: () => ensureProject,
|
|
10009
10025
|
deleteProject: () => deleteProject,
|
|
10010
10026
|
createProject: () => createProject,
|
|
10027
|
+
assertNotProjectAncestor: () => assertNotProjectAncestor,
|
|
10011
10028
|
addProjectSource: () => addProjectSource
|
|
10012
10029
|
});
|
|
10013
10030
|
function slugify(name) {
|
|
@@ -10039,17 +10056,24 @@ function createProject(input, db) {
|
|
|
10039
10056
|
const id = uuid();
|
|
10040
10057
|
const timestamp2 = now();
|
|
10041
10058
|
const derivedSlug = slugify(input.name);
|
|
10042
|
-
const taskListId = input.task_list_id === undefined ?
|
|
10059
|
+
const taskListId = input.task_list_id === undefined ? derivedSlug : slugify(input.task_list_id);
|
|
10043
10060
|
if (!derivedSlug || !taskListId)
|
|
10044
10061
|
throw new Error("Project name and task-list slug must be non-empty");
|
|
10045
10062
|
const slugConflict = d.query("SELECT id FROM projects WHERE task_list_id = ? LIMIT 1").get(taskListId);
|
|
10046
10063
|
if (slugConflict || !claimCanonicalSlug("project", "global", taskListId, id, d)) {
|
|
10047
10064
|
throw new ResourceConflictError("PROJECT_SLUG_CONFLICT", `Project slug "${taskListId}" already exists`);
|
|
10048
10065
|
}
|
|
10066
|
+
const parentId = input.parent_id ?? null;
|
|
10067
|
+
if (parentId !== null) {
|
|
10068
|
+
const parent = getProject(parentId, d);
|
|
10069
|
+
if (!parent)
|
|
10070
|
+
throw new ProjectNotFoundError(parentId);
|
|
10071
|
+
assertNotProjectAncestor(id, parentId, d);
|
|
10072
|
+
}
|
|
10049
10073
|
const taskPrefix = input.task_prefix || generatePrefix(input.name, d);
|
|
10050
10074
|
const machineId = currentStorageMachineId(d);
|
|
10051
|
-
d.run(`INSERT INTO projects (id, name, path, description, task_list_id, task_prefix, task_counter, created_at, updated_at, machine_id)
|
|
10052
|
-
VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?)`, [id, input.name, input.path, input.description || null, taskListId, taskPrefix, timestamp2, timestamp2, machineId]);
|
|
10075
|
+
d.run(`INSERT INTO projects (id, name, path, description, task_list_id, task_prefix, task_counter, parent_id, created_at, updated_at, machine_id)
|
|
10076
|
+
VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?)`, [id, input.name, input.path, input.description || null, taskListId, taskPrefix, parentId, timestamp2, timestamp2, machineId]);
|
|
10053
10077
|
return getProject(id, d);
|
|
10054
10078
|
})();
|
|
10055
10079
|
}
|
|
@@ -10074,6 +10098,64 @@ function listProjects(db) {
|
|
|
10074
10098
|
const d = db || getDatabase();
|
|
10075
10099
|
return d.query("SELECT * FROM projects ORDER BY name").all();
|
|
10076
10100
|
}
|
|
10101
|
+
function listChildProjects(parentId, db) {
|
|
10102
|
+
const d = db || getDatabase();
|
|
10103
|
+
return d.query("SELECT * FROM projects WHERE parent_id = ? ORDER BY name").all(parentId);
|
|
10104
|
+
}
|
|
10105
|
+
function orderProjectsParentFirst(projects) {
|
|
10106
|
+
const projectId = (project) => {
|
|
10107
|
+
const id = project.id;
|
|
10108
|
+
return typeof id === "string" && id.length > 0 ? id : null;
|
|
10109
|
+
};
|
|
10110
|
+
const parentId = (project) => {
|
|
10111
|
+
const parent = project.parent_id;
|
|
10112
|
+
return parent == null ? null : String(parent);
|
|
10113
|
+
};
|
|
10114
|
+
const byId = new Set;
|
|
10115
|
+
for (const project of projects) {
|
|
10116
|
+
const id = projectId(project);
|
|
10117
|
+
if (id !== null)
|
|
10118
|
+
byId.add(id);
|
|
10119
|
+
}
|
|
10120
|
+
const ordered = [];
|
|
10121
|
+
const emitted = new Set;
|
|
10122
|
+
let remaining = [...projects];
|
|
10123
|
+
let progress = true;
|
|
10124
|
+
while (progress && remaining.length > 0) {
|
|
10125
|
+
progress = false;
|
|
10126
|
+
const deferred = [];
|
|
10127
|
+
for (const project of remaining) {
|
|
10128
|
+
const parent = parentId(project);
|
|
10129
|
+
if (parent === null || emitted.has(parent) || !byId.has(parent)) {
|
|
10130
|
+
ordered.push(project);
|
|
10131
|
+
const id = projectId(project);
|
|
10132
|
+
if (id !== null)
|
|
10133
|
+
emitted.add(id);
|
|
10134
|
+
progress = true;
|
|
10135
|
+
} else {
|
|
10136
|
+
deferred.push(project);
|
|
10137
|
+
}
|
|
10138
|
+
}
|
|
10139
|
+
remaining = deferred;
|
|
10140
|
+
}
|
|
10141
|
+
ordered.push(...remaining);
|
|
10142
|
+
return ordered;
|
|
10143
|
+
}
|
|
10144
|
+
function assertNotProjectAncestor(projectId, ancestorId, db) {
|
|
10145
|
+
const d = db || getDatabase();
|
|
10146
|
+
let cursor = ancestorId;
|
|
10147
|
+
const seen = new Set;
|
|
10148
|
+
while (cursor !== null) {
|
|
10149
|
+
if (cursor === projectId) {
|
|
10150
|
+
throw new ResourceConflictError("PROJECT_PARENT_CYCLE", `Project "${projectId}" cannot be placed under its own descendant`);
|
|
10151
|
+
}
|
|
10152
|
+
if (seen.has(cursor))
|
|
10153
|
+
break;
|
|
10154
|
+
seen.add(cursor);
|
|
10155
|
+
const row = d.query("SELECT parent_id FROM projects WHERE id = ?").get(cursor);
|
|
10156
|
+
cursor = row?.parent_id ?? null;
|
|
10157
|
+
}
|
|
10158
|
+
}
|
|
10077
10159
|
function updateProject(id, input, db) {
|
|
10078
10160
|
const d = db || getDatabase();
|
|
10079
10161
|
const project = getProject(id, d);
|
|
@@ -10096,6 +10178,16 @@ function updateProject(id, input, db) {
|
|
|
10096
10178
|
sets.push("path = ?");
|
|
10097
10179
|
params.push(input.path);
|
|
10098
10180
|
}
|
|
10181
|
+
if (input.parent_id !== undefined) {
|
|
10182
|
+
if (input.parent_id !== null) {
|
|
10183
|
+
const parent = getProject(input.parent_id, d);
|
|
10184
|
+
if (!parent)
|
|
10185
|
+
throw new ProjectNotFoundError(input.parent_id);
|
|
10186
|
+
assertNotProjectAncestor(id, input.parent_id, d);
|
|
10187
|
+
}
|
|
10188
|
+
sets.push("parent_id = ?");
|
|
10189
|
+
params.push(input.parent_id);
|
|
10190
|
+
}
|
|
10099
10191
|
params.push(id);
|
|
10100
10192
|
d.run(`UPDATE projects SET ${sets.join(", ")} WHERE id = ?`, params);
|
|
10101
10193
|
return getProject(id, d);
|
|
@@ -21093,40 +21185,38 @@ var init_page_validation = __esm(() => {
|
|
|
21093
21185
|
|
|
21094
21186
|
// src/cli/cloud-router.ts
|
|
21095
21187
|
import { resolveStorageClient } from "@hasna/contracts/client/storage";
|
|
21096
|
-
import { normalizeStorageMode } from "@hasna/contracts/mode";
|
|
21097
21188
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
21098
21189
|
import { resolve as resolvePath } from "path";
|
|
21099
|
-
function
|
|
21100
|
-
const
|
|
21101
|
-
|
|
21190
|
+
function assertNoLegacyStorageMode(env = process.env) {
|
|
21191
|
+
for (const key of LEGACY_STORAGE_MODE_KEYS) {
|
|
21192
|
+
if (Object.hasOwn(env, key) && env[key] !== undefined) {
|
|
21193
|
+
throw new Error(`REMOTE_STORAGE_MODE_REMOVED: ${key} was removed. Deployment modes no longer exist: delete the storage-mode variable. ` + `The client uses the on-box SQLite store, or the HTTP API selected by ` + `HASNA_TODOS_API_URL + HASNA_TODOS_API_KEY. ` + `On the server, set HASNA_TODOS_DATABASE_URL to select the postgresql backend, ` + `or leave it unset for sqlite.`);
|
|
21194
|
+
}
|
|
21195
|
+
}
|
|
21102
21196
|
}
|
|
21103
21197
|
function resolveTodosCliStorageMode(env = process.env) {
|
|
21104
|
-
|
|
21105
|
-
|
|
21106
|
-
|
|
21107
|
-
|
|
21198
|
+
assertNoLegacyStorageMode(env);
|
|
21199
|
+
const urlValue = env.HASNA_TODOS_API_URL?.trim();
|
|
21200
|
+
const keyValue = env.HASNA_TODOS_API_KEY?.trim();
|
|
21201
|
+
if (urlValue && keyValue) {
|
|
21202
|
+
return {
|
|
21203
|
+
mode: "http",
|
|
21204
|
+
transport: "http",
|
|
21205
|
+
selected: true,
|
|
21206
|
+
source: "HASNA_TODOS_API_URL+HASNA_TODOS_API_KEY"
|
|
21207
|
+
};
|
|
21108
21208
|
}
|
|
21109
|
-
|
|
21110
|
-
|
|
21111
|
-
for (const [source, value] of [
|
|
21112
|
-
["HASNA_TODOS_STORAGE_MODE", canonical],
|
|
21113
|
-
["TODOS_STORAGE_MODE", fallback]
|
|
21114
|
-
]) {
|
|
21115
|
-
if (value && !(value in TRANSPORT_TOKENS)) {
|
|
21116
|
-
throw new Error(`REMOTE_STORAGE_MODE_INVALID: ${source}=${value} must be sqlite (local file) or http (hosted /v1 authority); ` + "legacy values local and remote are accepted; " + "local SQLite fallback is disabled for invalid routing state");
|
|
21117
|
-
}
|
|
21209
|
+
if (urlValue) {
|
|
21210
|
+
throw new Error("REMOTE_API_KEY_MISSING: remote Todos storage requires HASNA_TODOS_API_KEY; local SQLite fallback is disabled");
|
|
21118
21211
|
}
|
|
21119
|
-
|
|
21120
|
-
|
|
21121
|
-
if (canonicalTransport && fallbackTransport && canonicalTransport !== fallbackTransport) {
|
|
21122
|
-
throw new Error(`REMOTE_STORAGE_MODE_CONFLICT: HASNA_TODOS_STORAGE_MODE=${canonical} conflicts with ` + `TODOS_STORAGE_MODE=${fallback}; local SQLite fallback is disabled`);
|
|
21212
|
+
if (keyValue) {
|
|
21213
|
+
throw new Error("REMOTE_API_URL_MISSING: remote Todos storage requires HASNA_TODOS_API_URL; local SQLite fallback is disabled");
|
|
21123
21214
|
}
|
|
21124
|
-
const transport = canonicalTransport ?? fallbackTransport ?? "sqlite";
|
|
21125
21215
|
return {
|
|
21126
|
-
mode:
|
|
21127
|
-
transport,
|
|
21128
|
-
selected:
|
|
21129
|
-
source:
|
|
21216
|
+
mode: "sqlite",
|
|
21217
|
+
transport: "sqlite",
|
|
21218
|
+
selected: false,
|
|
21219
|
+
source: "default"
|
|
21130
21220
|
};
|
|
21131
21221
|
}
|
|
21132
21222
|
function requestedTransport(env) {
|
|
@@ -21170,7 +21260,7 @@ function getTodosRemoteAuthorityConfigStatus(env = process.env) {
|
|
|
21170
21260
|
return {
|
|
21171
21261
|
selected: true,
|
|
21172
21262
|
ok: false,
|
|
21173
|
-
mode:
|
|
21263
|
+
mode: "invalid",
|
|
21174
21264
|
api_url_configured: Boolean(env.HASNA_TODOS_API_URL?.trim()),
|
|
21175
21265
|
api_key_configured: Boolean(env.HASNA_TODOS_API_KEY?.trim()),
|
|
21176
21266
|
v1_base_url: null,
|
|
@@ -21216,27 +21306,12 @@ function getTodosRemoteAuthorityConfigStatus(env = process.env) {
|
|
|
21216
21306
|
local_fallback: false
|
|
21217
21307
|
};
|
|
21218
21308
|
}
|
|
21219
|
-
function serverStorageMode(normalize = normalizeStorageMode) {
|
|
21220
|
-
const useCache = normalize === normalizeStorageMode;
|
|
21221
|
-
if (useCache && cachedServerMode !== null)
|
|
21222
|
-
return cachedServerMode;
|
|
21223
|
-
for (const candidate of SERVER_MODE_CANDIDATES) {
|
|
21224
|
-
try {
|
|
21225
|
-
normalize(candidate);
|
|
21226
|
-
if (useCache)
|
|
21227
|
-
cachedServerMode = candidate;
|
|
21228
|
-
return candidate;
|
|
21229
|
-
} catch {}
|
|
21230
|
-
}
|
|
21231
|
-
throw new Error(`REMOTE_STORAGE_MODE_UNSUPPORTED: no known server storage mode is accepted by the installed ` + `@hasna/contracts (tried ${SERVER_MODE_CANDIDATES.join(", ")}); the storage-mode enum has changed. ` + `Add the new server token to SERVER_MODE_CANDIDATES in src/cli/cloud-router.ts; ` + `local SQLite fallback is disabled.`);
|
|
21232
|
-
}
|
|
21233
21309
|
function requireTodosRemoteAuthorityEnv(env) {
|
|
21234
21310
|
const status = getTodosRemoteAuthorityConfigStatus(env);
|
|
21235
21311
|
if (!status.ok)
|
|
21236
21312
|
throw new Error(status.issues[0]);
|
|
21237
21313
|
return {
|
|
21238
21314
|
...env,
|
|
21239
|
-
HASNA_TODOS_STORAGE_MODE: serverStorageMode(),
|
|
21240
21315
|
HASNA_TODOS_API_URL: status.v1_base_url.replace(/\/v1$/, ""),
|
|
21241
21316
|
HASNA_TODOS_API_KEY: env.HASNA_TODOS_API_KEY.trim()
|
|
21242
21317
|
};
|
|
@@ -21274,7 +21349,31 @@ function classifyRemoteRequestError(baseUrl, route, error) {
|
|
|
21274
21349
|
}
|
|
21275
21350
|
throw error;
|
|
21276
21351
|
}
|
|
21277
|
-
function
|
|
21352
|
+
function withBoundedRemoteRequest(options, requestTimeoutMs, run) {
|
|
21353
|
+
const controller = new AbortController;
|
|
21354
|
+
const onBaseAbort = () => controller.abort();
|
|
21355
|
+
if (options?.signal) {
|
|
21356
|
+
if (options.signal.aborted)
|
|
21357
|
+
controller.abort();
|
|
21358
|
+
else
|
|
21359
|
+
options.signal.addEventListener("abort", onBaseAbort, { once: true });
|
|
21360
|
+
}
|
|
21361
|
+
let timer;
|
|
21362
|
+
const deadline = new Promise((_, reject) => {
|
|
21363
|
+
timer = setTimeout(() => {
|
|
21364
|
+
controller.abort();
|
|
21365
|
+
reject(new DOMException(`Todos remote request exceeded the ${requestTimeoutMs}ms bounded request timeout`, "AbortError"));
|
|
21366
|
+
}, requestTimeoutMs);
|
|
21367
|
+
});
|
|
21368
|
+
const attempt = run({ ...options, signal: controller.signal });
|
|
21369
|
+
attempt.catch(() => {});
|
|
21370
|
+
return Promise.race([attempt, deadline]).finally(() => {
|
|
21371
|
+
if (timer)
|
|
21372
|
+
clearTimeout(timer);
|
|
21373
|
+
options?.signal?.removeEventListener("abort", onBaseAbort);
|
|
21374
|
+
});
|
|
21375
|
+
}
|
|
21376
|
+
function protectRemoteClient(client, requestTimeoutMs) {
|
|
21278
21377
|
const baseUrl = remoteAuthorityBase(client);
|
|
21279
21378
|
const protect = async (route, request) => {
|
|
21280
21379
|
try {
|
|
@@ -21283,25 +21382,26 @@ function protectRemoteClient(client) {
|
|
|
21283
21382
|
return classifyRemoteRequestError(baseUrl, route, error);
|
|
21284
21383
|
}
|
|
21285
21384
|
};
|
|
21385
|
+
const bounded = (options, run) => withBoundedRemoteRequest(options, requestTimeoutMs, run);
|
|
21286
21386
|
const transport = client.transport;
|
|
21287
21387
|
const protectedTransport = {
|
|
21288
21388
|
baseUrl: transport.baseUrl,
|
|
21289
|
-
request: (method, path, body, options) => protect(path, () => transport.request(method, path, body,
|
|
21290
|
-
get: (path, options) => protect(path, () => transport.get(path,
|
|
21291
|
-
post: (path, body, options) => protect(path, () => transport.post(path, body,
|
|
21292
|
-
put: (path, body, options) => protect(path, () => transport.put(path, body,
|
|
21293
|
-
patch: (path, body, options) => protect(path, () => transport.patch(path, body,
|
|
21294
|
-
del: (path, body, options) => protect(path, () => transport.del(path, body,
|
|
21389
|
+
request: (method, path, body, options) => protect(path, () => bounded(options, (opts) => transport.request(method, path, body, opts))),
|
|
21390
|
+
get: (path, options) => protect(path, () => bounded(options, (opts) => transport.get(path, opts))),
|
|
21391
|
+
post: (path, body, options) => protect(path, () => bounded(options, (opts) => transport.post(path, body, opts))),
|
|
21392
|
+
put: (path, body, options) => protect(path, () => bounded(options, (opts) => transport.put(path, body, opts))),
|
|
21393
|
+
patch: (path, body, options) => protect(path, () => bounded(options, (opts) => transport.patch(path, body, opts))),
|
|
21394
|
+
del: (path, body, options) => protect(path, () => bounded(options, (opts) => transport.del(path, body, opts)))
|
|
21295
21395
|
};
|
|
21296
21396
|
return {
|
|
21297
21397
|
name: client.name,
|
|
21298
21398
|
baseUrl: client.baseUrl,
|
|
21299
21399
|
transport: protectedTransport,
|
|
21300
|
-
list: (resource, options) => protect(`/${resource}`, () => client.list(resource,
|
|
21301
|
-
get: (resource, id, options) => protect(`/${resource}/${encodeURIComponent(id)}`, () => client.get(resource, id,
|
|
21302
|
-
create: (resource, body, options) => protect(`/${resource}`, () => client.create(resource, body,
|
|
21303
|
-
update: (resource, id, patch, options) => protect(`/${resource}/${encodeURIComponent(id)}`, () => client.update(resource, id, patch,
|
|
21304
|
-
delete: (resource, id, options) => protect(`/${resource}/${encodeURIComponent(id)}`, () => client.delete(resource, id,
|
|
21400
|
+
list: (resource, options) => protect(`/${resource}`, () => bounded(options, (opts) => client.list(resource, opts))),
|
|
21401
|
+
get: (resource, id, options) => protect(`/${resource}/${encodeURIComponent(id)}`, () => bounded(options, (opts) => client.get(resource, id, opts))),
|
|
21402
|
+
create: (resource, body, options) => protect(`/${resource}`, () => bounded(options, (opts) => client.create(resource, body, opts))),
|
|
21403
|
+
update: (resource, id, patch, options) => protect(`/${resource}/${encodeURIComponent(id)}`, () => bounded(options, (opts) => client.update(resource, id, patch, opts))),
|
|
21404
|
+
delete: (resource, id, options) => protect(`/${resource}/${encodeURIComponent(id)}`, () => bounded(options, (opts) => client.delete(resource, id, opts)))
|
|
21305
21405
|
};
|
|
21306
21406
|
}
|
|
21307
21407
|
function remoteAuthorityBase(client) {
|
|
@@ -21322,13 +21422,19 @@ async function requiredRemoteRoute(client, route, request, recognized404Codes =
|
|
|
21322
21422
|
throw error;
|
|
21323
21423
|
}
|
|
21324
21424
|
}
|
|
21325
|
-
function getTodosCloudClient(env = process.env) {
|
|
21425
|
+
function getTodosCloudClient(env = process.env, requestTimeoutMs = REMOTE_REQUEST_TIMEOUT_MS) {
|
|
21326
21426
|
if (requestedTransport(env) !== "http")
|
|
21327
21427
|
return null;
|
|
21328
21428
|
const resolved = resolveStorageClient("todos", requireTodosRemoteAuthorityEnv(env), {
|
|
21329
|
-
fetchImpl: (input, init) => globalThis.fetch(input, { ...init, redirect: "manual" })
|
|
21429
|
+
fetchImpl: (input, init) => globalThis.fetch(input, { ...init, redirect: "manual" }),
|
|
21430
|
+
timeoutMs: requestTimeoutMs
|
|
21330
21431
|
});
|
|
21331
|
-
|
|
21432
|
+
if (resolved.transport === "cloud-http")
|
|
21433
|
+
return protectRemoteClient(resolved.client, requestTimeoutMs);
|
|
21434
|
+
const transportName = resolved.transport;
|
|
21435
|
+
if (transportName === "http")
|
|
21436
|
+
return protectRemoteClient(resolved.client, requestTimeoutMs);
|
|
21437
|
+
return null;
|
|
21332
21438
|
}
|
|
21333
21439
|
function unwrapTask(raw) {
|
|
21334
21440
|
if (raw && typeof raw === "object" && "task" in raw) {
|
|
@@ -21799,7 +21905,7 @@ async function cloudResolveTaskListRef(client, ref, projectId) {
|
|
|
21799
21905
|
return input.toLowerCase();
|
|
21800
21906
|
return (await cloudResolveTaskList(client, ref, projectId)).id;
|
|
21801
21907
|
}
|
|
21802
|
-
var UUID_RE,
|
|
21908
|
+
var UUID_RE, completionCapabilityCache, retryCapabilityCache, taskCreatorCapabilityCache, gitRefCapabilityCache, remoteCommandCapabilityCache, LEGACY_STORAGE_MODE_KEYS, REMOTE_REQUEST_TIMEOUT_MS = 1e4, PRIORITY_RANK, listTagsCapabilityCache;
|
|
21803
21909
|
var init_cloud_router = __esm(() => {
|
|
21804
21910
|
init_types();
|
|
21805
21911
|
init_redaction();
|
|
@@ -21808,21 +21914,17 @@ var init_cloud_router = __esm(() => {
|
|
|
21808
21914
|
init_adoption_validation();
|
|
21809
21915
|
init_page_validation();
|
|
21810
21916
|
UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
21811
|
-
TRANSPORT_TOKENS = {
|
|
21812
|
-
sqlite: "sqlite",
|
|
21813
|
-
http: "http",
|
|
21814
|
-
local: "sqlite",
|
|
21815
|
-
remote: "http",
|
|
21816
|
-
self_hosted: "http",
|
|
21817
|
-
cloud: "http",
|
|
21818
|
-
hybrid: "http"
|
|
21819
|
-
};
|
|
21820
21917
|
completionCapabilityCache = new Map;
|
|
21821
21918
|
retryCapabilityCache = new Map;
|
|
21822
21919
|
taskCreatorCapabilityCache = new Map;
|
|
21823
21920
|
gitRefCapabilityCache = new Map;
|
|
21824
21921
|
remoteCommandCapabilityCache = new Map;
|
|
21825
|
-
|
|
21922
|
+
LEGACY_STORAGE_MODE_KEYS = [
|
|
21923
|
+
"HASNA_TODOS_STORAGE_MODE",
|
|
21924
|
+
"HASNA_TODOS_MODE",
|
|
21925
|
+
"TODOS_STORAGE_MODE",
|
|
21926
|
+
"TODOS_MODE"
|
|
21927
|
+
];
|
|
21826
21928
|
PRIORITY_RANK = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
21827
21929
|
listTagsCapabilityCache = new Map;
|
|
21828
21930
|
});
|
|
@@ -22330,7 +22432,7 @@ function addSourceOnce(projectId, type, name, uri, metadata, db) {
|
|
|
22330
22432
|
function bootstrapProject(options = {}, db) {
|
|
22331
22433
|
const d = db || getDatabase();
|
|
22332
22434
|
const discovery = discoverProjectWorkspace(options.path);
|
|
22333
|
-
const taskListSlug = options.taskListSlug ||
|
|
22435
|
+
const taskListSlug = options.taskListSlug || slugify(options.name || discovery.projectName);
|
|
22334
22436
|
if (options.dryRun) {
|
|
22335
22437
|
return {
|
|
22336
22438
|
dryRun: true,
|
|
@@ -28457,6 +28559,7 @@ function registerTaskProjectTools(server, ctx) {
|
|
|
28457
28559
|
name: exports_external.string().describe("Project name"),
|
|
28458
28560
|
path: exports_external.string().describe("Unique filesystem path for the project"),
|
|
28459
28561
|
description: exports_external.string().optional(),
|
|
28562
|
+
parent_id: exports_external.string().optional().describe("Optional parent project id to create this as a sub-project"),
|
|
28460
28563
|
status: exports_external.enum(["active", "completed", "on_hold", "archived"]).optional(),
|
|
28461
28564
|
short_id: exports_external.string().nullable().optional().describe("Short ID (auto-generated if omitted)"),
|
|
28462
28565
|
metadata: exports_external.record(exports_external.unknown()).optional()
|
|
@@ -35939,7 +36042,7 @@ var package_default;
|
|
|
35939
36042
|
var init_package = __esm(() => {
|
|
35940
36043
|
package_default = {
|
|
35941
36044
|
name: "@hasna/todos",
|
|
35942
|
-
version: "0.15.
|
|
36045
|
+
version: "0.15.35",
|
|
35943
36046
|
description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
|
|
35944
36047
|
type: "module",
|
|
35945
36048
|
main: "dist/index.js",
|
|
@@ -36013,9 +36116,10 @@ var init_package = __esm(() => {
|
|
|
36013
36116
|
"verify:release": "bun run scripts/verify-public-release.ts --mode=review",
|
|
36014
36117
|
"verify:release-review": "bun run scripts/verify-npm-release-agent-review.ts",
|
|
36015
36118
|
"verify:attested-container-candidate": "bun run scripts/attested-container-candidate.ts verify",
|
|
36016
|
-
"test:attested-container-candidate": "bun test scripts/attested-container-candidate.test.ts
|
|
36119
|
+
"test:attested-container-candidate": "bun test scripts/attested-container-candidate.test.ts",
|
|
36017
36120
|
"emit:iapp-deployment-compatibility-vector": "bun run scripts/attested-container-compatibility-vector.ts emit",
|
|
36018
36121
|
"verify:iapp-deployment-compatibility-vector": "bun run scripts/attested-container-compatibility-vector.ts verify",
|
|
36122
|
+
"test:attested-container-compatibility-vector": "bun test scripts/attested-container-compatibility-vector.test.ts",
|
|
36019
36123
|
"issue:release-review": "bun run scripts/issue-npm-release-agent-review.ts",
|
|
36020
36124
|
prepublishOnly: "bun run scripts/verify-public-release.ts --mode=publish",
|
|
36021
36125
|
postinstall: "mkdir -p $HOME/.hasna/todos $HOME/.hasna/todos/training 2>/dev/null || true"
|
|
@@ -37055,6 +37159,7 @@ function createAgentProjectDemoBundle() {
|
|
|
37055
37159
|
task_list_id: ids.list,
|
|
37056
37160
|
task_prefix: "DEMO",
|
|
37057
37161
|
task_counter: 4,
|
|
37162
|
+
parent_id: null,
|
|
37058
37163
|
created_at: createdAt,
|
|
37059
37164
|
updated_at: completedAt
|
|
37060
37165
|
});
|
|
@@ -45621,7 +45726,7 @@ function importSqliteTodosStorageSnapshot(snapshot, db) {
|
|
|
45621
45726
|
}
|
|
45622
45727
|
}
|
|
45623
45728
|
};
|
|
45624
|
-
applyRows("projects", "projects", PROJECT_COLUMNS, snapshot.projects, "updated_at");
|
|
45729
|
+
applyRows("projects", "projects", PROJECT_COLUMNS, orderProjectsParentFirst(snapshot.projects), "updated_at");
|
|
45625
45730
|
applyRows("project_machine_paths", "project_machine_paths", PROJECT_MACHINE_PATH_COLUMNS, snapshot.projectMachinePaths ?? [], "updated_at");
|
|
45626
45731
|
applyRows("agents", "agents", AGENT_COLUMNS, snapshot.agents, "last_seen_at");
|
|
45627
45732
|
applyRows("task_lists", "task_lists", TASK_LIST_COLUMNS, snapshot.taskLists, "updated_at");
|
|
@@ -45852,6 +45957,7 @@ var init_sqlite_snapshot = __esm(() => {
|
|
|
45852
45957
|
"task_list_id",
|
|
45853
45958
|
"task_prefix",
|
|
45854
45959
|
"task_counter",
|
|
45960
|
+
"parent_id",
|
|
45855
45961
|
"created_at",
|
|
45856
45962
|
"updated_at",
|
|
45857
45963
|
"machine_id",
|
|
@@ -47669,8 +47775,12 @@ class PostgresJsonRecordStore {
|
|
|
47669
47775
|
OR ($11::text <> $2
|
|
47670
47776
|
AND NOT EXISTS (SELECT 1 FROM parent_chain WHERE object_id = $2)
|
|
47671
47777
|
AND NOT EXISTS (SELECT 1 FROM parent_chain WHERE cycle))) AS parent_acyclic,
|
|
47672
|
-
(SELECT
|
|
47673
|
-
|
|
47778
|
+
(COALESCE((SELECT payload->>'plan_id' FROM locked_task), '')
|
|
47779
|
+
IS DISTINCT FROM COALESCE($3::jsonb->>'plan_id', '')) AS membership_changed,
|
|
47780
|
+
(NOT (COALESCE((SELECT payload->>'plan_id' FROM locked_task), '')
|
|
47781
|
+
IS DISTINCT FROM COALESCE($3::jsonb->>'plan_id', ''))
|
|
47782
|
+
OR $8::text IS NULL
|
|
47783
|
+
OR EXISTS (SELECT 1 FROM locked_plans WHERE object_id = $8)) AS target_plan_found,
|
|
47674
47784
|
(SELECT payload->>'project_id' FROM locked_plans WHERE object_id = $8) AS target_project_id
|
|
47675
47785
|
), guarded AS (
|
|
47676
47786
|
SELECT
|
|
@@ -47693,7 +47803,6 @@ class PostgresJsonRecordStore {
|
|
|
47693
47803
|
AND guarded.version_matches
|
|
47694
47804
|
AND guarded.parent_found
|
|
47695
47805
|
AND guarded.parent_acyclic
|
|
47696
|
-
AND guarded.all_plans_found
|
|
47697
47806
|
AND guarded.target_plan_found
|
|
47698
47807
|
AND NOT guarded.project_conflict
|
|
47699
47808
|
ON CONFLICT (service, object_type, object_id) DO UPDATE SET
|
|
@@ -47709,7 +47818,7 @@ class PostgresJsonRecordStore {
|
|
|
47709
47818
|
RETURNING payload
|
|
47710
47819
|
)
|
|
47711
47820
|
SELECT guarded.task_found, guarded.version_matches, guarded.parent_found, guarded.parent_acyclic,
|
|
47712
|
-
guarded.
|
|
47821
|
+
guarded.membership_changed, guarded.target_plan_found, guarded.project_conflict,
|
|
47713
47822
|
(SELECT payload FROM stored) AS payload,
|
|
47714
47823
|
(SELECT payload FROM locked_task) AS current_payload
|
|
47715
47824
|
FROM guarded`, [
|
|
@@ -47741,7 +47850,7 @@ class PostgresJsonRecordStore {
|
|
|
47741
47850
|
if (parentGuard && !row?.parent_acyclic && parentGuard.parentId) {
|
|
47742
47851
|
throw new ResourceConflictError("TASK_PARENT_CYCLE", `TASK_PARENT_CYCLE: assigning parent ${parentGuard.parentId} to task ${value.id} would create or retain a parent cycle`);
|
|
47743
47852
|
}
|
|
47744
|
-
if (!row?.
|
|
47853
|
+
if (!row?.target_plan_found) {
|
|
47745
47854
|
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 });
|
|
47746
47855
|
}
|
|
47747
47856
|
if (row.project_conflict) {
|
|
@@ -48950,17 +49059,26 @@ async function getChangedSince(since, filters, store) {
|
|
|
48950
49059
|
async function createProject2(input, store, context) {
|
|
48951
49060
|
const timestamp4 = new Date().toISOString();
|
|
48952
49061
|
const derivedSlug = slugifyRaw(input.name);
|
|
48953
|
-
const taskListId = input.task_list_id === undefined ?
|
|
49062
|
+
const taskListId = input.task_list_id === undefined ? derivedSlug : slugifyRaw(input.task_list_id);
|
|
48954
49063
|
if (!derivedSlug || !taskListId)
|
|
48955
49064
|
throw new Error("Project name and task-list slug must be non-empty");
|
|
49065
|
+
const parentId = input.parent_id ?? null;
|
|
49066
|
+
const id = randomUUID4();
|
|
49067
|
+
if (parentId !== null) {
|
|
49068
|
+
const parent = await store.get("projects", parentId);
|
|
49069
|
+
if (!parent)
|
|
49070
|
+
throw new ProjectNotFoundError(parentId);
|
|
49071
|
+
await assertNotProjectAncestorPostgres(id, parentId, store);
|
|
49072
|
+
}
|
|
48956
49073
|
const project = {
|
|
48957
|
-
id
|
|
49074
|
+
id,
|
|
48958
49075
|
name: input.name,
|
|
48959
49076
|
path: input.path,
|
|
48960
49077
|
description: input.description ?? null,
|
|
48961
49078
|
task_list_id: taskListId,
|
|
48962
49079
|
task_prefix: input.task_prefix ?? await generateProjectPrefix(input.name, store),
|
|
48963
49080
|
task_counter: 0,
|
|
49081
|
+
parent_id: parentId,
|
|
48964
49082
|
created_at: timestamp4,
|
|
48965
49083
|
updated_at: timestamp4,
|
|
48966
49084
|
machine_id: store.machineId(context),
|
|
@@ -48968,12 +49086,38 @@ async function createProject2(input, store, context) {
|
|
|
48968
49086
|
};
|
|
48969
49087
|
return store.upsert("projects", project, context);
|
|
48970
49088
|
}
|
|
49089
|
+
async function assertNotProjectAncestorPostgres(projectId, candidateParentId, store) {
|
|
49090
|
+
const all = await store.list("projects");
|
|
49091
|
+
const byId = new Map(all.map((project) => [project.id, project.parent_id ?? null]));
|
|
49092
|
+
let cursor = candidateParentId;
|
|
49093
|
+
const seen = new Set;
|
|
49094
|
+
while (cursor !== null) {
|
|
49095
|
+
if (cursor === projectId) {
|
|
49096
|
+
throw new ResourceConflictError("PROJECT_PARENT_CYCLE", `Project "${projectId}" cannot be placed under its own descendant`);
|
|
49097
|
+
}
|
|
49098
|
+
if (seen.has(cursor))
|
|
49099
|
+
break;
|
|
49100
|
+
seen.add(cursor);
|
|
49101
|
+
cursor = byId.get(cursor) ?? null;
|
|
49102
|
+
}
|
|
49103
|
+
}
|
|
48971
49104
|
async function updateProject2(id, input, store) {
|
|
48972
49105
|
if ("task_list_id" in input) {
|
|
48973
49106
|
throw new Error("task_list_id cannot be changed by updateProject; use renameProject for an atomic canonical rename");
|
|
48974
49107
|
}
|
|
48975
49108
|
const project = await requireRecord("projects", id, store);
|
|
48976
|
-
|
|
49109
|
+
if (input.parent_id !== undefined && input.parent_id !== null) {
|
|
49110
|
+
const parent = await store.get("projects", input.parent_id);
|
|
49111
|
+
if (!parent)
|
|
49112
|
+
throw new ProjectNotFoundError(input.parent_id);
|
|
49113
|
+
await assertNotProjectAncestorPostgres(id, input.parent_id, store);
|
|
49114
|
+
}
|
|
49115
|
+
const updated = {
|
|
49116
|
+
...project,
|
|
49117
|
+
...definedPatch(input),
|
|
49118
|
+
...input.parent_id !== undefined ? { parent_id: input.parent_id } : {},
|
|
49119
|
+
updated_at: new Date().toISOString()
|
|
49120
|
+
};
|
|
48977
49121
|
return store.upsert("projects", updated);
|
|
48978
49122
|
}
|
|
48979
49123
|
async function createPlan2(input, store, context) {
|
|
@@ -50844,7 +50988,7 @@ class StagedSqliteTodosProjectRegistrationTransaction {
|
|
|
50844
50988
|
}
|
|
50845
50989
|
async createProject(input) {
|
|
50846
50990
|
const derivedSlug = normalizeSlug(input.name);
|
|
50847
|
-
const taskListId = input.task_list_id === undefined ?
|
|
50991
|
+
const taskListId = input.task_list_id === undefined ? derivedSlug : normalizeSlug(input.task_list_id);
|
|
50848
50992
|
if (!derivedSlug || !taskListId) {
|
|
50849
50993
|
throw new Error("Project name and task-list slug must be non-empty");
|
|
50850
50994
|
}
|
|
@@ -50856,6 +51000,7 @@ class StagedSqliteTodosProjectRegistrationTransaction {
|
|
|
50856
51000
|
task_list_id: taskListId,
|
|
50857
51001
|
task_prefix: input.task_prefix ?? this.availableProjectPrefix(input.name),
|
|
50858
51002
|
task_counter: 0,
|
|
51003
|
+
parent_id: input.parent_id ?? null,
|
|
50859
51004
|
created_at: now(),
|
|
50860
51005
|
updated_at: now(),
|
|
50861
51006
|
machine_id: currentStorageMachineId(this.db)
|
|
@@ -50866,14 +51011,15 @@ class StagedSqliteTodosProjectRegistrationTransaction {
|
|
|
50866
51011
|
try {
|
|
50867
51012
|
const result = this.db.run(`INSERT INTO projects (
|
|
50868
51013
|
id, name, path, description, task_list_id, task_prefix,
|
|
50869
|
-
task_counter, created_at, updated_at, machine_id
|
|
50870
|
-
) VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?)`, [
|
|
51014
|
+
task_counter, parent_id, created_at, updated_at, machine_id
|
|
51015
|
+
) VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?)`, [
|
|
50871
51016
|
project.id,
|
|
50872
51017
|
project.name,
|
|
50873
51018
|
project.path,
|
|
50874
51019
|
project.description,
|
|
50875
51020
|
project.task_list_id,
|
|
50876
51021
|
project.task_prefix,
|
|
51022
|
+
project.parent_id,
|
|
50877
51023
|
project.created_at,
|
|
50878
51024
|
project.updated_at,
|
|
50879
51025
|
project.machine_id ?? null
|
|
@@ -51157,7 +51303,10 @@ function taskListSlug(projectSlug) {
|
|
|
51157
51303
|
if (!slug || slug !== projectSlug) {
|
|
51158
51304
|
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "project_slug must be canonical kebab-case");
|
|
51159
51305
|
}
|
|
51160
|
-
return
|
|
51306
|
+
return slug;
|
|
51307
|
+
}
|
|
51308
|
+
function legacyTaskListSlug(projectSlug) {
|
|
51309
|
+
return `todos-${normalizeSlug(projectSlug)}`;
|
|
51161
51310
|
}
|
|
51162
51311
|
function deterministicTaskPrefix(projectSlug) {
|
|
51163
51312
|
const letters = projectSlug.replace(/[^a-z0-9]/gi, "").toUpperCase();
|
|
@@ -51676,6 +51825,12 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
51676
51825
|
created_by_operation: false
|
|
51677
51826
|
};
|
|
51678
51827
|
}
|
|
51828
|
+
if (request.bind_existing === true && conflict2.path === path && conflict2.task_list_id === legacyTaskListSlug(request.project_slug)) {
|
|
51829
|
+
return {
|
|
51830
|
+
record: boundExistingProjectRecord(conflict2),
|
|
51831
|
+
created_by_operation: false
|
|
51832
|
+
};
|
|
51833
|
+
}
|
|
51679
51834
|
return this.terminalFor(transaction, request, normalizedCallDigest(request), "target_already_exists", { targetId: conflict2.id });
|
|
51680
51835
|
}
|
|
51681
51836
|
await this.fault("before_object_write", request);
|
|
@@ -51712,6 +51867,15 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
51712
51867
|
}
|
|
51713
51868
|
return this.terminalFor(transaction, request, normalizedCallDigest(request), "target_already_exists", { targetId: conflict.id });
|
|
51714
51869
|
}
|
|
51870
|
+
if (request.bind_existing === true) {
|
|
51871
|
+
const legacy = await transaction.findTaskListConflict(todosProjectId, legacyTaskListSlug(request.project_slug));
|
|
51872
|
+
if (legacy && legacy.project_id === todosProjectId) {
|
|
51873
|
+
return {
|
|
51874
|
+
record: boundExistingTaskListRecord(legacy),
|
|
51875
|
+
created_by_operation: false
|
|
51876
|
+
};
|
|
51877
|
+
}
|
|
51878
|
+
}
|
|
51715
51879
|
await this.fault("before_object_write", request);
|
|
51716
51880
|
const taskList = await transaction.createTaskList({
|
|
51717
51881
|
name: request.project_name,
|
|
@@ -55641,7 +55805,8 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
55641
55805
|
path: { type: "string", minLength: 1 },
|
|
55642
55806
|
description: { type: "string" },
|
|
55643
55807
|
task_list_id: { type: "string", minLength: 1, pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$" },
|
|
55644
|
-
task_prefix: { type: "string", minLength: 1 }
|
|
55808
|
+
task_prefix: { type: "string", minLength: 1 },
|
|
55809
|
+
parent_id: { type: "string", minLength: 1 }
|
|
55645
55810
|
}
|
|
55646
55811
|
},
|
|
55647
55812
|
UpdateProjectInput: {
|
|
@@ -55651,7 +55816,8 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
55651
55816
|
properties: {
|
|
55652
55817
|
name: { type: "string", minLength: 1 },
|
|
55653
55818
|
path: { type: "string", minLength: 1 },
|
|
55654
|
-
description: { type: "string", nullable: true }
|
|
55819
|
+
description: { type: "string", nullable: true },
|
|
55820
|
+
parent_id: { type: "string", minLength: 1, nullable: true }
|
|
55655
55821
|
}
|
|
55656
55822
|
},
|
|
55657
55823
|
RenameProjectInput: {
|
|
@@ -57950,6 +58116,7 @@ var init_openapi = __esm(() => {
|
|
|
57950
58116
|
task_list_id: { type: "string", nullable: true },
|
|
57951
58117
|
task_prefix: { type: "string", nullable: true },
|
|
57952
58118
|
task_counter: { type: "number" },
|
|
58119
|
+
parent_id: { type: "string", nullable: true },
|
|
57953
58120
|
created_at: { type: "string" },
|
|
57954
58121
|
updated_at: { type: "string" }
|
|
57955
58122
|
}
|
|
@@ -59367,7 +59534,7 @@ function validateProjectPatch(value) {
|
|
|
59367
59534
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
59368
59535
|
return { ok: false, message: "project patch must be an object" };
|
|
59369
59536
|
const body2 = value;
|
|
59370
|
-
const allowed = new Set(["name", "path", "description"]);
|
|
59537
|
+
const allowed = new Set(["name", "path", "description", "parent_id"]);
|
|
59371
59538
|
const unknown = Object.keys(body2).find((key2) => !allowed.has(key2));
|
|
59372
59539
|
if (unknown)
|
|
59373
59540
|
return { ok: false, message: `unknown project field: ${unknown}` };
|
|
@@ -59379,13 +59546,15 @@ function validateProjectPatch(value) {
|
|
|
59379
59546
|
return { ok: false, message: "path must be a non-empty string" };
|
|
59380
59547
|
if (body2["description"] !== undefined && body2["description"] !== null && typeof body2["description"] !== "string")
|
|
59381
59548
|
return { ok: false, message: "description must be a string or null" };
|
|
59549
|
+
if (body2["parent_id"] !== undefined && body2["parent_id"] !== null && (typeof body2["parent_id"] !== "string" || !body2["parent_id"].trim()))
|
|
59550
|
+
return { ok: false, message: "parent_id must be a string or null" };
|
|
59382
59551
|
return { ok: true, patch: body2 };
|
|
59383
59552
|
}
|
|
59384
59553
|
function validateProjectCreate(value) {
|
|
59385
59554
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
59386
59555
|
return { ok: false, message: "project body must be an object" };
|
|
59387
59556
|
const body2 = value;
|
|
59388
|
-
const allowed = new Set(["name", "path", "description", "task_list_id", "task_prefix"]);
|
|
59557
|
+
const allowed = new Set(["name", "path", "description", "task_list_id", "task_prefix", "parent_id"]);
|
|
59389
59558
|
const unknown = Object.keys(body2).find((key2) => !allowed.has(key2));
|
|
59390
59559
|
if (unknown)
|
|
59391
59560
|
return { ok: false, message: `unknown project field: ${unknown}` };
|
|
@@ -59403,6 +59572,9 @@ function validateProjectCreate(value) {
|
|
|
59403
59572
|
if (body2["task_prefix"] !== undefined && (typeof body2["task_prefix"] !== "string" || !body2["task_prefix"].trim())) {
|
|
59404
59573
|
return { ok: false, message: "task_prefix must be a non-empty string" };
|
|
59405
59574
|
}
|
|
59575
|
+
if (body2["parent_id"] !== undefined && (typeof body2["parent_id"] !== "string" || !body2["parent_id"].trim())) {
|
|
59576
|
+
return { ok: false, message: "parent_id must be a non-empty string" };
|
|
59577
|
+
}
|
|
59406
59578
|
return { ok: true, input: body2 };
|
|
59407
59579
|
}
|
|
59408
59580
|
function validatePlanCreate(value) {
|