@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/index.js
CHANGED
|
@@ -1884,6 +1884,10 @@ var init_migrations = __esm(() => {
|
|
|
1884
1884
|
);
|
|
1885
1885
|
INSERT OR IGNORE INTO _migrations (id) VALUES (70);
|
|
1886
1886
|
COMMIT;
|
|
1887
|
+
`,
|
|
1888
|
+
`BEGIN;
|
|
1889
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (71);
|
|
1890
|
+
COMMIT;
|
|
1887
1891
|
`
|
|
1888
1892
|
];
|
|
1889
1893
|
});
|
|
@@ -2557,6 +2561,8 @@ function ensureSchema(db) {
|
|
|
2557
2561
|
ensureColumn("projects", "task_list_id", "TEXT");
|
|
2558
2562
|
ensureColumn("projects", "task_prefix", "TEXT");
|
|
2559
2563
|
ensureColumn("projects", "task_counter", "INTEGER NOT NULL DEFAULT 0");
|
|
2564
|
+
ensureColumn("projects", "parent_id", "TEXT REFERENCES projects(id) ON DELETE SET NULL");
|
|
2565
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_projects_parent_id ON projects(parent_id)");
|
|
2560
2566
|
ensureColumn("tasks", "plan_id", "TEXT REFERENCES plans(id) ON DELETE SET NULL");
|
|
2561
2567
|
ensureColumn("tasks", "task_list_id", "TEXT REFERENCES task_lists(id) ON DELETE SET NULL");
|
|
2562
2568
|
ensureColumn("tasks", "short_id", "TEXT");
|
|
@@ -4177,6 +4183,7 @@ __export(exports_config, {
|
|
|
4177
4183
|
TODOS_STORAGE_FALLBACK_ENV: () => TODOS_STORAGE_FALLBACK_ENV,
|
|
4178
4184
|
TODOS_STORAGE_ENV: () => TODOS_STORAGE_ENV,
|
|
4179
4185
|
STORAGE_TABLES: () => STORAGE_TABLES,
|
|
4186
|
+
REMOVED_STORAGE_MODE_ENV_KEYS: () => REMOVED_STORAGE_MODE_ENV_KEYS,
|
|
4180
4187
|
CANONICAL_TODOS_RDS_RUNTIME_PATH_ENV: () => CANONICAL_TODOS_RDS_RUNTIME_PATH_ENV,
|
|
4181
4188
|
CANONICAL_TODOS_RDS_DATABASE: () => CANONICAL_TODOS_RDS_DATABASE,
|
|
4182
4189
|
CANONICAL_TODOS_RDS_CLUSTER_ENV: () => CANONICAL_TODOS_RDS_CLUSTER_ENV
|
|
@@ -4238,25 +4245,36 @@ function assertTodosRemoteStorageConfig(config) {
|
|
|
4238
4245
|
if (!isTodosPostgresBackend(config))
|
|
4239
4246
|
return;
|
|
4240
4247
|
if (!config.database?.url) {
|
|
4241
|
-
throw new Error(`${TODOS_STORAGE_ENV.databaseUrl} is required when
|
|
4248
|
+
throw new Error(`${TODOS_STORAGE_ENV.databaseUrl} is required when the postgresql backend is selected`);
|
|
4242
4249
|
}
|
|
4243
4250
|
}
|
|
4244
4251
|
function parseStorageBackend(value) {
|
|
4245
4252
|
const normalized = clean(value)?.toLowerCase();
|
|
4246
4253
|
if (!normalized)
|
|
4247
4254
|
return "sqlite";
|
|
4248
|
-
if (normalized === "sqlite"
|
|
4249
|
-
return
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4255
|
+
if (normalized === "sqlite")
|
|
4256
|
+
return "sqlite";
|
|
4257
|
+
if (normalized === "postgres" || normalized === "postgresql")
|
|
4258
|
+
return "postgres";
|
|
4259
|
+
if (["local", "remote", "cloud", "hybrid", "self_hosted"].includes(normalized)) {
|
|
4260
|
+
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.`);
|
|
4261
|
+
}
|
|
4262
|
+
throw new Error(`Storage backend must be sqlite or postgres`);
|
|
4254
4263
|
}
|
|
4255
4264
|
function parseStorageMode(value) {
|
|
4256
4265
|
return parseStorageBackend(value);
|
|
4257
4266
|
}
|
|
4258
4267
|
function getTodosStorageBackend(env = process.env) {
|
|
4259
|
-
|
|
4268
|
+
for (const key of REMOVED_STORAGE_MODE_ENV_KEYS) {
|
|
4269
|
+
if (Object.hasOwn(env, key) && env[key] !== undefined) {
|
|
4270
|
+
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.`);
|
|
4271
|
+
}
|
|
4272
|
+
}
|
|
4273
|
+
if (!getTodosStorageDatabaseUrl(env))
|
|
4274
|
+
return "sqlite";
|
|
4275
|
+
if (isTodosShadowEnabled(env))
|
|
4276
|
+
return "sqlite";
|
|
4277
|
+
return "postgres";
|
|
4260
4278
|
}
|
|
4261
4279
|
function getTodosStorageMode(env = process.env) {
|
|
4262
4280
|
return getTodosStorageBackend(env);
|
|
@@ -4330,7 +4348,7 @@ function parsePositiveInteger(value, fallback) {
|
|
|
4330
4348
|
}
|
|
4331
4349
|
return parsed;
|
|
4332
4350
|
}
|
|
4333
|
-
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"
|
|
4351
|
+
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";
|
|
4334
4352
|
var init_config = __esm(() => {
|
|
4335
4353
|
TODOS_STORAGE_TABLES = [
|
|
4336
4354
|
"todos_sync_records",
|
|
@@ -4338,7 +4356,6 @@ var init_config = __esm(() => {
|
|
|
4338
4356
|
];
|
|
4339
4357
|
STORAGE_TABLES = TODOS_STORAGE_TABLES;
|
|
4340
4358
|
TODOS_STORAGE_ENV = {
|
|
4341
|
-
mode: "HASNA_TODOS_STORAGE_MODE",
|
|
4342
4359
|
shadow: "HASNA_TODOS_SHADOW",
|
|
4343
4360
|
databaseUrl: "HASNA_TODOS_DATABASE_URL",
|
|
4344
4361
|
databaseSsl: "HASNA_TODOS_DATABASE_SSL",
|
|
@@ -4355,7 +4372,6 @@ var init_config = __esm(() => {
|
|
|
4355
4372
|
syncDryRun: "HASNA_TODOS_SYNC_DRY_RUN"
|
|
4356
4373
|
};
|
|
4357
4374
|
TODOS_STORAGE_FALLBACK_ENV = {
|
|
4358
|
-
mode: "TODOS_STORAGE_MODE",
|
|
4359
4375
|
shadow: "TODOS_SHADOW",
|
|
4360
4376
|
databaseUrl: "TODOS_DATABASE_URL",
|
|
4361
4377
|
databaseSsl: "TODOS_DATABASE_SSL",
|
|
@@ -4371,14 +4387,12 @@ var init_config = __esm(() => {
|
|
|
4371
4387
|
syncBatchSize: "TODOS_SYNC_BATCH_SIZE",
|
|
4372
4388
|
syncDryRun: "TODOS_SYNC_DRY_RUN"
|
|
4373
4389
|
};
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
cloud: "postgres"
|
|
4381
|
-
};
|
|
4390
|
+
REMOVED_STORAGE_MODE_ENV_KEYS = [
|
|
4391
|
+
"HASNA_TODOS_STORAGE_MODE",
|
|
4392
|
+
"HASNA_TODOS_MODE",
|
|
4393
|
+
"TODOS_STORAGE_MODE",
|
|
4394
|
+
"TODOS_MODE"
|
|
4395
|
+
];
|
|
4382
4396
|
});
|
|
4383
4397
|
|
|
4384
4398
|
// src/storage/shadow-outbox-schema.ts
|
|
@@ -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
|
});
|
|
@@ -12833,7 +12918,7 @@ var init_dispatches = __esm(() => {
|
|
|
12833
12918
|
// package.json
|
|
12834
12919
|
var package_default = {
|
|
12835
12920
|
name: "@hasna/todos",
|
|
12836
|
-
version: "0.15.
|
|
12921
|
+
version: "0.15.35",
|
|
12837
12922
|
description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
|
|
12838
12923
|
type: "module",
|
|
12839
12924
|
main: "dist/index.js",
|
|
@@ -12907,9 +12992,10 @@ var package_default = {
|
|
|
12907
12992
|
"verify:release": "bun run scripts/verify-public-release.ts --mode=review",
|
|
12908
12993
|
"verify:release-review": "bun run scripts/verify-npm-release-agent-review.ts",
|
|
12909
12994
|
"verify:attested-container-candidate": "bun run scripts/attested-container-candidate.ts verify",
|
|
12910
|
-
"test:attested-container-candidate": "bun test scripts/attested-container-candidate.test.ts
|
|
12995
|
+
"test:attested-container-candidate": "bun test scripts/attested-container-candidate.test.ts",
|
|
12911
12996
|
"emit:iapp-deployment-compatibility-vector": "bun run scripts/attested-container-compatibility-vector.ts emit",
|
|
12912
12997
|
"verify:iapp-deployment-compatibility-vector": "bun run scripts/attested-container-compatibility-vector.ts verify",
|
|
12998
|
+
"test:attested-container-compatibility-vector": "bun test scripts/attested-container-compatibility-vector.test.ts",
|
|
12913
12999
|
"issue:release-review": "bun run scripts/issue-npm-release-agent-review.ts",
|
|
12914
13000
|
prepublishOnly: "bun run scripts/verify-public-release.ts --mode=publish",
|
|
12915
13001
|
postinstall: "mkdir -p $HOME/.hasna/todos $HOME/.hasna/todos/training 2>/dev/null || true"
|
|
@@ -13598,6 +13684,7 @@ var TODOS_JSON_CONTRACTS = [
|
|
|
13598
13684
|
task_list_id: nullableIdField,
|
|
13599
13685
|
task_prefix: field(["string", "null"], "Optional task prefix.", true),
|
|
13600
13686
|
task_counter: field("integer", "Monotonic project task counter."),
|
|
13687
|
+
parent_id: field(["string", "null"], "Optional parent project id; null means top-level.", true),
|
|
13601
13688
|
created_at: isoDateField,
|
|
13602
13689
|
updated_at: isoDateField
|
|
13603
13690
|
},
|
|
@@ -16883,6 +16970,7 @@ function createAgentProjectDemoBundle() {
|
|
|
16883
16970
|
task_list_id: ids.list,
|
|
16884
16971
|
task_prefix: "DEMO",
|
|
16885
16972
|
task_counter: 4,
|
|
16973
|
+
parent_id: null,
|
|
16886
16974
|
created_at: createdAt,
|
|
16887
16975
|
updated_at: completedAt
|
|
16888
16976
|
});
|
|
@@ -26523,6 +26611,7 @@ var PROJECT_COLUMNS = [
|
|
|
26523
26611
|
"task_list_id",
|
|
26524
26612
|
"task_prefix",
|
|
26525
26613
|
"task_counter",
|
|
26614
|
+
"parent_id",
|
|
26526
26615
|
"created_at",
|
|
26527
26616
|
"updated_at",
|
|
26528
26617
|
"machine_id",
|
|
@@ -26742,7 +26831,7 @@ function importSqliteTodosStorageSnapshot(snapshot, db) {
|
|
|
26742
26831
|
}
|
|
26743
26832
|
}
|
|
26744
26833
|
};
|
|
26745
|
-
applyRows("projects", "projects", PROJECT_COLUMNS, snapshot.projects, "updated_at");
|
|
26834
|
+
applyRows("projects", "projects", PROJECT_COLUMNS, orderProjectsParentFirst(snapshot.projects), "updated_at");
|
|
26746
26835
|
applyRows("project_machine_paths", "project_machine_paths", PROJECT_MACHINE_PATH_COLUMNS, snapshot.projectMachinePaths ?? [], "updated_at");
|
|
26747
26836
|
applyRows("agents", "agents", AGENT_COLUMNS, snapshot.agents, "last_seen_at");
|
|
26748
26837
|
applyRows("task_lists", "task_lists", TASK_LIST_COLUMNS, snapshot.taskLists, "updated_at");
|
|
@@ -28197,8 +28286,12 @@ class PostgresJsonRecordStore {
|
|
|
28197
28286
|
OR ($11::text <> $2
|
|
28198
28287
|
AND NOT EXISTS (SELECT 1 FROM parent_chain WHERE object_id = $2)
|
|
28199
28288
|
AND NOT EXISTS (SELECT 1 FROM parent_chain WHERE cycle))) AS parent_acyclic,
|
|
28200
|
-
(SELECT
|
|
28201
|
-
|
|
28289
|
+
(COALESCE((SELECT payload->>'plan_id' FROM locked_task), '')
|
|
28290
|
+
IS DISTINCT FROM COALESCE($3::jsonb->>'plan_id', '')) AS membership_changed,
|
|
28291
|
+
(NOT (COALESCE((SELECT payload->>'plan_id' FROM locked_task), '')
|
|
28292
|
+
IS DISTINCT FROM COALESCE($3::jsonb->>'plan_id', ''))
|
|
28293
|
+
OR $8::text IS NULL
|
|
28294
|
+
OR EXISTS (SELECT 1 FROM locked_plans WHERE object_id = $8)) AS target_plan_found,
|
|
28202
28295
|
(SELECT payload->>'project_id' FROM locked_plans WHERE object_id = $8) AS target_project_id
|
|
28203
28296
|
), guarded AS (
|
|
28204
28297
|
SELECT
|
|
@@ -28221,7 +28314,6 @@ class PostgresJsonRecordStore {
|
|
|
28221
28314
|
AND guarded.version_matches
|
|
28222
28315
|
AND guarded.parent_found
|
|
28223
28316
|
AND guarded.parent_acyclic
|
|
28224
|
-
AND guarded.all_plans_found
|
|
28225
28317
|
AND guarded.target_plan_found
|
|
28226
28318
|
AND NOT guarded.project_conflict
|
|
28227
28319
|
ON CONFLICT (service, object_type, object_id) DO UPDATE SET
|
|
@@ -28237,7 +28329,7 @@ class PostgresJsonRecordStore {
|
|
|
28237
28329
|
RETURNING payload
|
|
28238
28330
|
)
|
|
28239
28331
|
SELECT guarded.task_found, guarded.version_matches, guarded.parent_found, guarded.parent_acyclic,
|
|
28240
|
-
guarded.
|
|
28332
|
+
guarded.membership_changed, guarded.target_plan_found, guarded.project_conflict,
|
|
28241
28333
|
(SELECT payload FROM stored) AS payload,
|
|
28242
28334
|
(SELECT payload FROM locked_task) AS current_payload
|
|
28243
28335
|
FROM guarded`, [
|
|
@@ -28269,7 +28361,7 @@ class PostgresJsonRecordStore {
|
|
|
28269
28361
|
if (parentGuard && !row?.parent_acyclic && parentGuard.parentId) {
|
|
28270
28362
|
throw new ResourceConflictError("TASK_PARENT_CYCLE", `TASK_PARENT_CYCLE: assigning parent ${parentGuard.parentId} to task ${value.id} would create or retain a parent cycle`);
|
|
28271
28363
|
}
|
|
28272
|
-
if (!row?.
|
|
28364
|
+
if (!row?.target_plan_found) {
|
|
28273
28365
|
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 });
|
|
28274
28366
|
}
|
|
28275
28367
|
if (row.project_conflict) {
|
|
@@ -29481,17 +29573,26 @@ async function getChangedSince(since, filters, store) {
|
|
|
29481
29573
|
async function createProject2(input, store, context) {
|
|
29482
29574
|
const timestamp3 = new Date().toISOString();
|
|
29483
29575
|
const derivedSlug = slugifyRaw(input.name);
|
|
29484
|
-
const taskListId = input.task_list_id === undefined ?
|
|
29576
|
+
const taskListId = input.task_list_id === undefined ? derivedSlug : slugifyRaw(input.task_list_id);
|
|
29485
29577
|
if (!derivedSlug || !taskListId)
|
|
29486
29578
|
throw new Error("Project name and task-list slug must be non-empty");
|
|
29579
|
+
const parentId = input.parent_id ?? null;
|
|
29580
|
+
const id = randomUUID3();
|
|
29581
|
+
if (parentId !== null) {
|
|
29582
|
+
const parent = await store.get("projects", parentId);
|
|
29583
|
+
if (!parent)
|
|
29584
|
+
throw new ProjectNotFoundError(parentId);
|
|
29585
|
+
await assertNotProjectAncestorPostgres(id, parentId, store);
|
|
29586
|
+
}
|
|
29487
29587
|
const project = {
|
|
29488
|
-
id
|
|
29588
|
+
id,
|
|
29489
29589
|
name: input.name,
|
|
29490
29590
|
path: input.path,
|
|
29491
29591
|
description: input.description ?? null,
|
|
29492
29592
|
task_list_id: taskListId,
|
|
29493
29593
|
task_prefix: input.task_prefix ?? await generateProjectPrefix(input.name, store),
|
|
29494
29594
|
task_counter: 0,
|
|
29595
|
+
parent_id: parentId,
|
|
29495
29596
|
created_at: timestamp3,
|
|
29496
29597
|
updated_at: timestamp3,
|
|
29497
29598
|
machine_id: store.machineId(context),
|
|
@@ -29499,12 +29600,38 @@ async function createProject2(input, store, context) {
|
|
|
29499
29600
|
};
|
|
29500
29601
|
return store.upsert("projects", project, context);
|
|
29501
29602
|
}
|
|
29603
|
+
async function assertNotProjectAncestorPostgres(projectId, candidateParentId, store) {
|
|
29604
|
+
const all = await store.list("projects");
|
|
29605
|
+
const byId = new Map(all.map((project) => [project.id, project.parent_id ?? null]));
|
|
29606
|
+
let cursor = candidateParentId;
|
|
29607
|
+
const seen = new Set;
|
|
29608
|
+
while (cursor !== null) {
|
|
29609
|
+
if (cursor === projectId) {
|
|
29610
|
+
throw new ResourceConflictError("PROJECT_PARENT_CYCLE", `Project "${projectId}" cannot be placed under its own descendant`);
|
|
29611
|
+
}
|
|
29612
|
+
if (seen.has(cursor))
|
|
29613
|
+
break;
|
|
29614
|
+
seen.add(cursor);
|
|
29615
|
+
cursor = byId.get(cursor) ?? null;
|
|
29616
|
+
}
|
|
29617
|
+
}
|
|
29502
29618
|
async function updateProject2(id, input, store) {
|
|
29503
29619
|
if ("task_list_id" in input) {
|
|
29504
29620
|
throw new Error("task_list_id cannot be changed by updateProject; use renameProject for an atomic canonical rename");
|
|
29505
29621
|
}
|
|
29506
29622
|
const project = await requireRecord("projects", id, store);
|
|
29507
|
-
|
|
29623
|
+
if (input.parent_id !== undefined && input.parent_id !== null) {
|
|
29624
|
+
const parent = await store.get("projects", input.parent_id);
|
|
29625
|
+
if (!parent)
|
|
29626
|
+
throw new ProjectNotFoundError(input.parent_id);
|
|
29627
|
+
await assertNotProjectAncestorPostgres(id, input.parent_id, store);
|
|
29628
|
+
}
|
|
29629
|
+
const updated = {
|
|
29630
|
+
...project,
|
|
29631
|
+
...definedPatch(input),
|
|
29632
|
+
...input.parent_id !== undefined ? { parent_id: input.parent_id } : {},
|
|
29633
|
+
updated_at: new Date().toISOString()
|
|
29634
|
+
};
|
|
29508
29635
|
return store.upsert("projects", updated);
|
|
29509
29636
|
}
|
|
29510
29637
|
async function createPlan2(input, store, context) {
|
|
@@ -35334,7 +35461,7 @@ class StagedSqliteTodosProjectRegistrationTransaction {
|
|
|
35334
35461
|
}
|
|
35335
35462
|
async createProject(input) {
|
|
35336
35463
|
const derivedSlug = normalizeSlug(input.name);
|
|
35337
|
-
const taskListId = input.task_list_id === undefined ?
|
|
35464
|
+
const taskListId = input.task_list_id === undefined ? derivedSlug : normalizeSlug(input.task_list_id);
|
|
35338
35465
|
if (!derivedSlug || !taskListId) {
|
|
35339
35466
|
throw new Error("Project name and task-list slug must be non-empty");
|
|
35340
35467
|
}
|
|
@@ -35346,6 +35473,7 @@ class StagedSqliteTodosProjectRegistrationTransaction {
|
|
|
35346
35473
|
task_list_id: taskListId,
|
|
35347
35474
|
task_prefix: input.task_prefix ?? this.availableProjectPrefix(input.name),
|
|
35348
35475
|
task_counter: 0,
|
|
35476
|
+
parent_id: input.parent_id ?? null,
|
|
35349
35477
|
created_at: now(),
|
|
35350
35478
|
updated_at: now(),
|
|
35351
35479
|
machine_id: currentStorageMachineId(this.db)
|
|
@@ -35356,14 +35484,15 @@ class StagedSqliteTodosProjectRegistrationTransaction {
|
|
|
35356
35484
|
try {
|
|
35357
35485
|
const result = this.db.run(`INSERT INTO projects (
|
|
35358
35486
|
id, name, path, description, task_list_id, task_prefix,
|
|
35359
|
-
task_counter, created_at, updated_at, machine_id
|
|
35360
|
-
) VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?)`, [
|
|
35487
|
+
task_counter, parent_id, created_at, updated_at, machine_id
|
|
35488
|
+
) VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?)`, [
|
|
35361
35489
|
project.id,
|
|
35362
35490
|
project.name,
|
|
35363
35491
|
project.path,
|
|
35364
35492
|
project.description,
|
|
35365
35493
|
project.task_list_id,
|
|
35366
35494
|
project.task_prefix,
|
|
35495
|
+
project.parent_id,
|
|
35367
35496
|
project.created_at,
|
|
35368
35497
|
project.updated_at,
|
|
35369
35498
|
project.machine_id ?? null
|
|
@@ -35759,7 +35888,10 @@ function taskListSlug(projectSlug) {
|
|
|
35759
35888
|
if (!slug || slug !== projectSlug) {
|
|
35760
35889
|
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "project_slug must be canonical kebab-case");
|
|
35761
35890
|
}
|
|
35762
|
-
return
|
|
35891
|
+
return slug;
|
|
35892
|
+
}
|
|
35893
|
+
function legacyTaskListSlug(projectSlug) {
|
|
35894
|
+
return `todos-${normalizeSlug(projectSlug)}`;
|
|
35763
35895
|
}
|
|
35764
35896
|
function deterministicTaskPrefix(projectSlug) {
|
|
35765
35897
|
const letters = projectSlug.replace(/[^a-z0-9]/gi, "").toUpperCase();
|
|
@@ -36278,6 +36410,12 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
36278
36410
|
created_by_operation: false
|
|
36279
36411
|
};
|
|
36280
36412
|
}
|
|
36413
|
+
if (request.bind_existing === true && conflict2.path === path && conflict2.task_list_id === legacyTaskListSlug(request.project_slug)) {
|
|
36414
|
+
return {
|
|
36415
|
+
record: boundExistingProjectRecord(conflict2),
|
|
36416
|
+
created_by_operation: false
|
|
36417
|
+
};
|
|
36418
|
+
}
|
|
36281
36419
|
return this.terminalFor(transaction, request, normalizedCallDigest(request), "target_already_exists", { targetId: conflict2.id });
|
|
36282
36420
|
}
|
|
36283
36421
|
await this.fault("before_object_write", request);
|
|
@@ -36314,6 +36452,15 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
36314
36452
|
}
|
|
36315
36453
|
return this.terminalFor(transaction, request, normalizedCallDigest(request), "target_already_exists", { targetId: conflict.id });
|
|
36316
36454
|
}
|
|
36455
|
+
if (request.bind_existing === true) {
|
|
36456
|
+
const legacy = await transaction.findTaskListConflict(todosProjectId, legacyTaskListSlug(request.project_slug));
|
|
36457
|
+
if (legacy && legacy.project_id === todosProjectId) {
|
|
36458
|
+
return {
|
|
36459
|
+
record: boundExistingTaskListRecord(legacy),
|
|
36460
|
+
created_by_operation: false
|
|
36461
|
+
};
|
|
36462
|
+
}
|
|
36463
|
+
}
|
|
36317
36464
|
await this.fault("before_object_write", request);
|
|
36318
36465
|
const taskList = await transaction.createTaskList({
|
|
36319
36466
|
name: request.project_name,
|
|
@@ -43481,54 +43628,49 @@ function createTodosTaskManifestHttpClient(options) {
|
|
|
43481
43628
|
import { createHash as createHash16 } from "crypto";
|
|
43482
43629
|
// src/cli/cloud-router.ts
|
|
43483
43630
|
import { resolveStorageClient } from "@hasna/contracts/client/storage";
|
|
43484
|
-
import { normalizeStorageMode } from "@hasna/contracts/mode";
|
|
43485
43631
|
init_types();
|
|
43486
43632
|
init_redaction();
|
|
43487
|
-
var TRANSPORT_TOKENS = {
|
|
43488
|
-
sqlite: "sqlite",
|
|
43489
|
-
http: "http",
|
|
43490
|
-
local: "sqlite",
|
|
43491
|
-
remote: "http",
|
|
43492
|
-
self_hosted: "http",
|
|
43493
|
-
cloud: "http",
|
|
43494
|
-
hybrid: "http"
|
|
43495
|
-
};
|
|
43496
43633
|
var completionCapabilityCache = new Map;
|
|
43497
43634
|
var retryCapabilityCache = new Map;
|
|
43498
43635
|
var taskCreatorCapabilityCache = new Map;
|
|
43499
43636
|
var gitRefCapabilityCache = new Map;
|
|
43500
43637
|
var remoteCommandCapabilityCache = new Map;
|
|
43501
|
-
|
|
43502
|
-
|
|
43503
|
-
|
|
43638
|
+
var LEGACY_STORAGE_MODE_KEYS = [
|
|
43639
|
+
"HASNA_TODOS_STORAGE_MODE",
|
|
43640
|
+
"HASNA_TODOS_MODE",
|
|
43641
|
+
"TODOS_STORAGE_MODE",
|
|
43642
|
+
"TODOS_MODE"
|
|
43643
|
+
];
|
|
43644
|
+
function assertNoLegacyStorageMode(env = process.env) {
|
|
43645
|
+
for (const key2 of LEGACY_STORAGE_MODE_KEYS) {
|
|
43646
|
+
if (Object.hasOwn(env, key2) && env[key2] !== undefined) {
|
|
43647
|
+
throw new Error(`REMOTE_STORAGE_MODE_REMOVED: ${key2} 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.`);
|
|
43648
|
+
}
|
|
43649
|
+
}
|
|
43504
43650
|
}
|
|
43505
43651
|
function resolveTodosCliStorageMode(env = process.env) {
|
|
43506
|
-
|
|
43507
|
-
|
|
43508
|
-
|
|
43509
|
-
|
|
43652
|
+
assertNoLegacyStorageMode(env);
|
|
43653
|
+
const urlValue = env.HASNA_TODOS_API_URL?.trim();
|
|
43654
|
+
const keyValue = env.HASNA_TODOS_API_KEY?.trim();
|
|
43655
|
+
if (urlValue && keyValue) {
|
|
43656
|
+
return {
|
|
43657
|
+
mode: "http",
|
|
43658
|
+
transport: "http",
|
|
43659
|
+
selected: true,
|
|
43660
|
+
source: "HASNA_TODOS_API_URL+HASNA_TODOS_API_KEY"
|
|
43661
|
+
};
|
|
43510
43662
|
}
|
|
43511
|
-
|
|
43512
|
-
|
|
43513
|
-
for (const [source9, value] of [
|
|
43514
|
-
["HASNA_TODOS_STORAGE_MODE", canonical],
|
|
43515
|
-
["TODOS_STORAGE_MODE", fallback]
|
|
43516
|
-
]) {
|
|
43517
|
-
if (value && !(value in TRANSPORT_TOKENS)) {
|
|
43518
|
-
throw new Error(`REMOTE_STORAGE_MODE_INVALID: ${source9}=${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");
|
|
43519
|
-
}
|
|
43663
|
+
if (urlValue) {
|
|
43664
|
+
throw new Error("REMOTE_API_KEY_MISSING: remote Todos storage requires HASNA_TODOS_API_KEY; local SQLite fallback is disabled");
|
|
43520
43665
|
}
|
|
43521
|
-
|
|
43522
|
-
|
|
43523
|
-
if (canonicalTransport && fallbackTransport && canonicalTransport !== fallbackTransport) {
|
|
43524
|
-
throw new Error(`REMOTE_STORAGE_MODE_CONFLICT: HASNA_TODOS_STORAGE_MODE=${canonical} conflicts with ` + `TODOS_STORAGE_MODE=${fallback}; local SQLite fallback is disabled`);
|
|
43666
|
+
if (keyValue) {
|
|
43667
|
+
throw new Error("REMOTE_API_URL_MISSING: remote Todos storage requires HASNA_TODOS_API_URL; local SQLite fallback is disabled");
|
|
43525
43668
|
}
|
|
43526
|
-
const transport = canonicalTransport ?? fallbackTransport ?? "sqlite";
|
|
43527
43669
|
return {
|
|
43528
|
-
mode:
|
|
43529
|
-
transport,
|
|
43530
|
-
selected:
|
|
43531
|
-
source:
|
|
43670
|
+
mode: "sqlite",
|
|
43671
|
+
transport: "sqlite",
|
|
43672
|
+
selected: false,
|
|
43673
|
+
source: "default"
|
|
43532
43674
|
};
|
|
43533
43675
|
}
|
|
43534
43676
|
function requestedTransport(env) {
|
|
@@ -43572,7 +43714,7 @@ function getTodosRemoteAuthorityConfigStatus(env = process.env) {
|
|
|
43572
43714
|
return {
|
|
43573
43715
|
selected: true,
|
|
43574
43716
|
ok: false,
|
|
43575
|
-
mode:
|
|
43717
|
+
mode: "invalid",
|
|
43576
43718
|
api_url_configured: Boolean(env.HASNA_TODOS_API_URL?.trim()),
|
|
43577
43719
|
api_key_configured: Boolean(env.HASNA_TODOS_API_KEY?.trim()),
|
|
43578
43720
|
v1_base_url: null,
|
|
@@ -43618,29 +43760,12 @@ function getTodosRemoteAuthorityConfigStatus(env = process.env) {
|
|
|
43618
43760
|
local_fallback: false
|
|
43619
43761
|
};
|
|
43620
43762
|
}
|
|
43621
|
-
var SERVER_MODE_CANDIDATES = ["postgres", "cloud", "self_hosted"];
|
|
43622
|
-
var cachedServerMode = null;
|
|
43623
|
-
function serverStorageMode(normalize2 = normalizeStorageMode) {
|
|
43624
|
-
const useCache = normalize2 === normalizeStorageMode;
|
|
43625
|
-
if (useCache && cachedServerMode !== null)
|
|
43626
|
-
return cachedServerMode;
|
|
43627
|
-
for (const candidate of SERVER_MODE_CANDIDATES) {
|
|
43628
|
-
try {
|
|
43629
|
-
normalize2(candidate);
|
|
43630
|
-
if (useCache)
|
|
43631
|
-
cachedServerMode = candidate;
|
|
43632
|
-
return candidate;
|
|
43633
|
-
} catch {}
|
|
43634
|
-
}
|
|
43635
|
-
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.`);
|
|
43636
|
-
}
|
|
43637
43763
|
function requireTodosRemoteAuthorityEnv(env) {
|
|
43638
43764
|
const status2 = getTodosRemoteAuthorityConfigStatus(env);
|
|
43639
43765
|
if (!status2.ok)
|
|
43640
43766
|
throw new Error(status2.issues[0]);
|
|
43641
43767
|
return {
|
|
43642
43768
|
...env,
|
|
43643
|
-
HASNA_TODOS_STORAGE_MODE: serverStorageMode(),
|
|
43644
43769
|
HASNA_TODOS_API_URL: status2.v1_base_url.replace(/\/v1$/, ""),
|
|
43645
43770
|
HASNA_TODOS_API_KEY: env.HASNA_TODOS_API_KEY.trim()
|
|
43646
43771
|
};
|
|
@@ -43678,7 +43803,32 @@ function classifyRemoteRequestError(baseUrl, route, error) {
|
|
|
43678
43803
|
}
|
|
43679
43804
|
throw error;
|
|
43680
43805
|
}
|
|
43681
|
-
|
|
43806
|
+
var REMOTE_REQUEST_TIMEOUT_MS = 1e4;
|
|
43807
|
+
function withBoundedRemoteRequest(options, requestTimeoutMs, run) {
|
|
43808
|
+
const controller = new AbortController;
|
|
43809
|
+
const onBaseAbort = () => controller.abort();
|
|
43810
|
+
if (options?.signal) {
|
|
43811
|
+
if (options.signal.aborted)
|
|
43812
|
+
controller.abort();
|
|
43813
|
+
else
|
|
43814
|
+
options.signal.addEventListener("abort", onBaseAbort, { once: true });
|
|
43815
|
+
}
|
|
43816
|
+
let timer;
|
|
43817
|
+
const deadline = new Promise((_, reject) => {
|
|
43818
|
+
timer = setTimeout(() => {
|
|
43819
|
+
controller.abort();
|
|
43820
|
+
reject(new DOMException(`Todos remote request exceeded the ${requestTimeoutMs}ms bounded request timeout`, "AbortError"));
|
|
43821
|
+
}, requestTimeoutMs);
|
|
43822
|
+
});
|
|
43823
|
+
const attempt = run({ ...options, signal: controller.signal });
|
|
43824
|
+
attempt.catch(() => {});
|
|
43825
|
+
return Promise.race([attempt, deadline]).finally(() => {
|
|
43826
|
+
if (timer)
|
|
43827
|
+
clearTimeout(timer);
|
|
43828
|
+
options?.signal?.removeEventListener("abort", onBaseAbort);
|
|
43829
|
+
});
|
|
43830
|
+
}
|
|
43831
|
+
function protectRemoteClient(client, requestTimeoutMs) {
|
|
43682
43832
|
const baseUrl = remoteAuthorityBase(client);
|
|
43683
43833
|
const protect = async (route, request) => {
|
|
43684
43834
|
try {
|
|
@@ -43687,25 +43837,26 @@ function protectRemoteClient(client) {
|
|
|
43687
43837
|
return classifyRemoteRequestError(baseUrl, route, error);
|
|
43688
43838
|
}
|
|
43689
43839
|
};
|
|
43840
|
+
const bounded = (options, run) => withBoundedRemoteRequest(options, requestTimeoutMs, run);
|
|
43690
43841
|
const transport = client.transport;
|
|
43691
43842
|
const protectedTransport = {
|
|
43692
43843
|
baseUrl: transport.baseUrl,
|
|
43693
|
-
request: (method, path, body2, options) => protect(path, () => transport.request(method, path, body2,
|
|
43694
|
-
get: (path, options) => protect(path, () => transport.get(path,
|
|
43695
|
-
post: (path, body2, options) => protect(path, () => transport.post(path, body2,
|
|
43696
|
-
put: (path, body2, options) => protect(path, () => transport.put(path, body2,
|
|
43697
|
-
patch: (path, body2, options) => protect(path, () => transport.patch(path, body2,
|
|
43698
|
-
del: (path, body2, options) => protect(path, () => transport.del(path, body2,
|
|
43844
|
+
request: (method, path, body2, options) => protect(path, () => bounded(options, (opts) => transport.request(method, path, body2, opts))),
|
|
43845
|
+
get: (path, options) => protect(path, () => bounded(options, (opts) => transport.get(path, opts))),
|
|
43846
|
+
post: (path, body2, options) => protect(path, () => bounded(options, (opts) => transport.post(path, body2, opts))),
|
|
43847
|
+
put: (path, body2, options) => protect(path, () => bounded(options, (opts) => transport.put(path, body2, opts))),
|
|
43848
|
+
patch: (path, body2, options) => protect(path, () => bounded(options, (opts) => transport.patch(path, body2, opts))),
|
|
43849
|
+
del: (path, body2, options) => protect(path, () => bounded(options, (opts) => transport.del(path, body2, opts)))
|
|
43699
43850
|
};
|
|
43700
43851
|
return {
|
|
43701
43852
|
name: client.name,
|
|
43702
43853
|
baseUrl: client.baseUrl,
|
|
43703
43854
|
transport: protectedTransport,
|
|
43704
|
-
list: (resource, options) => protect(`/${resource}`, () => client.list(resource,
|
|
43705
|
-
get: (resource, id, options) => protect(`/${resource}/${encodeURIComponent(id)}`, () => client.get(resource, id,
|
|
43706
|
-
create: (resource, body2, options) => protect(`/${resource}`, () => client.create(resource, body2,
|
|
43707
|
-
update: (resource, id, patch, options) => protect(`/${resource}/${encodeURIComponent(id)}`, () => client.update(resource, id, patch,
|
|
43708
|
-
delete: (resource, id, options) => protect(`/${resource}/${encodeURIComponent(id)}`, () => client.delete(resource, id,
|
|
43855
|
+
list: (resource, options) => protect(`/${resource}`, () => bounded(options, (opts) => client.list(resource, opts))),
|
|
43856
|
+
get: (resource, id, options) => protect(`/${resource}/${encodeURIComponent(id)}`, () => bounded(options, (opts) => client.get(resource, id, opts))),
|
|
43857
|
+
create: (resource, body2, options) => protect(`/${resource}`, () => bounded(options, (opts) => client.create(resource, body2, opts))),
|
|
43858
|
+
update: (resource, id, patch, options) => protect(`/${resource}/${encodeURIComponent(id)}`, () => bounded(options, (opts) => client.update(resource, id, patch, opts))),
|
|
43859
|
+
delete: (resource, id, options) => protect(`/${resource}/${encodeURIComponent(id)}`, () => bounded(options, (opts) => client.delete(resource, id, opts)))
|
|
43709
43860
|
};
|
|
43710
43861
|
}
|
|
43711
43862
|
function remoteAuthorityBase(client) {
|
|
@@ -43726,13 +43877,19 @@ async function requiredRemoteRoute(client, route, request, recognized404Codes =
|
|
|
43726
43877
|
throw error;
|
|
43727
43878
|
}
|
|
43728
43879
|
}
|
|
43729
|
-
function getTodosCloudClient(env = process.env) {
|
|
43880
|
+
function getTodosCloudClient(env = process.env, requestTimeoutMs = REMOTE_REQUEST_TIMEOUT_MS) {
|
|
43730
43881
|
if (requestedTransport(env) !== "http")
|
|
43731
43882
|
return null;
|
|
43732
43883
|
const resolved = resolveStorageClient("todos", requireTodosRemoteAuthorityEnv(env), {
|
|
43733
|
-
fetchImpl: (input, init) => globalThis.fetch(input, { ...init, redirect: "manual" })
|
|
43884
|
+
fetchImpl: (input, init) => globalThis.fetch(input, { ...init, redirect: "manual" }),
|
|
43885
|
+
timeoutMs: requestTimeoutMs
|
|
43734
43886
|
});
|
|
43735
|
-
|
|
43887
|
+
if (resolved.transport === "cloud-http")
|
|
43888
|
+
return protectRemoteClient(resolved.client, requestTimeoutMs);
|
|
43889
|
+
const transportName = resolved.transport;
|
|
43890
|
+
if (transportName === "http")
|
|
43891
|
+
return protectRemoteClient(resolved.client, requestTimeoutMs);
|
|
43892
|
+
return null;
|
|
43736
43893
|
}
|
|
43737
43894
|
function unwrapTask(raw) {
|
|
43738
43895
|
if (raw && typeof raw === "object" && "task" in raw) {
|
|
@@ -45527,7 +45684,7 @@ function getNativeStorageStatus(env = process.env) {
|
|
|
45527
45684
|
const remoteEnabled = isTodosPostgresBackend(config);
|
|
45528
45685
|
const remoteFieldsConfigured = Boolean(config.database || config.objectStorage);
|
|
45529
45686
|
if (!remoteEnabled && remoteFieldsConfigured) {
|
|
45530
|
-
warnings.push(`the sqlite backend
|
|
45687
|
+
warnings.push(`the sqlite backend ignores configured remote storage fields`);
|
|
45531
45688
|
}
|
|
45532
45689
|
if (remoteEnabled && !config.objectStorage) {
|
|
45533
45690
|
warnings.push(`${TODOS_STORAGE_ENV.s3Bucket} is not configured, so artifact sync will stay local`);
|
|
@@ -45620,7 +45777,7 @@ function storageEnvStatus(env) {
|
|
|
45620
45777
|
function fallbackConfig(env) {
|
|
45621
45778
|
let mode;
|
|
45622
45779
|
try {
|
|
45623
|
-
mode =
|
|
45780
|
+
mode = getTodosStorageBackend(env);
|
|
45624
45781
|
} catch {
|
|
45625
45782
|
mode = "sqlite";
|
|
45626
45783
|
}
|
|
@@ -47804,7 +47961,7 @@ function addSourceOnce(projectId, type, name, uri, metadata, db) {
|
|
|
47804
47961
|
function bootstrapProject(options = {}, db) {
|
|
47805
47962
|
const d = db || getDatabase();
|
|
47806
47963
|
const discovery = discoverProjectWorkspace(options.path);
|
|
47807
|
-
const taskListSlug2 = options.taskListSlug ||
|
|
47964
|
+
const taskListSlug2 = options.taskListSlug || slugify(options.name || discovery.projectName);
|
|
47808
47965
|
if (options.dryRun) {
|
|
47809
47966
|
return {
|
|
47810
47967
|
dryRun: true,
|
|
@@ -55222,21 +55379,22 @@ function upsertProject(raw, d) {
|
|
|
55222
55379
|
const existing = getProject(id, d);
|
|
55223
55380
|
const ts = now();
|
|
55224
55381
|
if (!existing) {
|
|
55225
|
-
d.run(`INSERT INTO projects (id, name, path, description, task_list_id, task_prefix, task_counter, created_at, updated_at)
|
|
55226
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
55382
|
+
d.run(`INSERT INTO projects (id, name, path, description, task_list_id, task_prefix, task_counter, parent_id, created_at, updated_at)
|
|
55383
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
55227
55384
|
id,
|
|
55228
55385
|
raw.name,
|
|
55229
55386
|
raw.path,
|
|
55230
55387
|
raw.description ?? null,
|
|
55231
|
-
raw.task_list_id ??
|
|
55388
|
+
raw.task_list_id ?? String(raw.name).toLowerCase().replace(/\s+/g, "-"),
|
|
55232
55389
|
raw.task_prefix ?? "TSK",
|
|
55233
55390
|
raw.task_counter ?? 0,
|
|
55391
|
+
raw.parent_id ?? null,
|
|
55234
55392
|
raw.created_at ?? ts,
|
|
55235
55393
|
raw.updated_at ?? ts
|
|
55236
55394
|
].map(sqlValue));
|
|
55237
55395
|
return "created";
|
|
55238
55396
|
}
|
|
55239
|
-
d.run(`UPDATE projects SET name = ?, path = ?, description = ?, task_list_id = ?, task_prefix = ?, task_counter = ?, updated_at = ?
|
|
55397
|
+
d.run(`UPDATE projects SET name = ?, path = ?, description = ?, task_list_id = ?, task_prefix = ?, task_counter = ?, parent_id = ?, updated_at = ?
|
|
55240
55398
|
WHERE id = ?`, [
|
|
55241
55399
|
raw.name ?? existing.name,
|
|
55242
55400
|
raw.path ?? existing.path,
|
|
@@ -55244,6 +55402,7 @@ function upsertProject(raw, d) {
|
|
|
55244
55402
|
raw.task_list_id ?? existing.task_list_id,
|
|
55245
55403
|
raw.task_prefix ?? existing.task_prefix,
|
|
55246
55404
|
raw.task_counter ?? existing.task_counter,
|
|
55405
|
+
raw.parent_id ?? existing.parent_id,
|
|
55247
55406
|
raw.updated_at ?? ts,
|
|
55248
55407
|
id
|
|
55249
55408
|
].map(sqlValue));
|
|
@@ -55324,7 +55483,7 @@ function importBundle(bundle, options = {}, db) {
|
|
|
55324
55483
|
};
|
|
55325
55484
|
if (options.dry_run)
|
|
55326
55485
|
return result;
|
|
55327
|
-
for (const raw of bundle.projects) {
|
|
55486
|
+
for (const raw of orderProjectsParentFirst(bundle.projects)) {
|
|
55328
55487
|
try {
|
|
55329
55488
|
const id = raw.id;
|
|
55330
55489
|
const local = getProject(id, d);
|