@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/cli/index.js
CHANGED
|
@@ -2123,7 +2123,7 @@ var package_default;
|
|
|
2123
2123
|
var init_package = __esm(() => {
|
|
2124
2124
|
package_default = {
|
|
2125
2125
|
name: "@hasna/todos",
|
|
2126
|
-
version: "0.15.
|
|
2126
|
+
version: "0.15.35",
|
|
2127
2127
|
description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
|
|
2128
2128
|
type: "module",
|
|
2129
2129
|
main: "dist/index.js",
|
|
@@ -2197,9 +2197,10 @@ var init_package = __esm(() => {
|
|
|
2197
2197
|
"verify:release": "bun run scripts/verify-public-release.ts --mode=review",
|
|
2198
2198
|
"verify:release-review": "bun run scripts/verify-npm-release-agent-review.ts",
|
|
2199
2199
|
"verify:attested-container-candidate": "bun run scripts/attested-container-candidate.ts verify",
|
|
2200
|
-
"test:attested-container-candidate": "bun test scripts/attested-container-candidate.test.ts
|
|
2200
|
+
"test:attested-container-candidate": "bun test scripts/attested-container-candidate.test.ts",
|
|
2201
2201
|
"emit:iapp-deployment-compatibility-vector": "bun run scripts/attested-container-compatibility-vector.ts emit",
|
|
2202
2202
|
"verify:iapp-deployment-compatibility-vector": "bun run scripts/attested-container-compatibility-vector.ts verify",
|
|
2203
|
+
"test:attested-container-compatibility-vector": "bun test scripts/attested-container-compatibility-vector.test.ts",
|
|
2203
2204
|
"issue:release-review": "bun run scripts/issue-npm-release-agent-review.ts",
|
|
2204
2205
|
prepublishOnly: "bun run scripts/verify-public-release.ts --mode=publish",
|
|
2205
2206
|
postinstall: "mkdir -p $HOME/.hasna/todos $HOME/.hasna/todos/training 2>/dev/null || true"
|
|
@@ -5657,40 +5658,38 @@ var init_page_validation = __esm(() => {
|
|
|
5657
5658
|
|
|
5658
5659
|
// src/cli/cloud-router.ts
|
|
5659
5660
|
import { resolveStorageClient } from "@hasna/contracts/client/storage";
|
|
5660
|
-
import { normalizeStorageMode } from "@hasna/contracts/mode";
|
|
5661
5661
|
import { randomUUID } from "crypto";
|
|
5662
5662
|
import { resolve as resolvePath } from "path";
|
|
5663
|
-
function
|
|
5664
|
-
const
|
|
5665
|
-
|
|
5663
|
+
function assertNoLegacyStorageMode(env = process.env) {
|
|
5664
|
+
for (const key of LEGACY_STORAGE_MODE_KEYS) {
|
|
5665
|
+
if (Object.hasOwn(env, key) && env[key] !== undefined) {
|
|
5666
|
+
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.`);
|
|
5667
|
+
}
|
|
5668
|
+
}
|
|
5666
5669
|
}
|
|
5667
5670
|
function resolveTodosCliStorageMode(env = process.env) {
|
|
5668
|
-
|
|
5669
|
-
|
|
5670
|
-
|
|
5671
|
-
|
|
5671
|
+
assertNoLegacyStorageMode(env);
|
|
5672
|
+
const urlValue = env.HASNA_TODOS_API_URL?.trim();
|
|
5673
|
+
const keyValue = env.HASNA_TODOS_API_KEY?.trim();
|
|
5674
|
+
if (urlValue && keyValue) {
|
|
5675
|
+
return {
|
|
5676
|
+
mode: "http",
|
|
5677
|
+
transport: "http",
|
|
5678
|
+
selected: true,
|
|
5679
|
+
source: "HASNA_TODOS_API_URL+HASNA_TODOS_API_KEY"
|
|
5680
|
+
};
|
|
5672
5681
|
}
|
|
5673
|
-
|
|
5674
|
-
|
|
5675
|
-
for (const [source, value] of [
|
|
5676
|
-
["HASNA_TODOS_STORAGE_MODE", canonical],
|
|
5677
|
-
["TODOS_STORAGE_MODE", fallback]
|
|
5678
|
-
]) {
|
|
5679
|
-
if (value && !(value in TRANSPORT_TOKENS)) {
|
|
5680
|
-
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");
|
|
5681
|
-
}
|
|
5682
|
+
if (urlValue) {
|
|
5683
|
+
throw new Error("REMOTE_API_KEY_MISSING: remote Todos storage requires HASNA_TODOS_API_KEY; local SQLite fallback is disabled");
|
|
5682
5684
|
}
|
|
5683
|
-
|
|
5684
|
-
|
|
5685
|
-
if (canonicalTransport && fallbackTransport && canonicalTransport !== fallbackTransport) {
|
|
5686
|
-
throw new Error(`REMOTE_STORAGE_MODE_CONFLICT: HASNA_TODOS_STORAGE_MODE=${canonical} conflicts with ` + `TODOS_STORAGE_MODE=${fallback}; local SQLite fallback is disabled`);
|
|
5685
|
+
if (keyValue) {
|
|
5686
|
+
throw new Error("REMOTE_API_URL_MISSING: remote Todos storage requires HASNA_TODOS_API_URL; local SQLite fallback is disabled");
|
|
5687
5687
|
}
|
|
5688
|
-
const transport = canonicalTransport ?? fallbackTransport ?? "sqlite";
|
|
5689
5688
|
return {
|
|
5690
|
-
mode:
|
|
5691
|
-
transport,
|
|
5692
|
-
selected:
|
|
5693
|
-
source:
|
|
5689
|
+
mode: "sqlite",
|
|
5690
|
+
transport: "sqlite",
|
|
5691
|
+
selected: false,
|
|
5692
|
+
source: "default"
|
|
5694
5693
|
};
|
|
5695
5694
|
}
|
|
5696
5695
|
function requestedTransport(env) {
|
|
@@ -5734,7 +5733,7 @@ function getTodosRemoteAuthorityConfigStatus(env = process.env) {
|
|
|
5734
5733
|
return {
|
|
5735
5734
|
selected: true,
|
|
5736
5735
|
ok: false,
|
|
5737
|
-
mode:
|
|
5736
|
+
mode: "invalid",
|
|
5738
5737
|
api_url_configured: Boolean(env.HASNA_TODOS_API_URL?.trim()),
|
|
5739
5738
|
api_key_configured: Boolean(env.HASNA_TODOS_API_KEY?.trim()),
|
|
5740
5739
|
v1_base_url: null,
|
|
@@ -5780,27 +5779,12 @@ function getTodosRemoteAuthorityConfigStatus(env = process.env) {
|
|
|
5780
5779
|
local_fallback: false
|
|
5781
5780
|
};
|
|
5782
5781
|
}
|
|
5783
|
-
function serverStorageMode(normalize = normalizeStorageMode) {
|
|
5784
|
-
const useCache = normalize === normalizeStorageMode;
|
|
5785
|
-
if (useCache && cachedServerMode !== null)
|
|
5786
|
-
return cachedServerMode;
|
|
5787
|
-
for (const candidate of SERVER_MODE_CANDIDATES) {
|
|
5788
|
-
try {
|
|
5789
|
-
normalize(candidate);
|
|
5790
|
-
if (useCache)
|
|
5791
|
-
cachedServerMode = candidate;
|
|
5792
|
-
return candidate;
|
|
5793
|
-
} catch {}
|
|
5794
|
-
}
|
|
5795
|
-
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.`);
|
|
5796
|
-
}
|
|
5797
5782
|
function requireTodosRemoteAuthorityEnv(env) {
|
|
5798
5783
|
const status = getTodosRemoteAuthorityConfigStatus(env);
|
|
5799
5784
|
if (!status.ok)
|
|
5800
5785
|
throw new Error(status.issues[0]);
|
|
5801
5786
|
return {
|
|
5802
5787
|
...env,
|
|
5803
|
-
HASNA_TODOS_STORAGE_MODE: serverStorageMode(),
|
|
5804
5788
|
HASNA_TODOS_API_URL: status.v1_base_url.replace(/\/v1$/, ""),
|
|
5805
5789
|
HASNA_TODOS_API_KEY: env.HASNA_TODOS_API_KEY.trim()
|
|
5806
5790
|
};
|
|
@@ -5838,7 +5822,31 @@ function classifyRemoteRequestError(baseUrl, route, error) {
|
|
|
5838
5822
|
}
|
|
5839
5823
|
throw error;
|
|
5840
5824
|
}
|
|
5841
|
-
function
|
|
5825
|
+
function withBoundedRemoteRequest(options, requestTimeoutMs, run) {
|
|
5826
|
+
const controller = new AbortController;
|
|
5827
|
+
const onBaseAbort = () => controller.abort();
|
|
5828
|
+
if (options?.signal) {
|
|
5829
|
+
if (options.signal.aborted)
|
|
5830
|
+
controller.abort();
|
|
5831
|
+
else
|
|
5832
|
+
options.signal.addEventListener("abort", onBaseAbort, { once: true });
|
|
5833
|
+
}
|
|
5834
|
+
let timer;
|
|
5835
|
+
const deadline = new Promise((_, reject) => {
|
|
5836
|
+
timer = setTimeout(() => {
|
|
5837
|
+
controller.abort();
|
|
5838
|
+
reject(new DOMException(`Todos remote request exceeded the ${requestTimeoutMs}ms bounded request timeout`, "AbortError"));
|
|
5839
|
+
}, requestTimeoutMs);
|
|
5840
|
+
});
|
|
5841
|
+
const attempt = run({ ...options, signal: controller.signal });
|
|
5842
|
+
attempt.catch(() => {});
|
|
5843
|
+
return Promise.race([attempt, deadline]).finally(() => {
|
|
5844
|
+
if (timer)
|
|
5845
|
+
clearTimeout(timer);
|
|
5846
|
+
options?.signal?.removeEventListener("abort", onBaseAbort);
|
|
5847
|
+
});
|
|
5848
|
+
}
|
|
5849
|
+
function protectRemoteClient(client, requestTimeoutMs) {
|
|
5842
5850
|
const baseUrl = remoteAuthorityBase(client);
|
|
5843
5851
|
const protect = async (route, request) => {
|
|
5844
5852
|
try {
|
|
@@ -5847,25 +5855,26 @@ function protectRemoteClient(client) {
|
|
|
5847
5855
|
return classifyRemoteRequestError(baseUrl, route, error);
|
|
5848
5856
|
}
|
|
5849
5857
|
};
|
|
5858
|
+
const bounded = (options, run) => withBoundedRemoteRequest(options, requestTimeoutMs, run);
|
|
5850
5859
|
const transport = client.transport;
|
|
5851
5860
|
const protectedTransport = {
|
|
5852
5861
|
baseUrl: transport.baseUrl,
|
|
5853
|
-
request: (method, path, body, options) => protect(path, () => transport.request(method, path, body,
|
|
5854
|
-
get: (path, options) => protect(path, () => transport.get(path,
|
|
5855
|
-
post: (path, body, options) => protect(path, () => transport.post(path, body,
|
|
5856
|
-
put: (path, body, options) => protect(path, () => transport.put(path, body,
|
|
5857
|
-
patch: (path, body, options) => protect(path, () => transport.patch(path, body,
|
|
5858
|
-
del: (path, body, options) => protect(path, () => transport.del(path, body,
|
|
5862
|
+
request: (method, path, body, options) => protect(path, () => bounded(options, (opts) => transport.request(method, path, body, opts))),
|
|
5863
|
+
get: (path, options) => protect(path, () => bounded(options, (opts) => transport.get(path, opts))),
|
|
5864
|
+
post: (path, body, options) => protect(path, () => bounded(options, (opts) => transport.post(path, body, opts))),
|
|
5865
|
+
put: (path, body, options) => protect(path, () => bounded(options, (opts) => transport.put(path, body, opts))),
|
|
5866
|
+
patch: (path, body, options) => protect(path, () => bounded(options, (opts) => transport.patch(path, body, opts))),
|
|
5867
|
+
del: (path, body, options) => protect(path, () => bounded(options, (opts) => transport.del(path, body, opts)))
|
|
5859
5868
|
};
|
|
5860
5869
|
return {
|
|
5861
5870
|
name: client.name,
|
|
5862
5871
|
baseUrl: client.baseUrl,
|
|
5863
5872
|
transport: protectedTransport,
|
|
5864
|
-
list: (resource, options) => protect(`/${resource}`, () => client.list(resource,
|
|
5865
|
-
get: (resource, id, options) => protect(`/${resource}/${encodeURIComponent(id)}`, () => client.get(resource, id,
|
|
5866
|
-
create: (resource, body, options) => protect(`/${resource}`, () => client.create(resource, body,
|
|
5867
|
-
update: (resource, id, patch, options) => protect(`/${resource}/${encodeURIComponent(id)}`, () => client.update(resource, id, patch,
|
|
5868
|
-
delete: (resource, id, options) => protect(`/${resource}/${encodeURIComponent(id)}`, () => client.delete(resource, id,
|
|
5873
|
+
list: (resource, options) => protect(`/${resource}`, () => bounded(options, (opts) => client.list(resource, opts))),
|
|
5874
|
+
get: (resource, id, options) => protect(`/${resource}/${encodeURIComponent(id)}`, () => bounded(options, (opts) => client.get(resource, id, opts))),
|
|
5875
|
+
create: (resource, body, options) => protect(`/${resource}`, () => bounded(options, (opts) => client.create(resource, body, opts))),
|
|
5876
|
+
update: (resource, id, patch, options) => protect(`/${resource}/${encodeURIComponent(id)}`, () => bounded(options, (opts) => client.update(resource, id, patch, opts))),
|
|
5877
|
+
delete: (resource, id, options) => protect(`/${resource}/${encodeURIComponent(id)}`, () => bounded(options, (opts) => client.delete(resource, id, opts)))
|
|
5869
5878
|
};
|
|
5870
5879
|
}
|
|
5871
5880
|
function remoteAuthorityBase(client) {
|
|
@@ -5886,13 +5895,19 @@ async function requiredRemoteRoute(client, route, request, recognized404Codes =
|
|
|
5886
5895
|
throw error;
|
|
5887
5896
|
}
|
|
5888
5897
|
}
|
|
5889
|
-
function getTodosCloudClient(env = process.env) {
|
|
5898
|
+
function getTodosCloudClient(env = process.env, requestTimeoutMs = REMOTE_REQUEST_TIMEOUT_MS) {
|
|
5890
5899
|
if (requestedTransport(env) !== "http")
|
|
5891
5900
|
return null;
|
|
5892
5901
|
const resolved = resolveStorageClient("todos", requireTodosRemoteAuthorityEnv(env), {
|
|
5893
|
-
fetchImpl: (input, init) => globalThis.fetch(input, { ...init, redirect: "manual" })
|
|
5894
|
-
|
|
5895
|
-
|
|
5902
|
+
fetchImpl: (input, init) => globalThis.fetch(input, { ...init, redirect: "manual" }),
|
|
5903
|
+
timeoutMs: requestTimeoutMs
|
|
5904
|
+
});
|
|
5905
|
+
if (resolved.transport === "cloud-http")
|
|
5906
|
+
return protectRemoteClient(resolved.client, requestTimeoutMs);
|
|
5907
|
+
const transportName = resolved.transport;
|
|
5908
|
+
if (transportName === "http")
|
|
5909
|
+
return protectRemoteClient(resolved.client, requestTimeoutMs);
|
|
5910
|
+
return null;
|
|
5896
5911
|
}
|
|
5897
5912
|
function isCloudRouting(env = process.env) {
|
|
5898
5913
|
return getTodosCloudClient(env) !== null;
|
|
@@ -7549,7 +7564,7 @@ async function cloudTimeline(client, options = {}) {
|
|
|
7549
7564
|
const limit = options.limit ?? 50;
|
|
7550
7565
|
return { entries: entries.slice(offset, offset + limit), total, limit, offset };
|
|
7551
7566
|
}
|
|
7552
|
-
var UUID_RE,
|
|
7567
|
+
var UUID_RE, COMPLETION_EVIDENCE_FIELDS, completionCapabilityCache, retryCapabilityCache, taskCreatorCapabilityCache, gitRefCapabilityCache, remoteCommandCapabilityCache, LEGACY_STORAGE_MODE_KEYS, REMOTE_REQUEST_TIMEOUT_MS = 1e4, PRIORITY_RANK, listTagsCapabilityCache, PLAN_COMPLETION_PROTECTED_FIELDS, RELATION_HYDRATION_CONCURRENCY = 6;
|
|
7553
7568
|
var init_cloud_router = __esm(() => {
|
|
7554
7569
|
init_types();
|
|
7555
7570
|
init_redaction();
|
|
@@ -7558,15 +7573,6 @@ var init_cloud_router = __esm(() => {
|
|
|
7558
7573
|
init_adoption_validation();
|
|
7559
7574
|
init_page_validation();
|
|
7560
7575
|
UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
7561
|
-
TRANSPORT_TOKENS = {
|
|
7562
|
-
sqlite: "sqlite",
|
|
7563
|
-
http: "http",
|
|
7564
|
-
local: "sqlite",
|
|
7565
|
-
remote: "http",
|
|
7566
|
-
self_hosted: "http",
|
|
7567
|
-
cloud: "http",
|
|
7568
|
-
hybrid: "http"
|
|
7569
|
-
};
|
|
7570
7576
|
COMPLETION_EVIDENCE_FIELDS = [
|
|
7571
7577
|
"attachment_ids",
|
|
7572
7578
|
"files_changed",
|
|
@@ -7580,7 +7586,12 @@ var init_cloud_router = __esm(() => {
|
|
|
7580
7586
|
taskCreatorCapabilityCache = new Map;
|
|
7581
7587
|
gitRefCapabilityCache = new Map;
|
|
7582
7588
|
remoteCommandCapabilityCache = new Map;
|
|
7583
|
-
|
|
7589
|
+
LEGACY_STORAGE_MODE_KEYS = [
|
|
7590
|
+
"HASNA_TODOS_STORAGE_MODE",
|
|
7591
|
+
"HASNA_TODOS_MODE",
|
|
7592
|
+
"TODOS_STORAGE_MODE",
|
|
7593
|
+
"TODOS_MODE"
|
|
7594
|
+
];
|
|
7584
7595
|
PRIORITY_RANK = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
7585
7596
|
listTagsCapabilityCache = new Map;
|
|
7586
7597
|
PLAN_COMPLETION_PROTECTED_FIELDS = [
|
|
@@ -7601,8 +7612,10 @@ var init_cloud_router = __esm(() => {
|
|
|
7601
7612
|
function applyTodosCliAuthorityEnvironment(authority, env = process.env) {
|
|
7602
7613
|
if (authority.route !== "local" || authority.selected_by !== "local-only-command")
|
|
7603
7614
|
return;
|
|
7604
|
-
env.
|
|
7605
|
-
env.
|
|
7615
|
+
env.HASNA_TODOS_API_URL = "";
|
|
7616
|
+
env.HASNA_TODOS_API_KEY = "";
|
|
7617
|
+
env.TODOS_API_URL = "";
|
|
7618
|
+
env.TODOS_API_KEY = "";
|
|
7606
7619
|
}
|
|
7607
7620
|
function isTodosCliCommandVisibleForRoute(command, route, remoteCapabilities = new Set) {
|
|
7608
7621
|
if (route === "local")
|
|
@@ -7846,7 +7859,7 @@ function assertInvocationRoutable(invocation) {
|
|
|
7846
7859
|
if (command && !owner) {
|
|
7847
7860
|
const suggestions = nearestCommands(command);
|
|
7848
7861
|
const didYouMean = suggestions.length > 0 ? ` Did you mean: ${suggestions.join(", ")}?` : "";
|
|
7849
|
-
throw new Error(`UNKNOWN_COMMAND: \`${command}\` is not a built-in todos command on the /v1 route.${didYouMean} ` + "Run `todos --help` for the commands available here; verbs contributed by optional packages are local-only. " + "(This is not a connectivity
|
|
7862
|
+
throw new Error(`UNKNOWN_COMMAND: \`${command}\` is not a built-in todos command on the /v1 route.${didYouMean} ` + "Run `todos --help` for the commands available here; verbs contributed by optional packages are local-only. " + "(This is not a connectivity or credential problem.)");
|
|
7850
7863
|
}
|
|
7851
7864
|
return owner;
|
|
7852
7865
|
}
|
|
@@ -7863,7 +7876,20 @@ function assertRemoteCommandSupported(invocation, owner) {
|
|
|
7863
7876
|
}
|
|
7864
7877
|
}
|
|
7865
7878
|
function initializeTodosCliAuthority(args = process.argv.slice(2), env = process.env) {
|
|
7866
|
-
|
|
7879
|
+
let mode;
|
|
7880
|
+
try {
|
|
7881
|
+
mode = resolveTodosCliStorageMode(env);
|
|
7882
|
+
} catch (error) {
|
|
7883
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
7884
|
+
if (message.includes("REMOTE_STORAGE_MODE_REMOVED"))
|
|
7885
|
+
throw error;
|
|
7886
|
+
const invocation2 = parseInvocation(args);
|
|
7887
|
+
if (isMetadataInvocation(args, invocation2)) {
|
|
7888
|
+
const status = getTodosRemoteAuthorityConfigStatus(env);
|
|
7889
|
+
return { route: "remote-diagnostic", v1_base_url: status.v1_base_url };
|
|
7890
|
+
}
|
|
7891
|
+
throw error;
|
|
7892
|
+
}
|
|
7867
7893
|
if (!mode.selected)
|
|
7868
7894
|
return { route: "local", v1_base_url: null };
|
|
7869
7895
|
const invocation = parseInvocation(args);
|
|
@@ -7878,7 +7904,7 @@ function initializeTodosCliAuthority(args = process.argv.slice(2), env = process
|
|
|
7878
7904
|
assertRemoteCommandSupported(invocation, owner);
|
|
7879
7905
|
const client = getTodosCloudClient(env);
|
|
7880
7906
|
if (!client) {
|
|
7881
|
-
throw new Error("REMOTE_API_UNAVAILABLE:
|
|
7907
|
+
throw new Error("REMOTE_API_UNAVAILABLE: HTTP routing did not resolve an authenticated /v1 client; local SQLite fallback is disabled");
|
|
7882
7908
|
}
|
|
7883
7909
|
return { route: "remote-http", v1_base_url: client.baseUrl };
|
|
7884
7910
|
}
|
|
@@ -10038,6 +10064,10 @@ var init_migrations = __esm(() => {
|
|
|
10038
10064
|
);
|
|
10039
10065
|
INSERT OR IGNORE INTO _migrations (id) VALUES (70);
|
|
10040
10066
|
COMMIT;
|
|
10067
|
+
`,
|
|
10068
|
+
`BEGIN;
|
|
10069
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (71);
|
|
10070
|
+
COMMIT;
|
|
10041
10071
|
`
|
|
10042
10072
|
];
|
|
10043
10073
|
});
|
|
@@ -10711,6 +10741,8 @@ function ensureSchema(db) {
|
|
|
10711
10741
|
ensureColumn("projects", "task_list_id", "TEXT");
|
|
10712
10742
|
ensureColumn("projects", "task_prefix", "TEXT");
|
|
10713
10743
|
ensureColumn("projects", "task_counter", "INTEGER NOT NULL DEFAULT 0");
|
|
10744
|
+
ensureColumn("projects", "parent_id", "TEXT REFERENCES projects(id) ON DELETE SET NULL");
|
|
10745
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_projects_parent_id ON projects(parent_id)");
|
|
10714
10746
|
ensureColumn("tasks", "plan_id", "TEXT REFERENCES plans(id) ON DELETE SET NULL");
|
|
10715
10747
|
ensureColumn("tasks", "task_list_id", "TEXT REFERENCES task_lists(id) ON DELETE SET NULL");
|
|
10716
10748
|
ensureColumn("tasks", "short_id", "TEXT");
|
|
@@ -12041,6 +12073,7 @@ __export(exports_config2, {
|
|
|
12041
12073
|
TODOS_STORAGE_FALLBACK_ENV: () => TODOS_STORAGE_FALLBACK_ENV,
|
|
12042
12074
|
TODOS_STORAGE_ENV: () => TODOS_STORAGE_ENV,
|
|
12043
12075
|
STORAGE_TABLES: () => STORAGE_TABLES,
|
|
12076
|
+
REMOVED_STORAGE_MODE_ENV_KEYS: () => REMOVED_STORAGE_MODE_ENV_KEYS,
|
|
12044
12077
|
CANONICAL_TODOS_RDS_RUNTIME_PATH_ENV: () => CANONICAL_TODOS_RDS_RUNTIME_PATH_ENV,
|
|
12045
12078
|
CANONICAL_TODOS_RDS_DATABASE: () => CANONICAL_TODOS_RDS_DATABASE,
|
|
12046
12079
|
CANONICAL_TODOS_RDS_CLUSTER_ENV: () => CANONICAL_TODOS_RDS_CLUSTER_ENV
|
|
@@ -12102,25 +12135,36 @@ function assertTodosRemoteStorageConfig(config) {
|
|
|
12102
12135
|
if (!isTodosPostgresBackend(config))
|
|
12103
12136
|
return;
|
|
12104
12137
|
if (!config.database?.url) {
|
|
12105
|
-
throw new Error(`${TODOS_STORAGE_ENV.databaseUrl} is required when
|
|
12138
|
+
throw new Error(`${TODOS_STORAGE_ENV.databaseUrl} is required when the postgresql backend is selected`);
|
|
12106
12139
|
}
|
|
12107
12140
|
}
|
|
12108
12141
|
function parseStorageBackend(value) {
|
|
12109
12142
|
const normalized = clean(value)?.toLowerCase();
|
|
12110
12143
|
if (!normalized)
|
|
12111
12144
|
return "sqlite";
|
|
12112
|
-
if (normalized === "sqlite"
|
|
12113
|
-
return
|
|
12114
|
-
|
|
12115
|
-
|
|
12116
|
-
|
|
12117
|
-
|
|
12145
|
+
if (normalized === "sqlite")
|
|
12146
|
+
return "sqlite";
|
|
12147
|
+
if (normalized === "postgres" || normalized === "postgresql")
|
|
12148
|
+
return "postgres";
|
|
12149
|
+
if (["local", "remote", "cloud", "hybrid", "self_hosted"].includes(normalized)) {
|
|
12150
|
+
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.`);
|
|
12151
|
+
}
|
|
12152
|
+
throw new Error(`Storage backend must be sqlite or postgres`);
|
|
12118
12153
|
}
|
|
12119
12154
|
function parseStorageMode(value) {
|
|
12120
12155
|
return parseStorageBackend(value);
|
|
12121
12156
|
}
|
|
12122
12157
|
function getTodosStorageBackend(env = process.env) {
|
|
12123
|
-
|
|
12158
|
+
for (const key of REMOVED_STORAGE_MODE_ENV_KEYS) {
|
|
12159
|
+
if (Object.hasOwn(env, key) && env[key] !== undefined) {
|
|
12160
|
+
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.`);
|
|
12161
|
+
}
|
|
12162
|
+
}
|
|
12163
|
+
if (!getTodosStorageDatabaseUrl(env))
|
|
12164
|
+
return "sqlite";
|
|
12165
|
+
if (isTodosShadowEnabled(env))
|
|
12166
|
+
return "sqlite";
|
|
12167
|
+
return "postgres";
|
|
12124
12168
|
}
|
|
12125
12169
|
function getTodosStorageMode(env = process.env) {
|
|
12126
12170
|
return getTodosStorageBackend(env);
|
|
@@ -12194,7 +12238,7 @@ function parsePositiveInteger(value, fallback) {
|
|
|
12194
12238
|
}
|
|
12195
12239
|
return parsed;
|
|
12196
12240
|
}
|
|
12197
|
-
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"
|
|
12241
|
+
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";
|
|
12198
12242
|
var init_config2 = __esm(() => {
|
|
12199
12243
|
TODOS_STORAGE_TABLES = [
|
|
12200
12244
|
"todos_sync_records",
|
|
@@ -12202,7 +12246,6 @@ var init_config2 = __esm(() => {
|
|
|
12202
12246
|
];
|
|
12203
12247
|
STORAGE_TABLES = TODOS_STORAGE_TABLES;
|
|
12204
12248
|
TODOS_STORAGE_ENV = {
|
|
12205
|
-
mode: "HASNA_TODOS_STORAGE_MODE",
|
|
12206
12249
|
shadow: "HASNA_TODOS_SHADOW",
|
|
12207
12250
|
databaseUrl: "HASNA_TODOS_DATABASE_URL",
|
|
12208
12251
|
databaseSsl: "HASNA_TODOS_DATABASE_SSL",
|
|
@@ -12219,7 +12262,6 @@ var init_config2 = __esm(() => {
|
|
|
12219
12262
|
syncDryRun: "HASNA_TODOS_SYNC_DRY_RUN"
|
|
12220
12263
|
};
|
|
12221
12264
|
TODOS_STORAGE_FALLBACK_ENV = {
|
|
12222
|
-
mode: "TODOS_STORAGE_MODE",
|
|
12223
12265
|
shadow: "TODOS_SHADOW",
|
|
12224
12266
|
databaseUrl: "TODOS_DATABASE_URL",
|
|
12225
12267
|
databaseSsl: "TODOS_DATABASE_SSL",
|
|
@@ -12235,14 +12277,12 @@ var init_config2 = __esm(() => {
|
|
|
12235
12277
|
syncBatchSize: "TODOS_SYNC_BATCH_SIZE",
|
|
12236
12278
|
syncDryRun: "TODOS_SYNC_DRY_RUN"
|
|
12237
12279
|
};
|
|
12238
|
-
|
|
12239
|
-
|
|
12240
|
-
|
|
12241
|
-
|
|
12242
|
-
|
|
12243
|
-
|
|
12244
|
-
cloud: "postgres"
|
|
12245
|
-
};
|
|
12280
|
+
REMOVED_STORAGE_MODE_ENV_KEYS = [
|
|
12281
|
+
"HASNA_TODOS_STORAGE_MODE",
|
|
12282
|
+
"HASNA_TODOS_MODE",
|
|
12283
|
+
"TODOS_STORAGE_MODE",
|
|
12284
|
+
"TODOS_MODE"
|
|
12285
|
+
];
|
|
12246
12286
|
});
|
|
12247
12287
|
|
|
12248
12288
|
// src/storage/shadow-outbox-schema.ts
|
|
@@ -12774,10 +12814,12 @@ __export(exports_projects, {
|
|
|
12774
12814
|
renameProject: () => renameProject,
|
|
12775
12815
|
removeProjectSource: () => removeProjectSource,
|
|
12776
12816
|
removeMachineLocalPath: () => removeMachineLocalPath,
|
|
12817
|
+
orderProjectsParentFirst: () => orderProjectsParentFirst,
|
|
12777
12818
|
nextTaskShortId: () => nextTaskShortId,
|
|
12778
12819
|
listProjects: () => listProjects,
|
|
12779
12820
|
listProjectSources: () => listProjectSources,
|
|
12780
12821
|
listMachineLocalPaths: () => listMachineLocalPaths,
|
|
12822
|
+
listChildProjects: () => listChildProjects,
|
|
12781
12823
|
getProjectWithSources: () => getProjectWithSources,
|
|
12782
12824
|
getProjectByPath: () => getProjectByPath,
|
|
12783
12825
|
getProject: () => getProject,
|
|
@@ -12785,6 +12827,7 @@ __export(exports_projects, {
|
|
|
12785
12827
|
ensureProject: () => ensureProject,
|
|
12786
12828
|
deleteProject: () => deleteProject,
|
|
12787
12829
|
createProject: () => createProject,
|
|
12830
|
+
assertNotProjectAncestor: () => assertNotProjectAncestor,
|
|
12788
12831
|
addProjectSource: () => addProjectSource
|
|
12789
12832
|
});
|
|
12790
12833
|
function slugify(name) {
|
|
@@ -12816,17 +12859,24 @@ function createProject(input, db) {
|
|
|
12816
12859
|
const id = uuid();
|
|
12817
12860
|
const timestamp2 = now();
|
|
12818
12861
|
const derivedSlug = slugify(input.name);
|
|
12819
|
-
const taskListId = input.task_list_id === undefined ?
|
|
12862
|
+
const taskListId = input.task_list_id === undefined ? derivedSlug : slugify(input.task_list_id);
|
|
12820
12863
|
if (!derivedSlug || !taskListId)
|
|
12821
12864
|
throw new Error("Project name and task-list slug must be non-empty");
|
|
12822
12865
|
const slugConflict = d.query("SELECT id FROM projects WHERE task_list_id = ? LIMIT 1").get(taskListId);
|
|
12823
12866
|
if (slugConflict || !claimCanonicalSlug("project", "global", taskListId, id, d)) {
|
|
12824
12867
|
throw new ResourceConflictError("PROJECT_SLUG_CONFLICT", `Project slug "${taskListId}" already exists`);
|
|
12825
12868
|
}
|
|
12869
|
+
const parentId = input.parent_id ?? null;
|
|
12870
|
+
if (parentId !== null) {
|
|
12871
|
+
const parent = getProject(parentId, d);
|
|
12872
|
+
if (!parent)
|
|
12873
|
+
throw new ProjectNotFoundError(parentId);
|
|
12874
|
+
assertNotProjectAncestor(id, parentId, d);
|
|
12875
|
+
}
|
|
12826
12876
|
const taskPrefix = input.task_prefix || generatePrefix(input.name, d);
|
|
12827
12877
|
const machineId = currentStorageMachineId(d);
|
|
12828
|
-
d.run(`INSERT INTO projects (id, name, path, description, task_list_id, task_prefix, task_counter, created_at, updated_at, machine_id)
|
|
12829
|
-
VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?)`, [id, input.name, input.path, input.description || null, taskListId, taskPrefix, timestamp2, timestamp2, machineId]);
|
|
12878
|
+
d.run(`INSERT INTO projects (id, name, path, description, task_list_id, task_prefix, task_counter, parent_id, created_at, updated_at, machine_id)
|
|
12879
|
+
VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?)`, [id, input.name, input.path, input.description || null, taskListId, taskPrefix, parentId, timestamp2, timestamp2, machineId]);
|
|
12830
12880
|
return getProject(id, d);
|
|
12831
12881
|
})();
|
|
12832
12882
|
}
|
|
@@ -12851,6 +12901,64 @@ function listProjects(db) {
|
|
|
12851
12901
|
const d = db || getDatabase();
|
|
12852
12902
|
return d.query("SELECT * FROM projects ORDER BY name").all();
|
|
12853
12903
|
}
|
|
12904
|
+
function listChildProjects(parentId, db) {
|
|
12905
|
+
const d = db || getDatabase();
|
|
12906
|
+
return d.query("SELECT * FROM projects WHERE parent_id = ? ORDER BY name").all(parentId);
|
|
12907
|
+
}
|
|
12908
|
+
function orderProjectsParentFirst(projects) {
|
|
12909
|
+
const projectId = (project) => {
|
|
12910
|
+
const id = project.id;
|
|
12911
|
+
return typeof id === "string" && id.length > 0 ? id : null;
|
|
12912
|
+
};
|
|
12913
|
+
const parentId = (project) => {
|
|
12914
|
+
const parent = project.parent_id;
|
|
12915
|
+
return parent == null ? null : String(parent);
|
|
12916
|
+
};
|
|
12917
|
+
const byId = new Set;
|
|
12918
|
+
for (const project of projects) {
|
|
12919
|
+
const id = projectId(project);
|
|
12920
|
+
if (id !== null)
|
|
12921
|
+
byId.add(id);
|
|
12922
|
+
}
|
|
12923
|
+
const ordered = [];
|
|
12924
|
+
const emitted = new Set;
|
|
12925
|
+
let remaining = [...projects];
|
|
12926
|
+
let progress = true;
|
|
12927
|
+
while (progress && remaining.length > 0) {
|
|
12928
|
+
progress = false;
|
|
12929
|
+
const deferred = [];
|
|
12930
|
+
for (const project of remaining) {
|
|
12931
|
+
const parent = parentId(project);
|
|
12932
|
+
if (parent === null || emitted.has(parent) || !byId.has(parent)) {
|
|
12933
|
+
ordered.push(project);
|
|
12934
|
+
const id = projectId(project);
|
|
12935
|
+
if (id !== null)
|
|
12936
|
+
emitted.add(id);
|
|
12937
|
+
progress = true;
|
|
12938
|
+
} else {
|
|
12939
|
+
deferred.push(project);
|
|
12940
|
+
}
|
|
12941
|
+
}
|
|
12942
|
+
remaining = deferred;
|
|
12943
|
+
}
|
|
12944
|
+
ordered.push(...remaining);
|
|
12945
|
+
return ordered;
|
|
12946
|
+
}
|
|
12947
|
+
function assertNotProjectAncestor(projectId, ancestorId, db) {
|
|
12948
|
+
const d = db || getDatabase();
|
|
12949
|
+
let cursor = ancestorId;
|
|
12950
|
+
const seen = new Set;
|
|
12951
|
+
while (cursor !== null) {
|
|
12952
|
+
if (cursor === projectId) {
|
|
12953
|
+
throw new ResourceConflictError("PROJECT_PARENT_CYCLE", `Project "${projectId}" cannot be placed under its own descendant`);
|
|
12954
|
+
}
|
|
12955
|
+
if (seen.has(cursor))
|
|
12956
|
+
break;
|
|
12957
|
+
seen.add(cursor);
|
|
12958
|
+
const row = d.query("SELECT parent_id FROM projects WHERE id = ?").get(cursor);
|
|
12959
|
+
cursor = row?.parent_id ?? null;
|
|
12960
|
+
}
|
|
12961
|
+
}
|
|
12854
12962
|
function updateProject(id, input, db) {
|
|
12855
12963
|
const d = db || getDatabase();
|
|
12856
12964
|
const project = getProject(id, d);
|
|
@@ -12873,6 +12981,16 @@ function updateProject(id, input, db) {
|
|
|
12873
12981
|
sets.push("path = ?");
|
|
12874
12982
|
params.push(input.path);
|
|
12875
12983
|
}
|
|
12984
|
+
if (input.parent_id !== undefined) {
|
|
12985
|
+
if (input.parent_id !== null) {
|
|
12986
|
+
const parent = getProject(input.parent_id, d);
|
|
12987
|
+
if (!parent)
|
|
12988
|
+
throw new ProjectNotFoundError(input.parent_id);
|
|
12989
|
+
assertNotProjectAncestor(id, input.parent_id, d);
|
|
12990
|
+
}
|
|
12991
|
+
sets.push("parent_id = ?");
|
|
12992
|
+
params.push(input.parent_id);
|
|
12993
|
+
}
|
|
12876
12994
|
params.push(id);
|
|
12877
12995
|
d.run(`UPDATE projects SET ${sets.join(", ")} WHERE id = ?`, params);
|
|
12878
12996
|
return getProject(id, d);
|
|
@@ -15385,8 +15503,8 @@ var init_secret_redaction = __esm(() => {
|
|
|
15385
15503
|
SECRET_REDACTION_SCHEMA = ["todos", "secret_redaction", "v1"].join(".");
|
|
15386
15504
|
DEFAULT_PATTERNS = [
|
|
15387
15505
|
{ name: "openai_sk", pattern: /\bsk-[a-zA-Z0-9]{10,}\b/g },
|
|
15388
|
-
{ name: "github_pat", pattern: /\
|
|
15389
|
-
{ name: "github_oauth", pattern: /\
|
|
15506
|
+
{ name: "github_pat", pattern: /\bgh[p]_[a-zA-Z0-9]{20,}\b/g },
|
|
15507
|
+
{ name: "github_oauth", pattern: /\bgh[o]_[a-zA-Z0-9]{20,}\b/g },
|
|
15390
15508
|
{ name: "aws_access_key", pattern: /\bAKIA[0-9A-Z]{16}\b/g },
|
|
15391
15509
|
{ name: "bearer_token", pattern: /\bBearer\s+[a-zA-Z0-9\-._~+/]+=*\b/gi },
|
|
15392
15510
|
{ name: "jwt", pattern: /\beyJ[a-zA-Z0-9_-]+\.eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\b/g },
|
|
@@ -15398,7 +15516,7 @@ var init_secret_redaction = __esm(() => {
|
|
|
15398
15516
|
/example\.com/i,
|
|
15399
15517
|
/your-api-key-here/i,
|
|
15400
15518
|
/sk-test/i,
|
|
15401
|
-
/
|
|
15519
|
+
/gh[p]_xxx/i
|
|
15402
15520
|
];
|
|
15403
15521
|
customRedactors = [];
|
|
15404
15522
|
});
|
|
@@ -23137,11 +23255,14 @@ function registerTaskCommands(program2) {
|
|
|
23137
23255
|
const narrowsAfterQuery = Boolean(opts.dueToday) || Boolean(opts.overdue) || creatorFilterActive && cloud || taskListFilterActive;
|
|
23138
23256
|
const withholdLimit = requestedLimit !== undefined && (reordersAfterQuery || narrowsAfterQuery);
|
|
23139
23257
|
const scanCeiling = cloud && (withholdLimit || requestedLimit === undefined) ? Math.max(requestedLimit ?? 0, listScanLimit()) : undefined;
|
|
23258
|
+
const probeLimit = !withholdLimit && requestedLimit !== undefined ? requestedLimit + 1 : undefined;
|
|
23140
23259
|
const serverFilter = (() => {
|
|
23141
23260
|
const base = withholdLimit ? (() => {
|
|
23142
23261
|
const { limit: _dropped, ...rest } = filter;
|
|
23143
23262
|
return rest;
|
|
23144
23263
|
})() : filter;
|
|
23264
|
+
if (probeLimit !== undefined)
|
|
23265
|
+
return { ...base, limit: probeLimit };
|
|
23145
23266
|
return scanCeiling === undefined ? base : { ...base, limit: scanCeiling };
|
|
23146
23267
|
})();
|
|
23147
23268
|
let tasks = cloud ? await cloudListTasks(cloud, serverFilter) : listTasks(serverFilter);
|
|
@@ -23190,8 +23311,21 @@ function registerTaskCommands(program2) {
|
|
|
23190
23311
|
return 0;
|
|
23191
23312
|
});
|
|
23192
23313
|
}
|
|
23193
|
-
|
|
23194
|
-
|
|
23314
|
+
let truncatedByLimit = false;
|
|
23315
|
+
if (requestedLimit !== undefined) {
|
|
23316
|
+
if (withholdLimit) {
|
|
23317
|
+
truncatedByLimit = tasks.length > requestedLimit;
|
|
23318
|
+
tasks = tasks.slice(0, requestedLimit);
|
|
23319
|
+
} else if (tasks.length > requestedLimit) {
|
|
23320
|
+
truncatedByLimit = true;
|
|
23321
|
+
tasks = tasks.slice(0, requestedLimit);
|
|
23322
|
+
}
|
|
23323
|
+
}
|
|
23324
|
+
if (truncatedByLimit) {
|
|
23325
|
+
console.error(chalk3.yellow(`Warning: the matching set has more than --limit ${requestedLimit} rows, so only the first
|
|
23326
|
+
` + ` ${requestedLimit} are shown. This read is bounded \u2014 raise --limit or narrow the
|
|
23327
|
+
` + ` query (--project, --status, --assigned) to see the full population.`));
|
|
23328
|
+
}
|
|
23195
23329
|
if (rosterPromise && assignedFilter) {
|
|
23196
23330
|
try {
|
|
23197
23331
|
const roster = await rosterPromise;
|
|
@@ -25291,7 +25425,7 @@ function importSqliteTodosStorageSnapshot(snapshot, db) {
|
|
|
25291
25425
|
}
|
|
25292
25426
|
}
|
|
25293
25427
|
};
|
|
25294
|
-
applyRows("projects", "projects", PROJECT_COLUMNS, snapshot.projects, "updated_at");
|
|
25428
|
+
applyRows("projects", "projects", PROJECT_COLUMNS, orderProjectsParentFirst(snapshot.projects), "updated_at");
|
|
25295
25429
|
applyRows("project_machine_paths", "project_machine_paths", PROJECT_MACHINE_PATH_COLUMNS, snapshot.projectMachinePaths ?? [], "updated_at");
|
|
25296
25430
|
applyRows("agents", "agents", AGENT_COLUMNS, snapshot.agents, "last_seen_at");
|
|
25297
25431
|
applyRows("task_lists", "task_lists", TASK_LIST_COLUMNS, snapshot.taskLists, "updated_at");
|
|
@@ -25522,6 +25656,7 @@ var init_sqlite_snapshot = __esm(() => {
|
|
|
25522
25656
|
"task_list_id",
|
|
25523
25657
|
"task_prefix",
|
|
25524
25658
|
"task_counter",
|
|
25659
|
+
"parent_id",
|
|
25525
25660
|
"created_at",
|
|
25526
25661
|
"updated_at",
|
|
25527
25662
|
"machine_id",
|
|
@@ -28526,7 +28661,7 @@ function addSourceOnce(projectId, type, name, uri, metadata, db) {
|
|
|
28526
28661
|
function bootstrapProject(options = {}, db) {
|
|
28527
28662
|
const d = db || getDatabase();
|
|
28528
28663
|
const discovery = discoverProjectWorkspace(options.path);
|
|
28529
|
-
const taskListSlug = options.taskListSlug ||
|
|
28664
|
+
const taskListSlug = options.taskListSlug || slugify(options.name || discovery.projectName);
|
|
28530
28665
|
if (options.dryRun) {
|
|
28531
28666
|
return {
|
|
28532
28667
|
dryRun: true,
|
|
@@ -30896,7 +31031,7 @@ function registerProjectCommands(program2) {
|
|
|
30896
31031
|
}
|
|
30897
31032
|
}
|
|
30898
31033
|
});
|
|
30899
|
-
program2.command("projects").description("List and manage projects").option("--add <path>", "Register a project by path").option("--show <project>", "Resolve and show a project").option("--update <project>", "Update a project's name, path, or description").option("--deregister <project>", "Deregister a project without deleting its tasks; refuses projects with incomplete tasks").option("--path-prefix <prefix>", "Require deregistered project path to start with this prefix").option("--dry-run", "Show what would change without modifying local state").option("--name <name>", "Project name (with --add)").option("--path <path>", "Project path (with --update)").option("--description <text>", "Project description (with --add or --update)").option("--task-list-id <id>", "Custom task list ID (with --add)").option("--ensure-task-list <project>", "Plan or apply creation of an existing project's declared task list").option("--rollback-task-list <project>", "Conditionally roll back a task list created by --ensure-task-list").option("--apply", "Apply --ensure-task-list or --rollback-task-list; ensure plans by default").option("--idempotency-key <key>", "Stable idempotency key for --ensure-task-list --apply").option("--receipt <id>", "Accepted ensure receipt for --rollback-task-list --apply").action(async (opts) => {
|
|
31034
|
+
program2.command("projects").description("List and manage projects").option("--add <path>", "Register a project by path").option("--show <project>", "Resolve and show a project").option("--update <project>", "Update a project's name, path, or description").option("--deregister <project>", "Deregister a project without deleting its tasks; refuses projects with incomplete tasks").option("--path-prefix <prefix>", "Require deregistered project path to start with this prefix").option("--dry-run", "Show what would change without modifying local state").option("--name <name>", "Project name (with --add)").option("--path <path>", "Project path (with --update)").option("--description <text>", "Project description (with --add or --update)").option("--task-list-id <id>", "Custom task list ID (with --add)").option("--parent <project>", "Parent project (id, path, task-list slug, or name) to create a sub-project under (with --add)").option("--ensure-task-list <project>", "Plan or apply creation of an existing project's declared task list").option("--rollback-task-list <project>", "Conditionally roll back a task list created by --ensure-task-list").option("--apply", "Apply --ensure-task-list or --rollback-task-list; ensure plans by default").option("--idempotency-key <key>", "Stable idempotency key for --ensure-task-list --apply").option("--receipt <id>", "Accepted ensure receipt for --rollback-task-list --apply").action(async (opts) => {
|
|
30900
31035
|
const globalOpts = program2.opts();
|
|
30901
31036
|
const cloud = getTodosCloudClient();
|
|
30902
31037
|
if (opts.ensureTaskList && opts.rollbackTaskList) {
|
|
@@ -31013,10 +31148,21 @@ function registerProjectCommands(program2) {
|
|
|
31013
31148
|
if (opts.add) {
|
|
31014
31149
|
const projectPath = resolve13(opts.add);
|
|
31015
31150
|
const name = opts.name || basename5(projectPath);
|
|
31151
|
+
let parentId;
|
|
31152
|
+
if (opts.parent !== undefined) {
|
|
31153
|
+
const parent = cloud ? await cloudResolveProject(cloud, opts.parent) : resolveExplicitProject(opts.parent);
|
|
31154
|
+
parentId = parent.id;
|
|
31155
|
+
}
|
|
31016
31156
|
const existing = cloud ? (await cloudListProjects(cloud)).find((project2) => project2.path === projectPath) : getProjectByPath(projectPath);
|
|
31017
31157
|
let project;
|
|
31018
31158
|
if (existing) {
|
|
31019
31159
|
project = existing;
|
|
31160
|
+
if (opts.parent !== undefined && existing.parent_id !== parentId) {
|
|
31161
|
+
if (cloud)
|
|
31162
|
+
project = await cloudUpdateProject(cloud, existing.id, { parent_id: parentId });
|
|
31163
|
+
else
|
|
31164
|
+
project = updateProject(existing.id, { parent_id: parentId });
|
|
31165
|
+
}
|
|
31020
31166
|
if (opts.taskListId) {
|
|
31021
31167
|
if (cloud && existing.task_list_id !== opts.taskListId) {
|
|
31022
31168
|
handleError(new Error("Remote project task-list slug changes require project-rename"));
|
|
@@ -31025,7 +31171,7 @@ function registerProjectCommands(program2) {
|
|
|
31025
31171
|
project = renameProject(existing.id, { new_slug: opts.taskListId }).project;
|
|
31026
31172
|
}
|
|
31027
31173
|
} else {
|
|
31028
|
-
const input = { name, path: projectPath, description: opts.description, task_list_id: opts.taskListId };
|
|
31174
|
+
const input = { name, path: projectPath, description: opts.description, task_list_id: opts.taskListId, parent_id: parentId };
|
|
31029
31175
|
project = cloud ? await cloudCreateProject(cloud, input) : createProject(input);
|
|
31030
31176
|
}
|
|
31031
31177
|
if (!cloud) {
|
|
@@ -31042,6 +31188,8 @@ function registerProjectCommands(program2) {
|
|
|
31042
31188
|
console.log(chalk5.green(`Project registered: ${project.name} (${project.path})`));
|
|
31043
31189
|
if (project.task_list_id)
|
|
31044
31190
|
console.log(chalk5.dim(` Task list: ${project.task_list_id}`));
|
|
31191
|
+
if (parentId)
|
|
31192
|
+
console.log(chalk5.dim(` Parent: ${parentId}`));
|
|
31045
31193
|
}
|
|
31046
31194
|
return;
|
|
31047
31195
|
}
|
|
@@ -32533,8 +32681,12 @@ class PostgresJsonRecordStore {
|
|
|
32533
32681
|
OR ($11::text <> $2
|
|
32534
32682
|
AND NOT EXISTS (SELECT 1 FROM parent_chain WHERE object_id = $2)
|
|
32535
32683
|
AND NOT EXISTS (SELECT 1 FROM parent_chain WHERE cycle))) AS parent_acyclic,
|
|
32536
|
-
(SELECT
|
|
32537
|
-
|
|
32684
|
+
(COALESCE((SELECT payload->>'plan_id' FROM locked_task), '')
|
|
32685
|
+
IS DISTINCT FROM COALESCE($3::jsonb->>'plan_id', '')) AS membership_changed,
|
|
32686
|
+
(NOT (COALESCE((SELECT payload->>'plan_id' FROM locked_task), '')
|
|
32687
|
+
IS DISTINCT FROM COALESCE($3::jsonb->>'plan_id', ''))
|
|
32688
|
+
OR $8::text IS NULL
|
|
32689
|
+
OR EXISTS (SELECT 1 FROM locked_plans WHERE object_id = $8)) AS target_plan_found,
|
|
32538
32690
|
(SELECT payload->>'project_id' FROM locked_plans WHERE object_id = $8) AS target_project_id
|
|
32539
32691
|
), guarded AS (
|
|
32540
32692
|
SELECT
|
|
@@ -32557,7 +32709,6 @@ class PostgresJsonRecordStore {
|
|
|
32557
32709
|
AND guarded.version_matches
|
|
32558
32710
|
AND guarded.parent_found
|
|
32559
32711
|
AND guarded.parent_acyclic
|
|
32560
|
-
AND guarded.all_plans_found
|
|
32561
32712
|
AND guarded.target_plan_found
|
|
32562
32713
|
AND NOT guarded.project_conflict
|
|
32563
32714
|
ON CONFLICT (service, object_type, object_id) DO UPDATE SET
|
|
@@ -32573,7 +32724,7 @@ class PostgresJsonRecordStore {
|
|
|
32573
32724
|
RETURNING payload
|
|
32574
32725
|
)
|
|
32575
32726
|
SELECT guarded.task_found, guarded.version_matches, guarded.parent_found, guarded.parent_acyclic,
|
|
32576
|
-
guarded.
|
|
32727
|
+
guarded.membership_changed, guarded.target_plan_found, guarded.project_conflict,
|
|
32577
32728
|
(SELECT payload FROM stored) AS payload,
|
|
32578
32729
|
(SELECT payload FROM locked_task) AS current_payload
|
|
32579
32730
|
FROM guarded`, [
|
|
@@ -32605,7 +32756,7 @@ class PostgresJsonRecordStore {
|
|
|
32605
32756
|
if (parentGuard && !row?.parent_acyclic && parentGuard.parentId) {
|
|
32606
32757
|
throw new ResourceConflictError("TASK_PARENT_CYCLE", `TASK_PARENT_CYCLE: assigning parent ${parentGuard.parentId} to task ${value.id} would create or retain a parent cycle`);
|
|
32607
32758
|
}
|
|
32608
|
-
if (!row?.
|
|
32759
|
+
if (!row?.target_plan_found) {
|
|
32609
32760
|
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 });
|
|
32610
32761
|
}
|
|
32611
32762
|
if (row.project_conflict) {
|
|
@@ -33814,17 +33965,26 @@ async function getChangedSince(since, filters, store) {
|
|
|
33814
33965
|
async function createProject2(input, store, context) {
|
|
33815
33966
|
const timestamp2 = new Date().toISOString();
|
|
33816
33967
|
const derivedSlug = slugifyRaw(input.name);
|
|
33817
|
-
const taskListId = input.task_list_id === undefined ?
|
|
33968
|
+
const taskListId = input.task_list_id === undefined ? derivedSlug : slugifyRaw(input.task_list_id);
|
|
33818
33969
|
if (!derivedSlug || !taskListId)
|
|
33819
33970
|
throw new Error("Project name and task-list slug must be non-empty");
|
|
33971
|
+
const parentId = input.parent_id ?? null;
|
|
33972
|
+
const id = randomUUID4();
|
|
33973
|
+
if (parentId !== null) {
|
|
33974
|
+
const parent = await store.get("projects", parentId);
|
|
33975
|
+
if (!parent)
|
|
33976
|
+
throw new ProjectNotFoundError(parentId);
|
|
33977
|
+
await assertNotProjectAncestorPostgres(id, parentId, store);
|
|
33978
|
+
}
|
|
33820
33979
|
const project = {
|
|
33821
|
-
id
|
|
33980
|
+
id,
|
|
33822
33981
|
name: input.name,
|
|
33823
33982
|
path: input.path,
|
|
33824
33983
|
description: input.description ?? null,
|
|
33825
33984
|
task_list_id: taskListId,
|
|
33826
33985
|
task_prefix: input.task_prefix ?? await generateProjectPrefix(input.name, store),
|
|
33827
33986
|
task_counter: 0,
|
|
33987
|
+
parent_id: parentId,
|
|
33828
33988
|
created_at: timestamp2,
|
|
33829
33989
|
updated_at: timestamp2,
|
|
33830
33990
|
machine_id: store.machineId(context),
|
|
@@ -33832,12 +33992,38 @@ async function createProject2(input, store, context) {
|
|
|
33832
33992
|
};
|
|
33833
33993
|
return store.upsert("projects", project, context);
|
|
33834
33994
|
}
|
|
33995
|
+
async function assertNotProjectAncestorPostgres(projectId, candidateParentId, store) {
|
|
33996
|
+
const all = await store.list("projects");
|
|
33997
|
+
const byId = new Map(all.map((project) => [project.id, project.parent_id ?? null]));
|
|
33998
|
+
let cursor = candidateParentId;
|
|
33999
|
+
const seen = new Set;
|
|
34000
|
+
while (cursor !== null) {
|
|
34001
|
+
if (cursor === projectId) {
|
|
34002
|
+
throw new ResourceConflictError("PROJECT_PARENT_CYCLE", `Project "${projectId}" cannot be placed under its own descendant`);
|
|
34003
|
+
}
|
|
34004
|
+
if (seen.has(cursor))
|
|
34005
|
+
break;
|
|
34006
|
+
seen.add(cursor);
|
|
34007
|
+
cursor = byId.get(cursor) ?? null;
|
|
34008
|
+
}
|
|
34009
|
+
}
|
|
33835
34010
|
async function updateProject2(id, input, store) {
|
|
33836
34011
|
if ("task_list_id" in input) {
|
|
33837
34012
|
throw new Error("task_list_id cannot be changed by updateProject; use renameProject for an atomic canonical rename");
|
|
33838
34013
|
}
|
|
33839
34014
|
const project = await requireRecord("projects", id, store);
|
|
33840
|
-
|
|
34015
|
+
if (input.parent_id !== undefined && input.parent_id !== null) {
|
|
34016
|
+
const parent = await store.get("projects", input.parent_id);
|
|
34017
|
+
if (!parent)
|
|
34018
|
+
throw new ProjectNotFoundError(input.parent_id);
|
|
34019
|
+
await assertNotProjectAncestorPostgres(id, input.parent_id, store);
|
|
34020
|
+
}
|
|
34021
|
+
const updated = {
|
|
34022
|
+
...project,
|
|
34023
|
+
...definedPatch(input),
|
|
34024
|
+
...input.parent_id !== undefined ? { parent_id: input.parent_id } : {},
|
|
34025
|
+
updated_at: new Date().toISOString()
|
|
34026
|
+
};
|
|
33841
34027
|
return store.upsert("projects", updated);
|
|
33842
34028
|
}
|
|
33843
34029
|
async function createPlan2(input, store, context) {
|
|
@@ -35229,7 +35415,7 @@ class StagedSqliteTodosProjectRegistrationTransaction {
|
|
|
35229
35415
|
}
|
|
35230
35416
|
async createProject(input) {
|
|
35231
35417
|
const derivedSlug = normalizeSlug(input.name);
|
|
35232
|
-
const taskListId = input.task_list_id === undefined ?
|
|
35418
|
+
const taskListId = input.task_list_id === undefined ? derivedSlug : normalizeSlug(input.task_list_id);
|
|
35233
35419
|
if (!derivedSlug || !taskListId) {
|
|
35234
35420
|
throw new Error("Project name and task-list slug must be non-empty");
|
|
35235
35421
|
}
|
|
@@ -35241,6 +35427,7 @@ class StagedSqliteTodosProjectRegistrationTransaction {
|
|
|
35241
35427
|
task_list_id: taskListId,
|
|
35242
35428
|
task_prefix: input.task_prefix ?? this.availableProjectPrefix(input.name),
|
|
35243
35429
|
task_counter: 0,
|
|
35430
|
+
parent_id: input.parent_id ?? null,
|
|
35244
35431
|
created_at: now(),
|
|
35245
35432
|
updated_at: now(),
|
|
35246
35433
|
machine_id: currentStorageMachineId(this.db)
|
|
@@ -35251,14 +35438,15 @@ class StagedSqliteTodosProjectRegistrationTransaction {
|
|
|
35251
35438
|
try {
|
|
35252
35439
|
const result = this.db.run(`INSERT INTO projects (
|
|
35253
35440
|
id, name, path, description, task_list_id, task_prefix,
|
|
35254
|
-
task_counter, created_at, updated_at, machine_id
|
|
35255
|
-
) VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?)`, [
|
|
35441
|
+
task_counter, parent_id, created_at, updated_at, machine_id
|
|
35442
|
+
) VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?)`, [
|
|
35256
35443
|
project.id,
|
|
35257
35444
|
project.name,
|
|
35258
35445
|
project.path,
|
|
35259
35446
|
project.description,
|
|
35260
35447
|
project.task_list_id,
|
|
35261
35448
|
project.task_prefix,
|
|
35449
|
+
project.parent_id,
|
|
35262
35450
|
project.created_at,
|
|
35263
35451
|
project.updated_at,
|
|
35264
35452
|
project.machine_id ?? null
|
|
@@ -35655,7 +35843,10 @@ function taskListSlug(projectSlug) {
|
|
|
35655
35843
|
if (!slug || slug !== projectSlug) {
|
|
35656
35844
|
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "project_slug must be canonical kebab-case");
|
|
35657
35845
|
}
|
|
35658
|
-
return
|
|
35846
|
+
return slug;
|
|
35847
|
+
}
|
|
35848
|
+
function legacyTaskListSlug(projectSlug) {
|
|
35849
|
+
return `todos-${normalizeSlug(projectSlug)}`;
|
|
35659
35850
|
}
|
|
35660
35851
|
function deterministicTaskPrefix(projectSlug) {
|
|
35661
35852
|
const letters = projectSlug.replace(/[^a-z0-9]/gi, "").toUpperCase();
|
|
@@ -36174,6 +36365,12 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
36174
36365
|
created_by_operation: false
|
|
36175
36366
|
};
|
|
36176
36367
|
}
|
|
36368
|
+
if (request.bind_existing === true && conflict2.path === path && conflict2.task_list_id === legacyTaskListSlug(request.project_slug)) {
|
|
36369
|
+
return {
|
|
36370
|
+
record: boundExistingProjectRecord(conflict2),
|
|
36371
|
+
created_by_operation: false
|
|
36372
|
+
};
|
|
36373
|
+
}
|
|
36177
36374
|
return this.terminalFor(transaction, request, normalizedCallDigest(request), "target_already_exists", { targetId: conflict2.id });
|
|
36178
36375
|
}
|
|
36179
36376
|
await this.fault("before_object_write", request);
|
|
@@ -36210,6 +36407,15 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
36210
36407
|
}
|
|
36211
36408
|
return this.terminalFor(transaction, request, normalizedCallDigest(request), "target_already_exists", { targetId: conflict.id });
|
|
36212
36409
|
}
|
|
36410
|
+
if (request.bind_existing === true) {
|
|
36411
|
+
const legacy = await transaction.findTaskListConflict(todosProjectId, legacyTaskListSlug(request.project_slug));
|
|
36412
|
+
if (legacy && legacy.project_id === todosProjectId) {
|
|
36413
|
+
return {
|
|
36414
|
+
record: boundExistingTaskListRecord(legacy),
|
|
36415
|
+
created_by_operation: false
|
|
36416
|
+
};
|
|
36417
|
+
}
|
|
36418
|
+
}
|
|
36213
36419
|
await this.fault("before_object_write", request);
|
|
36214
36420
|
const taskList = await transaction.createTaskList({
|
|
36215
36421
|
name: request.project_name,
|
|
@@ -51899,7 +52105,8 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
51899
52105
|
path: { type: "string", minLength: 1 },
|
|
51900
52106
|
description: { type: "string" },
|
|
51901
52107
|
task_list_id: { type: "string", minLength: 1, pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$" },
|
|
51902
|
-
task_prefix: { type: "string", minLength: 1 }
|
|
52108
|
+
task_prefix: { type: "string", minLength: 1 },
|
|
52109
|
+
parent_id: { type: "string", minLength: 1 }
|
|
51903
52110
|
}
|
|
51904
52111
|
},
|
|
51905
52112
|
UpdateProjectInput: {
|
|
@@ -51909,7 +52116,8 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
51909
52116
|
properties: {
|
|
51910
52117
|
name: { type: "string", minLength: 1 },
|
|
51911
52118
|
path: { type: "string", minLength: 1 },
|
|
51912
|
-
description: { type: "string", nullable: true }
|
|
52119
|
+
description: { type: "string", nullable: true },
|
|
52120
|
+
parent_id: { type: "string", minLength: 1, nullable: true }
|
|
51913
52121
|
}
|
|
51914
52122
|
},
|
|
51915
52123
|
RenameProjectInput: {
|
|
@@ -54208,6 +54416,7 @@ var init_openapi = __esm(() => {
|
|
|
54208
54416
|
task_list_id: { type: "string", nullable: true },
|
|
54209
54417
|
task_prefix: { type: "string", nullable: true },
|
|
54210
54418
|
task_counter: { type: "number" },
|
|
54419
|
+
parent_id: { type: "string", nullable: true },
|
|
54211
54420
|
created_at: { type: "string" },
|
|
54212
54421
|
updated_at: { type: "string" }
|
|
54213
54422
|
}
|
|
@@ -55278,7 +55487,7 @@ function validateProjectPatch(value) {
|
|
|
55278
55487
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
55279
55488
|
return { ok: false, message: "project patch must be an object" };
|
|
55280
55489
|
const body2 = value;
|
|
55281
|
-
const allowed = new Set(["name", "path", "description"]);
|
|
55490
|
+
const allowed = new Set(["name", "path", "description", "parent_id"]);
|
|
55282
55491
|
const unknown = Object.keys(body2).find((key2) => !allowed.has(key2));
|
|
55283
55492
|
if (unknown)
|
|
55284
55493
|
return { ok: false, message: `unknown project field: ${unknown}` };
|
|
@@ -55290,13 +55499,15 @@ function validateProjectPatch(value) {
|
|
|
55290
55499
|
return { ok: false, message: "path must be a non-empty string" };
|
|
55291
55500
|
if (body2["description"] !== undefined && body2["description"] !== null && typeof body2["description"] !== "string")
|
|
55292
55501
|
return { ok: false, message: "description must be a string or null" };
|
|
55502
|
+
if (body2["parent_id"] !== undefined && body2["parent_id"] !== null && (typeof body2["parent_id"] !== "string" || !body2["parent_id"].trim()))
|
|
55503
|
+
return { ok: false, message: "parent_id must be a string or null" };
|
|
55293
55504
|
return { ok: true, patch: body2 };
|
|
55294
55505
|
}
|
|
55295
55506
|
function validateProjectCreate(value) {
|
|
55296
55507
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
55297
55508
|
return { ok: false, message: "project body must be an object" };
|
|
55298
55509
|
const body2 = value;
|
|
55299
|
-
const allowed = new Set(["name", "path", "description", "task_list_id", "task_prefix"]);
|
|
55510
|
+
const allowed = new Set(["name", "path", "description", "task_list_id", "task_prefix", "parent_id"]);
|
|
55300
55511
|
const unknown = Object.keys(body2).find((key2) => !allowed.has(key2));
|
|
55301
55512
|
if (unknown)
|
|
55302
55513
|
return { ok: false, message: `unknown project field: ${unknown}` };
|
|
@@ -55314,6 +55525,9 @@ function validateProjectCreate(value) {
|
|
|
55314
55525
|
if (body2["task_prefix"] !== undefined && (typeof body2["task_prefix"] !== "string" || !body2["task_prefix"].trim())) {
|
|
55315
55526
|
return { ok: false, message: "task_prefix must be a non-empty string" };
|
|
55316
55527
|
}
|
|
55528
|
+
if (body2["parent_id"] !== undefined && (typeof body2["parent_id"] !== "string" || !body2["parent_id"].trim())) {
|
|
55529
|
+
return { ok: false, message: "parent_id must be a non-empty string" };
|
|
55530
|
+
}
|
|
55317
55531
|
return { ok: true, input: body2 };
|
|
55318
55532
|
}
|
|
55319
55533
|
function validatePlanCreate(value) {
|
|
@@ -62029,6 +62243,7 @@ function registerTaskProjectTools(server, ctx) {
|
|
|
62029
62243
|
name: exports_external.string().describe("Project name"),
|
|
62030
62244
|
path: exports_external.string().describe("Unique filesystem path for the project"),
|
|
62031
62245
|
description: exports_external.string().optional(),
|
|
62246
|
+
parent_id: exports_external.string().optional().describe("Optional parent project id to create this as a sub-project"),
|
|
62032
62247
|
status: exports_external.enum(["active", "completed", "on_hold", "archived"]).optional(),
|
|
62033
62248
|
short_id: exports_external.string().nullable().optional().describe("Short ID (auto-generated if omitted)"),
|
|
62034
62249
|
metadata: exports_external.record(exports_external.unknown()).optional()
|
|
@@ -69579,6 +69794,7 @@ function createAgentProjectDemoBundle() {
|
|
|
69579
69794
|
task_list_id: ids.list,
|
|
69580
69795
|
task_prefix: "DEMO",
|
|
69581
69796
|
task_counter: 4,
|
|
69797
|
+
parent_id: null,
|
|
69582
69798
|
created_at: createdAt,
|
|
69583
69799
|
updated_at: completedAt
|
|
69584
69800
|
});
|
|
@@ -82446,7 +82662,7 @@ No task claimed (nothing available).`));
|
|
|
82446
82662
|
verdict_exit_code: verdict2.verdict,
|
|
82447
82663
|
fail_on_findings: verdict2.fail_on_findings,
|
|
82448
82664
|
dry_run: true,
|
|
82449
|
-
mode: "
|
|
82665
|
+
mode: "http",
|
|
82450
82666
|
authority: { v1_base_url: cloud.baseUrl, local_fallback: false },
|
|
82451
82667
|
routes: {
|
|
82452
82668
|
stats: true,
|
|
@@ -82668,7 +82884,7 @@ Findings`));
|
|
|
82668
82884
|
checks.push({ name: "Version", ok: true, message: `v${getPackageVersion3()} \xB7 remote HTTP client` });
|
|
82669
82885
|
const ok = checks.every((check) => check.ok);
|
|
82670
82886
|
if (opts.json || globalOpts.json) {
|
|
82671
|
-
console.log(JSON.stringify({ ok, mode: "
|
|
82887
|
+
console.log(JSON.stringify({ ok, mode: "http", checks }));
|
|
82672
82888
|
} else {
|
|
82673
82889
|
console.log(chalk9.bold(`todos health
|
|
82674
82890
|
`));
|
|
@@ -89328,6 +89544,7 @@ var init_json_contracts = __esm(() => {
|
|
|
89328
89544
|
task_list_id: nullableIdField,
|
|
89329
89545
|
task_prefix: field(["string", "null"], "Optional task prefix.", true),
|
|
89330
89546
|
task_counter: field("integer", "Monotonic project task counter."),
|
|
89547
|
+
parent_id: field(["string", "null"], "Optional parent project id; null means top-level.", true),
|
|
89331
89548
|
created_at: isoDateField,
|
|
89332
89549
|
updated_at: isoDateField
|
|
89333
89550
|
},
|
|
@@ -93394,7 +93611,7 @@ function getNativeStorageStatus(env = process.env) {
|
|
|
93394
93611
|
const remoteEnabled = isTodosPostgresBackend(config);
|
|
93395
93612
|
const remoteFieldsConfigured = Boolean(config.database || config.objectStorage);
|
|
93396
93613
|
if (!remoteEnabled && remoteFieldsConfigured) {
|
|
93397
|
-
warnings.push(`the sqlite backend
|
|
93614
|
+
warnings.push(`the sqlite backend ignores configured remote storage fields`);
|
|
93398
93615
|
}
|
|
93399
93616
|
if (remoteEnabled && !config.objectStorage) {
|
|
93400
93617
|
warnings.push(`${TODOS_STORAGE_ENV.s3Bucket} is not configured, so artifact sync will stay local`);
|
|
@@ -93487,7 +93704,7 @@ function storageEnvStatus(env) {
|
|
|
93487
93704
|
function fallbackConfig(env) {
|
|
93488
93705
|
let mode;
|
|
93489
93706
|
try {
|
|
93490
|
-
mode =
|
|
93707
|
+
mode = getTodosStorageBackend(env);
|
|
93491
93708
|
} catch {
|
|
93492
93709
|
mode = "sqlite";
|
|
93493
93710
|
}
|