@danypops/papyrus 0.11.1 → 0.11.3
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/package.json +1 -1
- package/src/cli.ts +100 -12
- package/src/constants.ts +16 -0
- package/src/ops.ts +8 -3
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -82,7 +82,10 @@ const USAGE = `Usage:
|
|
|
82
82
|
papyrus tasks reject <id> [--json]
|
|
83
83
|
papyrus tasks retry <id> [--json]
|
|
84
84
|
papyrus tasks cancel <id> [--json]
|
|
85
|
-
papyrus tasks depend <id> <prerequisite-id> [--json]
|
|
85
|
+
papyrus tasks depend <id> <prerequisite-id> [--json]
|
|
86
|
+
papyrus tasks create --title <title> [--body <body>] [--status <status>] [--labels-json <json>] [--extra-json <json>] [--gates-json <json>] [--checklist-json <json>] [--template-id <id>] [--parent-id <id>] [--depends-on-json <json>] [--json]
|
|
87
|
+
papyrus tasks list [--status <status>] [--text <query>] [--limit <count>] [--json]
|
|
88
|
+
papyrus tasks show <id> [--json]`;
|
|
86
89
|
|
|
87
90
|
function usage(): never {
|
|
88
91
|
console.error(USAGE);
|
|
@@ -98,6 +101,20 @@ type CliCompletion = Omit<TaskCompletion, "artifact" | "blocked"> & {
|
|
|
98
101
|
gates: GateResult[];
|
|
99
102
|
};
|
|
100
103
|
|
|
104
|
+
function parseJsonObjectFlag(value: string | undefined, flag: string): Record<string, unknown> {
|
|
105
|
+
if (value === undefined) throw new Error(`${flag} requires a value`);
|
|
106
|
+
const parsed = JSON.parse(value) as unknown;
|
|
107
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error(`${flag} must be a JSON object`);
|
|
108
|
+
return parsed as Record<string, unknown>;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function parseJsonStringArrayFlag(value: string | undefined, flag: string): string[] {
|
|
112
|
+
if (value === undefined) throw new Error(`${flag} requires a value`);
|
|
113
|
+
const parsed = JSON.parse(value) as unknown;
|
|
114
|
+
if (!Array.isArray(parsed) || parsed.some((entry) => typeof entry !== "string")) throw new Error(`${flag} must be a JSON string array`);
|
|
115
|
+
return parsed as string[];
|
|
116
|
+
}
|
|
117
|
+
|
|
101
118
|
function artifactLabel(artifact: CliArtifact): string {
|
|
102
119
|
return `${artifact.id} ${artifact.title}`;
|
|
103
120
|
}
|
|
@@ -237,24 +254,48 @@ export async function runNoteCli(args: string[], client: TaskCliClient, projectR
|
|
|
237
254
|
export async function runTaskCli(args: string[], client: TaskCliClient, projectRoot: string = process.cwd()): Promise<string> {
|
|
238
255
|
const json = args.includes("--json");
|
|
239
256
|
const positional: string[] = [];
|
|
240
|
-
const updateInput: { title?: string; body?: string; labels?: string[]; status?: "todo" } = {};
|
|
241
257
|
let reason: string | undefined;
|
|
258
|
+
let title: string | undefined;
|
|
259
|
+
let body: string | undefined;
|
|
260
|
+
let labels: string[] | undefined;
|
|
261
|
+
// Deliberately unrestricted here -- tasks update alone restricts this to "todo" (accidental-
|
|
262
|
+
// creation recovery only), enforced in that case body, not in parsing shared by every action.
|
|
263
|
+
let status: string | undefined;
|
|
264
|
+
let extra: Record<string, unknown> | undefined;
|
|
265
|
+
let gates: unknown[] | undefined;
|
|
266
|
+
let checklist: Record<string, unknown> | undefined;
|
|
267
|
+
let templateId: string | undefined;
|
|
268
|
+
let parentId: string | undefined;
|
|
269
|
+
let dependsOn: string[] | undefined;
|
|
270
|
+
let text: string | undefined;
|
|
271
|
+
let limit: number | undefined;
|
|
242
272
|
for (let index = 0; index < args.length; index++) {
|
|
243
273
|
const argument = args[index]!;
|
|
244
274
|
if (argument === "--json") continue;
|
|
245
|
-
if (argument === "--title" || argument === "--body" || argument === "--labels-json" || argument === "--status" || argument === "--reason"
|
|
275
|
+
if (argument === "--title" || argument === "--body" || argument === "--labels-json" || argument === "--status" || argument === "--reason"
|
|
276
|
+
|| argument === "--extra-json" || argument === "--gates-json" || argument === "--checklist-json" || argument === "--template-id"
|
|
277
|
+
|| argument === "--parent-id" || argument === "--depends-on-json" || argument === "--text" || argument === "--limit") {
|
|
246
278
|
const value = args[++index];
|
|
247
279
|
if (value === undefined) throw new Error(`${argument} requires a value`);
|
|
248
|
-
if (argument === "--title")
|
|
249
|
-
else if (argument === "--body")
|
|
280
|
+
if (argument === "--title") title = value;
|
|
281
|
+
else if (argument === "--body") body = value;
|
|
250
282
|
else if (argument === "--reason") reason = value;
|
|
251
|
-
else if (argument === "--status")
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
283
|
+
else if (argument === "--status") status = value;
|
|
284
|
+
else if (argument === "--extra-json") extra = parseJsonObjectFlag(value, "--extra-json");
|
|
285
|
+
else if (argument === "--checklist-json") checklist = parseJsonObjectFlag(value, "--checklist-json");
|
|
286
|
+
else if (argument === "--template-id") templateId = value;
|
|
287
|
+
else if (argument === "--parent-id") parentId = value;
|
|
288
|
+
else if (argument === "--depends-on-json") dependsOn = parseJsonStringArrayFlag(value, "--depends-on-json");
|
|
289
|
+
else if (argument === "--text") text = value;
|
|
290
|
+
else if (argument === "--limit") {
|
|
291
|
+
if (Number.isNaN(Number(value))) throw new Error("--limit requires a numeric value");
|
|
292
|
+
limit = Number(value);
|
|
293
|
+
} else if (argument === "--gates-json") {
|
|
255
294
|
const parsed = JSON.parse(value) as unknown;
|
|
256
|
-
if (!Array.isArray(parsed)
|
|
257
|
-
|
|
295
|
+
if (!Array.isArray(parsed)) throw new Error("--gates-json must be a JSON array");
|
|
296
|
+
gates = parsed;
|
|
297
|
+
} else {
|
|
298
|
+
labels = parseJsonStringArrayFlag(value, "--labels-json");
|
|
258
299
|
}
|
|
259
300
|
continue;
|
|
260
301
|
}
|
|
@@ -297,6 +338,14 @@ export async function runTaskCli(args: string[], client: TaskCliClient, projectR
|
|
|
297
338
|
}
|
|
298
339
|
case "update": {
|
|
299
340
|
if (!id || dependencyId) throw new Error("tasks update requires exactly one task id");
|
|
341
|
+
const updateInput: { title?: string; body?: string; labels?: string[]; status?: "todo" } = {};
|
|
342
|
+
if (title !== undefined) updateInput.title = title;
|
|
343
|
+
if (body !== undefined) updateInput.body = body;
|
|
344
|
+
if (labels !== undefined) updateInput.labels = labels;
|
|
345
|
+
if (status !== undefined) {
|
|
346
|
+
if (status !== "todo") throw new Error("--status only supports todo for accidental creation recovery");
|
|
347
|
+
updateInput.status = status;
|
|
348
|
+
}
|
|
300
349
|
if (Object.keys(updateInput).length === 0) throw new Error("tasks update requires --title, --body, --labels-json, or --status todo");
|
|
301
350
|
if (updateInput.status !== undefined && !reason?.trim()) throw new Error("tasks update --status requires --reason");
|
|
302
351
|
if (reason !== undefined && updateInput.status === undefined) throw new Error("tasks update --reason requires --status todo");
|
|
@@ -307,6 +356,45 @@ export async function runTaskCli(args: string[], client: TaskCliClient, projectR
|
|
|
307
356
|
human = `Updated: ${artifactLabel(artifact)}`;
|
|
308
357
|
break;
|
|
309
358
|
}
|
|
359
|
+
case "create": {
|
|
360
|
+
if (id) throw new Error("tasks create accepts no positional arguments");
|
|
361
|
+
if (!title) throw new Error("tasks create requires --title");
|
|
362
|
+
const artifact = await client.call<Record<string, unknown>, CliArtifact>("tasks.create", {
|
|
363
|
+
title,
|
|
364
|
+
...(body !== undefined ? { body } : {}),
|
|
365
|
+
...(status !== undefined ? { status } : {}),
|
|
366
|
+
...(labels !== undefined ? { labels } : {}),
|
|
367
|
+
...(extra !== undefined ? { extra } : {}),
|
|
368
|
+
...(gates !== undefined ? { gates } : {}),
|
|
369
|
+
...(checklist !== undefined ? { checklist } : {}),
|
|
370
|
+
...(templateId !== undefined ? { template_id: templateId } : {}),
|
|
371
|
+
...(parentId !== undefined ? { parent_id: parentId } : {}),
|
|
372
|
+
...(dependsOn !== undefined ? { depends_on: dependsOn } : {}),
|
|
373
|
+
project_root: projectRoot, actor: "user", source: "cli",
|
|
374
|
+
});
|
|
375
|
+
result = artifact;
|
|
376
|
+
human = `Created task: ${artifactLabel(artifact)}`;
|
|
377
|
+
break;
|
|
378
|
+
}
|
|
379
|
+
case "list": {
|
|
380
|
+
if (id) throw new Error("tasks list accepts no positional arguments");
|
|
381
|
+
const rows = await client.call<Record<string, unknown>, CliArtifact[]>("tasks.list", {
|
|
382
|
+
...(status !== undefined ? { status } : {}),
|
|
383
|
+
...(text !== undefined ? { text } : {}),
|
|
384
|
+
...(limit !== undefined ? { limit } : {}),
|
|
385
|
+
project_root: projectRoot,
|
|
386
|
+
});
|
|
387
|
+
result = rows;
|
|
388
|
+
human = rows.length === 0 ? "No tasks found." : rows.map((row) => artifactLabel(row)).join("\n");
|
|
389
|
+
break;
|
|
390
|
+
}
|
|
391
|
+
case "show": {
|
|
392
|
+
if (!id || dependencyId) throw new Error("tasks show requires exactly one task id");
|
|
393
|
+
const artifact = await client.call<Record<string, unknown>, CliArtifact>("tasks.show", { id });
|
|
394
|
+
result = artifact;
|
|
395
|
+
human = `${artifactLabel(artifact)}\n\n${artifact.body ?? ""}`;
|
|
396
|
+
break;
|
|
397
|
+
}
|
|
310
398
|
case "history": {
|
|
311
399
|
if (!id || dependencyId) throw new Error("tasks history requires exactly one task id");
|
|
312
400
|
const page = await client.call<{ id: string; direction: "desc" }, import("./domain/task-event.ts").TaskHistoryPage>("tasks.history", { id, direction: "desc" });
|
|
@@ -415,7 +503,7 @@ export async function runTaskCli(args: string[], client: TaskCliClient, projectR
|
|
|
415
503
|
break;
|
|
416
504
|
}
|
|
417
505
|
default:
|
|
418
|
-
throw new Error("tasks action must be active, focused, focus, pause, unpause, clear-focus, update, graph, plan, history, scope, assign-project, complete, start, submit, reject, retry, cancel, or depend");
|
|
506
|
+
throw new Error("tasks action must be create, list, show, active, focused, focus, pause, unpause, clear-focus, update, graph, plan, history, scope, assign-project, complete, start, submit, reject, retry, cancel, or depend");
|
|
419
507
|
}
|
|
420
508
|
return json ? JSON.stringify(result) : human;
|
|
421
509
|
}
|
package/src/constants.ts
CHANGED
|
@@ -142,6 +142,22 @@ export const SEED_STATUSES = [
|
|
|
142
142
|
{ name: "deprecated", kind: "skill" },
|
|
143
143
|
] as const;
|
|
144
144
|
|
|
145
|
+
/**
|
|
146
|
+
* The initial status a newly created artifact of a kind gets when no caller-supplied
|
|
147
|
+
* status is given. This must be an explicit, named mapping — never derived from row order
|
|
148
|
+
* in the `statuses` table (SEED_STATUSES' listed order, or a migration's insertion order,
|
|
149
|
+
* is not a semantic guarantee; a migrated database can freely have a different physical
|
|
150
|
+
* row order for the same logical status set). Deriving "the default" from "whichever row
|
|
151
|
+
* happens to be first by rowid" was the root cause of a real production defect where
|
|
152
|
+
* migrated databases created new Tasks as done instead of todo.
|
|
153
|
+
*/
|
|
154
|
+
export const DEFAULT_STATUS_BY_KIND: Readonly<Record<string, string>> = {
|
|
155
|
+
doc: "draft",
|
|
156
|
+
task: "todo",
|
|
157
|
+
rule: "active",
|
|
158
|
+
skill: "active",
|
|
159
|
+
};
|
|
160
|
+
|
|
145
161
|
/**
|
|
146
162
|
* Universal relation names — any kind can link to any kind.
|
|
147
163
|
*
|
package/src/ops.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { createRequire } from "node:module";
|
|
|
6
6
|
import { exec } from "node:child_process";
|
|
7
7
|
import type { Db } from "./db.ts";
|
|
8
8
|
import { inTransaction } from "./db.ts";
|
|
9
|
+
import { DEFAULT_STATUS_BY_KIND } from "./constants.ts";
|
|
9
10
|
import type { Artifact, ArtifactQuery, CreateArtifactInput, UpdateArtifactInput } from "./domain/artifact.ts";
|
|
10
11
|
import type { Gate, GateResult, GateRunOptions } from "./domain/gate.ts";
|
|
11
12
|
export type { Artifact } from "./domain/artifact.ts";
|
|
@@ -102,9 +103,13 @@ function slugify(s: string): string {
|
|
|
102
103
|
}
|
|
103
104
|
|
|
104
105
|
function defaultStatusFor(db: Db, kind: string): string {
|
|
105
|
-
//
|
|
106
|
-
|
|
107
|
-
|
|
106
|
+
// Explicit per-kind mapping, never row order -- see DEFAULT_STATUS_BY_KIND's doc comment
|
|
107
|
+
// for the production defect this replaced (row order is not a semantic guarantee).
|
|
108
|
+
const candidate = DEFAULT_STATUS_BY_KIND[kind];
|
|
109
|
+
if (candidate === undefined) throw new Error(`no default status is configured for kind "${kind}"`);
|
|
110
|
+
const exists = db.prepare("SELECT 1 FROM statuses WHERE kind = ? AND name = ?").get(kind, candidate);
|
|
111
|
+
if (!exists) throw new Error(`configured default status "${candidate}" for kind "${kind}" is not a registered status`);
|
|
112
|
+
return candidate;
|
|
108
113
|
}
|
|
109
114
|
|
|
110
115
|
function rowToArtifact(row: Record<string, unknown>): Artifact {
|