@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.
- package/dist/chunk-CfYAbeIz.mjs +13 -0
- package/dist/cli.d.mts +7 -0
- package/dist/cli.mjs +287 -0
- package/dist/index.d.mts +682 -0
- package/dist/index.mjs +2 -0
- package/dist/initiatives-Ij_teFl_.mjs +1253 -0
- package/package.json +51 -0
|
@@ -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
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 };
|