@hasna/todos 0.15.52 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1884 -0
- package/README.md +314 -37
- package/dist/ai-tools.d.ts.map +1 -1
- package/dist/ai.d.ts.map +1 -1
- package/dist/cli/cloud-router.d.ts +99 -51
- package/dist/cli/cloud-router.d.ts.map +1 -1
- package/dist/cli/commands/agent-commands.d.ts.map +1 -1
- package/dist/cli/commands/config-serve-commands.d.ts.map +1 -1
- package/dist/cli/commands/delegate.d.ts.map +1 -1
- package/dist/cli/commands/environment-snapshots.d.ts.map +1 -1
- package/dist/cli/commands/machines-api.d.ts +4 -0
- package/dist/cli/commands/machines-api.d.ts.map +1 -0
- package/dist/cli/commands/machines.d.ts.map +1 -1
- package/dist/cli/commands/mcp-hooks-commands.d.ts.map +1 -1
- package/dist/cli/commands/plan-commands.d.ts +3 -0
- package/dist/cli/commands/plan-commands.d.ts.map +1 -0
- package/dist/cli/commands/plan-template-commands.d.ts.map +1 -1
- package/dist/cli/commands/pr-group-commands.d.ts.map +1 -1
- package/dist/cli/commands/project-commands.d.ts.map +1 -1
- package/dist/cli/commands/query-commands.d.ts.map +1 -1
- package/dist/cli/commands/storage-commands.d.ts.map +1 -1
- package/dist/cli/commands/task-list-commands.d.ts +3 -0
- package/dist/cli/commands/task-list-commands.d.ts.map +1 -0
- package/dist/cli/commands/template-commands.d.ts +3 -0
- package/dist/cli/commands/template-commands.d.ts.map +1 -0
- package/dist/cli/helpers.d.ts.map +1 -1
- package/dist/cli/index.js +15128 -11093
- package/dist/cli/machine-api.d.ts +7 -0
- package/dist/cli/machine-api.d.ts.map +1 -0
- package/dist/cli/plan-read-api.d.ts +6 -0
- package/dist/cli/plan-read-api.d.ts.map +1 -0
- package/dist/cli/project-api.d.ts +119 -0
- package/dist/cli/project-api.d.ts.map +1 -0
- package/dist/cli/stage-a.d.ts +34 -4
- package/dist/cli/stage-a.d.ts.map +1 -1
- package/dist/cli/task-query-api.d.ts +21 -0
- package/dist/cli/task-query-api.d.ts.map +1 -0
- package/dist/cli/template-api.d.ts +7 -0
- package/dist/cli/template-api.d.ts.map +1 -0
- package/dist/contracts.js +835 -265
- package/dist/db/builtin-templates.d.ts +2 -33
- package/dist/db/builtin-templates.d.ts.map +1 -1
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/handoffs.d.ts.map +1 -1
- package/dist/db/machines.d.ts.map +1 -1
- package/dist/db/migrations.d.ts.map +1 -1
- package/dist/db/plans.d.ts.map +1 -1
- package/dist/db/retrospectives.d.ts.map +1 -1
- package/dist/db/schema.d.ts.map +1 -1
- package/dist/db/task-lists.d.ts.map +1 -1
- package/dist/db/task-runs.d.ts +2 -0
- package/dist/db/task-runs.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3862 -1715
- package/dist/lib/assignee-validation.d.ts.map +1 -1
- package/dist/lib/builtin-template-library.d.ts +32 -0
- package/dist/lib/builtin-template-library.d.ts.map +1 -0
- package/dist/lib/cli-reference.d.ts.map +1 -1
- package/dist/lib/config.d.ts +10 -13
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/db-backup.d.ts +1 -1
- package/dist/lib/db-backup.d.ts.map +1 -1
- package/dist/lib/env.d.ts +18 -0
- package/dist/lib/env.d.ts.map +1 -0
- package/dist/lib/environment-snapshots.d.ts.map +1 -1
- package/dist/lib/gatherer.d.ts.map +1 -1
- package/dist/lib/headless-boundaries.d.ts +4 -3
- package/dist/lib/headless-boundaries.d.ts.map +1 -1
- package/dist/lib/local-encryption.d.ts +12 -0
- package/dist/lib/local-encryption.d.ts.map +1 -1
- package/dist/lib/local-opt-in.d.ts +90 -0
- package/dist/lib/local-opt-in.d.ts.map +1 -0
- package/dist/lib/paths.d.ts +9 -0
- package/dist/lib/paths.d.ts.map +1 -1
- package/dist/lib/plan-artifacts.d.ts +1 -0
- package/dist/lib/plan-artifacts.d.ts.map +1 -1
- package/dist/lib/plan-client-boundary.d.ts +2 -0
- package/dist/lib/plan-client-boundary.d.ts.map +1 -0
- package/dist/lib/plan-schedule.d.ts +6 -0
- package/dist/lib/plan-schedule.d.ts.map +1 -0
- package/dist/lib/project-panel.d.ts +6 -0
- package/dist/lib/project-panel.d.ts.map +1 -1
- package/dist/lib/project-receipt.d.ts +3 -0
- package/dist/lib/project-receipt.d.ts.map +1 -0
- package/dist/lib/sandbox-profiles.d.ts.map +1 -1
- package/dist/lib/shared-plan-artifacts.d.ts +14 -0
- package/dist/lib/shared-plan-artifacts.d.ts.map +1 -0
- package/dist/lib/sync.d.ts.map +1 -1
- package/dist/lib/task-list-client-boundary.d.ts +3 -0
- package/dist/lib/task-list-client-boundary.d.ts.map +1 -0
- package/dist/lib/task-routing.d.ts.map +1 -1
- package/dist/lib/template-client-boundary.d.ts +2 -0
- package/dist/lib/template-client-boundary.d.ts.map +1 -0
- package/dist/mcp/index.d.ts +1 -0
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +45271 -40452
- package/dist/mcp/plan-api.d.ts +6 -0
- package/dist/mcp/plan-api.d.ts.map +1 -0
- package/dist/mcp/remote-authority.d.ts +49 -0
- package/dist/mcp/remote-authority.d.ts.map +1 -0
- package/dist/mcp/task-coordination-api.d.ts +15 -0
- package/dist/mcp/task-coordination-api.d.ts.map +1 -0
- package/dist/mcp/task-list-api.d.ts +8 -0
- package/dist/mcp/task-list-api.d.ts.map +1 -0
- package/dist/mcp/token-utils.d.ts.map +1 -1
- package/dist/mcp/tools/environment-snapshots.d.ts.map +1 -1
- package/dist/mcp/tools/machines.d.ts.map +1 -1
- package/dist/mcp/tools/task-project-tools.d.ts.map +1 -1
- package/dist/mcp/tools/task-resources.d.ts.map +1 -1
- package/dist/mcp.d.ts +2 -2
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +44 -18
- package/dist/project-registration.js +2829 -556
- package/dist/registry.js +836 -266
- package/dist/release-provenance.json +6 -6
- package/dist/sdk/client.d.ts +51 -4
- package/dist/sdk/client.d.ts.map +1 -1
- package/dist/sdk/index.d.ts +2 -0
- package/dist/sdk/index.d.ts.map +1 -1
- package/dist/sdk/index.js +1209 -1080
- package/dist/sdk/resolve.d.ts +91 -0
- package/dist/sdk/resolve.d.ts.map +1 -0
- package/dist/sdk/v1.generated.d.ts +219 -3
- package/dist/sdk/v1.generated.d.ts.map +1 -1
- package/dist/server/auth-posture.d.ts +66 -7
- package/dist/server/auth-posture.d.ts.map +1 -1
- package/dist/server/client-ip.d.ts +39 -0
- package/dist/server/client-ip.d.ts.map +1 -0
- package/dist/server/cloud.d.ts +2 -1
- package/dist/server/cloud.d.ts.map +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +20556 -15541
- package/dist/server/openapi.d.ts +1341 -83
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/port.d.ts +2 -2
- package/dist/server/port.d.ts.map +1 -1
- package/dist/server/routes.d.ts +3 -6
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/serve.d.ts +2 -6
- package/dist/server/serve.d.ts.map +1 -1
- package/dist/server/v1.d.ts +3 -1
- package/dist/server/v1.d.ts.map +1 -1
- package/dist/storage/atomic-project-migration.d.ts +53 -0
- package/dist/storage/atomic-project-migration.d.ts.map +1 -0
- package/dist/storage/index.d.ts +3 -1
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/interfaces.d.ts +61 -0
- package/dist/storage/interfaces.d.ts.map +1 -1
- package/dist/storage/machine-registry.d.ts +36 -0
- package/dist/storage/machine-registry.d.ts.map +1 -0
- package/dist/storage/postgres-adapter.d.ts.map +1 -1
- package/dist/storage/postgres-sync.d.ts +2 -2
- package/dist/storage/postgres-sync.d.ts.map +1 -1
- package/dist/storage/run-artifact-creation-upload.d.ts +50 -0
- package/dist/storage/run-artifact-creation-upload.d.ts.map +1 -0
- package/dist/storage/s3-artifact-sync.d.ts +18 -0
- package/dist/storage/s3-artifact-sync.d.ts.map +1 -1
- package/dist/storage/sqlite-snapshot.d.ts +2 -0
- package/dist/storage/sqlite-snapshot.d.ts.map +1 -1
- package/dist/storage.d.ts +2 -2
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +2695 -322
- package/dist/task-manifest/postgres.d.ts +1 -1
- package/dist/task-manifest/sqlite.d.ts +1 -1
- package/dist/task-manifest.js +244 -73
- package/dist/task-subtree-transfer.js +198 -4
- package/dist/testing.d.ts +90 -29
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +34 -10
- package/dist/types/index.d.ts +40 -4
- package/dist/types/index.d.ts.map +1 -1
- package/docs/PLAN_API.md +93 -0
- package/docs/TASK_LIST_API.md +58 -0
- package/docs/TASK_QUERY_API.md +14 -0
- package/docs/TEMPLATE_API.md +18 -0
- package/docs/native-storage.md +405 -0
- package/package.json +18 -15
- package/postinstall.js +52 -1
- package/dashboard/dist/assets/index-BNQ4gJua.js +0 -342
- package/dashboard/dist/assets/index-DjzvHUWt.css +0 -1
- package/dashboard/dist/index.html +0 -13
- package/dashboard/dist/logo.jpg +0 -0
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
# Native Storage Boundary
|
|
2
|
+
|
|
3
|
+
`@hasna/todos` stays local-first where it is a local product. `todos-serve`, the
|
|
4
|
+
native storage tooling, and the plan-artifact files run on local SQLite and
|
|
5
|
+
local artifact files without network access, hosted credentials, SaaS accounts,
|
|
6
|
+
or a shared cloud runtime package.
|
|
7
|
+
|
|
8
|
+
The MCP server and the CLI are NOT wholly local. Their plan and task-list
|
|
9
|
+
command families — and the CLI's stateful template commands — are served by the
|
|
10
|
+
authenticated shared API, and the MCP tools that still read the on-box store are
|
|
11
|
+
served only under the explicit local opt-in (`HASNA_TODOS_LOCAL=1`) — see "MCP
|
|
12
|
+
tools and the on-box store" below. The plan-artifact surface is the local half
|
|
13
|
+
of a shared record: the Markdown companion file is local, the plan it describes
|
|
14
|
+
is not.
|
|
15
|
+
|
|
16
|
+
The CLI's client transport is NOT implicitly local. Since the fleet fail-closed
|
|
17
|
+
ruling (2026-09-04, hasna/apps#1613), a CLI run with neither API variable
|
|
18
|
+
present exits non-zero with an error naming `HASNA_TODOS_API_URL` and
|
|
19
|
+
`HASNA_TODOS_API_KEY` — it never silently serves the on-box SQLite store.
|
|
20
|
+
Serving local SQLite through the CLI requires the explicit opt-in
|
|
21
|
+
`HASNA_TODOS_LOCAL=1` (alias `TODOS_LOCAL=1`), which is how local-only CLI
|
|
22
|
+
deployments, self-hosters, and tests opt in deliberately.
|
|
23
|
+
|
|
24
|
+
Remote storage is explicit and repo-native. Internal Hasna deployments and
|
|
25
|
+
future SaaS wrappers should configure it through `HASNA_TODOS_*` variables and
|
|
26
|
+
provide the matching Postgres/S3 clients through the public `./storage` package
|
|
27
|
+
export.
|
|
28
|
+
|
|
29
|
+
## The data-backend switch
|
|
30
|
+
|
|
31
|
+
There is no deployment-mode axis. One switch selects where data lives, with
|
|
32
|
+
exactly two arms on each side of the HTTP boundary:
|
|
33
|
+
|
|
34
|
+
- **Client (CLI/MCP/TUI)** — `sqlite` (the on-box file, reachable only under
|
|
35
|
+
the explicit local opt-in `HASNA_TODOS_LOCAL=1` / `TODOS_LOCAL=1`) or `http`
|
|
36
|
+
(the authenticated Todos `/v1` authority). The client never opens Postgres
|
|
37
|
+
directly.
|
|
38
|
+
- **Server (`todos-serve`) / native storage tooling** — `sqlite` (no database
|
|
39
|
+
URL configured) or `postgres` (a `HASNA_TODOS_DATABASE_URL` is present).
|
|
40
|
+
|
|
41
|
+
`HASNA_TODOS_API_URL` + `HASNA_TODOS_API_KEY` select the authenticated HTTP
|
|
42
|
+
authority (client). `HASNA_TODOS_DATABASE_URL` selects the Postgres backend
|
|
43
|
+
(server/native storage tooling). The storage-mode variables
|
|
44
|
+
(`HASNA_TODOS_STORAGE_MODE` and their aliases) are RETIRED: their mere presence
|
|
45
|
+
is a hard error (owner directive 2026-08-15), and no deployment-mode token is
|
|
46
|
+
accepted. No third arm exists.
|
|
47
|
+
|
|
48
|
+
Legacy hosted API toggles are not storage selectors. They cannot select the
|
|
49
|
+
local store and cannot rescue a run that lacks both the API pair and the local
|
|
50
|
+
opt-in.
|
|
51
|
+
|
|
52
|
+
## CLI Remote HTTP Authority
|
|
53
|
+
|
|
54
|
+
The open CLI remote route uses only these canonical settings:
|
|
55
|
+
|
|
56
|
+
| Setting | Purpose |
|
|
57
|
+
| --- | --- |
|
|
58
|
+
| `HASNA_TODOS_API_URL` | Todos authority root, the same root ending in `/v1`, or the fleet gateway form `https://api.hasna.com/todos[/v1]`. |
|
|
59
|
+
| `HASNA_TODOS_API_KEY` | API key supplied to the authority as a bearer credential. |
|
|
60
|
+
|
|
61
|
+
Both must be set to select the HTTP authority. URL set without KEY (or KEY set
|
|
62
|
+
without URL) is a hard error naming the missing variable — the CLI never falls
|
|
63
|
+
back to the on-box SQLite store from a partial cloud configuration. With
|
|
64
|
+
NEITHER set, the CLI fails closed (non-zero exit, error naming both variables
|
|
65
|
+
and the opt-in) unless `HASNA_TODOS_LOCAL=1` (alias `TODOS_LOCAL=1`) explicitly
|
|
66
|
+
opts into local SQLite mode. There is no default local mode and no
|
|
67
|
+
`todos-local-fallback` notice path: a false-green rc 0 against the on-box store
|
|
68
|
+
is impossible (the incident 715712 response was tightened by the ruling).
|
|
69
|
+
|
|
70
|
+
The URL must use HTTPS, except for loopback development authorities. Userinfo,
|
|
71
|
+
query strings, fragments, redirects, `/api/v1`, and other non-`<app>[//v1]`
|
|
72
|
+
paths are rejected. The gateway form `https://api.hasna.com/<app>` is accepted
|
|
73
|
+
and `/v1` is appended (the gateway strips the `<app>` segment and forwards to
|
|
74
|
+
the `https://<app>.hasna.xyz` origin); `<app>/v1` is used verbatim. The CLI
|
|
75
|
+
does not infer an authority from `TODOS_URL`, does not use a private SaaS
|
|
76
|
+
route, and does not send the bearer credential across redirects.
|
|
77
|
+
|
|
78
|
+
Remote selection is fail-closed. A missing URL or key, conflicting mode
|
|
79
|
+
selectors, an incompatible collection route, authentication failure, timeout,
|
|
80
|
+
or server failure is reported with a `REMOTE_*` diagnostic before local SQLite
|
|
81
|
+
can open. A resource-level 404 remains a normal not-found result. There is no
|
|
82
|
+
SQLite or Postgres fallback for a remote CLI invocation.
|
|
83
|
+
|
|
84
|
+
The supported coordination surface includes storage/status diagnostics,
|
|
85
|
+
projects, task lists, project-scoped plans, task create/upsert/list/show/update,
|
|
86
|
+
task-list and plan moves, comments, start/complete/delete, and next/claim. A
|
|
87
|
+
command without a safe `/v1` equivalent exits with `REMOTE_COMMAND_UNSUPPORTED`
|
|
88
|
+
before local helpers run.
|
|
89
|
+
|
|
90
|
+
`todos storage status --json` is a configuration-only diagnostic. On the http
|
|
91
|
+
transport it reports the redacted `/v1` base, URL/key presence, HTTP transport, and
|
|
92
|
+
the disabled local fallback without opening a database or making a network
|
|
93
|
+
request. `todos health` and `todos doctor` authenticate against the required
|
|
94
|
+
remote routes.
|
|
95
|
+
|
|
96
|
+
`todos doctor` additionally audits REFERENTIAL INTEGRITY against the authority
|
|
97
|
+
and its exit code is a verdict, not a "the call succeeded" flag: `0` clean, `1`
|
|
98
|
+
findings (orphaned or dangling project / task-list references), `2` incomplete
|
|
99
|
+
(a condition could not be measured, so health was not established). It prefers
|
|
100
|
+
the server-side aggregate `GET /v1/integrity`, which the storage adapter computes
|
|
101
|
+
with one SQL COUNT per condition on Postgres and SQLite alike; an authority that
|
|
102
|
+
does not expose it leaves the task-level conditions `NOT CHECKED` unless
|
|
103
|
+
`--scan-tasks` completes a read-only paged walk of `/v1/tasks`. Findings are
|
|
104
|
+
report-only — `doctor --apply` remains refused outright in remote mode and never
|
|
105
|
+
repairs an integrity finding in any mode.
|
|
106
|
+
|
|
107
|
+
## Native AWS Configuration
|
|
108
|
+
|
|
109
|
+
- `HASNA_TODOS_DATABASE_URL`: Postgres connection string for RDS-backed task
|
|
110
|
+
state.
|
|
111
|
+
- `HASNA_TODOS_DATABASE_SSL`: boolean, defaults to `true`.
|
|
112
|
+
- `HASNA_TODOS_DATABASE_SCHEMA`: optional schema name for service-owned
|
|
113
|
+
isolation.
|
|
114
|
+
- `HASNA_TODOS_S3_BUCKET`: optional artifact bucket.
|
|
115
|
+
- `HASNA_TODOS_S3_PREFIX`: object prefix, defaults to `todos/`.
|
|
116
|
+
- `HASNA_TODOS_AWS_REGION`: AWS region for S3 and RDS-adjacent operations.
|
|
117
|
+
- `HASNA_TODOS_S3_ENDPOINT`: optional endpoint for tests or compatible object
|
|
118
|
+
stores.
|
|
119
|
+
- `HASNA_TODOS_S3_FORCE_PATH_STYLE`: boolean for local S3-compatible tests.
|
|
120
|
+
- `HASNA_TODOS_SYNC_BATCH_SIZE`: positive integer, defaults to `500`.
|
|
121
|
+
- `HASNA_TODOS_SYNC_DRY_RUN`: boolean sync preview flag.
|
|
122
|
+
|
|
123
|
+
Plain local-development fallbacks are accepted with the same names minus the
|
|
124
|
+
`HASNA_` prefix, for example `TODOS_DATABASE_URL`, and
|
|
125
|
+
`TODOS_S3_BUCKET`. Public docs and wrappers should still prefer the canonical
|
|
126
|
+
`HASNA_TODOS_*` names.
|
|
127
|
+
|
|
128
|
+
Production secrets should follow a consistent namespaced convention, for
|
|
129
|
+
example:
|
|
130
|
+
|
|
131
|
+
- `<org>/<division>/<app>/<env>/env`
|
|
132
|
+
- `<org>/<division>/<app>/<env>/rds`
|
|
133
|
+
- `<org>/<division>/<app>/<env>/s3`
|
|
134
|
+
|
|
135
|
+
Deployment-specific infrastructure identifiers — the managed Postgres cluster
|
|
136
|
+
name and the secrets-manager path that holds the runtime database URL — are
|
|
137
|
+
owned by the private hosting wrapper and supplied at runtime via the optional
|
|
138
|
+
`HASNA_TODOS_RDS_CLUSTER` and `HASNA_TODOS_RDS_RUNTIME_PATH` environment
|
|
139
|
+
variables; this open package ships no real cluster names or secrets-manager
|
|
140
|
+
paths. Runtime wiring should set `HASNA_TODOS_DATABASE_URL` from the resolved
|
|
141
|
+
secret. `TODOS_DATABASE_URL` is only a plain fallback for local development or
|
|
142
|
+
wrappers that have not yet migrated.
|
|
143
|
+
|
|
144
|
+
A SaaS wrapper owns tenant state, billing, accounts, deployment, observability,
|
|
145
|
+
and production secret wiring. The open package owns local storage, the public
|
|
146
|
+
storage contract, local tests, and explicit remote adapter interfaces.
|
|
147
|
+
|
|
148
|
+
## Public Adapter Exports
|
|
149
|
+
|
|
150
|
+
The public `@hasna/todos/storage` export now includes:
|
|
151
|
+
|
|
152
|
+
- `loadTodosStorageConfig` and `createTodosStorageAdapter` for backend selection.
|
|
153
|
+
- `STORAGE_TABLES`, `TODOS_STORAGE_ENV`, and `TODOS_STORAGE_FALLBACK_ENV` for
|
|
154
|
+
wrapper provenance and explicit env mapping.
|
|
155
|
+
- `exportSqliteTodosStorageSnapshot` and `importSqliteTodosStorageSnapshot`
|
|
156
|
+
for local SQLite state movement without a hosted service.
|
|
157
|
+
- `createHybridTodosStorageAdapter` for local SQLite CRUD with explicit
|
|
158
|
+
Postgres-backed remote snapshot push/pull.
|
|
159
|
+
- `createPostgresTodosStorageAdapter` for pure remote CRUD backed by the same
|
|
160
|
+
Postgres JSONB sync records and a caller-provided query client.
|
|
161
|
+
- `createPostgresTodosSyncStore` for RDS-backed snapshot push/pull and
|
|
162
|
+
cross-machine sync cursors through a caller-provided Postgres query client.
|
|
163
|
+
- `createTodosS3ArtifactStore` for S3 object reads/writes/deletes through
|
|
164
|
+
signed `fetch` requests and caller-provided credentials.
|
|
165
|
+
- `uploadRunArtifactsToS3` and `downloadRunArtifactsFromS3` for syncing
|
|
166
|
+
locally stored `task_run_artifacts` bytes to and from S3 while keeping the
|
|
167
|
+
local artifact metadata rows as the source of truth. Objects are
|
|
168
|
+
content-addressed under `artifacts/<task_id>/<sha256>` inside the configured
|
|
169
|
+
bucket prefix.
|
|
170
|
+
- `uploadRunArtifactAtCreation` for the creation-time hop: when
|
|
171
|
+
`HASNA_TODOS_S3_BUCKET` (or the `TODOS_S3_BUCKET` fallback) is set, the CLI
|
|
172
|
+
`runs artifact` command, the MCP `add_task_run_artifact` tool, and the
|
|
173
|
+
environment-snapshot flows upload the stored bytes to S3 right after the
|
|
174
|
+
artifact row is created and record the remote reference on its metadata, so
|
|
175
|
+
`todos storage artifacts download` restores the bytes on any machine. The
|
|
176
|
+
hop is fail-soft: without a bucket or with missing credentials the artifact
|
|
177
|
+
stays local-only and creation never fails.
|
|
178
|
+
|
|
179
|
+
These exports are dependency-light by design. The open package does not bundle
|
|
180
|
+
platform billing, tenant tables, deployment code, or a cloud SDK. Internal
|
|
181
|
+
deployments and wrappers can provide `pg` clients, credentials, and secret
|
|
182
|
+
loading from their own runtime.
|
|
183
|
+
|
|
184
|
+
## Plan Markdown Artifacts
|
|
185
|
+
|
|
186
|
+
Plan records are served by the authenticated shared API; a project-scoped plan
|
|
187
|
+
also has an optional local Markdown companion file that this CLI writes on this
|
|
188
|
+
machine. The root is always chosen by the caller with
|
|
189
|
+
`--artifact-root <directory>` — a real local project directory. A path returned
|
|
190
|
+
by the API is never used. The companion lives at:
|
|
191
|
+
|
|
192
|
+
```text
|
|
193
|
+
<artifact-root>/.hasna/todos/plans/<project-id>/<plan-slug>--<id8>.md
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
The Markdown file is an offline-readable artifact for agents, reviews, handoffs,
|
|
197
|
+
and branch work. It is not the registry: the shared plan record is the source of
|
|
198
|
+
truth, and the file is a client-side copy.
|
|
199
|
+
|
|
200
|
+
Each file uses the `hasna.todos.plan/v1` schema in frontmatter:
|
|
201
|
+
|
|
202
|
+
```markdown
|
|
203
|
+
---
|
|
204
|
+
schema: "hasna.todos.plan/v1"
|
|
205
|
+
plan_id: "<stable plan UUID>"
|
|
206
|
+
plan_slug: "launch-plan"
|
|
207
|
+
project_id: "<stable project UUID>"
|
|
208
|
+
task_list_id: null
|
|
209
|
+
agent_id: null
|
|
210
|
+
stable_id: "<same stable plan UUID>"
|
|
211
|
+
name: "Launch Plan"
|
|
212
|
+
status: "active"
|
|
213
|
+
created_at: "2026-06-30T00:00:00.000Z"
|
|
214
|
+
updated_at: "2026-06-30T00:00:00.000Z"
|
|
215
|
+
artifact_updated_at: "2026-06-30T00:00:00.000Z"
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
# Launch Plan
|
|
219
|
+
|
|
220
|
+
## Tasks
|
|
221
|
+
|
|
222
|
+
- [ ] Example task
|
|
223
|
+
<!-- todos: task_id=<task-id> status=pending priority=medium -->
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
The path resolver only accepts safe project and plan path segments and requires
|
|
227
|
+
a project-scoped plan; an unscoped plan has no artifact because the layout is
|
|
228
|
+
explicitly under `<project-id>`.
|
|
229
|
+
|
|
230
|
+
`todos plans --show <id-or-slug> --artifact-root <directory>` reads the
|
|
231
|
+
companion file when present and includes the parsed artifact metadata and body
|
|
232
|
+
in JSON output. The text view prints the artifact path. If the file is missing,
|
|
233
|
+
the command still shows the shared plan record.
|
|
234
|
+
|
|
235
|
+
For backwards compatibility, artifact readers also check the legacy UUID path:
|
|
236
|
+
|
|
237
|
+
```text
|
|
238
|
+
<artifact-root>/.hasna/todos/plans/<project-id>/<plan-id>.md
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
When both files exist, the slugged `<plan-slug>--<id8>.md` artifact wins. A
|
|
242
|
+
later `--write-artifacts` run materializes the slugged artifact while leaving
|
|
243
|
+
legacy files untouched for operator review.
|
|
244
|
+
|
|
245
|
+
For migration and diagnostics:
|
|
246
|
+
|
|
247
|
+
```bash
|
|
248
|
+
todos plans --write-artifacts --artifact-root <directory>
|
|
249
|
+
todos plans --artifact <id-or-slug> --artifact-root <directory> --json
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Before 0.16.0 the artifact root was implicit: the plan was read from local
|
|
253
|
+
SQLite and its recorded project path supplied the root, so the form was
|
|
254
|
+
`todos plans --artifact <id-or-slug> --json`. That invocation now exits
|
|
255
|
+
non-zero with `--artifact-root is required`. The root is always the caller's
|
|
256
|
+
choice, because a path returned by the shared API never authorizes client
|
|
257
|
+
filesystem access.
|
|
258
|
+
|
|
259
|
+
`--write-artifacts` materializes Markdown files for every project-scoped plan in
|
|
260
|
+
the current project scope using readable slug filenames. `--artifact` reports
|
|
261
|
+
the resolved file path, whether the file exists, parse errors, task references,
|
|
262
|
+
and deterministic conflicts between the shared plan record and Markdown
|
|
263
|
+
frontmatter/task comments. The CLI does not silently treat the Markdown file as
|
|
264
|
+
authoritative when conflicts exist; agents should resolve the conflict through
|
|
265
|
+
the CLI or an explicit migration task.
|
|
266
|
+
|
|
267
|
+
## MCP tools and the on-box store
|
|
268
|
+
|
|
269
|
+
The MCP server no longer opens the on-box SQLite store implicitly. `getDatabase()`
|
|
270
|
+
throws `API_DATABASE_FALLBACK_FORBIDDEN` unless the call supplies an explicit
|
|
271
|
+
path or the environment selects the local store
|
|
272
|
+
(`selectsTodosLocalStore()` — the deliberate `HASNA_TODOS_LOCAL=1` /
|
|
273
|
+
`TODOS_LOCAL=1` opt-in on an environment that configures no authority or
|
|
274
|
+
credential of its own).
|
|
275
|
+
|
|
276
|
+
Tools that read the store through the shared API or read no store at all are
|
|
277
|
+
unaffected. Tools that still call into the local data layer directly answer with
|
|
278
|
+
the typed `{"code":"API_DATABASE_FALLBACK_FORBIDDEN"}` payload on the default
|
|
279
|
+
posture, whose `suggestion` names the opt-in; the server logs the same code. The
|
|
280
|
+
ten plan/task-list tools (`create_plan`, `list_plans`, `get_plan`, `update_plan`,
|
|
281
|
+
`delete_plan`, `create_task_list`, `list_task_lists`, `get_task_list`,
|
|
282
|
+
`update_task_list`, `delete_task_list`) return the typed
|
|
283
|
+
`REMOTE_API_CONFIG_MISSING` refusal that names the missing configuration.
|
|
284
|
+
|
|
285
|
+
Measured at 0.16.0 with `TODOS_PROFILE=full` on the default (no-credential)
|
|
286
|
+
posture (361 tools, 125 of them taking no required arguments), 68 of the 125
|
|
287
|
+
returned `API_DATABASE_FALLBACK_FORBIDDEN`:
|
|
288
|
+
|
|
289
|
+
- templates: `list_templates`, `init_templates` (the rest of the family —
|
|
290
|
+
`create_template`, `preview_template`, `export_template`, `import_template`,
|
|
291
|
+
`create_task_from_template` — takes arguments, so it is outside the
|
|
292
|
+
zero-argument census and reads the same store; `list_template_library` and
|
|
293
|
+
`write_template_library` are bundled-only and unaffected)
|
|
294
|
+
- tags and labels: `list_tags`, `list_labels`
|
|
295
|
+
- agent work: `get_org_chart`, `suggest_agent_name`, `get_usage_ledger`,
|
|
296
|
+
`get_leaderboard`, `list_agent_run_queue`, `run_next_agent_dispatch`,
|
|
297
|
+
`list_task_runs`, `finish_task_run`, `list_task_findings`
|
|
298
|
+
- health and reporting: `run_doctor`, `todos_retro`, `todos_inbox`,
|
|
299
|
+
`build_local_report`, `get_planning_forecast`, `get_activity_timeline`,
|
|
300
|
+
`get_time_report`, `export_agent_reliability_scorecards`
|
|
301
|
+
- stale, blocked and archived work: `get_stale_tasks`, `get_blocked_tasks`,
|
|
302
|
+
`get_blocking_tasks`, `get_critical_path`, `patrol_tasks`,
|
|
303
|
+
`archive_completed`, `get_archived_tasks`, `notify_upcoming_deadlines`,
|
|
304
|
+
`get_sla_breaches`, `rebalance_workload`
|
|
305
|
+
- boards, calendar and review: `list_boards`, `list_calendar_events`,
|
|
306
|
+
`export_calendar_ics`, `get_review_queue`, `list_review_queue`
|
|
307
|
+
- knowledge, risk and search: `list_knowledge_records`,
|
|
308
|
+
`export_knowledge_records`, `list_risks`, `export_risk_register`,
|
|
309
|
+
`list_retrospectives`, `create_retrospective`, `export_retrospectives`,
|
|
310
|
+
`sync_kg`, `find_duplicate_tasks`, `list_search_views`,
|
|
311
|
+
`query_tasks_by_fields`, `migrate_workflow_states`
|
|
312
|
+
- files, locks and handoffs: `list_file_locks`, `list_handoffs`,
|
|
313
|
+
`get_latest_handoff`, `get_file_heat_map`, `list_active_files`,
|
|
314
|
+
`list_inbox_items`, `import_external_issues`
|
|
315
|
+
- backups, integrity and fixtures: `create_local_backup`,
|
|
316
|
+
`check_local_integrity`, `poll_local_snapshots`, `import_onboarding_fixture`
|
|
317
|
+
- focus and audit: `start_focus_session`, `list_focus_sessions`,
|
|
318
|
+
`get_idle_focus_prompts`, `get_audit_ledger`
|
|
319
|
+
- projects, notifications and dispatch: `bootstrap_project`,
|
|
320
|
+
`check_local_notifications`, `list_dispatches`, `run_due_dispatches`,
|
|
321
|
+
`generate_release_notes`
|
|
322
|
+
|
|
323
|
+
The shared-API MCP tools that need a credential — `list_tasks`, `list_projects`,
|
|
324
|
+
`get_status`, `get_my_tasks`, `get_health`, `standup`, `list_agents`,
|
|
325
|
+
`machines_*` — answer the typed `REMOTE_API_CONFIG_MISSING` on the default
|
|
326
|
+
posture: the documented fail-closed behaviour rather than a lost local surface.
|
|
327
|
+
Five more zero-argument tools refuse caller input or local state with the typed
|
|
328
|
+
`INVALID_INPUT` / `ENCRYPTION_KEY_UNAVAILABLE` / `ENCRYPTED_PAYLOAD_INVALID`,
|
|
329
|
+
and five answer a readable text refusal. Measured on the same 125-tool census, no
|
|
330
|
+
zero-argument tool returns an opaque `UNKNOWN_ERROR` — neither on this default
|
|
331
|
+
posture nor under the `HASNA_TODOS_LOCAL=1` opt-in below, where
|
|
332
|
+
`create_retrospective` and `finish_task_run` now answer the typed
|
|
333
|
+
`INVALID_INPUT` for their missing scope / run id instead of the opaque
|
|
334
|
+
`UNKNOWN_ERROR` they returned earlier in 0.16.0.
|
|
335
|
+
|
|
336
|
+
Run the MCP server with `HASNA_TODOS_LOCAL=1` to serve these tools from the
|
|
337
|
+
on-box store. That opt-in is honoured only when the environment configures no
|
|
338
|
+
authority or credential: with `HASNA_TODOS_API_KEY` or `HASNA_TODOS_API_URL`
|
|
339
|
+
set, a configured environment outranks the opt-in and these tools are
|
|
340
|
+
unreachable in 0.16.0. Converting them to the shared API is tracked separately;
|
|
341
|
+
this document records the boundary rather than claiming the surface is
|
|
342
|
+
converted.
|
|
343
|
+
|
|
344
|
+
## Hybrid Sync Shape (explicit migration machinery)
|
|
345
|
+
|
|
346
|
+
`createHybridTodosStorageAdapter` builds a local-plus-remote adapter when the
|
|
347
|
+
caller passes a Postgres-style query client or sync store. It is migration/sync
|
|
348
|
+
machinery invoked explicitly — it is NOT an arm of the data-backend switch, and
|
|
349
|
+
no environment value selects it:
|
|
350
|
+
|
|
351
|
+
- Local CRUD stays SQLite-backed and works offline.
|
|
352
|
+
- `adapter.sync.exportSnapshot()` and `adapter.sync.importSnapshot()` move the
|
|
353
|
+
storage-level SQLite snapshot.
|
|
354
|
+
- `adapter.remote.pushSnapshot()` writes the local snapshot into Postgres sync
|
|
355
|
+
records.
|
|
356
|
+
- `adapter.remote.pullSnapshot()` reads Postgres sync records and imports them
|
|
357
|
+
into local SQLite.
|
|
358
|
+
- `adapter.remote.syncOnce()` pulls first, then pushes the merged local
|
|
359
|
+
snapshot.
|
|
360
|
+
|
|
361
|
+
This is the open-package boundary. SaaS tenant wrappers can be added on top
|
|
362
|
+
without changing the local default or depending on a shared cloud package.
|
|
363
|
+
|
|
364
|
+
## Postgres CRUD Shape
|
|
365
|
+
|
|
366
|
+
A present `HASNA_TODOS_DATABASE_URL` builds a pure Postgres adapter when the
|
|
367
|
+
caller passes a Postgres-style query client to
|
|
368
|
+
`createTodosStorageAdapter`:
|
|
369
|
+
|
|
370
|
+
- CRUD uses the repo-owned `todos_sync_records` JSONB table rather than SaaS
|
|
371
|
+
tenant tables.
|
|
372
|
+
- The package does not import `pg`; wrappers or internal deployments provide
|
|
373
|
+
the connected client.
|
|
374
|
+
- `createPostgresTodosStorageAdapter` is also exported directly for callers
|
|
375
|
+
that want to bypass backend selection.
|
|
376
|
+
- Local SQLite remains the default unless `HASNA_TODOS_DATABASE_URL` is set
|
|
377
|
+
explicitly (with the one carve-out that a shadow mirror
|
|
378
|
+
`HASNA_TODOS_SHADOW=1` keeps SQLite as the source of truth).
|
|
379
|
+
|
|
380
|
+
## S3 Artifact Sync
|
|
381
|
+
|
|
382
|
+
Run artifacts recorded with local stored content can now be pushed to S3 through
|
|
383
|
+
the public storage helpers:
|
|
384
|
+
|
|
385
|
+
- `uploadRunArtifactsToS3({ store, db, filter })` verifies the local
|
|
386
|
+
content-addressed artifact bytes, uploads them using `createTodosS3ArtifactStore`,
|
|
387
|
+
and stores a `remote_artifact_store` reference on the `task_run_artifacts`
|
|
388
|
+
metadata row.
|
|
389
|
+
- `downloadRunArtifactsFromS3({ store, db, filter })` reads that remote
|
|
390
|
+
reference, downloads the object, verifies the checksum, and restores the local
|
|
391
|
+
content-addressed file.
|
|
392
|
+
|
|
393
|
+
The helper only needs a caller-provided S3 store. Credentials, secret loading,
|
|
394
|
+
tenant scoping, and production scheduling remain outside the open package.
|
|
395
|
+
|
|
396
|
+
The CLI exposes the same boundary:
|
|
397
|
+
|
|
398
|
+
- `todos storage artifacts upload --run-id <id> --json` previews uploadable
|
|
399
|
+
local artifact bytes without network access.
|
|
400
|
+
- `todos storage artifacts download --run-id <id> --json` previews remote
|
|
401
|
+
restore work without network access.
|
|
402
|
+
- Add `--apply` to perform the S3 operation. Apply mode requires
|
|
403
|
+
`HASNA_TODOS_S3_BUCKET` plus `HASNA_TODOS_S3_ACCESS_KEY_ID` and
|
|
404
|
+
`HASNA_TODOS_S3_SECRET_ACCESS_KEY`; `HASNA_TODOS_S3_SESSION_TOKEN` is
|
|
405
|
+
optional.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hasna/todos",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -53,37 +53,40 @@
|
|
|
53
53
|
}
|
|
54
54
|
},
|
|
55
55
|
"workspaces": [
|
|
56
|
-
"dashboard",
|
|
57
56
|
"ai"
|
|
58
57
|
],
|
|
59
58
|
"files": [
|
|
60
59
|
"dist",
|
|
61
|
-
"dashboard/dist",
|
|
62
60
|
"postinstall.js",
|
|
63
61
|
"LICENSE",
|
|
64
|
-
"README.md"
|
|
62
|
+
"README.md",
|
|
63
|
+
"CHANGELOG.md",
|
|
64
|
+
"docs/PLAN_API.md",
|
|
65
|
+
"docs/TASK_LIST_API.md",
|
|
66
|
+
"docs/TEMPLATE_API.md",
|
|
67
|
+
"docs/TASK_QUERY_API.md",
|
|
68
|
+
"docs/native-storage.md"
|
|
65
69
|
],
|
|
66
70
|
"scripts": {
|
|
67
|
-
"build": "
|
|
71
|
+
"build": "cd ai && bun install --frozen-lockfile && cd .. && rm -rf dist && bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/server/index.ts --outdir dist/server --target bun && bun build src/sdk/index.ts --outdir dist/sdk --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/index.ts src/mcp.ts src/registry.ts src/contracts.ts src/storage.ts src/testing.ts src/project-registration.ts src/task-manifest.ts src/task-subtree-transfer.ts --outdir dist --root src --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && tsc --emitDeclarationOnly --outDir dist",
|
|
68
72
|
"build:js": "rm -rf dist && bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/server/index.ts --outdir dist/server --target bun && bun build src/sdk/index.ts --outdir dist/sdk --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/index.ts src/mcp.ts src/registry.ts src/contracts.ts src/storage.ts src/testing.ts src/project-registration.ts src/task-manifest.ts src/task-subtree-transfer.ts --outdir dist --root src --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && tsc --emitDeclarationOnly --outDir dist",
|
|
69
73
|
"build:server": "rm -rf dist && bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/server/index.ts --outdir dist/server --target bun && bun build src/sdk/index.ts --outdir dist/sdk --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/index.ts src/mcp.ts src/registry.ts src/contracts.ts src/storage.ts src/testing.ts src/project-registration.ts src/task-manifest.ts src/task-subtree-transfer.ts --outdir dist --target bun --external '@hasna/contracts' --external '@hasna/contracts/*'",
|
|
70
74
|
"migrate": "bun run src/server/index.ts migrate",
|
|
71
75
|
"backfill:comment-redaction": "bun run src/server/index.ts redact-comments",
|
|
72
76
|
"generate:sdk": "bun run scripts/generate-sdk.ts",
|
|
73
|
-
"build:dashboard": "cd dashboard && bun install --frozen-lockfile && bun run build",
|
|
74
77
|
"typecheck": "tsc --noEmit -p tsconfig.typecheck.json",
|
|
75
|
-
"test": "bun test",
|
|
76
|
-
"test:no-cloud": "bun test src/no-cloud-boundary.test.ts src/local-first.test.ts src/lib/public-release-gate.test.ts",
|
|
78
|
+
"test": "bun test --timeout 120000",
|
|
79
|
+
"test:no-cloud": "bun test --timeout 120000 src/no-cloud-boundary.test.ts src/local-first.test.ts src/lib/public-release-gate.test.ts",
|
|
77
80
|
"dev:cli": "bun run src/cli/index.tsx",
|
|
78
81
|
"dev:mcp": "bun run src/mcp/index.ts",
|
|
79
82
|
"dev:serve": "bun run src/server/index.ts",
|
|
80
83
|
"verify:release": "bun run scripts/verify-public-release.ts --mode=review",
|
|
81
84
|
"verify:release-review": "bun run scripts/verify-npm-release-agent-review.ts",
|
|
82
85
|
"verify:attested-container-candidate": "bun run scripts/attested-container-candidate.ts verify",
|
|
83
|
-
"test:attested-container-candidate": "bun test scripts/attested-container-candidate.test.ts",
|
|
86
|
+
"test:attested-container-candidate": "bun test --timeout 120000 scripts/attested-container-candidate.test.ts",
|
|
84
87
|
"emit:iapp-deployment-compatibility-vector": "bun run scripts/attested-container-compatibility-vector.ts emit",
|
|
85
88
|
"verify:iapp-deployment-compatibility-vector": "bun run scripts/attested-container-compatibility-vector.ts verify",
|
|
86
|
-
"test:attested-container-compatibility-vector": "bun test scripts/attested-container-compatibility-vector.test.ts",
|
|
89
|
+
"test:attested-container-compatibility-vector": "bun test --timeout 120000 scripts/attested-container-compatibility-vector.test.ts",
|
|
87
90
|
"issue:release-review": "bun run scripts/issue-npm-release-agent-review.ts",
|
|
88
91
|
"prepublishOnly": "bun run scripts/verify-public-release.ts --mode=publish",
|
|
89
92
|
"postinstall": "node postinstall.js"
|
|
@@ -107,11 +110,12 @@
|
|
|
107
110
|
},
|
|
108
111
|
"repository": {
|
|
109
112
|
"type": "git",
|
|
110
|
-
"url": "https://github.com/hasna/
|
|
113
|
+
"url": "https://github.com/hasna/apps.git",
|
|
114
|
+
"directory": "apps/todos"
|
|
111
115
|
},
|
|
112
|
-
"homepage": "https://github.com/hasna/
|
|
116
|
+
"homepage": "https://github.com/hasna/apps",
|
|
113
117
|
"bugs": {
|
|
114
|
-
"url": "https://github.com/hasna/
|
|
118
|
+
"url": "https://github.com/hasna/apps/issues"
|
|
115
119
|
},
|
|
116
120
|
"engines": {
|
|
117
121
|
"bun": ">=1.0.0"
|
|
@@ -120,9 +124,8 @@
|
|
|
120
124
|
"author": "Andrei Hasna <andrei@hasna.com>",
|
|
121
125
|
"license": "Apache-2.0",
|
|
122
126
|
"dependencies": {
|
|
123
|
-
"@hasna/contracts": "0.
|
|
127
|
+
"@hasna/contracts": "1.0.2",
|
|
124
128
|
"@hasna/events": "^0.1.11",
|
|
125
|
-
"@hasna/paths": "0.1.0",
|
|
126
129
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
127
130
|
"chalk": "^5.4.1",
|
|
128
131
|
"commander": "^13.1.0",
|
package/postinstall.js
CHANGED
|
@@ -5,13 +5,64 @@
|
|
|
5
5
|
// otherwise the legacy ~/.hasna/todos default. Failures are non-fatal: the
|
|
6
6
|
// runtime creates the same directories on first use.
|
|
7
7
|
import { existsSync, mkdirSync } from "node:fs";
|
|
8
|
+
// --- Local path resolver -------------------------------------------------
|
|
9
|
+
// @hasna/paths was deleted (hasna/apps#1535, 2026-09-03); this in-package
|
|
10
|
+
// implementation preserves the resolver contract (XDG / macOS home layout
|
|
11
|
+
// honoring HASNA_{CONFIG,DATA,STATE,CACHE}_HOME, with the same env-override
|
|
12
|
+
// and home-override semantics the deleted package had).
|
|
13
|
+
import { homedir as pathsResolverHomedir } from "node:os";
|
|
14
|
+
import { join as pathsResolverJoin } from "node:path";
|
|
15
|
+
|
|
16
|
+
const PATHS_RESOLVER_KIND_ENV = {
|
|
17
|
+
config: "HASNA_CONFIG_HOME",
|
|
18
|
+
data: "HASNA_DATA_HOME",
|
|
19
|
+
state: "HASNA_STATE_HOME",
|
|
20
|
+
cache: "HASNA_CACHE_HOME",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function pathsResolverBaseDir(kind, options) {
|
|
24
|
+
const env = options.env ?? process.env;
|
|
25
|
+
const override = env[PATHS_RESOLVER_KIND_ENV[kind]];
|
|
26
|
+
if (typeof override === "string" && override.length > 0) return override;
|
|
27
|
+
const home = options.home ?? pathsResolverHomedir();
|
|
28
|
+
const platform = options.platform ?? process.platform;
|
|
29
|
+
if (platform === "darwin") {
|
|
30
|
+
switch (kind) {
|
|
31
|
+
case "config":
|
|
32
|
+
case "data":
|
|
33
|
+
return pathsResolverJoin(home, "Library", "Application Support", "Hasna");
|
|
34
|
+
case "cache":
|
|
35
|
+
return pathsResolverJoin(home, "Library", "Caches", "Hasna");
|
|
36
|
+
case "state":
|
|
37
|
+
return pathsResolverJoin(home, "Library", "Logs", "Hasna");
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
switch (kind) {
|
|
41
|
+
case "config":
|
|
42
|
+
return pathsResolverJoin(home, ".config", "hasna");
|
|
43
|
+
case "data":
|
|
44
|
+
return pathsResolverJoin(home, ".local", "share", "hasna");
|
|
45
|
+
case "state":
|
|
46
|
+
return pathsResolverJoin(home, ".local", "state", "hasna");
|
|
47
|
+
case "cache":
|
|
48
|
+
return pathsResolverJoin(home, ".cache", "hasna");
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function pathsResolverResolve(kind, options) {
|
|
53
|
+
const appSegment = options.internal === true ? pathsResolverJoin("internal", options.app) : options.app;
|
|
54
|
+
return pathsResolverJoin(pathsResolverBaseDir(kind, options), appSegment);
|
|
55
|
+
}
|
|
56
|
+
function dataDir(options) {
|
|
57
|
+
return pathsResolverResolve("data", options);
|
|
58
|
+
}
|
|
8
59
|
import { homedir } from "node:os";
|
|
9
60
|
import { join } from "node:path";
|
|
10
61
|
|
|
11
62
|
const DATA_HOME_OVERRIDE = (process.env["HASNA_DATA_HOME"] || "").trim();
|
|
12
63
|
|
|
13
64
|
try {
|
|
14
|
-
|
|
65
|
+
// (local resolver — @hasna/paths deleted, hasna/apps#1535)
|
|
15
66
|
const resolved = dataDir({
|
|
16
67
|
app: "todos",
|
|
17
68
|
home: process.env["HOME"] || process.env["USERPROFILE"] || homedir(),
|