@dreki-gg/taskman 0.2.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.
@@ -0,0 +1,13 @@
1
+ //#region \0rolldown/runtime.js
2
+ var __defProp = Object.defineProperty;
3
+ var __exportAll = (all, no_symbols) => {
4
+ let target = {};
5
+ for (var name in all) __defProp(target, name, {
6
+ get: all[name],
7
+ enumerable: true
8
+ });
9
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
10
+ return target;
11
+ };
12
+ //#endregion
13
+ export { __exportAll as t };
package/dist/cli.d.mts ADDED
@@ -0,0 +1,7 @@
1
+ import { Command } from "commander";
2
+
3
+ //#region src/cli.d.ts
4
+ declare function buildProgram(): Command;
5
+ declare function main(argv?: string[]): Promise<void>;
6
+ //#endregion
7
+ export { buildProgram, main };
package/dist/cli.mjs ADDED
@@ -0,0 +1,287 @@
1
+ #!/usr/bin/env node
2
+ import { N as readInitiativesManifest, P as upsertInitiativeEntry, S as initiativeRollup, T as reconcileInitiativeForPlan, V as upsertPlanEntry, X as makePlanRuntime, _ as applyInitiativeReconcile, a as filterPlans, b as collectPlanDrift, d as setTaskStatus, g as resolvePlanByName, i as loadInitiativeListItems, l as sortPlans, m as loadPlanData, n as formatInitiativeList, o as formatPlanList, s as loadPlanListItems, t as filterInitiatives, u as appendDeferredTask, v as applyReconcile, y as collectInitiativeDrift, z as readPlansManifest } from "./initiatives-Ij_teFl_.mjs";
3
+ import { Effect } from "effect";
4
+ import { Command } from "commander";
5
+ //#region src/cli/runtime.ts
6
+ /**
7
+ * Shared CLI plumbing: a single `runPlanIO` bridge plus stateless plan
8
+ * resolution that exits with a clear message when no single plan can be picked.
9
+ */
10
+ const runPlanIO = makePlanRuntime();
11
+ var CliError = class extends Error {};
12
+ /**
13
+ * Resolve a target plan directory from an optional `--plan` hint, else the sole
14
+ * in-progress plan. Throws `CliError` (caught at the top level → exit 1) with
15
+ * the in-progress candidates when resolution is ambiguous or misses.
16
+ */
17
+ async function resolvePlanDir(name) {
18
+ const { planName, planDir, candidates } = await runPlanIO(resolvePlanByName({ name }));
19
+ if (planName && planDir) return {
20
+ planName,
21
+ planDir
22
+ };
23
+ if (name) throw new CliError(`Plan "${name}" not found. In-progress plans: ${candidates.join(", ") || "(none)"}.`);
24
+ if (candidates.length > 1) throw new CliError(`Multiple in-progress plans — pass --plan <name>. Candidates: ${candidates.join(", ")}.`);
25
+ throw new CliError("No in-progress plan found in .plans/plans.jsonl. Pass --plan <name>.");
26
+ }
27
+ //#endregion
28
+ //#region src/cli/format.ts
29
+ const STATUS_GLYPH = {
30
+ done: "✓",
31
+ skipped: "⊘",
32
+ blocked: "✗",
33
+ pending: "○",
34
+ deferred: "+"
35
+ };
36
+ /** Print either pretty JSON (when `json`) or the supplied human text. */
37
+ function emit(json, payload, human) {
38
+ if (json) process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
39
+ else process.stdout.write(human + "\n");
40
+ }
41
+ //#endregion
42
+ //#region src/cli/commands/status.ts
43
+ /**
44
+ * `taskman status` — progress + task ids/statuses for the resolved plan.
45
+ */
46
+ async function statusCommand(opts) {
47
+ const { planDir } = await resolvePlanDir(opts.plan);
48
+ const plan = await runPlanIO(loadPlanData(planDir));
49
+ if (!plan) throw new CliError(`No tasks.jsonl found in ${planDir}.`);
50
+ const counts = {
51
+ done: 0,
52
+ skipped: 0,
53
+ blocked: 0,
54
+ pending: 0,
55
+ deferred: 0
56
+ };
57
+ for (const task of plan.tasks) counts[task.status] += 1;
58
+ const resolved = counts.done + counts.skipped;
59
+ const parts = [
60
+ `done ${counts.done}`,
61
+ `skipped ${counts.skipped}`,
62
+ `pending ${counts.pending}`
63
+ ];
64
+ if (counts.blocked) parts.push(`blocked ${counts.blocked}`);
65
+ if (counts.deferred) parts.push(`follow-up ${counts.deferred}`);
66
+ const lines = plan.tasks.map((t) => ` ${STATUS_GLYPH[t.status]} ${t.id} [${t.status}] ${t.description}`);
67
+ const human = `Plan: ${plan.title} (${plan.planName})\nProgress: ${resolved}/${plan.tasks.length} resolved — ${parts.join(", ")}\nTasks:\n${lines.join("\n")}`;
68
+ emit(Boolean(opts.json), {
69
+ active: true,
70
+ plan_name: plan.planName,
71
+ title: plan.title,
72
+ total: plan.tasks.length,
73
+ counts,
74
+ task_ids: plan.tasks.map((t) => t.id)
75
+ }, human);
76
+ }
77
+ //#endregion
78
+ //#region src/cli/commands/list.ts
79
+ /**
80
+ * `taskman list` (plans) and `taskman initiatives`.
81
+ */
82
+ const PLAN_FILTERS = [
83
+ "all",
84
+ "in-progress",
85
+ "done",
86
+ "superseded",
87
+ "abandoned"
88
+ ];
89
+ const SORTS = [
90
+ "name",
91
+ "date-asc",
92
+ "date-desc",
93
+ "tasks"
94
+ ];
95
+ async function listPlansCommand(opts) {
96
+ const filter = PLAN_FILTERS.includes(opts.status ?? "") ? opts.status : "all";
97
+ const sort = SORTS.includes(opts.sort) ? opts.sort : "date-desc";
98
+ const result = sortPlans(filterPlans(await runPlanIO(loadPlanListItems()), filter), sort);
99
+ emit(Boolean(opts.json), result, formatPlanList(result, filter, sort));
100
+ }
101
+ async function listInitiativesCommand(opts) {
102
+ const filter = PLAN_FILTERS.includes(opts.status ?? "") ? opts.status : "all";
103
+ const result = filterInitiatives(await runPlanIO(loadInitiativeListItems()), filter);
104
+ emit(Boolean(opts.json), result, formatInitiativeList(result, filter));
105
+ }
106
+ //#endregion
107
+ //#region src/cli/commands/initiative-status.ts
108
+ /**
109
+ * `taskman initiative-status [name]` — member plans + readiness rollup.
110
+ */
111
+ async function initiativeStatusCommand(name, opts) {
112
+ const initiatives = await runPlanIO(readInitiativesManifest());
113
+ if (initiatives.length === 0) throw new CliError("No initiatives in .plans/initiatives.jsonl.");
114
+ let target = name;
115
+ if (!target) {
116
+ const inProgress = initiatives.filter((i) => i.status === "in-progress");
117
+ if (inProgress.length === 1) target = inProgress[0].name;
118
+ else throw new CliError(`Pass an initiative name. Initiatives: ${initiatives.map((i) => i.name).join(", ")}.`);
119
+ }
120
+ const entry = initiatives.find((i) => i.name === target);
121
+ if (!entry) throw new CliError(`Initiative "${target}" not found. Available: ${initiatives.map((i) => i.name).join(", ")}.`);
122
+ const plans = await runPlanIO(readPlansManifest());
123
+ const rollup = initiativeRollup(entry.name, plans);
124
+ const memberLines = rollup.members.map((m) => {
125
+ const flag = m.status === "in-progress" ? m.ready ? " [ready]" : ` [blocked by ${m.blockedBy?.join(", ")}]` : "";
126
+ return ` ${m.status === "done" ? "✓" : "○"} ${m.name} [${m.status}] — ${m.title}${flag}`;
127
+ });
128
+ const human = `Initiative: ${entry.title} (${entry.name}) — ${entry.status}\nPlans: ${rollup.done}/${rollup.total} done — in-progress ${rollup.inProgress} (ready ${rollup.ready}, blocked ${rollup.blocked})\nMembers:\n${memberLines.join("\n")}`;
129
+ emit(Boolean(opts.json), {
130
+ ...rollup,
131
+ name: entry.name,
132
+ status: entry.status
133
+ }, human);
134
+ }
135
+ //#endregion
136
+ //#region src/cli/commands/update-task.ts
137
+ /**
138
+ * `taskman update-task <id> <status>` — set a task status + reconcile registry.
139
+ */
140
+ const VALID$1 = [
141
+ "done",
142
+ "skipped",
143
+ "blocked",
144
+ "pending"
145
+ ];
146
+ async function updateTaskCommand(taskId, status, opts) {
147
+ if (!VALID$1.includes(status)) throw new CliError(`Invalid status "${status}". Use one of: ${VALID$1.join(", ")}.`);
148
+ const { planName, planDir } = await resolvePlanDir(opts.plan);
149
+ const result = await runPlanIO(setTaskStatus(planDir, taskId, status, opts.notes));
150
+ emit(Boolean(opts.json), {
151
+ plan_name: planName,
152
+ task_id: result.task.id,
153
+ status: result.task.status,
154
+ finalizable: result.finalizable
155
+ }, `${result.task.id} → ${result.task.status} in ${planName}` + (result.finalizable ? " (plan now finalizable)" : ""));
156
+ }
157
+ //#endregion
158
+ //#region src/cli/commands/add-task.ts
159
+ /**
160
+ * `taskman add-task <description>` — append a deferred follow-up task.
161
+ */
162
+ async function addTaskCommand(description, opts) {
163
+ if (!opts.reason) throw new CliError("--reason is required (why the follow-up matters).");
164
+ const { planName, planDir } = await resolvePlanDir(opts.plan);
165
+ const task = await runPlanIO(appendDeferredTask(planDir, {
166
+ description,
167
+ reason: opts.reason,
168
+ details: opts.details
169
+ }));
170
+ emit(Boolean(opts.json), {
171
+ plan_name: planName,
172
+ task_id: task.id,
173
+ description: task.description,
174
+ status: task.status
175
+ }, `Captured follow-up ${task.id}: ${task.description} (deferred) in ${planName}.`);
176
+ }
177
+ //#endregion
178
+ //#region src/cli/commands/reconcile.ts
179
+ /**
180
+ * `taskman reconcile [--apply]` — detect (and optionally repair) status drift.
181
+ */
182
+ async function reconcileCommand(opts) {
183
+ const planRows = await runPlanIO(collectPlanDrift());
184
+ const initRows = await runPlanIO(collectInitiativeDrift());
185
+ let repairedPlans = [];
186
+ let repairedInits = [];
187
+ if (opts.apply) {
188
+ repairedPlans = await runPlanIO(applyReconcile(planRows).pipe(Effect.orDie));
189
+ repairedInits = await runPlanIO(applyInitiativeReconcile(initRows).pipe(Effect.orDie));
190
+ }
191
+ const planDrift = planRows.filter((r) => r.drift);
192
+ const initDrift = initRows.filter((r) => r.drift);
193
+ const lines = [];
194
+ if (planDrift.length === 0 && initDrift.length === 0) lines.push("No drift detected.");
195
+ else {
196
+ for (const r of planDrift) lines.push(` plan ${r.name}: ${r.drift}` + (r.drift === "status" ? ` (${r.registryStatus} → ${r.derivedStatus}, ${r.direction})` : ""));
197
+ for (const r of initDrift) lines.push(` initiative ${r.name}: status (${r.registryStatus} → ${r.derivedStatus})`);
198
+ }
199
+ if (opts.apply) lines.push(`Applied: ${repairedPlans.length} plan(s), ${repairedInits.length} initiative(s) repaired.`);
200
+ else if (planDrift.length || initDrift.length) lines.push("Run with --apply to repair safe (upgrade) drift.");
201
+ emit(Boolean(opts.json), {
202
+ plan_drift: planDrift,
203
+ initiative_drift: initDrift,
204
+ applied: opts.apply ? {
205
+ plans: repairedPlans.map((r) => r.name),
206
+ initiatives: repairedInits.map((r) => r.name)
207
+ } : null
208
+ }, lines.join("\n"));
209
+ }
210
+ //#endregion
211
+ //#region src/cli/commands/close.ts
212
+ /**
213
+ * `taskman close <status>` and `taskman close-initiative <status> [name]`.
214
+ *
215
+ * Sets a plan/initiative lifecycle status directly in the registry. Closing a
216
+ * plan re-projects its parent initiative.
217
+ */
218
+ const VALID = [
219
+ "done",
220
+ "superseded",
221
+ "abandoned",
222
+ "in-progress"
223
+ ];
224
+ function assertStatus(status) {
225
+ if (!VALID.includes(status)) throw new CliError(`Invalid status "${status}". Use one of: ${VALID.join(", ")}.`);
226
+ return status;
227
+ }
228
+ async function closePlanCommand(status, opts) {
229
+ const s = assertStatus(status);
230
+ const { planName } = await resolvePlanDir(opts.plan);
231
+ await runPlanIO(upsertPlanEntry(planName, {
232
+ status: s,
233
+ reason: opts.reason
234
+ }).pipe(Effect.andThen(reconcileInitiativeForPlan(planName))));
235
+ emit(Boolean(opts.json), {
236
+ plan_name: planName,
237
+ status: s,
238
+ reason: opts.reason ?? null
239
+ }, `Plan ${planName} → ${s}${opts.reason ? ` (${opts.reason})` : ""}.`);
240
+ }
241
+ async function closeInitiativeCommand(status, name, opts) {
242
+ const s = assertStatus(status);
243
+ if (!name) throw new CliError("Initiative name is required.");
244
+ await runPlanIO(upsertInitiativeEntry(name, {
245
+ status: s,
246
+ reason: opts.reason
247
+ }));
248
+ emit(Boolean(opts.json), {
249
+ initiative: name,
250
+ status: s,
251
+ reason: opts.reason ?? null
252
+ }, `Initiative ${name} → ${s}${opts.reason ? ` (${opts.reason})` : ""}.`);
253
+ }
254
+ //#endregion
255
+ //#region src/cli.ts
256
+ /**
257
+ * `taskman` CLI — drive the `.plans/` task ledger from any Node harness.
258
+ *
259
+ * Thin Commander wiring over the engine: each subcommand delegates to an action
260
+ * module under `cli/commands/`. Human text by default; `--json` for machines.
261
+ */
262
+ function buildProgram() {
263
+ const program = new Command();
264
+ program.name("taskman").description("Task-management engine over a .plans/ JSONL ledger").version("0.1.0");
265
+ program.command("status").description("Progress + task ids/statuses for the active plan").option("--plan <name>", "plan name (or .plans/<name>) to inspect").option("--json", "machine-readable JSON output").action((opts) => statusCommand(opts));
266
+ program.command("list").description("List plans").option("--status <status>", "all|in-progress|done|superseded|abandoned").option("--sort <field>", "name|date-asc|date-desc|tasks").option("--json", "machine-readable JSON output").action((opts) => listPlansCommand(opts));
267
+ program.command("initiatives").description("List initiatives").option("--status <status>", "all|in-progress|done|superseded|abandoned").option("--json", "machine-readable JSON output").action((opts) => listInitiativesCommand(opts));
268
+ program.command("initiative-status").description("Member plans + readiness for an initiative").argument("[name]", "initiative name (defaults to the sole in-progress one)").option("--json", "machine-readable JSON output").action((name, opts) => initiativeStatusCommand(name, opts));
269
+ program.command("update-task").description("Set a task status (done|skipped|blocked|pending)").argument("<id>", "task id, e.g. t-001").argument("<status>", "done|skipped|blocked|pending").option("--plan <name>", "plan to target").option("--notes <text>", "notes recorded on the task").option("--json", "machine-readable JSON output").action((id, status, opts) => updateTaskCommand(id, status, opts));
270
+ program.command("add-task").description("Append a deferred follow-up task").argument("<description>", "short task label").requiredOption("--reason <text>", "why this follow-up matters").option("--plan <name>", "plan to target").option("--details <text>", "fuller implementation notes").option("--json", "machine-readable JSON output").action((description, opts) => addTaskCommand(description, opts));
271
+ program.command("reconcile").description("Detect (and with --apply, repair) status drift").option("--apply", "repair safe in-progress→done drift").option("--json", "machine-readable JSON output").action((opts) => reconcileCommand(opts));
272
+ program.command("close").description("Set a plan lifecycle status").argument("<status>", "done|superseded|abandoned|in-progress").option("--plan <name>", "plan to target").option("--reason <text>", "why (recorded in the registry)").option("--json", "machine-readable JSON output").action((status, opts) => closePlanCommand(status, opts));
273
+ program.command("close-initiative").description("Set an initiative lifecycle status").argument("<status>", "done|superseded|abandoned|in-progress").argument("<name>", "initiative name").option("--reason <text>", "why (recorded in the registry)").option("--json", "machine-readable JSON output").action((status, name, opts) => closeInitiativeCommand(status, name, opts));
274
+ return program;
275
+ }
276
+ async function main(argv = process.argv) {
277
+ try {
278
+ await buildProgram().parseAsync(argv);
279
+ } catch (err) {
280
+ const message = err instanceof CliError ? err.message : err instanceof Error ? err.message : String(err);
281
+ process.stderr.write(`taskman: ${message}\n`);
282
+ process.exitCode = 1;
283
+ }
284
+ }
285
+ main();
286
+ //#endregion
287
+ export { buildProgram, main };