project-mgr 0.1.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/.env_sample ADDED
@@ -0,0 +1,5 @@
1
+ # SQLite database path. Relative paths are resolved from the current directory.
2
+ PMGR_DB_PATH=./data/project-mgr.sqlite
3
+
4
+ # For isolated manual testing, use a separate database instead:
5
+ # PMGR_DB_PATH=./data/manual-test.sqlite
@@ -0,0 +1,4 @@
1
+ dist
2
+ coverage
3
+ node_modules
4
+ data
package/AGENTS.md ADDED
@@ -0,0 +1,5 @@
1
+ # Project instructions
2
+
3
+ Follow the user-level instructions in `~/.agents/AGENTS.md`.
4
+
5
+ Unless the user explicitly requests another language, write project files, code comments, documentation, changelog entries, and agent-authored artifacts in English.
package/CHANGELOG.md ADDED
@@ -0,0 +1,20 @@
1
+ # Changelog
2
+
3
+ ## Unreleased
4
+
5
+ ### Fixed
6
+
7
+ - Approved the required `better-sqlite3` and `esbuild` build scripts for reproducible frozen pnpm installs.
8
+ - Made empty human-readable list queries explicit while preserving `[]` JSON output.
9
+ - Added top-level CLI action shorthand while keeping `status <action>` compatibility.
10
+
11
+ ## 0.1.0 - 2026-08-03
12
+
13
+ ### Added
14
+
15
+ - A local TypeScript CLI and SQLite append-only project status ledger.
16
+ - Single-record, JSON, and JSONL atomic writes; current, historical, and point-in-time queries; and restorable exports.
17
+ - Strict input validation, `externalRef` idempotency, automatic migrations, and deterministic time ordering.
18
+ - `.env_sample` and automatic loading of local `.env` database configuration by the run scripts.
19
+ - English usage documentation and a standalone architecture document.
20
+ - 100% coverage thresholds for statements, branches, functions, and lines.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dean
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,184 @@
1
+ # Project Manager
2
+
3
+ [Chinese](README_CN.md)
4
+
5
+ `project-mgr` is a local-first, append-only ledger for project status. It stores progress, blockers, decisions, and next steps as timestamped status snapshots, so people and AI agents can quickly inspect each project's current state, complete history, or state at a particular time.
6
+
7
+ The first release provides only a local CLI and SQLite. It has no HTTP API, long-running service, or agent skills. Data stays on the local machine and requires no account or cloud service.
8
+
9
+ ## Install and run
10
+
11
+ Requires Node.js 22+ and pnpm 10.30.2.
12
+
13
+ ```bash
14
+ pnpm install --frozen-lockfile
15
+ cp .env_sample .env
16
+ pnpm build
17
+ ```
18
+
19
+ `.env_sample` provides examples for all environment settings. The copied `.env` file is ignored by Git. `pnpm start` and `pnpm pmgr` automatically load the root `.env`; existing shell environment variables take precedence.
20
+
21
+ After building, run:
22
+
23
+ ```bash
24
+ pnpm start current
25
+ ```
26
+
27
+ During development, run the TypeScript CLI directly:
28
+
29
+ ```bash
30
+ pnpm pmgr current
31
+ ```
32
+
33
+ The default configuration in `.env_sample` is:
34
+
35
+ ```dotenv
36
+ PMGR_DB_PATH=./data/project-mgr.sqlite
37
+ ```
38
+
39
+ Set `PMGR_DB_PATH` in `.env` to use another location, or override it for one command:
40
+
41
+ ```bash
42
+ PMGR_DB_PATH=/absolute/path/projects.sqlite pnpm start current
43
+ ```
44
+
45
+ Relative paths are resolved from the working directory of the command. When `pnpm start ...` runs from this project's root, the default database is `./data/project-mgr.sqlite`. It is created automatically when the first valid command runs.
46
+
47
+ ### Use an isolated test database
48
+
49
+ For manual testing, set a persistent path in `.env` that is separate from production data:
50
+
51
+ ```dotenv
52
+ PMGR_DB_PATH=./data/manual-test.sqlite
53
+ ```
54
+
55
+ Then run commands normally:
56
+
57
+ ```bash
58
+ pnpm start add \
59
+ --project test-project \
60
+ --status active \
61
+ --summary "Testing the isolated database"
62
+
63
+ pnpm start history --project test-project
64
+ ```
65
+
66
+ Project keys must match exactly: a record added to `test-project` is not part of `project-mgr` history. Empty human-readable list queries print `No entries found.`; with `--json`, they print `[]`.
67
+
68
+ `data/` is ignored by Git, so test data is not committed. Delete `data/manual-test.sqlite` when it is no longer needed.
69
+
70
+ Automated tests use an in-memory database or SQLite files in the system temporary directory; they do not read or write the default database. `PMGR_DB_PATH=:memory:` also works for one command, but each CLI command runs in a separate process and its data disappears when the command exits, so it is not suitable for consecutive manual tests.
71
+
72
+ ## Quick start
73
+
74
+ Append a project status:
75
+
76
+ ```bash
77
+ pnpm start add \
78
+ --project project-mgr \
79
+ --status active \
80
+ --summary "The local status ledger is available" \
81
+ --progress "Completed SQLite and CLI support" \
82
+ --next "Connect a real project" \
83
+ --tag mvp
84
+ ```
85
+
86
+ Query the current status of all projects or one project:
87
+
88
+ ```bash
89
+ pnpm start current
90
+ pnpm start current --project project-mgr --json
91
+ ```
92
+
93
+ Query history and the status at a specified time:
94
+
95
+ ```bash
96
+ pnpm start history \
97
+ --project project-mgr \
98
+ --since 2026-08-01T00:00:00Z \
99
+ --limit 20 \
100
+ --offset 0
101
+
102
+ pnpm start at \
103
+ --project project-mgr \
104
+ --at 2026-08-03T12:00:00+08:00 \
105
+ --json
106
+ ```
107
+
108
+ ## Write status
109
+
110
+ `add` requires:
111
+
112
+ - `--project`: Unique project name. It may contain English letters, numbers, `.`, `_`, and `-`, and must start with a letter or number.
113
+ - `--status`: One of `idea`, `planned`, `active`, `blocked`, `paused`, `completed`, or `archived`.
114
+ - `--summary`: A summary of this status.
115
+
116
+ Optional arguments:
117
+
118
+ - `--progress`: Work already completed.
119
+ - `--blocker`, `--next`, `--decision`, and `--tag`: May be specified repeatedly.
120
+ - `--at`: When the status actually occurred. It must be an ISO 8601 timestamp with a timezone; defaults to the current time.
121
+ - `--source`: `human`, `agent`, or `import`; defaults to `human`.
122
+ - `--external-ref`: A globally idempotent key. A duplicate write to the same project returns the existing record; use by another project is an error.
123
+ - `--json`: Emit stable JSON.
124
+
125
+ ## Bulk import
126
+
127
+ `import` accepts a JSON object, a JSON array, or a JSONL file containing one object per line. Every record is validated before the whole batch is written in one SQLite transaction; any failure rolls back the entire batch.
128
+
129
+ ```json
130
+ {
131
+ "project": "project-mgr",
132
+ "status": "active",
133
+ "summary": "Core functionality is complete",
134
+ "progress": "CLI and SQLite have passed tests",
135
+ "blockers": [],
136
+ "nextSteps": ["Connect a real project"],
137
+ "decisions": ["The first release provides only a local CLI"],
138
+ "tags": ["mvp"],
139
+ "occurredAt": "2026-08-03T12:00:00+08:00",
140
+ "source": "human",
141
+ "externalRef": "project-mgr-2026-08-03-core"
142
+ }
143
+ ```
144
+
145
+ ```bash
146
+ pnpm start import statuses.jsonl
147
+ # Equivalent form
148
+ pnpm start import --file statuses.jsonl --json
149
+ ```
150
+
151
+ Unknown fields, unknown CLI options, extra positional arguments, invalid statuses, and timestamps without a timezone are rejected.
152
+
153
+ ## Query and export
154
+
155
+ | Command | Purpose | Key options |
156
+ | --------- | ----------------------------------------------- | ------------------------------------------------------------------ |
157
+ | `current` | Current status for all projects or one project | `--project`, `--json` |
158
+ | `history` | One project's history, newest first | `--project`, `--since`, `--until`, `--limit`, `--offset`, `--json` |
159
+ | `at` | The latest status at or before a specified time | `--project`, `--at`, `--json` |
160
+ | `export` | Export all records or one project's records | `--project`, `--format json\|jsonl` |
161
+
162
+ All six actions can also use the compatible `status <action>` form, for example `pmgr status export`. Human-readable multi-record results list project, status, occurrence time, and summary, or `No entries found.` when empty. `add`, `import`, `current`, `history`, and `at` support `--json`; `export` uses `--format` to select its format.
163
+
164
+ Exports omit the database-internal `id` and `recordedAt` fields and can be imported directly. Records are exported from oldest to newest so that restoring them preserves the insertion order of records with the same timestamp:
165
+
166
+ ```bash
167
+ pnpm start export --format jsonl > project-statuses.jsonl
168
+
169
+ PMGR_DB_PATH=./data/restored.sqlite \
170
+ pnpm start import project-statuses.jsonl
171
+ ```
172
+
173
+ ## Development and verification
174
+
175
+ ```bash
176
+ pnpm typecheck
177
+ pnpm lint
178
+ pnpm format-check
179
+ pnpm test
180
+ pnpm coverage
181
+ pnpm build
182
+ ```
183
+
184
+ Coverage thresholds for statements, branches, functions, and lines are all 100%. See the [architecture document](docs/architecture.md) for the detailed design.
package/README_CN.md ADDED
@@ -0,0 +1,184 @@
1
+ # Project Manager
2
+
3
+ [English](README.md)
4
+
5
+ `project-mgr` 是一个本地优先、只追加的项目状态账本。它把项目进展、阻塞、决策和下一步保存为带时间的状态快照,让个人或 AI Agent 能快速查询每个项目的当前状态、完整历史以及某个时间点的状态。
6
+
7
+ 首版只提供本地 CLI 和 SQLite,不包含 HTTP API、常驻服务或 Agent Skills。数据默认保存在本机,无需账号或云服务。
8
+
9
+ ## 安装与运行
10
+
11
+ 需要 Node.js 22+ 和 pnpm 10.30.2。
12
+
13
+ ```bash
14
+ pnpm install --frozen-lockfile
15
+ cp .env_sample .env
16
+ pnpm build
17
+ ```
18
+
19
+ `.env_sample` 提供全部环境配置示例;复制得到的 `.env` 已被 Git 忽略。`pnpm start` 和 `pnpm pmgr` 会自动读取项目根目录下的 `.env`,已有的 shell 环境变量优先级更高。
20
+
21
+ 构建后通过以下方式运行:
22
+
23
+ ```bash
24
+ pnpm start current
25
+ ```
26
+
27
+ 开发时也可以直接运行 TypeScript:
28
+
29
+ ```bash
30
+ pnpm pmgr current
31
+ ```
32
+
33
+ 默认配置位于 `.env_sample`:
34
+
35
+ ```dotenv
36
+ PMGR_DB_PATH=./data/project-mgr.sqlite
37
+ ```
38
+
39
+ 通过 `.env` 中的 `PMGR_DB_PATH` 指定其他位置,也可以为单条命令临时覆盖:
40
+
41
+ ```bash
42
+ PMGR_DB_PATH=/absolute/path/projects.sqlite pnpm start current
43
+ ```
44
+
45
+ 路径基于启动命令的当前工作目录计算。从本项目根目录执行 `pnpm start ...` 时,默认位置就是 `./data/project-mgr.sqlite`。数据库会在第一次执行有效命令时自动创建。
46
+
47
+ ### 使用独立测试数据库
48
+
49
+ 手工测试时,建议在 `.env` 中固定设置一个与正式数据分离的路径:
50
+
51
+ ```dotenv
52
+ PMGR_DB_PATH=./data/manual-test.sqlite
53
+ ```
54
+
55
+ 随后正常执行命令:
56
+
57
+ ```bash
58
+ pnpm start add \
59
+ --project test-project \
60
+ --status active \
61
+ --summary "测试独立数据库"
62
+
63
+ pnpm start history --project test-project
64
+ ```
65
+
66
+ 项目键必须完全一致:写入 `test-project` 的记录不会出现在 `project-mgr` 的历史中。人类可读的空列表查询会输出 `No entries found.`;使用 `--json` 时则输出 `[]`。
67
+
68
+ `data/` 已加入 Git ignore,测试数据不会进入版本库。不需要保留时,可以删除 `data/manual-test.sqlite`。
69
+
70
+ 自动化测试会使用内存数据库或系统临时目录中的 SQLite 文件,不会读写默认数据库。`PMGR_DB_PATH=:memory:` 也可用于单条命令,但每个 CLI 命令都是独立进程,命令结束后数据就会消失,不适合连续手工测试。
71
+
72
+ ## 快速开始
73
+
74
+ 追加一条项目状态:
75
+
76
+ ```bash
77
+ pnpm start add \
78
+ --project project-mgr \
79
+ --status active \
80
+ --summary "本地状态账本已可用" \
81
+ --progress "完成 SQLite 与 CLI" \
82
+ --next "接入真实项目" \
83
+ --tag mvp
84
+ ```
85
+
86
+ 查询所有项目或单个项目的当前状态:
87
+
88
+ ```bash
89
+ pnpm start current
90
+ pnpm start current --project project-mgr --json
91
+ ```
92
+
93
+ 查询历史与指定时间点状态:
94
+
95
+ ```bash
96
+ pnpm start history \
97
+ --project project-mgr \
98
+ --since 2026-08-01T00:00:00Z \
99
+ --limit 20 \
100
+ --offset 0
101
+
102
+ pnpm start at \
103
+ --project project-mgr \
104
+ --at 2026-08-03T12:00:00+08:00 \
105
+ --json
106
+ ```
107
+
108
+ ## 状态写入
109
+
110
+ `add` 必须提供:
111
+
112
+ - `--project`:项目唯一名称,只能使用英文字母、数字、`.`、`_`、`-`,并以字母或数字开头。
113
+ - `--status`:`idea`、`planned`、`active`、`blocked`、`paused`、`completed` 或 `archived`。
114
+ - `--summary`:本次状态摘要。
115
+
116
+ 可选参数:
117
+
118
+ - `--progress`:已经完成的工作。
119
+ - `--blocker`、`--next`、`--decision`、`--tag`:可以重复传入。
120
+ - `--at`:状态实际发生时间,必须是带时区的 ISO 8601 时间;缺省为当前时间。
121
+ - `--source`:`human`、`agent` 或 `import`;缺省为 `human`。
122
+ - `--external-ref`:全局幂等键。重复写入同一项目时返回原记录;若被其他项目占用则报错。
123
+ - `--json`:输出稳定 JSON。
124
+
125
+ ## 批量导入
126
+
127
+ `import` 接受一个 JSON 对象、JSON 数组,或每行一个对象的 JSONL 文件。所有记录先校验,再在同一个 SQLite 事务中写入;任一记录失败时整批回滚。
128
+
129
+ ```json
130
+ {
131
+ "project": "project-mgr",
132
+ "status": "active",
133
+ "summary": "核心功能实现完成",
134
+ "progress": "CLI 与 SQLite 已通过测试",
135
+ "blockers": [],
136
+ "nextSteps": ["接入真实项目"],
137
+ "decisions": ["首版只做本地 CLI"],
138
+ "tags": ["mvp"],
139
+ "occurredAt": "2026-08-03T12:00:00+08:00",
140
+ "source": "human",
141
+ "externalRef": "project-mgr-2026-08-03-core"
142
+ }
143
+ ```
144
+
145
+ ```bash
146
+ pnpm start import statuses.jsonl
147
+ # 等价写法
148
+ pnpm start import --file statuses.jsonl --json
149
+ ```
150
+
151
+ 未知字段、未知 CLI 参数、多余位置参数、非法状态和无时区时间都会被拒绝。
152
+
153
+ ## 查询与导出
154
+
155
+ | 命令 | 用途 | 主要选项 |
156
+ | --------- | -------------------------- | ------------------------------------------------------------------ |
157
+ | `current` | 所有项目或单项目的当前状态 | `--project`、`--json` |
158
+ | `history` | 单项目历史,最新记录在前 | `--project`、`--since`、`--until`、`--limit`、`--offset`、`--json` |
159
+ | `at` | 指定时间点及之前的最后状态 | `--project`、`--at`、`--json` |
160
+ | `export` | 导出全部或单项目记录 | `--project`、`--format json\|jsonl` |
161
+
162
+ 全部六个 action 仍可使用 `status <action>` 的兼容形式,例如 `pmgr status export`。人类可读的多条结果按“项目、状态、发生时间、摘要”输出;为空时输出 `No entries found.`。`add`、`import`、`current`、`history`、`at` 支持 `--json`;`export` 由 `--format` 控制格式。
163
+
164
+ 导出内容不包含数据库内部的 `id` 和 `recordedAt`,可以直接重新导入。记录按时间从旧到新导出,从而在恢复时保留相同时间戳记录的先后关系:
165
+
166
+ ```bash
167
+ pnpm start export --format jsonl > project-statuses.jsonl
168
+
169
+ PMGR_DB_PATH=./data/restored.sqlite \
170
+ pnpm start import project-statuses.jsonl
171
+ ```
172
+
173
+ ## 开发与验证
174
+
175
+ ```bash
176
+ pnpm typecheck
177
+ pnpm lint
178
+ pnpm format-check
179
+ pnpm test
180
+ pnpm coverage
181
+ pnpm build
182
+ ```
183
+
184
+ Statements、branches、functions、lines 的覆盖率门槛均为 100%。详细设计见[架构文档](docs/architecture.md)。
package/dist/cli.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ export declare function run(argv: string[], write?: {
3
+ (buffer: Uint8Array | string, cb?: (err?: Error | null) => void): boolean;
4
+ (str: Uint8Array | string, encoding?: BufferEncoding, cb?: (err?: Error | null) => void): boolean;
5
+ }): void;
package/dist/cli.js ADDED
@@ -0,0 +1,213 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync } from "node:fs";
3
+ import { basename, resolve } from "node:path";
4
+ import { pathToFileURL } from "node:url";
5
+ import { addStatus, at, current, history, importStatuses, openLedger, parseTime, toPortableStatusEntry, ValidationError, } from "./index.js";
6
+ const repeats = new Set(["next", "blocker", "decision", "tag"]);
7
+ function parse(argv) {
8
+ const command = [];
9
+ const flags = Object.create(null);
10
+ for (let index = 0; index < argv.length; index += 1) {
11
+ const token = argv[index];
12
+ if (!token.startsWith("--")) {
13
+ command.push(token);
14
+ continue;
15
+ }
16
+ const key = token.slice(2);
17
+ if (key === "json") {
18
+ if (flags[key] !== undefined)
19
+ throw new ValidationError(`--${key} may only be supplied once`);
20
+ flags[key] = true;
21
+ continue;
22
+ }
23
+ const value = argv[++index];
24
+ if (!value || value.startsWith("--"))
25
+ throw new ValidationError(`--${key} requires a value`);
26
+ if (repeats.has(key))
27
+ flags[key] = [...(flags[key] ?? []), value];
28
+ else if (flags[key] !== undefined)
29
+ throw new ValidationError(`--${key} may only be supplied once`);
30
+ else
31
+ flags[key] = value;
32
+ }
33
+ return { command, flags };
34
+ }
35
+ const allowedFlags = new Map([
36
+ [
37
+ "add",
38
+ new Set([
39
+ "project",
40
+ "status",
41
+ "summary",
42
+ "progress",
43
+ "blocker",
44
+ "next",
45
+ "decision",
46
+ "tag",
47
+ "at",
48
+ "source",
49
+ "external-ref",
50
+ "json",
51
+ ]),
52
+ ],
53
+ ["import", new Set(["file", "json"])],
54
+ ["current", new Set(["project", "json"])],
55
+ [
56
+ "history",
57
+ new Set(["project", "since", "until", "limit", "offset", "json"]),
58
+ ],
59
+ ["at", new Set(["project", "at", "json"])],
60
+ ["export", new Set(["project", "format"])],
61
+ ]);
62
+ function validateInvocation(command, flags) {
63
+ const statusCommand = command[0] === "status";
64
+ const action = command[statusCommand ? 1 : 0];
65
+ const allowed = typeof action === "string" ? allowedFlags.get(action) : undefined;
66
+ if (typeof action !== "string" || !allowed)
67
+ throw new ValidationError("usage: pmgr <add|import|current|history|at|export> (or: pmgr status <action>)");
68
+ const positional = command.slice(statusCommand ? 2 : 1);
69
+ if (action === "import") {
70
+ if (positional.length > 1)
71
+ throw new ValidationError("import accepts at most one file argument");
72
+ if (positional.length === 1 && flags.file !== undefined)
73
+ throw new ValidationError("import file must be positional or --file, not both");
74
+ }
75
+ else if (positional.length) {
76
+ throw new ValidationError(`${action} does not accept positional arguments`);
77
+ }
78
+ for (const key of Object.keys(flags))
79
+ if (!allowed.has(key))
80
+ throw new ValidationError(`unknown flag for ${action}: --${key}`);
81
+ return action;
82
+ }
83
+ function stringFlag(flags, name, required = false) {
84
+ const value = flags[name];
85
+ if (required && typeof value !== "string")
86
+ throw new ValidationError(`--${name} is required`);
87
+ return typeof value === "string" ? value : undefined;
88
+ }
89
+ function listFlag(flags, name) {
90
+ return flags[name] ?? [];
91
+ }
92
+ function numberFlag(flags, name, fallback) {
93
+ const value = stringFlag(flags, name);
94
+ if (!value)
95
+ return fallback;
96
+ const number = Number(value);
97
+ if (!Number.isInteger(number) || number < 0)
98
+ throw new ValidationError(`--${name} must be a non-negative integer`);
99
+ return number;
100
+ }
101
+ function output(value, json, write) {
102
+ if (json)
103
+ write(`${JSON.stringify(value)}\n`);
104
+ else if (Array.isArray(value)) {
105
+ const rows = value
106
+ .map((entry) => `${entry.project}\t${entry.status}\t${entry.occurredAt}\t${entry.summary}`)
107
+ .join("\n");
108
+ write(`${rows || "No entries found."}\n`);
109
+ }
110
+ else
111
+ write(`${JSON.stringify(value, null, 2)}\n`);
112
+ }
113
+ function imported(file) {
114
+ if (!file)
115
+ throw new ValidationError("import file is required");
116
+ const content = readFileSync(file, "utf8").trim();
117
+ if (!content)
118
+ throw new ValidationError(`${basename(file)} is empty`);
119
+ if (file.endsWith(".jsonl"))
120
+ return content.split(/\r?\n/).map((line, index) => {
121
+ try {
122
+ return JSON.parse(line);
123
+ }
124
+ catch {
125
+ throw new ValidationError(`${basename(file)} line ${index + 1} is not JSON`);
126
+ }
127
+ });
128
+ let parsed;
129
+ try {
130
+ parsed = JSON.parse(content);
131
+ }
132
+ catch {
133
+ throw new ValidationError(`${basename(file)} is not JSON`);
134
+ }
135
+ return Array.isArray(parsed) ? parsed : [parsed];
136
+ }
137
+ export function run(argv, write = process.stdout.write.bind(process.stdout)) {
138
+ const { command, flags } = parse(argv);
139
+ const action = validateInvocation(command, flags);
140
+ const db = openLedger();
141
+ const json = flags.json === true;
142
+ try {
143
+ switch (action) {
144
+ case "add":
145
+ output(addStatus(db, {
146
+ project: stringFlag(flags, "project", true),
147
+ status: stringFlag(flags, "status", true),
148
+ summary: stringFlag(flags, "summary", true),
149
+ progress: stringFlag(flags, "progress"),
150
+ blockers: listFlag(flags, "blocker"),
151
+ nextSteps: listFlag(flags, "next"),
152
+ decisions: listFlag(flags, "decision"),
153
+ tags: listFlag(flags, "tag"),
154
+ occurredAt: stringFlag(flags, "at"),
155
+ source: stringFlag(flags, "source"),
156
+ externalRef: stringFlag(flags, "external-ref"),
157
+ }).entry, json, write);
158
+ break;
159
+ case "import":
160
+ output(importStatuses(db, imported(stringFlag(flags, "file") ??
161
+ command[command[0] === "status" ? 2 : 1] ??
162
+ "")).map((result) => result.entry), json, write);
163
+ break;
164
+ case "current":
165
+ output(current(db, stringFlag(flags, "project")), json, write);
166
+ break;
167
+ case "history":
168
+ output(history(db, stringFlag(flags, "project", true), {
169
+ since: stringFlag(flags, "since") &&
170
+ parseTime(stringFlag(flags, "since"), "since"),
171
+ until: stringFlag(flags, "until") &&
172
+ parseTime(stringFlag(flags, "until"), "until"),
173
+ limit: numberFlag(flags, "limit", 100),
174
+ offset: numberFlag(flags, "offset", 0),
175
+ }), json, write);
176
+ break;
177
+ case "at":
178
+ output(at(db, stringFlag(flags, "project", true), parseTime(stringFlag(flags, "at", true), "at")) ?? null, json, write);
179
+ break;
180
+ case "export": {
181
+ const entries = history(db, stringFlag(flags, "project"), {
182
+ limit: -1,
183
+ })
184
+ .reverse()
185
+ .map(toPortableStatusEntry);
186
+ const format = stringFlag(flags, "format") ?? "json";
187
+ if (format === "jsonl")
188
+ write(entries.map((entry) => JSON.stringify(entry)).join("\n") +
189
+ (entries.length ? "\n" : ""));
190
+ else if (format === "json")
191
+ output(entries, true, write);
192
+ else
193
+ throw new ValidationError("--format must be json or jsonl");
194
+ break;
195
+ }
196
+ }
197
+ }
198
+ finally {
199
+ db.close();
200
+ }
201
+ }
202
+ /* c8 ignore start -- process entry glue is covered by the CLI integration tests through run(). */
203
+ if (process.argv[1] &&
204
+ import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
205
+ try {
206
+ run(process.argv.slice(2));
207
+ }
208
+ catch (error) {
209
+ console.error(error instanceof Error ? error.message : String(error));
210
+ process.exitCode = 1;
211
+ }
212
+ }
213
+ /* c8 ignore stop */
package/dist/db.d.ts ADDED
@@ -0,0 +1,20 @@
1
+ import Database from "better-sqlite3";
2
+ import { type Source, type StatusEntry } from "./domain.js";
3
+ export type Ledger = Database.Database;
4
+ export type InsertResult = {
5
+ entry: StatusEntry;
6
+ inserted: boolean;
7
+ };
8
+ export type HistoryOptions = {
9
+ since?: string;
10
+ until?: string;
11
+ limit?: number;
12
+ offset?: number;
13
+ };
14
+ export declare function databasePath(): string;
15
+ export declare function openLedger(path?: string): Ledger;
16
+ export declare function addStatus(db: Ledger, value: unknown, defaultSource?: Source): InsertResult;
17
+ export declare function importStatuses(db: Ledger, values: unknown[]): InsertResult[];
18
+ export declare function current(db: Ledger, project?: string): StatusEntry[];
19
+ export declare function history(db: Ledger, project?: string, options?: HistoryOptions): StatusEntry[];
20
+ export declare function at(db: Ledger, project: string, occurredAt: string): StatusEntry | undefined;