feat: add decision requirement and diary upsert APIs
This commit is contained in:
+111
-17
@@ -4,11 +4,13 @@ import { type UniDeskConfig, repoRoot } from "./config";
|
||||
import { coreInternalFetch } from "./microservices";
|
||||
|
||||
type DecisionRecordType = "meeting" | "decision" | "goal" | "blocker" | "debt" | "experiment";
|
||||
type RequirementRecordType = Exclude<DecisionRecordType, "meeting">;
|
||||
type DecisionRecordLevel = "G0" | "G1" | "G2" | "G3" | "P0" | "P1" | "P2" | "P3" | "none";
|
||||
type DecisionRecordStatus = "active" | "blocked" | "parked" | "done";
|
||||
|
||||
const serviceId = "decision-center";
|
||||
const typeValues = new Set<DecisionRecordType>(["meeting", "decision", "goal", "blocker", "debt", "experiment"]);
|
||||
const requirementTypeValues = new Set<RequirementRecordType>(["decision", "goal", "blocker", "debt", "experiment"]);
|
||||
const levelValues = new Set<DecisionRecordLevel>(["G0", "G1", "G2", "G3", "P0", "P1", "P2", "P3", "none"]);
|
||||
const statusValues = new Set<DecisionRecordStatus>(["active", "blocked", "parked", "done"]);
|
||||
|
||||
@@ -54,6 +56,12 @@ function parseType(raw: string | undefined, fallback: DecisionRecordType): Decis
|
||||
return value as DecisionRecordType;
|
||||
}
|
||||
|
||||
function parseRequirementType(raw: string | undefined, fallback: RequirementRecordType): RequirementRecordType {
|
||||
const value = raw || fallback;
|
||||
if (!requirementTypeValues.has(value as RequirementRecordType)) throw new Error(`--type must be one of: ${Array.from(requirementTypeValues).join(", ")}`);
|
||||
return value as RequirementRecordType;
|
||||
}
|
||||
|
||||
function parseLevel(raw: string | undefined, fallback: DecisionRecordLevel): DecisionRecordLevel {
|
||||
const value = raw || fallback;
|
||||
if (!levelValues.has(value as DecisionRecordLevel)) throw new Error(`--level must be one of: ${Array.from(levelValues).join(", ")}`);
|
||||
@@ -78,6 +86,21 @@ function readMarkdownFile(path: string): { absolutePath: string; markdown: strin
|
||||
return { absolutePath, markdown };
|
||||
}
|
||||
|
||||
function bodyFromArgs(args: string[], command: string): { body: string; bodySource: Record<string, string> } {
|
||||
const body = optionValue(args, ["--body"]);
|
||||
const bodyFile = optionValue(args, ["--body-file", "--markdown-file"]);
|
||||
const markdownFile = optionValue(args, ["--file"]);
|
||||
const sources = [body !== undefined, bodyFile !== undefined, markdownFile !== undefined].filter(Boolean).length;
|
||||
if (sources > 1) throw new Error(`${command} accepts only one of --body, --body-file, or --file`);
|
||||
if (body !== undefined) return { body, bodySource: { kind: "inline" } };
|
||||
const file = bodyFile ?? markdownFile;
|
||||
if (file !== undefined) {
|
||||
const { absolutePath, markdown } = readMarkdownFile(file);
|
||||
return { body: markdown, bodySource: { kind: "file", path: absolutePath } };
|
||||
}
|
||||
throw new Error(`${command} requires --body text or --body-file path`);
|
||||
}
|
||||
|
||||
function decisionProxy(path: string, init?: { method?: string; body?: unknown }): unknown {
|
||||
return coreInternalFetch(`/api/microservices/${encodeURIComponent(serviceId)}/proxy${path}`, init);
|
||||
}
|
||||
@@ -202,35 +225,32 @@ async function importDiaryAsync(args: string[], fetcher: (path: string, init?: {
|
||||
}
|
||||
|
||||
function listRecords(args: string[]): unknown {
|
||||
const params = new URLSearchParams();
|
||||
const type = optionValue(args, ["--type"]);
|
||||
const status = optionValue(args, ["--status"]);
|
||||
const level = optionValue(args, ["--level"]);
|
||||
const linkedGoalId = optionValue(args, ["--linked-goal-id", "--linkedGoalId"]);
|
||||
const limit = optionValue(args, ["--limit"]);
|
||||
if (type !== undefined) params.set("type", parseType(type, "meeting"));
|
||||
if (status !== undefined) params.set("status", parseStatus(status, "active"));
|
||||
if (level !== undefined) params.set("level", parseLevel(level, "none"));
|
||||
if (linkedGoalId !== undefined) params.set("linkedGoalId", linkedGoalId);
|
||||
if (limit !== undefined) params.set("limit", limit);
|
||||
const query = params.toString();
|
||||
const query = recordQuery(args);
|
||||
return unwrapProxyResponse(decisionProxy(`/api/records${query ? `?${query}` : ""}`));
|
||||
}
|
||||
|
||||
async function listRecordsAsync(args: string[], fetcher: (path: string, init?: { method?: string; body?: unknown }) => Promise<unknown>): Promise<unknown> {
|
||||
const query = recordQuery(args);
|
||||
return unwrapProxyResponse(await decisionProxyAsync(fetcher, `/api/records${query ? `?${query}` : ""}`));
|
||||
}
|
||||
|
||||
function recordQuery(args: string[], options: { requirementOnly?: boolean } = {}): string {
|
||||
const params = new URLSearchParams();
|
||||
const type = optionValue(args, ["--type"]);
|
||||
const status = optionValue(args, ["--status"]);
|
||||
const level = optionValue(args, ["--level"]);
|
||||
const linkedGoalId = optionValue(args, ["--linked-goal-id", "--linkedGoalId"]);
|
||||
const tag = optionValue(args, ["--tag", "--tags"]);
|
||||
const queryText = optionValue(args, ["--query", "--q"]);
|
||||
const limit = optionValue(args, ["--limit"]);
|
||||
if (type !== undefined) params.set("type", parseType(type, "meeting"));
|
||||
if (type !== undefined) params.set("type", options.requirementOnly === true ? parseRequirementType(type, "goal") : parseType(type, "meeting"));
|
||||
if (status !== undefined) params.set("status", parseStatus(status, "active"));
|
||||
if (level !== undefined) params.set("level", parseLevel(level, "none"));
|
||||
if (linkedGoalId !== undefined) params.set("linkedGoalId", linkedGoalId);
|
||||
if (tag !== undefined) params.set("tag", tag);
|
||||
if (queryText !== undefined) params.set("q", queryText);
|
||||
if (limit !== undefined) params.set("limit", limit);
|
||||
const query = params.toString();
|
||||
return unwrapProxyResponse(await decisionProxyAsync(fetcher, `/api/records${query ? `?${query}` : ""}`));
|
||||
return params.toString();
|
||||
}
|
||||
|
||||
function diaryQuery(args: string[]): string {
|
||||
@@ -276,6 +296,30 @@ async function showDiaryAsync(key: string | undefined, fetcher: (path: string, i
|
||||
return unwrapProxyResponse(await decisionProxyAsync(fetcher, `/api/diary/entries/${encodeURIComponent(key)}`));
|
||||
}
|
||||
|
||||
function diaryEditPayload(args: string[], command: string): { key: string; payload: Record<string, unknown>; bodySource: Record<string, string> } {
|
||||
const key = positionalArgs(args)[0];
|
||||
if (!key) throw new Error(`${command} requires entry id or YYYY-MM-DD date`);
|
||||
const { body, bodySource } = bodyFromArgs(args, command);
|
||||
const payload: Record<string, unknown> = { body };
|
||||
const title = optionValue(args, ["--title"]);
|
||||
const sourceFile = optionValue(args, ["--source-file", "--source-path", "--source"]);
|
||||
if (title !== undefined) payload.title = title;
|
||||
if (sourceFile !== undefined) payload.sourceFile = sourceFile;
|
||||
const tags = splitList(optionValues(args, ["--tag", "--tags"]));
|
||||
if (tags.length > 0) payload.tags = tags;
|
||||
return { key, payload, bodySource };
|
||||
}
|
||||
|
||||
function editDiary(args: string[]): unknown {
|
||||
const { key, payload, bodySource } = diaryEditPayload(args, "decision diary edit");
|
||||
return { key, bodySource, result: unwrapProxyResponse(decisionProxy(`/api/diary/entries/${encodeURIComponent(key)}`, { method: "PUT", body: payload })) };
|
||||
}
|
||||
|
||||
async function editDiaryAsync(args: string[], fetcher: (path: string, init?: { method?: string; body?: unknown }) => Promise<unknown>): Promise<unknown> {
|
||||
const { key, payload, bodySource } = diaryEditPayload(args, "decision diary edit");
|
||||
return { key, bodySource, result: unwrapProxyResponse(await decisionProxyAsync(fetcher, `/api/diary/entries/${encodeURIComponent(key)}`, { method: "PUT", body: payload })) };
|
||||
}
|
||||
|
||||
function showRecord(id: string | undefined): unknown {
|
||||
if (!id) throw new Error("decision show requires record id");
|
||||
return unwrapProxyResponse(decisionProxy(`/api/records/${encodeURIComponent(id)}`));
|
||||
@@ -286,6 +330,36 @@ async function showRecordAsync(id: string | undefined, fetcher: (path: string, i
|
||||
return unwrapProxyResponse(await decisionProxyAsync(fetcher, `/api/records/${encodeURIComponent(id)}`));
|
||||
}
|
||||
|
||||
function requirementPayload(args: string[], command: string): Record<string, unknown> {
|
||||
const title = optionValue(args, ["--title"]);
|
||||
const bodyArg = optionValue(args, ["--body"]);
|
||||
const bodyFile = optionValue(args, ["--body-file", "--markdown-file", "--file"]);
|
||||
const body = bodyArg !== undefined ? bodyArg : bodyFile !== undefined ? readMarkdownFile(bodyFile).markdown : undefined;
|
||||
if (!title && body === undefined) throw new Error(`${command} requires --title or --body-file/--body`);
|
||||
return {
|
||||
id: optionValue(args, ["--id"]),
|
||||
title,
|
||||
body,
|
||||
type: parseRequirementType(optionValue(args, ["--type"]), "goal"),
|
||||
level: parseLevel(optionValue(args, ["--level"]), "none"),
|
||||
status: parseStatus(optionValue(args, ["--status"]), "active"),
|
||||
linkedGoalId: optionValue(args, ["--linked-goal-id", "--linkedGoalId"]),
|
||||
tags: splitList(optionValues(args, ["--tag", "--tags"])),
|
||||
evidenceLinks: splitList(optionValues(args, ["--evidence", "--evidence-link", "--evidenceLinks"])),
|
||||
sourceSession: optionValue(args, ["--source-session", "--sourceSession"]),
|
||||
taskId: optionValue(args, ["--task-id", "--taskId"]),
|
||||
commitId: optionValue(args, ["--commit-id", "--commitId"]),
|
||||
};
|
||||
}
|
||||
|
||||
function upsertRequirement(args: string[]): unknown {
|
||||
return unwrapProxyResponse(decisionProxy("/api/requirements", { method: "PUT", body: requirementPayload(args, "decision requirement upsert") }));
|
||||
}
|
||||
|
||||
async function upsertRequirementAsync(args: string[], fetcher: (path: string, init?: { method?: string; body?: unknown }) => Promise<unknown>): Promise<unknown> {
|
||||
return unwrapProxyResponse(await decisionProxyAsync(fetcher, "/api/requirements", { method: "PUT", body: requirementPayload(args, "decision requirement upsert") }));
|
||||
}
|
||||
|
||||
export async function runDecisionCenterCommand(_config: UniDeskConfig, args: string[]): Promise<unknown> {
|
||||
const [action = "list", id] = args;
|
||||
if (action === "diary") {
|
||||
@@ -294,7 +368,17 @@ export async function runDecisionCenterCommand(_config: UniDeskConfig, args: str
|
||||
if (diaryAction === "list") return listDiary(args.slice(2));
|
||||
if (diaryAction === "months") return listDiaryMonths();
|
||||
if (diaryAction === "show") return showDiary(diaryId);
|
||||
throw new Error("decision diary command must be one of: import, list, months, show");
|
||||
if (diaryAction === "edit" || diaryAction === "upsert") return editDiary(args.slice(2));
|
||||
throw new Error("decision diary command must be one of: import, list, months, show, edit, upsert");
|
||||
}
|
||||
if (action === "requirement" || action === "requirements") {
|
||||
const [requirementAction = "list"] = args.slice(1);
|
||||
if (requirementAction === "list") {
|
||||
const query = recordQuery(args.slice(2), { requirementOnly: true });
|
||||
return unwrapProxyResponse(decisionProxy(`/api/requirements${query ? `?${query}` : ""}`));
|
||||
}
|
||||
if (requirementAction === "upsert") return upsertRequirement(args.slice(2));
|
||||
throw new Error("decision requirement command must be one of: list, upsert");
|
||||
}
|
||||
if (action === "upload") return uploadMeeting(args.slice(1));
|
||||
if (action === "list") return listRecords(args.slice(1));
|
||||
@@ -315,7 +399,17 @@ export async function runDecisionCenterCommandAsync(
|
||||
if (diaryAction === "list") return listDiaryAsync(args.slice(2), fetcher);
|
||||
if (diaryAction === "months") return listDiaryMonthsAsync(fetcher);
|
||||
if (diaryAction === "show") return showDiaryAsync(diaryId, fetcher);
|
||||
throw new Error("decision diary command must be one of: import, list, months, show");
|
||||
if (diaryAction === "edit" || diaryAction === "upsert") return editDiaryAsync(args.slice(2), fetcher);
|
||||
throw new Error("decision diary command must be one of: import, list, months, show, edit, upsert");
|
||||
}
|
||||
if (action === "requirement" || action === "requirements") {
|
||||
const [requirementAction = "list"] = args.slice(1);
|
||||
if (requirementAction === "list") {
|
||||
const query = recordQuery(args.slice(2), { requirementOnly: true });
|
||||
return unwrapProxyResponse(await decisionProxyAsync(fetcher, `/api/requirements${query ? `?${query}` : ""}`));
|
||||
}
|
||||
if (requirementAction === "upsert") return upsertRequirementAsync(args.slice(2), fetcher);
|
||||
throw new Error("decision requirement command must be one of: list, upsert");
|
||||
}
|
||||
if (action === "upload") return uploadMeetingAsync(args.slice(1), fetcher);
|
||||
if (action === "list") return listRecordsAsync(args.slice(1), fetcher);
|
||||
|
||||
Reference in New Issue
Block a user